diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 8646d8bf..efd1c73c 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -26,6 +26,7 @@ #include "database/Track.hpp" #include "utils/Logger.hpp" +#include "utils/Random.hpp" namespace { @@ -52,37 +53,11 @@ Grabber::Grabber(const std::filesystem::path& execPath) void Grabber::setDefaultCover(const std::filesystem::path& p) { - std::unique_lock lock {_mutex}; - _defaultCover = std::make_unique(); if (!_defaultCover->load(p)) throw LmsException("Cannot read default cover file '" + p.string() + "'"); } -Image -Grabber::getDefaultCover(std::size_t size) -{ - LMS_LOG(COVER, DEBUG) << "Getting a default cover using size = " << size; - std::unique_lock lock {_mutex}; - - auto it = _defaultCovers.find(size); - if (it == _defaultCovers.end()) - { - Image cover = *_defaultCover; - - LMS_LOG(COVER, DEBUG) << "default cover size = " << cover.getSize().width << " x " << cover.getSize().height; - - LMS_LOG(COVER, DEBUG) << "Scaling cover to size = " << size; - cover.scale(Geometry{size, size}); - LMS_LOG(COVER, DEBUG) << "Scaling DONE"; - auto res = _defaultCovers.insert(std::make_pair(size, cover)); - assert(res.second); - it = res.first; - } - - return it->second; -} - static std::optional getFromAvMediaFile(const Av::MediaFile& input) { @@ -173,7 +148,11 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st { using namespace Database; - std::optional cover; + const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Track, trackId, size}; + std::optional cover {loadFromCache(cacheEntryDesc)}; + + if (cover) + return *cover; bool hasCover {}; bool isMultiDisc {}; @@ -182,7 +161,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st { auto transaction {dbSession.createSharedTransaction()}; - Track::pointer track = Track::getById(dbSession, trackId); + const Track::pointer track {Track::getById(dbSession, trackId)}; if (track) { hasCover = track->hasCover(); @@ -207,9 +186,11 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st } if (!cover) - cover = getDefaultCover(size); - else - cover->scale(Geometry {size, size}); + cover = *_defaultCover; + + cover->scale(Geometry {size, size}); + + saveToCache(cacheEntryDesc, *cover); return *cover; } @@ -218,32 +199,53 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st Image Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t size) { - std::optional cover; + const CacheEntryDesc cacheEntryDesc {CacheEntryDesc::Type::Release, releaseId, size}; + std::optional cover {loadFromCache(cacheEntryDesc)}; + + if (cover) + return *cover; std::optional trackId; { auto transaction {session.createSharedTransaction()}; - auto release {Database::Release::getById(session, releaseId)}; + const auto release {Database::Release::getById(session, releaseId)}; if (release) { - auto tracks {release->getTracks()}; + const auto tracks {release->getTracks()}; if (!tracks.empty()) trackId = tracks.front().id(); } } if (trackId) - return getFromTrack(session, *trackId, size); - - if (!cover) - cover = getDefaultCover(size); + { + cover = getFromTrack(session, *trackId, size); + } else + { + if (!cover) + cover = *_defaultCover; + cover->scale(Geometry {size, size}); + } + + saveToCache(cacheEntryDesc, *cover); return *cover; } +void +Grabber::flushCache() +{ + std::unique_lock lock {_cacheMutex}; + + LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses; + _cacheHits = 0; + _cacheMisses = 0; + _cache.clear(); +} + std::vector Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Format format, std::size_t width) { @@ -262,5 +264,32 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, return cover.save(format); } +void +Grabber::saveToCache(const CacheEntryDesc& entryDesc, const Image& image) +{ + std::unique_lock lock {_cacheMutex}; + + if (_cache.size() >= _maxCacheEntries) + _cache.erase(Random::pickRandom(_cache)); + + _cache[entryDesc] = image; +} + +std::optional +Grabber::loadFromCache(const CacheEntryDesc& entryDesc) +{ + std::shared_lock lock {_cacheMutex}; + + auto it {_cache.find(entryDesc)}; + if (it == std::cend(_cache)) + { + ++_cacheMisses; + return std::nullopt; + } + + ++_cacheHits; + return it->second; +} + } // namespace CoverArt diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index df932315..1e6df791 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -20,10 +20,10 @@ #pragma once #include -#include -#include +#include #include #include +#include #include "cover/ICoverArtGrabber.hpp" #include "database/Types.hpp" @@ -36,7 +36,48 @@ namespace Database namespace CoverArt { + struct CacheEntryDesc + { + enum class Type + { + Track, + Release, + }; + Type type; + Database::IdType id; + std::size_t size; + + bool operator==(const CacheEntryDesc& other) const + { + return type == other.type + && id == other.id + && size == other.size; + } + }; + +} // ns CoverArt + +namespace std +{ + + template<> + class hash + { + public: + size_t operator()(const CoverArt::CacheEntryDesc& e) const + { + size_t h = std::hash()(static_cast(e.type)); + h ^= std::hash()(e.id) << 1; + h ^= std::hash()(e.size) << 1; + return h; + } + }; + +} // ns std + +namespace CoverArt +{ class Grabber : public IGrabber { public: @@ -47,12 +88,14 @@ namespace CoverArt Grabber(Grabber&&) = delete; Grabber& operator=(Grabber&&) = delete; + private: + void setDefaultCover(const std::filesystem::path& defaultCoverPath) override; std::vector getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) override; std::vector getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) override; + void flushCache() override; - private: Image getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size); Image getFromRelease(Database::Session& dbSession, Database::IdType releaseId, std::size_t size); @@ -60,14 +103,20 @@ namespace CoverArt std::optional getFromTrack(const std::filesystem::path& path) const; std::vector getCoverPaths(const std::filesystem::path& directoryPath) const; std::optional getFromDirectory(const std::filesystem::path& path) const; - Image getDefaultCover(std::size_t size); - std::mutex _mutex; - std::unique_ptr _defaultCover; - std::map _defaultCovers; + std::unique_ptr _defaultCover; // unique_ptr to defer initializing + std::shared_mutex _cacheMutex; + std::unordered_map _cache; + std::size_t _cacheMisses {}; + std::size_t _cacheHits {}; + + void saveToCache(const CacheEntryDesc& entryDesc, const Image& image); + std::optional loadFromCache(const CacheEntryDesc& entryDesc); + + static inline constexpr std::size_t _maxCacheEntries {300}; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize - static inline const std::size_t _maxFileSize {10000000}; + static inline constexpr std::size_t _maxFileSize {10000000}; static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize }; diff --git a/src/libs/cover/include/cover/ICoverArtGrabber.hpp b/src/libs/cover/include/cover/ICoverArtGrabber.hpp index c62e3955..e9cda763 100644 --- a/src/libs/cover/include/cover/ICoverArtGrabber.hpp +++ b/src/libs/cover/include/cover/ICoverArtGrabber.hpp @@ -40,6 +40,8 @@ class IGrabber virtual std::vector getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) = 0; virtual std::vector getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) = 0; + + virtual void flushCache() = 0; }; std::unique_ptr createGrabber(const std::filesystem::path& execPath); diff --git a/src/lms/main.cpp b/src/lms/main.cpp index bbaa1985..0911e8db 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -148,6 +148,9 @@ int main(int argc, char* argv[]) Scanner::IMediaScanner& mediaScanner {ServiceProvider::assign(Scanner::createMediaScanner(database))}; Recommendation::IEngine& recommendationEngine {ServiceProvider::assign(Recommendation::createEngine(database))}; + CoverArt::IGrabber& coverArtGrabber {ServiceProvider::assign(CoverArt::createGrabber(argv[0]))}; + coverArtGrabber.setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); + mediaScanner.scanComplete().connect([&]() { auto status = mediaScanner.getStatus(); @@ -161,11 +164,11 @@ int main(int argc, char* argv[]) { LMS_LOG(MAIN, INFO) << "Scanner did not change files, not reloading the recommendation engine..."; } + // Flush cover cache even if no changes: + // covers may be external files that changed and we don't keep track of them + coverArtGrabber.flushCache(); }); - CoverArt::IGrabber& coverArtGrabber {ServiceProvider::assign(CoverArt::createGrabber(argv[0]))}; - coverArtGrabber.setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); - API::Subsonic::SubsonicResource subsonicResource {database}; // bind API resources