diff --git a/conf/lms.conf b/conf/lms.conf index 07e7943f..02985108 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -61,7 +61,10 @@ api-subsonic = true; # Use this list to make the reported server version to 1.12.0 depending on the client's name # Main usage is to make auto detections for the 'p' (password) parameter work -api-subsonic-report-old-server-protocol = ("DSub"); +api-subsonic-old-server-protocol-clients = ("DSub"); + +# List of clients for whom a default cover is served (as they do not have their own) +api-subsonic-default-cover-clients = ("DSub", "substreamer"); # List of clients for whom open subsonic extensions and extra fields are disabled api-open-subsonic-disabled-clients = ("DSub"); diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 0912828e..91c8c612 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -345,9 +345,6 @@ namespace Cover } } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); @@ -397,9 +394,6 @@ namespace Cover cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index f23d437a..dd5b82c5 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -35,103 +35,97 @@ namespace Database { - class Session; + class Session; } namespace Av { - class IAudioFile; + class IAudioFile; } namespace Cover { - struct CacheEntryDesc - { - std::variant id; - std::size_t size; - - bool operator==(const CacheEntryDesc& other) const - { - return id == other.id - && size == other.size; - } - }; + struct CacheEntryDesc + { + std::variant id; + std::size_t size; + bool operator==(const CacheEntryDesc& other) const + { + return id == other.id + && size == other.size; + } + }; } // ns Cover namespace std { - - template<> - class hash - { - public: - size_t operator()(const Cover::CacheEntryDesc& e) const - { - size_t h {}; - std::visit([&](auto id) - { - using IdType = std::decay_t; - h ^= std::hash()(id); - }, e.id); - h ^= std::hash()(e.size) << 1; - return h; - } - }; + template<> + class hash + { + public: + size_t operator()(const Cover::CacheEntryDesc& e) const + { + size_t h{}; + std::visit([&](auto id) + { + using IdType = std::decay_t; + h ^= std::hash()(id); + }, e.id); + h ^= std::hash()(e.size) << 1; + return h; + } + }; } // ns std namespace Cover { - class CoverService : public ICoverService - { - public: - CoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + class CoverService : public ICoverService + { + public: + CoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); - CoverService(const CoverService&) = delete; - CoverService& operator=(const CoverService&) = delete; - CoverService(CoverService&&) = delete; - CoverService& operator=(CoverService&&) = delete; + CoverService(const CoverService&) = delete; + CoverService& operator=(const CoverService&) = delete; - private: - std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; - std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; - void flushCache() override; - void setJpegQuality(unsigned quality) override; + private: + std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; + std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; + std::shared_ptr getDefault(Image::ImageSize width) override; + void flushCache() override; + void setJpegQuality(unsigned quality) override; - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); - std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; - std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); + std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; + std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; - std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; - std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; - std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - std::shared_ptr getDefault(Image::ImageSize width); + std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; + std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; + std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; + std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - bool checkCoverFile(const std::filesystem::path& directoryPath) const; + bool checkCoverFile(const std::filesystem::path& directoryPath) const; - Database::Db& _db; + Database::Db& _db; - std::shared_mutex _cacheMutex; - std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; - std::atomic _cacheMisses {}; - std::atomic _cacheHits {}; - std::size_t _cacheSize {}; + std::shared_mutex _cacheMutex; + std::unordered_map> _cache; + std::unordered_map> _defaultCoverCache; + std::atomic _cacheMisses{}; + std::atomic _cacheHits{}; + std::size_t _cacheSize{}; - void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); - std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - const std::filesystem::path _defaultCoverPath; - const std::size_t _maxCacheSize; - static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize - const std::size_t _maxFileSize; - const std::vector _preferredFileNames; - unsigned _jpegQuality; - }; + const std::filesystem::path _defaultCoverPath; + const std::size_t _maxCacheSize; + static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize + const std::size_t _maxFileSize; + const std::vector _preferredFileNames; + unsigned _jpegQuality; + }; } // namespace Cover diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 0421f589..7ce4a2df 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -28,27 +28,27 @@ namespace Database { - class Db; + class Db; } namespace Cover { - class ICoverService - { - public: - virtual ~ICoverService() = default; + class ICoverService + { + public: + virtual ~ICoverService() = default; - virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; - virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; - virtual void flushCache() = 0; + virtual std::shared_ptr getDefault(Image::ImageSize width) = 0; - virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 - }; + virtual void flushCache() = 0; - std::unique_ptr createCoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 + }; + + std::unique_ptr createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); } // namespace CoverArt diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 05de5b5a..61b23f0a 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -42,6 +42,7 @@ namespace API::Subsonic ClientInfo clientInfo; ProtocolVersion serverProtocolVersion; bool enableOpenSubsonic{ true }; + bool enableDefaultCover{ }; }; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e5b0bb0d..4fc91b2a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -66,7 +66,7 @@ namespace API::Subsonic { std::unordered_map res; - Service::get()->visitStrings("api-subsonic-report-old-server-protocol", + Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", [&](std::string_view client) { res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 }); @@ -88,6 +88,19 @@ namespace API::Subsonic return res; } + std::unordered_set readDefaultCoverClients() + { + std::unordered_set res; + + Service::get()->visitStrings("api-subsonic-default-cover-clients", + [&](std::string_view client) + { + res.emplace(std::string{ client }); + }, { "DSub", "substreamer" }); + + return res; + } + std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { auto censorValue = [](const std::string& type, const std::string& value) -> std::string @@ -266,6 +279,7 @@ namespace API::Subsonic SubsonicResource::SubsonicResource(Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } + , _defaultCoverClients{ readDefaultCoverClients() } , _db{ db } { } @@ -378,8 +392,9 @@ namespace API::Subsonic const ClientInfo clientInfo{ getClientInfo(parameters) }; const Database::UserId userId{ authenticateUser(request, clientInfo) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; + bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) }; - return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic }; + return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover }; } Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index 63a04a5c..24c7025a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -53,6 +53,8 @@ namespace API::Subsonic const std::unordered_map _serverProtocolVersionsByClient; const std::unordered_set _openSubsonicDisabledClients; + const std::unordered_set _defaultCoverClients; + Database::Db& _db; }; diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index cddc758c..43729bcb 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -207,8 +207,9 @@ namespace API::Subsonic // Mandatory params const auto trackId{ getParameterAs(context.parameters, "id") }; const auto releaseId{ getParameterAs(context.parameters, "id") }; + const auto artistId{ getParameterAs(context.parameters, "id") }; - if (!trackId && !releaseId) + if (!trackId && !releaseId && !artistId) throw BadParameterGenericError{ "id" }; std::size_t size{ getParameterAs(context.parameters, "size").value_or(1024) }; @@ -219,6 +220,21 @@ namespace API::Subsonic cover = Service::get()->getFromTrack(*trackId, size); else if (releaseId) cover = Service::get()->getFromRelease(*releaseId, size); + else if (artistId) + { + // TODO handle a placeholder for artists + response.setStatus(404); + return; + } + + if (!cover && context.enableDefaultCover) + cover = Service::get()->getDefault(size); + + if (!cover) + { + response.setStatus(404); + return; + } response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); response.setMimeType(std::string{ cover->getMimeType() }); diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index 6b4c5a42..c9cdd0ab 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -35,88 +35,89 @@ namespace UserInterface { -CoverResource::CoverResource() -{ - LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) - { - if (stats.nbChanges()) - setChanged(); - }); -} + CoverResource::CoverResource() + { + LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) + { + if (stats.nbChanges()) + setChanged(); + }); + } -CoverResource::~CoverResource() -{ - beingDeleted(); -} + CoverResource::~CoverResource() + { + beingDeleted(); + } -std::string -CoverResource::getReleaseUrl(Database::ReleaseId releaseId, Size size) const -{ - return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast(size)); -} + std::string CoverResource::getReleaseUrl(Database::ReleaseId releaseId, Size size) const + { + return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast(size)); + } -std::string -CoverResource::getTrackUrl(Database::TrackId trackId, Size size) const -{ - return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast(size)); -} + std::string CoverResource::getTrackUrl(Database::TrackId trackId, Size size) const + { + return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast(size)); + } -void -CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) -{ - // Retrieve parameters - const std::string *trackIdStr = request.getParameter("trackid"); - const std::string *releaseIdStr = request.getParameter("releaseid"); - const std::string *sizeStr = request.getParameter("size"); + void CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) + { + // Retrieve parameters + const std::string* trackIdStr = request.getParameter("trackid"); + const std::string* releaseIdStr = request.getParameter("releaseid"); + const std::string* sizeStr = request.getParameter("size"); - // Mandatory parameter size - if (!sizeStr) - { - LOG(DEBUG) << "no size provided!"; - return; - } + // 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; - } + const auto size{ StringUtils::readAs(*sizeStr) }; + if (!size || *size > maxSize) + { + LOG(DEBUG) << "invalid size provided!"; + return; + } - std::shared_ptr cover; + std::shared_ptr cover; - if (trackIdStr) - { - LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; + if (trackIdStr) + { + LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; - const std::optional trackId {StringUtils::readAs(*trackIdStr)}; - if (!trackId) - { - LOG(DEBUG) << "track not found"; - return; - } + const std::optional trackId{ StringUtils::readAs(*trackIdStr) }; + if (!trackId) + { + LOG(DEBUG) << "track not found"; + return; + } - cover = Service::get()->getFromTrack(*trackId, *size); - } - else if (releaseIdStr) - { - LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size; + cover = Service::get()->getFromTrack(*trackId, *size); + if (!cover) + cover = Service::get()->getDefault(*size); + } + else if (releaseIdStr) + { + LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size; - const std::optional releaseId {StringUtils::readAs(*releaseIdStr)}; - if (!releaseId) - return; + const std::optional releaseId{ StringUtils::readAs(*releaseIdStr) }; + if (!releaseId) + return; - cover = Service::get()->getFromRelease(*releaseId, *size); - } - else - { - LOG(DEBUG) << "No track or release provided"; - return; - } + cover = Service::get()->getFromRelease(*releaseId, *size); + if (!cover) + cover = Service::get()->getDefault(*size); + } + else + { + LOG(DEBUG) << "No track or release provided"; + return; + } - response.setMimeType(std::string {cover->getMimeType()}); + response.setMimeType(std::string{ cover->getMimeType() }); - response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); -} + response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); + } } // namespace UserInterface