diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index c2d03eb6..adb37766 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(lmsav SHARED impl/AvInfo.cpp impl/AvTranscoder.cpp + impl/AvTranscodeResourceHandler.cpp impl/AvTypes.cpp ) @@ -18,6 +19,7 @@ target_link_libraries(lmsav PUBLIC lmsutils avformat avutil + wt ) install(TARGETS lmsav DESTINATION lib) diff --git a/src/libs/av/impl/AvTranscodeResourceHandler.cpp b/src/libs/av/impl/AvTranscodeResourceHandler.cpp new file mode 100644 index 00000000..fbea1f2f --- /dev/null +++ b/src/libs/av/impl/AvTranscodeResourceHandler.cpp @@ -0,0 +1,59 @@ +/* + * 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 "AvTranscodeResourceHandler.hpp" + +namespace Av +{ + + std::unique_ptr + createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) + { + return std::make_unique(trackPath, parameters); + } + + // TODO set some nice HTTP return code + + TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) + : _transcoder {trackPath, parameters} + { + _transcoder.start(); + } + + void + TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) + { + response.setMimeType(_transcoder.getOutputMimeType()); + + if (!_transcoder.isComplete()) + { + std::vector buffer; + + _transcoder.process(buffer, _chunkSize); + response.out().write(reinterpret_cast(&buffer[0]), buffer.size()); + } + } + + bool + TranscodeResourceHandler::isFinished() const + { + return _transcoder.isComplete(); + } +} + diff --git a/src/libs/av/impl/AvTranscodeResourceHandler.hpp b/src/libs/av/impl/AvTranscodeResourceHandler.hpp new file mode 100644 index 00000000..351e6e45 --- /dev/null +++ b/src/libs/av/impl/AvTranscodeResourceHandler.hpp @@ -0,0 +1,45 @@ +/* + * 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 "av/AvTranscoder.hpp" +#include "utils/IResourceHandler.hpp" + +namespace Av +{ + + class TranscodeResourceHandler final : public IResourceHandler + { + public: + TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + + private: + + void processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; + bool isFinished() const override; + + static constexpr std::size_t _chunkSize {262144}; + const std::filesystem::path _trackPath; + Transcoder _transcoder; + + }; +} + diff --git a/src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp new file mode 100644 index 00000000..041ebc91 --- /dev/null +++ b/src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp @@ -0,0 +1,34 @@ +/* + * 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 "utils/IResourceHandler.hpp" + +namespace Av +{ + struct TranscodeParameters; + + std::unique_ptr createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + +} + diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 0531b7bf..e5743a37 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(lmssubsonic SHARED + impl/ParameterParsing.cpp + impl/Stream.cpp impl/SubsonicId.cpp impl/SubsonicResource.cpp impl/SubsonicResponse.cpp diff --git a/src/libs/subsonic/impl/ParameterParsing.cpp b/src/libs/subsonic/impl/ParameterParsing.cpp new file mode 100644 index 00000000..dbfd0656 --- /dev/null +++ b/src/libs/subsonic/impl/ParameterParsing.cpp @@ -0,0 +1,43 @@ +/* + * 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 "ParameterParsing.hpp" + +namespace StringUtils +{ + template<> + std::optional + StringUtils::readAs(const std::string& str) + { + return API::Subsonic::IdFromString(str); + } + + template<> + std::optional + StringUtils::readAs(const std::string& str) + { + if (str == "true") + return true; + else if (str == "false") + return false; + + return {}; + } +} + diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp new file mode 100644 index 00000000..5ddad43a --- /dev/null +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -0,0 +1,99 @@ +/* + * 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 "utils/String.hpp" +#include "SubsonicId.hpp" +#include "SubsonicResponse.hpp" + +namespace API::Subsonic +{ + + template + std::vector + getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) + { + std::vector res; + + auto it = parameterMap.find(paramName); + if (it == parameterMap.end()) + return res; + + for (const std::string& param : it->second) + { + auto value {StringUtils::readAs(param)}; + if (!value) + throw BadParameterFormatGenericError {paramName}; + + res.emplace_back(std::move(*value)); + } + + return res; + } + + template + std::vector + getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) + { + std::vector res {getMultiParametersAs(parameterMap, param)}; + if (res.empty()) + throw RequiredParameterMissingError {}; + + return res; + } + + template + std::optional + getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) + { + std::vector params {getMultiParametersAs(parameterMap, param)}; + + if (params.size() != 1) + return {}; + + return T { std::move(params.front()) }; + } + + template + T + getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) + { + auto res {getParameterAs(parameterMap, param)}; + if (!res) + throw RequiredParameterMissingError {}; + + return *res; + } + + +} + +namespace StringUtils +{ + template<> + std::optional + StringUtils::readAs(const std::string& str); + + template<> + std::optional + StringUtils::readAs(const std::string& str); +} + diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp new file mode 100644 index 00000000..ec10a226 --- /dev/null +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -0,0 +1,44 @@ +/* + * 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 + + +namespace Database +{ + class Session; +} + +namespace API::Subsonic +{ + + struct RequestContext + { + const Wt::Http::ParameterMap& parameters; + Database::Session& dbSession; + std::string userName; + }; + +} + diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp new file mode 100644 index 00000000..e58f33cd --- /dev/null +++ b/src/libs/subsonic/impl/Stream.cpp @@ -0,0 +1,139 @@ +/* + * 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 "Stream.hpp" + +#include "av/AvTranscoder.hpp" +#include "av/AvTranscodeResourceHandlerCreator.hpp" +#include "av/AvTypes.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/User.hpp" +#include "utils/IResourceHandler.hpp" +#include "utils/FileResourceHandlerCreator.hpp" +#include "utils/Utils.hpp" +#include "ParameterParsing.hpp" +#include "SubsonicId.hpp" + +using namespace Database; + +namespace API::Subsonic::Stream +{ + +static +Av::Encoding +userTranscodeFormatToAvEncoding(AudioFormat format) +{ + switch (format) + { + case AudioFormat::MP3: return Av::Encoding::MP3; + case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS; + case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS; + case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS; + case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS; + default: return Av::Encoding::OGG_OPUS; + } +} + +struct StreamParameters +{ + std::filesystem::path trackPath; + std::optional transcodeParameters; +}; + + +static +StreamParameters +getStreamParameters(RequestContext& context) +{ + // Mandatory params + Id id {getMandatoryParameterAs(context.parameters, "id")}; + + // Optional params + std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; + std::optional format {getParameterAs(context.parameters, "format")}; + + StreamParameters parameters; + + auto transaction {context.dbSession.createSharedTransaction()}; + + { + auto track {Track::getById(context.dbSession, id.value)}; + if (!track) + throw RequestedDataNotFoundError {}; + + parameters.trackPath = track->getPath(); + } + + { + const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + if (!user) + throw UserNotAuthorizedError {}; + + // format = "raw" => no transcode. Other format values will be ignored + const bool transcode {(!format || (*format != "raw")) && user->getAudioTranscodeEnable()}; + if (transcode) + { + // "If set to zero, no limit is imposed" + if (!maxBitRate || *maxBitRate == 0) + maxBitRate = user->getAudioTranscodeBitrate() / 1000; + + *maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000); + + Av::TranscodeParameters transcodeParameters; + + transcodeParameters.bitrate = *maxBitRate * 1000; + transcodeParameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat()); + + parameters.transcodeParameters = std::move(transcodeParameters); + } + } + + return parameters; +} + +void +handle(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) +{ + std::shared_ptr resourceHandler; + + Wt::Http::ResponseContinuation *continuation = request.continuation(); + if (!continuation) + { + StreamParameters streamParameters {getStreamParameters(context)}; + if (streamParameters.transcodeParameters) + resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters); + else + resourceHandler = createFileResourceHandler(streamParameters.trackPath); + } + else + { + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + } + + resourceHandler->processRequest(request, response); + if (!resourceHandler->isFinished()) + { + Wt::Http::ResponseContinuation *continuation = response.createContinuation(); + continuation->setData(resourceHandler); + } +} + +} + diff --git a/src/libs/utils/include/utils/FileResourceHandler.hpp b/src/libs/subsonic/impl/Stream.hpp similarity index 58% rename from src/libs/utils/include/utils/FileResourceHandler.hpp rename to src/libs/subsonic/impl/Stream.hpp index fb663032..bf7fade3 100644 --- a/src/libs/utils/include/utils/FileResourceHandler.hpp +++ b/src/libs/subsonic/impl/Stream.hpp @@ -19,22 +19,13 @@ #pragma once -#include #include #include +#include "RequestContext.hpp" -// Helper used to deliver file contents from a WResource -namespace FileResourceHandler +namespace API::Subsonic::Stream { - struct ContinuationData - { - std::filesystem::path path; - ::uint64_t beyondLastByte; - ::uint64_t offset; - }; - - std::optional handleInitialRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const std::filesystem::path& path); - std::optional handleContinuationRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const ContinuationData& continuationData); + void handle(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); } diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 02915a4c..91cc61bc 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -17,6 +17,8 @@ * along with LMS. If not, see . */ +#pragma once + #include #include "database/Types.hpp" diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index c538d1eb..c09a4c98 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -19,15 +19,10 @@ #include "subsonic/SubsonicResource.hpp" #include -#include -#include -#include -#include #include #include "auth/IPasswordService.hpp" -#include "av/AvTranscoder.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" @@ -40,10 +35,13 @@ #include "database/User.hpp" #include "recommendation/IEngine.hpp" #include "utils/Logger.hpp" +#include "utils/Random.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" #include "utils/Utils.hpp" -#include "SubsonicId.hpp" +#include "ParameterParsing.hpp" +#include "RequestContext.hpp" +#include "Stream.hpp" #include "SubsonicResponse.hpp" using namespace Database; @@ -65,25 +63,6 @@ namespace API::Subsonic namespace StringUtils { - template<> - std::optional - StringUtils::readAs(const std::string& str) - { - return API::Subsonic::IdFromString(str); - } - - template<> - std::optional - StringUtils::readAs(const std::string& str) - { - if (str == "true") - return true; - else if (str == "false") - return false; - - return {}; - } - template<> std::optional StringUtils::readAs(const std::string& str) @@ -122,21 +101,6 @@ namespace StringUtils namespace API::Subsonic { -struct ClientInfo -{ - std::string name; - std::string user; - std::string password; - ClientVersion version; -}; - -struct RequestContext -{ - const Wt::Http::ParameterMap& parameters; - Session& dbSession; - std::string userName; -}; - static std::string makeNameFilesystemCompatible(const std::string& name) @@ -144,62 +108,6 @@ makeNameFilesystemCompatible(const std::string& name) return StringUtils::replaceInString(name, "/", "_"); } -template -std::vector -getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) -{ - std::vector res; - - auto it = parameterMap.find(paramName); - if (it == parameterMap.end()) - return res; - - for (const std::string& param : it->second) - { - auto value {StringUtils::readAs(param)}; - if (!value) - throw BadParameterFormatGenericError {paramName}; - - res.emplace_back(std::move(*value)); - } - - return res; -} - -template -std::vector -getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) -{ - std::vector res {getMultiParametersAs(parameterMap, param)}; - if (res.empty()) - throw RequiredParameterMissingError {}; - - return res; -} - -template -std::optional -getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) -{ - std::vector params {getMultiParametersAs(parameterMap, param)}; - - if (params.size() != 1) - return {}; - - return T { std::move(params.front()) }; -} - -template -T -getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) -{ - auto res {getParameterAs(parameterMap, param)}; - if (!res) - throw RequiredParameterMissingError {}; - - return *res; -} - static std::string decodePasswordIfNeeded(const std::string& password) @@ -216,6 +124,14 @@ decodePasswordIfNeeded(const std::string& password) return password; } +struct ClientInfo +{ + std::string name; + std::string user; + std::string password; + ClientVersion version; +}; + static ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters) @@ -238,14 +154,6 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) return res; } -// MediaRetrievals -struct MediaRetrievalResult -{ - std::string mimeType; - std::vector data; - Wt::cpp17::any continuationData; -}; - SubsonicResource::SubsonicResource(Db& db) : _sessionPool {db} { @@ -1165,9 +1073,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) std::make_move_iterator(std::end(similarArtistTracks))); } - auto now {std::chrono::system_clock::now()}; - std::mt19937 randGenerator {static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; - std::shuffle(std::begin(tracks), std::end(tracks), randGenerator); + Random::shuffleContainer(tracks); Response response {Response::createOkResponse()}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; @@ -1775,111 +1681,8 @@ handleNotImplemented(RequestContext&) } static -Av::Encoding -userTranscodeFormatToAvEncoding(AudioFormat format) -{ - switch (format) - { - case AudioFormat::MP3: return Av::Encoding::MP3; - case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS; - case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS; - case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS; - case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS; - default: return Av::Encoding::OGG_OPUS; - } -} - -static -std::shared_ptr -createTranscoder(RequestContext& context) -{ - // Mandatory params - Id id {getMandatoryParameterAs(context.parameters, "id")}; - - // Optional params - std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; - std::optional format {getParameterAs(context.parameters, "format")}; - - Av::TranscodeParameters parameters {}; - parameters.stripMetadata = false; // Since it can be cached and some players read the metadata from the downloaded file - - std::filesystem::path trackPath; - { - auto transaction {context.dbSession.createSharedTransaction()}; - - { - auto track {Track::getById(context.dbSession, id.value)}; - if (!track) - throw RequestedDataNotFoundError {}; - - trackPath = track->getPath(); - } - - { - const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; - if (!user) - throw UserNotAuthorizedError {}; - - // format = "raw" => no transcode. Other format values will be ignored - const bool transcode {(!format || (format && *format != "raw")) && user->getAudioTranscodeEnable()}; - if (transcode) - { - // "If set to zero, no limit is imposed" - if (!maxBitRate || *maxBitRate == 0) - maxBitRate = user->getAudioTranscodeBitrate() / 1000; - - *maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000); - - parameters.bitrate = *maxBitRate * 1000; - parameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat()); - } - } - } - - return std::make_shared(trackPath, parameters); -} - -static -MediaRetrievalResult -handleStream(RequestContext& context, Wt::Http::ResponseContinuation* continuation) -{ - MediaRetrievalResult res; - - std::shared_ptr transcoder; - - if (!continuation) - { - transcoder = createTranscoder(context); - transcoder->start(); - - res.mimeType = transcoder->getOutputMimeType(); - LMS_LOG(API_SUBSONIC, DEBUG) << "Mime type set to '" << transcoder->getOutputMimeType() << "'"; - } - else - { - transcoder = Wt::cpp17::any_cast>(continuation->data()); - } - - if (!transcoder) - throw InternalErrorGenericError {"Cannot create transcoder"}; - - if (!transcoder->isComplete()) - { - static constexpr std::size_t chunkSize {65536*4}; - res.data.reserve(chunkSize); - - transcoder->process(res.data, chunkSize); - } - - if (!transcoder->isComplete()) - res.continuationData = std::move(transcoder); - - return res; -} - -static -MediaRetrievalResult -handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*) +void +handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; @@ -1887,23 +1690,21 @@ handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*) std::size_t size {getParameterAs(context.parameters, "size").value_or(256)}; size = clamp(size, std::size_t {32}, std::size_t {1024}); - MediaRetrievalResult res; - + std::vector data; switch (id.type) { case Id::Type::Track: - res.data = ServiceProvider::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); + data = ServiceProvider::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); break; case Id::Type::Release: - res.data = ServiceProvider::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); + data = ServiceProvider::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); break; default: throw BadParameterGenericError {"id"}; } - res.mimeType = CoverArt::formatToMimeType(CoverArt::Format::JPEG); - - return res; + response.out().write(reinterpret_cast(&data[0]), data.size()); + response.setMimeType(CoverArt::formatToMimeType(CoverArt::Format::JPEG)); } using RequestHandlerFunc = std::function; @@ -2019,12 +1820,12 @@ static std::unordered_map requestEntryPoints {"startScan", {handleNotImplemented, true}}, }; -using MediaRetrievalHandlerFunc = std::function; +using MediaRetrievalHandlerFunc = std::function; static std::unordered_map mediaRetrievalHandlers { // Media retrieval {"getCoverArt", handleGetCoverArt}, - {"stream", handleStream}, + {"stream", Stream::handle}, }; void @@ -2091,26 +1892,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp auto itStreamHandler {mediaRetrievalHandlers.find(requestPath)}; if (itStreamHandler != mediaRetrievalHandlers.end()) { - MediaRetrievalResult res {itStreamHandler->second(requestContext, request.continuation())}; - - if (!res.mimeType.empty()) - response.setMimeType(res.mimeType); - if (!res.data.empty()) - { - response.out().write(reinterpret_cast(&res.data[0]), res.data.size()); - if (!response.out()) - { - LMS_LOG(API_SUBSONIC, ERROR) << "Write failed!"; - return; - } - } - - if (res.continuationData.has_value()) - { - auto continuation {response.createContinuation()}; - continuation->setData(std::move(res.continuationData)); - } - + itStreamHandler->second(requestContext, request, response); LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!"; return; } diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index f153e6f7..a88217ad 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -16,6 +16,7 @@ * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ +#pragma once #include #include diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp index 01628a07..ad7ec8eb 100644 --- a/src/libs/utils/impl/FileResourceHandler.cpp +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -17,25 +17,30 @@ * along with LMS. If not, see . */ -#include "utils/FileResourceHandler.hpp" +#include "FileResourceHandler.hpp" #include #include "utils/Logger.hpp" -namespace FileResourceHandler +std::unique_ptr +createFileResourceHandler(const std::filesystem::path& path) { + return std::make_unique(path); +} -static constexpr std::size_t _chunkSize {262144}; -static -std::optional -handleRequestPiecewise(const Wt::Http::Request& request, - Wt::Http::Response& response, - ContinuationData continuationData) +FileResourceHandler::FileResourceHandler(const std::filesystem::path& path) +: _path {path} { - ::uint64_t startByte {continuationData.offset}; - std::ifstream ifs {continuationData.path.string().c_str(), std::ios::in | std::ios::binary}; +} + + +void +FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) +{ + ::uint64_t startByte {_offset}; + std::ifstream ifs {_path.string().c_str(), std::ios::in | std::ios::binary}; LMS_LOG(UTILS, DEBUG) << "startByte = " << startByte; @@ -43,9 +48,10 @@ handleRequestPiecewise(const Wt::Http::Request& request, { if (!ifs) { - LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << continuationData.path.string() << "'"; + LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; response.setStatus(404); - return std::nullopt; + _isFinished = true; + return; } else { @@ -67,7 +73,8 @@ handleRequestPiecewise(const Wt::Http::Request& request, response.addHeader("Content-Range", contentRange.str()); LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; - return std::nullopt; + _isFinished = true; + return; } if (ranges.size() == 1) @@ -76,21 +83,21 @@ handleRequestPiecewise(const Wt::Http::Request& request, response.setStatus(206); startByte = ranges[0].firstByte(); - continuationData.beyondLastByte = ranges[0].lastByte() + 1; + _beyondLastByte = ranges[0].lastByte() + 1; std::ostringstream contentRange; contentRange << "bytes " << startByte << "-" - << continuationData.beyondLastByte - 1 << "/" << fileSize; + << _beyondLastByte - 1 << "/" << fileSize; response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(continuationData.beyondLastByte - startByte); + response.setContentLength(_beyondLastByte - startByte); } else { LMS_LOG(UTILS, DEBUG) << "No range requested"; - continuationData.beyondLastByte = fileSize; - response.setContentLength(continuationData.beyondLastByte); + _beyondLastByte = fileSize; + response.setContentLength(_beyondLastByte); } } @@ -99,7 +106,7 @@ handleRequestPiecewise(const Wt::Http::Request& request, std::vector buf; buf.resize(_chunkSize); - ::uint64_t restSize = continuationData.beyondLastByte - startByte; + ::uint64_t restSize = _beyondLastByte - startByte; ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); ifs.read(&buf[0], pieceSize); @@ -111,47 +118,21 @@ handleRequestPiecewise(const Wt::Http::Request& request, LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; if (ifs.good() && actualPieceSize < restSize) { - ContinuationData newContinuationData {continuationData}; - newContinuationData.offset = startByte + actualPieceSize; + _offset = startByte + actualPieceSize; - LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << newContinuationData.offset; - - return newContinuationData; + LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; } else { + _isFinished = true; LMS_LOG(UTILS, DEBUG) << "Job complete!"; } - - return std::nullopt; } - -std::optional -handleInitialRequest(const Wt::Http::Request& request, - Wt::Http::Response& response, - const std::filesystem::path& path) +bool +FileResourceHandler::isFinished() const { - ContinuationData continuationData; - continuationData.path = path; - continuationData.offset = 0; - continuationData.beyondLastByte = 0; - - LMS_LOG(UTILS, DEBUG) << "Initial request for file '" << path << "'"; - - return handleRequestPiecewise(request, response, continuationData); + return _isFinished; } -std::optional -handleContinuationRequest(const Wt::Http::Request& request, - Wt::Http::Response& response, - const ContinuationData& continuationData) -{ - LMS_LOG(UTILS, DEBUG) << "Continuation request for file '" << continuationData.path << "', offset = " << continuationData.offset; - return handleRequestPiecewise(request, response, continuationData); -} - -} // ns FileResourceHandler - - diff --git a/src/libs/utils/impl/FileResourceHandler.hpp b/src/libs/utils/impl/FileResourceHandler.hpp new file mode 100644 index 00000000..befd3bdd --- /dev/null +++ b/src/libs/utils/impl/FileResourceHandler.hpp @@ -0,0 +1,43 @@ +/* + * 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 "utils/IResourceHandler.hpp" + +class FileResourceHandler final : public IResourceHandler +{ + public: + FileResourceHandler(const std::filesystem::path& filePath); + + private: + + void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + bool isFinished() const override; + + static constexpr std::size_t _chunkSize {262144}; + + std::filesystem::path _path; + ::uint64_t _beyondLastByte {}; + ::uint64_t _offset {}; + bool _isFinished {}; + +}; + diff --git a/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp new file mode 100644 index 00000000..8f69e18a --- /dev/null +++ b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp @@ -0,0 +1,28 @@ +/* + * 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 "utils/IResourceHandler.hpp" + +std::unique_ptr createFileResourceHandler(const std::filesystem::path& path); + diff --git a/src/libs/utils/include/utils/IResourceHandler.hpp b/src/libs/utils/include/utils/IResourceHandler.hpp new file mode 100644 index 00000000..b3fb1a5e --- /dev/null +++ b/src/libs/utils/include/utils/IResourceHandler.hpp @@ -0,0 +1,34 @@ +/* + * 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 + +// Helper class to serve a resource (must be saved as continuation data if not complete) +class IResourceHandler +{ + public: + virtual ~IResourceHandler() = default; + + virtual void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0; + virtual bool isFinished() const = 0; +}; + diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index c2986cec..2a2fa5a6 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -24,7 +24,7 @@ #include "av/AvInfo.hpp" #include "database/Track.hpp" -#include "utils/FileResourceHandler.hpp" +#include "utils/FileResourceHandlerCreator.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" #include "LmsApplication.hpp" @@ -88,29 +88,31 @@ void AudioFileResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - std::optional continuationData; + std::shared_ptr fileResourceHandler; + if (!request.continuation()) { auto trackPath {getTrackPathFromURLArgs(request)}; if (!trackPath) return; - continuationData = FileResourceHandler::handleInitialRequest(request, response, *trackPath); + fileResourceHandler = createFileResourceHandler(*trackPath); } else { - auto currentContinuationData {Wt::cpp17::any_cast(request.continuation()->data())}; - continuationData = FileResourceHandler::handleContinuationRequest(request, response, currentContinuationData); + fileResourceHandler = Wt::cpp17::any_cast>(request.continuation()->data()); } - if (continuationData) + fileResourceHandler->processRequest(request, response); + + if (!fileResourceHandler->isFinished()) { auto* continuation {response.createContinuation()}; - continuation->setData(*continuationData); + continuation->setData(fileResourceHandler); } - } + } // namespace UserInterface