diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 2d8353e9..78affb86 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -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; diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index 6b980130..c471b40b 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -283,6 +283,16 @@ User::getStarredTracks() const return std::vector>(_starredTracks.begin(), _starredTracks.end()); } + +bool +User::checkBitrate(Database::Bitrate bitrate) const +{ + if (audioTranscodeAllowedBitrates.find(bitrate) == std::cend(audioTranscodeAllowedBitrates)) + return false; + + return static_cast(_maxAudioTranscodeBitrate) >= bitrate; +} + } // namespace Database diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index c6283b00..f1e0d255 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -170,6 +170,8 @@ class User : public Wt::Dbo::Dbo bool hasStarredTrack(Wt::Dbo::ptr track) const; std::vector> getStarredTracks() const; + bool checkBitrate(Bitrate bitrate) const; + template void persist(Action& a) { diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index d422a93b..6cdfdddc 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -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 diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 8a6dbcc2..1dfb5b30 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -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(); - _audioResource = std::make_shared(); + _audioTranscodeResource = std::make_shared(); + _audioFileResource = std::make_shared(); setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm")); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index cd0f04ca..474562d3 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -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 getImageResource() { return _imageResource; } - std::shared_ptr getAudioResource() { return _audioResource; } + std::shared_ptr getAudioTranscodeResource() { return _audioTranscodeResource; } + std::shared_ptr getAudioFileResource() { return _audioFileResource; } Database::Session& getDbSession() { return _dbSession;} Wt::Dbo::ptr getUser(); @@ -128,8 +130,9 @@ class LmsApplication : public Wt::WApplication Events _events; std::optional _userId; std::optional _userAuthStrong; + std::shared_ptr _audioTranscodeResource; + std::shared_ptr _audioFileResource; std::shared_ptr _imageResource; - std::shared_ptr _audioResource; }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 8b376ad1..bd7d7f36 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -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(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(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 diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp new file mode 100644 index 00000000..88b8c2e0 --- /dev/null +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -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 . + */ + +#include "AudioFileResource.hpp" + +#include +#include + +#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 +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 +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(*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(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(startByte)); + + std::vector 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 + + diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp new file mode 100644 index 00000000..0ba77397 --- /dev/null +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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 + + + + diff --git a/src/lms/ui/resource/AudioResource.cpp b/src/lms/ui/resource/AudioResource.cpp deleted file mode 100644 index 520ce7ed..00000000 --- a/src/lms/ui/resource/AudioResource.cpp +++ /dev/null @@ -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 . - */ - -#include "AudioResource.hpp" - -#include - -#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 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>(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(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 data; - data.reserve(_chunkSize); - - transcoder->process(data, _chunkSize); - - response.out().write(reinterpret_cast(&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 - - diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp new file mode 100644 index 00000000..213789dd --- /dev/null +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -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 . + */ + +#include "AudioTranscodeResource.hpp" + +#include + +#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 + readAs(const std::string& str) + { + + auto encodedFormat {readAs(str)}; + if (!encodedFormat) + return std::nullopt; + + Database::AudioFormat format {static_cast(*encodedFormat)}; + + // check + switch (static_cast(*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 +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(user->getAudioTranscodeFormat())); + computedUrl += "&bitrate=" + std::to_string(LmsApp->getUser()->getAudioTranscodeBitrate()); + + return computedUrl; +} + +template +std::optional +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(*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 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>(continuation->data()); + } + else + { + LOG(DEBUG) << "First request: creating transcoder"; + + // mandatory parameters + auto trackId {readParameterAs(request, "trackid")}; + auto format {readParameterAs(request, "format")}; + auto bitrate {readParameterAs(request, "bitrate")}; + + if (!trackId || !format || !bitrate) + return; + + auto encoding {AudioFormatToAvEncoding(*format)}; + if (!encoding) + return; + + // optional parameter + auto offset {readParameterAs(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(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 data; + data.reserve(_chunkSize); + + transcoder->process(data, _chunkSize); + + response.out().write(reinterpret_cast(&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 + + diff --git a/src/lms/ui/resource/AudioResource.hpp b/src/lms/ui/resource/AudioTranscodeResource.hpp similarity index 72% rename from src/lms/ui/resource/AudioResource.hpp rename to src/lms/ui/resource/AudioTranscodeResource.hpp index 66cd07a5..fa5d2ed7 100644 --- a/src/lms/ui/resource/AudioResource.hpp +++ b/src/lms/ui/resource/AudioTranscodeResource.hpp @@ -19,28 +19,32 @@ #pragma once +#include #include -#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 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 diff --git a/src/lms/ui/resource/ImageResource.cpp b/src/lms/ui/resource/ImageResource.cpp index 6abd1d42..03c059b9 100644 --- a/src/lms/ui/resource/ImageResource.cpp +++ b/src/lms/ui/resource/ImageResource.cpp @@ -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(*sizeStr)}; if (!size || *size > maxSize) + { + LOG(DEBUG) << "invalid size provided!"; return; + } std::vector cover; if (trackIdStr) { + LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; + const auto trackId {StringUtils::readAs(*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(*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());