diff --git a/approot/artist.xml b/approot/artist.xml index 99cc5733..13d24a6a 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -5,7 +5,7 @@
- ${artwork class="Lms-cursor-pointer rounded d-block mx-auto"} + ${artwork class="rounded d-block mx-auto"}

${name}

diff --git a/approot/release.xml b/approot/release.xml index caf2b811..92160133 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -9,7 +9,7 @@
- ${cover class="Lms-cursor-pointer rounded d-block mx-auto"} + ${artwork class="rounded d-block mx-auto"}

${name}${}[${comment}]${}

@@ -55,9 +55,9 @@

- ${} -
${image class="Lms-cursor-pointer"}
- ${
} + ${} +
${artwork class="Lms-cursor-pointer"}
+ ${
}
${disc-title}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} diff --git a/src/libs/database/impl/Artwork.cpp b/src/libs/database/impl/Artwork.cpp index 92929873..e9264f06 100644 --- a/src/libs/database/impl/Artwork.cpp +++ b/src/libs/database/impl/Artwork.cpp @@ -19,6 +19,8 @@ #include "database/Artwork.hpp" +#include + #include "database/Image.hpp" #include "database/Session.hpp" #include "database/TrackEmbeddedImage.hpp" @@ -74,4 +76,15 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.image_id = ?").bind(id)); } + Wt::WDateTime Artwork::getLastWrittenTime() const + { + auto query{ session()->query("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") }; + query.leftJoin("image ON artwork.image_id = image.id"); + query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id"); + query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id"); + query.leftJoin("track ON track.id = track_embedded_image_link.track_id"); + query.where("artwork.id = ?").bind(getId()); + + return utils::fetchQuerySingleResult(query); + } } // namespace lms::db diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index b27575c4..ae308b53 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -374,11 +374,21 @@ namespace lms::db return _preferredArtwork; } + ArtworkId Track::getPreferredArtworkId() const + { + return _preferredArtwork.id(); + } + ObjectPtr Track::getPreferredMediaArtwork() const { return _preferredMediaArtwork; } + ArtworkId Track::getPreferredMediaArtworkId() const + { + return _preferredMediaArtwork.id(); + } + RangeResults Track::findIds(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); diff --git a/src/libs/database/include/database/Artwork.hpp b/src/libs/database/include/database/Artwork.hpp index f373c502..581dcc00 100644 --- a/src/libs/database/include/database/Artwork.hpp +++ b/src/libs/database/include/database/Artwork.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include "database/ArtworkId.hpp" #include "database/ImageId.hpp" @@ -46,6 +47,7 @@ namespace lms::db // getters TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); } ImageId getImageId() const { return _image.id(); } + Wt::WDateTime getLastWrittenTime() const; template void persist(Action& a) diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index e56e628e..f7b4cf80 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -306,7 +306,9 @@ namespace lms::db ObjectPtr getMediaLibrary() const; ObjectPtr getDirectory() const; ObjectPtr getPreferredArtwork() const; + ArtworkId getPreferredArtworkId() const; ObjectPtr getPreferredMediaArtwork() const; + ArtworkId getPreferredMediaArtworkId() const; std::vector>> getClusterGroups(const std::vector& clusterTypes, std::size_t size) const; diff --git a/src/libs/database/test/Artwork.cpp b/src/libs/database/test/Artwork.cpp new file mode 100644 index 00000000..832aba0d --- /dev/null +++ b/src/libs/database/test/Artwork.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025of 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 "Common.hpp" + +#include "database/Artwork.hpp" +#include "database/Image.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" + +namespace lms::db::tests +{ + using ScopedArtwork = ScopedEntity; + using ScopedImage = ScopedEntity; + using ScopedTrackEmbeddedImage = ScopedEntity; + + TEST_F(DatabaseFixture, Artwork_image) + { + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artwork::getCount(session), 0); + } + + ScopedImage image{ session, "/MyImage" }; + ScopedArtwork artwork{ session, image.lockAndGet() }; + + const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artwork::getCount(session), 1); + } + + { + auto transaction{ session.createWriteTransaction() }; + image.get().modify()->setLastWriteTime(dateTime); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime); + } + } + + TEST_F(DatabaseFixture, Artwork_trackEmbeddedImage) + { + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artwork::getCount(session), 0); + } + + ScopedTrackEmbeddedImage image{ session }; + ScopedTrack track{ session }; + ScopedArtwork artwork{ session, image.lockAndGet() }; + + const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track.get(), image.get()); + track.get().modify()->setLastWriteTime(dateTime); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime); + } + } +} // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index b3e1fc42..756b0a2b 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(test-database Artist.cpp ArtistInfo.cpp + Artwork.cpp AuthToken.cpp Cluster.cpp Common.cpp diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 1ac708a9..fa3a7467 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -26,12 +26,13 @@ #include "database/Artwork.hpp" #include "database/Db.hpp" #include "database/Image.hpp" +#include "database/ImageId.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackEmbeddedImage.hpp" #include "database/TrackEmbeddedImageLink.hpp" -#include "database/Types.hpp" +#include "database/TrackList.hpp" #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" @@ -87,12 +88,12 @@ namespace lms::artwork return image; } - std::shared_ptr ArtworkService::getDefaultReleaseCover() + std::shared_ptr ArtworkService::getDefaultReleaseArtwork() { return _defaultReleaseCover; } - std::shared_ptr ArtworkService::getDefaultArtistImage() + std::shared_ptr ArtworkService::getDefaultArtistArtwork() { return _defaultArtistImage; } @@ -136,150 +137,104 @@ namespace lms::artwork return image; } - ArtworkService::ImageFindResult ArtworkService::findArtistImage(db::ArtistId artistId) + db::ArtworkId ArtworkService::findTrackListImage(db::TrackListId trackListId) { + db::ArtworkId artworkId; + + // Iterate over all tracks and stop when we find an artwork db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - ImageFindResult res; + db::TrackList::pointer trackList{ db::TrackList::find(session, trackListId) }; + if (!trackList) + return artworkId; - const db::Artist::pointer artist{ db::Artist::find(session, artistId) }; - if (!artist) - return res; + const auto entries{ trackList->getEntries(db::Range{ 0, 10 }) }; + for (const auto& entry : entries.results) + { + const auto track{ entry->getTrack() }; + if (track->getPreferredMediaArtworkId().isValid()) + { + artworkId = track->getPreferredMediaArtworkId(); + break; // stop iteration + } - const db::Artwork::pointer artwork{ artist->getPreferredArtwork() }; - if (artwork && artwork->getImageId().isValid()) - res = artwork->getImageId(); - else if (artwork && artwork->getTrackEmbeddedImageId().isValid()) - res = artwork->getTrackEmbeddedImageId(); + if (track->getPreferredArtworkId().isValid()) + { + artworkId = track->getPreferredArtworkId(); + break; // stop iteration + } + } - return res; + return artworkId; } - ArtworkService::ImageFindResult ArtworkService::findTrackImage(db::TrackId trackId) + std::shared_ptr ArtworkService::getImage(db::ArtworkId artworkId, std::optional width) { - ImageFindResult res; + const ImageCache::EntryDesc cacheEntryDesc{ artworkId, width }; - db::Session& session{ _db.getTLSSession() }; - auto transaction{ session.createReadTransaction() }; + std::shared_ptr image{ _cache.getImage(cacheEntryDesc) }; + if (image) + return image; - const db::Track::pointer track{ db::Track::find(session, trackId) }; - if (!track) - return res; + db::TrackEmbeddedImageId trackEmbeddedImageId; + db::ImageId imageId; - const db::Artwork::pointer artwork{ track->getPreferredArtwork() }; - if (artwork && artwork->getImageId().isValid()) - res = artwork->getImageId(); - else if (artwork && artwork->getTrackEmbeddedImageId().isValid()) - res = artwork->getTrackEmbeddedImageId(); + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; - return res; - } + db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) }; + if (artwork) + { + trackEmbeddedImageId = artwork->getTrackEmbeddedImageId(); + imageId = artwork->getImageId(); + } + } - ArtworkService::ImageFindResult ArtworkService::findTrackMediaImage(db::TrackId trackId) - { - db::Session& session{ _db.getTLSSession() }; - auto transaction{ session.createReadTransaction() }; - ImageFindResult res; + if (trackEmbeddedImageId.isValid()) + image = getTrackEmbeddedImage(trackEmbeddedImageId, width); + else if (imageId.isValid()) + image = getImage(imageId, width); - const db::Track::pointer track{ db::Track::find(session, trackId) }; - if (!track) - return res; + if (image) + _cache.addImage(cacheEntryDesc, image); - const db::Artwork::pointer artwork{ track->getPreferredMediaArtwork() }; - if (artwork && artwork->getImageId().isValid()) - res = artwork->getImageId(); - else if (artwork && artwork->getTrackEmbeddedImageId().isValid()) - res = artwork->getTrackEmbeddedImageId(); - - return res; - } - - ArtworkService::ImageFindResult ArtworkService::findReleaseImage(db::ReleaseId releaseId) - { - db::Session& session{ _db.getTLSSession() }; - auto transaction{ session.createReadTransaction() }; - - ImageFindResult res; - const db::Release::pointer release{ db::Release::find(session, releaseId) }; - if (!release) - return res; - - const db::Artwork::pointer artwork{ release->getPreferredArtwork() }; - if (artwork && artwork->getImageId().isValid()) - res = artwork->getImageId(); - else if (artwork && artwork->getTrackEmbeddedImageId().isValid()) - res = artwork->getTrackEmbeddedImageId(); - - return res; - } - - ArtworkService::ImageFindResult ArtworkService::findTrackListImage(db::TrackListId trackListId) - { - db::Session& session{ _db.getTLSSession() }; - auto transaction{ session.createReadTransaction() }; - - ImageFindResult res; - - db::TrackEmbeddedImage::FindParameters params; - params.setTrackList(trackListId); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { - res = image->getId(); - }); - - // TODO fallback on release image if not found - return res; + return image; } std::shared_ptr ArtworkService::getImage(db::ImageId imageId, std::optional width) { - const ImageCache::EntryDesc cacheEntryDesc{ imageId, width }; - - std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; - if (cover) - return cover; - std::filesystem::path imageFile; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; const db::Image::pointer image{ db::Image::find(session, imageId) }; - if (image) - imageFile = image->getAbsoluteFilePath(); + if (!image) + return nullptr; + + imageFile = image->getAbsoluteFilePath(); } - cover = getFromImageFile(imageFile, width); - if (cover) - _cache.addImage(cacheEntryDesc, cover); - - return cover; + return getFromImageFile(imageFile, width); } std::shared_ptr ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) { - const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width }; - - std::shared_ptr image{ _cache.getImage(cacheEntryDesc) }; - if (image) - return image; + std::shared_ptr image; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; + // TODO: could be put outside transaction db::TrackEmbeddedImageLink::find(session, trackEmbeddedImageId, [&](const db::TrackEmbeddedImageLink::pointer& link) { if (!image) image = getTrackImage(link->getTrack()->getAbsoluteFilePath(), link->getIndex(), width); }); } - if (image) - _cache.addImage(cacheEntryDesc, image); - return image; } diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index 4bc43992..a0aa24d5 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -22,6 +22,8 @@ #include #include +#include "database/ImageId.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "services/artwork/IArtworkService.hpp" #include "ImageCache.hpp" @@ -47,21 +49,19 @@ namespace lms::artwork ArtworkService& operator=(const ArtworkService&) = delete; private: - ImageFindResult findArtistImage(db::ArtistId artistId) override; - ImageFindResult findTrackImage(db::TrackId trackId) override; - ImageFindResult findTrackMediaImage(db::TrackId trackId) override; - ImageFindResult findReleaseImage(db::ReleaseId releaseId) override; - ImageFindResult findTrackListImage(db::TrackListId trackListId) override; + db::ArtworkId findTrackListImage(db::TrackListId trackListId) override; - std::shared_ptr getImage(db::ImageId imageId, std::optional width) override; - std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) override; + std::shared_ptr getImage(db::ArtworkId artworkId, std::optional width) override; - std::shared_ptr getDefaultReleaseCover() override; - std::shared_ptr getDefaultArtistImage() override; + std::shared_ptr getDefaultReleaseArtwork() override; + std::shared_ptr getDefaultArtistArtwork() override; void flushCache() override; void setJpegQuality(unsigned quality) override; + std::shared_ptr getImage(db::ImageId imageId, std::optional width); + std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width); + std::unique_ptr getFromImageFile(const std::filesystem::path& p, std::optional width) const; std::unique_ptr getTrackImage(const std::filesystem::path& path, std::size_t index, std::optional width) const; diff --git a/src/libs/services/artwork/impl/ImageCache.hpp b/src/libs/services/artwork/impl/ImageCache.hpp index 37570ff9..995e67f7 100644 --- a/src/libs/services/artwork/impl/ImageCache.hpp +++ b/src/libs/services/artwork/impl/ImageCache.hpp @@ -25,10 +25,8 @@ #include #include #include -#include -#include "database/ImageId.hpp" -#include "database/TrackEmbeddedImageId.hpp" +#include "database/ArtworkId.hpp" #include "image/IEncodedImage.hpp" namespace lms::artwork @@ -40,8 +38,7 @@ namespace lms::artwork struct EntryDesc { - using VariantType = std::variant; - VariantType id; + db::ArtworkId id; std::optional size; bool operator==(const EntryDesc& other) const = default; @@ -63,13 +60,13 @@ namespace lms::artwork std::size_t operator()(const EntryDesc& entry) const { assert(entry.size); // should not cache unresized images - return std::hash{}(entry.id) ^ std::hash{}(*entry.size); + return std::hash{}(entry.id) ^ std::hash{}(*entry.size); } }; std::unordered_map, EntryHasher> _cache; std::size_t _cacheSize{}; - mutable std::atomic _cacheMisses{}; - mutable std::atomic _cacheHits{}; + mutable std::atomic _cacheMisses; + mutable std::atomic _cacheHits; }; } // namespace lms::artwork \ No newline at end of file diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index ff4933e0..4d8c1605 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -22,13 +22,8 @@ #include #include #include -#include -#include "database/ArtistId.hpp" -#include "database/ImageId.hpp" -#include "database/ReleaseId.hpp" -#include "database/TrackEmbeddedImageId.hpp" -#include "database/TrackId.hpp" +#include "database/ArtworkId.hpp" #include "database/TrackListId.hpp" #include "image/IEncodedImage.hpp" @@ -44,27 +39,15 @@ namespace lms::artwork public: virtual ~IArtworkService() = default; - // Helpers to find artworks - using ImageFindResult = std::variant; - virtual ImageFindResult findArtistImage(db::ArtistId artistId) = 0; + // Helpers to get preferred artworks + virtual db::ArtworkId findTrackListImage(db::TrackListId trackListId) = 0; - // Will get Disc/Media artwork if available, otherwise, will fallback on release artwork - virtual ImageFindResult findTrackImage(db::TrackId trackId) = 0; - - // Will get Disc/Media artwork if available, no fallback - virtual ImageFindResult findTrackMediaImage(db::TrackId trackId) = 0; - - // Will get Release if available, otherwise, will fallback on embedded artworks - virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0; - virtual ImageFindResult findTrackListImage(db::TrackListId trackListId) = 0; - - // Image retrieval - virtual std::shared_ptr getImage(db::ImageId imageId, std::optional width) = 0; - virtual std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) = 0; + // Image retrieval, no width means original size + virtual std::shared_ptr getImage(db::ArtworkId artworkId, std::optional width) = 0; // Svg images don't have image "size" - virtual std::shared_ptr getDefaultReleaseCover() = 0; - virtual std::shared_ptr getDefaultArtistImage() = 0; + virtual std::shared_ptr getDefaultReleaseArtwork() = 0; + virtual std::shared_ptr getDefaultArtistArtwork() = 0; virtual void flushCache() = 0; diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index 3c4f1c33..b6ce83d2 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -60,14 +60,14 @@ namespace lms::scanner return false; } - struct ReleaseImageAssociation + struct ReleaseArtworkAssociation { - db::ReleaseId releaseId; + db::Release::pointer release; ReleaseArtwork preferredArtwork; }; - using ReleaseImageAssociationContainer = std::deque; + using ReleaseArtworkAssociationContainer = std::deque; - struct SearchReleaseImageContext + struct SearchReleaseArtworkContext { db::Session& session; db::ReleaseId lastRetrievedReleaseId; @@ -75,7 +75,7 @@ namespace lms::scanner const std::vector& releaseImageFileNames; }; - db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath) + db::Image::pointer findImageInDirectory(SearchReleaseArtworkContext& searchContext, const std::filesystem::path& directoryPath) { db::Image::pointer image; @@ -101,7 +101,7 @@ namespace lms::scanner return image; } - db::Image::pointer computePreferredReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + db::Image::pointer computePreferredReleaseImage(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release) { db::Image::pointer image; @@ -149,7 +149,7 @@ namespace lms::scanner return image; } - ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release) { const db::Image::pointer image{ computePreferredReleaseImage(searchContext, release) }; if (image) @@ -187,7 +187,7 @@ namespace lms::scanner return ReleaseArtwork{}; } - bool fetchNextReleaseArtworksToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations) + bool fetchNextReleaseArtworksToUpdate(SearchReleaseArtworkContext& searchContext, ReleaseArtworkAssociationContainer& ReleaseArtworkAssociations) { const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId }; @@ -201,7 +201,7 @@ namespace lms::scanner const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() }; if (!isSameArtwork(preferredArtwork, currentPreferredArtwork)) - releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), preferredArtwork }); + ReleaseArtworkAssociations.push_back(ReleaseArtworkAssociation{ release, preferredArtwork }); searchContext.processedReleaseCount++; }); } @@ -209,16 +209,14 @@ namespace lms::scanner return releaseId != searchContext.lastRetrievedReleaseId; } - void updateReleaseArtwork(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation) + void updateReleaseArtwork(db::Session& session, const ReleaseArtworkAssociation& ReleaseArtworkAssociation) { - db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) }; - assert(release); + db::Release::pointer release{ ReleaseArtworkAssociation.release }; db::Artwork::pointer artwork; - - if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) + if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&ReleaseArtworkAssociation.preferredArtwork) }) artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); - else if (const db::ImageId * imageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) + else if (const db::ImageId * imageId{ std::get_if(&ReleaseArtworkAssociation.preferredArtwork) }) artwork = utils::getOrCreateArtworkFromImage(session, *imageId); release.modify()->setPreferredArtwork(artwork); @@ -229,7 +227,7 @@ namespace lms::scanner LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from release '" << release->getName() << "'"); } - void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations) + void updateReleaseArtworks(db::Session& session, ReleaseArtworkAssociationContainer& imageAssociations) { constexpr std::size_t writeBatchSize{ 50 }; @@ -280,19 +278,19 @@ namespace lms::scanner context.currentStepStats.totalElems = db::Release::getCount(session); } - SearchReleaseImageContext searchContext{ + SearchReleaseArtworkContext searchContext{ .session = session, .lastRetrievedReleaseId = {}, .releaseImageFileNames = _releaseImageFileNames, }; - ReleaseImageAssociationContainer releaseImageAssociations; - while (fetchNextReleaseArtworksToUpdate(searchContext, releaseImageAssociations)) + ReleaseArtworkAssociationContainer ReleaseArtworkAssociations; + while (fetchNextReleaseArtworksToUpdate(searchContext, ReleaseArtworkAssociations)) { if (_abortScan) return; - updateReleaseImages(session, releaseImageAssociations); + updateReleaseArtworks(session, ReleaseArtworkAssociations); context.currentStepStats.processedElems = searchContext.processedReleaseCount; _progressCallback(context.currentStepStats); } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp index 346bc09d..bf4da692 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp @@ -62,22 +62,22 @@ namespace lms::scanner return !std::holds_alternative(res); } - struct TrackImageAssociation + struct TrackArtworksAssociation { db::Track::pointer track; TrackArtwork preferredArtwork; TrackArtwork preferredMediaArtwork; }; - using TrackImageAssociationContainer = std::deque; + using TrackArtworksAssociationContainer = std::deque; - struct SearchTrackImageContext + struct SearchTrackArtworkContext { db::Session& session; db::TrackId lastRetrievedTrackId; std::size_t processedTrackCount{}; }; - TrackArtwork computePreferredTrackArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track) + TrackArtwork computePreferredTrackArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track) { // Try to get a media image TrackArtwork res; @@ -135,7 +135,7 @@ namespace lms::scanner return res; } - TrackArtwork computePreferredTrackMediaArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track) + TrackArtwork computePreferredTrackMediaArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track) { TrackArtwork res; { @@ -165,7 +165,7 @@ namespace lms::scanner return res; } - bool fetchNextTrackArtworksToUpdate(SearchTrackImageContext& searchContext, TrackImageAssociationContainer& trackImageAssociations) + bool fetchNextTrackArtworksToUpdate(SearchTrackArtworkContext& searchContext, TrackArtworksAssociationContainer& TrackArtworksAssociations) { const db::TrackId trackId{ searchContext.lastRetrievedTrackId }; @@ -184,7 +184,7 @@ namespace lms::scanner if (!isSameArtwork(preferredArtwork, currentPreferredArtwork) || !isSameArtwork(preferredMediaArtwork, currentPreferredMediaArtwork)) { - trackImageAssociations.push_back(TrackImageAssociation{ track, preferredArtwork, preferredMediaArtwork }); + TrackArtworksAssociations.push_back(TrackArtworksAssociation{ track, preferredArtwork, preferredMediaArtwork }); } searchContext.processedTrackCount++; @@ -226,24 +226,24 @@ namespace lms::scanner LMS_LOG(DBUPDATER, DEBUG, "Removed preferred media artwork from track '" << track->getAbsoluteFilePath() << "'"); } - void updateTrackPreferredArtworks(db::Session& session, const TrackImageAssociation& trackImageAssociation) + void updateTrackPreferredArtworks(db::Session& session, const TrackArtworksAssociation& TrackArtworksAssociation) { - db::Track::pointer track{ trackImageAssociation.track }; + db::Track::pointer track{ TrackArtworksAssociation.track }; { const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() }; - if (!isSameArtwork(trackImageAssociation.preferredArtwork, currentPreferredArtwork)) - updateTrackPreferredArtwork(session, track, trackImageAssociation.preferredArtwork); + if (!isSameArtwork(TrackArtworksAssociation.preferredArtwork, currentPreferredArtwork)) + updateTrackPreferredArtwork(session, track, TrackArtworksAssociation.preferredArtwork); } { const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() }; - if (!isSameArtwork(trackImageAssociation.preferredMediaArtwork, currentPreferredMediaArtwork)) - updateTrackPreferredMediaArtwork(session, track, trackImageAssociation.preferredMediaArtwork); + if (!isSameArtwork(TrackArtworksAssociation.preferredMediaArtwork, currentPreferredMediaArtwork)) + updateTrackPreferredMediaArtwork(session, track, TrackArtworksAssociation.preferredMediaArtwork); } } - void updateTrackPreferredArtworks(db::Session& session, TrackImageAssociationContainer& imageAssociations) + void updateTrackPreferredArtworks(db::Session& session, TrackArtworksAssociationContainer& imageAssociations) { constexpr std::size_t writeBatchSize{ 50 }; @@ -279,18 +279,18 @@ namespace lms::scanner context.currentStepStats.totalElems = db::Track::getCount(session); } - SearchTrackImageContext searchContext{ + SearchTrackArtworkContext searchContext{ .session = session, .lastRetrievedTrackId = {}, }; - TrackImageAssociationContainer trackImageAssociations; - while (fetchNextTrackArtworksToUpdate(searchContext, trackImageAssociations)) + TrackArtworksAssociationContainer TrackArtworksAssociations; + while (fetchNextTrackArtworksToUpdate(searchContext, TrackArtworksAssociations)) { if (_abortScan) return; - updateTrackPreferredArtworks(session, trackImageAssociations); + updateTrackPreferredArtworks(session, TrackArtworksAssociations); context.currentStepStats.processedElems = searchContext.processedTrackCount; _progressCallback(context.currentStepStats); } diff --git a/src/libs/subsonic/impl/CoverArtId.cpp b/src/libs/subsonic/impl/CoverArtId.cpp index 54555a9b..fc91c008 100644 --- a/src/libs/subsonic/impl/CoverArtId.cpp +++ b/src/libs/subsonic/impl/CoverArtId.cpp @@ -22,8 +22,7 @@ #include #include "core/String.hpp" -#include "database/ImageId.hpp" -#include "database/TrackEmbeddedImageId.hpp" +#include "database/ArtworkId.hpp" namespace lms::api::subsonic { @@ -31,14 +30,9 @@ namespace lms::api::subsonic { constexpr char timestampSeparatorChar{ '-' }; - std::string idToString(db::ImageId id) + std::string idToString(db::ArtworkId id) { - return "im-" + id.toString(); - } - - std::string idToString(db::TrackEmbeddedImageId id) - { - return "trim-" + id.toString(); + return "art-" + id.toString(); } } // namespace @@ -46,19 +40,10 @@ namespace lms::api::subsonic { std::string res; - // produce "type-id[-timestamp]" - if (db::TrackEmbeddedImageId * imageId{ std::get_if(&coverId.id) }) - { - res = idToString(*imageId); - assert(!coverId.timestamp.has_value()); - } - else if (db::ImageId * imageId{ std::get_if(&coverId.id) }) - { - res = idToString(*imageId); - res += timestampSeparatorChar; - assert(coverId.timestamp.has_value()); - res += std::to_string(*coverId.timestamp); - } + // produce "art-id-timestamp" + res = idToString(coverId.id); + res += timestampSeparatorChar; + res += std::to_string(coverId.timestamp); assert(!res.empty()); return res; @@ -74,30 +59,20 @@ namespace lms::core::stringUtils std::optional res; std::vector values{ core::stringUtils::splitString(str, '-') }; - if (values.size() <= 1) + + // expect "art-id-timestamp" + if (values.size() != 3) + return res; + + if (values[0] != "art") + return res; + + const auto value{ core::stringUtils::readAs(values[1]) }; + const auto timestamp{ core::stringUtils::readAs(values[2]) }; + if (!value || !timestamp) return std::nullopt; - if (values[0] == "trim") - { - // expect "trim-id" - if (values.size() == 2) - { - if (const auto value{ core::stringUtils::readAs(values[1]) }) - res.emplace(db::TrackEmbeddedImageId{ *value }); - } - } - else if (values[0] == "im") - { - // expect "im-id-timestamp" - if (values.size() == 3) - { - const auto imageId{ core::stringUtils::readAs(values[1]) }; - const auto timestamp{ core::stringUtils::readAs(values[2]) }; - - if (imageId && timestamp) - res.emplace(db::ImageId{ *imageId }, *timestamp); - } - } + res.emplace(api::subsonic::CoverArtId{ *value, *timestamp }); return res; } diff --git a/src/libs/subsonic/impl/CoverArtId.hpp b/src/libs/subsonic/impl/CoverArtId.hpp index 8d6f7a56..11a763e6 100644 --- a/src/libs/subsonic/impl/CoverArtId.hpp +++ b/src/libs/subsonic/impl/CoverArtId.hpp @@ -20,24 +20,16 @@ #pragma once #include -#include #include "core/String.hpp" -#include "database/ImageId.hpp" -#include "database/TrackEmbeddedImageId.hpp" +#include "database/ArtworkId.hpp" namespace lms::api::subsonic { struct CoverArtId { - std::variant id; - std::optional timestamp; - - CoverArtId(db::ImageId _id, std::time_t _timestamp) - : id{ _id } - , timestamp{ _timestamp } {} - CoverArtId(db::TrackEmbeddedImageId _id) - : id{ _id } {} + db::ArtworkId id; + std::time_t timestamp; }; std::string idToString(CoverArtId coverId); @@ -46,9 +38,6 @@ namespace lms::api::subsonic // Used to parse parameters namespace lms::core::stringUtils { - template<> - std::optional readAs(std::string_view str); - template<> std::optional readAs(std::string_view str); } // namespace lms::core::stringUtils diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index b03239c3..805a0468 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -336,12 +336,7 @@ namespace lms::api::subsonic if (size) *size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 }); - std::shared_ptr image; - if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&coverArtId.id) }) - image = core::Service::get()->getTrackEmbeddedImage(*trackEmbeddedImageId, size); - else if (const db::ImageId * imageId{ std::get_if(&coverArtId.id) }) - image = core::Service::get()->getImage(*imageId, size); - + std::shared_ptr image{ core::Service::get()->getImage(coverArtId.id, size) }; if (!image) { response.setStatus(404); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 3925e703..914d79f1 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -23,14 +23,13 @@ #include "core/Service.hpp" #include "core/String.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" -#include "database/Image.hpp" #include "database/Release.hpp" #include "database/Track.hpp" #include "database/Types.hpp" #include "database/User.hpp" -#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -88,19 +87,12 @@ namespace lms::api::subsonic albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime())); + if (const auto artwork{ release->getPreferredArtwork() }) { - const auto imageResult{ core::Service::get()->findReleaseImage(release->getId()) }; - if (const db::ImageId * imageId{ std::get_if(&imageResult) }) - { - if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) - { - const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; - albumNode.setAttribute("coverArt", idToString(coverArtId)); - } - } - else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) - albumNode.setAttribute("coverArt", idToString(*embeddedImageId)); + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + albumNode.setAttribute("coverArt", idToString(coverArtId)); } + if (const auto originalYear{ release->getOriginalYear() }) albumNode.setAttribute("year", *originalYear); else if (const auto year{ release->getYear() }) diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 6246d318..f47a76bf 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -23,11 +23,10 @@ #include "core/Service.hpp" #include "core/String.hpp" #include "database/Artist.hpp" -#include "database/Image.hpp" +#include "database/Artwork.hpp" #include "database/Release.hpp" #include "database/TrackArtistLink.hpp" #include "database/User.hpp" -#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "CoverArtId.hpp" @@ -96,18 +95,10 @@ namespace lms::api::subsonic artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("name", artist->getName()); + if (const auto artwork{ artist->getPreferredArtwork() }) { - const auto imageResult{ core::Service::get()->findArtistImage(artist->getId()) }; - if (const db::ImageId * imageId{ std::get_if(&imageResult) }) - { - if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) - { - const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; - artistNode.setAttribute("coverArt", idToString(coverArtId)); - } - } - else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) - artistNode.setAttribute("coverArt", idToString(*embeddedImageId)); + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + artistNode.setAttribute("coverArt", idToString(coverArtId)); } const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) }; diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index 024b0ccd..d053c14a 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -21,7 +21,7 @@ #include "core/Service.hpp" #include "core/String.hpp" -#include "database/Image.hpp" +#include "database/Artwork.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" #include "services/artwork/IArtworkService.hpp" @@ -46,18 +46,13 @@ namespace lms::api::subsonic if (const db::User::pointer user{ tracklist->getUser() }) playlistNode.setAttribute("owner", user->getLoginName()); + if (const db::ArtworkId artworkId{ core::Service::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid()) { - const auto imageResult{ core::Service::get()->findTrackListImage(tracklist->getId()) }; - if (const db::ImageId * imageId{ std::get_if(&imageResult) }) + if (const auto artwork{ db::Artwork::find(context.dbSession, artworkId) }) { - if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) - { - const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; - playlistNode.setAttribute("coverArt", idToString(coverArtId)); - } + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + playlistNode.setAttribute("coverArt", idToString(coverArtId)); } - else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) - playlistNode.setAttribute("coverArt", idToString(*embeddedImageId)); } return playlistNode; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 7ee106f5..7e762666 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -21,21 +21,19 @@ #include -#include "av/IAudioFile.hpp" #include "core/ITraceLogger.hpp" #include "core/MimeTypes.hpp" #include "core/Service.hpp" #include "core/String.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" -#include "database/Image.hpp" #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" #include "database/Types.hpp" #include "database/User.hpp" -#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -111,18 +109,14 @@ namespace lms::api::subsonic trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } + auto artwork{ track->getPreferredMediaArtwork() }; + if (!artwork) + artwork = track->getPreferredArtwork(); + + if (artwork) { - const auto imageResult{ core::Service::get()->findTrackImage(track->getId()) }; - if (const db::ImageId * imageId{ std::get_if(&imageResult) }) - { - if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) - { - const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - } - } - else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) - trackResponse.setAttribute("coverArt", idToString(*embeddedImageId)); + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + trackResponse.setAttribute("coverArt", idToString(coverArtId)); } const std::vector& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 87a76c35..58a29e14 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -251,12 +251,26 @@ namespace lms::ui << " replayGain: " << replayGain << "," << " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\"," << " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," - << " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," - << " artwork: [" - << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," - << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," - << " ]" - << "};"; + << " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","; + + db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; + if (!artworkId.isValid()) + artworkId = track->getPreferredArtworkId(); + if (artworkId.isValid()) + { + oss << " artwork: [" + << " { src: \"" << LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," + << " { src: \"" << LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," + << " ]"; + } + else + { + oss << " artwork: [" + << " { src: \"" << LmsApp->getArtworkResource()->getDefaultTrackArtworkUrl() << "\", type: \"image/svg+xml\" }," + << " ]"; + } + oss << "};"; + // Update 'sizes' above to match this: static_assert(static_cast>(ArtworkResource::Size::Small) == 128); static_assert(static_cast>(ArtworkResource::Size::Large) == 512); diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 530ce1c8..b7835fe1 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -490,8 +490,18 @@ namespace lms::ui entry->bindWidget("artists-md", utils::createArtistAnchorList(artists)); } - auto image{ utils::createTrackImage(track->getId(), ArtworkResource::Size::Small) }; - image->addStyleClass("Lms-cover-track rounded"); + db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; + if (!artworkId.isValid()) + artworkId = track->getPreferredArtworkId(); + + std::unique_ptr image; + if (artworkId.isValid()) + image = utils::createArtworkImage(artworkId, ArtworkResource::Size::Small); + else + image = utils::createDefaultTrackArtworkImage(); + + image->addStyleClass("Lms-cover-track rounded"); // HACK + if (const auto release{ track->getRelease() }) { entry->setCondition("if-has-release", true); diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 1e8191f4..ba11e50c 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -40,6 +40,18 @@ namespace lms::ui::utils { + namespace + { + std::unique_ptr createArtworkImage() + { + auto image{ std::make_unique() }; + image->setStyleClass("Lms-cover img-fluid"); // HACK + image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + + return image; + } + } // namespace + std::string durationToString(std::chrono::milliseconds msDuration) { const std::chrono::seconds duration{ std::chrono::duration_cast(msDuration) }; @@ -76,46 +88,32 @@ namespace lms::ui::utils LmsApp->getModalManager().show(std::move(rawImage)); } - std::unique_ptr createArtistImage(db::ArtistId artistId, ArtworkResource::Size size) + std::unique_ptr createArtworkImage(db::ArtworkId artworkId, ArtworkResource::Size size) { - auto image{ std::make_unique() }; - image->setImageLink(LmsApp->getArtworkResource()->getArtistImageUrl(artistId, size)); - image->setStyleClass("Lms-cover img-fluid"); // HACK - image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + auto image{ createArtworkImage() }; + image->setImageLink(LmsApp->getArtworkResource()->getArtworkUrl(artworkId, size)); return image; } - std::unique_ptr createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size) + std::unique_ptr createDefaultArtistArtworkImage() { - auto cover{ std::make_unique() }; - cover->setImageLink(LmsApp->getArtworkResource()->getReleaseCoverUrl(releaseId, size)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK - return cover; + auto image{ createArtworkImage() }; + image->setImageLink(LmsApp->getArtworkResource()->getDefaultArtistArtworkUrl()); + return image; } - std::unique_ptr createTrackImage(db::TrackId trackId, ArtworkResource::Size size) + std::unique_ptr createDefaultReleaseArtworkImage() { - auto cover{ std::make_unique() }; - cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK - - return cover; + auto image{ createArtworkImage() }; + image->setImageLink(LmsApp->getArtworkResource()->getDefaultReleaseArtworkUrl()); + return image; } - std::unique_ptr createTrackMediaImage(db::TrackId trackId, ArtworkResource::Size size) + std::unique_ptr createDefaultTrackArtworkImage() { - std::string url{ LmsApp->getArtworkResource()->getTrackMediaImageUrl(trackId, size) }; - if (url.empty()) - return nullptr; - - auto cover{ std::make_unique() }; - cover->setImageLink(std::move(url)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK - - return cover; + auto image{ createArtworkImage() }; + image->setImageLink(LmsApp->getArtworkResource()->getDefaultTrackArtworkUrl()); + return image; } std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete) diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index c29077e2..f2cdc20a 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -57,10 +57,10 @@ namespace lms::ui::utils void showArtworkModal(Wt::WLink image); - std::unique_ptr createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size); - std::unique_ptr createTrackImage(db::TrackId trackId, ArtworkResource::Size size); - std::unique_ptr createTrackMediaImage(db::TrackId trackId, ArtworkResource::Size size); // return nullptr if no image - std::unique_ptr createArtistImage(db::ArtistId artistId, ArtworkResource::Size size); + std::unique_ptr createArtworkImage(db::ArtworkId artworkId, ArtworkResource::Size size); + std::unique_ptr createDefaultArtistArtworkImage(); + std::unique_ptr createDefaultReleaseArtworkImage(); + std::unique_ptr createDefaultTrackArtworkImage(); std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false); std::unique_ptr createFilterCluster(db::ClusterId clusterId, bool canDelete = false); diff --git a/src/lms/ui/explore/ArtistListHelpers.cpp b/src/lms/ui/explore/ArtistListHelpers.cpp index 49571f00..c7162cb7 100644 --- a/src/lms/ui/explore/ArtistListHelpers.cpp +++ b/src/lms/ui/explore/ArtistListHelpers.cpp @@ -22,18 +22,23 @@ #include "database/Artist.hpp" -#include "LmsApplication.hpp" #include "Utils.hpp" namespace lms::ui::ArtistListHelpers { - std::unique_ptr createEntry(const db::ObjectPtr& artist) + std::unique_ptr createEntry(const db::Artist::pointer& artist) { auto entry{ std::make_unique(Wt::WString::tr("Lms.Explore.Artists.template.entry")) }; entry->bindWidget("name", utils::createArtistAnchor(artist)); Wt::WAnchor* anchor{ entry->bindWidget("image", utils::createArtistAnchor(artist, false)) }; - auto image{ utils::createArtistImage(artist->getId(), ArtworkResource::Size::Large) }; + + std::unique_ptr image; + if (artist->getPreferredArtworkId().isValid()) + image = utils::createArtworkImage(artist->getPreferredArtworkId(), ArtworkResource::Size::Large); + else + image = utils::createDefaultArtistArtworkImage(); + image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack anchor->setImage(std::move(image)); diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 21b8d9c5..c2da9b5c 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -24,6 +24,7 @@ #include "core/String.hpp" #include "database/Artist.hpp" #include "database/ArtistInfo.hpp" +#include "database/ArtworkId.hpp" #include "database/Cluster.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" @@ -119,7 +120,7 @@ namespace lms::ui LmsApp->setTitle(artist->getName()); _artistId = *artistId; - refreshArtwork(); + refreshArtwork(artist->getPreferredArtworkId()); refreshArtistInfo(); refreshReleases(); refreshAppearsOnReleases(); @@ -191,12 +192,24 @@ namespace lms::ui } } - void Artist::refreshArtwork() + void Artist::refreshArtwork(db::ArtworkId artworkId) { - auto* image{ bindWidget("artwork", utils::createArtistImage(_artistId, ArtworkResource::Size::Large)) }; - image->clicked().connect([this] { - utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtistImageUrl(_artistId) }); - }); + std::unique_ptr artworkImage; + if (artworkId.isValid()) + { + artworkImage = utils::createArtworkImage(artworkId, ArtworkResource::Size::Large); + artworkImage->addStyleClass("Lms-cursor-pointer"); // HACK + } + else + artworkImage = utils::createDefaultArtistArtworkImage(); + + auto* image{ bindWidget("artwork", std::move(artworkImage)) }; + if (artworkId.isValid()) + { + image->clicked().connect([artworkId] { + utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId) }); + }); + } } void Artist::refreshArtistInfo() diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index bcd9b527..4a613508 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -22,6 +22,7 @@ #include #include "database/ArtistId.hpp" +#include "database/ArtworkId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" @@ -47,7 +48,7 @@ namespace lms::ui private: void refreshView(); - void refreshArtwork(); + void refreshArtwork(db::ArtworkId artworkId); void refreshArtistInfo(); void refreshReleases(); void refreshAppearsOnReleases(); diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index b8e14c2a..45143712 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -51,7 +51,13 @@ namespace lms::ui::releaseListHelpers { Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) }; - auto image{ utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large) }; + + std::unique_ptr image; + if (release->getPreferredArtworkId().isValid()) + image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::Size::Large); + else + image = utils::createDefaultReleaseArtworkImage(); + image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack anchor->setImage(std::move(image)); } diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 9241d1d0..8fcfa166 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -306,11 +306,7 @@ namespace lms::ui bindString("duration", utils::durationToString(release->getDuration()), Wt::TextFormat::Plain); refreshReleaseArtists(release); - - auto* image{ bindWidget("cover", utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large)) }; - image->clicked().connect([=] { - utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getReleaseCoverUrl(*releaseId) }); - }); + refreshArtwork(release->getPreferredArtworkId()); Wt::WContainerWidget* clusterContainers{ bindNew("clusters") }; { @@ -391,22 +387,24 @@ namespace lms::ui // Expect to be called in asc order std::map trackContainers; - auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle, db::TrackId trackId) -> Wt::WContainerWidget* { + auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle, db::ArtworkId mediaArtworkId) -> Wt::WContainerWidget* { if (auto it{ trackContainers.find(discNumber) }; it != std::cend(trackContainers)) return it->second; Template* disc{ rootContainer->addNew