From dd2aa49bb02eceb7f3ff1cbddb7877bac4103ed1 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 21 Jun 2025 18:13:20 +0200 Subject: [PATCH] Now resolve the preferred track artworks during scan steps --- src/libs/database/impl/Cluster.cpp | 1 + src/libs/database/impl/Migration.cpp | 93 +++++- src/libs/database/impl/PlayQueue.cpp | 1 + src/libs/database/impl/Release.cpp | 5 + src/libs/database/impl/Track.cpp | 55 ++++ src/libs/database/include/database/Track.hpp | 24 +- src/libs/database/test/Release.cpp | 12 + .../services/artwork/impl/ArtworkService.cpp | 107 +------ src/libs/services/scanner/CMakeLists.txt | 2 + .../services/scanner/impl/ScannerService.cpp | 2 + .../services/scanner/impl/scanners/Utils.hpp | 9 +- .../scanner/impl/steps/ArtworkUtils.cpp | 104 ++++++ .../scanner/impl/steps/ArtworkUtils.hpp | 41 +++ .../steps/ScanStepAssociateReleaseImages.cpp | 86 ++--- .../steps/ScanStepAssociateTrackImages.cpp | 298 ++++++++++++++++++ .../steps/ScanStepAssociateTrackImages.hpp | 40 +++ 16 files changed, 704 insertions(+), 176 deletions(-) create mode 100644 src/libs/services/scanner/impl/steps/ArtworkUtils.cpp create mode 100644 src/libs/services/scanner/impl/steps/ArtworkUtils.hpp create mode 100644 src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp create mode 100644 src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.hpp diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 44841f4f..720e040e 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -20,6 +20,7 @@ #include "database/Cluster.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Directory.hpp" #include "database/MediaLibrary.hpp" #include "database/Release.hpp" diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index e46d1ae1..1f0e7e6b 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -1231,7 +1231,6 @@ FROM tracklist)"); 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, @@ -1267,8 +1266,96 @@ FROM release)"); utils::executeCommand(*session.getDboSession(), "DROP TABLE release"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release"); + // Replaced image by artwork for track + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scan_version" integer not null, + "track_number" integer, + "disc_number" integer, + "total_track" integer, + "disc_subtitle" text not null, + "name" text not null, + "duration" integer, + "bitrate" integer not null, + "bits_per_sample" integer not null, + "channel_count" integer not null, + "sample_rate" integer not null, + "date" text, + "original_date" text, + "absolute_file_path" text not null, + "relative_file_path" text not null, + "file_stem" text not null, + "file_name" text not null, + "file_size" bigint not null, + "file_last_write" text, + "file_added" text, + "mbid" text not null, + "recording_mbid" text not null, + "copyright" text not null, + "copyright_url" text not null, + "advisory" integer not null, + "track_replay_gain" real, + "release_replay_gain" real, + "artist_display_name" text not null, + "comment" text not null, + "release_id" bigint, + "media_library_id" bigint, + "directory_id" bigint, + "preferred_artwork_id" bigint, + "preferred_media_artwork_id" bigint, + constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred, + constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred, + constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred + ))"); + + // Migrate data, with the new preferred_artwork_id and preferred_media_artwork_id fields set to null + utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup +SELECT + id, + version, + scan_version, + track_number, + disc_number, + total_track, + disc_subtitle, + name, + duration, + bitrate, + bits_per_sample, + channel_count, + sample_rate, + date, + original_date, + absolute_file_path, + relative_file_path, + file_stem, + file_name, + file_size, + file_last_write, + file_added, + mbid, + recording_mbid, + copyright, + copyright_url, + advisory, + track_replay_gain, + release_replay_gain, + artist_display_name, + comment, + release_id, + media_library_id, + directory_id, + NULL as preferred_artwork_id, + NULL as preferred_media_artwork_id +FROM track)"); + utils::executeCommand(*session.getDboSession(), "DROP TABLE track"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track"); + // 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"); + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1"); } bool doDbMigration(Session& session) @@ -1339,7 +1426,7 @@ FROM release)"); { 89, migrateFromV89 }, { 90, migrateFromV90 }, { 91, migrateFromV91 }, - { 92, migrateFromV92 } + { 92, migrateFromV92 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/PlayQueue.cpp b/src/libs/database/impl/PlayQueue.cpp index cd1af8f3..d5941f20 100644 --- a/src/libs/database/impl/PlayQueue.cpp +++ b/src/libs/database/impl/PlayQueue.cpp @@ -21,6 +21,7 @@ #include +#include "database/Artwork.hpp" #include "database/Directory.hpp" #include "database/MediaLibrary.hpp" #include "database/Release.hpp" diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index c2a39890..12c85d6b 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -706,6 +706,11 @@ namespace lms::db return ObjectPtr{ _preferredArtwork }; } + ArtworkId Release::getPreferredArtworkId() const + { + return _preferredArtwork.id(); + } + void Release::clearLabels() { _labels.clear(); diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index ab631042..a66d9c27 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -23,6 +23,7 @@ #include "core/ILogger.hpp" #include "database/Artist.hpp" +#include "database/Artwork.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" #include "database/MediaLibrary.hpp" @@ -324,6 +325,20 @@ namespace lms::db return utils::execRangeQuery(query, range); } + void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId) + { + session.checkWriteTransaction(); + + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = ? WHERE id = ?", artworkId, trackId); + } + + void Track::updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId) + { + session.checkWriteTransaction(); + + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = ? WHERE id = ?", artworkId, trackId); + } + std::vector Track::getClusters() const { return utils::fetchQueryResults(_clusters.find()); @@ -338,6 +353,26 @@ namespace lms::db return utils::fetchQueryResults(query); } + ObjectPtr Track::getMediaLibrary() const + { + return _mediaLibrary; + } + + ObjectPtr Track::getDirectory() const + { + return _directory; + } + + ObjectPtr Track::getPreferredArtwork() const + { + return _preferredArtwork; + } + + ObjectPtr Track::getPreferredMediaArtwork() const + { + return _preferredMediaArtwork; + } + RangeResults Track::findIds(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); @@ -484,6 +519,26 @@ namespace lms::db _embeddedImageLinks.insert(getDboPtr(image)); } + void Track::setMediaLibrary(ObjectPtr mediaLibrary) + { + _mediaLibrary = getDboPtr(mediaLibrary); + } + + void Track::setDirectory(ObjectPtr directory) + { + _directory = getDboPtr(directory); + } + + void Track::setPreferredArtwork(ObjectPtr artwork) + { + _preferredArtwork = getDboPtr(artwork); + } + + void Track::setPreferredMediaArtwork(ObjectPtr artwork) + { + _preferredMediaArtwork = getDboPtr(artwork); + } + std::optional Track::getYear() const { return _date.getYear(); diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index f6957c1b..e56e628e 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -35,6 +35,7 @@ #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" +#include "database/ArtworkId.hpp" #include "database/ClusterId.hpp" #include "database/DirectoryId.hpp" #include "database/Filters.hpp" @@ -50,6 +51,7 @@ namespace lms::db { class Artist; + class Artwork; class Cluster; class ClusterType; class Directory; @@ -215,6 +217,10 @@ namespace lms::db static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range = std::nullopt); + // Update utility functions + static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId); + static void updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId); + // Accessors void setScanVersion(std::size_t version) { _scanVersion = version; } void setTrackNumber(std::optional num) { _trackNumber = num; } @@ -252,8 +258,10 @@ namespace lms::db void addLyrics(const ObjectPtr& lyrics); void clearEmbeddedImageLinks(); void addEmbeddedImageLink(const ObjectPtr& link); - void setMediaLibrary(ObjectPtr mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); } - void setDirectory(ObjectPtr directory) { _directory = getDboPtr(directory); } + void setMediaLibrary(ObjectPtr mediaLibrary); + void setDirectory(ObjectPtr directory); + void setPreferredArtwork(ObjectPtr artwork); + void setPreferredMediaArtwork(ObjectPtr artwork); std::size_t getScanVersion() const { return _scanVersion; } std::optional getTrackNumber() const { return _trackNumber; } @@ -295,8 +303,10 @@ namespace lms::db ObjectPtr getRelease() const { return _release; } std::vector> getClusters() const; std::vector getClusterIds() const; - ObjectPtr getMediaLibrary() const { return _mediaLibrary; } - ObjectPtr getDirectory() const { return _directory; } + ObjectPtr getMediaLibrary() const; + ObjectPtr getDirectory() const; + ObjectPtr getPreferredArtwork() const; + ObjectPtr getPreferredMediaArtwork() const; std::vector>> getClusterGroups(const std::vector& clusterTypes, std::size_t size) const; @@ -331,11 +341,13 @@ namespace lms::db Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); - Wt::Dbo::field(a, _comment, "comment"); + Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); + Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track"); @@ -382,6 +394,8 @@ namespace lms::db Wt::Dbo::ptr _release; Wt::Dbo::ptr _mediaLibrary; Wt::Dbo::ptr _directory; + Wt::Dbo::ptr _preferredArtwork; + Wt::Dbo::ptr _preferredMediaArtwork; Wt::Dbo::collection> _trackArtistLinks; Wt::Dbo::collection> _clusters; Wt::Dbo::collection> _trackLyrics; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 995c5b21..3d3b3a11 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1220,6 +1220,18 @@ namespace lms::db::tests ASSERT_TRUE(releaseArtwork); EXPECT_EQ(releaseArtwork->getId(), artwork.getId()); } + + // Check cascade delete + { + auto transaction{ session.createWriteTransaction() }; + image.lockAndGet().remove(); + } + + { + auto transaction{ session.createReadTransaction() }; + auto releaseArtwork(release.get()->getPreferredArtwork()); + ASSERT_FALSE(releaseArtwork); + } } TEST_F(DatabaseFixture, Release_sortDateAdded) diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 11426470..c39573d6 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -39,14 +39,6 @@ namespace lms::artwork { - namespace - { - bool isImageFound(const ArtworkService::ImageFindResult& res) - { - return !std::holds_alternative(res); - } - } // namespace - std::unique_ptr createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath) { return std::make_unique(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath); @@ -170,72 +162,15 @@ namespace lms::artwork db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - { - db::TrackEmbeddedImage::FindParameters params; - params.setTrack(trackId); - params.setImageTypes({ db::ImageType::Media }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); - 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 another track of the same disc const db::Track::pointer track{ db::Track::find(session, trackId) }; if (!track) return res; - const db::ReleaseId releaseId{ track->getReleaseId() }; - if (!releaseId.isValid()) - return res; - - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(releaseId); - params.setDiscNumber(track->getDiscNumber()); - params.setImageTypes({ db::ImageType::Media }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); - 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 front cover for this track - { - db::TrackEmbeddedImage::FindParameters params; - params.setTrack(trackId); - params.setImageTypes({ db::ImageType::FrontCover }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); - 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; -#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; - - // Fallback on the first front cover found on the release - { - 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(); }); - } + const db::Artwork::pointer artwork{ track->getPreferredArtwork() }; + if (artwork && artwork->getImageId().isValid()) + res = artwork->getImageId(); + else if (artwork && artwork->getTrackEmbeddedImageId().isValid()) + res = artwork->getTrackEmbeddedImageId(); return res; } @@ -246,33 +181,15 @@ namespace lms::artwork auto transaction{ session.createReadTransaction() }; ImageFindResult res; - { - db::TrackEmbeddedImage::FindParameters params; - params.setTrack(trackId); - params.setImageTypes({ db::ImageType::Media }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); - } - - if (isImageFound(res)) + const db::Track::pointer track{ db::Track::find(session, trackId) }; + if (!track) return res; - // fallback on another track of the same disc - if (const db::Track::pointer track{ db::Track::find(session, trackId) }) - { - const db::ReleaseId releaseId{ track->getReleaseId() }; - if (releaseId.isValid()) - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(releaseId); - params.setDiscNumber(track->getDiscNumber()); - params.setImageTypes({ db::ImageType::Media }); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); - 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{ track->getPreferredMediaArtwork() }; + 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/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index cd28e4b7..62d6f0f2 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(lmsscanner STATIC impl/scanners/LyricsFileScanner.cpp impl/scanners/PlayListFileScanner.cpp impl/scanners/Utils.cpp + impl/steps/ArtworkUtils.cpp impl/steps/FileScanQueue.cpp impl/steps/ScanErrorLogger.cpp impl/steps/ScanStepArtistReconciliation.cpp @@ -15,6 +16,7 @@ add_library(lmsscanner STATIC impl/steps/ScanStepAssociateExternalLyrics.cpp impl/steps/ScanStepAssociatePlayListTracks.cpp impl/steps/ScanStepAssociateReleaseImages.cpp + impl/steps/ScanStepAssociateTrackImages.cpp impl/steps/ScanStepBase.cpp impl/steps/ScanStepCheckForDuplicatedFiles.cpp impl/steps/ScanStepCheckForRemovedFiles.cpp diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index c03cebbf..654cad04 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -41,6 +41,7 @@ #include "steps/ScanStepAssociateExternalLyrics.hpp" #include "steps/ScanStepAssociatePlayListTracks.hpp" #include "steps/ScanStepAssociateReleaseImages.hpp" +#include "steps/ScanStepAssociateTrackImages.hpp" #include "steps/ScanStepCheckForDuplicatedFiles.hpp" #include "steps/ScanStepCheckForRemovedFiles.hpp" #include "steps/ScanStepCompact.hpp" @@ -463,6 +464,7 @@ namespace lms::scanner _scanSteps.emplace_back(std::make_unique(params)); _scanSteps.emplace_back(std::make_unique(params)); _scanSteps.emplace_back(std::make_unique(params)); + _scanSteps.emplace_back(std::make_unique(params)); // must come after ScanStepAssociateReleaseImages _scanSteps.emplace_back(std::make_unique(params)); _scanSteps.emplace_back(std::make_unique(params)); _scanSteps.emplace_back(std::make_unique(params)); diff --git a/src/libs/services/scanner/impl/scanners/Utils.hpp b/src/libs/services/scanner/impl/scanners/Utils.hpp index 710e3309..bba624b8 100644 --- a/src/libs/services/scanner/impl/scanners/Utils.hpp +++ b/src/libs/services/scanner/impl/scanners/Utils.hpp @@ -30,10 +30,7 @@ namespace lms::db class Session; } // namespace lms::db -namespace lms::scanner +namespace lms::scanner::utils { - namespace utils - { - db::ObjectPtr getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr& mediaLibrary); - } // namespace utils -} // namespace lms::scanner \ No newline at end of file + db::ObjectPtr getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr& mediaLibrary); +} // namespace lms::scanner::utils \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ArtworkUtils.cpp b/src/libs/services/scanner/impl/steps/ArtworkUtils.cpp new file mode 100644 index 00000000..153aca0d --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ArtworkUtils.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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 "ArtworkUtils.hpp" + +#include "database/Artwork.hpp" +#include "database/Image.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" + +namespace lms::scanner::utils +{ + namespace + { + std::filesystem::path toPath(db::Session& session, const db::TrackEmbeddedImageId trackEmbeddedImageId) + { + session.checkReadTransaction(); + + 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, db::ImageId imageId) + { + session.checkReadTransaction(); + + db::Image::pointer image{ db::Image::find(session, imageId) }; + return image ? image->getAbsoluteFilePath() : std::filesystem::path{}; + } + + } // namespace + + db::ObjectPtr getOrCreateArtworkFromTrackEmbeddedImage(db::Session& session, db::TrackEmbeddedImageId trackEmbeddedImageId) + { + assert(trackEmbeddedImageId.isValid()); + session.checkWriteTransaction(); + + db::ObjectPtr artwork{ db::Artwork::find(session, trackEmbeddedImageId) }; + if (!artwork) + { + db::TrackEmbeddedImage::pointer trackEmbeddedImage{ db::TrackEmbeddedImage::find(session, trackEmbeddedImageId) }; + assert(trackEmbeddedImage); + artwork = session.create(trackEmbeddedImage); + } + return artwork; + } + + db::ObjectPtr getOrCreateArtworkFromImage(db::Session& session, db::ImageId imageId) + { + assert(imageId.isValid()); + session.checkWriteTransaction(); + + db::ObjectPtr artwork{ db::Artwork::find(session, imageId) }; + if (!artwork) + { + db::Image::pointer image{ db::Image::find(session, imageId) }; + assert(image); + artwork = session.create(image); + } + return artwork; + } + + std::filesystem::path toPath(db::Session& session, db::ArtworkId artworkId) + { + session.checkReadTransaction(); + + db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) }; + if (!artwork) + return std::filesystem::path{}; + + if (artwork->getTrackEmbeddedImageId().isValid()) + return toPath(session, artwork->getTrackEmbeddedImageId()); + if (artwork->getImageId().isValid()) + return toPath(session, artwork->getImageId()); + + return std::filesystem::path{}; + } + +} // namespace lms::scanner::utils \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ArtworkUtils.hpp b/src/libs/services/scanner/impl/steps/ArtworkUtils.hpp new file mode 100644 index 00000000..8d18b009 --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ArtworkUtils.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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 Artwork; + class Session; +} // namespace lms::db + +namespace lms::scanner::utils +{ + db::ObjectPtr getOrCreateArtworkFromTrackEmbeddedImage(db::Session& session, db::TrackEmbeddedImageId trackEmbeddedImageId); + db::ObjectPtr getOrCreateArtworkFromImage(db::Session& session, db::ImageId imageId); + + std::filesystem::path toPath(db::Session& session, db::ArtworkId artworkId); +} // namespace lms::scanner::utils \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index a6735963..9833701e 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -38,14 +38,15 @@ #include "database/TrackEmbeddedImage.hpp" #include "database/TrackEmbeddedImageId.hpp" +#include "ArtworkUtils.hpp" #include "ScanContext.hpp" namespace lms::scanner { namespace { - using PreferredArtwork = std::variant; - bool isSameArtwork(PreferredArtwork preferredArtwork, const db::Artwork::pointer& artwork) + using Artwork = std::variant; + bool isSameArtwork(Artwork preferredArtwork, const db::ObjectPtr& artwork) { if (std::holds_alternative(preferredArtwork)) return !artwork; @@ -59,30 +60,10 @@ namespace lms::scanner 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; - PreferredArtwork preferredArtwork; + Artwork preferredArtwork; }; using ReleaseImageAssociationContainer = std::deque; @@ -120,7 +101,7 @@ namespace lms::scanner return image; } - db::Image::pointer computeBestReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + db::Image::pointer computePreferredReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) { db::Image::pointer image; @@ -168,11 +149,11 @@ namespace lms::scanner return image; } - PreferredArtwork computeBestReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + Artwork computePreferredReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) { - const db::Image::pointer image{ computeBestReleaseImage(searchContext, release) }; + const db::Image::pointer image{ computePreferredReleaseImage(searchContext, release) }; if (image) - return PreferredArtwork{ image->getId() }; + return Artwork{ image->getId() }; // Fallback on embedded Front image db::TrackEmbeddedImageId trackEmbeddedImageId; @@ -187,7 +168,7 @@ namespace lms::scanner } if (trackEmbeddedImageId.isValid()) - return PreferredArtwork{ trackEmbeddedImageId }; + return Artwork{ trackEmbeddedImageId }; // Fallback on embedded media image { @@ -201,9 +182,9 @@ namespace lms::scanner } if (trackEmbeddedImageId.isValid()) - return PreferredArtwork{ trackEmbeddedImageId }; + return Artwork{ trackEmbeddedImageId }; - return PreferredArtwork{}; + return Artwork{}; } bool fetchNextReleaseArtworksToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations) @@ -216,7 +197,7 @@ namespace lms::scanner auto transaction{ searchContext.session.createReadTransaction() }; db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) { - const PreferredArtwork preferredArtwork{ computeBestReleaseArtwork(searchContext, release) }; + const Artwork preferredArtwork{ computePreferredReleaseArtwork(searchContext, release) }; const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() }; if (!isSameArtwork(preferredArtwork, currentPreferredArtwork)) @@ -228,30 +209,6 @@ namespace lms::scanner return releaseId != searchContext.lastRetrievedReleaseId; } - db::Artwork::pointer getOrCreateArtworkFromTrackEmbeddedImage(db::Session& session, const db::TrackEmbeddedImageId& trackEmbeddedImageId) - { - db::Artwork::pointer artwork{ db::Artwork::find(session, trackEmbeddedImageId) }; - if (!artwork) - { - db::TrackEmbeddedImage::pointer trackEmbeddedImage{ db::TrackEmbeddedImage::find(session, trackEmbeddedImageId) }; - assert(trackEmbeddedImage); - artwork = session.create(trackEmbeddedImage); - } - return artwork; - } - - db::Artwork::pointer getOrCreateArtworkFromImage(db::Session& session, const db::ImageId& imageId) - { - db::Artwork::pointer artwork{ db::Artwork::find(session, imageId) }; - if (!artwork) - { - db::Image::pointer image{ db::Image::find(session, imageId) }; - assert(image); - artwork = session.create(image); - } - return artwork; - } - void updateReleaseArtwork(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation) { db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) }; @@ -260,21 +217,16 @@ namespace lms::scanner db::Artwork::pointer artwork; if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) - { - artwork = getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); - LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork in release '" << release->getName() << "' with embedded image in track " << toPath(session, *trackEmbeddedImageId)); - } + artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); else if (const db::ImageId * imageId{ std::get_if(&releaseImageAssociation.preferredArtwork) }) - { - artwork = getOrCreateArtworkFromImage(session, *imageId); - 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() << "'"); - } + artwork = utils::getOrCreateArtworkFromImage(session, *imageId); release.modify()->setPreferredArtwork(artwork); + + if (artwork) + LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork in release '" << release->getName() << "' with image in " << utils::toPath(session, artwork->getId())); + else + LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from release '" << release->getName() << "'"); } void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations) diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp new file mode 100644 index 00000000..e1a0526e --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp @@ -0,0 +1,298 @@ +/* + * 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 "ScanStepAssociateTrackImages.hpp" + +#include +#include +#include + +#include "core/ILogger.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 "ArtworkUtils.hpp" +#include "ScanContext.hpp" + +namespace lms::scanner +{ + namespace + { + // May come from an embedded image in a track, or from what has been previously resolved for the release + using Artwork = std::variant; + bool isSameArtwork(Artwork preferredArtwork, const db::ObjectPtr& 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::ArtworkId* artworkId = std::get_if(&preferredArtwork)) + return artwork && *artworkId == artwork->getId(); + + return false; + } + + bool isValid(const Artwork& res) + { + return !std::holds_alternative(res); + } + + struct TrackImageAssociation + { + db::Track::pointer track; + Artwork preferredArtwork; + Artwork preferredMediaArtwork; + }; + using TrackImageAssociationContainer = std::deque; + + struct SearchTrackImageContext + { + db::Session& session; + db::TrackId lastRetrievedTrackId; + std::size_t processedTrackCount{}; + }; + + Artwork computePreferredTrackArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track) + { + // Try to get a media image + Artwork res; + { + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(track->getId()); + params.setImageTypes({ db::ImageType::Media }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + } + + if (isValid(res)) + return res; + + // Fallback on another track of the same disc + const db::ReleaseId releaseId{ track->getReleaseId() }; + if (!releaseId.isValid()) + return Artwork{}; + + { + db::TrackEmbeddedImage::FindParameters params; + params.setRelease(releaseId); + params.setDiscNumber(track->getDiscNumber()); + params.setImageTypes({ db::ImageType::Media }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + } + + if (isValid(res)) + return res; + + // Fallback on front cover for this track + { + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(track->getId()); + params.setImageTypes({ db::ImageType::FrontCover }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + } + + if (isValid(res)) + return res; + + // Fallback on the artwork already resolved for the release + if (const db::Release::pointer release{ db::Release::find(searchContext.session, releaseId) }) + { + db::ArtworkId artworkId{ release->getPreferredArtworkId() }; + if (artworkId.isValid()) + res = artworkId; + } + + return res; + } + + Artwork computePreferredTrackMediaArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track) + { + Artwork res; + { + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(track->getId()); + params.setImageTypes({ db::ImageType::Media }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + } + + if (isValid(res)) + return res; + + // fallback on another track of the same disc + if (const db::ReleaseId releaseId{ track->getReleaseId() }; releaseId.isValid()) + { + db::TrackEmbeddedImage::FindParameters params; + params.setRelease(releaseId); + params.setDiscNumber(track->getDiscNumber()); + params.setImageTypes({ db::ImageType::Media }); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + } + + return res; + } + + bool fetchNextTrackArtworksToUpdate(SearchTrackImageContext& searchContext, TrackImageAssociationContainer& trackImageAssociations) + { + const db::TrackId trackId{ searchContext.lastRetrievedTrackId }; + + { + constexpr std::size_t readBatchSize{ 100 }; + + auto transaction{ searchContext.session.createReadTransaction() }; + + db::Track::find(searchContext.session, searchContext.lastRetrievedTrackId, readBatchSize, [&](const db::Track::pointer& track) { + const Artwork preferredArtwork{ computePreferredTrackArtwork(searchContext, track) }; + const Artwork preferredMediaArtwork{ computePreferredTrackMediaArtwork(searchContext, track) }; + + const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() }; + const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() }; + + if (!isSameArtwork(preferredArtwork, currentPreferredArtwork) + || !isSameArtwork(preferredMediaArtwork, currentPreferredMediaArtwork)) + { + trackImageAssociations.push_back(TrackImageAssociation{ track, preferredArtwork, preferredMediaArtwork }); + } + + searchContext.processedTrackCount++; + }); + } + + return trackId != searchContext.lastRetrievedTrackId; + } + + void updateTrackPreferredArtwork(db::Session& session, db::Track::pointer& track, Artwork preferredArtwork) + { + db::Artwork::pointer artwork; + if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&preferredArtwork) }) + artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); + else if (const db::ArtworkId * artworkId{ std::get_if(&preferredArtwork) }) + artwork = db::Artwork::find(session, *artworkId); + + // Using track.modify() is quite CPU intensive as the track class has too many fields + db::Track::updatePreferredArtwork(session, track->getId(), artwork->getId()); + if (artwork) + LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork in track " << track->getAbsoluteFilePath() << " with image in " << utils::toPath(session, artwork->getId())); + else + LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from track " << track->getAbsoluteFilePath()); + } + + void updateTrackPreferredMediaArtwork(db::Session& session, db::Track::pointer& track, Artwork preferredArtwork) + { + db::Artwork::pointer artwork; + if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&preferredArtwork) }) + artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); + else if (const db::ArtworkId * artworkId{ std::get_if(&preferredArtwork) }) + artwork = db::Artwork::find(session, *artworkId); + + // Using track.modify() is quite CPU intensive as the track class has too many fields + db::Track::updatePreferredMediaArtwork(session, track->getId(), artwork->getId()); + if (artwork) + LMS_LOG(DBUPDATER, DEBUG, "Updated preferred media artwork in track '" << track->getAbsoluteFilePath() << "' with image in " << utils::toPath(session, artwork->getId())); + else + LMS_LOG(DBUPDATER, DEBUG, "Removed preferred media artwork from track '" << track->getAbsoluteFilePath() << "'"); + } + + void updateTrackPreferredArtworks(db::Session& session, const TrackImageAssociation& trackImageAssociation) + { + db::Track::pointer track{ trackImageAssociation.track }; + + { + const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() }; + if (!isSameArtwork(trackImageAssociation.preferredArtwork, currentPreferredArtwork)) + updateTrackPreferredArtwork(session, track, trackImageAssociation.preferredArtwork); + } + + { + const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() }; + if (!isSameArtwork(trackImageAssociation.preferredMediaArtwork, currentPreferredMediaArtwork)) + updateTrackPreferredMediaArtwork(session, track, trackImageAssociation.preferredMediaArtwork); + } + } + + void updateTrackPreferredArtworks(db::Session& session, TrackImageAssociationContainer& imageAssociations) + { + constexpr std::size_t writeBatchSize{ 50 }; + + while (!imageAssociations.empty()) + { + auto transaction{ session.createWriteTransaction() }; + + for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i) + { + updateTrackPreferredArtworks(session, imageAssociations.front()); + imageAssociations.pop_front(); + } + } + } + } // namespace + + ScanStepAssociateTrackImages::ScanStepAssociateTrackImages(InitParams& initParams) + : ScanStepBase{ initParams } + { + } + + bool ScanStepAssociateTrackImages::needProcess(const ScanContext& context) const + { + return context.stats.nbChanges() > 0; + } + + void ScanStepAssociateTrackImages::process(ScanContext& context) + { + auto& session{ _db.getTLSSession() }; + + { + auto transaction{ session.createReadTransaction() }; + context.currentStepStats.totalElems = db::Track::getCount(session); + } + + SearchTrackImageContext searchContext{ + .session = session, + .lastRetrievedTrackId = {}, + }; + + TrackImageAssociationContainer trackImageAssociations; + while (fetchNextTrackArtworksToUpdate(searchContext, trackImageAssociations)) + { + if (_abortScan) + return; + + updateTrackPreferredArtworks(session, trackImageAssociations); + context.currentStepStats.processedElems = searchContext.processedTrackCount; + _progressCallback(context.currentStepStats); + } + } +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.hpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.hpp new file mode 100644 index 00000000..14ecb277 --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 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 "ScanStepBase.hpp" + +namespace lms::scanner +{ + class ScanStepAssociateTrackImages : public ScanStepBase + { + public: + ScanStepAssociateTrackImages(InitParams& initParams); + ~ScanStepAssociateTrackImages() override = default; + ScanStepAssociateTrackImages(const ScanStepAssociateTrackImages&) = delete; + ScanStepAssociateTrackImages& operator=(const ScanStepAssociateTrackImages&) = delete; + + private: + ScanStep getStep() const override { return ScanStep::AssociateReleaseImages; } + core::LiteralString getStepName() const override { return "Associate track images"; } + bool needProcess(const ScanContext& context) const override; + void process(ScanContext& context) override; + }; +} // namespace lms::scanner