Added a scan step to scan artist images, ref #435

This commit is contained in:
emeric
2024-06-29 16:04:16 +02:00
parent 504c0824fb
commit 71dc99ad46
37 changed files with 983 additions and 348 deletions
+2 -1
View File
@@ -116,7 +116,8 @@
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}/{2} entries ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}/{2} entries ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scanning artist images: {1}/{2} artists ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scanning audio files: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message> <message id="Lms.Admin.ScannerController.step-status">Step status</message>
<!--Tracing--> <!--Tracing-->
+2 -1
View File
@@ -116,7 +116,8 @@
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}/{2} entrées ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}/{2} entrées ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scan des images des artistes: {1}/{2} artists ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scan des fichiers audio : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message> <message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
<!--Tracing--> <!--Tracing-->
+2 -1
View File
@@ -116,7 +116,8 @@
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}/{2} voci ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}/{2} voci ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scansione delle immagini degli artisti: {1}/{2} artisti ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scansione dei file audio: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message> <message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
<!--Tracing--> <!--Tracing-->
+2 -1
View File
@@ -133,7 +133,8 @@
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}/{2} wpisów ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}/{2} wpisów ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} plików ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-artist-images">Skanowanie obrazów artystów: {1}/{2} artystów ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Skanowanie plików: {1}/{2} plików ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Obecny krok</message> <message id="Lms.Admin.ScannerController.step-status">Obecny krok</message>
<!--Tracing--> <!--Tracing-->
+2 -1
View File
@@ -116,7 +116,8 @@
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message> <message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
<!--Tracing--> <!--Tracing-->
+1
View File
@@ -3,6 +3,7 @@ add_library(lmsdatabase SHARED
impl/AuthToken.cpp impl/AuthToken.cpp
impl/Cluster.cpp impl/Cluster.cpp
impl/Db.cpp impl/Db.cpp
impl/Image.cpp
impl/Listen.cpp impl/Listen.cpp
impl/MediaLibrary.cpp impl/MediaLibrary.cpp
impl/Migration.cpp impl/Migration.cpp
+6
View File
@@ -22,6 +22,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
@@ -273,6 +274,11 @@ namespace lms::db
utils::forEachQueryRangeResult(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
ObjectPtr<Image> Artist::getImage() const
{
return ObjectPtr<Image>{ _image.lock() };
}
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{ {
assert(session()); assert(session());
+56
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "database/Image.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#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<Image>{ new Image{ p } });
}
std::size_t Image::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM image"));
}
Image::pointer Image::find(Session& session, ImageId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Image>().where("id = ?").bind(id));
}
} // namespace lms::db
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013-2016 Emeric Poupon * Copyright (C) 2024 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
+246 -223
View File
@@ -35,7 +35,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 59 }; static constexpr Version LMS_DATABASE_VERSION{ 60 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -86,11 +86,13 @@ namespace lms::db::Migration
Db& _db; Db& _db;
}; };
static void migrateFromV33(Session& session) namespace
{ {
// remove name from track_artist_link void migrateFromV33(Session& session)
// Drop Auth mode {
session.getDboSession()->execute(R"( // remove name from track_artist_link
// Drop Auth mode
session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "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 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("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("DROP TABLE track_artist_link");
session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
} }
static void migrateFromV34(Session& session) void migrateFromV34(Session& session)
{ {
// Add scrobbling state // Add scrobbling state
// By default, everything needs to be sent // 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<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
} }
static void migrateFromV35(Session& session) void migrateFromV35(Session& session)
{ {
// Add creattion/last modif date time for tracklists // 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 creation_date_time TEXT");
session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
} }
static void migrateFromV36(Session& session) void migrateFromV36(Session& session)
{ {
// Increased precision for track durations (now in milliseconds instead of secodns) // 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV37(Session& session) void migrateFromV37(Session& session)
{ {
// Support Performer tags (via subtypes) // Support Performer tags (via subtypes)
session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT"); 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV38(Session& session) void migrateFromV38(Session& session)
{ {
// migrate release-specific tags from Track to Release // migrate release-specific tags from Track to Release
session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER"); 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" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "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 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("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("DROP TABLE track");
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV39(Session& session) void migrateFromV39(Session& session)
{ {
// add release type // add release type
session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER");
session.getDboSession()->execute("ALTER TABLE release ADD secondary_types 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV40(Session& session) void migrateFromV40(Session& session)
{ {
// add artist_display_name in Release and Track // 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 release ADD artist_display_name TEXT NOT NULL DEFAULT ''");
session.getDboSession()->execute("ALTER TABLE track 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV41(Session& session) void migrateFromV41(Session& session)
{ {
// add artist_display_name in Release and Track // 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_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 RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate");
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
} }
static void migrateFromV42(Session& session) void migrateFromV42(Session& session)
{ {
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); 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_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_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_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_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_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_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_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_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) // 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 RENAME COLUMN scrobbler TO scrobbling_backend");
session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER"); 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 scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); 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 scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); 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 scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); 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 scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); 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) void migrateFromV43(Session& session)
{ {
// add counts in genre table // add counts in genre table
session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER"); session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER");
session.getDboSession()->execute("ALTER TABLE cluster ADD release_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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV44(Session& session) void migrateFromV44(Session& session)
{ {
// add bitrate // add bitrate
session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV45(Session& session) void migrateFromV45(Session& session)
{ {
// add subsonic_enable_transcoding_by_default, default is disabled // 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<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")");
} }
void migrateFromV46(Session& session) void migrateFromV46(Session& session)
{ {
// add extra tags to parse // add extra tags to parse
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text not null "name" text not null
);)"); );)");
session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM 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("DROP TABLE cluster_type");
session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV47(Session& session) void migrateFromV47(Session& session)
{ {
// release type, new way // release type, new way
session.getDboSession()->execute("ALTER TABLE release DROP primary_type"); session.getDboSession()->execute("ALTER TABLE release DROP primary_type");
session.getDboSession()->execute("ALTER TABLE release DROP secondary_types"); 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, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text 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_type_id" bigint,
"release_id" bigint, "release_id" bigint,
primary key ("release_type_id", "release_id"), 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_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 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_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" on "release_release_type" ("release_id"))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV48(Session& session) void migrateFromV48(Session& session)
{ {
// Regression for the extra tags not being parsed // Regression for the extra tags not being parsed
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV49(Session& session) 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) // 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 year INTEGER");
session.getDboSession()->execute("ALTER TABLE track ADD original_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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV50(Session& session) void migrateFromV50(Session& session)
{ {
// MediaLibrary support // MediaLibrary support
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"path" text not null, "path" text not null,
"name" text not null "name" text not null
))"); ))");
const int scanSettingsId{ session.getDboSession()->query<int>("SELECT id FROM scan_settings") }; const int scanSettingsId{ session.getDboSession()->query<int>("SELECT id FROM scan_settings") };
// Convert the existing media_directory in the scan_settings table to a media_library with id '1' // 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") session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name")
SELECT 1, 0, s_s.media_directory, "Main" SELECT 1, 0, s_s.media_directory, "Main"
FROM scan_settings s_s FROM scan_settings s_s
WHERE id = ?)") WHERE id = ?)")
.bind(scanSettingsId); .bind(scanSettingsId);
// Remove the outdated column in scan_settings // Remove the outdated column in scan_settings
session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory"); session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory");
// Add the media_library column in tracks, with id '1' // Add the media_library column in tracks, with id '1'
session.getDboSession()->execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "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 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 // Migrate data, with the new media_library_id field set to 1
session.getDboSession()->execute(R"(INSERT INTO track_backup session.getDboSession()->execute(R"(INSERT INTO track_backup
SELECT SELECT
id, id,
version, version,
@@ -405,76 +407,96 @@ SELECT
release_id, release_id,
1 1
FROM track)"); FROM track)");
session.getDboSession()->execute("DROP TABLE track"); session.getDboSession()->execute("DROP TABLE track");
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
} }
void migrateFromV51(Session& session) void migrateFromV51(Session& session)
{ {
// Add custom artist tag delimiters, no need to rescan since it has no effect when empty // 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 artist_tag_delimiters TEXT NOT NULL DEFAULT ''");
session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_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) void migrateFromV52(Session& session)
{ {
// Add sort name for releases // Add sort name for releases
session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV53(Session& session) void migrateFromV53(Session& session)
{ {
// Add release group mbid // Add release group mbid
session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV54(Session& session) void migrateFromV54(Session& session)
{ {
// Add file size + relative file path // 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 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 file_size BIGINT NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''"); 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV55(Session& session) void migrateFromV55(Session& session)
{ {
// Add bitsPerSample, channelCount and sampleRate // 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 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 channel_count INTEGER NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD sample_rate 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 // 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"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV56(Session& session) void migrateFromV56(Session& session)
{ {
// Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) };
for (const auto& indexName : indexeNames) for (const auto& indexName : indexeNames)
session.getDboSession()->execute("DROP INDEX " + indexName); session.getDboSession()->execute("DROP INDEX " + indexName);
} }
void migrateFromV57(Session& session) void migrateFromV57(Session& session)
{ {
// useless index, may have been already removed in the previous step // useless index, may have been already removed in the previous step
session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx");
} }
void migrateFromV58(Session& session) void migrateFromV58(Session& session)
{ {
// DSF support // DSF support
session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'"); 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) bool doDbMigration(Session& session)
{ {
@@ -511,6 +533,7 @@ SELECT
{ 56, migrateFromV56 }, { 56, migrateFromV56 },
{ 57, migrateFromV57 }, { 57, migrateFromV57 },
{ 58, migrateFromV58 }, { 58, migrateFromV58 },
{ 59, migrateFromV59 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+4
View File
@@ -26,6 +26,7 @@
#include "database/AuthToken.hpp" #include "database/AuthToken.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Listen.hpp" #include "database/Listen.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
@@ -92,6 +93,7 @@ namespace lms::db
_session.mapClass<AuthToken>("auth_token"); _session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster"); _session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type"); _session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Image>("image");
_session.mapClass<Listen>("listen"); _session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library"); _session.mapClass<MediaLibrary>("media_library");
_session.mapClass<Release>("release"); _session.mapClass<Release>("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_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 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_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_id_idx ON listen(id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
@@ -44,6 +44,7 @@ namespace lms::db
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Image;
class Release; class Release;
class Session; class Session;
class StarredArtist; class StarredArtist;
@@ -139,6 +140,7 @@ namespace lms::db
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); } std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
ObjectPtr<Image> getImage() const;
// No artistLinkTypes means get them all // No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const; RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
@@ -159,6 +161,7 @@ namespace lms::db
Wt::Dbo::field(a, _sortName, "sort_name"); Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid"); 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, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); 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 _sortName;
std::string _MBID; // Musicbrainz Identifier std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::weak_ptr<Image> _image;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
}; };
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include "database/ArtistId.hpp"
#include "database/ImageId.hpp"
#include "database/Object.hpp"
namespace lms::db
{
class Artist;
class Session;
class Image final : public Object<Image, ImageId>
{
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) { _artist = getDboPtr(artist); }
template<class Action>
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> _artist;
};
} // namespace lms::db
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(ImageId)
@@ -95,7 +95,7 @@ namespace lms::db
// Can get raw dbo ptr only from Objects // Can get raw dbo ptr only from Objects
template<typename SomeObject> template<typename SomeObject>
static Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) static Wt::Dbo::ptr<SomeObject> getDboPtr(const ObjectPtr<SomeObject>& ptr)
{ {
return ptr._obj; return ptr._obj;
} }
+1
View File
@@ -4,6 +4,7 @@ add_executable(test-database
Cluster.cpp Cluster.cpp
Common.cpp Common.cpp
DatabaseTest.cpp DatabaseTest.cpp
Image.cpp
Listen.cpp Listen.cpp
Migration.cpp Migration.cpp
Release.cpp Release.cpp
+2
View File
@@ -22,6 +22,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Listen.hpp" #include "database/Listen.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
@@ -80,6 +81,7 @@ namespace lms::db::tests
EXPECT_EQ(Cluster::getCount(session), 0); EXPECT_EQ(Cluster::getCount(session), 0);
EXPECT_EQ(ClusterType::getCount(session), 0); EXPECT_EQ(ClusterType::getCount(session), 0);
EXPECT_EQ(Listen::getCount(session), 0); EXPECT_EQ(Listen::getCount(session), 0);
EXPECT_EQ(Image::getCount(session), 0);
EXPECT_EQ(MediaLibrary::getCount(session), 0); EXPECT_EQ(MediaLibrary::getCount(session), 0);
EXPECT_EQ(Release::getCount(session), 0); EXPECT_EQ(Release::getCount(session), 0);
EXPECT_EQ(StarredArtist::getCount(session), 0); EXPECT_EQ(StarredArtist::getCount(session), 0);
+66
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/Image.hpp"
namespace lms::db::tests
{
using ScopedImage = ScopedEntity<db::Image>;
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
@@ -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) void RawImage::resize(ImageSize width)
{ {
try try
@@ -35,6 +35,9 @@ namespace lms::image::GraphicsMagick
RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
RawImage(const std::filesystem::path& path); RawImage(const std::filesystem::path& path);
ImageSize getWidth() const override;
ImageSize getHeight() const override;
void resize(ImageSize width) override; void resize(ImageSize width) override;
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override; std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
+3 -2
View File
@@ -33,11 +33,12 @@ namespace lms::image::STB
RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
RawImage(const std::filesystem::path& path); RawImage(const std::filesystem::path& path);
ImageSize getWidth() const override;
ImageSize getHeight() const override;
void resize(ImageSize width) override; void resize(ImageSize width) override;
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override; std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
ImageSize getWidth() const;
ImageSize getHeight() const;
const std::byte* getData() const; const std::byte* getData() const;
private: private:
@@ -27,6 +27,10 @@ namespace lms::image
{ {
public: public:
virtual ~IRawImage() = default; virtual ~IRawImage() = default;
virtual ImageSize getWidth() const = 0;
virtual ImageSize getHeight() const = 0;
virtual void resize(ImageSize width) = 0; virtual void resize(ImageSize width) = 0;
virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0; virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0;
}; };
+1 -1
View File
@@ -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 // 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() if (!_artistTagDelimiters.empty()
&& track.artists.size() > 1 && track.artists.size() > 1
&& getTagValuesAs<std::string>(tagReader, { TagType::Artist }, _artistTagDelimiters).size() > 1) && getTagValuesAs<std::string>(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1)
{ {
std::vector<std::string_view> artistNames; std::vector<std::string_view> artistNames;
std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; }); std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
+4 -89
View File
@@ -30,6 +30,7 @@
#include "core/Utils.hpp" #include "core/Utils.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
@@ -86,19 +87,6 @@ namespace lms::cover
return res; return res;
} }
std::vector<std::string> constructArtistFileNames()
{
std::vector<std::string> res;
core::Service<core::IConfig>::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<std::filesystem::path>& extensions) bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{ {
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(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<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames{ constructPreferredFileNames() } , _preferredFileNames{ constructPreferredFileNames() }
, _artistFileNames{ constructArtistFileNames() }
{ {
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
@@ -389,87 +376,15 @@ namespace lms::cover
if (artistImage) if (artistImage)
return artistImage; return artistImage;
std::string artistName;
std::string artistMBID;
std::set<std::filesystem::path> releasePaths;
std::set<std::filesystem::path> multiArtistReleasePaths;
{ {
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const Artist::pointer artist{ Artist::find(session, artistId) }; if (const Artist::pointer artist{ db::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<std::string> artistFileNames;
if (!artistMBID.empty())
artistFileNames.push_back(artistMBID);
artistFileNames.push_back(artistName);
std::vector<std::string> 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)
{ {
artistImage = getFromDirectory(releasePath, width, artistFileNamesWithGenericNames, false); if (const db::Image::pointer image{ artist->getImage() })
if (artistImage) artistImage = getFromCoverFile(image->getPath(), width);
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;
} }
} }
@@ -76,7 +76,6 @@ namespace lms::cover
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
const std::size_t _maxFileSize; const std::size_t _maxFileSize;
const std::vector<std::string> _preferredFileNames; const std::vector<std::string> _preferredFileNames;
const std::vector<std::string> _artistFileNames;
unsigned _jpegQuality; unsigned _jpegQuality;
}; };
+4 -2
View File
@@ -8,7 +8,8 @@ add_library(lmsscanner SHARED
impl/ScanStepDiscoverFiles.cpp impl/ScanStepDiscoverFiles.cpp
impl/ScanStepOptimize.cpp impl/ScanStepOptimize.cpp
impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepRemoveOrphanDbFiles.cpp
impl/ScanStepScanFiles.cpp impl/ScanStepScanArtistImages.cpp
impl/ScanStepScanAudioFiles.cpp
) )
target_include_directories(lmsscanner INTERFACE target_include_directories(lmsscanner INTERFACE
@@ -20,10 +21,11 @@ target_include_directories(lmsscanner PRIVATE
) )
target_link_libraries(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE
lmscore
lmsdatabase lmsdatabase
lmsimage
lmsmetadata lmsmetadata
lmsrecommendation lmsrecommendation
lmscore
) )
target_link_libraries(lmsscanner PUBLIC target_link_libraries(lmsscanner PUBLIC
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ScanStepScanArtistImages.hpp"
#include <array>
#include <cassert>
#include <deque>
#include <set>
#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<image::IRawImage> 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<ArtistImageInfo>;
bool isFileSupported(const std::filesystem::path& file)
{
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
return (std::find(std::cbegin(fileExtensions), std::cend(fileExtensions), file.extension()) != std::cend(fileExtensions));
}
std::multimap<std::string, std::filesystem::path> getImagePaths(const std::filesystem::path& directoryPath, const std::vector<std::string>& fileNames)
{
std::multimap<std::string, std::filesystem::path> 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<std::string>& fileNames, ImageInfo& imageInfo)
{
assert(!imageInfo);
const std::multimap<std::string, std::filesystem::path> 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<std::string>& 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<std::filesystem::path> releasePaths;
std::set<std::filesystem::path> 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<std::string> artistFileNames;
if (!artistMBID.empty())
artistFileNames.push_back(artistMBID);
artistFileNames.push_back(artist->getName());
std::vector<std::string> 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<std::string>& 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<db::Image>(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<std::string> constructArtistFileNames()
{
std::vector<std::string> res;
core::Service<core::IConfig>::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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
#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<std::string> _artistFileNames;
};
} // namespace lms::scanner
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "ScanStepScanFiles.hpp" #include "ScanStepScanAudioFiles.hpp"
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
@@ -301,14 +301,14 @@ namespace lms::scanner
} }
} // namespace } // 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 } : _metadataParser{ parser }
, _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" } , _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" }
, _abort{ abort } , _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 }; 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 }; std::scoped_lock lock{ _mutex };
return _scanResults.size(); return _scanResults.size();
} }
size_t ScanStepScanFiles::MetadataScanQueue::popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount) size_t ScanStepScanAudioFiles::MetadataScanQueue::popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount)
{ {
results.clear(); results.clear();
results.reserve(maxCount); results.reserve(maxCount);
@@ -372,7 +372,7 @@ namespace lms::scanner
return results.size(); 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"); LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
@@ -380,7 +380,7 @@ namespace lms::scanner
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; }); _condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
} }
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) ScanStepScanAudioFiles::ScanStepScanAudioFiles(InitParams& initParams)
: ScanStepBase{ initParams } : ScanStepBase{ initParams }
, _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib , _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib
, _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan } , _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan }
@@ -388,7 +388,7 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, INFO, "Using " << _metadataScanQueue.getThreadCount() << " thread(s) for scanning file metadata"); 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 scanQueueMaxScanRequestCount{ 100 * _metadataScanQueue.getThreadCount() };
const std::size_t processMetaDataBatchSize{ 5 }; 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 }; ScanStats& stats{ context.stats };
@@ -498,7 +498,7 @@ namespace lms::scanner
return true; // need to scan return true; // need to scan
} }
void ScanStepScanFiles::processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo) void ScanStepScanAudioFiles::processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{ {
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults"); 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 }; ScanStats& stats{ context.stats };
@@ -34,14 +34,14 @@
namespace lms::scanner namespace lms::scanner
{ {
class ScanStepScanFiles : public ScanStepBase class ScanStepScanAudioFiles : public ScanStepBase
{ {
public: public:
ScanStepScanFiles(InitParams& initParams); ScanStepScanAudioFiles(InitParams& initParams);
private: private:
ScanStep getStep() const override { return ScanStep::ScanFiles; } ScanStep getStep() const override { return ScanStep::ScanAudioFiles; }
core::LiteralString getStepName() const override { return "Scan files"; } core::LiteralString getStepName() const override { return "Scan audio files"; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo); bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo);
@@ -36,7 +36,8 @@
#include "ScanStepDiscoverFiles.hpp" #include "ScanStepDiscoverFiles.hpp"
#include "ScanStepOptimize.hpp" #include "ScanStepOptimize.hpp"
#include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp"
#include "ScanStepScanFiles.hpp" #include "ScanStepScanArtistImages.hpp"
#include "ScanStepScanAudioFiles.hpp"
namespace lms::scanner namespace lms::scanner
{ {
@@ -336,10 +337,12 @@ namespace lms::scanner
_db _db
}; };
// Order is important
_scanSteps.clear(); _scanSteps.clear();
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepScanAudioFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepScanArtistImages>(params));
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params)); _scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params)); _scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
_scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params)); _scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
@@ -57,6 +57,7 @@ namespace lms::scanner
DuplicateReason reason; DuplicateReason reason;
}; };
// Alphabetical order
enum class ScanStep enum class ScanStep
{ {
CheckForMissingFiles, CheckForMissingFiles,
@@ -67,7 +68,8 @@ namespace lms::scanner
FetchTrackFeatures, FetchTrackFeatures,
Optimize, Optimize,
ReloadSimilarityEngine, ReloadSimilarityEngine,
ScanFiles, ScanArtistImages,
ScanAudioFiles,
}; };
static inline constexpr unsigned ScanProgressStepCount{ 9 }; static inline constexpr unsigned ScanProgressStepCount{ 9 };
+16
View File
@@ -76,6 +76,22 @@ namespace lms::core::stringUtils
return std::nullopt; return std::nullopt;
} }
template<>
std::optional<db::ImageId> readAs(std::string_view str)
{
std::vector<std::string_view> 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<db::ImageId::ValueType>(values[1]) })
return db::ImageId{ *value };
return std::nullopt;
}
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str) std::optional<db::MediaLibraryId> readAs(std::string_view str)
{ {
+5
View File
@@ -21,6 +21,7 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/ImageId.hpp"
#include "database/MediaLibraryId.hpp" #include "database/MediaLibraryId.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
#include "database/TrackId.hpp" #include "database/TrackId.hpp"
@@ -33,6 +34,7 @@ namespace lms::api::subsonic
}; };
std::string idToString(db::ArtistId id); std::string idToString(db::ArtistId id);
std::string idToString(db::ImageId id);
std::string idToString(db::MediaLibraryId id); std::string idToString(db::MediaLibraryId id);
std::string idToString(db::ReleaseId id); std::string idToString(db::ReleaseId id);
std::string idToString(db::TrackId id); std::string idToString(db::TrackId id);
@@ -49,6 +51,9 @@ namespace lms::core::stringUtils
template<> template<>
std::optional<db::ArtistId> readAs(std::string_view str); std::optional<db::ArtistId> readAs(std::string_view str);
template<>
std::optional<db::ImageId> readAs(std::string_view str);
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str); std::optional<db::MediaLibraryId> readAs(std::string_view str);
@@ -39,7 +39,7 @@ namespace lms::api::subsonic::Scan
{ {
std::size_t count{}; std::size_t count{};
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanFiles) if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanAudioFiles)
count = scanStatus.currentScanStepStats->processedElems; count = scanStatus.currentScanStepStats->processedElems;
statusResponse.setAttribute("count", count); statusResponse.setAttribute("count", count);
+3 -1
View File
@@ -23,6 +23,7 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/TrackArtistLink.hpp" #include "database/TrackArtistLink.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -92,7 +93,8 @@ namespace lms::api::subsonic
artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName()); 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) if (id3)
{ {
+9 -2
View File
@@ -310,8 +310,15 @@ namespace lms::ui
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::ScanFiles: case scanner::ScanStep::ScanArtistImages:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files") _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.processedElems)
.arg(stepStats.totalElems) .arg(stepStats.totalElems)
.arg(stepStats.progress())); .arg(stepStats.progress()));