Added a scan step to scan artist images, ref #435
This commit is contained in:
@@ -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-->
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2016 Emeric Poupon
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ add_executable(test-database
|
||||
Cluster.cpp
|
||||
Common.cpp
|
||||
DatabaseTest.cpp
|
||||
Image.cpp
|
||||
Listen.cpp
|
||||
Migration.cpp
|
||||
Release.cpp
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
@@ -80,6 +81,7 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||
EXPECT_EQ(Listen::getCount(session), 0);
|
||||
EXPECT_EQ(Image::getCount(session), 0);
|
||||
EXPECT_EQ(MediaLibrary::getCount(session), 0);
|
||||
EXPECT_EQ(Release::getCount(session), 0);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
+11
-11
@@ -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 };
|
||||
|
||||
+4
-4
@@ -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 };
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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()));
|
||||
|
||||
Reference in New Issue
Block a user