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-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-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>
<!--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-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-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>
<!--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-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-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>
<!--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-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-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>
<!--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-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>
<!--Tracing-->
+1
View File
@@ -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
+6
View File
@@ -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<Image> Artist::getImage() const
{
return ObjectPtr<Image>{ _image.lock() };
}
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{
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.
*
+246 -223
View File
@@ -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<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)) + ")");
}
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<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)) + ")");
}
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<int>(/*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<int>(/*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<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'
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<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)
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<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)
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{};
+4
View File
@@ -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<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Image>("image");
_session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library");
_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_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)");
@@ -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<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
ObjectPtr<Image> getImage() const;
// No artistLinkTypes means get them all
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, _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> _image;
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
};
@@ -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
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;
}
+1
View File
@@ -4,6 +4,7 @@ add_executable(test-database
Cluster.cpp
Common.cpp
DatabaseTest.cpp
Image.cpp
Listen.cpp
Migration.cpp
Release.cpp
+2
View File
@@ -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);
+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)
{
try
@@ -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<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::filesystem::path& path);
ImageSize getWidth() const override;
ImageSize getHeight() const override;
void resize(ImageSize width) override;
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
ImageSize getWidth() const;
ImageSize getHeight() const;
const std::byte* getData() const;
private:
@@ -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<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
if (!_artistTagDelimiters.empty()
&& 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::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 "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<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)
{
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 }
, _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames{ constructPreferredFileNames() }
, _artistFileNames{ constructArtistFileNames() }
{
setJpegQuality(core::Service<core::IConfig>::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<std::filesystem::path> releasePaths;
std::set<std::filesystem::path> 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<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)
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);
}
}
@@ -76,7 +76,6 @@ namespace lms::cover
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
const std::size_t _maxFileSize;
const std::vector<std::string> _preferredFileNames;
const std::vector<std::string> _artistFileNames;
unsigned _jpegQuality;
};
+4 -2
View File
@@ -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
@@ -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/>.
*/
#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<MetaDataScanResult>& results, std::size_t maxCount)
size_t ScanStepScanAudioFiles::MetadataScanQueue::popResults(std::vector<MetaDataScanResult>& 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<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");
@@ -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 };
@@ -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);
@@ -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
{
@@ -336,10 +337,12 @@ namespace lms::scanner
_db
};
// Order is important
_scanSteps.clear();
_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<ScanStepScanArtistImages>(params));
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
_scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
@@ -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 };
+16
View File
@@ -76,6 +76,22 @@ namespace lms::core::stringUtils
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<>
std::optional<db::MediaLibraryId> readAs(std::string_view str)
{
+5
View File
@@ -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<db::ArtistId> readAs(std::string_view str);
template<>
std::optional<db::ImageId> readAs(std::string_view str);
template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str);
@@ -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);
+3 -1
View File
@@ -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)
{
+9 -2
View File
@@ -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()));