From 05000d8a1aad12207402579beed05a3c8f3cefec Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 15 Mar 2025 14:48:37 +0100 Subject: [PATCH] Deduplicate embedded images, ref #628 --- src/libs/core/include/core/TaggedType.hpp | 41 +++ src/libs/database/CMakeLists.txt | 2 + src/libs/database/impl/Artist.cpp | 2 +- src/libs/database/impl/AuthToken.cpp | 4 +- src/libs/database/impl/Cluster.cpp | 4 +- src/libs/database/impl/Directory.cpp | 4 +- src/libs/database/impl/Image.cpp | 4 +- src/libs/database/impl/Listen.cpp | 2 +- src/libs/database/impl/MediaLibrary.cpp | 6 +- src/libs/database/impl/Migration.cpp | 36 ++- src/libs/database/impl/PlayListFile.cpp | 6 +- src/libs/database/impl/PlayQueue.cpp | 4 +- src/libs/database/impl/RatedArtist.cpp | 2 +- src/libs/database/impl/RatedRelease.cpp | 2 +- src/libs/database/impl/RatedTrack.cpp | 2 +- src/libs/database/impl/Release.cpp | 8 +- src/libs/database/impl/Session.cpp | 18 +- src/libs/database/impl/StarredArtist.cpp | 2 +- src/libs/database/impl/StarredRelease.cpp | 2 +- src/libs/database/impl/StarredTrack.cpp | 2 +- src/libs/database/impl/Track.cpp | 29 +- src/libs/database/impl/TrackArtistLink.cpp | 2 +- src/libs/database/impl/TrackBookmark.cpp | 2 +- src/libs/database/impl/TrackEmbeddedImage.cpp | 124 ++++++++ .../database/impl/TrackEmbeddedImageLink.cpp | 66 ++++ src/libs/database/impl/TrackFeatures.cpp | 2 +- src/libs/database/impl/TrackList.cpp | 4 +- src/libs/database/impl/TrackLyrics.cpp | 4 +- src/libs/database/impl/UIState.cpp | 5 +- src/libs/database/impl/User.cpp | 6 +- .../impl/{ => traits}/EnumSetTraits.hpp | 0 .../impl/{ => traits}/IdTypeTraits.hpp | 0 .../impl/traits/ImageHashTypeTraits.hpp | 62 ++++ .../{ => traits}/PartialDateTimeTraits.hpp | 0 .../database/impl/{ => traits}/PathTraits.hpp | 0 .../impl/{ => traits}/StringViewTraits.hpp | 0 src/libs/database/include/database/Track.hpp | 22 +- .../include/database/TrackEmbeddedImage.hpp | 133 ++++++++ .../include/database/TrackEmbeddedImageId.hpp | 24 ++ .../database/TrackEmbeddedImageLink.hpp | 88 ++++++ .../database/TrackEmbeddedImageLinkId.hpp | 24 ++ src/libs/database/include/database/Types.hpp | 35 +++ src/libs/database/test/CMakeLists.txt | 1 + src/libs/database/test/Migration.cpp | 4 + src/libs/database/test/TrackEmbeddedImage.cpp | 283 ++++++++++++++++++ .../services/artwork/impl/ArtworkService.cpp | 15 +- .../services/artwork/impl/ArtworkService.hpp | 4 +- src/libs/services/artwork/impl/ImageCache.hpp | 3 +- .../services/artwork/IArtworkService.hpp | 6 +- .../impl/scanners/AudioFileScanner.cpp | 149 ++++++++- .../steps/ScanStepRemoveOrphanedDbEntries.cpp | 8 + .../steps/ScanStepRemoveOrphanedDbEntries.hpp | 1 + src/libs/subsonic/impl/CoverArtId.cpp | 101 ++++--- src/libs/subsonic/impl/CoverArtId.hpp | 13 +- .../impl/endpoints/MediaRetrieval.cpp | 5 +- src/libs/subsonic/impl/responses/Album.cpp | 10 +- src/libs/subsonic/impl/responses/Playlist.cpp | 14 +- src/libs/subsonic/impl/responses/Song.cpp | 27 +- src/lms/ui/MediaPlayer.cpp | 4 +- src/lms/ui/Utils.cpp | 2 +- src/lms/ui/resource/ArtworkResource.cpp | 66 ++-- src/lms/ui/resource/ArtworkResource.hpp | 5 +- src/tools/CMakeLists.txt | 1 - src/tools/cover/CMakeLists.txt | 10 - src/tools/cover/LmsCover.cpp | 109 ------- 65 files changed, 1307 insertions(+), 319 deletions(-) create mode 100644 src/libs/core/include/core/TaggedType.hpp create mode 100644 src/libs/database/impl/TrackEmbeddedImage.cpp create mode 100644 src/libs/database/impl/TrackEmbeddedImageLink.cpp rename src/libs/database/impl/{ => traits}/EnumSetTraits.hpp (100%) rename src/libs/database/impl/{ => traits}/IdTypeTraits.hpp (100%) create mode 100644 src/libs/database/impl/traits/ImageHashTypeTraits.hpp rename src/libs/database/impl/{ => traits}/PartialDateTimeTraits.hpp (100%) rename src/libs/database/impl/{ => traits}/PathTraits.hpp (100%) rename src/libs/database/impl/{ => traits}/StringViewTraits.hpp (100%) create mode 100644 src/libs/database/include/database/TrackEmbeddedImage.hpp create mode 100644 src/libs/database/include/database/TrackEmbeddedImageId.hpp create mode 100644 src/libs/database/include/database/TrackEmbeddedImageLink.hpp create mode 100644 src/libs/database/include/database/TrackEmbeddedImageLinkId.hpp create mode 100644 src/libs/database/test/TrackEmbeddedImage.cpp delete mode 100644 src/tools/cover/CMakeLists.txt delete mode 100644 src/tools/cover/LmsCover.cpp diff --git a/src/libs/core/include/core/TaggedType.hpp b/src/libs/core/include/core/TaggedType.hpp new file mode 100644 index 00000000..1963db33 --- /dev/null +++ b/src/libs/core/include/core/TaggedType.hpp @@ -0,0 +1,41 @@ +/* + * 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 + +namespace lms::core +{ + template + class TaggedType + { + public: + using underlying_type = T; + + explicit constexpr TaggedType() = default; + explicit constexpr TaggedType(T value) + : _value{ value } {} + + constexpr T value() const { return _value; } + + auto operator<=>(const TaggedType&) const = default; + + private: + T _value{}; + }; +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index a2aece03..12aee1b8 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -25,6 +25,8 @@ add_library(lmsdatabase STATIC impl/SqlQuery.cpp impl/Track.cpp impl/TrackBookmark.cpp + impl/TrackEmbeddedImage.cpp + impl/TrackEmbeddedImageLink.cpp impl/TrackLyrics.cpp impl/Types.cpp impl/UIState.cpp diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 250f00c0..f0c5638f 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -29,9 +29,9 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/AuthToken.cpp b/src/libs/database/impl/AuthToken.cpp index d963c653..8622975f 100644 --- a/src/libs/database/impl/AuthToken.cpp +++ b/src/libs/database/impl/AuthToken.cpp @@ -24,9 +24,9 @@ #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index f59f9e59..44841f4f 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -27,10 +27,10 @@ #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Directory.cpp b/src/libs/database/impl/Directory.cpp index d335cace..6f4f1778 100644 --- a/src/libs/database/impl/Directory.cpp +++ b/src/libs/database/impl/Directory.cpp @@ -23,9 +23,9 @@ #include "database/Session.hpp" #include "database/Types.hpp" -#include "IdTypeTraits.hpp" -#include "PathTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PathTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Image.cpp b/src/libs/database/impl/Image.cpp index 0466f9a0..1b010d09 100644 --- a/src/libs/database/impl/Image.cpp +++ b/src/libs/database/impl/Image.cpp @@ -26,9 +26,9 @@ #include "database/Release.hpp" #include "database/Session.hpp" -#include "IdTypeTraits.hpp" -#include "PathTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PathTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Listen.cpp b/src/libs/database/impl/Listen.cpp index be05082a..73e58490 100644 --- a/src/libs/database/impl/Listen.cpp +++ b/src/libs/database/impl/Listen.cpp @@ -22,9 +22,9 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index 81033c7b..cbbadaae 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -23,10 +23,10 @@ #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" -#include "PathTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PathTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 31322d8e..1b0a857f 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{ 83 }; + static constexpr Version LMS_DATABASE_VERSION{ 84 }; } VersionInfo::VersionInfo() @@ -1101,11 +1101,42 @@ FROM tracklist)"); void migrateFromV82(Session& session) { - // new setting to display inline artist relationships in the release view + // New setting to display inline artist relationships in the release view utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_enable_inline_artist_relationships BOOLEAN NOT NULL DEFAULT(false)"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_inline_artist_relationships BIGINT NOT NULL DEFAULT(68)"); // Composer + Performer } + void migrateFromV83(Session& session) + { + // New embedded track image handling + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN has_cover"); + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image" ( + "id" integer primary key autoincrement, + "version" integer not null, + "hash" text not null, + "size" integer not null, + "width" integer not null, + "height" integer not null, + "mime_type" text not null +))"); + + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image_link" ( + "id" integer primary key autoincrement, + "version" integer not null, + "index" integer not null, + "is_preferred" boolean not null, + "type" integer not null, + "description" text not null, + "track_id" bigint, + "track_embedded_image_id" bigint, + constraint "fk_track_embedded_image_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_embedded_image_link_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred + ))"); + + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1165,6 +1196,7 @@ FROM tracklist)"); { 80, migrateFromV80 }, { 81, migrateFromV81 }, { 82, migrateFromV82 }, + { 83, migrateFromV83 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/PlayListFile.cpp b/src/libs/database/impl/PlayListFile.cpp index 776b1af4..8d60ed03 100644 --- a/src/libs/database/impl/PlayListFile.cpp +++ b/src/libs/database/impl/PlayListFile.cpp @@ -28,10 +28,10 @@ #include "database/TrackList.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" -#include "PathTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PathTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/PlayQueue.cpp b/src/libs/database/impl/PlayQueue.cpp index e08efbab..cd1af8f3 100644 --- a/src/libs/database/impl/PlayQueue.cpp +++ b/src/libs/database/impl/PlayQueue.cpp @@ -28,9 +28,9 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/RatedArtist.cpp b/src/libs/database/impl/RatedArtist.cpp index f953c27a..3e480f1f 100644 --- a/src/libs/database/impl/RatedArtist.cpp +++ b/src/libs/database/impl/RatedArtist.cpp @@ -25,8 +25,8 @@ #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/RatedRelease.cpp b/src/libs/database/impl/RatedRelease.cpp index 69cc51be..b7de0d78 100644 --- a/src/libs/database/impl/RatedRelease.cpp +++ b/src/libs/database/impl/RatedRelease.cpp @@ -25,8 +25,8 @@ #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/RatedTrack.cpp b/src/libs/database/impl/RatedTrack.cpp index 5cf3bda5..0c0ec110 100644 --- a/src/libs/database/impl/RatedTrack.cpp +++ b/src/libs/database/impl/RatedTrack.cpp @@ -25,8 +25,8 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 574a9401..4c2575c1 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -31,12 +31,12 @@ #include "database/Types.hpp" #include "database/User.hpp" -#include "EnumSetTraits.hpp" -#include "IdTypeTraits.hpp" -#include "PartialDateTimeTraits.hpp" #include "SqlQuery.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/EnumSetTraits.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PartialDateTimeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 8eeb58ac..6c44db6e 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -43,6 +43,8 @@ #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" #include "database/TrackBookmark.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackList.hpp" #include "database/TrackLyrics.hpp" @@ -50,11 +52,12 @@ #include "database/UIState.hpp" #include "database/User.hpp" -#include "EnumSetTraits.hpp" #include "Migration.hpp" -#include "PartialDateTimeTraits.hpp" -#include "PathTraits.hpp" #include "Utils.hpp" +#include "traits/EnumSetTraits.hpp" +#include "traits/ImageHashTypeTraits.hpp" +#include "traits/PartialDateTimeTraits.hpp" +#include "traits/PathTraits.hpp" namespace lms::db { @@ -122,6 +125,8 @@ namespace lms::db _session.mapClass("track"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); + _session.mapClass("track_embedded_image"); + _session.mapClass("track_embedded_image_link"); _session.mapClass("track_features"); _session.mapClass("tracklist"); _session.mapClass("tracklist_entry"); @@ -209,6 +214,13 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)"); + + 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 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)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)"); diff --git a/src/libs/database/impl/StarredArtist.cpp b/src/libs/database/impl/StarredArtist.cpp index df53f6f6..e19071c1 100644 --- a/src/libs/database/impl/StarredArtist.cpp +++ b/src/libs/database/impl/StarredArtist.cpp @@ -25,8 +25,8 @@ #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/StarredRelease.cpp b/src/libs/database/impl/StarredRelease.cpp index 18311213..1b5fded8 100644 --- a/src/libs/database/impl/StarredRelease.cpp +++ b/src/libs/database/impl/StarredRelease.cpp @@ -25,8 +25,8 @@ #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/StarredTrack.cpp b/src/libs/database/impl/StarredTrack.cpp index e9e3ae4e..842b276d 100644 --- a/src/libs/database/impl/StarredTrack.cpp +++ b/src/libs/database/impl/StarredTrack.cpp @@ -25,8 +25,8 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 55bc04be..ab631042 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -29,16 +29,18 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/TrackArtistLink.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackLyrics.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" -#include "PartialDateTimeTraits.hpp" -#include "PathTraits.hpp" #include "SqlQuery.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PartialDateTimeTraits.hpp" +#include "traits/PathTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { @@ -177,12 +179,15 @@ namespace lms::db if (params.directory.isValid()) query.where("t.directory_id = ?").bind(params.directory); - if (params.hasEmbeddedImage.has_value()) - query.where("t.has_cover = ?").bind(params.hasEmbeddedImage.value()); - if (params.fileSize.has_value()) query.where("t.file_size = ?").bind(static_cast(params.fileSize.value())); + if (params.embeddedImageId.isValid()) + { + query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id"); + query.where("t_e_i_l.track_embedded_image_id = ?").bind(params.embeddedImageId); + } + switch (params.sortMethod) { case TrackSortMethod::None: @@ -469,6 +474,16 @@ namespace lms::db _trackLyrics.insert(getDboPtr(lyrics)); } + void Track::clearEmbeddedImageLinks() + { + _embeddedImageLinks.clear(); + } + + void Track::addEmbeddedImageLink(const ObjectPtr& image) + { + _embeddedImageLinks.insert(getDboPtr(image)); + } + std::optional Track::getYear() const { return _date.getYear(); diff --git a/src/libs/database/impl/TrackArtistLink.cpp b/src/libs/database/impl/TrackArtistLink.cpp index 0817df8d..2d1dcd2e 100644 --- a/src/libs/database/impl/TrackArtistLink.cpp +++ b/src/libs/database/impl/TrackArtistLink.cpp @@ -23,8 +23,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/TrackBookmark.cpp b/src/libs/database/impl/TrackBookmark.cpp index 601ce35c..f0d0bc22 100644 --- a/src/libs/database/impl/TrackBookmark.cpp +++ b/src/libs/database/impl/TrackBookmark.cpp @@ -23,8 +23,8 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/TrackEmbeddedImage.cpp b/src/libs/database/impl/TrackEmbeddedImage.cpp new file mode 100644 index 00000000..c802d4b8 --- /dev/null +++ b/src/libs/database/impl/TrackEmbeddedImage.cpp @@ -0,0 +1,124 @@ +/* + * 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/Directory.hpp" + +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/Types.hpp" + +#include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/ImageHashTypeTraits.hpp" + +namespace lms::db +{ + namespace + { + Wt::Dbo::Query> createQuery(Session& session, const TrackEmbeddedImage::FindParameters& params) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>("SELECT t_e_i FROM track_embedded_image t_e_i") }; + + if (params.isPreferred + || params.track.isValid() + || params.release.isValid() + || params.trackList.isValid() + || params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize) + { + 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.track.isValid()) + query.where("t_e_i_l.track_id = ?").bind(params.track); + + if (params.release.isValid()) + { + query.join("track t ON t_e_i_l.track_id = t.id"); + query.where("t.release_id = ?").bind(params.release); + } + + if (params.trackList.isValid()) + { + 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); + } + } + + 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); + break; + } + + return query; + } + } // namespace + + TrackEmbeddedImage::pointer TrackEmbeddedImage::create(Session& session) + { + return session.getDboSession()->add(std::unique_ptr{ new TrackEmbeddedImage{} }); + } + + std::size_t TrackEmbeddedImage::getCount(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_embedded_image")); + } + + TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, TrackEmbeddedImageId id) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); + } + + void TrackEmbeddedImage::find(Session& session, const FindParameters& params, const std::function& func) + { + session.checkReadTransaction(); + + auto query{ createQuery(session, params) }; + utils::forEachQueryRangeResult(query, params.range, func); + } + + TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, std::size_t size, ImageHashType hash) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->find().where("size = ?").bind(static_cast(size)).where("hash = ?").bind(hash) }; + return utils::fetchQuerySingleResult(query); + } + + RangeResults TrackEmbeddedImage::findOrphanIds(Session& session, std::optional range) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query("SELECT t_e_i.id FROM track_embedded_image t_e_i LEFT JOIN track_embedded_image_link t_e_i_l ON t_e_i.id = t_e_i_l.track_embedded_image_id WHERE t_e_i_l.track_embedded_image_id IS NULL") }; + return utils::execRangeQuery(query, range); + } + +} // namespace lms::db diff --git a/src/libs/database/impl/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/TrackEmbeddedImageLink.cpp new file mode 100644 index 00000000..cecc6f2d --- /dev/null +++ b/src/libs/database/impl/TrackEmbeddedImageLink.cpp @@ -0,0 +1,66 @@ +/* + * 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/TrackEmbeddedImageLink.hpp" + +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/Types.hpp" + +#include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" + +namespace lms::db +{ + TrackEmbeddedImageLink::TrackEmbeddedImageLink(ObjectPtr track, ObjectPtr image) + : _track{ getDboPtr(track) } + , _image{ getDboPtr(image) } + { + } + + TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::create(Session& session, ObjectPtr track, ObjectPtr image) + { + return session.getDboSession()->add(std::unique_ptr{ new TrackEmbeddedImageLink{ track, image } }); + } + + std::size_t TrackEmbeddedImageLink::getCount(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_embedded_image_link")); + } + + TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageLinkId id) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); + } + + ObjectPtr TrackEmbeddedImageLink::getTrack() const + { + return _track; + } + + ObjectPtr TrackEmbeddedImageLink::getImage() const + { + return _image; + } +} // namespace lms::db diff --git a/src/libs/database/impl/TrackFeatures.cpp b/src/libs/database/impl/TrackFeatures.cpp index 53e68c7f..cfb088ea 100644 --- a/src/libs/database/impl/TrackFeatures.cpp +++ b/src/libs/database/impl/TrackFeatures.cpp @@ -27,8 +27,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 9892d93c..1b32030f 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -26,10 +26,10 @@ #include "database/Track.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/TrackLyrics.cpp b/src/libs/database/impl/TrackLyrics.cpp index 5bc1b495..18779f55 100644 --- a/src/libs/database/impl/TrackLyrics.cpp +++ b/src/libs/database/impl/TrackLyrics.cpp @@ -28,9 +28,9 @@ #include "database/Session.hpp" #include "database/Track.hpp" -#include "IdTypeTraits.hpp" -#include "PathTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/PathTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/UIState.cpp b/src/libs/database/impl/UIState.cpp index 30abf96e..22a11d75 100644 --- a/src/libs/database/impl/UIState.cpp +++ b/src/libs/database/impl/UIState.cpp @@ -19,13 +19,12 @@ #include "database/UIState.hpp" -#include "core/ILogger.hpp" #include "database/Session.hpp" #include "database/User.hpp" -#include "IdTypeTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index c5f8d06e..fcbb6e71 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -27,10 +27,10 @@ #include "database/Track.hpp" #include "database/UIState.hpp" -#include "EnumSetTraits.hpp" -#include "IdTypeTraits.hpp" -#include "StringViewTraits.hpp" #include "Utils.hpp" +#include "traits/EnumSetTraits.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/StringViewTraits.hpp" namespace lms::db { diff --git a/src/libs/database/impl/EnumSetTraits.hpp b/src/libs/database/impl/traits/EnumSetTraits.hpp similarity index 100% rename from src/libs/database/impl/EnumSetTraits.hpp rename to src/libs/database/impl/traits/EnumSetTraits.hpp diff --git a/src/libs/database/impl/IdTypeTraits.hpp b/src/libs/database/impl/traits/IdTypeTraits.hpp similarity index 100% rename from src/libs/database/impl/IdTypeTraits.hpp rename to src/libs/database/impl/traits/IdTypeTraits.hpp diff --git a/src/libs/database/impl/traits/ImageHashTypeTraits.hpp b/src/libs/database/impl/traits/ImageHashTypeTraits.hpp new file mode 100644 index 00000000..1a161ffe --- /dev/null +++ b/src/libs/database/impl/traits/ImageHashTypeTraits.hpp @@ -0,0 +1,62 @@ +/* + * 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 "core/String.hpp" +#include "database/Types.hpp" + +namespace Wt::Dbo +{ + template<> + struct sql_value_traits + { + static const bool specialized = true; + // Uses an underlying string to encode this big value + + static std::string type(SqlConnection* conn, int size) + { + return sql_value_traits::type(conn, size); + } + + static void bind(const lms::db::ImageHashType& v, SqlStatement* statement, int column, int size) + { + std::string valueAsStr{ std::to_string(v.value()) }; + sql_value_traits::bind(valueAsStr, statement, column, size); + } + + static bool read(lms::db::ImageHashType& v, SqlStatement* statement, int column, int size) + { + std::string valueAsStr; + if (sql_value_traits::read(valueAsStr, statement, column, size)) + { + if (const auto parsedValue{ lms::core::stringUtils::readAs(valueAsStr) }) + { + v = lms::db::ImageHashType{ *parsedValue }; + return true; + } + } + + v = lms::db::ImageHashType{}; + return false; + } + }; +} // namespace Wt::Dbo diff --git a/src/libs/database/impl/PartialDateTimeTraits.hpp b/src/libs/database/impl/traits/PartialDateTimeTraits.hpp similarity index 100% rename from src/libs/database/impl/PartialDateTimeTraits.hpp rename to src/libs/database/impl/traits/PartialDateTimeTraits.hpp diff --git a/src/libs/database/impl/PathTraits.hpp b/src/libs/database/impl/traits/PathTraits.hpp similarity index 100% rename from src/libs/database/impl/PathTraits.hpp rename to src/libs/database/impl/traits/PathTraits.hpp diff --git a/src/libs/database/impl/StringViewTraits.hpp b/src/libs/database/impl/traits/StringViewTraits.hpp similarity index 100% rename from src/libs/database/impl/StringViewTraits.hpp rename to src/libs/database/impl/traits/StringViewTraits.hpp diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 6258da9c..f6957c1b 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -41,6 +41,7 @@ #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackId.hpp" #include "database/TrackListId.hpp" #include "database/Types.hpp" @@ -52,6 +53,7 @@ namespace lms::db class Cluster; class ClusterType; class Directory; + class TrackEmbeddedImageLink; class MediaLibrary; class Release; class Session; @@ -85,8 +87,8 @@ namespace lms::db std::optional trackNumber; // matching this track number std::optional discNumber; // matching this disc number DirectoryId directory; // if set, tracks in this directory - std::optional hasEmbeddedImage; // if set, tracks that have or not embedded images std::optional fileSize; // if set, tracks that match this file size + TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image FindParameters& setFilters(const Filters& _filters) { @@ -182,16 +184,16 @@ namespace lms::db directory = _directory; return *this; } - FindParameters& setHasEmbeddedImage(std::optional _hasEmbeddedImage) - { - hasEmbeddedImage = _hasEmbeddedImage; - return *this; - } FindParameters& setFileSize(std::optional _fileSize) { fileSize = _fileSize; return *this; } + FindParameters& setEmbeddedImage(TrackEmbeddedImageId _embeddedImageId) + { + embeddedImageId = _embeddedImageId; + return *this; + } }; Track() = default; @@ -232,7 +234,6 @@ namespace lms::db void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; } void setDate(const core::PartialDateTime& date) { _date = date; } void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; } - void setHasCover(bool hasCover) { _hasCover = hasCover; } void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } void setCopyright(std::string_view copyright); @@ -249,6 +250,8 @@ namespace lms::db void clearLyrics(); void clearEmbeddedLyrics(); 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); } @@ -273,7 +276,6 @@ namespace lms::db std::optional getOriginalYear() const; const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } const Wt::WDateTime& getAddedTime() const { return _fileAdded; } - bool hasCover() const { return _hasCover; } bool hasLyrics() const; std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } @@ -321,7 +323,6 @@ namespace lms::db Wt::Dbo::field(a, _fileSize, "file_size"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileAdded, "file_added"); - Wt::Dbo::field(a, _hasCover, "has_cover"); Wt::Dbo::field(a, _trackMBID, "mbid"); Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); Wt::Dbo::field(a, _copyright, "copyright"); @@ -338,6 +339,7 @@ namespace lms::db 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"); + Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track"); } private: @@ -368,7 +370,6 @@ namespace lms::db long long _fileSize{}; Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileAdded; - bool _hasCover{}; std::string _trackMBID; std::string _recordingMBID; std::string _copyright; @@ -384,6 +385,7 @@ namespace lms::db Wt::Dbo::collection> _trackArtistLinks; Wt::Dbo::collection> _clusters; Wt::Dbo::collection> _trackLyrics; + Wt::Dbo::collection> _embeddedImageLinks; }; namespace Debug diff --git a/src/libs/database/include/database/TrackEmbeddedImage.hpp b/src/libs/database/include/database/TrackEmbeddedImage.hpp new file mode 100644 index 00000000..b90ed6fc --- /dev/null +++ b/src/libs/database/include/database/TrackEmbeddedImage.hpp @@ -0,0 +1,133 @@ +/* + * 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 +#include + +#include + +#include "database/Object.hpp" +#include "database/ReleaseId.hpp" +#include "database/TrackEmbeddedImageId.hpp" +#include "database/TrackId.hpp" +#include "database/TrackListId.hpp" +#include "database/Types.hpp" + +namespace lms::db +{ + class TrackEmbeddedImageLink; + class Session; + class Track; + + class TrackEmbeddedImage final : public Object + { + public: + TrackEmbeddedImage() = default; + + struct FindParameters + { + std::optional range; + TrackId track; + ReleaseId release; + TrackListId trackList; + std::optional isPreferred; + TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; + + FindParameters& setRange(std::optional _range) + { + range = _range; + return *this; + } + FindParameters& setTrack(TrackId _track) + { + track = _track; + return *this; + } + FindParameters& setRelease(ReleaseId _release) + { + release = _release; + return *this; + } + FindParameters& setTrackList(TrackListId _trackList) + { + trackList = _trackList; + return *this; + } + FindParameters& setIsPreferred(std::optional _isPreferred) + { + isPreferred = _isPreferred; + return *this; + } + FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod) + { + sortMethod = _sortMethod; + return *this; + } + }; + + // find + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackEmbeddedImageId id); + static void find(Session& session, const FindParameters& params, const std::function& func); + static pointer find(Session& session, std::size_t size, ImageHashType hash); + static RangeResults findOrphanIds(Session& session, std::optional range); + + // getters + ImageHashType getHash() const { return _hash; } + std::size_t getSize() const { return _size; } + std::size_t getWidth() const { return _width; } + std::size_t getHeight() const { return _height; } + std::string_view getMimeType() const { return _mimeType; } + + // setters + void setHash(ImageHashType hash) { _hash = hash; } + void setSize(std::size_t size) { _size = static_cast(size); } + void setWidth(std::size_t width) { _width = static_cast(width); } + void setHeight(std::size_t height) { _height = static_cast(height); } + void setMimeType(std::string_view mimeType) { _mimeType = mimeType; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _hash, "hash"); + Wt::Dbo::field(a, _size, "size"); + Wt::Dbo::field(a, _width, "width"); + Wt::Dbo::field(a, _height, "height"); + Wt::Dbo::field(a, _mimeType, "mime_type"); + + Wt::Dbo::hasMany(a, _links, Wt::Dbo::ManyToOne, "track_embedded_image"); + } + + private: + friend class Session; + + static pointer create(Session& session); + + ImageHashType _hash; + int _size{}; + int _width{}; + int _height{}; + std::string _mimeType; + + Wt::Dbo::collection> _links; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/TrackEmbeddedImageId.hpp b/src/libs/database/include/database/TrackEmbeddedImageId.hpp new file mode 100644 index 00000000..de9881fb --- /dev/null +++ b/src/libs/database/include/database/TrackEmbeddedImageId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 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(TrackEmbeddedImageId) diff --git a/src/libs/database/include/database/TrackEmbeddedImageLink.hpp b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp new file mode 100644 index 00000000..e8b66534 --- /dev/null +++ b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp @@ -0,0 +1,88 @@ +/* + * 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 + +#include + +#include "database/Object.hpp" +#include "database/TrackEmbeddedImageLinkId.hpp" +#include "database/Types.hpp" + +namespace lms::db +{ + class TrackEmbeddedImage; + class Session; + class Track; + + class TrackEmbeddedImageLink final : public Object + { + public: + TrackEmbeddedImageLink() = default; + + using HashType = std::uint64_t; + + // find + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackEmbeddedImageLinkId id); + + // 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; } + + template + 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"); + + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _image, "track_embedded_image", Wt::Dbo::OnDeleteCascade); + } + + private: + TrackEmbeddedImageLink(ObjectPtr track, ObjectPtr image); + + friend class Session; + static pointer create(Session& session, ObjectPtr track, ObjectPtr image); + + int _index{}; // index within the track + bool _isPreferred{}; + ImageType _type{ ImageType::Unknown }; + std::string _description; + + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _image; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/TrackEmbeddedImageLinkId.hpp b/src/libs/database/include/database/TrackEmbeddedImageLinkId.hpp new file mode 100644 index 00000000..d6a37f22 --- /dev/null +++ b/src/libs/database/include/database/TrackEmbeddedImageLinkId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 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(TrackEmbeddedImageLinkId) diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 8cc556b6..043ade8e 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -26,6 +26,7 @@ #include #include "core/Exception.hpp" +#include "core/TaggedType.hpp" namespace lms::db { @@ -136,6 +137,8 @@ namespace lms::db Name, }; + using ImageHashType = core::TaggedType; + enum class LabelSortMethod { None, @@ -165,6 +168,12 @@ namespace lms::db Name, }; + enum class TrackEmbeddedImageSortMethod + { + None, + FrontCoverAndSize, + }; + enum class TrackListSortMethod { None, @@ -194,6 +203,32 @@ namespace lms::db EmbeddedFirst, }; + enum class ImageType + { + Unknown = 0, + Other = 1, + FileIcon = 2, + OtherFileIcon = 3, + FrontCover = 4, + BackCover = 5, + LeafletPage = 6, + Media = 7, + LeadArtist = 8, + Artist = 9, + Conductor = 10, + Band = 11, + Composer = 12, + Lyricist = 13, + RecordingLocation = 14, + DuringRecording = 15, + DuringPerformance = 16, + MovieScreenCapture = 17, + ColouredFish = 18, + Illustration = 19, + BandLogo = 20, + PublisherLogo = 21 + }; + enum class TrackArtistLinkType { Artist = 0, // regular track artist diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index e36da2f4..ae3767b2 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(test-database StarredTrack.cpp Track.cpp TrackBookmark.cpp + TrackEmbeddedImage.cpp TrackFeatures.cpp TrackList.cpp TrackLyrics.cpp diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 14ba0525..bdb1da45 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.cpp @@ -32,6 +32,8 @@ #include "database/StarredArtist.hpp" #include "database/StarredRelease.hpp" #include "database/StarredTrack.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" #include "database/TrackLyrics.hpp" #include "database/UIState.hpp" #include "database/User.hpp" @@ -344,6 +346,8 @@ VALUES EXPECT_FALSE(Cluster::find(session, ClusterId{})); EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{})); EXPECT_FALSE(Directory::find(session, DirectoryId{})); + EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{})); + EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{})); EXPECT_FALSE(Image::find(session, ImageId{})); EXPECT_FALSE(Label::find(session, LabelId{})); EXPECT_FALSE(Listen::find(session, ListenId{})); diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp new file mode 100644 index 00000000..3ba25be0 --- /dev/null +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -0,0 +1,283 @@ +/* + * 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 + +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" +#include "database/Types.hpp" + +#include "Common.hpp" + +namespace lms::db::tests +{ + using ScopedTrackEmbeddedImage = ScopedEntity; + using ScopedTrackEmbeddedImageLink = ScopedEntity; + + TEST_F(DatabaseFixture, TrackEmbeddedImage) + { + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0); + } + + ScopedTrackEmbeddedImage image{ session }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(TrackEmbeddedImage::getCount(session), 1); + + const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + EXPECT_EQ(img->getHash(), db::ImageHashType{}); + EXPECT_EQ(img->getSize(), 0); + EXPECT_EQ(img->getWidth(), 0); + EXPECT_EQ(img->getHeight(), 0); + EXPECT_EQ(img->getMimeType(), ""); + } + + { + auto transaction{ session.createWriteTransaction() }; + + TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + img.modify()->setHash(db::ImageHashType{ std::numeric_limits::max() }); + img.modify()->setSize(1024 * 1024); + img.modify()->setWidth(640); + img.modify()->setHeight(480); + img.modify()->setMimeType("image/jpeg"); + } + + { + auto transaction{ session.createReadTransaction() }; + + const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + EXPECT_EQ(img->getHash(), db::ImageHashType{ std::numeric_limits::max() }); + EXPECT_EQ(img->getSize(), 1024 * 1024); + EXPECT_EQ(img->getWidth(), 640); + EXPECT_EQ(img->getHeight(), 480); + EXPECT_EQ(img->getMimeType(), "image/jpeg"); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByHash) + { + ScopedTrackEmbeddedImage image{ session }; + constexpr std::size_t size{ 1024 }; + constexpr db::ImageHashType hash{ 42 }; + { + auto transaction{ session.createWriteTransaction() }; + + TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + img.modify()->setHash(hash); + img.modify()->setSize(size); + } + + { + auto transaction{ session.createReadTransaction() }; + + const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size, hash) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + EXPECT_EQ(image.getId(), img->getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size + 1, hash) }; + EXPECT_EQ(img, TrackEmbeddedImage::pointer{}); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams) + { + ScopedTrackEmbeddedImage image{ session }; + ScopedTrack track{ session }; + ScopedRelease release{ session, "MyRelease" }; + ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; + + { + 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; }); + EXPECT_TRUE(visited); + } + + { + 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() }; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setIsPreferred(true); + params.setRelease(release.getId()); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_TRUE(visited); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setIsPreferred(true); + params.setTrack(track.getId()); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_TRUE(visited); + } + } + + TEST_F(DatabaseFixture, Track_findByEmbeddedImage) + { + ScopedTrackEmbeddedImage image{ session }; + ScopedTrack track{ session }; + ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; + + { + auto transaction{ session.createReadTransaction() }; + + Track::FindParameters params; + params.setEmbeddedImage(image->getId()); + + bool visited{}; + Track::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_TRUE(visited); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImage_findOrphans) + { + ScopedTrackEmbeddedImage image{ session }; + + { + auto transaction{ session.createReadTransaction() }; + + auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; + ASSERT_EQ(orphans.results.size(), 1); + EXPECT_EQ(orphans.results[0], image.getId()); + } + + { + ScopedTrack track{ session }; + ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; + + { + auto transaction{ session.createReadTransaction() }; + + auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; + ASSERT_EQ(orphans.results.size(), 0); + } + } + + { + auto transaction{ session.createReadTransaction() }; + + auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; + ASSERT_EQ(orphans.results.size(), 1); + EXPECT_EQ(orphans.results[0], image.getId()); + } + } + + TEST_F(DatabaseFixture, TrackEmbeddedImageLink) + { + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0); + } + + ScopedTrack track{ session }; + ScopedTrackEmbeddedImage image{ session }; + ScopedTrackEmbeddedImageLink imageLink{ session, track.lockAndGet(), image.lockAndGet() }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(TrackEmbeddedImageLink::getCount(session), 1); + + const TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; + ASSERT_NE(link, TrackEmbeddedImageLink::pointer{}); + EXPECT_EQ(link->getIndex(), 0); + EXPECT_EQ(link->getType(), ImageType::Unknown); + EXPECT_EQ(link->getDescription(), ""); + EXPECT_EQ(link->getTrack(), track.get()); + EXPECT_EQ(link->getImage(), image.get()); + } + + { + auto transaction{ session.createWriteTransaction() }; + + TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; + ASSERT_NE(link, TrackEmbeddedImage::pointer{}); + link.modify()->setIndex(2); + link.modify()->setType(ImageType::FrontCover); + link.modify()->setDescription("MyDesc"); + } + + { + auto transaction{ session.createReadTransaction() }; + + const TrackEmbeddedImageLink::pointer img{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; + ASSERT_NE(img, TrackEmbeddedImage::pointer{}); + EXPECT_EQ(img->getIndex(), 2); + EXPECT_EQ(img->getType(), ImageType::FrontCover); + EXPECT_EQ(img->getDescription(), "MyDesc"); + } + } +} // 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 9dca6e5a..a1b83427 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -24,7 +24,6 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" -#include "core/String.hpp" #include "core/Utils.hpp" #include "database/Db.hpp" #include "database/Image.hpp" @@ -204,9 +203,9 @@ namespace lms::cover return cover; } - std::shared_ptr ArtworkService::getTrackImage(db::TrackId trackId, std::optional width) + std::shared_ptr ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) { - const ImageCache::EntryDesc cacheEntryDesc{ trackId, width }; + const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width }; std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; if (cover) @@ -217,12 +216,14 @@ namespace lms::cover db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - const db::Track::pointer track{ db::Track::find(session, trackId) }; - if (track && track->hasCover()) - trackFile = track->getAbsoluteFilePath(); + db::Track::FindParameters params; + params.setEmbeddedImage(trackEmbeddedImageId); + db::Track::find(session, params, [&](const db::Track::pointer& track) { + if (!cover) + cover = getTrackImage(track->getAbsoluteFilePath(), width); + }); } - cover = getTrackImage(trackFile, width); if (cover) _cache.addImage(cacheEntryDesc, cover); diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index ab04d5ad..addfdca5 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -22,7 +22,6 @@ #include #include -#include "image/IEncodedImage.hpp" #include "services/artwork/IArtworkService.hpp" #include "ImageCache.hpp" @@ -49,7 +48,8 @@ namespace lms::cover private: std::shared_ptr getImage(db::ImageId imageId, std::optional width) override; - std::shared_ptr getTrackImage(db::TrackId trackId, std::optional width) override; + std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) override; + std::shared_ptr getDefaultReleaseCover() override; std::shared_ptr getDefaultArtistImage() override; diff --git a/src/libs/services/artwork/impl/ImageCache.hpp b/src/libs/services/artwork/impl/ImageCache.hpp index 6b60d577..61b484d5 100644 --- a/src/libs/services/artwork/impl/ImageCache.hpp +++ b/src/libs/services/artwork/impl/ImageCache.hpp @@ -26,6 +26,7 @@ #include #include "database/ImageId.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackId.hpp" #include "image/IEncodedImage.hpp" @@ -38,7 +39,7 @@ namespace lms::cover struct EntryDesc { - using VariantType = std::variant; + using VariantType = std::variant; VariantType id; std::optional size; diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index 66f986e6..5da77f4a 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -24,7 +24,7 @@ #include #include "database/ImageId.hpp" -#include "database/TrackId.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "image/IEncodedImage.hpp" namespace lms::db @@ -40,9 +40,7 @@ namespace lms::cover virtual ~IArtworkService() = default; virtual std::shared_ptr getImage(db::ImageId imageId, std::optional width) = 0; - - // no logic to fallback to release here - virtual std::shared_ptr getTrackImage(db::TrackId trackId, std::optional width) = 0; + virtual std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) = 0; // Svg images dont have image "size" virtual std::shared_ptr getDefaultReleaseCover() = 0; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index a82fd73c..26ddd8af 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -25,6 +25,7 @@ #include "core/PartialDateTime.hpp" #include "core/Path.hpp" #include "core/Service.hpp" +#include "core/XxHash3.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" @@ -34,6 +35,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" #include "database/TrackFeatures.hpp" #include "database/TrackLyrics.hpp" #include "database/Types.hpp" @@ -330,6 +333,117 @@ namespace lms::scanner return lyrics; } + struct ImageInfo + { + std::size_t index; + metadata::Image::Type type{ metadata::Image::Type::Unknown }; + std::uint64_t hash{}; + std::size_t size{}; + image::ImageProperties properties; + std::string mimeType; + std::string description; + }; + + db::ImageType convertImageType(metadata::Image::Type type) + { + switch (type) + { + case metadata::Image::Type::Unknown: + return db::ImageType::Unknown; + case metadata::Image::Type::Other: + return db::ImageType::Other; + case metadata::Image::Type::FileIcon: + return db::ImageType::FileIcon; + case metadata::Image::Type::OtherFileIcon: + return db::ImageType::OtherFileIcon; + case metadata::Image::Type::FrontCover: + return db::ImageType::FrontCover; + case metadata::Image::Type::BackCover: + return db::ImageType::BackCover; + case metadata::Image::Type::LeafletPage: + return db::ImageType::LeafletPage; + case metadata::Image::Type::Media: + return db::ImageType::Media; + case metadata::Image::Type::LeadArtist: + return db::ImageType::LeadArtist; + case metadata::Image::Type::Artist: + return db::ImageType::Artist; + case metadata::Image::Type::Conductor: + return db::ImageType::Conductor; + case metadata::Image::Type::Band: + return db::ImageType::Band; + case metadata::Image::Type::Composer: + return db::ImageType::Composer; + case metadata::Image::Type::Lyricist: + return db::ImageType::Lyricist; + case metadata::Image::Type::RecordingLocation: + return db::ImageType::RecordingLocation; + case metadata::Image::Type::DuringRecording: + return db::ImageType::DuringRecording; + case metadata::Image::Type::DuringPerformance: + return db::ImageType::DuringPerformance; + case metadata::Image::Type::MovieScreenCapture: + return db::ImageType::MovieScreenCapture; + case metadata::Image::Type::ColouredFish: + return db::ImageType::ColouredFish; + case metadata::Image::Type::Illustration: + return db::ImageType::Illustration; + case metadata::Image::Type::BandLogo: + return db::ImageType::BandLogo; + case metadata::Image::Type::PublisherLogo: + return db::ImageType::PublisherLogo; + } + } + + db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo) + { + db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) }; + if (!image) + { + image = session.create(); + image.modify()->setSize(imageInfo.size); + image.modify()->setHash(db::ImageHashType{ imageInfo.hash }); + image.modify()->setWidth(imageInfo.properties.width); + image.modify()->setHeight(imageInfo.properties.height); + image.modify()->setMimeType(imageInfo.mimeType); + } + + return image; + } + + db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo) + { + const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) }; + db::TrackEmbeddedImageLink::pointer imageLink{ session.create(track, image) }; + imageLink.modify()->setIndex(imageInfo.index); + imageLink.modify()->setType(convertImageType(imageInfo.type)); + imageLink.modify()->setDescription(imageInfo.description); + + return imageLink; + } + + 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) { if (!advisory) @@ -348,7 +462,7 @@ namespace lms::scanner return db::Advisory::UnSet; } - db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, bool hasEmbeddedCover, const FileInfo& fileInfo) + db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo) { db::Track::FindParameters params; // Add as many fields as possible to limit errors @@ -362,7 +476,6 @@ namespace lms::scanner } if (parsedTrack.position) params.setTrackNumber(*parsedTrack.position); - params.setHasEmbeddedImage(hasEmbeddedCover); params.setFileSize(fileInfo.fileSize); bool error{}; @@ -414,7 +527,7 @@ namespace lms::scanner metadata::IAudioFileParser& _parser; const ScannerSettings& _settings; std::unique_ptr _parsedTrack; - bool _hasEmbeddedCover{}; + std::vector _parsedImages; }; void AudioFileScanOperation::scan() @@ -426,16 +539,32 @@ namespace lms::scanner { _parsedTrack = _parser.parseMetaData(_file); + std::size_t index{}; _parser.parseImages(_file, [&](const metadata::Image& image) { try { - image::probeImage(image.data); - _hasEmbeddedCover = true; + image::ImageProperties properties{ image::probeImage(image.data) }; + + ImageInfo info; + info.index = index; + info.type = image.type; + { + LMS_SCOPED_TRACE_DETAILED("Scanner", "ImageHash"); + info.hash = core::xxHash3_64(image.data); + } + info.size = image.data.size(); + info.mimeType = image.mimeType; + info.description = image.description; + info.properties = properties; + + _parsedImages.push_back(std::move(info)); } catch (const image::Exception& e) { LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file); } + + index++; }); } catch (const metadata::Exception& e) @@ -521,7 +650,7 @@ namespace lms::scanner if (!track) { // maybe the file just moved? - track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, _hasEmbeddedCover, *fileInfo); + track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo); if (track) { LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath()); @@ -662,7 +791,6 @@ namespace lms::scanner track.modify()->setTrackMBID(_parsedTrack->mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setHasCover(_hasEmbeddedCover); track.modify()->setCopyright(_parsedTrack->copyright); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); @@ -672,10 +800,9 @@ namespace lms::scanner track.modify()->clearEmbeddedLyrics(); for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics) - { - db::TrackLyrics::pointer lyrics{ createLyrics(dbSession, lyricsInfo) }; - track.modify()->addLyrics(lyrics); - } + track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo)); + + updateEmbeddedImages(dbSession, track, _parsedImages); if (added) { diff --git a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp index 817fd7b7..131f5c65 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp @@ -27,6 +27,7 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" namespace lms::scanner { @@ -40,6 +41,7 @@ namespace lms::scanner removeOrphanedLabels(context); removeOrphanedCountries(context); removeOrphanedDirectories(context); + removeOrphanedTrackEmbeddedImages(context); } void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusters(ScanContext& context) @@ -90,6 +92,12 @@ namespace lms::scanner removeOrphanedEntries(context); } + void ScanStepRemoveOrphanedDbEntries::removeOrphanedTrackEmbeddedImages(ScanContext& context) + { + LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned embedded images in tracks..."); + removeOrphanedEntries(context); + } + template void ScanStepRemoveOrphanedDbEntries::removeOrphanedEntries(ScanContext& context) { diff --git a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp index eac6099c..72ca0315 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp @@ -41,6 +41,7 @@ namespace lms::scanner void removeOrphanedLabels(ScanContext& context); void removeOrphanedCountries(ScanContext& context); void removeOrphanedDirectories(ScanContext& context); + void removeOrphanedTrackEmbeddedImages(ScanContext& context); template void removeOrphanedEntries(ScanContext& context); diff --git a/src/libs/subsonic/impl/CoverArtId.cpp b/src/libs/subsonic/impl/CoverArtId.cpp index 7b64fd3b..8ec94c8b 100644 --- a/src/libs/subsonic/impl/CoverArtId.cpp +++ b/src/libs/subsonic/impl/CoverArtId.cpp @@ -19,32 +19,46 @@ #include "CoverArtId.hpp" -#include "SubsonicId.hpp" #include "core/String.hpp" +#include "database/ImageId.hpp" +#include "database/TrackEmbeddedImageId.hpp" namespace lms::api::subsonic { namespace { - constexpr char timestampSeparatorChar{ ':' }; - } + constexpr char timestampSeparatorChar{ '-' }; - std::string idToString(db::ImageId id) - { - return "im-" + id.toString(); - } + std::string idToString(db::ImageId id) + { + return "im-" + id.toString(); + } + + std::string idToString(db::TrackEmbeddedImageId id) + { + return "trim-" + id.toString(); + } + } // namespace std::string idToString(CoverArtId coverId) { - // produce "id:timestamp" - std::string res{ std::visit([](auto&& id) { - return idToString(id); - }, - coverId.id) }; + std::string res; - res += timestampSeparatorChar; - res += std::to_string(coverId.timestamp); + // 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); + } + assert(!res.empty()); return res; } } // namespace lms::api::subsonic @@ -52,46 +66,37 @@ namespace lms::api::subsonic // Used to parse parameters namespace lms::core::stringUtils { - template<> - std::optional readAs(std::string_view str) - { - std::vector values{ core::stringUtils::splitString(str, '-') }; - if (values.size() != 2) - return std::nullopt; - - if (values[0] != "im") - return std::nullopt; - - if (const auto value{ core::stringUtils::readAs(values[1]) }) - return db::ImageId{ *value }; - - return std::nullopt; - } - template<> std::optional readAs(std::string_view str) { - // expect "id:timestamp" - auto timeStampSeparator{ str.find_last_of(api::subsonic::timestampSeparatorChar) }; - if (timeStampSeparator == std::string_view::npos) + std::optional res; + + std::vector values{ core::stringUtils::splitString(str, '-') }; + if (values.size() <= 1) return std::nullopt; - std::string_view strId{ str.substr(0, timeStampSeparator) }; - std::string_view strTimestamp{ str.substr(timeStampSeparator + 1) }; + 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]) }; - api::subsonic::CoverArtId cover; - if (const auto imagetId{ readAs(strId) }) - cover.id = *imagetId; - else if (const auto trackId{ readAs(strId) }) - cover.id = *trackId; - else - return std::nullopt; + if (imageId && timestamp) + res.emplace(db::ImageId{ *imageId }, *timestamp); + } + } - if (const auto timestamp{ readAs(strTimestamp) }) - cover.timestamp = *timestamp; - else - return std::nullopt; - - return cover; + return res; } } // namespace lms::core::stringUtils diff --git a/src/libs/subsonic/impl/CoverArtId.hpp b/src/libs/subsonic/impl/CoverArtId.hpp index 6753e9bc..8d6f7a56 100644 --- a/src/libs/subsonic/impl/CoverArtId.hpp +++ b/src/libs/subsonic/impl/CoverArtId.hpp @@ -24,18 +24,23 @@ #include "core/String.hpp" #include "database/ImageId.hpp" -#include "database/TrackId.hpp" +#include "database/TrackEmbeddedImageId.hpp" namespace lms::api::subsonic { struct CoverArtId { - std::variant id; - std::time_t timestamp; + std::variant id; + std::optional timestamp; + + CoverArtId(db::ImageId _id, std::time_t _timestamp) + : id{ _id } + , timestamp{ _timestamp } {} + CoverArtId(db::TrackEmbeddedImageId _id) + : id{ _id } {} }; std::string idToString(CoverArtId coverId); - std::string idToString(db::ImageId imageId); } // namespace lms::api::subsonic // Used to parse parameters diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 4e63ee45..b3e764ae 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -30,6 +30,7 @@ #include "core/Utils.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackLyrics.hpp" #include "database/User.hpp" #include "services/artwork/IArtworkService.hpp" @@ -336,8 +337,8 @@ namespace lms::api::subsonic *size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 }); std::shared_ptr image; - if (const db::TrackId * trackId{ std::get_if(&coverArtId.id) }) - image = core::Service::get()->getTrackImage(*trackId, size); + 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); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 55fe3d82..eacefb0f 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -28,6 +28,7 @@ #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" @@ -93,13 +94,14 @@ namespace lms::api::subsonic } else { - db::Track::FindParameters params; + db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); - params.setHasEmbeddedImage(true); + params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); params.setRange(db::Range{ 0, 1 }); - db::Track::find(context.dbSession, params, [&](const db::Track::pointer& track) { - const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() }; + db::TrackEmbeddedImage::find(context.dbSession, params, [&](const db::TrackEmbeddedImage::pointer& image) { + const CoverArtId coverArtId{ image->getId() }; albumNode.setAttribute("coverArt", idToString(coverArtId)); }); } diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index a0c7f4be..e6a01ff4 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -20,7 +20,7 @@ #include "Playlist.hpp" #include "core/String.hpp" -#include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" @@ -43,14 +43,14 @@ namespace lms::api::subsonic if (const db::User::pointer user{ tracklist->getUser() }) playlistNode.setAttribute("owner", user->getLoginName()); - db::Track::FindParameters params; + db::TrackEmbeddedImage::FindParameters params; params.setTrackList(tracklist->getId()); - params.setHasEmbeddedImage(true); - params.setRange(db::Range{ 0, 1 }); - params.setSortMethod(db::TrackSortMethod::TrackList); + params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setRange(db::Range{ .offset = 0, .size = 1 }); - db::Track::find(session, params, [&](const db::Track::pointer& track) { - const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() }; + db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { + const CoverArtId coverArtId{ image->getId() }; playlistNode.setAttribute("coverArt", idToString(coverArtId)); }); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index ee1cc5c2..7fbcb3c9 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -32,6 +32,7 @@ #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" @@ -110,17 +111,25 @@ namespace lms::api::subsonic const Release::pointer release{ track->getRelease() }; - if (track->hasCover()) { - const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - } - else if (release) - { - if (const db::Image::pointer image{ release->getImage() }) - { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; + 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)); + } } } diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 87a76c35..22f4b984 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -253,8 +253,8 @@ namespace lms::ui << " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," << " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," << " artwork: [" - << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," - << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," + << " { 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\" }," << " ]" << "};"; // Update 'sizes' above to match this: diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index d85c815d..11305fc1 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -84,7 +84,7 @@ namespace lms::ui::utils std::unique_ptr createTrackImage(db::TrackId trackId, ArtworkResource::Size size) { auto cover{ std::make_unique() }; - cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size)); + cover->setImageLink(LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, size)); cover->setStyleClass("Lms-cover img-fluid"); // HACK cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK diff --git a/src/lms/ui/resource/ArtworkResource.cpp b/src/lms/ui/resource/ArtworkResource.cpp index a085298b..fb550db5 100644 --- a/src/lms/ui/resource/ArtworkResource.cpp +++ b/src/lms/ui/resource/ArtworkResource.cpp @@ -31,6 +31,8 @@ #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" +#include "database/Types.hpp" #include "services/artwork/IArtworkService.hpp" #include "LmsApplication.hpp" @@ -89,13 +91,14 @@ namespace lms::ui } else { - db::Track::FindParameters params; + db::TrackEmbeddedImage::FindParameters params; params.setRelease(releaseId); - params.setHasEmbeddedImage(true); + params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); params.setRange(db::Range{ 0, 1 }); - db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) { - url = getImageUrl(track->getId(), size, "release"); + db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) { + url = getImageUrl(image->getId(), size, "release"); }); } } @@ -107,24 +110,32 @@ namespace lms::ui return url; } - std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, std::optional size) const + std::string ArtworkResource::getPreferredTrackImageUrl(db::TrackId trackId, std::optional size) const { std::string url; { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) }; - if (track) + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(trackId); + params.setIsPreferred(true); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) { + url = getImageUrl(image->getId(), size, "release"); + }); + + if (url.empty()) { - if (track->hasCover()) + db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) }; + if (track) { - url = getImageUrl(trackId, size, "release"); - } - else if (const db::Release::pointer release{ track->getRelease() }) - { - if (const db::Image::pointer image{ release->getImage() }) - url = getImageUrl(image->getId(), size, "release"); + if (const db::Release::pointer release{ track->getRelease() }) + { + if (const db::Image::pointer image{ release->getImage() }) + url = getImageUrl(image->getId(), size, "release"); + } } } } @@ -144,9 +155,9 @@ namespace lms::ui return res; } - std::string ArtworkResource::getImageUrl(db::TrackId trackId, std::optional size, std::string_view type) const + std::string ArtworkResource::getImageUrl(db::TrackEmbeddedImageId trackId, std::optional size, std::string_view type) const { - std::string res{ url() + "&trackid=" + trackId.toString() + "&type=" + std::string{ type } }; + std::string res{ url() + "&trimageid=" + trackId.toString() + "&type=" + std::string{ type } }; if (size) res += "&size=" + std::to_string(static_cast(*size)); return res; @@ -168,13 +179,13 @@ namespace lms::ui // Retrieve parameters const std::string* imageIdStr = request.getParameter("imageid"); - const std::string* trackIdStr = request.getParameter("trackid"); + const std::string* trackEmbeddedImageIdStr = request.getParameter("trimageid"); const std::string* sizeStr = request.getParameter("size"); const std::string* typeStr = request.getParameter("type"); std::shared_ptr image; - if ((imageIdStr || trackIdStr)) + if ((imageIdStr || trackEmbeddedImageIdStr)) { const auto size{ sizeStr ? core::stringUtils::readAs(*sizeStr) : std::nullopt }; if (size && *size > maxSize) @@ -185,22 +196,13 @@ namespace lms::ui if (imageIdStr) { - const std::optional imageId{ core::stringUtils::readAs(*imageIdStr) }; - if (!imageId) - return; - - image = core::Service::get()->getImage(*imageId, size); + if (const auto imageId{ core::stringUtils::readAs(*imageIdStr) }) + image = core::Service::get()->getImage(*imageId, size); } - else if (trackIdStr) + else if (trackEmbeddedImageIdStr) { - const std::optional trackId{ core::stringUtils::readAs(*trackIdStr) }; - if (!trackId) - { - LOG(DEBUG, "track not found"); - return; - } - - image = core::Service::get()->getTrackImage(*trackId, size); + if (const auto imageId{ core::stringUtils::readAs(*trackEmbeddedImageIdStr) }) + image = core::Service::get()->getTrackEmbeddedImage(*imageId, size); } } diff --git a/src/lms/ui/resource/ArtworkResource.hpp b/src/lms/ui/resource/ArtworkResource.hpp index a6adb692..d54ddfa3 100644 --- a/src/lms/ui/resource/ArtworkResource.hpp +++ b/src/lms/ui/resource/ArtworkResource.hpp @@ -26,6 +26,7 @@ #include "database/ArtistId.hpp" #include "database/ImageId.hpp" #include "database/ReleaseId.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackId.hpp" namespace lms::ui @@ -46,11 +47,11 @@ namespace lms::ui std::string getArtistImageUrl(db::ArtistId artistId, std::optional size = std::nullopt) const; std::string getReleaseCoverUrl(db::ReleaseId releaseId, std::optional size = std::nullopt) const; - std::string getTrackImageUrl(db::TrackId trackId, std::optional size = std::nullopt) const; + std::string getPreferredTrackImageUrl(db::TrackId trackId, std::optional size = std::nullopt) const; private: std::string getImageUrl(db::ImageId imageId, std::optional size, std::string_view type) const; - std::string getImageUrl(db::TrackId trackId, std::optional size, std::string_view type) const; + std::string getImageUrl(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional size, std::string_view type) const; std::string getDefaultArtistImageUrl() const; std::string getDefaultReleaseCoverUrl() const; diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index df12a1c7..a390bd6a 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,4 +1,3 @@ add_subdirectory(db-generator) -add_subdirectory(cover) add_subdirectory(metadata) add_subdirectory(recommendation) diff --git a/src/tools/cover/CMakeLists.txt b/src/tools/cover/CMakeLists.txt deleted file mode 100644 index 3f80c8a0..00000000 --- a/src/tools/cover/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ - -add_executable(lms-cover - LmsCover.cpp - ) - -target_link_libraries(lms-cover PRIVATE - lmsartwork - Boost::program_options - ) - diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp deleted file mode 100644 index 2eaa8039..00000000 --- a/src/tools/cover/LmsCover.cpp +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2020 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 -#include -#include - -#include - -#include "core/IConfig.hpp" -#include "core/ILogger.hpp" -#include "core/Service.hpp" -#include "core/StreamLogger.hpp" -#include "core/SystemPaths.hpp" -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "image/Image.hpp" -#include "services/artwork/IArtworkService.hpp" - -namespace lms -{ - void dumpTrackCovers(db::Session& session, image::ImageSize width) - { - using namespace db; - - RangeResults trackIds; - { - auto transaction{ session.createReadTransaction() }; - trackIds = db::Track::findIds(session, db::Track::FindParameters{}); - } - - for (const db::TrackId trackId : trackIds.results) - { - std::cout << "Getting cover for track id " << trackId.toString() << std::endl; - core::Service::get()->getTrackImage(trackId, width); - } - } -} // namespace lms - -int main(int argc, char* argv[]) -{ - try - { - using namespace lms; - namespace po = boost::program_options; - - // log to stdout - core::Service logger{ std::make_unique(std::cout) }; - - po::options_description desc{ "Allowed options" }; - - // clang-format off - desc.add_options() - ("help,h", "print usage message") - ("conf,c", po::value()->default_value(core::sysconfDirectory / "lms.conf"), "LMS config file") - ("default-release-cover,d", po::value(), "Default release cover path") - ("default-artist-image,d", po::value(), "Default artist image") - ("tracks,t", "dump covers for tracks") - ("size,s", po::value()->default_value(512), "Requested cover size") - ("quality,q", po::value()->default_value(75), "JPEG quality (1-100)"); - // clang-format on - - po::variables_map vm; - po::store(po::parse_command_line(argc, argv, desc), vm); - - if (vm.count("help")) - { - std::cout << desc << std::endl; - return EXIT_SUCCESS; - } - - image::init(argv[0]); - core::Service config{ core::createConfig(vm["conf"].as()) }; - db::Db db{ config->getPath("working-dir", "/var/lms") / "lms.db" }; - core::Service coverArtService{ cover::createArtworkService(db, vm["default-release-cover,"].as(), vm["default-artist-image"].as()) }; - - coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as())); - - db::Session session{ db }; - - if (vm.count("tracks")) - dumpTrackCovers(session, vm["size"].as()); - } - catch (std::exception& e) - { - std::cerr << "Caught exception: " << e.what() << std::endl; - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -}