Merge branch 'develop' for release v3.55.0

This commit is contained in:
emeric
2024-07-06 23:33:07 +02:00
77 changed files with 2574 additions and 863 deletions
+2 -2
View File
@@ -33,7 +33,7 @@ Please refer to [Deployment](#deployment) for further configuration options.
__Note__: this installation process and the default values of the configuration files have been written for _Debian Bookworm_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. __Note__: this installation process and the default values of the configuration files have been written for _Debian Bookworm_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution.
### Build dependencies ### Build dependencies
__Notes__: __Notes__:
* a C++17 compiler is needed * a C++20 compiler is needed
* ffmpeg version 4 minimum is required * ffmpeg version 4 minimum is required
```sh ```sh
apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev libgtest-dev libarchive-dev apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev libgtest-dev libarchive-dev
@@ -41,7 +41,7 @@ apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libav
__Notes__: __Notes__:
* libpam0g-dev is optional (only for using PAM authentication) * libpam0g-dev is optional (only for using PAM authentication)
* libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM) * libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM)
You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).</br> You also need _Wt4_, which is not packaged on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).</br>
No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended). No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended).
### Build ### Build
Get the latest stable release and build it: Get the latest stable release and build it:
+7 -4
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,15 +109,17 @@
<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-files">Scanning files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message> <message id="Lms.Admin.ScannerController.step-status">Step status</message>
<!--Tracing--> <!--Tracing-->
+7 -4
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,15 +109,17 @@
<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 supprimés... {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-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Retrait des entrées orphelines: {1} entrées...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} ({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-->
+7 -4
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,15 +109,17 @@
<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-files">Scansione files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione dei file: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message> <message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
<!--Tracing--> <!--Tracing-->
+7 -4
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,9 +133,10 @@
<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-files">Skanowanie plików: {1}/{2} plików ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Usuwanie osieroconych wpisów: {1} wpisów...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} ({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 -2
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,13 +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-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message> <message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
+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
@@ -78,6 +78,7 @@ namespace lms::core::stringUtils::tests
}; };
TestCase tests[]{ TestCase tests[]{
{ "", "", { "" } },
{ "abc", "", { "abc" } }, { "abc", "", { "abc" } },
{ "abc", "-", { "abc" } }, { "abc", "-", { "abc" } },
{ "abc", "b", { "a", "c" } }, { "abc", "b", { "a", "c" } },
+2
View File
@@ -3,6 +3,8 @@ 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/Listen.cpp impl/Listen.cpp
impl/MediaLibrary.cpp impl/MediaLibrary.cpp
impl/Migration.cpp impl/Migration.cpp
+15 -3
View File
@@ -22,6 +22,8 @@
#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/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
@@ -221,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)
@@ -273,6 +275,11 @@ namespace lms::db
utils::forEachQueryRangeResult(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
ObjectPtr<Image> Artist::getImage() const
{
return ObjectPtr<Image>{ _image.lock() };
}
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{ {
assert(session()); assert(session());
@@ -358,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
+115
View File
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "database/Image.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "database/Artist.hpp"
#include "database/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<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)
{
return session.getDboSession()->add(std::unique_ptr<Image>{ new Image{ p } });
}
std::size_t Image::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM image"));
}
Image::pointer Image::find(Session& session, ImageId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->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
+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)
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013-2016 Emeric Poupon * Copyright (C) 2024 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
+380 -223
View File
@@ -35,7 +35,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 59 }; static constexpr Version LMS_DATABASE_VERSION{ 61 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -86,11 +86,13 @@ namespace lms::db::Migration
Db& _db; Db& _db;
}; };
static void migrateFromV33(Session& session) namespace
{ {
// remove name from track_artist_link void migrateFromV33(Session& session)
// Drop Auth mode {
session.getDboSession()->execute(R"( // remove name from track_artist_link
// Drop Auth mode
session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -101,49 +103,49 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
); );
))"); ))");
session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link");
session.getDboSession()->execute("DROP TABLE track_artist_link"); session.getDboSession()->execute("DROP TABLE track_artist_link");
session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
} }
static void migrateFromV34(Session& session) void migrateFromV34(Session& session)
{ {
// Add scrobbling state // Add scrobbling state
// By default, everything needs to be sent // By default, everything needs to be sent
session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/ 0)) + ")");
} }
static void migrateFromV35(Session& session) void migrateFromV35(Session& session)
{ {
// Add creattion/last modif date time for tracklists // Add creattion/last modif date time for tracklists
session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT");
session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
} }
static void migrateFromV36(Session& session) void migrateFromV36(Session& session)
{ {
// Increased precision for track durations (now in milliseconds instead of secodns) // Increased precision for track durations (now in milliseconds instead of secodns)
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV37(Session& session) void migrateFromV37(Session& session)
{ {
// Support Performer tags (via subtypes) // Support Performer tags (via subtypes)
session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT"); session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV38(Session& session) void migrateFromV38(Session& session)
{ {
// migrate release-specific tags from Track to Release // migrate release-specific tags from Track to Release
session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER");
session.getDboSession()->execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -170,178 +172,178 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
); );
))"); ))");
session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track");
session.getDboSession()->execute("DROP TABLE track"); session.getDboSession()->execute("DROP TABLE track");
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV39(Session& session) void migrateFromV39(Session& session)
{ {
// add release type // add release type
session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER");
session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV40(Session& session) void migrateFromV40(Session& session)
{ {
// add artist_display_name in Release and Track // add artist_display_name in Release and Track
session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''");
session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV41(Session& session) void migrateFromV41(Session& session)
{ {
// add artist_display_name in Release and Track // add artist_display_name in Release and Track
session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format");
session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate");
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
} }
static void migrateFromV42(Session& session) void migrateFromV42(Session& session)
{ {
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx");
session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx");
// New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service)
session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend");
session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER"); session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER");
session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend");
session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend"); session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend");
} }
static void migrateFromV43(Session& session) void migrateFromV43(Session& session)
{ {
// add counts in genre table // add counts in genre table
session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER"); session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER");
session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER"); session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV44(Session& session) void migrateFromV44(Session& session)
{ {
// add bitrate // add bitrate
session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV45(Session& session) void migrateFromV45(Session& session)
{ {
// add subsonic_enable_transcoding_by_default, default is disabled // add subsonic_enable_transcoding_by_default, default is disabled
session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")"); session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/ 0)) + ")");
} }
void migrateFromV46(Session& session) void migrateFromV46(Session& session)
{ {
// add extra tags to parse // add extra tags to parse
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text not null "name" text not null
);)"); );)");
session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type");
session.getDboSession()->execute("DROP TABLE cluster_type"); session.getDboSession()->execute("DROP TABLE cluster_type");
session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type");
session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV47(Session& session) void migrateFromV47(Session& session)
{ {
// release type, new way // release type, new way
session.getDboSession()->execute("ALTER TABLE release DROP primary_type"); session.getDboSession()->execute("ALTER TABLE release DROP primary_type");
session.getDboSession()->execute("ALTER TABLE release DROP secondary_types"); session.getDboSession()->execute("ALTER TABLE release DROP secondary_types");
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text not null))"); "name" text not null))");
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" (
"release_type_id" bigint, "release_type_id" bigint,
"release_id" bigint, "release_id" bigint,
primary key ("release_type_id", "release_id"), primary key ("release_type_id", "release_id"),
constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred, constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))"); ))");
session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))");
session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV48(Session& session) void migrateFromV48(Session& session)
{ {
// Regression for the extra tags not being parsed // Regression for the extra tags not being parsed
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV49(Session& session) void migrateFromV49(Session& session)
{ {
// Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing)
session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER"); session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER");
session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER"); session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV50(Session& session) void migrateFromV50(Session& session)
{ {
// MediaLibrary support // MediaLibrary support
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"path" text not null, "path" text not null,
"name" text not null "name" text not null
))"); ))");
const int scanSettingsId{ session.getDboSession()->query<int>("SELECT id FROM scan_settings") }; const int scanSettingsId{ session.getDboSession()->query<int>("SELECT id FROM scan_settings") };
// Convert the existing media_directory in the scan_settings table to a media_library with id '1' // Convert the existing media_directory in the scan_settings table to a media_library with id '1'
session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name")
SELECT 1, 0, s_s.media_directory, "Main" SELECT 1, 0, s_s.media_directory, "Main"
FROM scan_settings s_s FROM scan_settings s_s
WHERE id = ?)") WHERE id = ?)")
.bind(scanSettingsId); .bind(scanSettingsId);
// Remove the outdated column in scan_settings // Remove the outdated column in scan_settings
session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory"); session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory");
// Add the media_library column in tracks, with id '1' // Add the media_library column in tracks, with id '1'
session.getDboSession()->execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -374,8 +376,8 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred
))"); ))");
// Migrate data, with the new media_library_id field set to 1 // Migrate data, with the new media_library_id field set to 1
session.getDboSession()->execute(R"(INSERT INTO track_backup session.getDboSession()->execute(R"(INSERT INTO track_backup
SELECT SELECT
id, id,
version, version,
@@ -405,76 +407,229 @@ SELECT
release_id, release_id,
1 1
FROM track)"); FROM track)");
session.getDboSession()->execute("DROP TABLE track"); session.getDboSession()->execute("DROP TABLE track");
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track"); session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
} }
void migrateFromV51(Session& session) void migrateFromV51(Session& session)
{ {
// Add custom artist tag delimiters, no need to rescan since it has no effect when empty // Add custom artist tag delimiters, no need to rescan since it has no effect when empty
session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''");
session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''");
} }
void migrateFromV52(Session& session) void migrateFromV52(Session& session)
{ {
// Add sort name for releases // Add sort name for releases
session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV53(Session& session) void migrateFromV53(Session& session)
{ {
// Add release group mbid // Add release group mbid
session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV54(Session& session) void migrateFromV54(Session& session)
{ {
// Add file size + relative file path // Add file size + relative file path
session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path"); session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path");
session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)"); session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV55(Session& session) void migrateFromV55(Session& session)
{ {
// Add bitsPerSample, channelCount and sampleRate // Add bitsPerSample, channelCount and sampleRate
session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)"); session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)"); session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)"); session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV56(Session& session) void migrateFromV56(Session& session)
{ {
// Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) };
for (const auto& indexName : indexeNames) for (const auto& indexName : indexeNames)
session.getDboSession()->execute("DROP INDEX " + indexName); session.getDboSession()->execute("DROP INDEX " + indexName);
} }
void migrateFromV57(Session& session) void migrateFromV57(Session& session)
{ {
// useless index, may have been already removed in the previous step // useless index, may have been already removed in the previous step
session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS cluster_name_idx");
} }
void migrateFromV58(Session& session) void migrateFromV58(Session& session)
{ {
// DSF support // DSF support
session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'"); session.getDboSession()->execute("UPDATE scan_settings SET audio_file_extensions = audio_file_extensions || ' .dsf'");
} }
void migrateFromV59(Session& session)
{
// Dedicated image table
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "image" (
"id" integer primary key autoincrement,
"version" integer not null,
"path" text not null,
"stem" text not null,
"file_last_write" text,
"file_size" integer not null,
"width" integer not null,
"height" integer not null,
"artist_id" bigint,
constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
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
bool doDbMigration(Session& session) bool doDbMigration(Session& session)
{ {
@@ -511,6 +666,8 @@ SELECT
{ 56, migrateFromV56 }, { 56, migrateFromV56 },
{ 57, migrateFromV57 }, { 57, migrateFromV57 },
{ 58, migrateFromV58 }, { 58, migrateFromV58 },
{ 59, migrateFromV59 },
{ 60, migrateFromV60 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+12 -8
View File
@@ -45,7 +45,8 @@ namespace lms::db
if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName
|| params.sortMethod == ReleaseSortMethod::LastWritten || params.sortMethod == ReleaseSortMethod::LastWritten
|| params.sortMethod == ReleaseSortMethod::Date || params.sortMethod == ReleaseSortMethod::DateAsc
|| params.sortMethod == ReleaseSortMethod::DateDesc
|| params.sortMethod == ReleaseSortMethod::OriginalDate || params.sortMethod == ReleaseSortMethod::OriginalDate
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc || params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid() || params.writtenAfter.isValid()
@@ -189,8 +190,11 @@ namespace lms::db
case ReleaseSortMethod::LastWritten: case ReleaseSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC"); query.orderBy("t.file_last_write DESC");
break; break;
case ReleaseSortMethod::Date: case ReleaseSortMethod::DateAsc:
query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)), r.name COLLATE NOCASE"); query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) ASC, r.name COLLATE NOCASE");
break;
case ReleaseSortMethod::DateDesc:
query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC, r.name COLLATE NOCASE");
break; break;
case ReleaseSortMethod::OriginalDate: case ReleaseSortMethod::OriginalDate:
query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE"); query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE");
@@ -222,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)
@@ -254,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)
@@ -331,7 +335,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(r.id)", params)); return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params));
} }
std::size_t Release::getDiscCount() const std::size_t Release::getDiscCount() const
+5 -1
View File
@@ -59,7 +59,11 @@ namespace lms::db
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
{ {
return core::stringUtils::splitString(_extraTagsToScan, ';'); std::vector<std::string_view> tags{ core::stringUtils::splitString(_extraTagsToScan, ';') };
if (tags.size() == 1 && tags.front().empty())
tags.clear();
return tags;
} }
std::vector<std::string> ScanSettings::getArtistTagDelimiters() const std::vector<std::string> ScanSettings::getArtistTagDelimiters() const
+13
View File
@@ -26,6 +26,8 @@
#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/Listen.hpp" #include "database/Listen.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
@@ -92,6 +94,8 @@ 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<Listen>("listen"); _session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library"); _session.mapClass<MediaLibrary>("media_library");
_session.mapClass<Release>("release"); _session.mapClass<Release>("release");
@@ -177,6 +181,15 @@ 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_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)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
+9 -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)
@@ -429,6 +430,8 @@ namespace lms::db
query.bind(type); query.bind(type);
query.where("t.id = ?").bind(getId()); query.where("t.id = ?").bind(getId());
query.groupBy("t_a_l.artist_id");
query.orderBy("t_a_l.id");
return utils::fetchQueryResults<Artist::pointer>(query); return utils::fetchQueryResults<Artist::pointer>(query);
} }
@@ -463,6 +466,7 @@ namespace lms::db
query.where("t.id = ?").bind(getId()); query.where("t.id = ?").bind(getId());
query.groupBy("t_a_l.artist_id"); query.groupBy("t_a_l.artist_id");
query.orderBy("t_a_l.id");
return utils::fetchQueryResults(query); return utils::fetchQueryResults(query);
} }
+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)
@@ -44,6 +44,7 @@ namespace lms::db
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Image;
class Release; class Release;
class Session; class Session;
class StarredArtist; class StarredArtist;
@@ -139,6 +140,7 @@ namespace lms::db
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); } std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
ObjectPtr<Image> getImage() const;
// No artistLinkTypes means get them all // No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const; RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
@@ -151,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)
@@ -159,6 +162,7 @@ namespace lms::db
Wt::Dbo::field(a, _sortName, "sort_name"); Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::hasOne(a, _image, "artist");
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
} }
@@ -175,6 +179,7 @@ namespace lms::db
std::string _sortName; std::string _sortName;
std::string _MBID; // Musicbrainz Identifier std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::weak_ptr<Image> _image;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
}; };
@@ -0,0 +1,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)
@@ -0,0 +1,123 @@
/*
* 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 <Wt/WDateTime.h>
#include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp"
#include "database/ImageId.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class Artist;
class Directory;
class Session;
class Image final : public Object<Image, ImageId>
{
public:
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
static std::size_t getCount(Session& session);
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
const std::filesystem::path& getAbsoluteFilePath() const { return _fileAbsolutePath; }
std::string_view getFileStem() const { return _fileStem; }
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
std::size_t getFileSize() const { return _fileSize; }
std::size_t getWidth() const { return _width; }
std::size_t getHeight() const { return _height; }
// setters
void setAbsoluteFilePath(const std::filesystem::path& p);
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setWidth(std::size_t width) { _width = width; }
void setHeight(std::size_t height) { _height = height; }
void setArtist(const ObjectPtr<Artist>& artist) { _artist = getDboPtr(artist); }
void setDirectory(const ObjectPtr<Directory>& directory) { _directory = getDboPtr(directory); }
template<class Action>
void persist(Action& a)
{
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, _fileSize, "file_size");
Wt::Dbo::field(a, _width, "width");
Wt::Dbo::field(a, _height, "height");
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Image(const std::filesystem::path& p);
static pointer create(Session& session, const std::filesystem::path& p);
std::filesystem::path _fileAbsolutePath;
std::string _fileStem;
Wt::WDateTime _fileLastWrite;
int _fileSize{};
int _width{};
int _height{};
Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<Directory> _directory;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(ImageId)
@@ -95,7 +95,7 @@ namespace lms::db
// Can get raw dbo ptr only from Objects // Can get raw dbo ptr only from Objects
template<typename SomeObject> template<typename SomeObject>
static Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) static Wt::Dbo::ptr<SomeObject> getDboPtr(const ObjectPtr<SomeObject>& ptr)
{ {
return ptr._obj; return ptr._obj;
} }
@@ -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;
}; };
+2 -1
View File
@@ -129,7 +129,8 @@ namespace lms::db
Id, Id,
Name, Name,
ArtistNameThenName, ArtistNameThenName,
Date, DateAsc,
DateDesc,
OriginalDate, OriginalDate,
OriginalDateDesc, OriginalDateDesc,
Random, Random,
+2
View File
@@ -4,6 +4,8 @@ add_executable(test-database
Cluster.cpp Cluster.cpp
Common.cpp Common.cpp
DatabaseTest.cpp DatabaseTest.cpp
Directory.cpp
Image.cpp
Listen.cpp Listen.cpp
Migration.cpp Migration.cpp
Release.cpp Release.cpp
+2
View File
@@ -22,6 +22,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Listen.hpp" #include "database/Listen.hpp"
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
@@ -80,6 +81,7 @@ namespace lms::db::tests
EXPECT_EQ(Cluster::getCount(session), 0); EXPECT_EQ(Cluster::getCount(session), 0);
EXPECT_EQ(ClusterType::getCount(session), 0); EXPECT_EQ(ClusterType::getCount(session), 0);
EXPECT_EQ(Listen::getCount(session), 0); EXPECT_EQ(Listen::getCount(session), 0);
EXPECT_EQ(Image::getCount(session), 0);
EXPECT_EQ(MediaLibrary::getCount(session), 0); EXPECT_EQ(MediaLibrary::getCount(session), 0);
EXPECT_EQ(Release::getCount(session), 0); EXPECT_EQ(Release::getCount(session), 0);
EXPECT_EQ(StarredArtist::getCount(session), 0); EXPECT_EQ(StarredArtist::getCount(session), 0);
+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
+101
View File
@@ -0,0 +1,101 @@
/*
* 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"
#include "database/Image.hpp"
namespace lms::db::tests
{
using ScopedDirectory = ScopedEntity<db::Directory>;
using ScopedImage = ScopedEntity<db::Image>;
TEST_F(DatabaseFixture, Image)
{
ScopedImage image{ session, "/path/to/image" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Image::getCount(session), 1);
Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{});
EXPECT_EQ(img->getAbsoluteFilePath(), "/path/to/image");
EXPECT_EQ(img->getFileStem(), "image");
EXPECT_EQ(img->getWidth(), 0);
EXPECT_EQ(img->getHeight(), 0);
EXPECT_EQ(img->getFileSize(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{});
img.modify()->setAbsoluteFilePath("/path/to/another/image2");
img.modify()->setWidth(640);
img.modify()->setHeight(480);
img.modify()->setFileSize(1024 * 1024);
}
{
auto transaction{ session.createReadTransaction() };
Image::pointer img{ Image::find(session, image.getId()) };
ASSERT_NE(img, Image::pointer{});
EXPECT_EQ(img->getAbsoluteFilePath(), "/path/to/another/image2");
EXPECT_EQ(img->getFileStem(), "image2");
EXPECT_EQ(img->getWidth(), 640);
EXPECT_EQ(img->getHeight(), 480);
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
+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
+25 -1
View File
@@ -639,22 +639,36 @@ namespace lms::db::tests
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId())), 0);
} }
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Producer);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1);
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer }));
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
@@ -669,6 +683,7 @@ namespace lms::db::tests
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer })); releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
@@ -849,12 +864,21 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::Date)) }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::DateAsc)) };
ASSERT_EQ(releases.results.size(), 2); ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release2.getId()); EXPECT_EQ(releases.results.front(), release2.getId());
EXPECT_EQ(releases.results.back(), release1.getId()); EXPECT_EQ(releases.results.back(), release1.getId());
} }
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::DateDesc)) };
ASSERT_EQ(releases.results.size(), 2);
EXPECT_EQ(releases.results.front(), release1.getId());
EXPECT_EQ(releases.results.back(), release2.getId());
}
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
+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,64 @@
/*
* 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 "RawImage.hpp"
#include "core/ILogger.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<GraphicsMagick::RawImage>(encodedData, encodedDataSize);
}
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
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
@@ -20,6 +20,7 @@
#include "JPEGImage.hpp" #include "JPEGImage.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "image/Exception.hpp" #include "image/Exception.hpp"
#include "RawImage.hpp" #include "RawImage.hpp"
@@ -28,6 +29,8 @@ namespace lms::image::GraphicsMagick
{ {
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
try try
{ {
Magick::Image image{ rawImage.getMagickImage() }; Magick::Image image{ rawImage.getMagickImage() };
+16 -31
View File
@@ -19,46 +19,19 @@
#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"
#include "core/ITraceLogger.hpp"
#include "image/Exception.hpp" #include "image/Exception.hpp"
#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
@@ -104,10 +77,22 @@ namespace lms::image::GraphicsMagick
} }
} }
ImageSize RawImage::getWidth() const
{
return _image.size().width();
}
ImageSize RawImage::getHeight() const
{
return _image.size().height();
}
void RawImage::resize(ImageSize width) void RawImage::resize(ImageSize width)
{ {
try try
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "Resize");
_image.resize(Magick::Geometry{ static_cast<unsigned int>(width), static_cast<unsigned int>(width) }); _image.resize(Magick::Geometry{ static_cast<unsigned int>(width), static_cast<unsigned int>(width) });
} }
catch (Magick::Exception& e) catch (Magick::Exception& e)
@@ -35,6 +35,9 @@ namespace lms::image::GraphicsMagick
RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
RawImage(const std::filesystem::path& path); RawImage(const std::filesystem::path& path);
ImageSize getWidth() const override;
ImageSize getHeight() const override;
void resize(ImageSize width) override; void resize(ImageSize width) override;
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override; std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
+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)
+3 -2
View File
@@ -33,11 +33,12 @@ namespace lms::image::STB
RawImage(const std::byte* encodedData, std::size_t encodedDataSize); RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
RawImage(const std::filesystem::path& path); RawImage(const std::filesystem::path& path);
ImageSize getWidth() const override;
ImageSize getHeight() const override;
void resize(ImageSize width) override; void resize(ImageSize width) override;
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override; std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
ImageSize getWidth() const;
ImageSize getHeight() const;
const std::byte* getData() const; const std::byte* getData() const;
private: private:
@@ -27,6 +27,10 @@ namespace lms::image
{ {
public: public:
virtual ~IRawImage() = default; virtual ~IRawImage() = default;
virtual ImageSize getWidth() const = 0;
virtual ImageSize getHeight() const = 0;
virtual void resize(ImageSize width) = 0; virtual void resize(ImageSize width) = 0;
virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0; virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0;
}; };
+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);
@@ -172,15 +172,6 @@ namespace lms::metadata
} }
} }
size_t AvFormatTagReader::countTagValues(TagType tag) const
{
size_t count{};
visitTagValues(tag, [&](std::string_view) {
count++;
});
return count;
}
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{ {
auto itTagNames{ tagMapping.find(tag) }; auto itTagNames{ tagMapping.find(tag) };
@@ -37,7 +37,6 @@ namespace lms::metadata
AvFormatTagReader(const AvFormatTagReader&) = delete; AvFormatTagReader(const AvFormatTagReader&) = delete;
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete; AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
size_t countTagValues(TagType tag) const override;
void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
-1
View File
@@ -148,7 +148,6 @@ namespace lms::metadata
virtual ~ITagReader() = default; virtual ~ITagReader() = default;
using TagValueVisitor = std::function<void(std::string_view value)>; using TagValueVisitor = std::function<void(std::string_view value)>;
virtual size_t countTagValues(TagType tag) const = 0;
virtual void visitTagValues(TagType tag, TagValueVisitor visitor) const = 0; virtual void visitTagValues(TagType tag, TagValueVisitor visitor) const = 0;
virtual void visitTagValues(std::string_view tag, TagValueVisitor visitor) const = 0; virtual void visitTagValues(std::string_view tag, TagValueVisitor visitor) const = 0;
+4 -2
View File
@@ -296,10 +296,12 @@ namespace lms::metadata
track.medium = getMedium(tagReader); track.medium = getMedium(tagReader);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters); track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters);
// We consider the artist display name is put in the Artist tag (picard case)
// But to please most users, if we find a custom delimiter in the Artist tag, we construct the artist diplay string with a "nicer" join
if (!_artistTagDelimiters.empty() if (!_artistTagDelimiters.empty()
&& track.artists.size() > 1 && track.artists.size() > 1
&& tagReader.countTagValues(TagType::Artist) <= 1 && getTagValuesAs<std::string>(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1)
&& tagReader.countTagValues(TagType::Artists) <= 1)
{ {
std::vector<std::string_view> artistNames; std::vector<std::string_view> artistNames;
std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; }); std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
@@ -340,15 +340,6 @@ namespace lms::metadata
#endif #endif
} }
size_t TagLibTagReader::countTagValues(TagType tag) const
{
size_t count{};
visitTagValues(tag, [&](std::string_view) {
count++;
});
return count;
}
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{ {
auto itTagNames{ tagMapping.find(tag) }; auto itTagNames{ tagMapping.find(tag) };
@@ -40,7 +40,6 @@ namespace lms::metadata
TagLibTagReader& operator=(const TagLibTagReader&) = delete; TagLibTagReader& operator=(const TagLibTagReader&) = delete;
void computeAudioProperties(); void computeAudioProperties();
size_t countTagValues(TagType tag) const override;
void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
-9
View File
@@ -47,15 +47,6 @@ namespace lms::metadata
{ {
} }
size_t countTagValues(TagType tag) const
{
auto itValues{ _tags.find(tag) };
if (itValues != std::cend(_tags))
return itValues->second.size();
return 0;
}
void visitTagValues(TagType tag, TagValueVisitor visitor) const override void visitTagValues(TagType tag, TagValueVisitor visitor) const override
{ {
auto itValues{ _tags.find(tag) }; auto itValues{ _tags.find(tag) };
+5 -89
View File
@@ -30,6 +30,7 @@
#include "core/Utils.hpp" #include "core/Utils.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
@@ -86,19 +87,6 @@ namespace lms::cover
return res; return res;
} }
std::vector<std::string> constructArtistFileNames()
{
std::vector<std::string> res;
core::Service<core::IConfig>::get()->visitStrings("artist-image-file-names",
[&res](std::string_view fileName) {
res.emplace_back(fileName);
},
{ "artist" });
return res;
}
bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions) bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
{ {
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
@@ -118,7 +106,6 @@ namespace lms::cover
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames{ constructPreferredFileNames() } , _preferredFileNames{ constructPreferredFileNames() }
, _artistFileNames{ constructArtistFileNames() }
{ {
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
@@ -389,87 +376,15 @@ namespace lms::cover
if (artistImage) if (artistImage)
return artistImage; return artistImage;
std::string artistName;
std::string artistMBID;
std::set<std::filesystem::path> releasePaths;
std::set<std::filesystem::path> multiArtistReleasePaths;
{ {
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const Artist::pointer artist{ Artist::find(session, artistId) }; if (const Artist::pointer artist{ Artist::find(session, artistId) })
if (!artist)
return artistImage;
artistName = artist->getName();
if (auto mbid{ artist->getMBID() })
artistMBID = mbid->getAsString();
Track::FindParameters params;
params.setArtist(artistId, { TrackArtistLinkType::ReleaseArtist });
Track::find(session, params, [&](const Track::pointer& track) {
Artist::FindParameters artistFindParams;
artistFindParams.setTrack(track->getId());
artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist);
const auto releaseArtists{ Artist::findIds(session, artistFindParams) };
if (releaseArtists.results.size() == 1)
releasePaths.insert(track->getAbsoluteFilePath().parent_path());
else
multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path());
});
}
std::vector<std::string> artistFileNames;
if (!artistMBID.empty())
artistFileNames.push_back(artistMBID);
artistFileNames.push_back(artistName);
std::vector<std::string> artistFileNamesWithGenericNames{ artistFileNames };
artistFileNamesWithGenericNames.insert(artistFileNamesWithGenericNames.end(), std::cbegin(_artistFileNames), std::cend(_artistFileNames));
// Expect layout like this:
// ReleaseArtist/Release/Tracks'
// /artist-mbid.jpg
// /artist-name.jpg
// /artist.jpg
if (!releasePaths.empty())
{
const std::filesystem::path artistPath{ releasePaths.size() == 1 ? releasePaths.begin()->parent_path() : core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
artistImage = getFromDirectory(artistPath, width, artistFileNamesWithGenericNames, false);
}
// Expect layout like this:
// ReleaseArtist/Release/Tracks'
// /artist-mbid.jpg
// /artist-name.jpg
// /artist.jpg
if (!artistImage)
{
for (const std::filesystem::path& releasePath : releasePaths)
{ {
artistImage = getFromDirectory(releasePath, width, artistFileNamesWithGenericNames, false); if (const db::Image::pointer image{ artist->getImage() })
if (artistImage) artistImage = getFromCoverFile(image->getAbsoluteFilePath(), width);
break;
}
}
// Expect layout like this:
// Only search for the artist's name in the release path, as we can't map a generic name to several artists
// ReleaseArtist/Release/Tracks'
// /artist-name.jpg
// /artist-mbid.jpg
if (!artistImage)
{
for (const std::filesystem::path& releasePath : multiArtistReleasePaths)
{
artistImage = getFromDirectory(releasePath, width, artistFileNames, false);
if (artistImage)
break;
} }
} }
@@ -481,6 +396,7 @@ namespace lms::cover
void CoverService::flushCache() void CoverService::flushCache()
{ {
_cache.flush();
} }
void CoverService::setJpegQuality(unsigned quality) void CoverService::setJpegQuality(unsigned quality)
@@ -76,7 +76,6 @@ namespace lms::cover
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
const std::size_t _maxFileSize; const std::size_t _maxFileSize;
const std::vector<std::string> _preferredFileNames; const std::vector<std::string> _preferredFileNames;
const std::vector<std::string> _artistFileNames;
unsigned _jpegQuality; unsigned _jpegQuality;
}; };
+7 -3
View File
@@ -1,13 +1,16 @@
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/ScanStepScanFiles.cpp impl/ScanStepScanFiles.cpp
) )
@@ -20,10 +23,11 @@ target_include_directories(lmsscanner PRIVATE
) )
target_link_libraries(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE
lmscore
lmsdatabase lmsdatabase
lmsimage
lmsmetadata lmsmetadata
lmsrecommendation lmsrecommendation
lmscore
) )
target_link_libraries(lmsscanner PUBLIC target_link_libraries(lmsscanner PUBLIC
@@ -0,0 +1,149 @@
/*
* 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 "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
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
#include "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepAssociateArtistImages : public ScanStepBase
{
public:
ScanStepAssociateArtistImages(InitParams& initParams);
private:
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
core::LiteralString getStepName() const override { return "Associate artist images"; }
void process(ScanContext& context) override;
const std::vector<std::string> _artistFileNames;
};
} // namespace lms::scanner
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "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,111 @@
/*
* 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
{
void ScanStepRemoveOrphanedDbEntries::process(ScanContext& context)
{
removeOrphanedClusters(context);
removeOrphanedClusterTypes(context);
removeOrphanedArtists(context);
removeOrphanedReleases(context);
removeOrphanedDirectories(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusters(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned clusters...");
removeOrphanedEntries<db::Cluster>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusterTypes(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned cluster types...");
removeOrphanedEntries<db::ClusterType>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists...");
removeOrphanedEntries<db::Artist>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleases(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases...");
removeOrphanedEntries<db::Release>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedDirectories(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned directories...");
removeOrphanedEntries<db::Directory>(context);
}
template<typename T>
void ScanStepRemoveOrphanedDbEntries::removeOrphanedEntries(ScanStepRemoveOrphanedDbEntries::ScanContext& context)
{
constexpr std::size_t batchSize = 100;
using IdType = typename T::IdType;
db::Session& session{ _db.getTLSSession() };
db::RangeResults<IdType> entries;
while (!_abortScan)
{
{
auto transaction{ session.createReadTransaction() };
entries = T::findOrphanIds(session, db::Range{ 0, batchSize });
};
if (entries.results.empty())
break;
{
auto transaction{ session.createWriteTransaction() };
for (const IdType objectId : entries.results)
{
if (_abortScan)
break;
typename T::pointer entry{ T::find(session, objectId) };
entry.remove();
}
}
context.currentStepStats.processedElems += entries.results.size();
_progressCallback(context.currentStepStats);
}
}
} // namespace lms::scanner
@@ -0,0 +1,47 @@
/*
* 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(ScanContext& context);
void removeOrphanedClusterTypes(ScanContext& context);
void removeOrphanedArtists(ScanContext& context);
void removeOrphanedReleases(ScanContext& context);
void removeOrphanedDirectories(ScanContext& context);
template<typename T>
void removeOrphanedEntries(ScanStepRemoveOrphanedDbEntries::ScanContext& context);
};
} // namespace lms::scanner
@@ -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
ScanStepScanFiles::MetadataScanQueue::MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort)
: _metadataParser{ parser }
, _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" }
, _abort{ abort }
{
}
void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path)
{
{
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 ScanStepScanFiles::MetadataScanQueue::getResultsCount() const
{
std::scoped_lock lock{ _mutex };
return _scanResults.size();
}
size_t ScanStepScanFiles::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 ScanStepScanFiles::MetadataScanQueue::wait(std::size_t maxScanRequestCount)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
std::unique_lock lock{ _mutex };
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
}
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) ScanStepScanFiles::ScanStepScanFiles(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 ScanStepScanFiles::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 ScanStepScanFiles::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 ScanStepScanFiles::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 ScanStepScanFiles::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++;
} }
} }
@@ -19,17 +19,14 @@
#pragma once #pragma once
#include <condition_variable>
#include <deque>
#include <filesystem> #include <filesystem>
#include <mutex>
#include <span> #include <span>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/IOContextRunner.hpp"
#include "metadata/IParser.hpp" #include "metadata/IParser.hpp"
#include "FileScanQueue.hpp"
#include "ScanStepBase.hpp" #include "ScanStepBase.hpp"
namespace lms::scanner namespace lms::scanner
@@ -43,46 +40,18 @@ namespace lms::scanner
ScanStep getStep() const override { return ScanStep::ScanFiles; } ScanStep getStep() const override { return ScanStep::ScanFiles; }
core::LiteralString getStepName() const override { return "Scan files"; } core::LiteralString getStepName() const override { return "Scan files"; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
void process(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary);
bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo); bool checkAudioFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo);
struct MetaDataScanResult bool checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file);
{
std::filesystem::path path; void processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
std::unique_ptr<metadata::Track> trackMetaData; 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);
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; std::unique_ptr<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse; const std::vector<std::string> _extraTagsToParse;
class MetadataScanQueue FileScanQueue _fileScanQueue;
{
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 } // namespace lms::scanner
@@ -29,13 +29,16 @@
#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 "ScanStepScanFiles.hpp" #include "ScanStepScanFiles.hpp"
namespace lms::scanner namespace lms::scanner
@@ -336,14 +339,17 @@ namespace lms::scanner
_db _db
}; };
// Order is important
_scanSteps.clear(); _scanSteps.clear();
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params)); _scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(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()
@@ -361,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
@@ -57,19 +58,22 @@ namespace lms::scanner
DuplicateReason reason; DuplicateReason reason;
}; };
// 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,
RemoveOrphanedDbEntries,
ScanFiles, ScanFiles,
}; };
static inline constexpr unsigned ScanProgressStepCount{ 9 }; static inline constexpr unsigned ScanProgressStepCount{ 11 };
// reduced scan stats // reduced scan stats
struct ScanStepStats struct ScanStepStats
@@ -90,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
@@ -104,9 +104,9 @@ namespace lms::api::subsonic
const int toYear{ getMandatoryParameterAs<int>(context.parameters, "toYear") }; const int toYear{ getMandatoryParameterAs<int>(context.parameters, "toYear") };
Release::FindParameters params; Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::Date); params.setSortMethod(fromYear > toYear ? ReleaseSortMethod::DateDesc : ReleaseSortMethod::DateAsc);
params.setRange(range); params.setRange(range);
params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); params.setDateRange(DateRange::fromYearRange(std::min(fromYear, toYear), std::max(fromYear, toYear)));
params.setMediaLibrary(mediaLibraryId); params.setMediaLibrary(mediaLibraryId);
releases = Release::findIds(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
+3 -1
View File
@@ -23,6 +23,7 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/TrackArtistLink.hpp" #include "database/TrackArtistLink.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -92,7 +93,8 @@ namespace lms::api::subsonic
artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName()); artistNode.setAttribute("name", artist->getName());
artistNode.setAttribute("coverArt", idToString(artist->getId())); if (const db::Image::pointer artistImage{ artist->getImage() })
artistNode.setAttribute("coverArt", idToString(artist->getId()));
if (id3) if (id3)
{ {
+24 -14
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,50 +269,58 @@ 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.processedElems));
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::ScanFiles: case ScanStep::ScanFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files")
.arg(stepStats.processedElems) .arg(stepStats.processedElems)
.arg(stepStats.totalElems) .arg(stepStats.totalElems)