From 7494d055e7508663188f9526e26362dd8525fb9a Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 17 Jun 2025 09:46:42 +0200 Subject: [PATCH] Introduced the artwork table. Now resolve the preferred release artwork during scan steps --- src/libs/database/CMakeLists.txt | 1 + src/libs/database/impl/Artwork.cpp | 77 ++++++++++ src/libs/database/impl/Image.cpp | 1 - src/libs/database/impl/Migration.cpp | 59 ++++++- src/libs/database/impl/Release.cpp | 15 +- src/libs/database/impl/Session.cpp | 7 +- .../include/database/ArtistInfoId.hpp | 2 +- .../database/include/database/Artwork.hpp | 67 ++++++++ .../database/include/database/ArtworkId.hpp | 24 +++ src/libs/database/include/database/Image.hpp | 3 - .../database/include/database/Release.hpp | 16 +- src/libs/database/test/Release.cpp | 17 ++- .../services/artwork/impl/ArtworkService.cpp | 40 +---- .../steps/ScanStepAssociateReleaseImages.cpp | 144 +++++++++++++++--- .../steps/ScanStepAssociateReleaseImages.hpp | 2 +- 15 files changed, 384 insertions(+), 91 deletions(-) create mode 100644 src/libs/database/impl/Artwork.cpp create mode 100644 src/libs/database/include/database/Artwork.hpp create mode 100644 src/libs/database/include/database/ArtworkId.hpp diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 87d95d63..29b07d8b 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(lmsdatabase STATIC impl/Artist.cpp impl/ArtistInfo.cpp + impl/Artwork.cpp impl/AuthToken.cpp impl/Cluster.cpp impl/Db.cpp diff --git a/src/libs/database/impl/Artwork.cpp b/src/libs/database/impl/Artwork.cpp new file mode 100644 index 00000000..d8d03573 --- /dev/null +++ b/src/libs/database/impl/Artwork.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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 "database/Artwork.hpp" + +#include "database/Image.hpp" +#include "database/Session.hpp" +#include "database/TrackEmbeddedImage.hpp" + +#include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" + +namespace lms::db +{ + Artwork::Artwork(ObjectPtr trackEmbeddedImage) + : _trackEmbeddedImage{ getDboPtr(trackEmbeddedImage) } + { + } + + Artwork::Artwork(ObjectPtr image) + : _image{ getDboPtr(image) } + { + } + + Artwork::pointer Artwork::create(Session& session, ObjectPtr trackEmbeddedImage) + { + session.checkWriteTransaction(); + return session.getDboSession()->add(std::unique_ptr{ new Artwork{ trackEmbeddedImage } }); + } + + Artwork::pointer Artwork::create(Session& session, ObjectPtr image) + { + session.checkWriteTransaction(); + return session.getDboSession()->add(std::unique_ptr{ new Artwork{ image } }); + } + + std::size_t Artwork::getCount(Session& session) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM artwork")); + } + + Artwork::pointer Artwork::find(Session& session, ArtworkId id) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.id = ?").bind(id)); + } + + Artwork::pointer Artwork::find(Session& session, TrackEmbeddedImageId id) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a JOIN track_embedded_image t_e_i ON a.track_embedded_image_id = t_e_i.id").where("t_e_i.id = ?").bind(id)); + } + + Artwork::pointer Artwork::find(Session& session, ImageId id) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a JOIN image i ON a.image_id = i.id").where("i.id = ?").bind(id)); + } + +} // namespace lms::db diff --git a/src/libs/database/impl/Image.cpp b/src/libs/database/impl/Image.cpp index 1b010d09..e0d43c4d 100644 --- a/src/libs/database/impl/Image.cpp +++ b/src/libs/database/impl/Image.cpp @@ -23,7 +23,6 @@ #include "database/Artist.hpp" #include "database/Directory.hpp" -#include "database/Release.hpp" #include "database/Session.hpp" #include "Utils.hpp" diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index f6ba2b4c..e46d1ae1 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 92 }; + static constexpr Version LMS_DATABASE_VERSION{ 93 }; } VersionInfo::VersionInfo() @@ -411,7 +411,7 @@ SELECT copyright_url, track_replay_gain, release_replay_gain, - COALESCE(artist_display_name, ""), + artist_display_name, release_id, 1 FROM track)"); @@ -1219,6 +1219,58 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred"); } + void migrateFromV92(Session& session) + { + // Create the new artwork table + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artwork" ( + "id" integer primary key autoincrement, + "version" integer not null, + "track_embedded_image_id" bigint, + "image_id" bigint, + constraint "fk_artwork_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred, + constraint "fk_artwork_image" foreign key ("image_id") references "image" ("id") on delete cascade deferrable initially deferred))"); + + // Replaced image by artwork for release + // Create the new table, copy the data, drop the old table, rename the new one + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "sort_name" text not null, + "mbid" text not null, + "group_mbid" text not null, + "total_disc" integer, + "artist_display_name" text not null, + "is_compilation" boolean not null, + "barcode" text not null, + "comment" text not null, + "preferred_artwork_id" bigint, + constraint "fk_release_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))"); + + // Migrate data, with the new preferred_artwork_id field set to null + utils::executeCommand(*session.getDboSession(), R"(INSERT INTO release_backup +SELECT + id, + version, + name, + sort_name, + mbid, + group_mbid, + total_disc, + COALESCE(artist_display_name, ''), + is_compilation, + barcode, + comment, + NULL as preferred_artwork_id +FROM release)"); + + utils::executeCommand(*session.getDboSession(), "DROP TABLE release"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release"); + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1286,7 +1338,8 @@ FROM tracklist)"); { 88, migrateFromV88 }, { 89, migrateFromV89 }, { 90, migrateFromV90 }, - { 91, migrateFromV91 } + { 91, migrateFromV91 }, + { 92, migrateFromV92 } }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index d14174b8..c2a39890 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -23,9 +23,9 @@ #include "core/PartialDateTime.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" -#include "database/Image.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/Types.hpp" @@ -701,14 +701,9 @@ namespace lms::db return utils::fetchQueryResults(query); } - ObjectPtr Release::getImage() const + ObjectPtr Release::getPreferredArtwork() const { - return ObjectPtr{ _image }; - } - - ImageId Release::getImageId() const - { - return _image.id(); + return ObjectPtr{ _preferredArtwork }; } void Release::clearLabels() @@ -741,9 +736,9 @@ namespace lms::db _releaseTypes.insert(getDboPtr(releaseType)); } - void Release::setImage(ObjectPtr image) + void Release::setPreferredArtwork(ObjectPtr artwork) { - _image = getDboPtr(image); + _preferredArtwork = getDboPtr(artwork); } bool Release::hasVariousArtists() const diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index b9e2f3ca..cc6f9dad 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -23,6 +23,7 @@ #include "core/ITraceLogger.hpp" #include "database/Artist.hpp" #include "database/ArtistInfo.hpp" +#include "database/Artwork.hpp" #include "database/AuthToken.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" @@ -102,6 +103,7 @@ namespace lms::db _session.mapClass("artist"); _session.mapClass("artist_info"); + _session.mapClass("artwork"); _session.mapClass("auth_token"); _session.mapClass("cluster"); _session.mapClass("cluster_type"); @@ -206,6 +208,10 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)"); @@ -256,7 +262,6 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); diff --git a/src/libs/database/include/database/ArtistInfoId.hpp b/src/libs/database/include/database/ArtistInfoId.hpp index 1056f37c..5a046270 100644 --- a/src/libs/database/include/database/ArtistInfoId.hpp +++ b/src/libs/database/include/database/ArtistInfoId.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * diff --git a/src/libs/database/include/database/Artwork.hpp b/src/libs/database/include/database/Artwork.hpp new file mode 100644 index 00000000..f373c502 --- /dev/null +++ b/src/libs/database/include/database/Artwork.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "database/ArtworkId.hpp" +#include "database/ImageId.hpp" +#include "database/Object.hpp" +#include "database/TrackEmbeddedImageId.hpp" + +namespace lms::db +{ + class Image; + class Session; + class TrackEmbeddedImage; + + class Artwork final : public Object + { + public: + Artwork() = default; + + // find + static std::size_t getCount(Session& session); + static pointer find(Session& session, ArtworkId id); + static pointer find(Session& session, TrackEmbeddedImageId id); + static pointer find(Session& session, ImageId id); + + // getters + TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); } + ImageId getImageId() const { return _image.id(); } + + template + void persist(Action& a) + { + Wt::Dbo::belongsTo(a, _trackEmbeddedImage, "track_embedded_image", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteCascade); + } + + private: + friend class Session; + Artwork(ObjectPtr trackEmbeddedImage); + Artwork(ObjectPtr image); + static pointer create(Session& session, ObjectPtr trackEmbeddedImage); + static pointer create(Session& session, ObjectPtr image); + + Wt::Dbo::ptr _trackEmbeddedImage; + Wt::Dbo::ptr _image; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/ArtworkId.hpp b/src/libs/database/include/database/ArtworkId.hpp new file mode 100644 index 00000000..ba667613 --- /dev/null +++ b/src/libs/database/include/database/ArtworkId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ArtworkId) diff --git a/src/libs/database/include/database/Image.hpp b/src/libs/database/include/database/Image.hpp index de6f78b4..816b4785 100644 --- a/src/libs/database/include/database/Image.hpp +++ b/src/libs/database/include/database/Image.hpp @@ -34,7 +34,6 @@ namespace lms::db { class Artist; class Directory; - class Release; class Session; class Image final : public Object @@ -101,7 +100,6 @@ namespace lms::db Wt::Dbo::field(a, _height, "height"); Wt::Dbo::hasMany(a, _artists, Wt::Dbo::ManyToOne, "image"); - Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToOne, "image"); Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade); } @@ -118,7 +116,6 @@ namespace lms::db int _height{}; Wt::Dbo::collection> _artists; - Wt::Dbo::collection> _releases; Wt::Dbo::ptr _directory; }; } // namespace lms::db diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 03ed0b8f..1f55173a 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -31,10 +31,10 @@ #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" +#include "database/ArtworkId.hpp" #include "database/CountryId.hpp" #include "database/DirectoryId.hpp" #include "database/Filters.hpp" -#include "database/ImageId.hpp" #include "database/LabelId.hpp" #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" @@ -46,9 +46,9 @@ namespace lms::db { class Artist; + class Artwork; class Cluster; class ClusterType; - class Image; class Release; class Session; class Track; @@ -292,8 +292,8 @@ namespace lms::db core::EnumSet getAdvisories() const; std::string_view getBarcode() const { return _barcode; } std::string_view getComment() const { return _comment; } - ObjectPtr getImage() const; - ImageId getImageId() const; + ObjectPtr getPreferredArtwork() const; + ArtworkId getPreferredArtworkId() const; // Setters void setName(std::string_view name) { _name = name; } @@ -311,7 +311,7 @@ namespace lms::db void addReleaseType(ObjectPtr releaseType); void setBarcode(std::string_view barcode) { _barcode = barcode; } void setComment(std::string_view comment) { _comment = comment; } - void setImage(ObjectPtr image); + void setPreferredArtwork(ObjectPtr artwork); // Get the artists of this release std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; @@ -333,9 +333,9 @@ namespace lms::db Wt::Dbo::field(a, _isCompilation, "is_compilation"); Wt::Dbo::field(a, _barcode, "barcode"); Wt::Dbo::field(a, _comment, "comment"); - Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); - Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull); + Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); + Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade); @@ -361,8 +361,8 @@ namespace lms::db std::string _barcode; std::string _comment; - Wt::Dbo::ptr _image; Wt::Dbo::collection> _tracks; + Wt::Dbo::ptr _preferredArtwork; Wt::Dbo::collection> _labels; Wt::Dbo::collection> _releaseTypes; Wt::Dbo::collection> _countries; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index c2e48600..995c5b21 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -20,10 +20,12 @@ #include "Common.hpp" #include "core/PartialDateTime.hpp" +#include "database/Artwork.hpp" #include "database/Image.hpp" namespace lms::db::tests { + using ScopedArtwork = ScopedEntity; using ScopedImage = ScopedEntity; using ScopedLabel = ScopedEntity; using ScopedCountry = ScopedEntity; @@ -1195,27 +1197,28 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Release_image) + TEST_F(DatabaseFixture, Release_artwork) { ScopedRelease release{ session, "MyRelease" }; { auto transaction{ session.createReadTransaction() }; - EXPECT_FALSE(release.get()->getImage()); + EXPECT_FALSE(release.get()->getPreferredArtwork()); } - ScopedImage image{ session, "/myImage" }; + ScopedImage image{ session, "/image.jpg" }; + ScopedArtwork artwork{ session, image.lockAndGet() }; { auto transaction{ session.createWriteTransaction() }; - release.get().modify()->setImage(image.get()); + release.get().modify()->setPreferredArtwork(artwork.get()); } { auto transaction{ session.createReadTransaction() }; - auto releaseImage(release.get()->getImage()); - ASSERT_TRUE(releaseImage); - EXPECT_EQ(releaseImage->getId(), image.getId()); + auto releaseArtwork(release.get()->getPreferredArtwork()); + ASSERT_TRUE(releaseArtwork); + EXPECT_EQ(releaseArtwork->getId(), artwork.getId()); } } diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 3f1784c6..11426470 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -23,6 +23,7 @@ #include "core/ILogger.hpp" #include "core/Utils.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Db.hpp" #include "database/Image.hpp" #include "database/Release.hpp" @@ -215,14 +216,14 @@ namespace lms::artwork if (isImageFound(res)) return res; - +#if 0 // Fallback on external cover of the release if (const db::Release::pointer release{ db::Release::find(session, releaseId) }) { if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid()) res = imageId; } - +#endif if (isImageFound(res)) return res; @@ -286,36 +287,11 @@ namespace lms::artwork if (!release) return res; - if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid()) - res = imageId; - - if (isImageFound(res)) - return res; - - // Fallback on embedded Front image - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(releaseId); - params.setImageTypes({ db::ImageType::FrontCover }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); - } - - if (isImageFound(res)) - return res; - - // Fallback on embedded media image - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(releaseId); - params.setImageTypes({ db::ImageType::Media }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); - } + 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; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index d758e192..7939a401 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -23,16 +23,20 @@ #include #include #include +#include #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Path.hpp" +#include "database/Artwork.hpp" #include "database/Db.hpp" #include "database/Directory.hpp" #include "database/Image.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "ScanContext.hpp" @@ -40,10 +44,45 @@ namespace lms::scanner { namespace { + using PreferredArtwork = std::variant; + bool isSameArtwork(PreferredArtwork preferredArtwork, const db::Artwork::pointer& artwork) + { + if (std::holds_alternative(preferredArtwork)) + return !artwork; + + if (const db::TrackEmbeddedImageId* trackEmbeddedImageId = std::get_if(&preferredArtwork)) + return artwork && *trackEmbeddedImageId == artwork->getTrackEmbeddedImageId(); + + if (const db::ImageId* imageId = std::get_if(&preferredArtwork)) + return artwork && *imageId == artwork->getImageId(); + + return false; + } + + std::filesystem::path toPath(db::Session& session, const db::TrackEmbeddedImageId trackEmbeddedImageId) + { + std::filesystem::path res; + + db::Track::FindParameters params; + params.setEmbeddedImage(trackEmbeddedImageId); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::Track::find(session, params, [&](const db::Track::pointer& track) { + res = track->getAbsoluteFilePath(); + }); + + return res; + } + + std::filesystem::path toPath(db::Session& session, const db::ImageId imageId) + { + db::Image::pointer image{ db::Image::find(session, imageId) }; + return image ? image->getAbsoluteFilePath() : std::filesystem::path{}; + } + struct ReleaseImageAssociation { db::ReleaseId releaseId; - db::ImageId imageId; + PreferredArtwork preferredArtwork; }; using ReleaseImageAssociationContainer = std::deque; @@ -52,7 +91,7 @@ namespace lms::scanner db::Session& session; db::ReleaseId lastRetrievedReleaseId; std::size_t processedReleaseCount{}; - const std::vector& releaseFileNames; + const std::vector& releaseImageFileNames; }; db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath) @@ -62,7 +101,7 @@ namespace lms::scanner const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) }; if (directory) // may not exist for releases that are split on different media libraries { - for (std::string_view fileStem : searchContext.releaseFileNames) + for (std::string_view fileStem : searchContext.releaseImageFileNames) { db::Image::FindParameters params; params.setDirectory(directory->getId()); @@ -129,7 +168,45 @@ namespace lms::scanner return image; } - bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations) + PreferredArtwork computeBestReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + { + const db::Image::pointer image{ computeBestReleaseImage(searchContext, release) }; + if (image) + return PreferredArtwork{ image->getId() }; + + // Fallback on embedded Front image + db::TrackEmbeddedImageId trackEmbeddedImageId; + { + db::TrackEmbeddedImage::FindParameters params; + params.setRelease(release->getId()); + params.setImageTypes({ db::ImageType::FrontCover }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); }); + } + + if (trackEmbeddedImageId.isValid()) + return PreferredArtwork{ trackEmbeddedImageId }; + + // Fallback on embedded media image + { + db::TrackEmbeddedImage::FindParameters params; + params.setRelease(release->getId()); + params.setImageTypes({ db::ImageType::Media }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); }); + } + + if (trackEmbeddedImageId.isValid()) + return PreferredArtwork{ trackEmbeddedImageId }; + + return PreferredArtwork{}; + } + + bool fetchNextReleaseArtworksToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations) { const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId }; @@ -139,13 +216,11 @@ namespace lms::scanner auto transaction{ searchContext.session.createReadTransaction() }; db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) { - db::Image::pointer image{ computeBestReleaseImage(searchContext, release) }; + const PreferredArtwork preferredArtwork{ computeBestReleaseArtwork(searchContext, release) }; + const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() }; - if (image != release->getImage()) - { - LMS_LOG(DBUPDATER, DEBUG, "Updating release image for release '" << release->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "") << "'"); - releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), image ? image->getId() : db::ImageId{} }); - } + if (!isSameArtwork(preferredArtwork, currentPreferredArtwork)) + releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), preferredArtwork }); searchContext.processedReleaseCount++; }); } @@ -153,16 +228,40 @@ namespace lms::scanner return releaseId != searchContext.lastRetrievedReleaseId; } - void updateReleaseImage(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation) + void updateReleaseArtwork(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation) { db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) }; assert(release); - db::Image::pointer image; - if (releaseImageAssociation.imageId.isValid()) - image = db::Image::find(session, releaseImageAssociation.imageId); + db::Artwork::pointer artwork; + if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) + { + artwork = db::Artwork::find(session, *trackEmbeddedImageId); + if (!artwork) + { + db::TrackEmbeddedImage::pointer trackEmbeddedImage{ db::TrackEmbeddedImage::find(session, *trackEmbeddedImageId) }; + assert(trackEmbeddedImage); + artwork = session.create(trackEmbeddedImage); + } - release.modify()->setImage(image); + LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork in release '" << release->getName() << "' with embedded image in track " << toPath(session, *trackEmbeddedImageId)); + } + else if (const db::ImageId * imageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) + { + artwork = db::Artwork::find(session, *imageId); + if (!artwork) + { + db::Image::pointer image{ db::Image::find(session, *imageId) }; + assert(image); + artwork = session.create(image); + } + + LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork in release '" << release->getName() << "' with image " << toPath(session, *imageId)); + } + else + LMS_LOG(DBUPDATER, DEBUG, "Removing preferred artwork from release '" << release->getName() << "'"); + + release.modify()->setPreferredArtwork(artwork); } void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations) @@ -175,13 +274,13 @@ namespace lms::scanner for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i) { - updateReleaseImage(session, imageAssociations.front()); + updateReleaseArtwork(session, imageAssociations.front()); imageAssociations.pop_front(); } } } - std::vector constructReleaseFileNames() + std::vector constructReleaseImageFileNames() { std::vector res; @@ -198,16 +297,13 @@ namespace lms::scanner ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams) : ScanStepBase{ initParams } - , _releaseFileNames{ constructReleaseFileNames() } + , _releaseImageFileNames{ constructReleaseImageFileNames() } { } bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const { - if (context.stats.nbChanges() > 0) - return true; - - return false; + return context.stats.nbChanges() > 0; } void ScanStepAssociateReleaseImages::process(ScanContext& context) @@ -222,11 +318,11 @@ namespace lms::scanner SearchReleaseImageContext searchContext{ .session = session, .lastRetrievedReleaseId = {}, - .releaseFileNames = _releaseFileNames, + .releaseImageFileNames = _releaseImageFileNames, }; ReleaseImageAssociationContainer releaseImageAssociations; - while (fetchNextReleaseImagesToUpdate(searchContext, releaseImageAssociations)) + while (fetchNextReleaseArtworksToUpdate(searchContext, releaseImageAssociations)) { if (_abortScan) return; diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp index 70376a09..07c59862 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp @@ -40,6 +40,6 @@ namespace lms::scanner bool needProcess(const ScanContext& context) const override; void process(ScanContext& context) override; - const std::vector _releaseFileNames; + const std::vector _releaseImageFileNames; }; } // namespace lms::scanner