From b96a16095a1ab17dd5d732fd794f0d6225a94e36 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 1 Oct 2023 20:48:07 +0200 Subject: [PATCH] Extracted covert art handling --- src/libs/subsonic/CMakeLists.txt | 4 +- src/libs/subsonic/impl/SubsonicResource.cpp | 33 +-- .../{Bookmark.cpp => Bookmarks.cpp} | 2 +- .../{Bookmark.hpp => Bookmarks.hpp} | 0 .../impl/entrypoints/MediaRetrieval.cpp | 200 ++++++++++++++++++ .../{Stream.hpp => MediaRetrieval.hpp} | 3 +- src/libs/subsonic/impl/entrypoints/Stream.cpp | 182 ---------------- 7 files changed, 209 insertions(+), 215 deletions(-) rename src/libs/subsonic/impl/entrypoints/{Bookmark.cpp => Bookmarks.cpp} (99%) rename src/libs/subsonic/impl/entrypoints/{Bookmark.hpp => Bookmarks.hpp} (100%) create mode 100644 src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp rename src/libs/subsonic/impl/entrypoints/{Stream.hpp => MediaRetrieval.hpp} (88%) delete mode 100644 src/libs/subsonic/impl/entrypoints/Stream.cpp diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 960ff5d9..873facdd 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,8 +1,8 @@ add_library(lmssubsonic SHARED - impl/entrypoints/Bookmark.cpp + impl/entrypoints/Bookmarks.cpp + impl/entrypoints/MediaRetrieval.cpp impl/entrypoints/Scan.cpp - impl/entrypoints/Stream.cpp impl/entrypoints/UserManagement.cpp impl/responses/Album.cpp impl/responses/Artist.cpp diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 9d7dbbe6..c551bf09 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -39,7 +39,6 @@ #include "services/database/User.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "services/cover/ICoverService.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" @@ -47,9 +46,9 @@ #include "utils/String.hpp" #include "utils/Utils.hpp" -#include "entrypoints/Bookmark.hpp" +#include "entrypoints/Bookmarks.hpp" +#include "entrypoints/MediaRetrieval.hpp" #include "entrypoints/Scan.hpp" -#include "entrypoints/Stream.hpp" #include "entrypoints/UserManagement.hpp" #include "responses/Artist.hpp" #include "responses/Album.hpp" @@ -1288,30 +1287,6 @@ handleNotImplemented(RequestContext&) throw NotImplementedGenericError {}; } -static -void -handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) -{ - // Mandatory params - const auto trackId {getParameterAs(context.parameters, "id")}; - const auto releaseId {getParameterAs(context.parameters, "id")}; - - if (!trackId && !releaseId) - throw BadParameterGenericError {"id"}; - - std::size_t size {getParameterAs(context.parameters, "size").value_or(1024)}; - size = ::Utils::clamp(size, std::size_t {32}, std::size_t {2048}); - - std::shared_ptr cover; - if (trackId) - cover = Service::get()->getFromTrack(*trackId, size); - else if (releaseId) - cover = Service::get()->getFromRelease(*releaseId, size); - - response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); - response.setMimeType(std::string {cover->getMimeType()}); -} - using RequestHandlerFunc = std::function; using CheckImplementedFunc = std::function; struct RequestEntryPointInfo @@ -1430,8 +1405,8 @@ using MediaRetrievalHandlerFunc = std::function mediaRetrievalHandlers { // Media retrieval - {"/download", Stream::handleDownload}, - {"/stream", Stream::handleStream}, + {"/download", handleDownload}, + {"/stream", handleStream}, {"/getCoverArt", handleGetCoverArt}, }; diff --git a/src/libs/subsonic/impl/entrypoints/Bookmark.cpp b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp similarity index 99% rename from src/libs/subsonic/impl/entrypoints/Bookmark.cpp rename to src/libs/subsonic/impl/entrypoints/Bookmarks.cpp index 6ef1d075..2e007cdf 100644 --- a/src/libs/subsonic/impl/entrypoints/Bookmark.cpp +++ b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "Bookmark.hpp" +#include "Bookmarks.hpp" #include "services/database/Session.hpp" #include "services/database/User.hpp" diff --git a/src/libs/subsonic/impl/entrypoints/Bookmark.hpp b/src/libs/subsonic/impl/entrypoints/Bookmarks.hpp similarity index 100% rename from src/libs/subsonic/impl/entrypoints/Bookmark.hpp rename to src/libs/subsonic/impl/entrypoints/Bookmarks.hpp diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp new file mode 100644 index 00000000..96e3436e --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -0,0 +1,200 @@ +/* + * 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 "MediaRetrieval.hpp" + +#include "av/TranscodeParameters.hpp" +#include "av/TranscodeResourceHandlerCreator.hpp" +#include "av/Types.hpp" +#include "services/cover/ICoverService.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "utils/IResourceHandler.hpp" +#include "utils/Logger.hpp" +#include "utils/FileResourceHandlerCreator.hpp" +#include "utils/Utils.hpp" +#include "ParameterParsing.hpp" +#include "SubsonicId.hpp" + +using namespace Database; + +namespace API::Subsonic +{ + namespace { + Av::Format userTranscodeFormatToAvFormat(AudioFormat format) + { + switch (format) + { + case AudioFormat::MP3: return Av::Format::MP3; + case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; + case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; + case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; + case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; + default: return Av::Format::OGG_OPUS; + } + } + + struct StreamParameters + { + Av::InputFileParameters inputFileParameters; + std::optional transcodeParameters; + bool estimateContentLength{}; + }; + + StreamParameters getStreamParameters(RequestContext& context) + { + // Mandatory params + const TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; + + // Optional params + std::optional maxBitRate{ getParameterAs(context.parameters, "maxBitRate") }; + std::optional format{ getParameterAs(context.parameters, "format") }; + bool estimateContentLength{ getParameterAs(context.parameters, "estimateContentLength").value_or(false) }; + + StreamParameters parameters; + + parameters.estimateContentLength = estimateContentLength; + + auto transaction{ context.dbSession.createSharedTransaction() }; + + { + auto track{ Track::find(context.dbSession, id) }; + if (!track) + throw RequestedDataNotFoundError{}; + + parameters.inputFileParameters.trackPath = track->getPath(); + parameters.inputFileParameters.duration = track->getDuration(); + } + + { + const User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + // format = "raw" => no transcode. Other format values will be ignored + const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() }; + if (transcode) + { + std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 }; + + // "If set to zero, no limit is imposed" + if (maxBitRate && *maxBitRate != 0) + bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate); + + Av::TranscodeParameters transcodeParameters; + + transcodeParameters.bitrate = bitRate * 1000; + transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat()); + transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) + + parameters.transcodeParameters = std::move(transcodeParameters); + } + } + + return parameters; + } + } + + void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) + { + std::shared_ptr resourceHandler; + + Wt::Http::ResponseContinuation* continuation{ request.continuation() }; + if (!continuation) + { + // Mandatory params + Database::TrackId id{ getMandatoryParameterAs(context.parameters, "id") }; + + std::filesystem::path trackPath; + { + auto transaction{ context.dbSession.createSharedTransaction() }; + + auto track{ Track::find(context.dbSession, id) }; + if (!track) + throw RequestedDataNotFoundError{}; + + trackPath = track->getPath(); + } + + resourceHandler = createFileResourceHandler(trackPath); + } + else + { + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + } + + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); + } + + void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) + { + std::shared_ptr resourceHandler; + + try + { + Wt::Http::ResponseContinuation* continuation = request.continuation(); + if (!continuation) + { + StreamParameters streamParameters{ getStreamParameters(context) }; + if (streamParameters.transcodeParameters) + resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); + else + resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); + } + else + { + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + } + + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); + } + catch (const Av::Exception& e) + { + LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what(); + } + } + + void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) + { + // Mandatory params + const auto trackId{ getParameterAs(context.parameters, "id") }; + const auto releaseId{ getParameterAs(context.parameters, "id") }; + + if (!trackId && !releaseId) + throw BadParameterGenericError{ "id" }; + + std::size_t size{ getParameterAs(context.parameters, "size").value_or(1024) }; + size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 }); + + std::shared_ptr cover; + if (trackId) + cover = Service::get()->getFromTrack(*trackId, size); + else if (releaseId) + cover = Service::get()->getFromRelease(*releaseId, size); + + response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); + response.setMimeType(std::string{ cover->getMimeType() }); + } + +} // namespace API::Subsonic diff --git a/src/libs/subsonic/impl/entrypoints/Stream.hpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.hpp similarity index 88% rename from src/libs/subsonic/impl/entrypoints/Stream.hpp rename to src/libs/subsonic/impl/entrypoints/MediaRetrieval.hpp index c2de5709..6033d290 100644 --- a/src/libs/subsonic/impl/entrypoints/Stream.hpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.hpp @@ -24,9 +24,10 @@ #include "RequestContext.hpp" -namespace API::Subsonic::Stream +namespace API::Subsonic { void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); + void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); } diff --git a/src/libs/subsonic/impl/entrypoints/Stream.cpp b/src/libs/subsonic/impl/entrypoints/Stream.cpp deleted file mode 100644 index 91c13eaf..00000000 --- a/src/libs/subsonic/impl/entrypoints/Stream.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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/TranscodeParameters.hpp" -#include "av/TranscodeResourceHandlerCreator.hpp" -#include "av/Types.hpp" -#include "services/database/Session.hpp" -#include "services/database/Track.hpp" -#include "services/database/User.hpp" -#include "utils/IResourceHandler.hpp" -#include "utils/Logger.hpp" -#include "utils/FileResourceHandlerCreator.hpp" -#include "utils/Utils.hpp" -#include "ParameterParsing.hpp" -#include "SubsonicId.hpp" - -using namespace Database; - -namespace API::Subsonic::Stream -{ - -static -Av::Format -userTranscodeFormatToAvFormat(AudioFormat format) -{ - switch (format) - { - case AudioFormat::MP3: return Av::Format::MP3; - case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; - case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; - case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; - case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; - default: return Av::Format::OGG_OPUS; - } -} - -struct StreamParameters -{ - Av::InputFileParameters inputFileParameters; - std::optional transcodeParameters; - bool estimateContentLength {}; -}; - -static -StreamParameters -getStreamParameters(RequestContext& context) -{ - // Mandatory params - const TrackId id {getMandatoryParameterAs(context.parameters, "id")}; - - // Optional params - std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; - std::optional format {getParameterAs(context.parameters, "format")}; - bool estimateContentLength {getParameterAs(context.parameters, "estimateContentLength").value_or(false)}; - - StreamParameters parameters; - - parameters.estimateContentLength = estimateContentLength; - - auto transaction {context.dbSession.createSharedTransaction()}; - - { - auto track {Track::find(context.dbSession, id)}; - if (!track) - throw RequestedDataNotFoundError {}; - - parameters.inputFileParameters.trackPath = track->getPath(); - parameters.inputFileParameters.duration = track->getDuration(); - } - - { - const User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - // format = "raw" => no transcode. Other format values will be ignored - const bool transcode {(!format || (*format != "raw")) && user->getSubsonicTranscodeEnable()}; - if (transcode) - { - std::size_t bitRate {user->getSubsonicTranscodeBitrate() / 1000}; - - // "If set to zero, no limit is imposed" - if (maxBitRate && *maxBitRate != 0) - bitRate = Utils::clamp(*maxBitRate, std::size_t {48}, bitRate); - - Av::TranscodeParameters transcodeParameters; - - transcodeParameters.bitrate = bitRate * 1000; - transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat()); - transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - - parameters.transcodeParameters = std::move(transcodeParameters); - } - } - - return parameters; -} - -void -handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) -{ - std::shared_ptr resourceHandler; - - Wt::Http::ResponseContinuation* continuation {request.continuation()}; - if (!continuation) - { - // Mandatory params - Database::TrackId id {getMandatoryParameterAs(context.parameters, "id")}; - - std::filesystem::path trackPath; - { - auto transaction {context.dbSession.createSharedTransaction()}; - - auto track {Track::find(context.dbSession, id)}; - if (!track) - throw RequestedDataNotFoundError {}; - - trackPath = track->getPath(); - } - - resourceHandler = createFileResourceHandler(trackPath); - } - else - { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); - } - - continuation = resourceHandler->processRequest(request, response); - if (continuation) - continuation->setData(resourceHandler); -} - -void -handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) -{ - std::shared_ptr resourceHandler; - - try - { - Wt::Http::ResponseContinuation* continuation = request.continuation(); - if (!continuation) - { - StreamParameters streamParameters {getStreamParameters(context)}; - if (streamParameters.transcodeParameters) - resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); - else - resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); - } - else - { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); - } - - continuation = resourceHandler->processRequest(request, response); - if (continuation) - continuation->setData(resourceHandler); - } - catch (const Av::Exception& e) - { - LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what(); - } -} - -} // namespace API::Subsonic::Stream