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/messages.xml b/approot/messages.xml index 056dddeb..3616320f 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -133,6 +133,7 @@ Associating external lyrics: {1}%... Associating playlist tracks: {1}%... Associating release images: {1}%... +Associating track images: {1}%... Checking for duplicate files... {1} files Checking for removed files... {1}% Compacting database... diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 325890c3..95a4e7e5 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -133,6 +133,7 @@ Association des paroles externes: {1}%... Association des pistes des listes de lectures: {1}%... Asignando imágenes de los álbumes: {1}%... +Asignando imágenes de las pistas: {1}%... Comprobando ficheros duplicados ... {1} ficheros Comprobando ficheros eliminados... {1}% Compactando la base de datos... diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 5fea473e..1bad1e5b 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -133,6 +133,7 @@ Association des paroles externes: {1}%... Association des pistes des listes de lectures: {1}%... Association des images des albums: {1}%... +Association des images des pistes: {1}%... Vérification des fichiers dupliqués... {1} fichiers Vérification des fichiers supprimés... {1}% Compactage de la base de données... diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 91935ade..f7b78df6 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -133,6 +133,7 @@ Associazione dei testi esterni: {1}%... Associando brani della playlist: {1}%... Associando immagini degli album: {1}%... +Associando immagini delle tracce: {1}%... Controllo duplicati... {1} files Controllo file... {1}% Compattazione del database... diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 78c2d88b..6700c280 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -142,6 +142,7 @@ Kojarzenie zewnętrznych tekstów: {1}%... Kojarzenie utworów z playlisty: {1}%... Kojarzenie obrazów albumów: {1}%... +Kojarzenie obrazów ścieżek: {1}%... Sprawdzanie duplikatów... {1} plik Sprawdzanie duplikatów... {1} pliki diff --git a/approot/release.xml b/approot/release.xml index 0dbbc8ed..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}]${}

@@ -54,8 +54,11 @@ -
-

${disc-title}

+

+ ${} +
${artwork class="Lms-cursor-pointer"}
+ ${
} +
${disc-title}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
-

+ ${tracks class="d-grid gap-1 Lms-row-container"}
diff --git a/src/libs/core/impl/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp index 561fda0f..fd23bdd0 100644 --- a/src/libs/core/impl/FileResourceHandler.cpp +++ b/src/libs/core/impl/FileResourceHandler.cpp @@ -19,6 +19,8 @@ #include "FileResourceHandler.hpp" +#include + #include "core/ILogger.hpp" #include "core/MimeTypes.hpp" @@ -99,7 +101,7 @@ namespace lms::core } // end initial response setup ::uint64_t restSize = _beyondLastByte - _offset; - ::uint64_t pieceSize = std::min(restSize, _chunkSize); + ::uint64_t pieceSize = std::min(restSize, static_cast<::uint64_t>(_chunkSize)); std::vector buf(pieceSize); 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/Artist.cpp b/src/libs/database/impl/Artist.cpp index ee61e067..57ff3c50 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -21,9 +21,9 @@ #include #include "core/ILogger.hpp" +#include "database/Artwork.hpp" #include "database/Cluster.hpp" #include "database/Directory.hpp" -#include "database/Image.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" @@ -315,9 +315,14 @@ AND NOT EXISTS ( return getMBID().has_value(); } - ObjectPtr Artist::getImage() const + ObjectPtr Artist::getPreferredArtwork() const { - return ObjectPtr{ _image }; + return ObjectPtr{ _preferredArtwork }; + } + + ArtworkId Artist::getPreferredArtworkId() const + { + return _preferredArtwork.id(); } RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const @@ -416,8 +421,8 @@ AND NOT EXISTS ( LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'"); } - void Artist::setImage(ObjectPtr image) + void Artist::setPreferredArtwork(ObjectPtr artwork) { - _image = getDboPtr(image); + _preferredArtwork = getDboPtr(artwork); } } // namespace lms::db diff --git a/src/libs/database/impl/Artwork.cpp b/src/libs/database/impl/Artwork.cpp new file mode 100644 index 00000000..e9264f06 --- /dev/null +++ b/src/libs/database/impl/Artwork.cpp @@ -0,0 +1,90 @@ +/* + * 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 + +#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").where("a.track_embedded_image_id = ?").bind(id)); + } + + Artwork::pointer Artwork::find(Session& session, ImageId id) + { + session.checkReadTransaction(); + 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/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/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 47fbbde9..63a67cc6 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{ 91 }; + 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)"); @@ -1213,6 +1213,173 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1"); } + void migrateFromV91(Session& session) + { + dropIndexes(session); + 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 + 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"); + + // 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"); + + // Replaced image by artwork for artist + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "sort_name" text not null, + "mbid" text not null, + "preferred_artwork_id" bigint, + constraint "fk_artist_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 artist_backup +SELECT + id, + version, + name, + sort_name, + mbid, + NULL as preferred_artwork_id +FROM artist)"); + + utils::executeCommand(*session.getDboSession(), "DROP TABLE artist"); + utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist"); + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1280,6 +1447,8 @@ FROM tracklist)"); { 88, migrateFromV88 }, { 89, migrateFromV89 }, { 90, migrateFromV90 }, + { 91, migrateFromV91 }, + { 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 4d367881..12c85d6b 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,9 +701,14 @@ namespace lms::db return utils::fetchQueryResults(query); } - ObjectPtr Release::getImage() const + ObjectPtr Release::getPreferredArtwork() const { - return ObjectPtr{ _image }; + return ObjectPtr{ _preferredArtwork }; + } + + ArtworkId Release::getPreferredArtworkId() const + { + return _preferredArtwork.id(); } void Release::clearLabels() @@ -736,9 +741,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 0511fb97..deac39c9 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"); @@ -196,7 +198,6 @@ namespace lms::db { auto transaction{ createWriteTransaction() }; utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); @@ -206,6 +207,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)"); @@ -229,7 +234,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_type_track_embedded_image_id_track_id_idx ON track_embedded_image_link(type,track_embedded_image_id,track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)"); @@ -256,7 +261,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)"); @@ -397,6 +401,16 @@ namespace lms::db LMS_LOG(DB, INFO, "Analyze complete!"); } + bool Session::areAllTablesEmpty() + { + const std::vector entryList{ utils::fetchQueryResults(_session.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")) }; + + return std::all_of(entryList.cbegin(), entryList.cend(), [this](const std::string& entry) { + const auto count{ utils::fetchQuerySingleResult(_session.query("SELECT COUNT(*) FROM " + entry)) }; + return count == 0; + }); + } + void Session::retrieveEntriesToAnalyze(std::vector& entryList) { auto transaction{ createReadTransaction() }; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index ab631042..ae308b53 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,26 @@ namespace lms::db return utils::execRangeQuery(query, range); } + void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId) + { + session.checkWriteTransaction(); + + if (artworkId.isValid()) + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = ? WHERE id = ?", artworkId, trackId); + else + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = NULL WHERE id = ?", trackId); + } + + void Track::updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId) + { + session.checkWriteTransaction(); + + if (artworkId.isValid()) + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = ? WHERE id = ?", artworkId, trackId); + else + utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = NULL WHERE id = ?", trackId); + } + std::vector Track::getClusters() const { return utils::fetchQueryResults(_clusters.find()); @@ -338,6 +359,36 @@ 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; + } + + 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(); @@ -484,6 +535,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/impl/TrackEmbeddedImage.cpp b/src/libs/database/impl/TrackEmbeddedImage.cpp index c802d4b8..9b844b8d 100644 --- a/src/libs/database/impl/TrackEmbeddedImage.cpp +++ b/src/libs/database/impl/TrackEmbeddedImage.cpp @@ -38,24 +38,50 @@ namespace lms::db auto query{ session.getDboSession()->query>("SELECT t_e_i FROM track_embedded_image t_e_i") }; - if (params.isPreferred + if (params.artist.isValid() + || params.discNumber.has_value() || params.track.isValid() || params.release.isValid() || params.trackList.isValid() - || params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize) + || !params.imageTypes.empty() + || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc + || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc) { query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id"); - if (params.isPreferred) - query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value()); + if (params.artist.isValid()) + { + query.join("track_artist_link t_a_l ON t_a_l.track_id = t_e_i_l.track_id"); + query.where("t_a_l.artist_id = ?").bind(params.artist); + + if (!params.trackArtistLinkTypes.empty()) + { + std::string clause{ "t_a_l.type IN (" }; + for (const auto& type : params.trackArtistLinkTypes) + { + if (clause.back() != '(') + clause += ","; + clause += "?"; + query.bind(type); + } + clause += ")"; + query.where(clause); + } + } if (params.track.isValid()) query.where("t_e_i_l.track_id = ?").bind(params.track); - if (params.release.isValid()) + if (params.release.isValid() + || params.discNumber.has_value() + || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc + || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc) { query.join("track t ON t_e_i_l.track_id = t.id"); - query.where("t.release_id = ?").bind(params.release); + if (params.release.isValid()) + query.where("t.release_id = ?").bind(params.release); + if (params.discNumber.has_value()) + query.where("t.disc_number = ?").bind(params.discNumber.value()); } if (params.trackList.isValid()) @@ -63,14 +89,38 @@ namespace lms::db query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id"); query.where("t_l_e.tracklist_id = ?").bind(params.trackList); } + + if (!params.imageTypes.empty()) + { + std::string clause{ "t_e_i_l.type IN (" }; + for (const auto& type : params.imageTypes) + { + if (clause.back() != '(') + clause += ","; + clause += "?"; + query.bind(type); + } + clause += ")"; + query.where(clause); + } } switch (params.sortMethod) { case TrackEmbeddedImageSortMethod::None: break; - case TrackEmbeddedImageSortMethod::FrontCoverAndSize: - query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover); + case TrackEmbeddedImageSortMethod::SizeDesc: + query.orderBy("t_e_i.size DESC"); + break; + case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc: + query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC"); + break; + case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc: + query.orderBy("t.track_number, t_e_i.size DESC"); + break; + case TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc: + assert(params.trackList.isValid()); + query.orderBy("t_l_e.id, t_e_i.size DESC"); break; } diff --git a/src/libs/database/impl/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/TrackEmbeddedImageLink.cpp index cecc6f2d..300d2ed3 100644 --- a/src/libs/database/impl/TrackEmbeddedImageLink.cpp +++ b/src/libs/database/impl/TrackEmbeddedImageLink.cpp @@ -54,6 +54,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } + void TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function visitor) + { + auto query{ session.getDboSession()->find() }; + query.where("track_embedded_image_id = ?").bind(trackEmbeddedImageId); + + return utils::forEachQueryResult(query, visitor); + } + ObjectPtr TrackEmbeddedImageLink::getTrack() const { return _track; diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index d2b24cdf..86972a75 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -31,6 +31,7 @@ #include "core/EnumSet.hpp" #include "core/UUID.hpp" #include "database/ArtistId.hpp" +#include "database/ArtworkId.hpp" #include "database/ClusterId.hpp" #include "database/Filters.hpp" #include "database/MediaLibraryId.hpp" @@ -42,10 +43,9 @@ namespace lms::db { - + class Artwork; class Cluster; class ClusterType; - class Image; class Release; class Session; class StarredArtist; @@ -136,7 +136,8 @@ namespace lms::db const std::string& getSortName() const { return _sortName; } std::optional getMBID() const; bool hasMBID() const; - ObjectPtr getImage() const; + ObjectPtr getPreferredArtwork() const; + ArtworkId getPreferredArtworkId() const; void visitLinks(std::function& link)> visitor) const; // No artistLinkTypes means get them all @@ -150,7 +151,7 @@ namespace lms::db void setName(std::string_view name); void setMBID(const std::optional& mbid) { _mbid = mbid ? mbid->getAsString() : ""; } void setSortName(std::string_view sortName); - void setImage(ObjectPtr image); + void setPreferredArtwork(ObjectPtr artwork); template void persist(Action& a) @@ -159,7 +160,7 @@ namespace lms::db Wt::Dbo::field(a, _sortName, "sort_name"); Wt::Dbo::field(a, _mbid, "mbid"); - Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull); + Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); } @@ -176,7 +177,7 @@ namespace lms::db std::string _sortName; std::string _mbid; // Musicbrainz Identifier - Wt::Dbo::ptr _image; + Wt::Dbo::ptr _preferredArtwork; Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist Wt::Dbo::collection> _starredArtists; // starred entries for this artist }; 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..581dcc00 --- /dev/null +++ b/src/libs/database/include/database/Artwork.hpp @@ -0,0 +1,69 @@ +/* + * 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 + +#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(); } + Wt::WDateTime getLastWrittenTime() const; + + 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..5361fabc 100644 --- a/src/libs/database/include/database/Image.hpp +++ b/src/libs/database/include/database/Image.hpp @@ -32,9 +32,7 @@ namespace lms::db { - class Artist; class Directory; - class Release; class Session; class Image final : public Object @@ -100,8 +98,6 @@ namespace lms::db Wt::Dbo::field(a, _width, "width"); 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); } @@ -117,8 +113,6 @@ namespace lms::db int _width{}; 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 885b9672..1f55173a 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -31,6 +31,7 @@ #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" @@ -45,9 +46,9 @@ namespace lms::db { class Artist; + class Artwork; class Cluster; class ClusterType; - class Image; class Release; class Session; class Track; @@ -291,7 +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; + ObjectPtr getPreferredArtwork() const; + ArtworkId getPreferredArtworkId() const; // Setters void setName(std::string_view name) { _name = name; } @@ -309,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; @@ -331,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); @@ -359,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/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index af85f48b..e1b4cd66 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -96,6 +96,8 @@ namespace lms::db void retrieveEntriesToAnalyze(std::vector& entryList); void analyzeEntry(const std::string& entry); + bool areAllTablesEmpty(); // need to acquire a read transaction + void prepareTablesIfNeeded(); // need to run only once at startup bool migrateSchemaIfNeeded(); // returns true if migration was performed void createIndexesIfNeeded(); diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index f6957c1b..f7b4cf80 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,12 @@ 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; + ArtworkId getPreferredArtworkId() const; + ObjectPtr getPreferredMediaArtwork() const; + ArtworkId getPreferredMediaArtworkId() const; std::vector>> getClusterGroups(const std::vector& clusterTypes, std::size_t size) const; @@ -331,11 +343,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 +396,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/include/database/TrackEmbeddedImage.hpp b/src/libs/database/include/database/TrackEmbeddedImage.hpp index b90ed6fc..47e8fae8 100644 --- a/src/libs/database/include/database/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/TrackEmbeddedImage.hpp @@ -25,6 +25,8 @@ #include +#include "core/EnumSet.hpp" +#include "database/ArtistId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" #include "database/TrackEmbeddedImageId.hpp" @@ -46,10 +48,13 @@ namespace lms::db struct FindParameters { std::optional range; + ArtistId artist; + core::EnumSet trackArtistLinkTypes; TrackId track; ReleaseId release; + std::optional discNumber; TrackListId trackList; - std::optional isPreferred; + core::EnumSet imageTypes; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; FindParameters& setRange(std::optional _range) @@ -57,6 +62,13 @@ namespace lms::db range = _range; return *this; } + FindParameters& setArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}) + { + artist = _artist; + trackArtistLinkTypes = _trackArtistLinkTypes; + return *this; + } + FindParameters& setTrack(TrackId _track) { track = _track; @@ -67,14 +79,19 @@ namespace lms::db release = _release; return *this; } + FindParameters& setDiscNumber(std::optional _discNumber) + { + discNumber = _discNumber; + return *this; + } FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } - FindParameters& setIsPreferred(std::optional _isPreferred) + FindParameters& setImageTypes(core::EnumSet _imageTypes) { - isPreferred = _isPreferred; + imageTypes = _imageTypes; return *this; } FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod) diff --git a/src/libs/database/include/database/TrackEmbeddedImageLink.hpp b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp index e8b66534..77dd0d2e 100644 --- a/src/libs/database/include/database/TrackEmbeddedImageLink.hpp +++ b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp @@ -25,6 +25,7 @@ #include #include "database/Object.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackEmbeddedImageLinkId.hpp" #include "database/Types.hpp" @@ -44,18 +45,17 @@ namespace lms::db // find static std::size_t getCount(Session& session); static pointer find(Session& session, TrackEmbeddedImageLinkId id); + static void find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function visitor); // getters ObjectPtr getTrack() const; ObjectPtr getImage() const; std::size_t getIndex() const { return _index; } - bool isPreferred() const { return _isPreferred; } ImageType getType() const { return _type; } std::string_view getDescription() const { return _description; } // setters void setIndex(std::size_t index) { _index = static_cast(index); } - void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; } void setType(ImageType type) { _type = type; } void setDescription(std::string_view description) { _description = description; } @@ -63,7 +63,6 @@ namespace lms::db void persist(Action& a) { Wt::Dbo::field(a, _index, "index"); - Wt::Dbo::field(a, _isPreferred, "is_preferred"); Wt::Dbo::field(a, _type, "type"); Wt::Dbo::field(a, _description, "description"); @@ -78,7 +77,6 @@ namespace lms::db static pointer create(Session& session, ObjectPtr track, ObjectPtr image); int _index{}; // index within the track - bool _isPreferred{}; ImageType _type{ ImageType::Unknown }; std::string _description; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 043ade8e..f7fb08dd 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -171,7 +172,10 @@ namespace lms::db enum class TrackEmbeddedImageSortMethod { None, - FrontCoverAndSize, + SizeDesc, + TrackNumberThenSizeDesc, + DiscNumberThenTrackNumberThenSizeDesc, + TrackListIndexAscThenSizeDesc, }; enum class TrackListSortMethod diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index f7228502..1b82997b 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -19,10 +19,12 @@ #include "Common.hpp" +#include "database/Artwork.hpp" #include "database/Image.hpp" namespace lms::db::tests { + using ScopedArtwork = ScopedEntity; using ScopedImage = ScopedEntity; TEST_F(DatabaseFixture, Artist) @@ -711,27 +713,28 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Artist_image) + TEST_F(DatabaseFixture, Artist_artwork) { - ScopedArtist release{ session, "MyArtist" }; + ScopedImage image{ session, "/image1.jpg" }; + ScopedArtwork artwork{ session, image.lockAndGet() }; + + ScopedArtist artist{ session, "MyArtist" }; { auto transaction{ session.createReadTransaction() }; - EXPECT_FALSE(release.get()->getImage()); + EXPECT_FALSE(artist.get()->getPreferredArtwork()); } - ScopedImage image{ session, "/myImage" }; - { auto transaction{ session.createWriteTransaction() }; - release.get().modify()->setImage(image.get()); + artist.get().modify()->setPreferredArtwork(artwork.get()); } { auto transaction{ session.createReadTransaction() }; - auto artistImage(release.get()->getImage()); - ASSERT_TRUE(artistImage); - EXPECT_EQ(artistImage->getId(), image.getId()); + auto artistArtwork(artist.get()->getPreferredArtwork()); + ASSERT_TRUE(artistArtwork); + EXPECT_EQ(artistArtwork->getId(), artwork.getId()); } } 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/database/test/Common.cpp b/src/libs/database/test/Common.cpp index 67b62159..2a59b829 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -19,23 +19,10 @@ #include "Common.hpp" -#include "database/Artist.hpp" -#include "database/Cluster.hpp" #include "database/Db.hpp" -#include "database/Image.hpp" -#include "database/Listen.hpp" -#include "database/MediaLibrary.hpp" #include "database/Release.hpp" #include "database/Session.hpp" -#include "database/StarredArtist.hpp" -#include "database/StarredRelease.hpp" -#include "database/StarredTrack.hpp" -#include "database/Track.hpp" -#include "database/TrackArtistLink.hpp" -#include "database/TrackBookmark.hpp" -#include "database/TrackList.hpp" #include "database/Types.hpp" -#include "database/User.hpp" namespace lms::db::tests { @@ -75,24 +62,9 @@ namespace lms::db::tests { using namespace db; - auto transaction{ session.createWriteTransaction() }; + auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::getCount(session), 0); - EXPECT_EQ(Cluster::getCount(session), 0); - EXPECT_EQ(ClusterType::getCount(session), 0); - EXPECT_EQ(Label::getCount(session), 0); - EXPECT_EQ(Listen::getCount(session), 0); - EXPECT_EQ(Image::getCount(session), 0); - EXPECT_EQ(MediaLibrary::getCount(session), 0); - EXPECT_EQ(Release::getCount(session), 0); - EXPECT_EQ(ReleaseType::getCount(session), 0); - EXPECT_EQ(StarredArtist::getCount(session), 0); - EXPECT_EQ(StarredRelease::getCount(session), 0); - EXPECT_EQ(StarredTrack::getCount(session), 0); - EXPECT_EQ(Track::getCount(session), 0); - EXPECT_EQ(TrackBookmark::getCount(session), 0); - EXPECT_EQ(TrackList::getCount(session), 0); - EXPECT_EQ(User::getCount(session), 0); + EXPECT_TRUE(session.areAllTablesEmpty()); } TEST_F(DatabaseFixture, vacuum) diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index c2e48600..3d3b3a11 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,40 @@ 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()); + } + + // Check cascade delete + { + auto transaction{ session.createWriteTransaction() }; + image.lockAndGet().remove(); + } + + { + auto transaction{ session.createReadTransaction() }; + auto releaseArtwork(release.get()->getPreferredArtwork()); + ASSERT_FALSE(releaseArtwork); } } diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 8d65c4ba..3f1744e0 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -21,8 +21,14 @@ #include +#include "database/Artwork.hpp" +#include "database/Image.hpp" + namespace lms::db::tests { + using ScopedArtwork = ScopedEntity; + using ScopedImage = ScopedEntity; + TEST_F(DatabaseFixture, Track) { { @@ -439,4 +445,41 @@ namespace lms::db::tests EXPECT_EQ(tracks.results[3], track3.getId()); } } + + TEST_F(DatabaseFixture, Track_updateArtworks) + { + ScopedTrack track{ session }; + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{}); + EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{}); + } + + ScopedImage image1{ session, "/image1.jpg" }; + ScopedArtwork artwork1{ session, image1.lockAndGet() }; + ScopedImage image2{ session, "/image2.jpg" }; + ScopedArtwork artwork2{ session, image2.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + Track::updatePreferredArtwork(session, track->getId(), artwork1->getId()); + Track::updatePreferredMediaArtwork(session, track->getId(), artwork2->getId()); + } + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getPreferredArtwork()->getId(), artwork1->getId()); + EXPECT_EQ(track->getPreferredMediaArtwork()->getId(), artwork2->getId()); + } + + { + auto transaction{ session.createWriteTransaction() }; + Track::updatePreferredArtwork(session, track->getId(), ArtworkId{}); + Track::updatePreferredMediaArtwork(session, track->getId(), ArtworkId{}); + } + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{}); + EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{}); + } + } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp index 3ba25be0..16169592 100644 --- a/src/libs/database/test/TrackEmbeddedImage.cpp +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -21,6 +21,7 @@ #include "database/TrackEmbeddedImage.hpp" #include "database/TrackEmbeddedImageLink.hpp" +#include "database/TrackList.hpp" #include "database/Types.hpp" #include "Common.hpp" @@ -118,23 +119,6 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); - - bool visited{}; - TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); - EXPECT_FALSE(visited); - } - - { - auto transaction{ session.createWriteTransaction() }; - link.get().modify()->setIsPreferred(true); - } - - { - auto transaction{ session.createReadTransaction() }; - - TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -142,15 +126,8 @@ namespace lms::db::tests } { - auto transaction{ session.createReadTransaction() }; - - TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); - params.setRelease(release.getId()); - - bool visited{}; - TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); - EXPECT_FALSE(visited); + auto transaction{ session.createWriteTransaction() }; + link.get().modify()->setType(ImageType::FrontCover); } { @@ -162,9 +139,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); params.setRelease(release.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -175,9 +151,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); params.setTrack(track.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -185,6 +160,159 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts) + { + ScopedTrackEmbeddedImage image1{ session }; + ScopedTrackEmbeddedImage image2{ session }; + ScopedTrackEmbeddedImage image3{ session }; + ScopedTrackEmbeddedImage image4{ session }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedRelease release{ session, "MyRelease" }; + ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() }; + ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() }; + ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() }; + ScopedTrackEmbeddedImageLink link4{ session, track2.lockAndGet(), image4.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setRelease(release.get()); + track1.get().modify()->setTrackNumber(2); + + link1.get().modify()->setType(ImageType::FrontCover); + image1.get().modify()->setSize(750); + link2.get().modify()->setType(ImageType::Media); + image2.get().modify()->setSize(1000); + link3.get().modify()->setType(ImageType::Media); + image3.get().modify()->setSize(2000); + + track2.get().modify()->setRelease(release.get()); + track2.get().modify()->setTrackNumber(1); + + link4.get().modify()->setType(ImageType::Media); + image4.get().modify()->setSize(1500); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setRelease(release.getId()); + params.setImageTypes({ ImageType::Media }); + params.setSortMethod(TrackEmbeddedImageSortMethod::SizeDesc); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image3.getId()); + EXPECT_EQ(visitedIds[1], image4.getId()); + EXPECT_EQ(visitedIds[2], image2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setRelease(release.getId()); + params.setImageTypes({ ImageType::Media }); + params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image4.getId()); + EXPECT_EQ(visitedIds[1], image3.getId()); + EXPECT_EQ(visitedIds[2], image2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setRelease(release.getId()); + params.setImageTypes({ ImageType::Media }); + params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image4.getId()); + EXPECT_EQ(visitedIds[1], image3.getId()); + EXPECT_EQ(visitedIds[2], image2.getId()); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist) + { + ScopedTrackEmbeddedImage image{ session }; + ScopedTrack track{ session }; + ScopedArtist artist{ session, "MyArtist" }; + ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setArtist(artist.getId()); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); + ASSERT_TRUE(visited); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist }); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); + ASSERT_FALSE(visited); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_discNumber) + { + ScopedTrackEmbeddedImage image{ session }; + ScopedTrack track{ session }; + ScopedRelease release{ session, "MyRelease" }; + ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + track.get().modify()->setDiscNumber(1); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setDiscNumber(1); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); + ASSERT_TRUE(visited); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setDiscNumber(2); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); + ASSERT_FALSE(visited); + } + } + TEST_F(DatabaseFixture, Track_findByEmbeddedImage) { ScopedTrackEmbeddedImage image{ session }; @@ -279,5 +407,54 @@ namespace lms::db::tests EXPECT_EQ(img->getType(), ImageType::FrontCover); EXPECT_EQ(img->getDescription(), "MyDesc"); } + + { + auto transaction{ session.createReadTransaction() }; + + bool visited{}; + TrackEmbeddedImageLink::find(session, image->getId(), [&](const TrackEmbeddedImageLink::pointer& link) { + EXPECT_EQ(link->getIndex(), 2); + EXPECT_EQ(link->getType(), ImageType::FrontCover); + EXPECT_EQ(link->getDescription(), "MyDesc"); + EXPECT_EQ(link->getTrack(), track.get()); + + visited = true; + }); + EXPECT_TRUE(visited); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_TrackList) + { + ScopedTrackList trackList{ session, "MytrackList", TrackListType::PlayList }; + + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrackEmbeddedImage image1{ session }; + ScopedTrackEmbeddedImageLink imageLink1{ session, track1.lockAndGet(), image1.lockAndGet() }; + ScopedTrackEmbeddedImage image2{ session }; + ScopedTrackEmbeddedImageLink imageLink2{ session, track2.lockAndGet(), image2.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track2.get(), trackList.get()); + session.create(track1.get(), trackList.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setTrackList(trackList.getId()); + params.setSortMethod(TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { + visitedIds.push_back(image->getId()); + }); + ASSERT_EQ(visitedIds.size(), 2); + EXPECT_EQ(visitedIds[0], image2.getId()); + EXPECT_EQ(visitedIds[1], image1.getId()); + } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index a1b83427..fa3a7467 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -19,31 +19,27 @@ #include "ArtworkService.hpp" -#include -#include - #include "core/IConfig.hpp" #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/ImageId.hpp" +#include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" +#include "database/TrackList.hpp" #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" #include "metadata/IAudioFileParser.hpp" -namespace lms::cover +namespace lms::artwork { - namespace - { - bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions) - { - return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); - } - } // 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); @@ -92,59 +88,28 @@ namespace lms::cover 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; } - bool ArtworkService::checkImageFile(const std::filesystem::path& filePath) + std::unique_ptr ArtworkService::getTrackImage(const std::filesystem::path& p, std::size_t index, std::optional width) const { - std::error_code ec; - - if (!isFileSupported(filePath, _fileExtensions)) - return false; - - if (!std::filesystem::exists(filePath, ec)) - return false; - - if (!std::filesystem::is_regular_file(filePath, ec)) - return false; - - return true; - } - - std::unique_ptr ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional width) const - { - struct CandidateImage - { - std::unique_ptr image; - bool isFront{}; - std::size_t index; - - // > means is better candidate - bool operator>(const CandidateImage& other) const - { - if (!isFront && other.isFront) - return false; - if (isFront && !other.isFront) - return true; - - return index < other.index; - } - }; - - std::vector candidateImages; - std::size_t pictureIndex{}; + std::unique_ptr image; try { + std::size_t currentIndex{}; + _audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) { - std::unique_ptr image; + if (currentIndex++ != index) + return; + try { if (!width) @@ -162,8 +127,6 @@ namespace lms::cover { LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what()); } - - candidateImages.emplace_back(CandidateImage{ .image = std::move(image), .isFront = parsedImage.type == metadata::Image::Type::FrontCover, .index = pictureIndex++ }); }); } catch (const metadata::Exception& e) @@ -171,63 +134,108 @@ namespace lms::cover LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what()); } - std::stable_sort(std::begin(candidateImages), std::end(candidateImages), std::greater<>()); - if (!candidateImages.empty()) - return std::move(candidateImages.front().image); + return image; + } - return {}; + 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() }; + + db::TrackList::pointer trackList{ db::TrackList::find(session, trackListId) }; + if (!trackList) + return artworkId; + + 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 + } + + if (track->getPreferredArtworkId().isValid()) + { + artworkId = track->getPreferredArtworkId(); + break; // stop iteration + } + } + + return artworkId; + } + + std::shared_ptr ArtworkService::getImage(db::ArtworkId artworkId, std::optional width) + { + const ImageCache::EntryDesc cacheEntryDesc{ artworkId, width }; + + std::shared_ptr image{ _cache.getImage(cacheEntryDesc) }; + if (image) + return image; + + db::TrackEmbeddedImageId trackEmbeddedImageId; + db::ImageId imageId; + + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) }; + if (artwork) + { + trackEmbeddedImageId = artwork->getTrackEmbeddedImageId(); + imageId = artwork->getImageId(); + } + } + + if (trackEmbeddedImageId.isValid()) + image = getTrackEmbeddedImage(trackEmbeddedImageId, width); + else if (imageId.isValid()) + image = getImage(imageId, width); + + if (image) + _cache.addImage(cacheEntryDesc, image); + + 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; - std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; - if (cover) - return cover; - - std::filesystem::path trackFile; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - db::Track::FindParameters params; - params.setEmbeddedImage(trackEmbeddedImageId); - db::Track::find(session, params, [&](const db::Track::pointer& track) { - if (!cover) - cover = getTrackImage(track->getAbsoluteFilePath(), width); + // 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 (cover) - _cache.addImage(cacheEntryDesc, cover); - - return cover; + return image; } void ArtworkService::flushCache() @@ -241,5 +249,4 @@ namespace lms::cover LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality); } - -} // namespace lms::cover +} // namespace lms::artwork diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index addfdca5..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" @@ -36,7 +38,7 @@ namespace lms::metadata class IAudioFileParser; } -namespace lms::cover +namespace lms::artwork { class ArtworkService : public IArtworkService { @@ -47,19 +49,21 @@ namespace lms::cover ArtworkService& operator=(const ArtworkService&) = delete; private: - std::shared_ptr getImage(db::ImageId imageId, std::optional width) override; - std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) override; + db::ArtworkId findTrackListImage(db::TrackListId trackListId) override; - std::shared_ptr getDefaultReleaseCover() override; - std::shared_ptr getDefaultArtistImage() override; + std::shared_ptr getImage(db::ArtworkId artworkId, std::optional width) override; + + std::shared_ptr getDefaultReleaseArtwork() override; + std::shared_ptr getDefaultArtistArtwork() override; void flushCache() override; void setJpegQuality(unsigned quality) override; - std::unique_ptr getFromImageFile(const std::filesystem::path& p, std::optional width) const; - std::unique_ptr getTrackImage(const std::filesystem::path& path, std::optional width) const; + std::shared_ptr getImage(db::ImageId imageId, std::optional width); + std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width); - static bool checkImageFile(const std::filesystem::path& filePath); + 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; db::Db& _db; @@ -72,4 +76,4 @@ namespace lms::cover unsigned _jpegQuality; }; -} // namespace lms::cover +} // namespace lms::artwork diff --git a/src/libs/services/artwork/impl/ImageCache.cpp b/src/libs/services/artwork/impl/ImageCache.cpp index 586cf4b4..349cb984 100644 --- a/src/libs/services/artwork/impl/ImageCache.cpp +++ b/src/libs/services/artwork/impl/ImageCache.cpp @@ -24,7 +24,7 @@ #include "core/ILogger.hpp" #include "core/Random.hpp" -namespace lms::cover +namespace lms::artwork { ImageCache::ImageCache(std::size_t maxCacheSize) : _maxCacheSize{ maxCacheSize } @@ -79,4 +79,4 @@ namespace lms::cover _cacheSize = 0; _cache.clear(); } -} // namespace lms::cover \ No newline at end of file +} // namespace lms::artwork \ No newline at end of file diff --git a/src/libs/services/artwork/impl/ImageCache.hpp b/src/libs/services/artwork/impl/ImageCache.hpp index 364c1061..995e67f7 100644 --- a/src/libs/services/artwork/impl/ImageCache.hpp +++ b/src/libs/services/artwork/impl/ImageCache.hpp @@ -25,13 +25,11 @@ #include #include #include -#include -#include "database/ImageId.hpp" -#include "database/TrackEmbeddedImageId.hpp" +#include "database/ArtworkId.hpp" #include "image/IEncodedImage.hpp" -namespace lms::cover +namespace lms::artwork { class ImageCache { @@ -40,8 +38,7 @@ namespace lms::cover 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::cover 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::cover \ No newline at end of file +} // 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 5da77f4a..4d8c1605 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -23,8 +23,8 @@ #include #include -#include "database/ImageId.hpp" -#include "database/TrackEmbeddedImageId.hpp" +#include "database/ArtworkId.hpp" +#include "database/TrackListId.hpp" #include "image/IEncodedImage.hpp" namespace lms::db @@ -32,19 +32,22 @@ namespace lms::db class Db; } -namespace lms::cover +namespace lms::artwork { class IArtworkService { public: virtual ~IArtworkService() = default; - virtual std::shared_ptr getImage(db::ImageId imageId, std::optional width) = 0; - virtual std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) = 0; + // Helpers to get preferred artworks + virtual db::ArtworkId findTrackListImage(db::TrackListId trackListId) = 0; - // Svg images dont have image "size" - virtual std::shared_ptr getDefaultReleaseCover() = 0; - virtual std::shared_ptr getDefaultArtistImage() = 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 getDefaultReleaseArtwork() = 0; + virtual std::shared_ptr getDefaultArtistArtwork() = 0; virtual void flushCache() = 0; @@ -53,4 +56,4 @@ namespace lms::cover std::unique_ptr createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath); -} // namespace lms::cover +} // namespace lms::artwork diff --git a/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp b/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp index 2e25b1fc..5256e2d7 100644 --- a/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp +++ b/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp @@ -19,6 +19,8 @@ #include "PlaylistGeneratorService.hpp" +#include + #include "core/ILogger.hpp" #include "database/Db.hpp" #include "database/Session.hpp" 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..251da22f 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" @@ -461,8 +462,9 @@ 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)); _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)); // 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/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp index 8a33e817..e5fcf43b 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp @@ -350,24 +350,12 @@ namespace lms::scanner void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span images) { - db::TrackEmbeddedImageLink::pointer preferredImageLink; - track.modify()->clearEmbeddedImageLinks(); for (const ImageInfo& imageInfo : images) { db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) }; track.modify()->addEmbeddedImageLink(link); - - if (!preferredImageLink - || (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover) - || (preferredImageLink->getImage()->getSize() < link->getImage()->getSize())) - { - preferredImageLink = link; - } } - - if (preferredImageLink) - preferredImageLink.modify()->setIsPreferred(true); } db::Advisory getAdvisory(std::optional advisory) 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/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index 96c16532..f190cb92 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "core/IConfig.hpp" #include "core/ILogger.hpp" @@ -31,27 +32,38 @@ #include "core/String.hpp" #include "database/Artist.hpp" #include "database/ArtistInfo.hpp" +#include "database/Artwork.hpp" #include "database/Db.hpp" #include "database/Directory.hpp" #include "database/Image.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "ArtworkUtils.hpp" #include "ScanContext.hpp" namespace lms::scanner { namespace { - constexpr std::size_t readBatchSize{ 100 }; - constexpr std::size_t writeBatchSize{ 20 }; - - struct ArtistImageAssociation + using ArtistArtwork = std::variant; // TODO handle embedded images in tracks? + bool isSameArtwork(ArtistArtwork preferredArtwork, const db::ObjectPtr& artwork) { - db::ArtistId artistId; - db::ImageId imageId; + if (std::holds_alternative(preferredArtwork)) + return !artwork; + + if (const db::ImageId* imageId = std::get_if(&preferredArtwork)) + return artwork && *imageId == artwork->getImageId(); + + return false; + } + + struct ArtistArtworkAssociation + { + db::Artist::pointer artist; + ArtistArtwork preferredArtwork; }; - using ArtistImageAssociationContainer = std::deque; + using ArtistArtworkAssociationContainer = std::deque; struct SearchArtistImageContext { @@ -87,7 +99,7 @@ namespace lms::scanner return image; } - db::Image::pointer getImageFromMbid(SearchArtistImageContext& searchContext, const core::UUID& mbid) + db::ImageId getImageFromMbid(SearchArtistImageContext& searchContext, const core::UUID& mbid) { db::Image::pointer image; @@ -97,10 +109,10 @@ namespace lms::scanner image = foundImg; }); - return image; + return image ? image->getId() : db::ImageId{}; } - db::Image::pointer searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId) + db::ImageId searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId) { db::Image::pointer image; @@ -115,10 +127,10 @@ namespace lms::scanner if (fileInfoPaths.size() > 1) LMS_LOG(DBUPDATER, DEBUG, "Found " << fileInfoPaths.size() << " artist info files for same artist: " << core::stringUtils::joinStrings(fileInfoPaths, ", ")); - return image; + return image ? image->getId() : db::ImageId{}; } - db::Image::pointer searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId) + db::ImageId searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId) { db::Image::pointer image; @@ -147,7 +159,7 @@ namespace lms::scanner { image = findImageInDirectory(searchContext, directoryToInspect, searchContext.artistFileNames); if (image) - return image; + return image->getId(); std::filesystem::path parentPath{ directoryToInspect.parent_path() }; if (parentPath == directoryToInspect) @@ -164,44 +176,44 @@ namespace lms::scanner { image = findImageInDirectory(searchContext, releasePath, searchContext.artistFileNames); if (image) - return image; + return image->getId(); } } - return image; + return image ? image->getId() : db::ImageId{}; } - db::Image::pointer computeBestArtistImage(SearchArtistImageContext& searchContext, const db::Artist::pointer& artist) + ArtistArtwork computePreferredArtwork(SearchArtistImageContext& searchContext, const db::Artist::pointer& artist) { - db::Image::pointer image; + db::ImageId imageId; if (const auto mbid{ artist->getMBID() }) - image = getImageFromMbid(searchContext, *mbid); + imageId = getImageFromMbid(searchContext, *mbid); - if (!image) - image = searchImageInArtistInfoDirectory(searchContext, artist->getId()); + if (!imageId.isValid()) + imageId = searchImageInArtistInfoDirectory(searchContext, artist->getId()); - if (!image) - image = searchImageInDirectories(searchContext, artist->getId()); + if (!imageId.isValid()) + imageId = searchImageInDirectories(searchContext, artist->getId()); - return image; + return imageId.isValid() ? ArtistArtwork{ imageId } : ArtistArtwork{}; } - bool fetchNextArtistImagesToUpdate(SearchArtistImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations) + bool fetchNextArtistArtworksToUpdate(SearchArtistImageContext& searchContext, ArtistArtworkAssociationContainer& ArtistArtworkAssociations) { const db::ArtistId artistId{ searchContext.lastRetrievedArtistId }; { + constexpr std::size_t readBatchSize{ 100 }; + auto transaction{ searchContext.session.createReadTransaction() }; db::Artist::find(searchContext.session, searchContext.lastRetrievedArtistId, readBatchSize, [&](const db::Artist::pointer& artist) { - db::Image::pointer image{ computeBestArtistImage(searchContext, artist) }; + ArtistArtwork preferredArtwork{ computePreferredArtwork(searchContext, artist) }; + + if (!isSameArtwork(preferredArtwork, artist->getPreferredArtwork())) + ArtistArtworkAssociations.push_back(ArtistArtworkAssociation{ artist, preferredArtwork }); - if (image != artist->getImage()) - { - LMS_LOG(DBUPDATER, DEBUG, "Updating artist image for artist '" << artist->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "") << "'"); - artistImageAssociations.push_back(ArtistImageAssociation{ artist->getId(), image ? image->getId() : db::ImageId{} }); - } searchContext.processedArtistCount++; }); } @@ -209,27 +221,32 @@ namespace lms::scanner return artistId != searchContext.lastRetrievedArtistId; } - void updateArtistImage(db::Session& session, const ArtistImageAssociation& artistImageAssociation) + void updateArtistPreferredArtwork(db::Session& session, const ArtistArtworkAssociation& ArtistArtworkAssociation) { - db::Artist::pointer artist{ db::Artist::find(session, artistImageAssociation.artistId) }; - assert(artist); + db::Artist::pointer artist{ ArtistArtworkAssociation.artist }; - db::Image::pointer image; - if (artistImageAssociation.imageId.isValid()) - image = db::Image::find(session, artistImageAssociation.imageId); + db::Artwork::pointer artwork; + if (const db::ImageId * imageId{ std::get_if(&ArtistArtworkAssociation.preferredArtwork) }) + artwork = utils::getOrCreateArtworkFromImage(session, *imageId); - artist.modify()->setImage(image); + artist.modify()->setPreferredArtwork(artwork); + if (artwork) + LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork for artist '" << artist->getName() << "' with image in " << utils::toPath(session, artwork->getId())); + else + LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from artist '" << artist->getName() << "'"); } - void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations) + void updateArtistArtworks(db::Session& session, ArtistArtworkAssociationContainer& imageAssociations) { + constexpr std::size_t writeBatchSize{ 50 }; + while (!imageAssociations.empty()) { auto transaction{ session.createWriteTransaction() }; for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i) { - updateArtistImage(session, imageAssociations.front()); + updateArtistPreferredArtwork(session, imageAssociations.front()); imageAssociations.pop_front(); } } @@ -258,10 +275,7 @@ namespace lms::scanner bool ScanStepAssociateArtistImages::needProcess(const ScanContext& context) const { - if (context.stats.nbChanges() > 0) - return true; - - return false; + return context.stats.nbChanges() > 0; } void ScanStepAssociateArtistImages::process(ScanContext& context) @@ -279,13 +293,13 @@ namespace lms::scanner .artistFileNames = _artistFileNames, }; - ArtistImageAssociationContainer artistImageAssociations; - while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations)) + ArtistArtworkAssociationContainer ArtistArtworkAssociations; + while (fetchNextArtistArtworksToUpdate(searchContext, ArtistArtworkAssociations)) { if (_abortScan) return; - updateArtistImages(session, artistImageAssociations); + updateArtistArtworks(session, ArtistArtworkAssociations); context.currentStepStats.processedElems = searchContext.processedArtistCount; _progressCallback(context.currentStepStats); } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index d758e192..b6ce83d2 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -23,46 +23,66 @@ #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 "ArtworkUtils.hpp" #include "ScanContext.hpp" namespace lms::scanner { namespace { - struct ReleaseImageAssociation + using ReleaseArtwork = std::variant; + bool isSameArtwork(ReleaseArtwork preferredArtwork, const db::ObjectPtr& artwork) { - db::ReleaseId releaseId; - db::ImageId imageId; - }; - using ReleaseImageAssociationContainer = std::deque; + if (std::holds_alternative(preferredArtwork)) + return !artwork; - struct SearchReleaseImageContext + 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; + } + + struct ReleaseArtworkAssociation + { + db::Release::pointer release; + ReleaseArtwork preferredArtwork; + }; + using ReleaseArtworkAssociationContainer = std::deque; + + struct SearchReleaseArtworkContext { 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) + db::Image::pointer findImageInDirectory(SearchReleaseArtworkContext& searchContext, const std::filesystem::path& directoryPath) { db::Image::pointer image; 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()); @@ -81,7 +101,7 @@ namespace lms::scanner return image; } - db::Image::pointer computeBestReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release) + db::Image::pointer computePreferredReleaseImage(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release) { db::Image::pointer image; @@ -129,7 +149,45 @@ namespace lms::scanner return image; } - bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations) + ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release) + { + const db::Image::pointer image{ computePreferredReleaseImage(searchContext, release) }; + if (image) + return ReleaseArtwork{ 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 ReleaseArtwork{ 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 ReleaseArtwork{ trackEmbeddedImageId }; + + return ReleaseArtwork{}; + } + + bool fetchNextReleaseArtworksToUpdate(SearchReleaseArtworkContext& searchContext, ReleaseArtworkAssociationContainer& ReleaseArtworkAssociations) { const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId }; @@ -139,13 +197,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 ReleaseArtwork preferredArtwork{ computePreferredReleaseArtwork(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)) + ReleaseArtworkAssociations.push_back(ReleaseArtworkAssociation{ release, preferredArtwork }); searchContext.processedReleaseCount++; }); } @@ -153,21 +209,27 @@ namespace lms::scanner return releaseId != searchContext.lastRetrievedReleaseId; } - void updateReleaseImage(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::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(&ReleaseArtworkAssociation.preferredArtwork) }) + artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId); + else if (const db::ImageId * imageId{ std::get_if(&ReleaseArtworkAssociation.preferredArtwork) }) + artwork = utils::getOrCreateArtworkFromImage(session, *imageId); - release.modify()->setImage(image); + 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) + void updateReleaseArtworks(db::Session& session, ReleaseArtworkAssociationContainer& imageAssociations) { - constexpr std::size_t writeBatchSize{ 20 }; + constexpr std::size_t writeBatchSize{ 50 }; while (!imageAssociations.empty()) { @@ -175,13 +237,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 +260,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) @@ -219,19 +278,19 @@ namespace lms::scanner context.currentStepStats.totalElems = db::Release::getCount(session); } - SearchReleaseImageContext searchContext{ + SearchReleaseArtworkContext searchContext{ .session = session, .lastRetrievedReleaseId = {}, - .releaseFileNames = _releaseFileNames, + .releaseImageFileNames = _releaseImageFileNames, }; - ReleaseImageAssociationContainer releaseImageAssociations; - while (fetchNextReleaseImagesToUpdate(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/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 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..bf4da692 --- /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 TrackArtwork = std::variant; + bool isSameArtwork(TrackArtwork 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 TrackArtwork& res) + { + return !std::holds_alternative(res); + } + + struct TrackArtworksAssociation + { + db::Track::pointer track; + TrackArtwork preferredArtwork; + TrackArtwork preferredMediaArtwork; + }; + using TrackArtworksAssociationContainer = std::deque; + + struct SearchTrackArtworkContext + { + db::Session& session; + db::TrackId lastRetrievedTrackId; + std::size_t processedTrackCount{}; + }; + + TrackArtwork computePreferredTrackArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track) + { + // Try to get a media image + TrackArtwork 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 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(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; + } + + TrackArtwork computePreferredTrackMediaArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track) + { + TrackArtwork 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(SearchTrackArtworkContext& searchContext, TrackArtworksAssociationContainer& TrackArtworksAssociations) + { + 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 TrackArtwork preferredArtwork{ computePreferredTrackArtwork(searchContext, track) }; + const TrackArtwork 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)) + { + TrackArtworksAssociations.push_back(TrackArtworksAssociation{ track, preferredArtwork, preferredMediaArtwork }); + } + + searchContext.processedTrackCount++; + }); + } + + return trackId != searchContext.lastRetrievedTrackId; + } + + void updateTrackPreferredArtwork(db::Session& session, db::Track::pointer& track, TrackArtwork 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, TrackArtwork 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 TrackArtworksAssociation& TrackArtworksAssociation) + { + db::Track::pointer track{ TrackArtworksAssociation.track }; + + { + const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() }; + if (!isSameArtwork(TrackArtworksAssociation.preferredArtwork, currentPreferredArtwork)) + updateTrackPreferredArtwork(session, track, TrackArtworksAssociation.preferredArtwork); + } + + { + const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() }; + if (!isSameArtwork(TrackArtworksAssociation.preferredMediaArtwork, currentPreferredMediaArtwork)) + updateTrackPreferredMediaArtwork(session, track, TrackArtworksAssociation.preferredMediaArtwork); + } + } + + void updateTrackPreferredArtworks(db::Session& session, TrackArtworksAssociationContainer& 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); + } + + SearchTrackArtworkContext searchContext{ + .session = session, + .lastRetrievedTrackId = {}, + }; + + TrackArtworksAssociationContainer TrackArtworksAssociations; + while (fetchNextTrackArtworksToUpdate(searchContext, TrackArtworksAssociations)) + { + if (_abortScan) + return; + + updateTrackPreferredArtworks(session, TrackArtworksAssociations); + 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..8314ad10 --- /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::AssociateTrackImages; } + core::LiteralString getStepName() const override { return "Associate track images"; } + bool needProcess(const ScanContext& context) const override; + void process(ScanContext& context) override; + }; +} // namespace lms::scanner diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 13951d34..2de12172 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -49,6 +49,7 @@ namespace lms::scanner AssociateExternalLyrics, AssociatePlayListTracks, AssociateReleaseImages, + AssociateTrackImages, CheckForDuplicatedFiles, CheckForRemovedFiles, ComputeClusterStats, 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/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index 480bac6e..179269b1 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -19,6 +19,8 @@ #include "AlbumSongLists.hpp" +#include + #include "core/Service.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 4b5d40c1..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/endpoints/Playlists.cpp b/src/libs/subsonic/impl/endpoints/Playlists.cpp index 5224b988..a3d2e4bb 100644 --- a/src/libs/subsonic/impl/endpoints/Playlists.cpp +++ b/src/libs/subsonic/impl/endpoints/Playlists.cpp @@ -59,7 +59,7 @@ namespace lms::api::subsonic Response::Node& playlistsNode{ response.createNode("playlists") }; auto addTrackList{ [&](const db::TrackList::pointer& trackList) { - playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession)); + playlistsNode.addArrayChild("playlist", createPlaylistNode(context, trackList)); } }; // First add user's playlists @@ -104,7 +104,7 @@ namespace lms::api::subsonic throw RequestedDataNotFoundError{}; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) }; + Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) @@ -157,7 +157,7 @@ namespace lms::api::subsonic } Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) }; + Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index cec71030..914d79f1 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -23,12 +23,11 @@ #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/TrackEmbeddedImage.hpp" #include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -87,24 +86,13 @@ namespace lms::api::subsonic } albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime())); - if (const auto image{ release->getImage() }) + + if (const auto artwork{ release->getPreferredArtwork() }) { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; albumNode.setAttribute("coverArt", idToString(coverArtId)); } - else - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(release->getId()); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); - params.setRange(db::Range{ 0, 1 }); - db::TrackEmbeddedImage::find(context.dbSession, params, [&](const db::TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - 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 632e3548..f47a76bf 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -23,7 +23,7 @@ #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" @@ -95,9 +95,9 @@ namespace lms::api::subsonic artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("name", artist->getName()); - if (const auto image{ artist->getImage() }) + if (const auto artwork{ artist->getPreferredArtwork() }) { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; artistNode.setAttribute("coverArt", idToString(coverArtId)); } diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index e6a01ff4..d053c14a 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -19,17 +19,20 @@ #include "Playlist.hpp" +#include "core/Service.hpp" #include "core/String.hpp" -#include "database/TrackEmbeddedImage.hpp" +#include "database/Artwork.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" +#include "services/artwork/IArtworkService.hpp" #include "CoverArtId.hpp" +#include "RequestContext.hpp" #include "SubsonicId.hpp" namespace lms::api::subsonic { - Response::Node createPlaylistNode(const db::TrackList::pointer& tracklist, db::Session& session) + Response::Node createPlaylistNode(RequestContext& context, const db::TrackList::pointer& tracklist) { Response::Node playlistNode; @@ -43,16 +46,14 @@ namespace lms::api::subsonic if (const db::User::pointer user{ tracklist->getUser() }) playlistNode.setAttribute("owner", user->getLoginName()); - db::TrackEmbeddedImage::FindParameters params; - params.setTrackList(tracklist->getId()); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - playlistNode.setAttribute("coverArt", idToString(coverArtId)); - }); + if (const db::ArtworkId artworkId{ core::Service::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid()) + { + if (const auto artwork{ db::Artwork::find(context.dbSession, artworkId) }) + { + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + playlistNode.setAttribute("coverArt", idToString(coverArtId)); + } + } return playlistNode; } diff --git a/src/libs/subsonic/impl/responses/Playlist.hpp b/src/libs/subsonic/impl/responses/Playlist.hpp index 07246674..6c6c8e00 100644 --- a/src/libs/subsonic/impl/responses/Playlist.hpp +++ b/src/libs/subsonic/impl/responses/Playlist.hpp @@ -26,10 +26,11 @@ namespace lms::db { class TrackList; - class Session; } // namespace lms::db namespace lms::api::subsonic { - Response::Node createPlaylistNode(const db::ObjectPtr& tracklist, db::Session& session); -} \ No newline at end of file + struct RequestContext; + + Response::Node createPlaylistNode(RequestContext& context, const db::ObjectPtr& tracklist); +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 830a11a5..7e762666 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -21,19 +21,17 @@ #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/TrackEmbeddedImage.hpp" #include "database/Types.hpp" #include "database/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -111,28 +109,14 @@ namespace lms::api::subsonic trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } - const Release::pointer release{ track->getRelease() }; + auto artwork{ track->getPreferredMediaArtwork() }; + if (!artwork) + artwork = track->getPreferredArtwork(); + if (artwork) { - TrackEmbeddedImage::FindParameters params; - params.setTrack(track->getId()); - params.setIsPreferred(true); - params.setRange(Range{ .offset = 0, .size = 1 }); - - bool hasEmbeddedImage{}; - TrackEmbeddedImage::find(context.dbSession, params, [&](const TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - }); - - if (!hasEmbeddedImage && release) - { - if (const db::Image::pointer image{ release->getImage() }) - { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - } - } + CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; + trackResponse.setAttribute("coverArt", idToString(coverArtId)); } const std::vector& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; @@ -147,6 +131,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); } + const Release::pointer release{ track->getRelease() }; if (release) { trackResponse.setAttribute("album", release->getName()); diff --git a/src/lms/main.cpp b/src/lms/main.cpp index de5e4364..0a96921e 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -370,7 +370,7 @@ namespace lms } image::init(argv[0]); - core::Service artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") }; + core::Service artworkService{ artwork::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") }; core::Service recommendationService{ recommendation::createRecommendationService(database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; core::Service scannerService{ scanner::createScannerService(database) }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 22f4b984..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()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," - << " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(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 0cbfe325..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,32 +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()->getPreferredTrackImageUrl(trackId, size)); - cover->setStyleClass("Lms-cover img-fluid"); // HACK - cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + auto image{ createArtworkImage() }; + image->setImageLink(LmsApp->getArtworkResource()->getDefaultReleaseArtworkUrl()); + return image; + } - return cover; + std::unique_ptr createDefaultTrackArtworkImage() + { + 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 2039db95..f2cdc20a 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -34,6 +34,7 @@ #include "database/ReleaseId.hpp" #include "database/TrackId.hpp" +#include "database/Types.hpp" #include "resource/ArtworkResource.hpp" namespace lms::db @@ -56,9 +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 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/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 5cefb88a..2f3e196e 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -188,6 +188,11 @@ namespace lms::ui .arg(stepStats.progress())); break; + case ScanStep::AssociateTrackImages: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-track-images") + .arg(stepStats.progress())); + break; + case ScanStep::CheckForDuplicatedFiles: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files") .arg(stepStats.processedElems)); 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/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp index 135c1da3..11188c1f 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.cpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp @@ -19,6 +19,8 @@ #include "DatabaseCollectorBase.hpp" +#include + #include "core/String.hpp" #include "explore/Filters.hpp" 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 8fd13d70..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,13 +387,26 @@ 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) -> 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