Now delevering the raw file when transcoding is not enabled (no more remux done via fmmpeg). Fixes #28, Ref #36
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
var LMS = LMS || {};
|
||||
|
||||
const Mode = {
|
||||
Transcode: 1,
|
||||
File: 2,
|
||||
}
|
||||
Object.freeze(Mode);
|
||||
|
||||
LMS.mediaplayer = function () {
|
||||
|
||||
var _root = {};
|
||||
@@ -9,6 +15,7 @@ LMS.mediaplayer = function () {
|
||||
var _offset = 0;
|
||||
var _duration = 0;
|
||||
var _audioSrc;
|
||||
var _mode = Mode.File;
|
||||
|
||||
var _updateControls = function() {
|
||||
if (_elems.audio.paused) {
|
||||
@@ -125,11 +132,22 @@ LMS.mediaplayer = function () {
|
||||
if (!_elems.audio.hasAttribute("src"))
|
||||
return;
|
||||
|
||||
_offset = parseInt(_elems.seek.value, 10);
|
||||
_elems.audio.src = _audioSrc + "&offset=" + _offset;
|
||||
_elems.audio.load();
|
||||
_playTrack();
|
||||
let selectedOffset = parseInt(_elems.seek.value, 10);
|
||||
|
||||
switch (_mode) {
|
||||
case Mode.Transcode:
|
||||
_offset = selectedOffset;
|
||||
_elems.audio.src = _audioSrc + "&offset=" + _offset;
|
||||
_elems.audio.load();
|
||||
_elems.audio.currentTime = 0;
|
||||
_playTrack();
|
||||
break;
|
||||
|
||||
case Mode.File:
|
||||
_elems.audio.currentTime = selectedOffset;
|
||||
_playTrack();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
_elems.audio.addEventListener("play", _updateControls);
|
||||
@@ -175,6 +193,7 @@ LMS.mediaplayer = function () {
|
||||
_offset = 0;
|
||||
_duration = params.duration;
|
||||
_audioSrc = params.resource;
|
||||
_mode = params.mode;
|
||||
|
||||
_elems.seek.max = _duration;
|
||||
_elems.audio.src = _audioSrc;
|
||||
|
||||
@@ -283,6 +283,16 @@ User::getStarredTracks() const
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
User::checkBitrate(Database::Bitrate bitrate) const
|
||||
{
|
||||
if (audioTranscodeAllowedBitrates.find(bitrate) == std::cend(audioTranscodeAllowedBitrates))
|
||||
return false;
|
||||
|
||||
return static_cast<Bitrate>(_maxAudioTranscodeBitrate) >= bitrate;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,8 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
|
||||
|
||||
bool checkBitrate(Bitrate bitrate) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
|
||||
@@ -29,8 +29,9 @@ add_executable(lms
|
||||
ui/explore/ReleaseView.cpp
|
||||
ui/explore/TracksInfoView.cpp
|
||||
ui/explore/TracksView.cpp
|
||||
ui/resource/AudioFileResource.cpp
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
ui/resource/ImageResource.cpp
|
||||
ui/resource/AudioResource.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lms PRIVATE
|
||||
|
||||
@@ -44,8 +44,9 @@
|
||||
#include "admin/DatabaseSettingsView.hpp"
|
||||
#include "admin/UserView.hpp"
|
||||
#include "admin/UsersView.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
#include "resource/AudioTranscodeResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/AudioResource.hpp"
|
||||
#include "Auth.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
@@ -400,7 +401,8 @@ void
|
||||
LmsApplication::createHome()
|
||||
{
|
||||
_imageResource = std::make_shared<ImageResource>();
|
||||
_audioResource = std::make_shared<AudioResource>();
|
||||
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
||||
_audioFileResource = std::make_shared<AudioFileResource>();
|
||||
|
||||
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ namespace Database {
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioResource;
|
||||
class AudioTranscodeResource;
|
||||
class AudioFileResource;
|
||||
class Auth;
|
||||
class ImageResource;
|
||||
class LmsApplicationException;
|
||||
@@ -82,7 +83,8 @@ class LmsApplication : public Wt::WApplication
|
||||
|
||||
// Session application data
|
||||
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
|
||||
std::shared_ptr<AudioResource> getAudioResource() { return _audioResource; }
|
||||
std::shared_ptr<AudioTranscodeResource> getAudioTranscodeResource() { return _audioTranscodeResource; }
|
||||
std::shared_ptr<AudioFileResource> getAudioFileResource() { return _audioFileResource; }
|
||||
Database::Session& getDbSession() { return _dbSession;}
|
||||
|
||||
Wt::Dbo::ptr<Database::User> getUser();
|
||||
@@ -128,8 +130,9 @@ class LmsApplication : public Wt::WApplication
|
||||
Events _events;
|
||||
std::optional<Database::IdType> _userId;
|
||||
std::optional<bool> _userAuthStrong;
|
||||
std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource;
|
||||
std::shared_ptr<AudioFileResource> _audioFileResource;
|
||||
std::shared_ptr<ImageResource> _imageResource;
|
||||
std::shared_ptr<AudioResource> _audioResource;
|
||||
};
|
||||
|
||||
|
||||
|
||||
+63
-58
@@ -19,15 +19,16 @@
|
||||
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/AudioResource.hpp"
|
||||
#include "resource/AudioTranscodeResource.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
|
||||
#include "utils/String.hpp"
|
||||
|
||||
@@ -57,68 +58,72 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play)
|
||||
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
|
||||
|
||||
try
|
||||
std::string resource;
|
||||
bool transcode;
|
||||
if (LmsApp->getUser()->getAudioTranscodeEnable())
|
||||
{
|
||||
const Av::MediaFile mediaFile {track->getPath()};
|
||||
|
||||
const std::string resource {LmsApp->getAudioResource()->getUrl(trackId)};
|
||||
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
|
||||
|
||||
const auto artists {track->getArtists()};
|
||||
|
||||
std::ostringstream oss;
|
||||
oss
|
||||
<< "var params = {"
|
||||
<< " resource: \"" << resource << "\","
|
||||
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
|
||||
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
|
||||
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
|
||||
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||
<< " artwork: ["
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " ]"
|
||||
<< "};";
|
||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
|
||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
_artist->setTextFormat(Wt::TextFormat::Plain);
|
||||
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
|
||||
_artist->setLink(LmsApp->createArtistLink(artists.front()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_artist->setText("");
|
||||
_artist->setLink({});
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
_release->setTextFormat(Wt::TextFormat::Plain);
|
||||
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_release->setText("");
|
||||
_release->setLink({});
|
||||
}
|
||||
|
||||
wApp->doJavaScript(oss.str());
|
||||
resource = LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser());
|
||||
transcode = true;
|
||||
}
|
||||
catch (Av::MediaFileException& e)
|
||||
else
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "MediaFileException: " << e.what();
|
||||
resource = LmsApp->getAudioFileResource()->getUrl(trackId);
|
||||
transcode = false;
|
||||
}
|
||||
|
||||
const auto artists {track->getArtists()};
|
||||
|
||||
std::ostringstream oss;
|
||||
oss
|
||||
<< "var params = {"
|
||||
<< " mode: " << (transcode ? "Mode.Transcode" : "Mode.File") << ","
|
||||
<< " resource: \"" << resource << "\","
|
||||
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
|
||||
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
|
||||
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
|
||||
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||
<< " artwork: ["
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
|
||||
<< " ]"
|
||||
<< "};";
|
||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
|
||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
_artist->setTextFormat(Wt::TextFormat::Plain);
|
||||
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
|
||||
_artist->setLink(LmsApp->createArtistLink(artists.front()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_artist->setText("");
|
||||
_artist->setLink({});
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
_release->setTextFormat(Wt::TextFormat::Plain);
|
||||
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_release->setText("");
|
||||
_release->setLink({});
|
||||
}
|
||||
|
||||
wApp->doJavaScript(oss.str());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileResource.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
#define LOG(level) LMS_LOG(UI, level) << "Audio file resource: "
|
||||
|
||||
AudioFileResource:: ~AudioFileResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
std::string
|
||||
AudioFileResource::getUrl(Database::IdType trackId) const
|
||||
{
|
||||
return url()+ "&trackid=" + std::to_string(trackId);
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<std::filesystem::path>
|
||||
getTrackPathFromTrackId(Database::IdType trackId)
|
||||
{
|
||||
// DbSession are not thread safe
|
||||
Wt::WApplication::UpdateLock lock {LmsApp};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return track->getPath();
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<std::filesystem::path>
|
||||
getTrackPathFromURLArgs(const Wt::Http::Request& request)
|
||||
{
|
||||
const std::string* trackIdParameter {request.getParameter("trackid")};
|
||||
if (!trackIdParameter)
|
||||
{
|
||||
LOG(ERROR) << "Missing trackid URL parameter!";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto trackId {StringUtils::readAs<Database::IdType>(*trackIdParameter)};
|
||||
if (!trackId)
|
||||
{
|
||||
LOG(ERROR) << "Bad trackid URL parameter!";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return getTrackPathFromTrackId(*trackId);
|
||||
}
|
||||
|
||||
void
|
||||
AudioFileResource::handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response)
|
||||
{
|
||||
if (!request.continuation())
|
||||
{
|
||||
LOG(DEBUG) << "Initial request";
|
||||
|
||||
auto trackPath {getTrackPathFromURLArgs(request)};
|
||||
if (!trackPath)
|
||||
return;
|
||||
|
||||
ContinuationData continuationData;
|
||||
continuationData.path = *trackPath;
|
||||
continuationData.offset = 0;
|
||||
continuationData.beyondLastByte = 0;
|
||||
|
||||
{
|
||||
std::error_code ec;
|
||||
continuationData.fileSize = std::filesystem::file_size(*trackPath, ec);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
LOG(ERROR) << "Cannot get file size for '" << *trackPath << "': " << ec.message();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(DEBUG) << "Initial request. File = '" << continuationData.path << "', size = " << continuationData.fileSize;
|
||||
|
||||
handleRequestPiecewise(request, response, continuationData);
|
||||
|
||||
const auto fileFormat {Av::guessMediaFileFormat(*trackPath)};
|
||||
const std::string mimeType {fileFormat ? fileFormat->mimeType : "application/octet-stream"};
|
||||
response.setMimeType(mimeType);
|
||||
|
||||
LOG(DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||
}
|
||||
else
|
||||
{
|
||||
ContinuationData continuationData {Wt::cpp17::any_cast<ContinuationData>(request.continuation()->data())};
|
||||
handleRequestPiecewise(request, response, continuationData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
AudioFileResource::handleRequestPiecewise(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response,
|
||||
ContinuationData continuationData)
|
||||
{
|
||||
LOG(DEBUG) << "Handling request. File = '" << continuationData.path << "', size = " << continuationData.fileSize << ", offset = " << continuationData.offset << ", beyondLastByte = " << continuationData.beyondLastByte;
|
||||
|
||||
::uint64_t startByte {continuationData.offset};
|
||||
std::ifstream ifs {continuationData.path.string().c_str(), std::ios::in | std::ios::binary};
|
||||
|
||||
if (startByte == 0)
|
||||
{
|
||||
if (!ifs)
|
||||
{
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
response.setStatus(200);
|
||||
}
|
||||
|
||||
const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(continuationData.fileSize)};
|
||||
if (!ranges.isSatisfiable())
|
||||
{
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes */" << continuationData.fileSize;
|
||||
response.setStatus(416); // Requested range not satisfiable
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (ranges.size() == 1)
|
||||
{
|
||||
response.setStatus(206);
|
||||
startByte = ranges[0].firstByte();
|
||||
continuationData.beyondLastByte = ranges[0].lastByte() + 1;
|
||||
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes " << startByte << "-"
|
||||
<< continuationData.beyondLastByte - 1 << "/" << continuationData.fileSize;
|
||||
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
response.setContentLength(continuationData.beyondLastByte - startByte);
|
||||
}
|
||||
else
|
||||
{
|
||||
continuationData.beyondLastByte = continuationData.fileSize;
|
||||
response.setContentLength(continuationData.beyondLastByte);
|
||||
}
|
||||
}
|
||||
|
||||
ifs.seekg(static_cast<std::istream::pos_type>(startByte));
|
||||
|
||||
std::vector<char> buf;
|
||||
buf.resize(_chunkSize);
|
||||
|
||||
::uint64_t restSize = continuationData.beyondLastByte - startByte;
|
||||
::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size();
|
||||
|
||||
ifs.read(&buf[0], pieceSize);
|
||||
const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())};
|
||||
response.out().write(&buf[0], actualPieceSize);
|
||||
|
||||
LOG(DEBUG) << "Written " << actualPieceSize << " bytes!";
|
||||
|
||||
if (ifs.good() && actualPieceSize < restSize)
|
||||
{
|
||||
LOG(DEBUG) << "Still more to do";
|
||||
|
||||
auto* continuation {response.createContinuation()};
|
||||
ContinuationData newContinuationData {continuationData};
|
||||
newContinuationData.offset = startByte + actualPieceSize;
|
||||
continuation->setData(newContinuationData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <Wt/WResource.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioFileResource : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
~AudioFileResource();
|
||||
|
||||
std::string getUrl(Database::IdType trackId) const;
|
||||
|
||||
private:
|
||||
|
||||
static constexpr std::size_t _chunkSize {262144};
|
||||
|
||||
struct ContinuationData
|
||||
{
|
||||
std::filesystem::path path;
|
||||
::uint64_t beyondLastByte;
|
||||
::uint64_t fileSize;
|
||||
::uint64_t offset;
|
||||
};
|
||||
|
||||
void handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response);
|
||||
|
||||
void handleRequestPiecewise(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response,
|
||||
ContinuationData continuationData);
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioResource.hpp"
|
||||
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
|
||||
AudioResource:: ~AudioResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
std::string
|
||||
AudioResource::getUrl(Database::IdType trackId) const
|
||||
{
|
||||
return url()+ "&trackid=" + std::to_string(trackId);
|
||||
}
|
||||
|
||||
void
|
||||
AudioResource::handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<Av::Transcoder> transcoder;
|
||||
|
||||
// First, see if this request is for a continuation
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
if (continuation)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Continuation! " << continuation ;
|
||||
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
|
||||
}
|
||||
else
|
||||
{
|
||||
Database::IdType trackId;
|
||||
Av::TranscodeParameters parameters {};
|
||||
parameters.stripMetadata = true;
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "First request: creating transcoder";
|
||||
try
|
||||
{
|
||||
auto trackIdStr = request.getParameter("trackid");
|
||||
if (!trackIdStr)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Missing trackid transcode parameter!";
|
||||
return;
|
||||
}
|
||||
trackId = std::stol(*request.getParameter("trackid"));
|
||||
|
||||
auto offsetStr = request.getParameter("offset");
|
||||
parameters.offset = std::chrono::seconds(offsetStr ? std::stol(*offsetStr) : 0);
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Exception while handling URL parameters: " << e.what();
|
||||
return;
|
||||
}
|
||||
|
||||
// DbSession are not thread safe
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock(LmsApp);
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Missing track";
|
||||
return;
|
||||
}
|
||||
|
||||
if (LmsApp->getUser()->getAudioTranscodeEnable())
|
||||
{
|
||||
parameters.bitrate = LmsApp->getUser()->getAudioTranscodeBitrate();
|
||||
|
||||
switch (LmsApp->getUser()->getAudioTranscodeFormat())
|
||||
{
|
||||
case Database::AudioFormat::MP3:
|
||||
parameters.encoding = Av::Encoding::MP3;
|
||||
break;
|
||||
case Database::AudioFormat::OGG_OPUS:
|
||||
parameters.encoding = Av::Encoding::OGG_OPUS;
|
||||
break;
|
||||
case Database::AudioFormat::MATROSKA_OPUS:
|
||||
parameters.encoding = Av::Encoding::MATROSKA_OPUS;
|
||||
break;
|
||||
case Database::AudioFormat::OGG_VORBIS:
|
||||
parameters.encoding = Av::Encoding::OGG_VORBIS;
|
||||
break;
|
||||
case Database::AudioFormat::WEBM_VORBIS:
|
||||
parameters.encoding = Av::Encoding::WEBM_VORBIS;
|
||||
break;
|
||||
default:
|
||||
parameters.encoding = Av::Encoding::OGG_OPUS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
parameters.bitrate = 0;
|
||||
|
||||
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
|
||||
}
|
||||
|
||||
|
||||
if (!transcoder->start())
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Cannot start transcoder";
|
||||
return;
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Transcoder started";
|
||||
|
||||
std::string mimeType {transcoder->getOutputMimeType()};
|
||||
response.setMimeType(mimeType);
|
||||
LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
{
|
||||
std::vector<unsigned char> data;
|
||||
data.reserve(_chunkSize);
|
||||
|
||||
transcoder->process(data, _chunkSize);
|
||||
|
||||
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||
LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
|
||||
|
||||
if (!response.out())
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Write failed!";
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete() && response.out())
|
||||
{
|
||||
continuation = response.createContinuation();
|
||||
continuation->setData(transcoder);
|
||||
}
|
||||
else
|
||||
LMS_LOG(UI, DEBUG) << "No more data!";
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioTranscodeResource.hpp"
|
||||
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#define LOG(level) LMS_LOG(UI, level) << "Audio transcode resource: "
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template <>
|
||||
std::optional<Database::AudioFormat>
|
||||
readAs(const std::string& str)
|
||||
{
|
||||
|
||||
auto encodedFormat {readAs<int>(str)};
|
||||
if (!encodedFormat)
|
||||
return std::nullopt;
|
||||
|
||||
Database::AudioFormat format {static_cast<Database::AudioFormat>(*encodedFormat)};
|
||||
|
||||
// check
|
||||
switch (static_cast<Database::AudioFormat>(*encodedFormat))
|
||||
{
|
||||
case Database::AudioFormat::MP3:
|
||||
[[fallthrough]];
|
||||
case Database::AudioFormat::OGG_OPUS:
|
||||
[[fallthrough]];
|
||||
case Database::AudioFormat::MATROSKA_OPUS:
|
||||
[[fallthrough]];
|
||||
case Database::AudioFormat::OGG_VORBIS:
|
||||
[[fallthrough]];
|
||||
case Database::AudioFormat::WEBM_VORBIS:
|
||||
return format;
|
||||
}
|
||||
|
||||
LOG(ERROR) << "Cannot determine audio format from value '" << str << "'";
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
AudioTranscodeResource:: ~AudioTranscodeResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<Av::Encoding>
|
||||
AudioFormatToAvEncoding(Database::AudioFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case Database::AudioFormat::MP3: return Av::Encoding::MP3;
|
||||
case Database::AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
|
||||
case Database::AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
|
||||
case Database::AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
|
||||
case Database::AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
|
||||
}
|
||||
|
||||
LOG(ERROR) << "Cannot convert from audio format to encoding";
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string
|
||||
AudioTranscodeResource::getUrlForUser(Database::IdType trackId, Database::User::pointer user) const
|
||||
{
|
||||
std::string computedUrl {url() + "&trackid=" + std::to_string(trackId)};
|
||||
|
||||
computedUrl += "&format=" + std::to_string(static_cast<std::size_t>(user->getAudioTranscodeFormat()));
|
||||
computedUrl += "&bitrate=" + std::to_string(LmsApp->getUser()->getAudioTranscodeBitrate());
|
||||
|
||||
return computedUrl;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T>
|
||||
readParameterAs(const Wt::Http::Request& request, const std::string& parameterName)
|
||||
{
|
||||
auto paramStr {request.getParameter(parameterName)};
|
||||
if (!paramStr)
|
||||
{
|
||||
LOG(ERROR) << "Missing parameter '" << parameterName << "'";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto res {StringUtils::readAs<T>(*paramStr)};
|
||||
if (!res)
|
||||
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'";
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<Av::Transcoder> transcoder;
|
||||
|
||||
// First, see if this request is for a continuation
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
if (continuation)
|
||||
{
|
||||
LOG(DEBUG) << "Continuation! " << continuation ;
|
||||
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG) << "First request: creating transcoder";
|
||||
|
||||
// mandatory parameters
|
||||
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
|
||||
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return;
|
||||
|
||||
auto encoding {AudioFormatToAvEncoding(*format)};
|
||||
if (!encoding)
|
||||
return;
|
||||
|
||||
// optional parameter
|
||||
auto offset {readParameterAs<std::size_t>(request, "offset")};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
// DbSession are not thread safe
|
||||
Wt::WApplication::UpdateLock lock(LmsApp);
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
return;
|
||||
}
|
||||
|
||||
trackPath = track->getPath();
|
||||
|
||||
if (!LmsApp->getUser()->checkBitrate(*bitrate))
|
||||
{
|
||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Av::TranscodeParameters parameters {};
|
||||
parameters.stripMetadata = true;
|
||||
parameters.encoding = *encoding;
|
||||
parameters.bitrate = *bitrate;
|
||||
parameters.offset = std::chrono::seconds {offset ? *offset : 0};
|
||||
|
||||
transcoder = std::make_shared<Av::Transcoder>(trackPath, parameters);
|
||||
if (!transcoder->start())
|
||||
{
|
||||
LOG(ERROR) << "Cannot start transcoder";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(DEBUG) << "Transcoder started";
|
||||
|
||||
std::string mimeType {transcoder->getOutputMimeType()};
|
||||
response.setMimeType(mimeType);
|
||||
LOG(DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
{
|
||||
std::vector<unsigned char> data;
|
||||
data.reserve(_chunkSize);
|
||||
|
||||
transcoder->process(data, _chunkSize);
|
||||
|
||||
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||
LOG(DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
|
||||
|
||||
if (!response.out())
|
||||
{
|
||||
LOG(ERROR) << "Write failed!";
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete() && response.out())
|
||||
{
|
||||
continuation = response.createContinuation();
|
||||
continuation->setData(transcoder);
|
||||
}
|
||||
else
|
||||
LOG(DEBUG) << "No more data!";
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
+10
-6
@@ -19,28 +19,32 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#include <Wt/WResource.h>
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioResource : public Wt::WResource
|
||||
class AudioTranscodeResource : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
~AudioResource();
|
||||
~AudioTranscodeResource();
|
||||
|
||||
std::string getUrl(Database::IdType trackId) const;
|
||||
// Url depends on the user since settings are used in parameters
|
||||
std::string getUrlForUser(Database::IdType trackId, Wt::Dbo::ptr<Database::User> user) const;
|
||||
|
||||
void handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response);
|
||||
|
||||
private:
|
||||
|
||||
static const std::size_t _chunkSize = 65536*4;
|
||||
static constexpr std::size_t _chunkSize {262144};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -31,6 +31,8 @@
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#define LOG(level) LMS_LOG(UI, level) << "Image resource: "
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ImageResource::~ImageResource()
|
||||
@@ -60,19 +62,30 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
||||
|
||||
// Mandatory parameter size
|
||||
if (!sizeStr)
|
||||
{
|
||||
LOG(DEBUG) << "no size provided!";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto size {StringUtils::readAs<std::size_t>(*sizeStr)};
|
||||
if (!size || *size > maxSize)
|
||||
{
|
||||
LOG(DEBUG) << "invalid size provided!";
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> cover;
|
||||
|
||||
if (trackIdStr)
|
||||
{
|
||||
LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size;
|
||||
|
||||
const auto trackId {StringUtils::readAs<Database::IdType>(*trackIdStr)};
|
||||
if (!trackId)
|
||||
{
|
||||
LOG(DEBUG) << "track not found";
|
||||
return;
|
||||
}
|
||||
|
||||
// DbSession are not thread safe
|
||||
{
|
||||
@@ -82,6 +95,8 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
||||
}
|
||||
else if (releaseIdStr)
|
||||
{
|
||||
LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size;
|
||||
|
||||
const auto releaseId {StringUtils::readAs<Database::IdType>(*releaseIdStr)};
|
||||
if (!releaseId)
|
||||
return;
|
||||
@@ -93,7 +108,10 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG) << "No track or release provided";
|
||||
return;
|
||||
}
|
||||
|
||||
response.setMimeType(getMimeType());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user