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()));