From 71dc99ad46d8f62ff253d5454eb4020350d11cef Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 29 Jun 2024 16:04:16 +0200 Subject: [PATCH] Added a scan step to scan artist images, ref #435 --- approot/messages.xml | 3 +- approot/messages_fr.xml | 3 +- approot/messages_it.xml | 3 +- approot/messages_pl.xml | 3 +- approot/messages_zh.xml | 3 +- src/libs/database/CMakeLists.txt | 1 + src/libs/database/impl/Artist.cpp | 6 + src/libs/database/impl/Image.cpp | 56 +++ src/libs/database/impl/MediaLibrary.cpp | 2 +- src/libs/database/impl/Migration.cpp | 469 +++++++++--------- src/libs/database/impl/Session.cpp | 4 + src/libs/database/include/database/Artist.hpp | 4 + src/libs/database/include/database/Image.hpp | 86 ++++ .../database/include/database/ImageId.hpp | 24 + src/libs/database/include/database/Object.hpp | 2 +- src/libs/database/test/CMakeLists.txt | 1 + src/libs/database/test/Common.cpp | 2 + src/libs/database/test/Image.cpp | 66 +++ .../image/impl/graphicsmagick/RawImage.cpp | 10 + .../image/impl/graphicsmagick/RawImage.hpp | 3 + src/libs/image/impl/stb/RawImage.hpp | 5 +- src/libs/image/include/image/IRawImage.hpp | 4 + src/libs/metadata/impl/Parser.cpp | 2 +- src/libs/services/cover/impl/CoverService.cpp | 93 +--- src/libs/services/cover/impl/CoverService.hpp | 1 - src/libs/services/scanner/CMakeLists.txt | 6 +- .../scanner/impl/ScanStepScanArtistImages.cpp | 347 +++++++++++++ .../scanner/impl/ScanStepScanArtistImages.hpp | 41 ++ ...anFiles.cpp => ScanStepScanAudioFiles.cpp} | 22 +- ...anFiles.hpp => ScanStepScanAudioFiles.hpp} | 8 +- .../services/scanner/impl/ScannerService.cpp | 9 +- .../include/services/scanner/ScannerStats.hpp | 4 +- src/libs/subsonic/impl/SubsonicId.cpp | 16 + src/libs/subsonic/impl/SubsonicId.hpp | 5 + .../impl/entrypoints/MediaLibraryScanning.cpp | 2 +- src/libs/subsonic/impl/responses/Artist.cpp | 4 +- src/lms/ui/admin/ScannerController.cpp | 11 +- 37 files changed, 983 insertions(+), 348 deletions(-) create mode 100644 src/libs/database/impl/Image.cpp create mode 100644 src/libs/database/include/database/Image.hpp create mode 100644 src/libs/database/include/database/ImageId.hpp create mode 100644 src/libs/database/test/Image.cpp create mode 100644 src/libs/services/scanner/impl/ScanStepScanArtistImages.cpp create mode 100644 src/libs/services/scanner/impl/ScanStepScanArtistImages.hpp rename src/libs/services/scanner/impl/{ScanStepScanFiles.cpp => ScanStepScanAudioFiles.cpp} (95%) rename src/libs/services/scanner/impl/{ScanStepScanFiles.hpp => ScanStepScanAudioFiles.hpp} (95%) diff --git a/approot/messages.xml b/approot/messages.xml index 4f1d5872..306b08aa 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -116,7 +116,8 @@ Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... Optimizing database... {1}/{2} entries ({3}%)... Reloading similarity engine: {1}%... -Scanning files: {1}/{2} files ({3}%)... +Scanning artist images: {1}/{2} artists ({3}%)... +Scanning audio files: {1}/{2} files ({3}%)... Step status diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 6a61b253..4ed82095 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -116,7 +116,8 @@ Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)... Optimisation de la base de données... {1}/{2} entrées ({3}%)... Rechargement du moteur de recommandation : {1}%... -Scan des fichiers : {1}/{2} fichiers ({3}%)... +Scan des images des artistes: {1}/{2} artists ({3}%)... +Scan des fichiers audio : {1}/{2} fichiers ({3}%)... Statut de l'étape diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 631454f9..2e04db1a 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -116,7 +116,8 @@ Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)... Ottimizzazione del database... {1}/{2} voci ({3}%)... Ricarica motore di tracce simili: {1}%... -Scansione files: {1}/{2} files ({3}%)... +Scansione delle immagini degli artisti: {1}/{2} artisti ({3}%)... +Scansione dei file audio: {1}/{2} files ({3}%)... Stato passo diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index b7e0a884..d1bf52da 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -133,7 +133,8 @@ Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)... Optymalizowanie bazy danych... {1}/{2} wpisów ({3}%)... Przeładowywanie silnika podobieństw: {1}%... -Skanowanie plików: {1}/{2} plików ({3}%)... +Skanowanie obrazów artystów: {1}/{2} artystów ({3}%)... +Skanowanie plików: {1}/{2} plików ({3}%)... Obecny krok diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 745cfe82..027bddca 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -116,7 +116,8 @@ 从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)... 重载相似引擎中 {1}%... -扫描文件中: {1}/{2} 个文件 ({3}%)... + +扫描文件中: {1}/{2} 个文件 ({3}%)... 当前步骤状态 diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index b5e5a194..4c55f425 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(lmsdatabase SHARED impl/AuthToken.cpp impl/Cluster.cpp impl/Db.cpp + impl/Image.cpp impl/Listen.cpp impl/MediaLibrary.cpp impl/Migration.cpp diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index ae93df40..065fbd1e 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -22,6 +22,7 @@ #include "core/ILogger.hpp" #include "database/Cluster.hpp" +#include "database/Image.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" @@ -273,6 +274,11 @@ namespace lms::db utils::forEachQueryRangeResult(query, params.range, func); } + ObjectPtr Artist::getImage() const + { + return ObjectPtr{ _image.lock() }; + } + RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const { assert(session()); diff --git a/src/libs/database/impl/Image.cpp b/src/libs/database/impl/Image.cpp new file mode 100644 index 00000000..2a0f5e3e --- /dev/null +++ b/src/libs/database/impl/Image.cpp @@ -0,0 +1,56 @@ +/* + * 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 "database/Image.hpp" + +#include + +#include "database/Artist.hpp" +#include "database/Session.hpp" + +#include "IdTypeTraits.hpp" +#include "PathTraits.hpp" +#include "Utils.hpp" + +namespace lms::db +{ + Image::Image(const std::filesystem::path& p) + : _path{ p } + { + } + + Image::pointer Image::create(Session& session, const std::filesystem::path& p) + { + return session.getDboSession()->add(std::unique_ptr{ new Image{ p } }); + } + + std::size_t Image::getCount(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM image")); + } + + Image::pointer Image::find(Session& session, ImageId id) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); + } +} // namespace lms::db diff --git a/src/libs/database/impl/MediaLibrary.cpp b/src/libs/database/impl/MediaLibrary.cpp index 74a7a2e1..59655bc1 100644 --- a/src/libs/database/impl/MediaLibrary.cpp +++ b/src/libs/database/impl/MediaLibrary.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2016 Emeric Poupon + * Copyright (C) 2024 Emeric Poupon * * This file is part of LMS. * diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 4dacba8b..f18c41cd 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{ 59 }; + static constexpr Version LMS_DATABASE_VERSION{ 60 }; } VersionInfo::VersionInfo() @@ -86,11 +86,13 @@ namespace lms::db::Migration Db& _db; }; - static void migrateFromV33(Session& session) + namespace { - // remove name from track_artist_link - // Drop Auth mode - session.getDboSession()->execute(R"( + void migrateFromV33(Session& session) + { + // remove name from track_artist_link + // Drop Auth mode + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -101,49 +103,49 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); - session.getDboSession()->execute("DROP TABLE track_artist_link"); - session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); - } + session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); + session.getDboSession()->execute("DROP TABLE track_artist_link"); + session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); + } - static void migrateFromV34(Session& session) - { - // Add scrobbling state - // By default, everything needs to be sent - session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); - session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); - session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); - } + void migrateFromV34(Session& session) + { + // Add scrobbling state + // By default, everything needs to be sent + session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); + session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); + session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/ 0)) + ")"); + } - static void migrateFromV35(Session& session) - { - // Add creattion/last modif date time for tracklists - session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); - session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); - } + void migrateFromV35(Session& session) + { + // Add creattion/last modif date time for tracklists + session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); + session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); + } - static void migrateFromV36(Session& session) - { - // Increased precision for track durations (now in milliseconds instead of secodns) - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + void migrateFromV36(Session& session) + { + // Increased precision for track durations (now in milliseconds instead of secodns) + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV37(Session& session) - { - // Support Performer tags (via subtypes) - session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT"); + void migrateFromV37(Session& session) + { + // Support Performer tags (via subtypes) + session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV38(Session& session) - { - // migrate release-specific tags from Track to Release - session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER"); + void migrateFromV38(Session& session) + { + // migrate release-specific tags from Track to Release + session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER"); - session.getDboSession()->execute(R"( + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -170,178 +172,178 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ); ))"); - session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); - session.getDboSession()->execute("DROP TABLE track"); - session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); + session.getDboSession()->execute("DROP TABLE track"); + session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV39(Session& session) - { - // add release type - session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER"); - session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER"); + void migrateFromV39(Session& session) + { + // add release type + session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER"); + session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV40(Session& session) - { - // add artist_display_name in Release and Track - session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); - session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); + void migrateFromV40(Session& session) + { + // add artist_display_name in Release and Track + session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV41(Session& session) - { - // add artist_display_name in Release and Track - session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); - session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); - session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); - } + void migrateFromV41(Session& session) + { + // add artist_display_name in Release and Track + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); + session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); + } - static void migrateFromV42(Session& session) - { - session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); - session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); + void migrateFromV42(Session& session) + { + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); + session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); - // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) - session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); - session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER"); - session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); - session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); - session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); - session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); - session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); + // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) + session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); + session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER"); + session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); + session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); - session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend"); - } + session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend"); + } - static void migrateFromV43(Session& session) - { - // add counts in genre table - session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER"); - session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER"); + void migrateFromV43(Session& session) + { + // add counts in genre table + session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER"); + session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - static void migrateFromV44(Session& session) - { - // add bitrate - session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); + void migrateFromV44(Session& session) + { + // add bitrate + session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV45(Session& session) - { - // add subsonic_enable_transcoding_by_default, default is disabled - session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")"); - } + void migrateFromV45(Session& session) + { + // add subsonic_enable_transcoding_by_default, default is disabled + session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")"); + } - void migrateFromV46(Session& session) - { - // add extra tags to parse - session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( + void migrateFromV46(Session& session) + { + // add extra tags to parse + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null );)"); - session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); - session.getDboSession()->execute("DROP TABLE cluster_type"); - session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); + session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); + session.getDboSession()->execute("DROP TABLE cluster_type"); + session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); - session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); + session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV47(Session& session) - { - // release type, new way - session.getDboSession()->execute("ALTER TABLE release DROP primary_type"); - session.getDboSession()->execute("ALTER TABLE release DROP secondary_types"); + void migrateFromV47(Session& session) + { + // release type, new way + session.getDboSession()->execute("ALTER TABLE release DROP primary_type"); + session.getDboSession()->execute("ALTER TABLE release DROP secondary_types"); - session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null))"); - session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( "release_type_id" bigint, "release_id" bigint, primary key ("release_type_id", "release_id"), constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred, constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); - session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); + session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); + session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV48(Session& session) - { - // Regression for the extra tags not being parsed - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + void migrateFromV48(Session& session) + { + // Regression for the extra tags not being parsed + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV49(Session& session) - { - // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) - session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER"); - session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER"); + void migrateFromV49(Session& session) + { + // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) + session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER"); + session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV50(Session& session) - { - // MediaLibrary support - session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( + void migrateFromV50(Session& session) + { + // MediaLibrary support + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( "id" integer primary key autoincrement, "version" integer not null, "path" text not null, "name" text not null ))"); - const int scanSettingsId{ session.getDboSession()->query("SELECT id FROM scan_settings") }; + const int scanSettingsId{ session.getDboSession()->query("SELECT id FROM scan_settings") }; - // Convert the existing media_directory in the scan_settings table to a media_library with id '1' - session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") + // Convert the existing media_directory in the scan_settings table to a media_library with id '1' + session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") SELECT 1, 0, s_s.media_directory, "Main" FROM scan_settings s_s WHERE id = ?)") - .bind(scanSettingsId); + .bind(scanSettingsId); - // Remove the outdated column in scan_settings - session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory"); + // Remove the outdated column in scan_settings + session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory"); - // Add the media_library column in tracks, with id '1' - session.getDboSession()->execute(R"( + // Add the media_library column in tracks, with id '1' + session.getDboSession()->execute(R"( CREATE TABLE IF NOT EXISTS "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -374,8 +376,8 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred ))"); - // Migrate data, with the new media_library_id field set to 1 - session.getDboSession()->execute(R"(INSERT INTO track_backup + // Migrate data, with the new media_library_id field set to 1 + session.getDboSession()->execute(R"(INSERT INTO track_backup SELECT id, version, @@ -405,76 +407,96 @@ SELECT release_id, 1 FROM track)"); - session.getDboSession()->execute("DROP TABLE track"); - session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); - } + session.getDboSession()->execute("DROP TABLE track"); + session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); + } - void migrateFromV51(Session& session) - { - // Add custom artist tag delimiters, no need to rescan since it has no effect when empty - session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); - session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); - } + void migrateFromV51(Session& session) + { + // Add custom artist tag delimiters, no need to rescan since it has no effect when empty + session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); + session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); + } - void migrateFromV52(Session& session) - { - // Add sort name for releases - session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); + void migrateFromV52(Session& session) + { + // Add sort name for releases + session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV53(Session& session) - { - // Add release group mbid - session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); + void migrateFromV53(Session& session) + { + // Add release group mbid + session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV54(Session& session) - { - // Add file size + relative file path - session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path"); - session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)"); - session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''"); + void migrateFromV54(Session& session) + { + // Add file size + relative file path + session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path"); + session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV55(Session& session) - { - // Add bitsPerSample, channelCount and sampleRate - session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)"); - session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)"); - session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)"); + void migrateFromV55(Session& session) + { + // Add bitsPerSample, channelCount and sampleRate + session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)"); + session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); - } + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } - void migrateFromV56(Session& session) - { - // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all - std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; - for (const auto& indexName : indexeNames) - session.getDboSession()->execute("DROP INDEX " + indexName); - } + void migrateFromV56(Session& session) + { + // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all + std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; + for (const auto& indexName : indexeNames) + session.getDboSession()->execute("DROP INDEX " + indexName); + } - void migrateFromV57(Session& session) - { - // useless index, may have been already removed in the previous step - session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx"); - } + void migrateFromV57(Session& session) + { + // useless index, may have been already removed in the previous step + session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx"); + } - void migrateFromV58(Session& session) - { - // DSF support - session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'"); - } + void migrateFromV58(Session& session) + { + // DSF support + session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'"); + } + + void migrateFromV59(Session& session) + { + // Dedicated image table + session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "image" ( + "id" integer primary key autoincrement, + "version" integer not null, + "path" text not null, + "file_last_write" text, + "file_size" integer not null, + "width" integer not null, + "height" integer not null, + "artist_id" bigint, + constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred +))"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } + } // namespace bool doDbMigration(Session& session) { @@ -511,6 +533,7 @@ SELECT { 56, migrateFromV56 }, { 57, migrateFromV57 }, { 58, migrateFromV58 }, + { 59, migrateFromV59 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index d0c5c90d..9364e030 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -26,6 +26,7 @@ #include "database/AuthToken.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" +#include "database/Image.hpp" #include "database/Listen.hpp" #include "database/MediaLibrary.hpp" #include "database/Release.hpp" @@ -92,6 +93,7 @@ namespace lms::db _session.mapClass("auth_token"); _session.mapClass("cluster"); _session.mapClass("cluster_type"); + _session.mapClass("image"); _session.mapClass("listen"); _session.mapClass("media_library"); _session.mapClass("release"); @@ -177,6 +179,8 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 2081f9d9..2b914463 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -44,6 +44,7 @@ namespace lms::db class Cluster; class ClusterType; + class Image; class Release; class Session; class StarredArtist; @@ -139,6 +140,7 @@ namespace lms::db const std::string& getName() const { return _name; } const std::string& getSortName() const { return _sortName; } std::optional getMBID() const { return core::UUID::fromString(_MBID); } + ObjectPtr getImage() const; // No artistLinkTypes means get them all RangeResults findSimilarArtistIds(core::EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; @@ -159,6 +161,7 @@ namespace lms::db Wt::Dbo::field(a, _sortName, "sort_name"); Wt::Dbo::field(a, _MBID, "mbid"); + Wt::Dbo::hasOne(a, _image, "artist"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); } @@ -175,6 +178,7 @@ namespace lms::db std::string _sortName; std::string _MBID; // Musicbrainz Identifier + Wt::Dbo::weak_ptr _image; Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist Wt::Dbo::collection> _starredArtists; // starred entries for this artist }; diff --git a/src/libs/database/include/database/Image.hpp b/src/libs/database/include/database/Image.hpp new file mode 100644 index 00000000..d164392b --- /dev/null +++ b/src/libs/database/include/database/Image.hpp @@ -0,0 +1,86 @@ +/* + * 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/ArtistId.hpp" +#include "database/ImageId.hpp" +#include "database/Object.hpp" + +namespace lms::db +{ + class Artist; + class Session; + + class Image final : public Object + { + public: + Image() = default; + + // find + static std::size_t getCount(Session& session); + static pointer find(Session& session, ImageId id); + + // getters + const std::filesystem::path& getPath() const { return _path; } + const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } + std::size_t getFileSize() const { return _fileSize; } + std::size_t getWidth() const { return _width; } + std::size_t getHeight() const { return _height; } + + // setters + void setPath(const std::filesystem::path& p) { _path = p; } + void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } + void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } + void setWidth(std::size_t width) { _width = width; } + void setHeight(std::size_t height) { _height = height; } + void setArtist(const ObjectPtr& artist) { _artist = getDboPtr(artist); } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _path, "path"); + Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); + Wt::Dbo::field(a, _fileSize, "file_size"); + + Wt::Dbo::field(a, _width, "width"); + Wt::Dbo::field(a, _height, "height"); + + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + } + + private: + friend class Session; + Image(const std::filesystem::path& p); + static pointer create(Session& session, const std::filesystem::path& p); + + std::filesystem::path _path; + Wt::WDateTime _fileLastWrite; + int _fileSize{}; + int _width{}; + int _height{}; + + Wt::Dbo::ptr _artist; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/ImageId.hpp b/src/libs/database/include/database/ImageId.hpp new file mode 100644 index 00000000..5b4424f0 --- /dev/null +++ b/src/libs/database/include/database/ImageId.hpp @@ -0,0 +1,24 @@ +/* + * 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 "database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ImageId) diff --git a/src/libs/database/include/database/Object.hpp b/src/libs/database/include/database/Object.hpp index 187b0c3f..b7acee4f 100644 --- a/src/libs/database/include/database/Object.hpp +++ b/src/libs/database/include/database/Object.hpp @@ -95,7 +95,7 @@ namespace lms::db // Can get raw dbo ptr only from Objects template - static Wt::Dbo::ptr getDboPtr(ObjectPtr ptr) + static Wt::Dbo::ptr getDboPtr(const ObjectPtr& ptr) { return ptr._obj; } diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 451581c8..6f161970 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(test-database Cluster.cpp Common.cpp DatabaseTest.cpp + Image.cpp Listen.cpp Migration.cpp Release.cpp diff --git a/src/libs/database/test/Common.cpp b/src/libs/database/test/Common.cpp index b61399b7..f9b277e0 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -22,6 +22,7 @@ #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" +#include "database/Image.hpp" #include "database/Listen.hpp" #include "database/MediaLibrary.hpp" #include "database/Release.hpp" @@ -80,6 +81,7 @@ namespace lms::db::tests EXPECT_EQ(Cluster::getCount(session), 0); EXPECT_EQ(ClusterType::getCount(session), 0); EXPECT_EQ(Listen::getCount(session), 0); + EXPECT_EQ(Image::getCount(session), 0); EXPECT_EQ(MediaLibrary::getCount(session), 0); EXPECT_EQ(Release::getCount(session), 0); EXPECT_EQ(StarredArtist::getCount(session), 0); diff --git a/src/libs/database/test/Image.cpp b/src/libs/database/test/Image.cpp new file mode 100644 index 00000000..99a7c55c --- /dev/null +++ b/src/libs/database/test/Image.cpp @@ -0,0 +1,66 @@ +/* + * 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 "Common.hpp" + +#include "database/Image.hpp" + +namespace lms::db::tests +{ + using ScopedImage = ScopedEntity; + + TEST_F(DatabaseFixture, Image) + { + ScopedImage image{ session, "/path/to/image" }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Image::getCount(session), 1); + + Image::pointer img{ Image::find(session, image.getId()) }; + ASSERT_NE(img, Image::pointer{}); + EXPECT_EQ(img->getPath(), "/path/to/image"); + EXPECT_EQ(img->getWidth(), 0); + EXPECT_EQ(img->getHeight(), 0); + EXPECT_EQ(img->getFileSize(), 0); + } + + { + auto transaction{ session.createWriteTransaction() }; + + Image::pointer img{ Image::find(session, image.getId()) }; + ASSERT_NE(img, Image::pointer{}); + img.modify()->setPath("/path/to/another/image"); + img.modify()->setWidth(640); + img.modify()->setHeight(480); + img.modify()->setFileSize(1024 * 1024); + } + + { + auto transaction{ session.createReadTransaction() }; + + Image::pointer img{ Image::find(session, image.getId()) }; + ASSERT_NE(img, Image::pointer{}); + EXPECT_EQ(img->getPath(), "/path/to/another/image"); + EXPECT_EQ(img->getWidth(), 640); + EXPECT_EQ(img->getHeight(), 480); + EXPECT_EQ(img->getFileSize(), 1024 * 1024); + } + } +} // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/image/impl/graphicsmagick/RawImage.cpp b/src/libs/image/impl/graphicsmagick/RawImage.cpp index 408c8e90..5979ae61 100644 --- a/src/libs/image/impl/graphicsmagick/RawImage.cpp +++ b/src/libs/image/impl/graphicsmagick/RawImage.cpp @@ -104,6 +104,16 @@ namespace lms::image::GraphicsMagick } } + ImageSize RawImage::getWidth() const + { + return _image.size().width(); + } + + ImageSize RawImage::getHeight() const + { + return _image.size().height(); + } + void RawImage::resize(ImageSize width) { try diff --git a/src/libs/image/impl/graphicsmagick/RawImage.hpp b/src/libs/image/impl/graphicsmagick/RawImage.hpp index b217f9d2..13fc7b59 100644 --- a/src/libs/image/impl/graphicsmagick/RawImage.hpp +++ b/src/libs/image/impl/graphicsmagick/RawImage.hpp @@ -35,6 +35,9 @@ namespace lms::image::GraphicsMagick RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::filesystem::path& path); + ImageSize getWidth() const override; + ImageSize getHeight() const override; + void resize(ImageSize width) override; std::unique_ptr encodeToJPEG(unsigned quality) const override; diff --git a/src/libs/image/impl/stb/RawImage.hpp b/src/libs/image/impl/stb/RawImage.hpp index 65fd822c..ceaccd8c 100644 --- a/src/libs/image/impl/stb/RawImage.hpp +++ b/src/libs/image/impl/stb/RawImage.hpp @@ -33,11 +33,12 @@ namespace lms::image::STB RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::filesystem::path& path); + ImageSize getWidth() const override; + ImageSize getHeight() const override; + void resize(ImageSize width) override; std::unique_ptr encodeToJPEG(unsigned quality) const override; - ImageSize getWidth() const; - ImageSize getHeight() const; const std::byte* getData() const; private: diff --git a/src/libs/image/include/image/IRawImage.hpp b/src/libs/image/include/image/IRawImage.hpp index 19b074a8..75ed8c88 100644 --- a/src/libs/image/include/image/IRawImage.hpp +++ b/src/libs/image/include/image/IRawImage.hpp @@ -27,6 +27,10 @@ namespace lms::image { public: virtual ~IRawImage() = default; + + virtual ImageSize getWidth() const = 0; + virtual ImageSize getHeight() const = 0; + virtual void resize(ImageSize width) = 0; virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; }; diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp index ffff85c8..d99ffec5 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/Parser.cpp @@ -301,7 +301,7 @@ namespace lms::metadata // But to please most users, if we find a custom delimiter in the Artist tag, we construct the artist diplay string with a "nicer" join if (!_artistTagDelimiters.empty() && track.artists.size() > 1 - && getTagValuesAs(tagReader, { TagType::Artist }, _artistTagDelimiters).size() > 1) + && getTagValuesAs(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1) { std::vector artistNames; std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; }); diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index acc8e848..80571a85 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -30,6 +30,7 @@ #include "core/Utils.hpp" #include "database/Artist.hpp" #include "database/Db.hpp" +#include "database/Image.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" @@ -86,19 +87,6 @@ namespace lms::cover return res; } - std::vector constructArtistFileNames() - { - std::vector res; - - core::Service::get()->visitStrings("artist-image-file-names", - [&res](std::string_view fileName) { - res.emplace_back(fileName); - }, - { "artist" }); - - return res; - } - bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions) { return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); @@ -118,7 +106,6 @@ namespace lms::cover , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _maxFileSize{ core::Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _preferredFileNames{ constructPreferredFileNames() } - , _artistFileNames{ constructArtistFileNames() } { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); @@ -389,87 +376,15 @@ namespace lms::cover if (artistImage) return artistImage; - std::string artistName; - std::string artistMBID; - - std::set releasePaths; - std::set multiArtistReleasePaths; - { Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - const Artist::pointer artist{ Artist::find(session, artistId) }; - if (!artist) - return artistImage; - - artistName = artist->getName(); - if (auto mbid{ artist->getMBID() }) - artistMBID = mbid->getAsString(); - - Track::FindParameters params; - params.setArtist(artistId, { TrackArtistLinkType::ReleaseArtist }); - - Track::find(session, params, [&](const Track::pointer& track) { - Artist::FindParameters artistFindParams; - artistFindParams.setTrack(track->getId()); - artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist); - - const auto releaseArtists{ Artist::findIds(session, artistFindParams) }; - if (releaseArtists.results.size() == 1) - releasePaths.insert(track->getAbsoluteFilePath().parent_path()); - else - multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path()); - }); - } - - std::vector artistFileNames; - if (!artistMBID.empty()) - artistFileNames.push_back(artistMBID); - artistFileNames.push_back(artistName); - - std::vector artistFileNamesWithGenericNames{ artistFileNames }; - artistFileNamesWithGenericNames.insert(artistFileNamesWithGenericNames.end(), std::cbegin(_artistFileNames), std::cend(_artistFileNames)); - - // Expect layout like this: - // ReleaseArtist/Release/Tracks' - // /artist-mbid.jpg - // /artist-name.jpg - // /artist.jpg - if (!releasePaths.empty()) - { - const std::filesystem::path artistPath{ releasePaths.size() == 1 ? releasePaths.begin()->parent_path() : core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; - artistImage = getFromDirectory(artistPath, width, artistFileNamesWithGenericNames, false); - } - - // Expect layout like this: - // ReleaseArtist/Release/Tracks' - // /artist-mbid.jpg - // /artist-name.jpg - // /artist.jpg - if (!artistImage) - { - for (const std::filesystem::path& releasePath : releasePaths) + if (const Artist::pointer artist{ db::Artist::find(session, artistId) }) { - artistImage = getFromDirectory(releasePath, width, artistFileNamesWithGenericNames, false); - if (artistImage) - break; - } - } - - // Expect layout like this: - // Only search for the artist's name in the release path, as we can't map a generic name to several artists - // ReleaseArtist/Release/Tracks' - // /artist-name.jpg - // /artist-mbid.jpg - if (!artistImage) - { - for (const std::filesystem::path& releasePath : multiArtistReleasePaths) - { - artistImage = getFromDirectory(releasePath, width, artistFileNames, false); - if (artistImage) - break; + if (const db::Image::pointer image{ artist->getImage() }) + artistImage = getFromCoverFile(image->getPath(), width); } } diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index 0eb0bf94..a5899bc4 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -76,7 +76,6 @@ namespace lms::cover static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize const std::size_t _maxFileSize; const std::vector _preferredFileNames; - const std::vector _artistFileNames; unsigned _jpegQuality; }; diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index dbe27c9c..28b02f43 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -8,7 +8,8 @@ add_library(lmsscanner SHARED impl/ScanStepDiscoverFiles.cpp impl/ScanStepOptimize.cpp impl/ScanStepRemoveOrphanDbFiles.cpp - impl/ScanStepScanFiles.cpp + impl/ScanStepScanArtistImages.cpp + impl/ScanStepScanAudioFiles.cpp ) target_include_directories(lmsscanner INTERFACE @@ -20,10 +21,11 @@ target_include_directories(lmsscanner PRIVATE ) target_link_libraries(lmsscanner PRIVATE + lmscore lmsdatabase + lmsimage lmsmetadata lmsrecommendation - lmscore ) target_link_libraries(lmsscanner PUBLIC diff --git a/src/libs/services/scanner/impl/ScanStepScanArtistImages.cpp b/src/libs/services/scanner/impl/ScanStepScanArtistImages.cpp new file mode 100644 index 00000000..9696231a --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepScanArtistImages.cpp @@ -0,0 +1,347 @@ +/* + * 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 "ScanStepScanArtistImages.hpp" + +#include +#include +#include +#include + +#include "core/IConfig.hpp" +#include "core/ILogger.hpp" +#include "core/Path.hpp" +#include "database/Artist.hpp" +#include "database/Db.hpp" +#include "database/Image.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" + +namespace lms::scanner +{ + namespace + { + constexpr std::size_t readBatchSize{ 10 }; + constexpr std::size_t writeBatchSize{ 5 }; + + struct ImageInfo + { + operator bool() const { return !imagePath.empty(); } + void clear() + { + imagePath.clear(); + lastWriteTime = {}; + fileSize = {}; + height = {}; + width = {}; + } + + std::filesystem::path imagePath; + Wt::WDateTime lastWriteTime; + std::size_t fileSize{}; + std::size_t height{}; + std::size_t width{}; + }; + + bool tryDecodeImage(const std::filesystem::path& imagePath, ImageInfo& imageInfo) + { + assert(!imageInfo); + + try + { + std::unique_ptr rawImage{ image::decodeImage(imagePath) }; + imageInfo.imagePath = imagePath; + imageInfo.fileSize = std::filesystem::file_size(imagePath); + imageInfo.width = rawImage->getWidth(); + imageInfo.height = rawImage->getHeight(); + imageInfo.lastWriteTime = core::pathUtils::getLastWriteTime(imagePath); + } + catch (const image::Exception& e) + { + LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file '" << imagePath.string() << "': " << e.what()); + return false; + } + + return true; + } + + struct ArtistImageInfo + { + db::ArtistId artistId; + ImageInfo imageInfo; + }; + + using ArtistImageInfoContainer = std::deque; + + bool isFileSupported(const std::filesystem::path& file) + { + static const std::array fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize + + return (std::find(std::cbegin(fileExtensions), std::cend(fileExtensions), file.extension()) != std::cend(fileExtensions)); + } + + std::multimap getImagePaths(const std::filesystem::path& directoryPath, const std::vector& fileNames) + { + std::multimap res; + std::error_code ec; + + std::filesystem::directory_iterator itPath(directoryPath, ec); + const std::filesystem::directory_iterator itEnd; + while (!ec && itPath != itEnd) + { + const std::filesystem::path& path{ *itPath }; + const std::string stem{ path.stem().string() }; + if (isFileSupported(path) + && std::any_of(std::cbegin(fileNames), std::cend(fileNames), [&](const std::string& fileName) { return core::stringUtils::stringCaseInsensitiveEqual(stem, fileName); })) + { + res.emplace(stem, path); + } + + itPath.increment(ec); + } + + return res; + } + + bool findImageInDirectory(const std::filesystem::path& directory, const std::vector& fileNames, ImageInfo& imageInfo) + { + assert(!imageInfo); + + const std::multimap coverPaths{ getImagePaths(directory, fileNames) }; + + for (const std::string_view fileName : fileNames) + { + const auto range{ coverPaths.equal_range(std::string{ fileName }) }; + for (auto it{ range.first }; it != range.second; ++it) + { + if (tryDecodeImage(it->second, imageInfo)) + return true; + } + } + + return false; + } + + void fetchArtistImageInfo(db::Session& session, const std::vector& genericArtistFileNames, const db::Artist::pointer& artist, ImageInfo& imageInfo) + { + const std::string artistMBID{ [&] { + std::string artistMBID; + if (auto mbid{ artist->getMBID() }) + artistMBID = mbid->getAsString(); + return artistMBID; + }() }; + + std::set releasePaths; + std::set multiArtistReleasePaths; + + db::Track::FindParameters params; + params.setArtist(artist->getId(), { db::TrackArtistLinkType::ReleaseArtist }); + + db::Track::find(session, params, [&](const db::Track::pointer& track) { + db::Artist::FindParameters artistFindParams; + artistFindParams.setTrack(track->getId()); + artistFindParams.setLinkType(db::TrackArtistLinkType::ReleaseArtist); + + const auto releaseArtists{ db::Artist::findIds(session, artistFindParams) }; + if (releaseArtists.results.size() == 1) + releasePaths.insert(track->getAbsoluteFilePath().parent_path()); + else + multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path()); + }); + + std::vector artistFileNames; + if (!artistMBID.empty()) + artistFileNames.push_back(artistMBID); + artistFileNames.push_back(artist->getName()); + + std::vector artistFileNamesWithGenericNames{ artistFileNames }; + artistFileNamesWithGenericNames.insert(artistFileNamesWithGenericNames.end(), std::cbegin(genericArtistFileNames), std::cend(genericArtistFileNames)); + + // Expect layout like this: + // ReleaseArtist/Release/Tracks' + // /artist-mbid.jpg + // /artist-name.jpg + // /artist.jpg + if (!releasePaths.empty()) + { + const std::filesystem::path artistPath{ releasePaths.size() == 1 ? releasePaths.begin()->parent_path() : core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; + if (findImageInDirectory(artistPath, artistFileNamesWithGenericNames, imageInfo)) + return; + } + + // Expect layout like this: + // ReleaseArtist/Release/Tracks' + // /artist-mbid.jpg + // /artist-name.jpg + // /artist.jpg + for (const std::filesystem::path& releasePath : releasePaths) + { + // TODO: what if an artist has released an album that bears their name? + if (findImageInDirectory(releasePath, artistFileNamesWithGenericNames, imageInfo)) + return; + } + + // Expect layout like this: + // Only search for the artist's name in the release path, as we can't map a generic name to several artists + // ReleaseArtist/Release/Tracks' + // /artist-name.jpg + // /artist-mbid.jpg + for (const std::filesystem::path& releasePath : multiArtistReleasePaths) + { + if (findImageInDirectory(releasePath, artistFileNames, imageInfo)) + return; + } + } + + bool artistImageNeedsUpdate(const db::Image::pointer& image, const ImageInfo& imageInfo) + { + if (!imageInfo && !image) // no image as before + return false; + else if (!imageInfo && image) // no longer has image + return true; + else if (imageInfo && !image) // image has been added + return true; + + assert(imageInfo); + // artist image still here, consider it is the same only if the last modified time is the same + return imageInfo.lastWriteTime != image->getLastWriteTime(); + } + + struct SearchImageContext + { + db::Session& session; + db::ArtistId lastRetrievedArtistId; + const std::vector& artistFileNames; + bool fullScan; + }; + + bool fetchNextArtistImagesToUpdate(SearchImageContext& searchContext, ArtistImageInfoContainer& artistImageInfoList) + { + const db::ArtistId artistId{ searchContext.lastRetrievedArtistId }; + ImageInfo imageInfo; + + { + auto transaction{ searchContext.session.createReadTransaction() }; + + db::Artist::find(searchContext.session, searchContext.lastRetrievedArtistId, readBatchSize, [&](const db::Artist::pointer& artist) { + imageInfo.clear(); + + fetchArtistImageInfo(searchContext.session, searchContext.artistFileNames, artist, imageInfo); + if (imageInfo) + LMS_LOG(DBUPDATER, DEBUG, "Found artist image for artist '" << artist->getName() << "' at '" << imageInfo.imagePath << "'"); + + if (searchContext.fullScan || artistImageNeedsUpdate(artist->getImage(), imageInfo)) + artistImageInfoList.push_back(ArtistImageInfo{ artist->getId(), imageInfo }); + }); + } + + return artistId != searchContext.lastRetrievedArtistId; + } + + void updateArtistImage(db::Session& session, const ArtistImageInfo& artistImageInfo) + { + db::Artist::pointer artist{ db::Artist::find(session, artistImageInfo.artistId) }; + assert(artist); + + db::Image::pointer image{ artist->getImage() }; + const ImageInfo& imageInfo{ artistImageInfo.imageInfo }; + + if (!imageInfo) + { + if (image) + image.remove(); + return; + } + + if (!image) + { + image = session.create(imageInfo.imagePath); + image.modify()->setArtist(artist); + } + else + image.modify()->setPath(imageInfo.imagePath); + + image.modify()->setLastWriteTime(imageInfo.lastWriteTime); + image.modify()->setFileSize(imageInfo.fileSize); + image.modify()->setHeight(imageInfo.height); + image.modify()->setWidth(imageInfo.width); + } + + void updateArtistImages(db::Session& session, ArtistImageInfoContainer& imageInfoList) + { + if (imageInfoList.empty()) + return; + + auto transaction{ session.createWriteTransaction() }; + + for (std::size_t i{}; !imageInfoList.empty() && i < writeBatchSize; ++i) + { + updateArtistImage(session, imageInfoList.front()); + imageInfoList.pop_front(); + } + } + + std::vector constructArtistFileNames() + { + std::vector res; + + core::Service::get()->visitStrings("artist-image-file-names", + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "artist" }); + + return res; + } + + } // namespace + + ScanStepScanArtistImages::ScanStepScanArtistImages(InitParams& initParams) + : ScanStepBase{ initParams } + , _artistFileNames{ constructArtistFileNames() } + { + } + + void ScanStepScanArtistImages::process(ScanContext& context) + { + auto& session{ _db.getTLSSession() }; + + { + auto transaction{ session.createReadTransaction() }; + context.currentStepStats.totalElems = db::Artist::getCount(session); + } + + SearchImageContext searchContext{ + .session = session, + .lastRetrievedArtistId = {}, + .artistFileNames = _artistFileNames, + .fullScan = context.scanOptions.fullScan + }; + + ArtistImageInfoContainer imageInfoList; + while (fetchNextArtistImagesToUpdate(searchContext, imageInfoList)) + { + updateArtistImages(session, imageInfoList); + context.currentStepStats.processedElems += readBatchSize; + _progressCallback(context.currentStepStats); + } + } +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScanStepScanArtistImages.hpp b/src/libs/services/scanner/impl/ScanStepScanArtistImages.hpp new file mode 100644 index 00000000..0765dda1 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepScanArtistImages.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "ScanStepBase.hpp" + +namespace lms::scanner +{ + class ScanStepScanArtistImages : public ScanStepBase + { + public: + ScanStepScanArtistImages(InitParams& initParams); + + private: + ScanStep getStep() const override { return ScanStep::ScanArtistImages; } + core::LiteralString getStepName() const override { return "Scan artist images"; } + void process(ScanContext& context) override; + + const std::vector _artistFileNames; + }; +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanAudioFiles.cpp similarity index 95% rename from src/libs/services/scanner/impl/ScanStepScanFiles.cpp rename to src/libs/services/scanner/impl/ScanStepScanAudioFiles.cpp index e296b38b..6896d2db 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanAudioFiles.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "ScanStepScanFiles.hpp" +#include "ScanStepScanAudioFiles.hpp" #include "core/Exception.hpp" #include "core/IConfig.hpp" @@ -301,14 +301,14 @@ namespace lms::scanner } } // namespace - ScanStepScanFiles::MetadataScanQueue::MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort) + ScanStepScanAudioFiles::MetadataScanQueue::MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort) : _metadataParser{ parser } , _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" } , _abort{ abort } { } - void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path) + void ScanStepScanAudioFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path) { { std::scoped_lock lock{ _mutex }; @@ -348,13 +348,13 @@ namespace lms::scanner }); } - std::size_t ScanStepScanFiles::MetadataScanQueue::getResultsCount() const + std::size_t ScanStepScanAudioFiles::MetadataScanQueue::getResultsCount() const { std::scoped_lock lock{ _mutex }; return _scanResults.size(); } - size_t ScanStepScanFiles::MetadataScanQueue::popResults(std::vector& results, std::size_t maxCount) + size_t ScanStepScanAudioFiles::MetadataScanQueue::popResults(std::vector& results, std::size_t maxCount) { results.clear(); results.reserve(maxCount); @@ -372,7 +372,7 @@ namespace lms::scanner return results.size(); } - void ScanStepScanFiles::MetadataScanQueue::wait(std::size_t maxScanRequestCount) + void ScanStepScanAudioFiles::MetadataScanQueue::wait(std::size_t maxScanRequestCount) { LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults"); @@ -380,7 +380,7 @@ namespace lms::scanner _condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; }); } - ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) + ScanStepScanAudioFiles::ScanStepScanAudioFiles(InitParams& initParams) : ScanStepBase{ initParams } , _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib , _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan } @@ -388,7 +388,7 @@ namespace lms::scanner LMS_LOG(DBUPDATER, INFO, "Using " << _metadataScanQueue.getThreadCount() << " thread(s) for scanning file metadata"); } - void ScanStepScanFiles::process(ScanContext& context) + void ScanStepScanAudioFiles::process(ScanContext& context) { const std::size_t scanQueueMaxScanRequestCount{ 100 * _metadataScanQueue.getThreadCount() }; const std::size_t processMetaDataBatchSize{ 5 }; @@ -446,7 +446,7 @@ namespace lms::scanner } } - bool ScanStepScanFiles::checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo) + bool ScanStepScanAudioFiles::checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo) { ScanStats& stats{ context.stats }; @@ -498,7 +498,7 @@ namespace lms::scanner return true; // need to scan } - void ScanStepScanFiles::processMetaDataScanResults(ScanContext& context, std::span scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo) + void ScanStepScanAudioFiles::processMetaDataScanResults(ScanContext& context, std::span scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo) { LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults"); @@ -525,7 +525,7 @@ namespace lms::scanner } } - void ScanStepScanFiles::processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo) + void ScanStepScanAudioFiles::processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo) { ScanStats& stats{ context.stats }; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/ScanStepScanAudioFiles.hpp similarity index 95% rename from src/libs/services/scanner/impl/ScanStepScanFiles.hpp rename to src/libs/services/scanner/impl/ScanStepScanAudioFiles.hpp index 7af546da..238418a3 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepScanAudioFiles.hpp @@ -34,14 +34,14 @@ namespace lms::scanner { - class ScanStepScanFiles : public ScanStepBase + class ScanStepScanAudioFiles : public ScanStepBase { public: - ScanStepScanFiles(InitParams& initParams); + ScanStepScanAudioFiles(InitParams& initParams); private: - ScanStep getStep() const override { return ScanStep::ScanFiles; } - core::LiteralString getStepName() const override { return "Scan files"; } + ScanStep getStep() const override { return ScanStep::ScanAudioFiles; } + core::LiteralString getStepName() const override { return "Scan audio files"; } void process(ScanContext& context) override; bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo); diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 852d28d2..26f44579 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -36,7 +36,8 @@ #include "ScanStepDiscoverFiles.hpp" #include "ScanStepOptimize.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" -#include "ScanStepScanFiles.hpp" +#include "ScanStepScanArtistImages.hpp" +#include "ScanStepScanAudioFiles.hpp" namespace lms::scanner { @@ -335,11 +336,13 @@ namespace lms::scanner _abortScan, _db }; - + + // Order is important _scanSteps.clear(); _scanSteps.push_back(std::make_unique(params)); - _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index b056faf8..fdc77c14 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -57,6 +57,7 @@ namespace lms::scanner DuplicateReason reason; }; + // Alphabetical order enum class ScanStep { CheckForMissingFiles, @@ -67,7 +68,8 @@ namespace lms::scanner FetchTrackFeatures, Optimize, ReloadSimilarityEngine, - ScanFiles, + ScanArtistImages, + ScanAudioFiles, }; static inline constexpr unsigned ScanProgressStepCount{ 9 }; diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index 9ec69508..508e1c93 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -76,6 +76,22 @@ namespace lms::core::stringUtils return std::nullopt; } + 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) { diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index d7a88efa..a08713df 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -21,6 +21,7 @@ #include "core/String.hpp" #include "database/ArtistId.hpp" +#include "database/ImageId.hpp" #include "database/MediaLibraryId.hpp" #include "database/ReleaseId.hpp" #include "database/TrackId.hpp" @@ -33,6 +34,7 @@ namespace lms::api::subsonic }; std::string idToString(db::ArtistId id); + std::string idToString(db::ImageId id); std::string idToString(db::MediaLibraryId id); std::string idToString(db::ReleaseId id); std::string idToString(db::TrackId id); @@ -49,6 +51,9 @@ namespace lms::core::stringUtils template<> std::optional readAs(std::string_view str); + template<> + std::optional readAs(std::string_view str); + template<> std::optional readAs(std::string_view str); diff --git a/src/libs/subsonic/impl/entrypoints/MediaLibraryScanning.cpp b/src/libs/subsonic/impl/entrypoints/MediaLibraryScanning.cpp index 3a899b2b..f2447616 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaLibraryScanning.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaLibraryScanning.cpp @@ -39,7 +39,7 @@ namespace lms::api::subsonic::Scan { std::size_t count{}; - if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanFiles) + if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanAudioFiles) count = scanStatus.currentScanStepStats->processedElems; statusResponse.setAttribute("count", count); diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index ca382b29..734b02cd 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -23,6 +23,7 @@ #include "core/Service.hpp" #include "core/String.hpp" #include "database/Artist.hpp" +#include "database/Image.hpp" #include "database/Release.hpp" #include "database/TrackArtistLink.hpp" #include "database/User.hpp" @@ -92,7 +93,8 @@ namespace lms::api::subsonic artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("name", artist->getName()); - artistNode.setAttribute("coverArt", idToString(artist->getId())); + if (const db::Image::pointer artistImage{ artist->getImage() }) + artistNode.setAttribute("coverArt", idToString(artist->getId())); if (id3) { diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 33b4aa50..c4162ef2 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -310,8 +310,15 @@ namespace lms::ui .arg(stepStats.progress())); break; - case scanner::ScanStep::ScanFiles: - _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files") + case scanner::ScanStep::ScanArtistImages: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-artist-images") + .arg(stepStats.processedElems) + .arg(stepStats.totalElems) + .arg(stepStats.progress())); + break; + + case scanner::ScanStep::ScanAudioFiles: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-audio-files") .arg(stepStats.processedElems) .arg(stepStats.totalElems) .arg(stepStats.progress()));