Put all directories and images in database, use the info to associate an image to each artist

This commit is contained in:
emeric
2024-07-06 13:53:41 +02:00
parent 45c8b82865
commit d1324c1c3a
59 changed files with 2023 additions and 960 deletions
+6 -5
View File
@@ -87,7 +87,8 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message> <message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Cannot parse file</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message>
<message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message> <message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message>
@@ -108,16 +109,16 @@
<message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled on {1}</message> <message id="Lms.Admin.ScannerController.status-scheduled">Scheduled on {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking files... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message> <message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}/{2} entries ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scanning artist images: {1}/{2} artists ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scanning audio files: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message> <message id="Lms.Admin.ScannerController.step-status">Step status</message>
<!--Tracing--> <!--Tracing-->
+6 -5
View File
@@ -87,7 +87,8 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message> <message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Impossible d'analyser le fichier</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message>
<message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message> <message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message>
@@ -108,16 +109,16 @@
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié le {1}</message> <message id="Lms.Admin.ScannerController.status-scheduled">Planifié le {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message> <message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers : {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers : {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}/{2} entrées ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scan des images des artistes: {1}/{2} artists ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scan des fichiers audio : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message> <message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
<!--Tracing--> <!--Tracing-->
+6 -5
View File
@@ -87,7 +87,8 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message> <message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Non in grado di analizzare il file</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</message>
<message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message> <message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message>
@@ -108,16 +109,16 @@
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Pianificato il {1}</message> <message id="Lms.Admin.ScannerController.status-scheduled">Pianificato il {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Controllo file... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message> <message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}/{2} voci ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-artist-images">Scansione delle immagini degli artisti: {1}/{2} artisti ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scansione dei file: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Scansione dei file audio: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message> <message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
<!--Tracing--> <!--Tracing-->
+6 -5
View File
@@ -88,7 +88,8 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message> <message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Nie udało się przeparsować pliku</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku</message>
<message id="Lms.Admin.ScannerController.compact">Sprasuj bazę danych. <strong>Uwaga:</strong> może to trochę zająć, a cała aplikacja będzie w tym czasie zablokowana!</message> <message id="Lms.Admin.ScannerController.compact">Sprasuj bazę danych. <strong>Uwaga:</strong> może to trochę zająć, a cała aplikacja będzie w tym czasie zablokowana!</message>
<message id="Lms.Admin.ScannerController.duplicates-header"> <message id="Lms.Admin.ScannerController.duplicates-header">
@@ -117,12 +118,13 @@
<message id="Lms.Admin.ScannerController.status-not-scheduled">Nie zaplanowano</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Nie zaplanowano</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano na {1}</message> <message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano na {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files"> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural> <plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural> <plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
<plural case="2">Sprawdzanie duplikatów... {1} plików</plural> <plural case="2">Sprawdzanie duplikatów... {1} plików</plural>
</message> </message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Sprawdzanie plików... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message> <message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files"> <message id="Lms.Admin.ScannerController.step-discovering-files">
@@ -131,10 +133,9 @@
<plural case="2">Odkrywanie plików: {1} plików</plural> <plural case="2">Odkrywanie plików: {1} plików</plural>
</message> </message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}/{2} wpisów ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-artist-images">Skanowanie obrazów artystów: {1}/{2} artystów ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">Skanowanie plików: {1}/{2} plików ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Obecny krok</message> <message id="Lms.Admin.ScannerController.step-status">Obecny krok</message>
<!--Tracing--> <!--Tracing-->
+5 -4
View File
@@ -87,7 +87,8 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message> <message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">无法解析文件</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message> <message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message>
@@ -109,15 +110,15 @@
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {1}</message> <message id="Lms.Admin.ScannerController.status-scheduled">计划于 {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message> <message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-audio-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message> <message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
<!--Tracing--> <!--Tracing-->
+1 -1
View File
@@ -87,7 +87,7 @@ cover-jpeg-quality = 75;
cover-preferred-file-names = ("cover", "front"); cover-preferred-file-names = ("cover", "front");
# File names for artist images (order is important) # File names for artist images (order is important)
# Files whose name is the artist's MBID, then the artist's name, are searched before the names in this list # Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries.
artist-image-file-names = ("artist"); artist-image-file-names = ("artist");
# Playqueue max entry count # Playqueue max entry count
+1
View File
@@ -3,6 +3,7 @@ add_library(lmsdatabase SHARED
impl/AuthToken.cpp impl/AuthToken.cpp
impl/Cluster.cpp impl/Cluster.cpp
impl/Db.cpp impl/Db.cpp
impl/Directory.cpp
impl/Image.cpp impl/Image.cpp
impl/Listen.cpp impl/Listen.cpp
impl/MediaLibrary.cpp impl/MediaLibrary.cpp
+9 -3
View File
@@ -22,6 +22,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp" #include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -222,19 +223,19 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQueryResults<Artist::pointer>(session.getDboSession()->find<Artist>().where("name = ?").bind(std::string{ name, 0, _maxNameLength }).orderBy("LENGTH(mbid) DESC")); // put mbid entries first return utils::fetchQueryResults<Artist::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.name = ?").bind(std::string{ name, 0, _maxNameLength }).orderBy("LENGTH(a.mbid) DESC")); // put mbid entries first
} }
Artist::pointer Artist::find(Session& session, const core::UUID& mbid) Artist::pointer Artist::find(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() })); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.mbid = ?").bind(std::string{ mbid.getAsString() }));
} }
Artist::pointer Artist::find(Session& session, ArtistId id) Artist::pointer Artist::find(Session& session, ArtistId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Artist>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.id = ?").bind(id));
} }
bool Artist::exists(Session& session, ArtistId id) bool Artist::exists(Session& session, ArtistId id)
@@ -364,4 +365,9 @@ namespace lms::db
_sortName = std::string(sortName, 0, _maxNameLength); _sortName = std::string(sortName, 0, _maxNameLength);
} }
void Artist::setImage(ObjectPtr<Image> image)
{
_image = getDboPtr(image);
}
} // namespace lms::db } // namespace lms::db
+1
View File
@@ -20,6 +20,7 @@
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/ScanSettings.hpp" #include "database/ScanSettings.hpp"
+162
View File
@@ -0,0 +1,162 @@
/*
* 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/Directory.hpp"
#include "database/Session.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
namespace lms::db
{
namespace
{
Wt::Dbo::Query<Wt::Dbo::ptr<Directory>> createQuery(Session& session, const Directory::FindParameters& params)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };
if (params.artist.isValid())
{
query.join("track t ON t.directory_id = d.id")
.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?")
.bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
{
std::ostringstream oss;
bool first{ true };
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
query.where(oss.str());
}
query.groupBy("d.id");
}
return query;
}
} // namespace
Directory::Directory(const std::filesystem::path& p)
{
setAbsolutePath(p);
}
Directory::pointer Directory::create(Session& session, const std::filesystem::path& p)
{
return session.getDboSession()->add(std::unique_ptr<Directory>{ new Directory{ p } });
}
std::size_t Directory::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM directory"));
}
Directory::pointer Directory::find(Session& session, DirectoryId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.id = ?").bind(id));
}
Directory::pointer Directory::find(Session& session, const std::filesystem::path& path)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.absolute_path = ?").bind(path));
}
void Directory::find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").orderBy("d.id").where("d.id > ?").bind(lastRetrievedDirectory).limit(static_cast<int>(count)) };
utils::forEachQueryResult(query, [&](const Directory::pointer& image) {
func(image);
lastRetrievedDirectory = image->getId();
});
}
void Directory::find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func)
{
auto query{ createQuery(session, params) };
utils::forEachQueryResult(query, [&func](const Directory::pointer& dir) {
func(dir);
});
}
RangeResults<DirectoryId> Directory::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<DirectoryId>("SELECT d.id FROM directory d") };
query.leftJoin("directory d_child ON d.id = d_child.parent_directory_id");
query.leftJoin("track t ON d.id = t.directory_id");
query.leftJoin("image i ON d.id = i.directory_id");
query.where("d_child.id IS NULL");
query.where("t.directory_id IS NULL");
query.where("i.directory_id IS NULL");
return utils::execRangeQuery<DirectoryId>(query, range);
}
void Directory::setAbsolutePath(const std::filesystem::path& p)
{
assert(p.is_absolute());
if (!p.has_filename() && p.has_parent_path())
{
_absolutePath = p.parent_path();
_name = _absolutePath.filename();
}
else
{
_absolutePath = p;
_name = p.filename();
}
}
void Directory::setParent(ObjectPtr<Directory> parent)
{
#ifndef NDEBUG
if (parent)
{
assert(_absolutePath.has_parent_path());
assert(parent->getAbsolutePath() == _absolutePath.parent_path());
}
#endif
_parent = getDboPtr(parent);
}
} // namespace lms::db
+62 -3
View File
@@ -22,6 +22,7 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Directory.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
@@ -30,9 +31,24 @@
namespace lms::db namespace lms::db
{ {
Image::Image(const std::filesystem::path& p) namespace
: _path{ p }
{ {
Wt::Dbo::Query<Wt::Dbo::ptr<Image>> createQuery(Session& session, const Image::FindParameters& params)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i FROM image i") };
if (params.directory.isValid())
query.where("i.directory_id = ?").bind(params.directory);
if (!params.fileStem.empty())
query.where("i.stem = ?").bind(params.fileStem);
return query;
}
} // namespace
Image::Image(const std::filesystem::path& p)
{
setAbsoluteFilePath(p);
} }
Image::pointer Image::create(Session& session, const std::filesystem::path& p) Image::pointer Image::create(Session& session, const std::filesystem::path& p)
@@ -51,6 +67,49 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Image>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.id = ?").bind(id));
} }
Image::pointer Image::find(Session& session, const std::filesystem::path& path)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.absolute_file_path = ?").bind(path));
}
void Image::find(Session& session, ImageId& lastRetrievedImage, std::size_t count, const std::function<void(const Image::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").orderBy("i.id").where("i.id > ?").bind(lastRetrievedImage).limit(static_cast<int>(count)) };
utils::forEachQueryResult(query, [&](const Image::pointer& image) {
func(image);
lastRetrievedImage = image->getId();
});
}
RangeResults<Image::pointer> Image::find(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery(session, params) };
return utils::execRangeQuery<Image::pointer>(query, params.range);
}
void Image::find(Session& session, const FindParameters& params, const std::function<void(const Image::pointer&)>& func)
{
auto query{ createQuery(session, params) };
utils::forEachQueryResult(query, [&](const Image::pointer& image) {
func(image);
});
}
void Image::setAbsoluteFilePath(const std::filesystem::path& p)
{
assert(p.is_absolute());
_fileAbsolutePath = p;
_fileStem = p.stem().string();
}
} // namespace lms::db } // namespace lms::db
+1 -1
View File
@@ -210,7 +210,7 @@ namespace lms::db
Listen::pointer Listen::find(Session& session, ListenId id) Listen::pointer Listen::find(Session& session, ListenId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l").where("l.id = ?").bind(id));
} }
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters) RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
+135 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 60 }; static constexpr Version LMS_DATABASE_VERSION{ 61 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -485,6 +485,7 @@ SELECT
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"path" text not null, "path" text not null,
"stem" text not null,
"file_last_write" text, "file_last_write" text,
"file_size" integer not null, "file_size" integer not null,
"width" integer not null, "width" integer not null,
@@ -496,6 +497,138 @@ SELECT
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV60(Session& session)
{
// Dedicated directory table
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "directory" (
"id" integer primary key autoincrement,
"version" integer not null,
"absolute_path" text not null,
"name" text not null,
"parent_directory_id" bigint,
constraint "fk_directory_directory" foreign key ("parent_directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
))");
// Add a ref in track, need to recreate a new table
session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"scan_version" integer not null,
"track_number" integer,
"disc_number" integer,
"total_track" integer,
"disc_subtitle" text not null,
"name" text not null,
"duration" integer,
"bitrate" integer not null,
"bits_per_sample" integer not null,
"channel_count" integer not null,
"sample_rate" integer not null,
"date" text,
"year" integer,
"original_date" text,
"original_year" integer,
"absolute_file_path" text not null,
"relative_file_path" text not null,
"file_size" bigint not null,
"file_last_write" text,
"file_added" text,
"has_cover" boolean not null,
"mbid" text not null,
"recording_mbid" text not null,
"copyright" text not null,
"copyright_url" text not null,
"track_replay_gain" real,
"release_replay_gain" real,
"artist_display_name" text not null,
"release_id" bigint,
"media_library_id" bigint,
"directory_id" bigint,
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
))");
// Migrate data, with the new directory_id field set to null
session.getDboSession()->execute(R"(INSERT INTO track_backup
SELECT
id,
version,
scan_version,
track_number,
disc_number,
total_track,
disc_subtitle,
name,
duration,
bitrate,
bits_per_sample,
channel_count,
sample_rate,
date,
year,
original_date,
original_year,
absolute_file_path,
relative_file_path,
file_size,
file_last_write,
file_added,
has_cover,
mbid,
recording_mbid,
copyright,
copyright_url,
track_replay_gain,
release_replay_gain,
artist_display_name,
release_id,
media_library_id,
NULL
FROM track)");
session.getDboSession()->execute("DROP TABLE track");
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
// Add a ref in image + rename path to absolute_file_path, need to recreate a new table
session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "image_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"absolute_file_path" text not null,
"stem" text not null,
"file_last_write" text,
"file_size" integer not null,
"width" integer not null,
"height" integer not null,
"artist_id" bigint,
"directory_id" bigint,
constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
constraint "fk_image_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
))");
// Migrate data, with the new directory_id field set to null
session.getDboSession()->execute(R"(INSERT INTO image_backup
SELECT
id,
version,
path,
stem,
file_last_write,
file_size,
width,
height,
artist_id,
NULL
FROM image
)");
session.getDboSession()->execute("DROP TABLE image");
session.getDboSession()->execute("ALTER TABLE image_backup RENAME TO image");
// 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 } // namespace
bool doDbMigration(Session& session) bool doDbMigration(Session& session)
@@ -534,6 +667,7 @@ SELECT
{ 57, migrateFromV57 }, { 57, migrateFromV57 },
{ 58, migrateFromV58 }, { 58, migrateFromV58 },
{ 59, migrateFromV59 }, { 59, migrateFromV59 },
{ 60, migrateFromV60 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+4 -4
View File
@@ -226,14 +226,14 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.id = ?").bind(id));
} }
ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name) ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>().where("name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
} }
Release::Release(const std::string& name, const std::optional<core::UUID>& MBID) Release::Release(const std::string& name, const std::optional<core::UUID>& MBID)
@@ -258,14 +258,14 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>().where("mbid = ?").bind(mbid.getAsString())); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT r from release r").where("r.mbid = ?").bind(mbid.getAsString()));
} }
Release::pointer Release::find(Session& session, ReleaseId id) Release::pointer Release::find(Session& session, ReleaseId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT r from release r").where("r.id = ?").bind(id));
} }
bool Release::exists(Session& session, ReleaseId id) bool Release::exists(Session& session, ReleaseId id)
+9
View File
@@ -26,6 +26,7 @@
#include "database/AuthToken.hpp" #include "database/AuthToken.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp" #include "database/Image.hpp"
#include "database/Listen.hpp" #include "database/Listen.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
@@ -93,6 +94,7 @@ namespace lms::db
_session.mapClass<AuthToken>("auth_token"); _session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster"); _session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type"); _session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Directory>("directory");
_session.mapClass<Image>("image"); _session.mapClass<Image>("image");
_session.mapClass<Listen>("listen"); _session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library"); _session.mapClass<MediaLibrary>("media_library");
@@ -179,7 +181,14 @@ namespace lms::db
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
_session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)");
_session.execute("CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
_session.execute("CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
+6 -5
View File
@@ -24,6 +24,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -219,21 +220,21 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Track>().where("absolute_file_path = ?").bind(p.string())); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.absolute_file_path = ?").bind(p.string()));
} }
Track::pointer Track::find(Session& session, TrackId id) Track::pointer Track::find(Session& session, TrackId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Track>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.id = ?").bind(id));
} }
void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library) void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->find<Track>().orderBy("id").where("id > ?").bind(lastRetrievedTrack).limit(static_cast<int>(count)) }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").orderBy("t.id").where("t.id > ?").bind(lastRetrievedTrack).limit(static_cast<int>(count)) };
if (library.isValid()) if (library.isValid())
query.where("media_library_id = ?").bind(library); query.where("media_library_id = ?").bind(library);
@@ -255,14 +256,14 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>().where("mbid = ?").bind(mbid.getAsString())); return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.mbid = ?").bind(mbid.getAsString()));
} }
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid) std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>().where("recording_mbid = ?").bind(mbid.getAsString())); return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.recording_mbid = ?").bind(mbid.getAsString()));
} }
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range) RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
+1
View File
@@ -23,6 +23,7 @@
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Directory.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
+2 -2
View File
@@ -136,7 +136,7 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
assert(userId.isValid()); assert(userId.isValid());
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>().where("name = ?").bind(name).where("type = ?").bind(type).where("user_id = ?").bind(userId)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackList>>("select t_l from tracklist t_l").where("t_l.name = ?").bind(name).where("t_l.type = ?").bind(type).where("t_l.user_id = ?").bind(userId));
} }
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params) RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
@@ -157,7 +157,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackList>>("select t_l from tracklist t_l").where("t_l.id = ?").bind(id));
} }
bool TrackList::isEmpty() const bool TrackList::isEmpty() const
+3 -3
View File
@@ -78,17 +78,17 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<User>().where("type = ?").bind(UserType::DEMO)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<User>>("SELECT u from user u").where("u.type = ?").bind(UserType::DEMO));
} }
User::pointer User::find(Session& session, UserId id) User::pointer User::find(Session& session, UserId id)
{ {
return utils::fetchQuerySingleResult(session.getDboSession()->find<User>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<User>>("SELECT u from user u").where("u.id = ?").bind(id));
} }
User::pointer User::find(Session& session, std::string_view name) User::pointer User::find(Session& session, std::string_view name)
{ {
return utils::fetchQuerySingleResult(session.getDboSession()->find<User>().where("login_name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<User>>("SELECT u from user u").where("u.login_name = ?").bind(name));
} }
void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate) void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
@@ -153,6 +153,7 @@ namespace lms::db
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
void setSortName(const std::string& sortName); void setSortName(const std::string& sortName);
void setImage(ObjectPtr<Image> image);
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -0,0 +1,97 @@
/*
* 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 <functional>
#include <Wt/Dbo/Dbo.h>
#include "core/EnumSet.hpp"
#include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class Session;
class Directory final : public Object<Directory, DirectoryId>
{
public:
Directory() = default;
struct FindParameters
{
std::optional<Range> range;
ArtistId artist; // only tracks that involve this artist
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
{
artist = _artist;
trackArtistLinkTypes = _trackArtistLinkTypes;
return *this;
}
};
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, DirectoryId id);
static pointer find(Session& session, const std::filesystem::path& path);
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// getters
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
std::string_view getName() const { return _name; }
ObjectPtr<Directory> getParent() const { return _parent; }
// setters
void setAbsolutePath(const std::filesystem::path& p);
void setParent(ObjectPtr<Directory> parent);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _absolutePath, "absolute_path");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::belongsTo(a, _parent, "parent_directory", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Directory(const std::filesystem::path& p);
static pointer create(Session& session, const std::filesystem::path& p);
std::filesystem::path _absolutePath;
std::string _name;
Wt::Dbo::ptr<Directory> _parent;
};
} // 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(DirectoryId)
+41 -4
View File
@@ -20,17 +20,21 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <functional>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp"
#include "database/ImageId.hpp" #include "database/ImageId.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp"
namespace lms::db namespace lms::db
{ {
class Artist; class Artist;
class Directory;
class Session; class Session;
class Image final : public Object<Image, ImageId> class Image final : public Object<Image, ImageId>
@@ -38,29 +42,59 @@ namespace lms::db
public: public:
Image() = default; Image() = default;
struct FindParameters
{
std::optional<Range> range;
std::string fileStem; // if set, images with this file stem
DirectoryId directory; // if set, images in this directory
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setFileStem(std::string_view _fileStem)
{
fileStem = _fileStem;
return *this;
}
FindParameters& setDirectory(DirectoryId _directory)
{
directory = _directory;
return *this;
}
};
// find // find
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, ImageId id); static pointer find(Session& session, ImageId id);
static pointer find(Session& session, const std::filesystem::path& file);
static RangeResults<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Image::pointer&)>& func);
static void find(Session& session, ImageId& lastRetrievedImage, std::size_t count, const std::function<void(const Image::pointer&)>& func);
// getters // getters
const std::filesystem::path& getPath() const { return _path; } const std::filesystem::path& getAbsoluteFilePath() const { return _fileAbsolutePath; }
std::string_view getFileStem() const { return _fileStem; }
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
std::size_t getFileSize() const { return _fileSize; } std::size_t getFileSize() const { return _fileSize; }
std::size_t getWidth() const { return _width; } std::size_t getWidth() const { return _width; }
std::size_t getHeight() const { return _height; } std::size_t getHeight() const { return _height; }
// setters // setters
void setPath(const std::filesystem::path& p) { _path = p; } void setAbsoluteFilePath(const std::filesystem::path& p);
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setWidth(std::size_t width) { _width = width; } void setWidth(std::size_t width) { _width = width; }
void setHeight(std::size_t height) { _height = height; } void setHeight(std::size_t height) { _height = height; }
void setArtist(const ObjectPtr<Artist>& artist) { _artist = getDboPtr(artist); } void setArtist(const ObjectPtr<Artist>& artist) { _artist = getDboPtr(artist); }
void setDirectory(const ObjectPtr<Directory>& directory) { _directory = getDboPtr(directory); }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _path, "path"); Wt::Dbo::field(a, _fileAbsolutePath, "absolute_file_path");
Wt::Dbo::field(a, _fileStem, "stem");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileSize, "file_size"); Wt::Dbo::field(a, _fileSize, "file_size");
@@ -68,6 +102,7 @@ namespace lms::db
Wt::Dbo::field(a, _height, "height"); Wt::Dbo::field(a, _height, "height");
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
} }
private: private:
@@ -75,12 +110,14 @@ namespace lms::db
Image(const std::filesystem::path& p); Image(const std::filesystem::path& p);
static pointer create(Session& session, const std::filesystem::path& p); static pointer create(Session& session, const std::filesystem::path& p);
std::filesystem::path _path; std::filesystem::path _fileAbsolutePath;
std::string _fileStem;
Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileLastWrite;
int _fileSize{}; int _fileSize{};
int _width{}; int _width{};
int _height{}; int _height{};
Wt::Dbo::ptr<Artist> _artist; Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<Directory> _directory;
}; };
} // namespace lms::db } // namespace lms::db
@@ -50,6 +50,7 @@ namespace lms::db
class Artist; class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Directory;
class MediaLibrary; class MediaLibrary;
class Release; class Release;
class Session; class Session;
@@ -225,6 +226,7 @@ namespace lms::db
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); } void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters); void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); } void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
void setDirectory(ObjectPtr<Directory> directory) { _directory = getDboPtr(directory); }
std::size_t getScanVersion() const { return _scanVersion; } std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; } std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
@@ -263,6 +265,7 @@ namespace lms::db
std::vector<ObjectPtr<Cluster>> getClusters() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> getClusterIds() const; std::vector<ClusterId> getClusterIds() const;
ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; } ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; }
ObjectPtr<Directory> getDirectory() const { return _directory; }
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
@@ -299,6 +302,7 @@ namespace lms::db
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
} }
@@ -342,6 +346,7 @@ namespace lms::db
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary; Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
Wt::Dbo::ptr<Directory> _directory;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters; Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
}; };
+1
View File
@@ -4,6 +4,7 @@ add_executable(test-database
Cluster.cpp Cluster.cpp
Common.cpp Common.cpp
DatabaseTest.cpp DatabaseTest.cpp
Directory.cpp
Image.cpp Image.cpp
Listen.cpp Listen.cpp
Migration.cpp Migration.cpp
+155
View File
@@ -0,0 +1,155 @@
/*
* 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/Directory.hpp"
namespace lms::db::tests
{
using ScopedDirectory = ScopedEntity<db::Directory>;
TEST_F(DatabaseFixture, Directory)
{
ScopedDirectory directory{ session, "/path/to/dir/" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Directory::getCount(session), 1);
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getAbsolutePath(), "/path/to/dir");
EXPECT_EQ(dir->getName(), "dir");
}
{
auto transaction{ session.createWriteTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
dir.modify()->setAbsolutePath("/path/to/another/dir2");
}
{
auto transaction{ session.createReadTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getAbsolutePath(), "/path/to/another/dir2");
EXPECT_EQ(dir->getName(), "dir2");
}
{
auto transaction{ session.createWriteTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
dir.modify()->setAbsolutePath("/foo/");
}
{
auto transaction{ session.createReadTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getAbsolutePath(), "/foo");
EXPECT_EQ(dir->getName(), "foo");
}
{
auto transaction{ session.createWriteTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
dir.modify()->setAbsolutePath("/");
}
{
auto transaction{ session.createReadTransaction() };
Directory::pointer dir{ Directory::find(session, directory.getId()) };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getAbsolutePath(), "/");
EXPECT_EQ(dir->getName(), "");
}
{
auto transaction{ session.createReadTransaction() };
Directory::pointer dir{ Directory::find(session, "/") };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getId(), directory.getId());
}
}
TEST_F(DatabaseFixture, parent)
{
ScopedDirectory parent{ session, "/path/to/dir/" };
ScopedDirectory child{ session, "/path/to/dir/child" };
{
auto transaction{ session.createReadTransaction() };
auto dir{ child->getParent() };
EXPECT_EQ(dir, Directory::pointer{});
}
{
auto transaction{ session.createWriteTransaction() };
child.get().modify()->setParent(parent.lockAndGet());
}
{
auto transaction{ session.createReadTransaction() };
auto dir{ child->getParent() };
ASSERT_NE(dir, Directory::pointer{});
EXPECT_EQ(dir->getId(), parent.getId());
}
}
TEST_F(DatabaseFixture, Directory_orphaned)
{
ScopedDirectory parent{ session, "/path/to/dir/" };
ScopedDirectory child{ session, "/path/to/dir/child" };
{
auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findOrphanIds(session).results };
EXPECT_EQ(directories.size(), 2);
}
{
auto transaction{ session.createWriteTransaction() };
child.get().modify()->setParent(parent.lockAndGet());
}
{
auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findOrphanIds(session).results };
ASSERT_EQ(directories.size(), 1);
EXPECT_EQ(directories.front(), child.getId());
}
}
} // namespace lms::db::tests
+38 -3
View File
@@ -19,10 +19,12 @@
#include "Common.hpp" #include "Common.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp" #include "database/Image.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedDirectory = ScopedEntity<db::Directory>;
using ScopedImage = ScopedEntity<db::Image>; using ScopedImage = ScopedEntity<db::Image>;
TEST_F(DatabaseFixture, Image) TEST_F(DatabaseFixture, Image)
@@ -35,7 +37,8 @@ namespace lms::db::tests
Image::pointer img{ Image::find(session, image.getId()) }; Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{}); ASSERT_NE(img, Image::pointer{});
EXPECT_EQ(img->getPath(), "/path/to/image"); EXPECT_EQ(img->getAbsoluteFilePath(), "/path/to/image");
EXPECT_EQ(img->getFileStem(), "image");
EXPECT_EQ(img->getWidth(), 0); EXPECT_EQ(img->getWidth(), 0);
EXPECT_EQ(img->getHeight(), 0); EXPECT_EQ(img->getHeight(), 0);
EXPECT_EQ(img->getFileSize(), 0); EXPECT_EQ(img->getFileSize(), 0);
@@ -46,7 +49,7 @@ namespace lms::db::tests
Image::pointer img{ Image::find(session, image.getId()) }; Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{}); ASSERT_NE(img, Image::pointer{});
img.modify()->setPath("/path/to/another/image"); img.modify()->setAbsoluteFilePath("/path/to/another/image2");
img.modify()->setWidth(640); img.modify()->setWidth(640);
img.modify()->setHeight(480); img.modify()->setHeight(480);
img.modify()->setFileSize(1024 * 1024); img.modify()->setFileSize(1024 * 1024);
@@ -57,10 +60,42 @@ namespace lms::db::tests
Image::pointer img{ Image::find(session, image.getId()) }; Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{}); ASSERT_NE(img, Image::pointer{});
EXPECT_EQ(img->getPath(), "/path/to/another/image"); EXPECT_EQ(img->getAbsoluteFilePath(), "/path/to/another/image2");
EXPECT_EQ(img->getFileStem(), "image2");
EXPECT_EQ(img->getWidth(), 640); EXPECT_EQ(img->getWidth(), 640);
EXPECT_EQ(img->getHeight(), 480); EXPECT_EQ(img->getHeight(), 480);
EXPECT_EQ(img->getFileSize(), 1024 * 1024); EXPECT_EQ(img->getFileSize(), 1024 * 1024);
} }
{
auto transaction{ session.createReadTransaction() };
Image::pointer img{ Image::find(session, "/path/to/another/image2") };
ASSERT_NE(img, Image::pointer{});
EXPECT_EQ(img->getId(), image->getId());
}
}
TEST_F(DatabaseFixture, Image_inDirectory)
{
ScopedImage image{ session, "/path/to/image" };
ScopedDirectory directory{ session, "/path/to" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())).results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
image.get().modify()->setDirectory(directory.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto results{ Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())).results };
ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.front()->getId(), image.getId());
}
} }
} // namespace lms::db::tests } // namespace lms::db::tests
+25
View File
@@ -18,8 +18,14 @@
*/ */
#include "Common.hpp" #include "Common.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/StarredArtist.hpp"
#include "database/StarredRelease.hpp"
#include "database/StarredTrack.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
@@ -318,5 +324,24 @@ VALUES
// Now perform full migration // Now perform full migration
db.getTLSSession().migrateSchemaIfNeeded(); db.getTLSSession().migrateSchemaIfNeeded();
// Now perform some dummy finds to ensure all fields are correctly mapped
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Artist::find(session, ArtistId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Listen::find(session, ListenId{}));
EXPECT_FALSE(Release::find(session, ReleaseId{}));
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
EXPECT_FALSE(Track::find(session, TrackId{}));
EXPECT_FALSE(TrackList::find(session, TrackListId{}));
EXPECT_FALSE(User::find(session, UserId{}));
}
} }
} // namespace lms::db::tests } // namespace lms::db::tests
+2
View File
@@ -25,6 +25,7 @@ if (${LMS_IMAGE_BACKEND} STREQUAL "stb")
message(STATUS "Using stb (resize version ${STB_IMAGE_RESIZE_VERSION})") message(STATUS "Using stb (resize version ${STB_IMAGE_RESIZE_VERSION})")
target_sources(lmsimage PRIVATE target_sources(lmsimage PRIVATE
impl/stb/Image.cpp
impl/stb/JPEGImage.cpp impl/stb/JPEGImage.cpp
impl/stb/RawImage.cpp impl/stb/RawImage.cpp
) )
@@ -36,6 +37,7 @@ elseif (${LMS_IMAGE_BACKEND} STREQUAL "graphicsmagick")
message(STATUS "Using graphicsmagick") message(STATUS "Using graphicsmagick")
target_sources(lmsimage PRIVATE target_sources(lmsimage PRIVATE
impl/graphicsmagick/Image.cpp
impl/graphicsmagick/JPEGImage.cpp impl/graphicsmagick/JPEGImage.cpp
impl/graphicsmagick/RawImage.cpp impl/graphicsmagick/RawImage.cpp
) )
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2015 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 "image/Image.hpp"
#include <array>
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
namespace lms::image
{
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
{
return std::make_unique<GraphicsMagick::RawImage>(encodedData, encodedDataSize);
}
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{
return std::make_unique<GraphicsMagick::RawImage>(path);
}
void init(const std::filesystem::path& path)
{
Magick::InitializeMagick(path.string().c_str());
if (auto nbThreads{ MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource) }; nbThreads != 1)
LMS_LOG(COVER, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources");
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
LMS_LOG(COVER, ERROR, "Cannot set Magick thread resource limit to 1!");
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
LMS_LOG(COVER, ERROR, "Cannot set Magick disk resource limit to 0!");
LMS_LOG(COVER, INFO, "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource));
LMS_LOG(COVER, INFO, "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource));
}
std::span<const std::filesystem::path> getSupportedFileExtensions()
{
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
return fileExtensions;
}
} // namespace lms::image
@@ -19,6 +19,9 @@
#include "RawImage.hpp" #include "RawImage.hpp"
#include <algorithm>
#include <array>
#include <magick/resource.h> #include <magick/resource.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
@@ -26,39 +29,8 @@
#include "JPEGImage.hpp" #include "JPEGImage.hpp"
namespace lms::image
{
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
{
return std::make_unique<GraphicsMagick::RawImage>(encodedData, encodedDataSize);
}
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{
return std::make_unique<GraphicsMagick::RawImage>(path);
}
void init(const std::filesystem::path& path)
{
Magick::InitializeMagick(path.string().c_str());
if (auto nbThreads{ MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource) }; nbThreads != 1)
LMS_LOG(COVER, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources");
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
LMS_LOG(COVER, ERROR, "Cannot set Magick thread resource limit to 1!");
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
LMS_LOG(COVER, ERROR, "Cannot set Magick disk resource limit to 0!");
LMS_LOG(COVER, INFO, "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource));
LMS_LOG(COVER, INFO, "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource));
}
} // namespace lms::image
namespace lms::image::GraphicsMagick namespace lms::image::GraphicsMagick
{ {
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
{ {
try try
+50
View File
@@ -0,0 +1,50 @@
/*
* 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 "image/Image.hpp"
#include <array>
#include "RawImage.hpp"
#include "core/ITraceLogger.hpp"
namespace lms::image
{
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
return std::make_unique<STB::RawImage>(encodedData, encodedDataSize);
}
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
return std::make_unique<STB::RawImage>(path);
}
void init(const std::filesystem::path&)
{
}
std::span<const std::filesystem::path> getSupportedFileExtensions()
{
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
return fileExtensions;
}
} // namespace lms::image
-19
View File
@@ -41,25 +41,6 @@
#include "JPEGImage.hpp" #include "JPEGImage.hpp"
namespace lms::image
{
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
return std::make_unique<STB::RawImage>(encodedData, encodedDataSize);
}
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
return std::make_unique<STB::RawImage>(path);
}
void init(const std::filesystem::path&)
{
}
} // namespace lms::image
namespace lms::image::STB namespace lms::image::STB
{ {
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize) RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
+2
View File
@@ -21,6 +21,7 @@
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <span>
#include "image/IEncodedImage.hpp" #include "image/IEncodedImage.hpp"
#include "image/IRawImage.hpp" #include "image/IRawImage.hpp"
@@ -28,6 +29,7 @@
namespace lms::image namespace lms::image
{ {
void init(const std::filesystem::path& path); void init(const std::filesystem::path& path);
std::span<const std::filesystem::path> getSupportedFileExtensions();
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize);
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path); std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& path); std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& path);
@@ -381,10 +381,10 @@ namespace lms::cover
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
if (const Artist::pointer artist{ db::Artist::find(session, artistId) }) if (const Artist::pointer artist{ Artist::find(session, artistId) })
{ {
if (const db::Image::pointer image{ artist->getImage() }) if (const db::Image::pointer image{ artist->getImage() })
artistImage = getFromCoverFile(image->getPath(), width); artistImage = getFromCoverFile(image->getAbsoluteFilePath(), width);
} }
} }
+6 -4
View File
@@ -1,15 +1,17 @@
add_library(lmsscanner SHARED add_library(lmsscanner SHARED
impl/FileScanQueue.cpp
impl/ScannerService.cpp impl/ScannerService.cpp
impl/ScannerStats.cpp impl/ScannerStats.cpp
impl/ScanStepCheckDuplicatedDbFiles.cpp impl/ScanStepAssociateArtistImages.cpp
impl/ScanStepCheckForDuplicatedFiles.cpp
impl/ScanStepCheckForRemovedFiles.cpp
impl/ScanStepCompact.cpp impl/ScanStepCompact.cpp
impl/ScanStepComputeClusterStats.cpp impl/ScanStepComputeClusterStats.cpp
impl/ScanStepDiscoverFiles.cpp impl/ScanStepDiscoverFiles.cpp
impl/ScanStepOptimize.cpp impl/ScanStepOptimize.cpp
impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepRemoveOrphanedDbEntries.cpp
impl/ScanStepScanArtistImages.cpp impl/ScanStepScanFiles.cpp
impl/ScanStepScanAudioFiles.cpp
) )
target_include_directories(lmsscanner INTERFACE target_include_directories(lmsscanner INTERFACE
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2023 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 "FileScanQueue.hpp"
#include "core/Exception.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/Path.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
#include "metadata/Exception.hpp"
namespace lms::scanner
{
FileScanQueue::FileScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort)
: _metadataParser{ parser }
, _scanContextRunner{ _scanContext, threadCount, "FileScan" }
, _abort{ abort }
{
}
void FileScanQueue::pushScanRequest(const std::filesystem::path& path, ScanRequestType type)
{
{
std::scoped_lock lock{ _mutex };
_ongoingScanCount += 1;
}
_scanContext.post([=, this] {
if (_abort)
{
std::scoped_lock lock{ _mutex };
_ongoingScanCount -= 1;
}
else
{
FileScanResult result;
result.path = path;
switch (type)
{
case ScanRequestType::AudioFile:
result.scanData = scanAudioFile(path);
break;
case ScanRequestType::ImageFile:
result.scanData = scanImageFile(path);
}
{
std::scoped_lock lock{ _mutex };
_scanResults.emplace_back(std::move(result));
_ongoingScanCount -= 1;
}
}
_condVar.notify_all();
});
}
AudioFileScanData FileScanQueue::scanAudioFile(const std::filesystem::path& path)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
std::unique_ptr<metadata::Track> track;
try
{
track = _metadataParser.parse(path);
}
catch (const metadata::Exception& e)
{
LMS_LOG(DBUPDATER, INFO, "Failed to parse audio file '" << path.string() << "'");
}
return track;
}
ImageFileScanData FileScanQueue::scanImageFile(const std::filesystem::path& path)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanImageFile");
std::optional<ImageInfo> optInfo;
try
{
std::unique_ptr<image::IRawImage> rawImage{ image::decodeImage(path) };
ImageInfo& imageInfo{ optInfo.emplace() };
imageInfo.width = rawImage->getWidth();
imageInfo.height = rawImage->getHeight();
}
catch (const image::Exception& e)
{
LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file '" << path.string() << "': " << e.what());
}
return optInfo;
}
std::size_t FileScanQueue::getResultsCount() const
{
std::scoped_lock lock{ _mutex };
return _scanResults.size();
}
size_t FileScanQueue::popResults(std::vector<FileScanResult>& results, std::size_t maxCount)
{
results.clear();
results.reserve(maxCount);
{
std::scoped_lock lock{ _mutex };
while (results.size() < maxCount && !_scanResults.empty())
{
results.push_back(std::move(_scanResults.front()));
_scanResults.pop_front();
}
}
return results.size();
}
void FileScanQueue::wait(std::size_t maxScanRequestCount)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
std::unique_lock lock{ _mutex };
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
}
} // namespace lms::scanner
@@ -0,0 +1,83 @@
/*
* 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 <condition_variable>
#include <deque>
#include <filesystem>
#include <mutex>
#include <span>
#include <variant>
#include <vector>
#include "core/IOContextRunner.hpp"
#include "metadata/IParser.hpp"
namespace lms::scanner
{
struct ImageInfo
{
std::size_t height{};
std::size_t width{};
};
using AudioFileScanData = std::unique_ptr<metadata::Track>;
using ImageFileScanData = std::optional<ImageInfo>;
struct FileScanResult
{
std::filesystem::path path;
std::variant<std::monostate, AudioFileScanData, ImageFileScanData> scanData;
};
class FileScanQueue
{
public:
FileScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort);
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
enum ScanRequestType
{
AudioFile,
ImageFile,
};
void pushScanRequest(const std::filesystem::path& path, ScanRequestType type);
std::size_t getResultsCount() const;
size_t popResults(std::vector<FileScanResult>& results, std::size_t maxCount);
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
private:
AudioFileScanData scanAudioFile(const std::filesystem::path& path);
ImageFileScanData scanImageFile(const std::filesystem::path& path);
metadata::IParser& _metadataParser;
boost::asio::io_context _scanContext;
core::IOContextRunner _scanContextRunner;
mutable std::mutex _mutex;
std::size_t _ongoingScanCount{};
std::deque<FileScanResult> _scanResults;
std::condition_variable _condVar;
bool& _abort;
};
} // namespace lms::scanner
@@ -0,0 +1,232 @@
/*
* 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 "ScanStepAssociateArtistImages.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/Directory.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{ 100 };
constexpr std::size_t writeBatchSize{ 10 };
struct ArtistImageAssociation
{
db::ArtistId artistId;
db::ImageId imageId;
};
using ArtistImageAssociationContainer = std::deque<ArtistImageAssociation>;
struct SearchImageContext
{
db::Session& session;
db::ArtistId lastRetrievedArtistId;
const std::vector<std::string>& artistFileNames;
};
db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath)
{
db::Image::pointer image;
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
if (directory) // may not exist for artists that are split on different media libraries
{
for (std::string_view fileStem : searchContext.artistFileNames)
{
db::Image::FindParameters params;
params.setDirectory(directory->getId());
params.setFileStem(fileStem);
db::Image::find(searchContext.session, params, [&](const db::Image::pointer foundImg) {
if (!image)
image = foundImg;
});
if (image)
break;
}
}
return image;
}
db::Image::pointer computeBestArtistImage(SearchImageContext& searchContext, const db::Artist::pointer& artist)
{
db::Image::pointer image;
const auto mbid{ artist->getMBID() };
if (mbid)
{
// Find anywhere, since it is suppoed to be unique!
db::Image::find(searchContext.session, db::Image::FindParameters{}.setFileStem(mbid->getAsString()), [&](const db::Image::pointer foundImg) {
if (!image)
image = foundImg;
});
}
if (!image)
{
std::set<std::filesystem::path> releasePaths;
db::Directory::FindParameters params;
params.setArtist(artist->getId(), { db::TrackArtistLinkType::ReleaseArtist });
db::Directory::find(searchContext.session, params, [&](const db::Directory::pointer& directory) {
releasePaths.insert(directory->getAbsolutePath());
});
// Expect layout like this:
// ReleaseArtist/Release/Tracks'
// /artist.jpg
// /someOtherUserConfiguredArtistFile.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)) };
image = findImageInDirectory(searchContext, artistPath);
}
if (!image)
{
// Expect layout like this:
// ReleaseArtist/Release/Tracks'
// /artist.jpg
// /someOtherUserConfiguredArtistFile.jpg
for (const std::filesystem::path& releasePath : releasePaths)
{
image = findImageInDirectory(searchContext, releasePath);
if (image)
break;
}
}
}
return image;
}
bool fetchNextArtistImagesToUpdate(SearchImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
{
const db::ArtistId artistId{ searchContext.lastRetrievedArtistId };
{
auto transaction{ searchContext.session.createReadTransaction() };
db::Artist::find(searchContext.session, searchContext.lastRetrievedArtistId, readBatchSize, [&](const db::Artist::pointer& artist) {
db::Image::pointer image{ computeBestArtistImage(searchContext, artist) };
if (image != artist->getImage())
{
LMS_LOG(DBUPDATER, DEBUG, "Updating artist image for artist '" << artist->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "<none>") << "'");
artistImageAssociations.push_back(ArtistImageAssociation{ artist->getId(), image ? image->getId() : db::ImageId{} });
}
});
}
return artistId != searchContext.lastRetrievedArtistId;
}
void updateArtistImage(db::Session& session, const ArtistImageAssociation& artistImageAssociation)
{
db::Artist::pointer artist{ db::Artist::find(session, artistImageAssociation.artistId) };
assert(artist);
db::Image::pointer image;
if (artistImageAssociation.imageId.isValid())
image = db::Image::find(session, artistImageAssociation.imageId);
artist.modify()->setImage(image);
}
void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations)
{
if (imageAssociations.empty())
return;
auto transaction{ session.createWriteTransaction() };
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
{
updateArtistImage(session, imageAssociations.front());
imageAssociations.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
ScanStepAssociateArtistImages::ScanStepAssociateArtistImages(InitParams& initParams)
: ScanStepBase{ initParams }
, _artistFileNames{ constructArtistFileNames() }
{
}
void ScanStepAssociateArtistImages::process(ScanContext& context)
{
if (context.stats.nbChanges() == 0)
return;
auto& session{ _db.getTLSSession() };
{
auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = db::Artist::getCount(session);
}
SearchImageContext searchContext{
.session = session,
.lastRetrievedArtistId = {},
.artistFileNames = _artistFileNames,
};
ArtistImageAssociationContainer artistImageAssociations;
while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations))
{
updateArtistImages(session, artistImageAssociations);
context.currentStepStats.processedElems += readBatchSize;
_progressCallback(context.currentStepStats);
}
}
} // namespace lms::scanner
@@ -26,14 +26,14 @@
namespace lms::scanner namespace lms::scanner
{ {
class ScanStepScanArtistImages : public ScanStepBase class ScanStepAssociateArtistImages : public ScanStepBase
{ {
public: public:
ScanStepScanArtistImages(InitParams& initParams); ScanStepAssociateArtistImages(InitParams& initParams);
private: private:
ScanStep getStep() const override { return ScanStep::ScanArtistImages; } ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
core::LiteralString getStepName() const override { return "Scan artist images"; } core::LiteralString getStepName() const override { return "Associate artist images"; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
const std::vector<std::string> _artistFileNames; const std::vector<std::string> _artistFileNames;
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "ScanStepCheckDuplicatedDbFiles.hpp" #include "ScanStepCheckForDuplicatedFiles.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
@@ -26,7 +26,7 @@
namespace lms::scanner namespace lms::scanner
{ {
void ScanStepCheckDuplicatedDbFiles::process(ScanContext& context) void ScanStepCheckForDuplicatedFiles::process(ScanContext& context)
{ {
using namespace db; using namespace db;
@@ -23,14 +23,14 @@
namespace lms::scanner namespace lms::scanner
{ {
class ScanStepCheckDuplicatedDbFiles : public ScanStepBase class ScanStepCheckForDuplicatedFiles : public ScanStepBase
{ {
public: public:
using ScanStepBase::ScanStepBase; using ScanStepBase::ScanStepBase;
private: private:
core::LiteralString getStepName() const override { return "Check for duplicated files"; } core::LiteralString getStepName() const override { return "Check for duplicated files"; }
ScanStep getStep() const override { return ScanStep::CheckForDuplicateFiles; } ScanStep getStep() const override { return ScanStep::CheckForDuplicatedFiles; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
}; };
} // namespace lms::scanner } // namespace lms::scanner
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2023 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 "ScanStepCheckForRemovedFiles.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
namespace lms::scanner
{
namespace
{
constexpr std::size_t batchSize = 100;
}
void ScanStepCheckForRemovedFiles::process(ScanContext& context)
{
if (_abortScan)
return;
db::Session& session{ _db.getTLSSession() };
{
auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = 0;
context.currentStepStats.totalElems += db::Track::getCount(session);
context.currentStepStats.totalElems += db::Image::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
checkForRemovedFiles<db::Track>(context, _settings.supportedAudioFileExtensions);
checkForRemovedFiles<db::Image>(context, _settings.supportedImageFileExtensions);
}
template<typename Object>
void ScanStepCheckForRemovedFiles::checkForRemovedFiles(ScanContext& context, const std::vector<std::filesystem::path>& supportedFileExtensions)
{
using namespace db;
if (_abortScan)
return;
Session& session{ _db.getTLSSession() };
std::vector<typename Object::pointer> objectsToRemove;
typename Object::IdType lastCheckedId;
bool endReached{};
while (!endReached)
{
if (_abortScan)
break;
objectsToRemove.clear();
{
auto transaction{ session.createReadTransaction() };
endReached = true;
Object::find(session, lastCheckedId, batchSize, [&](const typename Object::pointer& object) {
endReached = false;
if (!checkFile(object->getAbsoluteFilePath(), supportedFileExtensions))
objectsToRemove.push_back(object);
context.currentStepStats.processedElems++;
});
}
if (!objectsToRemove.empty())
{
auto transaction{ session.createWriteTransaction() };
for (typename Object::pointer& object : objectsToRemove)
{
object.remove();
context.stats.deletions++;
}
}
_progressCallback(context.currentStepStats);
}
}
bool ScanStepCheckForRemovedFiles::checkFile(const std::filesystem::path& p, const std::vector<std::filesystem::path>& allowedExtensions)
{
try
{
// For each track, make sure the the file still exists
// and still belongs to a media directory
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
return false;
}
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
}))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
return false;
}
if (!core::pathUtils::hasFileAnyExtension(p, allowedExtensions))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
return false;
}
return true;
}
catch (std::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
return false;
}
}
} // namespace lms::scanner
@@ -25,21 +25,19 @@
namespace lms::scanner namespace lms::scanner
{ {
class ScanStepRemoveOrphanDbFiles : public ScanStepBase class ScanStepCheckForRemovedFiles : public ScanStepBase
{ {
public: public:
using ScanStepBase::ScanStepBase; using ScanStepBase::ScanStepBase;
private: private:
core::LiteralString getStepName() const override { return "Check orphaned entries"; } core::LiteralString getStepName() const override { return "Check for removed files"; }
ScanStep getStep() const override { return ScanStep::CheckForMissingFiles; } ScanStep getStep() const override { return ScanStep::CheckForRemovedFiles; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
void removeOrphanTracks(ScanContext& context); template<typename Object>
void removeOrphanClusters(); void checkForRemovedFiles(ScanContext& context, const std::vector<std::filesystem::path>& supportedFileExtensions);
void removeOrphanClusterTypes();
void removeOrphanArtists(); bool checkFile(const std::filesystem::path& p, const std::vector<std::filesystem::path>& allowedExtensions);
void removeOrphanReleases();
bool checkFile(const std::filesystem::path& p);
}; };
} // namespace lms::scanner } // namespace lms::scanner
@@ -26,7 +26,7 @@ namespace lms::scanner
{ {
void ScanStepDiscoverFiles::process(ScanContext& context) void ScanStepDiscoverFiles::process(ScanContext& context)
{ {
context.stats.filesScanned = 0; context.stats.totalFileCount = 0;
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries) for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
{ {
@@ -36,7 +36,7 @@ namespace lms::scanner
if (_abortScan) if (_abortScan)
return false; return false;
if (!ec && core::pathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) if (!ec && (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions) || core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions)))
{ {
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
currentDirectoryProcessElemsCount++; currentDirectoryProcessElemsCount++;
@@ -50,8 +50,8 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << currentDirectoryProcessElemsCount << " files in '" << mediaLibrary.rootDirectory << "'"); LMS_LOG(DBUPDATER, DEBUG, "Discovered " << currentDirectoryProcessElemsCount << " files in '" << mediaLibrary.rootDirectory << "'");
} }
context.stats.filesScanned = context.currentStepStats.processedElems; context.stats.totalFileCount = context.currentStepStats.processedElems;
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in all directories"); LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.totalFileCount << " files in all directories");
} }
} // namespace lms::scanner } // namespace lms::scanner
@@ -1,199 +0,0 @@
/*
* Copyright (C) 2023 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 "ScanStepRemoveOrphanDbFiles.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
namespace lms::scanner
{
using namespace db;
namespace
{
constexpr std::size_t batchSize = 100;
template<typename T>
void removeOrphanEntries(Session& session, bool& abortScan)
{
using IdType = typename T::IdType;
RangeResults<IdType> entries;
while (!abortScan)
{
{
auto transaction{ session.createReadTransaction() };
entries = T::findOrphanIds(session, Range{ 0, batchSize });
};
{
auto transaction{ session.createWriteTransaction() };
for (const IdType objectId : entries.results)
{
if (abortScan)
break;
typename T::pointer entry{ T::find(session, objectId) };
entry.remove();
}
}
if (!entries.moreResults)
break;
}
}
} // namespace
void ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
{
removeOrphanTracks(context);
removeOrphanClusters();
removeOrphanClusterTypes();
removeOrphanArtists();
removeOrphanReleases();
}
void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context)
{
using namespace db;
if (_abortScan)
return;
Session& session{ _db.getTLSSession() };
LMS_LOG(DBUPDATER, DEBUG, "Checking tracks to be removed...");
{
auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = Track::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " tracks to be checked...");
// TODO handle only files in context.directory?
std::vector<Track::pointer> tracksToRemove;
TrackId lastCheckedTrackID;
bool endReached{};
while (!endReached)
{
if (_abortScan)
break;
tracksToRemove.clear();
{
auto transaction{ session.createReadTransaction() };
endReached = true;
Track::find(session, lastCheckedTrackID, batchSize, [&](const Track::pointer& track) {
endReached = false;
if (!checkFile(track->getAbsoluteFilePath()))
tracksToRemove.push_back(track);
context.currentStepStats.processedElems++;
});
}
if (!tracksToRemove.empty())
{
auto transaction{ session.createWriteTransaction() };
for (Track::pointer& track : tracksToRemove)
{
track.remove();
context.stats.deletions++;
}
}
_progressCallback(context.currentStepStats);
}
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.processedElems << " tracks checked!");
}
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan clusters...");
removeOrphanEntries<db::Cluster>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanClusterTypes()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan cluster types...");
removeOrphanEntries<db::ClusterType>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
removeOrphanEntries<db::Artist>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan releases...");
removeOrphanEntries<db::Release>(_db.getTLSSession(), _abortScan);
}
bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p)
{
try
{
// For each track, make sure the the file still exists
// and still belongs to a media directory
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
return false;
}
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
}))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
return false;
}
if (!core::pathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
return false;
}
return true;
}
catch (std::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
return false;
}
}
} // namespace lms::scanner
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2023 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 "ScanStepRemoveOrphanedDbEntries.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
namespace lms::scanner
{
using namespace db;
namespace
{
constexpr std::size_t batchSize = 100;
template<typename T>
void removeOrphanedEntries(Session& session, bool& abortScan)
{
using IdType = typename T::IdType;
RangeResults<IdType> entries;
while (!abortScan)
{
{
auto transaction{ session.createReadTransaction() };
entries = T::findOrphanIds(session, Range{ 0, batchSize });
};
{
auto transaction{ session.createWriteTransaction() };
for (const IdType objectId : entries.results)
{
if (abortScan)
break;
typename T::pointer entry{ T::find(session, objectId) };
entry.remove();
}
}
if (!entries.moreResults)
break;
}
}
} // namespace
void ScanStepRemoveOrphanedDbEntries::process(ScanContext& context)
{
auto& session{ _db.getTLSSession() };
{
auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = 0;
context.currentStepStats.totalElems += Cluster::getCount(session);
context.currentStepStats.totalElems += ClusterType::getCount(session);
context.currentStepStats.totalElems += Artist::getCount(session);
context.currentStepStats.totalElems += Release::getCount(session);
context.currentStepStats.totalElems += Directory::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " database entries to be checked...");
removeOrphanedClusters();
removeOrphanedClusterTypes();
removeOrphanedArtists();
removeOrphanedReleases();
removeOrphanedDirectories();
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusters()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned clusters...");
removeOrphanedEntries<db::Cluster>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusterTypes()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned cluster types...");
removeOrphanedEntries<db::ClusterType>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists...");
removeOrphanedEntries<db::Artist>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleases()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases...");
removeOrphanedEntries<db::Release>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedDirectories()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases...");
removeOrphanedEntries<db::Directory>(_db.getTLSSession(), _abortScan);
}
} // namespace lms::scanner
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 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 "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepRemoveOrphanedDbEntries : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
core::LiteralString getStepName() const override { return "Remove orphaned DB entries"; }
ScanStep getStep() const override { return ScanStep::RemoveOrphanedDbEntries; }
void process(ScanContext& context) override;
void removeOrphanedClusters();
void removeOrphanedClusterTypes();
void removeOrphanedArtists();
void removeOrphanedReleases();
void removeOrphanedDirectories();
};
} // namespace lms::scanner
@@ -1,347 +0,0 @@
/*
* 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
@@ -1,88 +0,0 @@
/*
* Copyright (C) 2023 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 <condition_variable>
#include <deque>
#include <filesystem>
#include <mutex>
#include <span>
#include <string>
#include <vector>
#include "core/IOContextRunner.hpp"
#include "metadata/IParser.hpp"
#include "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepScanAudioFiles : public ScanStepBase
{
public:
ScanStepScanAudioFiles(InitParams& initParams);
private:
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);
struct MetaDataScanResult
{
std::filesystem::path path;
std::unique_ptr<metadata::Track> trackMetaData;
};
void processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
std::unique_ptr<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse;
class MetadataScanQueue
{
public:
MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort);
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
void pushScanRequest(const std::filesystem::path& path);
std::size_t getResultsCount() const;
size_t popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount);
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
private:
metadata::IParser& _metadataParser;
boost::asio::io_context _scanContext;
core::IOContextRunner _scanContextRunner;
mutable std::mutex _mutex;
std::size_t _ongoingScanCount{};
std::deque<MetaDataScanResult> _scanResults;
std::condition_variable _condVar;
bool& _abort;
};
MetadataScanQueue _metadataScanQueue;
std::deque<MetaDataScanResult> _metaDataScanResults;
};
} // namespace lms::scanner
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "ScanStepScanAudioFiles.hpp" #include "ScanStepScanFiles.hpp"
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
@@ -27,6 +27,8 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -102,6 +104,22 @@ namespace lms::scanner
return res; return res;
} }
Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const std::filesystem::path& rootPath)
{
Directory::pointer directory{ Directory::find(session, path) };
if (!directory)
{
Directory::pointer parentDirectory;
if (path != rootPath)
parentDirectory = getOrCreateDirectory(session, path.parent_path(), rootPath);
directory = session.create<Directory>(path);
directory.modify()->setParent(parentDirectory);
}
return directory;
}
Artist::pointer createArtist(Session& session, const metadata::Artist& artistInfo) Artist::pointer createArtist(Session& session, const metadata::Artist& artistInfo)
{ {
Artist::pointer artist{ session.create<Artist>(artistInfo.name) }; Artist::pointer artist{ session.create<Artist>(artistInfo.name) };
@@ -301,98 +319,16 @@ namespace lms::scanner
} }
} // namespace } // namespace
ScanStepScanAudioFiles::MetadataScanQueue::MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort) ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
: _metadataParser{ parser }
, _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" }
, _abort{ abort }
{
}
void ScanStepScanAudioFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path)
{
{
std::scoped_lock lock{ _mutex };
_ongoingScanCount += 1;
}
_scanContext.post([=, this] {
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "AudioFileParseJob");
std::unique_ptr<metadata::Track> track;
if (_abort)
{
std::scoped_lock lock{ _mutex };
_ongoingScanCount -= 1;
}
else
{
try
{
track = _metadataParser.parse(path);
}
catch (const metadata::Exception& e)
{
LMS_LOG(DBUPDATER, INFO, "Failed to parse '" << path.string() << "'");
}
{
std::scoped_lock lock{ _mutex };
_scanResults.emplace_back(MetaDataScanResult{ std::move(path), std::move(track) });
_ongoingScanCount -= 1;
}
}
_condVar.notify_all();
});
}
std::size_t ScanStepScanAudioFiles::MetadataScanQueue::getResultsCount() const
{
std::scoped_lock lock{ _mutex };
return _scanResults.size();
}
size_t ScanStepScanAudioFiles::MetadataScanQueue::popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount)
{
results.clear();
results.reserve(maxCount);
{
std::scoped_lock lock{ _mutex };
while (results.size() < maxCount && !_scanResults.empty())
{
results.push_back(std::move(_scanResults.front()));
_scanResults.pop_front();
}
}
return results.size();
}
void ScanStepScanAudioFiles::MetadataScanQueue::wait(std::size_t maxScanRequestCount)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
std::unique_lock lock{ _mutex };
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
}
ScanStepScanAudioFiles::ScanStepScanAudioFiles(InitParams& initParams)
: ScanStepBase{ initParams } : ScanStepBase{ initParams }
, _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib , _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib
, _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan } , _fileScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan }
{ {
LMS_LOG(DBUPDATER, INFO, "Using " << _metadataScanQueue.getThreadCount() << " thread(s) for scanning file metadata"); LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
} }
void ScanStepScanAudioFiles::process(ScanContext& context) void ScanStepScanFiles::process(ScanContext& context)
{ {
const std::size_t scanQueueMaxScanRequestCount{ 100 * _metadataScanQueue.getThreadCount() };
const std::size_t processMetaDataBatchSize{ 5 };
{ {
std::vector<std::string> tagsToParse{ _extraTagsToParse }; std::vector<std::string> tagsToParse{ _extraTagsToParse };
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags)); tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
@@ -401,56 +337,77 @@ namespace lms::scanner
_metadataParser->setDefaultTagDelimiters(_settings.defaultTagDelimiters); _metadataParser->setDefaultTagDelimiters(_settings.defaultTagDelimiters);
} }
std::vector<MetaDataScanResult> scanResults; context.currentStepStats.totalElems = context.stats.totalFileCount;
context.currentStepStats.totalElems = context.stats.filesScanned;
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries) for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
{ process(context, mediaLibrary);
core::pathUtils::exploreFilesRecursive( }
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
if (_abortScan) void ScanStepScanFiles::process(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary)
return false; {
const std::size_t scanQueueMaxScanRequestCount{ 100 * _fileScanQueue.getThreadCount() };
const std::size_t processFileResultsBatchSize{ 5 };
if (ec) std::vector<FileScanResult> scanResults;
core::pathUtils::exploreFilesRecursive(
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
if (_abortScan)
return false;
if (ec)
{
LMS_LOG(DBUPDATER, ERROR, "Cannot scan file '" << path.string() << "': " << ec.message());
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
}
else
{
bool fileToProcess{};
if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions))
{ {
LMS_LOG(DBUPDATER, ERROR, "Cannot process entry '" << path.string() << "': " << ec.message()); fileToProcess = true;
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() }); if (checkAudioFileNeedScan(context, path, mediaLibrary))
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::AudioFile);
} }
else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions))
{ {
if (checkFileNeedScan(context, path, mediaLibrary)) fileToProcess = true;
_metadataScanQueue.pushScanRequest(path); if (checkImageFileNeedScan(context, path))
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::ImageFile);
}
if (fileToProcess)
{
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats); _progressCallback(context.currentStepStats);
} }
}
while (_metadataScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2)) while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2))
{ {
_metadataScanQueue.popResults(scanResults, processMetaDataBatchSize); _fileScanQueue.popResults(scanResults, processFileResultsBatchSize);
processMetaDataScanResults(context, scanResults, mediaLibrary); processFileScanResults(context, scanResults, mediaLibrary);
} }
_metadataScanQueue.wait(scanQueueMaxScanRequestCount); _fileScanQueue.wait(scanQueueMaxScanRequestCount);
return true; return true;
}, },
&excludeDirFileName); &excludeDirFileName);
_metadataScanQueue.wait(); _fileScanQueue.wait();
while (!_abortScan && _metadataScanQueue.popResults(scanResults, processMetaDataBatchSize) > 0) while (!_abortScan && _fileScanQueue.popResults(scanResults, processFileResultsBatchSize) > 0)
processMetaDataScanResults(context, scanResults, mediaLibrary); processFileScanResults(context, scanResults, mediaLibrary);
}
} }
bool ScanStepScanAudioFiles::checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo) bool ScanStepScanFiles::checkAudioFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{ {
ScanStats& stats{ context.stats }; ScanStats& stats{ context.stats };
Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) }; const Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) };
// Should rarely fail as we are currently iterating it // Should rarely fail as we are currently iterating it
if (!lastWriteTime.isValid()) if (!lastWriteTime.isValid())
{ {
@@ -498,35 +455,77 @@ namespace lms::scanner
return true; // need to scan return true; // need to scan
} }
void ScanStepScanAudioFiles::processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo) bool ScanStepScanFiles::checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file)
{
ScanStats& stats{ context.stats };
const Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) };
// Should rarely fail as we are currently iterating it
if (!lastWriteTime.isValid())
{
stats.skips++;
return false;
}
if (!context.scanOptions.fullScan)
{
db::Session& dbSession{ _db.getTLSSession() };
auto transaction{ _db.getTLSSession().createReadTransaction() };
const db::Image::pointer image{ db::Image::find(dbSession, file) };
if (image && image->getLastWriteTime() == lastWriteTime)
{
stats.skips++;
return false;
}
}
return true; // need to scan
}
void ScanStepScanFiles::processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{ {
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults"); LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults");
db::Session& dbSession{ _db.getTLSSession() }; db::Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createWriteTransaction() }; auto transaction{ dbSession.createWriteTransaction() };
for (const MetaDataScanResult& scanResult : scanResults) for (const FileScanResult& scanResult : scanResults)
{ {
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessScanResult");
if (_abortScan) if (_abortScan)
return; return;
if (scanResult.trackMetaData) if (const AudioFileScanData * scanData{ std::get_if<AudioFileScanData>(&scanResult.scanData) })
{ {
context.stats.scans++; if (metadata::Track * track{ scanData->get() })
{
processFileMetaData(context, scanResult.path, *scanResult.trackMetaData, libraryInfo); context.stats.scans++;
processAudioFileScanData(context, scanResult.path, *track, libraryInfo);
}
else
{
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotReadAudioFile);
}
} }
else else if (const ImageFileScanData * scanData{ std::get_if<ImageFileScanData>(&scanResult.scanData) })
{ {
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotParseFile); if (scanData->has_value())
{
context.stats.scans++;
processImageFileScanData(context, scanResult.path, scanData->value(), libraryInfo);
}
else
{
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotReadImageFile);
}
} }
} }
} }
void ScanStepScanAudioFiles::processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo) void ScanStepScanFiles::processAudioFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{ {
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
ScanStats& stats{ context.stats }; ScanStats& stats{ context.stats };
const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) }; const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) };
@@ -637,6 +636,8 @@ namespace lms::scanner
track.modify()->setLastWriteTime(fileInfo->lastWriteTime); track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this
track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory));
track.modify()->clearArtistLinks(); track.modify()->clearArtistLinks();
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata.artists, false)) for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata.artists, false))
@@ -712,12 +713,57 @@ namespace lms::scanner
if (added) if (added)
{ {
LMS_LOG(DBUPDATER, DEBUG, "Added '" << file.string() << "'"); LMS_LOG(DBUPDATER, DEBUG, "Added audio file '" << file.string() << "'");
stats.additions++; stats.additions++;
} }
else else
{ {
LMS_LOG(DBUPDATER, DEBUG, "Updated '" << file.string() << "'"); LMS_LOG(DBUPDATER, DEBUG, "Updated audio file '" << file.string() << "'");
stats.updates++;
}
}
void ScanStepScanFiles::processImageFileScanData(ScanContext& context, const std::filesystem::path& file, const ImageInfo& imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessImageScanData");
ScanStats& stats{ context.stats };
const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) };
if (!fileInfo)
{
stats.skips++;
return;
}
db::Session& dbSession{ _db.getTLSSession() };
db::Image::pointer image{ db::Image::find(dbSession, file) };
bool added;
if (!image)
{
image = dbSession.create<db::Image>(file);
added = true;
}
else
{
added = false;
}
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
image.modify()->setFileSize(fileInfo->fileSize);
image.modify()->setHeight(imageInfo.height);
image.modify()->setWidth(imageInfo.width);
image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory));
if (added)
{
LMS_LOG(DBUPDATER, DEBUG, "Added image '" << file.string() << "'");
stats.additions++;
}
else
{
LMS_LOG(DBUPDATER, DEBUG, "Updated image '" << file.string() << "'");
stats.updates++; stats.updates++;
} }
} }
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 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 <span>
#include <string>
#include <vector>
#include "metadata/IParser.hpp"
#include "FileScanQueue.hpp"
#include "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepScanFiles : public ScanStepBase
{
public:
ScanStepScanFiles(InitParams& initParams);
private:
ScanStep getStep() const override { return ScanStep::ScanFiles; }
core::LiteralString getStepName() const override { return "Scan files"; }
void process(ScanContext& context) override;
void process(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary);
bool checkAudioFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo);
bool checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file);
void processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processAudioFileScanData(ScanContext& context, const std::filesystem::path& path, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processImageFileScanData(ScanContext& context, const std::filesystem::path& path, const ImageInfo& imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo);
std::unique_ptr<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse;
FileScanQueue _fileScanQueue;
};
} // namespace lms::scanner
@@ -29,15 +29,17 @@
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/ScanSettings.hpp" #include "database/ScanSettings.hpp"
#include "database/TrackFeatures.hpp" #include "database/TrackFeatures.hpp"
#include "image/Image.hpp"
#include "ScanStepCheckDuplicatedDbFiles.hpp" #include "ScanStepAssociateArtistImages.hpp"
#include "ScanStepCheckForDuplicatedFiles.hpp"
#include "ScanStepCheckForRemovedFiles.hpp"
#include "ScanStepCompact.hpp" #include "ScanStepCompact.hpp"
#include "ScanStepComputeClusterStats.hpp" #include "ScanStepComputeClusterStats.hpp"
#include "ScanStepDiscoverFiles.hpp" #include "ScanStepDiscoverFiles.hpp"
#include "ScanStepOptimize.hpp" #include "ScanStepOptimize.hpp"
#include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepRemoveOrphanedDbEntries.hpp"
#include "ScanStepScanArtistImages.hpp" #include "ScanStepScanFiles.hpp"
#include "ScanStepScanAudioFiles.hpp"
namespace lms::scanner namespace lms::scanner
{ {
@@ -340,13 +342,14 @@ namespace lms::scanner
// Order is important // Order is important
_scanSteps.clear(); _scanSteps.clear();
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepScanAudioFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepScanArtistImages>(params)); _scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params)); _scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params)); _scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
_scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params)); _scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
_scanSteps.push_back(std::make_unique<ScanStepCheckDuplicatedDbFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
} }
ScannerSettings ScannerService::readSettings() ScannerSettings ScannerService::readSettings()
@@ -364,9 +367,16 @@ namespace lms::scanner
newSettings.updatePeriod = scanSettings->getUpdatePeriod(); newSettings.updatePeriod = scanSettings->getUpdatePeriod();
{ {
const auto fileExtensions{ scanSettings->getAudioFileExtensions() }; const auto audioFileExtensions{ scanSettings->getAudioFileExtensions() };
newSettings.supportedExtensions.reserve(fileExtensions.size()); newSettings.supportedAudioFileExtensions.reserve(audioFileExtensions.size());
std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions), std::transform(std::cbegin(audioFileExtensions), std::end(audioFileExtensions), std::back_inserter(newSettings.supportedAudioFileExtensions),
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
}
{
const auto imageFileExtensions{ image::getSupportedFileExtensions() };
newSettings.supportedImageFileExtensions.reserve(imageFileExtensions.size());
std::transform(std::cbegin(imageFileExtensions), std::end(imageFileExtensions), std::back_inserter(newSettings.supportedImageFileExtensions),
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; }); [](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
} }
@@ -35,7 +35,8 @@ namespace lms::scanner
std::size_t scanVersion{}; std::size_t scanVersion{};
Wt::WTime startTime; Wt::WTime startTime;
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never }; db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
std::vector<std::filesystem::path> supportedExtensions; std::vector<std::filesystem::path> supportedAudioFileExtensions;
std::vector<std::filesystem::path> supportedImageFileExtensions;
bool skipDuplicateMBID{}; bool skipDuplicateMBID{};
std::vector<std::string> extraTags; std::vector<std::string> extraTags;
std::vector<std::string> artistTagDelimiters; std::vector<std::string> artistTagDelimiters;
@@ -30,10 +30,11 @@ namespace lms::scanner
{ {
enum class ScanErrorType enum class ScanErrorType
{ {
CannotReadFile, // cannot read file CannotReadFile, // cannot read file
CannotParseFile, // cannot parse file CannotReadAudioFile, // cannot parse audio file
NoAudioTrack, // no audio track found CannotReadImageFile, // cannot parse image file
BadDuration, // bad duration NoAudioTrack, // no audio track found
BadDuration, // bad duration
}; };
enum class DuplicateReason enum class DuplicateReason
@@ -60,18 +61,19 @@ namespace lms::scanner
// Alphabetical order // Alphabetical order
enum class ScanStep enum class ScanStep
{ {
CheckForMissingFiles, AssociateArtistImages,
CheckForDuplicateFiles, CheckForDuplicatedFiles,
CheckForRemovedFiles,
ComputeClusterStats, ComputeClusterStats,
Compact, Compact,
DiscoverFiles, DiscoverFiles,
FetchTrackFeatures, FetchTrackFeatures,
Optimize, Optimize,
ReloadSimilarityEngine, ReloadSimilarityEngine,
ScanArtistImages, RemoveOrphanedDbEntries,
ScanAudioFiles, ScanFiles,
}; };
static inline constexpr unsigned ScanProgressStepCount{ 9 }; static inline constexpr unsigned ScanProgressStepCount{ 11 };
// reduced scan stats // reduced scan stats
struct ScanStepStats struct ScanStepStats
@@ -92,7 +94,7 @@ namespace lms::scanner
Wt::WDateTime startTime; Wt::WDateTime startTime;
Wt::WDateTime stopTime; Wt::WDateTime stopTime;
std::size_t filesScanned{}; // Total number of files scanned (estimated) std::size_t totalFileCount{}; // Total number of files (estimated)
std::size_t skips{}; // no change since last scan std::size_t skips{}; // no change since last scan
std::size_t scans{}; // actually scanned filed std::size_t scans{}; // actually scanned filed
-16
View File
@@ -76,22 +76,6 @@ namespace lms::core::stringUtils
return std::nullopt; return std::nullopt;
} }
template<>
std::optional<db::ImageId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
if (values[0] != "im")
return std::nullopt;
if (const auto value{ core::stringUtils::readAs<db::ImageId::ValueType>(values[1]) })
return db::ImageId{ *value };
return std::nullopt;
}
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str) std::optional<db::MediaLibraryId> readAs(std::string_view str)
{ {
-5
View File
@@ -21,7 +21,6 @@
#include "core/String.hpp" #include "core/String.hpp"
#include "database/ArtistId.hpp" #include "database/ArtistId.hpp"
#include "database/ImageId.hpp"
#include "database/MediaLibraryId.hpp" #include "database/MediaLibraryId.hpp"
#include "database/ReleaseId.hpp" #include "database/ReleaseId.hpp"
#include "database/TrackId.hpp" #include "database/TrackId.hpp"
@@ -34,7 +33,6 @@ namespace lms::api::subsonic
}; };
std::string idToString(db::ArtistId id); std::string idToString(db::ArtistId id);
std::string idToString(db::ImageId id);
std::string idToString(db::MediaLibraryId id); std::string idToString(db::MediaLibraryId id);
std::string idToString(db::ReleaseId id); std::string idToString(db::ReleaseId id);
std::string idToString(db::TrackId id); std::string idToString(db::TrackId id);
@@ -51,9 +49,6 @@ namespace lms::core::stringUtils
template<> template<>
std::optional<db::ArtistId> readAs(std::string_view str); std::optional<db::ArtistId> readAs(std::string_view str);
template<>
std::optional<db::ImageId> readAs(std::string_view str);
template<> template<>
std::optional<db::MediaLibraryId> readAs(std::string_view str); std::optional<db::MediaLibraryId> readAs(std::string_view str);
@@ -39,7 +39,7 @@ namespace lms::api::subsonic::Scan
{ {
std::size_t count{}; std::size_t count{};
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanAudioFiles) if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanFiles)
count = scanStatus.currentScanStepStats->processedElems; count = scanStatus.currentScanStepStats->processedElems;
statusResponse.setAttribute("count", count); statusResponse.setAttribute("count", count);
+25 -22
View File
@@ -119,8 +119,10 @@ namespace lms::ui
{ {
case scanner::ScanErrorType::CannotReadFile: case scanner::ScanErrorType::CannotReadFile:
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file"); return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file");
case scanner::ScanErrorType::CannotParseFile: case scanner::ScanErrorType::CannotReadAudioFile:
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-parse-file"); return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-audio-file");
case scanner::ScanErrorType::CannotReadImageFile:
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file");
case scanner::ScanErrorType::NoAudioTrack: case scanner::ScanErrorType::NoAudioTrack:
return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track"); return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
case scanner::ScanErrorType::BadDuration: case scanner::ScanErrorType::BadDuration:
@@ -267,58 +269,59 @@ namespace lms::ui
switch (stepStats.currentStep) switch (stepStats.currentStep)
{ {
case ScanStep::CheckForDuplicateFiles: case ScanStep::AssociateArtistImages:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-artist-images")
.arg(stepStats.progress()));
break;
case ScanStep::CheckForDuplicatedFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
.arg(stepStats.processedElems)); .arg(stepStats.processedElems));
break; break;
case scanner::ScanStep::CheckForMissingFiles: case ScanStep::CheckForRemovedFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-missing-files") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-removed-files")
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::Compact: case ScanStep::Compact:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compact")); _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compact"));
break; break;
case scanner::ScanStep::ComputeClusterStats: case ScanStep::ComputeClusterStats:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats")
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::DiscoverFiles: case ScanStep::DiscoverFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files")
.arg(stepStats.processedElems)); .arg(stepStats.processedElems));
break; break;
case scanner::ScanStep::FetchTrackFeatures: case ScanStep::FetchTrackFeatures:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features")
.arg(stepStats.processedElems) .arg(stepStats.processedElems)
.arg(stepStats.totalElems) .arg(stepStats.totalElems)
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::Optimize: case ScanStep::Optimize:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-optimize") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-optimize")
.arg(stepStats.processedElems)
.arg(stepStats.totalElems)
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::ReloadSimilarityEngine: case ScanStep::RemoveOrphanedDbEntries:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-removing-orphaned-entries")
.arg(stepStats.progress()));
break;
case ScanStep::ReloadSimilarityEngine:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine")
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case scanner::ScanStep::ScanArtistImages: case ScanStep::ScanFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-artist-images") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files")
.arg(stepStats.processedElems)
.arg(stepStats.totalElems)
.arg(stepStats.progress()));
break;
case scanner::ScanStep::ScanAudioFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-audio-files")
.arg(stepStats.processedElems) .arg(stepStats.processedElems)
.arg(stepStats.totalElems) .arg(stepStats.totalElems)
.arg(stepStats.progress())); .arg(stepStats.progress()));