Merge branch 'develop' for release v3.69.0
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
|
||||
project(lms)
|
||||
project(lms VERSION 3.69.0)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@ If neither exists, it will then search for a file named `artist` (or another nam
|
||||
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
|
||||
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
|
||||
|
||||
## Disc image lookup
|
||||
_LMS_ automatically associates images with each disc in your collection. Name the image file after the disc's subtitle or another identifier configured in `lms.conf` (see the `medium-image-file-names` setting), and place it in the same directory as the disc's tracks. If no suitable image is found, LMS will also look for embedded images within the tracks of the disc.
|
||||
|
||||
## Playlist support
|
||||
_LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists.
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Admin.About.template">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Admin.About.about}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid d-grid gap-2">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
${tr:Lms.Admin.About.version}
|
||||
</div>
|
||||
<div class="col">
|
||||
${version}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
${tr:Lms.Admin.About.homepage}
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="${homepage-link}" target="_blank" class="dropdown-item link-success">${homepage-link}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${ok-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+1
-1
@@ -44,6 +44,7 @@
|
||||
<li>${scan-settings class="dropdown-item"}</li>
|
||||
<li>${scanner class="dropdown-item"}</li>
|
||||
<li>${users class="dropdown-item"}</li>
|
||||
<li>${about class="dropdown-item"}</li>
|
||||
${<if-debug-tools>}
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>${debug-tools class="dropdown-item"}</li>
|
||||
@@ -57,7 +58,6 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>${settings class="dropdown-item"}</li>
|
||||
<li>${logout class="dropdown-item"}</li>
|
||||
</ul>
|
||||
|
||||
@@ -52,12 +52,18 @@
|
||||
<message id="Lms.Error.user-not-found">User not found</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> About</message>
|
||||
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Debug tools</message>
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Libraries</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Scan settings</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||
|
||||
<!--About-->
|
||||
<message id="Lms.Admin.About.about">About</message>
|
||||
<message id="Lms.Admin.About.homepage">Homepage</message>
|
||||
<message id="Lms.Admin.About.version">Version</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Music libraries</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Create library</message>
|
||||
@@ -101,11 +107,11 @@
|
||||
|
||||
<!--Scanner Controller-->
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}: {2}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file ({1})</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file: {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Cannot parse lyrics file</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Cannot parse playlist 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>
|
||||
|
||||
@@ -52,12 +52,18 @@
|
||||
<message id="Lms.Error.user-not-found">No existe el usuario</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> Acerca de</message>
|
||||
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Herramientas de depuración</message>
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliotecas</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Opciones de escaneo</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Escanear</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Usuarios</message>
|
||||
|
||||
<!--About-->
|
||||
<message id="Lms.Admin.About.about">Acerca de</message>
|
||||
<message id="Lms.Admin.About.homepage">Página principal</message>
|
||||
<message id="Lms.Admin.About.version">Versión</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Bibliotecas musicales</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Crear una biblioteca</message>
|
||||
@@ -101,11 +107,11 @@
|
||||
|
||||
<!--Scanner Controller-->
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1} : {2}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen : {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message>
|
||||
<message id="Lms.Admin.ScannerController.compact">Compactar la base de datos. <strong>¡Atención!:</strong> esta operación puede tardar bastante tiempo y la aplicación se bloqueará mientras dura la operación de compactación</message>
|
||||
|
||||
@@ -52,12 +52,18 @@
|
||||
<message id="Lms.Error.user-not-found">L'utilisateur n'existe pas</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> À propos</message>
|
||||
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Outils de débogage</message>
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliothèques</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Paramètres du scan</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||
|
||||
<!--About-->
|
||||
<message id="Lms.Admin.About.about">À propos</message>
|
||||
<message id="Lms.Admin.About.homepage">Page d’accueil</message>
|
||||
<message id="Lms.Admin.About.version">Version</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Bibliothèques musicales</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Créer une bibliothèque</message>
|
||||
@@ -101,11 +107,11 @@
|
||||
|
||||
<!--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-embedded-image">Impossible de lire l'image à l'indice {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossible de lire l'image à l'indice {1} : {2}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier ({1})</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image : {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossible d'analyser le fichier de paroles</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossible d'analyser le fichier de liste de lecture</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>
|
||||
|
||||
@@ -52,12 +52,18 @@
|
||||
<message id="Lms.Error.user-not-found">Utente non trovato</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> Informazioni</message>
|
||||
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Strumenti di debug</message>
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Librerie</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Impostazioni di scansione</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message>
|
||||
|
||||
<!--About-->
|
||||
<message id="Lms.Admin.About.about">Informazioni</message>
|
||||
<message id="Lms.Admin.About.homepage">Pagina iniziale</message>
|
||||
<message id="Lms.Admin.About.version">Versione</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Librerie musicali</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Crea libreria</message>
|
||||
@@ -101,11 +107,11 @@
|
||||
|
||||
<!--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-embedded-image">Impossibile leggere l'immagine all'indice {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossibile leggere l'immagine all'indice {1} : {2}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file ({1})</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine : {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossibile analizzare il file dei testi</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossibile analizzare il file della playlist</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>
|
||||
|
||||
@@ -53,12 +53,18 @@
|
||||
<message id="Lms.Error.user-not-found">Użytkownik nie odnaleziony</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> O programie</message>
|
||||
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Narzędzia debugowania</message>
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Biblioteki</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Ustawienia skanowania</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Skaner</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Użytkownicy</message>
|
||||
|
||||
<!--About-->
|
||||
<message id="Lms.Admin.About.about">O programie</message>
|
||||
<message id="Lms.Admin.About.homepage">Strona główna</message>
|
||||
<message id="Lms.Admin.About.version">Wersja</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Biblioteki muzyczne</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Utwórz bibliotekę</message>
|
||||
@@ -102,11 +108,11 @@
|
||||
|
||||
<!--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-embedded-image">Nie można odczytać obrazu pod indeksem {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1} : {2}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku ({1})</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu : {1}</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Nie można przetworzyć pliku z tekstem</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Nie można przeanalizować pliku playlisty</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>
|
||||
|
||||
@@ -58,6 +58,12 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<!--About-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!--MediaLibraries-->
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message>
|
||||
<message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
<message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
<message id="Lms.template.star-btn"><i class="fa fa-fw fa-star-o"></i></message>
|
||||
<message id="Lms.template.toggle-visibility-btn"><i class="fa fa-fw fa-eye"></i></message>
|
||||
<message id="Lms.template.unstar-btn"><i class="fa fa-fw fa-star"></i></message>
|
||||
|
||||
</messages>
|
||||
|
||||
+2
-1
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
${container class="d-grid gap-3"}
|
||||
${disc-container class="d-grid gap-3"}
|
||||
${<if-has-copyright>}
|
||||
<div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div>
|
||||
${</if-has-copyright>}
|
||||
@@ -101,6 +101,7 @@
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||
${star-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||
${star-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
|
||||
+7
-4
@@ -7,9 +7,8 @@ working-dir = "/var/lms";
|
||||
# ffmpeg location
|
||||
ffmpeg-file = "/usr/bin/ffmpeg";
|
||||
|
||||
# Log files, empty means stdout
|
||||
# Log files, empty means debug+info on stdout, warning+error+fatal on stderr
|
||||
log-file = "";
|
||||
access-log-file = "";
|
||||
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
|
||||
# "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application
|
||||
log-min-severity = "info";
|
||||
@@ -99,13 +98,17 @@ cover-max-cache-size = 30;
|
||||
# JPEG quality for covers (range is 1-100)
|
||||
cover-jpeg-quality = 75;
|
||||
|
||||
# Preferred file names for covers (order is important)
|
||||
# Preferred file names for covers (order is important, accept wildcards)
|
||||
cover-preferred-file-names = ("cover", "front", "folder", "default");
|
||||
|
||||
# File names for artist images (order is important)
|
||||
# File names for artist images (order is important, accept wildcards)
|
||||
# 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");
|
||||
|
||||
# File names for medium images (order is important, accept wildcards)
|
||||
# Note: files named after the disc itself are always searched before the names in this list.
|
||||
medium-image-file-names = ("discsubtitle");
|
||||
|
||||
# Playqueue max entry count
|
||||
playqueue-max-entry-count = 1000;
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@ pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
|
||||
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
|
||||
|
||||
set(LMS_VERSION ${PROJECT_VERSION})
|
||||
|
||||
configure_file(
|
||||
impl/Version.cpp.in
|
||||
impl/Version.cpp
|
||||
@ONLY
|
||||
)
|
||||
|
||||
add_library(lmscore STATIC
|
||||
impl/http/Client.cpp
|
||||
impl/http/SendQueue.cpp
|
||||
@@ -19,12 +27,11 @@ add_library(lmscore STATIC
|
||||
impl/Path.cpp
|
||||
impl/Random.cpp
|
||||
impl/RecursiveSharedMutex.cpp
|
||||
impl/StreamLogger.cpp
|
||||
impl/String.cpp
|
||||
impl/TraceLogger.cpp
|
||||
impl/UUID.cpp
|
||||
impl/WtLogger.cpp
|
||||
impl/XxHash3.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/impl/Version.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmscore INTERFACE
|
||||
|
||||
@@ -17,19 +17,17 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/StreamLogger.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
// The trace logger is meant to built/destroyed once
|
||||
const Service<logging::ILogger> logger{ std::make_unique<logging::StreamLogger>(std::cout, logging::StreamLogger::allSeverities) };
|
||||
const Service<logging::ILogger> logger{ logging::createLogger() };
|
||||
const Service<tracing::ITraceLogger> traceLogger{ tracing::createTraceLogger(tracing::Level::Overview) };
|
||||
|
||||
static void BM_TraceLogger_Overview(benchmark::State& state)
|
||||
|
||||
@@ -17,7 +17,17 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
@@ -63,6 +73,8 @@ namespace lms::core::logging
|
||||
return "UI";
|
||||
case Module::UTILS:
|
||||
return "UTILS";
|
||||
case Module::WT:
|
||||
return "WT";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -103,4 +115,94 @@ namespace lms::core::logging
|
||||
{
|
||||
return _oss.str();
|
||||
}
|
||||
|
||||
std::unique_ptr<ILogger> createLogger(Severity minSeverity, const std::filesystem::path& logFilePath)
|
||||
{
|
||||
return std::make_unique<Logger>(minSeverity, logFilePath);
|
||||
}
|
||||
|
||||
Logger::OutputStream::OutputStream(std::ostream& os)
|
||||
: stream{ os }
|
||||
{
|
||||
}
|
||||
|
||||
Logger::Logger(Severity minSeverity, const std::filesystem::path& logFilePath)
|
||||
{
|
||||
if (!logFilePath.empty())
|
||||
{
|
||||
_logFileStream = std::make_unique<std::ofstream>(logFilePath, std::ios::out | std::ios::app);
|
||||
if (!_logFileStream->is_open())
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
throw LmsException{ "Cannot open log file '" + logFilePath.string() + "' for writing: " + ec.message() };
|
||||
}
|
||||
}
|
||||
|
||||
switch (minSeverity)
|
||||
{
|
||||
case core::logging::Severity::DEBUG:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::DEBUG });
|
||||
else
|
||||
addOutputStream(std::cout, { core::logging::Severity::DEBUG });
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::INFO:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::INFO });
|
||||
else
|
||||
addOutputStream(std::cout, { core::logging::Severity::INFO });
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::WARNING:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::WARNING });
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::WARNING });
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::ERROR:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::ERROR });
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::ERROR });
|
||||
[[fallthrough]];
|
||||
case core::logging::Severity::FATAL:
|
||||
if (_logFileStream)
|
||||
addOutputStream(*_logFileStream, { core::logging::Severity::FATAL });
|
||||
else
|
||||
addOutputStream(std::cerr, { core::logging::Severity::FATAL });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::~Logger() = default;
|
||||
|
||||
void Logger::addOutputStream(std::ostream& os, Severity severity)
|
||||
{
|
||||
auto it{ std::find_if(_outputStreams.begin(), _outputStreams.end(), [&os](const OutputStream& outputStream) { return &outputStream.stream == &os; }) };
|
||||
if (it == _outputStreams.end())
|
||||
it = _outputStreams.emplace(_outputStreams.end(), os);
|
||||
|
||||
assert(!_severityToOutputStreamMap.contains(severity));
|
||||
_severityToOutputStreamMap.emplace(severity, &(*it));
|
||||
}
|
||||
|
||||
bool Logger::isSeverityActive(Severity severity) const
|
||||
{
|
||||
return _severityToOutputStreamMap.contains(severity);
|
||||
}
|
||||
|
||||
void Logger::processLog(const Log& log)
|
||||
{
|
||||
processLog(log.getModule(), log.getSeverity(), log.getMessage());
|
||||
}
|
||||
|
||||
void Logger::processLog(Module module, Severity severity, std::string_view message)
|
||||
{
|
||||
assert(isSeverityActive(severity)); // should have been filtered out by a isSeverityActive call
|
||||
OutputStream* outputStream{ _severityToOutputStreamMap.at(severity) };
|
||||
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
|
||||
|
||||
std::unique_lock lock{ outputStream->mutex };
|
||||
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
|
||||
}
|
||||
|
||||
} // namespace lms::core::logging
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <iosfwd>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
class Logger final : public ILogger
|
||||
{
|
||||
public:
|
||||
Logger(Severity minSeverity, const std::filesystem::path& logFilePath);
|
||||
~Logger() override;
|
||||
Logger(const Logger&) = delete;
|
||||
Logger& operator=(const Logger&) = delete;
|
||||
|
||||
private:
|
||||
bool isSeverityActive(Severity severity) const override;
|
||||
void processLog(const Log& log) override;
|
||||
void processLog(Module module, Severity severity, std::string_view message) override;
|
||||
|
||||
void addOutputStream(std::ostream& os, Severity severity);
|
||||
|
||||
struct OutputStream
|
||||
{
|
||||
OutputStream(std::ostream& os);
|
||||
|
||||
std::mutex mutex;
|
||||
std::ostream& stream;
|
||||
};
|
||||
|
||||
std::list<OutputStream> _outputStreams;
|
||||
std::unordered_map<Severity, OutputStream*> _severityToOutputStreamMap;
|
||||
std::unique_ptr<std::ofstream> _logFileStream;
|
||||
};
|
||||
} // namespace lms::core::logging
|
||||
@@ -86,4 +86,24 @@ namespace lms::core::pathUtils
|
||||
|
||||
return longestCommonPath;
|
||||
}
|
||||
|
||||
std::string sanitizeFileStem(const std::string_view fileStem)
|
||||
{
|
||||
// Keep UTF8-encoded characters, but skip illegal ASCII characters
|
||||
constexpr std::array<unsigned char, 9> illegalChars{ '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
|
||||
static_assert(std::all_of(std::begin(illegalChars), std::end(illegalChars), [](unsigned char c) { return c < 128; }), "Illegal characters must be ASCII");
|
||||
|
||||
std::string sanitized;
|
||||
sanitized.reserve(fileStem.size());
|
||||
|
||||
for (const char c : fileStem)
|
||||
{
|
||||
if (std::any_of(std::begin(illegalChars), std::end(illegalChars), [c](char illegalChar) { return c == illegalChar; }))
|
||||
continue;
|
||||
|
||||
sanitized.push_back(c);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
} // namespace lms::core::pathUtils
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core::tracing
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,22 +17,12 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
#include <thread>
|
||||
#include "core/Version.hpp"
|
||||
|
||||
#include "core/StreamLogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
namespace lms::core
|
||||
{
|
||||
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
|
||||
: _os{ os }
|
||||
, _severities{ severities }
|
||||
std::string_view getVersion()
|
||||
{
|
||||
return "v" "@LMS_VERSION@";
|
||||
}
|
||||
|
||||
void StreamLogger::processLog(const Log& log)
|
||||
{
|
||||
assert(isSeverityActive(log.getSeverity()));
|
||||
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
|
||||
}
|
||||
} // namespace lms::core::logging
|
||||
} // namespace lms::core
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "core/WtLogger.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include <Wt/WLogger.h>
|
||||
#include <Wt/WServer.h>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string to_string(std::thread::id id)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << id;
|
||||
return oss.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WtLogger::WtLogger(Severity minSeverity)
|
||||
: _minSeverity{ minSeverity }
|
||||
{
|
||||
}
|
||||
|
||||
std::string WtLogger::computeLogConfig(Severity minSeverity)
|
||||
{
|
||||
switch (minSeverity)
|
||||
{
|
||||
case Severity::DEBUG:
|
||||
return "*";
|
||||
case Severity::INFO:
|
||||
return "* -debug";
|
||||
case Severity::WARNING:
|
||||
return "* -debug -info";
|
||||
case Severity::ERROR:
|
||||
return "* -debug -info -warning";
|
||||
case Severity::FATAL:
|
||||
return "* -debug -info -warning -error";
|
||||
}
|
||||
|
||||
throw LmsException{ "Unhandled severity" };
|
||||
}
|
||||
|
||||
bool WtLogger::isSeverityActive(Severity severity) const
|
||||
{
|
||||
return static_cast<int>(severity) <= static_cast<int>(_minSeverity);
|
||||
}
|
||||
|
||||
void WtLogger::processLog(const Log& log)
|
||||
{
|
||||
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
|
||||
}
|
||||
} // namespace lms::core::logging
|
||||
@@ -19,11 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "core/Service.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
@@ -57,6 +58,7 @@ namespace lms::core::logging
|
||||
TRANSCODING,
|
||||
UI,
|
||||
UTILS,
|
||||
WT,
|
||||
};
|
||||
|
||||
const char* getModuleName(Module mod);
|
||||
@@ -92,7 +94,11 @@ namespace lms::core::logging
|
||||
|
||||
virtual bool isSeverityActive(Severity severity) const = 0;
|
||||
virtual void processLog(const Log& log) = 0;
|
||||
virtual void processLog(Module module, Severity severity, std::string_view message) = 0;
|
||||
};
|
||||
|
||||
static constexpr Severity defaultMinSeverity{ Severity::INFO };
|
||||
std::unique_ptr<ILogger> createLogger(Severity minSeverity = defaultMinSeverity, const std::filesystem::path& logFilePath = {});
|
||||
} // namespace lms::core::logging
|
||||
|
||||
#define LMS_LOG(module, severity, message) \
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -54,4 +56,7 @@ namespace lms::core::pathUtils
|
||||
|
||||
return longestCommonPath;
|
||||
}
|
||||
|
||||
// A method that sanitize a file stem, removing any illegal chars
|
||||
std::string sanitizeFileStem(std::string_view fileStem);
|
||||
} // namespace lms::core::pathUtils
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "core/EnumSet.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
class StreamLogger final : public ILogger
|
||||
{
|
||||
public:
|
||||
static constexpr EnumSet<Severity> allSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO, Severity::DEBUG };
|
||||
static constexpr EnumSet<Severity> defaultSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO };
|
||||
|
||||
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
|
||||
|
||||
bool isSeverityActive(Severity severity) const override { return _severities.contains(severity); }
|
||||
void processLog(const Log& log) override;
|
||||
|
||||
private:
|
||||
std::ostream& _os;
|
||||
const EnumSet<Severity> _severities;
|
||||
};
|
||||
} // namespace lms::core::logging
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,22 +19,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
namespace lms::core
|
||||
{
|
||||
class WtLogger final : public ILogger
|
||||
{
|
||||
public:
|
||||
WtLogger(Severity minSeverity);
|
||||
|
||||
static std::string computeLogConfig(Severity minSeverity);
|
||||
|
||||
private:
|
||||
bool isSeverityActive(Severity severity) const override;
|
||||
void processLog(const Log& log) override;
|
||||
const Severity _minSeverity;
|
||||
};
|
||||
} // namespace lms::core::logging
|
||||
std::string_view getVersion();
|
||||
} // namespace lms::core
|
||||
@@ -103,4 +103,34 @@ namespace lms::core::pathUtils::tests
|
||||
EXPECT_EQ(core::pathUtils::isPathInRootPath(test.path, test.rootPath), test.expectedResult) << "Failed: path = " << test.path << ", rootPath = " << test.rootPath;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Path, sanitizeFileStem)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string input;
|
||||
std::string_view expectedOutput;
|
||||
};
|
||||
|
||||
TestCase tests[]{
|
||||
{ "", "" }, // empty input
|
||||
{ "valid_file_name", "valid_file_name" },
|
||||
{ "invalid:file*name?", "invalidfilename" },
|
||||
{ "another|invalid<name>", "anotherinvalidname" },
|
||||
{ "/leading/slash", "leadingslash" },
|
||||
{ "\\backslash\\file", "backslashfile" },
|
||||
{ "file_with_äöüß", "file_with_äöüß" }, // keep German umlauts
|
||||
{ "file_with_éèêë", "file_with_éèêë" }, // keep French accents
|
||||
{ "héllo 漢字", "héllo 漢字" }, // keep UTF8 characters
|
||||
{ "file_with_üñîçødë", "file_with_üñîçødë" }, // keep special characters
|
||||
{ "file_with_!@#$%^&*()_+", "file_with_!@#$%^&()_+" }, // remove special characters
|
||||
{ "file_with_", "file_with_" }, // handle double dots
|
||||
{ "file.with.extension", "file.with.extension" }, // keep extensions
|
||||
};
|
||||
|
||||
for (const TestCase& test : tests)
|
||||
{
|
||||
EXPECT_EQ(core::pathUtils::sanitizeFileStem(test.input), test.expectedOutput) << "Failed: input = " << test.input;
|
||||
}
|
||||
}
|
||||
} // namespace lms::core::pathUtils::tests
|
||||
@@ -8,6 +8,7 @@ add_library(lmsdatabase STATIC
|
||||
impl/objects/Image.cpp
|
||||
impl/objects/Listen.cpp
|
||||
impl/objects/MediaLibrary.cpp
|
||||
impl/objects/Medium.cpp
|
||||
impl/objects/PlayListFile.cpp
|
||||
impl/objects/PlayQueue.cpp
|
||||
impl/objects/TrackArtistLink.cpp
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 98 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 99 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -1423,6 +1423,111 @@ WHERE art.image_id IS NULL)");
|
||||
dropIndexes(session);
|
||||
}
|
||||
|
||||
void migrateFromV98(Session& session)
|
||||
{
|
||||
// Medium support
|
||||
dropIndexes(session);
|
||||
|
||||
// New table for mediums
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "medium" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"position" integer,
|
||||
"track_count" integer,
|
||||
"media" text not null,
|
||||
"replay_gain" real,
|
||||
"release_id" bigint,
|
||||
"preferred_artwork_id" bigint,
|
||||
constraint "fk_medium_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_medium_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
|
||||
))");
|
||||
|
||||
// New primary key in track -> need to recreate the table
|
||||
utils::executeCommand(*session.getDboSession(), 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,
|
||||
"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,
|
||||
"original_date" text,
|
||||
"absolute_file_path" text not null,
|
||||
"file_size" bigint not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"mbid" text not null,
|
||||
"recording_mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"advisory" integer not null,
|
||||
"replay_gain" real,
|
||||
"artist_display_name" text not null,
|
||||
"comment" text not null,
|
||||
"medium_id" bigint,
|
||||
"release_id" bigint,
|
||||
"media_library_id" bigint,
|
||||
"directory_id" bigint,
|
||||
"preferred_artwork_id" bigint,
|
||||
"preferred_media_artwork_id" bigint,
|
||||
constraint "fk_track_medium" foreign key ("medium_id") references "medium" ("id") on delete cascade deferrable initially deferred,
|
||||
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,
|
||||
constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred,
|
||||
constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
|
||||
))");
|
||||
|
||||
// Migrate data:
|
||||
// - removed disc_number, total_track, disc_subtitle, release_replay_gain
|
||||
// - renamed track_replay_gain to replay_gain
|
||||
// - with the new medium_id field set to null
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
scan_version,
|
||||
track_number,
|
||||
name,
|
||||
duration,
|
||||
bitrate,
|
||||
bits_per_sample,
|
||||
channel_count,
|
||||
sample_rate,
|
||||
date,
|
||||
original_date,
|
||||
absolute_file_path,
|
||||
file_size,
|
||||
file_last_write,
|
||||
file_added,
|
||||
mbid,
|
||||
recording_mbid,
|
||||
copyright,
|
||||
copyright_url,
|
||||
advisory,
|
||||
track_replay_gain AS replay_gain,
|
||||
artist_display_name,
|
||||
comment,
|
||||
NULL as medium_id,
|
||||
release_id,
|
||||
media_library_id,
|
||||
directory_id,
|
||||
preferred_artwork_id,
|
||||
preferred_media_artwork_id
|
||||
FROM track)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scan rescan all audio files
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1497,6 +1602,7 @@ WHERE art.image_id IS NULL)");
|
||||
{ 95, migrateFromV95 },
|
||||
{ 96, migrateFromV96 },
|
||||
{ 97, migrateFromV97 },
|
||||
{ 98, migrateFromV98 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Listen.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "database/objects/PlayQueue.hpp"
|
||||
#include "database/objects/RatedArtist.hpp"
|
||||
@@ -82,6 +83,7 @@ namespace lms::db
|
||||
_session.mapClass<Label>("label");
|
||||
_session.mapClass<Listen>("listen");
|
||||
_session.mapClass<MediaLibrary>("media_library");
|
||||
_session.mapClass<Medium>("medium");
|
||||
_session.mapClass<PlayListFile>("playlist_file");
|
||||
_session.mapClass<PlayQueue>("playqueue");
|
||||
_session.mapClass<RatedArtist>("rated_artist");
|
||||
@@ -235,6 +237,8 @@ namespace lms::db
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS medium_release_position_idx ON medium(release_id, position)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_directory_idx ON playlist_file(directory_id);");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)");
|
||||
@@ -262,12 +266,12 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_medium_idx ON track(medium_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_disc_idx ON track(release_id, disc_number)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)");
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
namespace lms::db::utils
|
||||
{
|
||||
std::string escapeLikeKeyword(std::string_view keyword)
|
||||
std::string escapeForLikeKeyword(std::string_view keyword)
|
||||
{
|
||||
return core::stringUtils::escapeString(keyword, "%_", escapeChar);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace lms::db::utils
|
||||
{
|
||||
#define ESCAPE_CHAR_STR "\\"
|
||||
static inline constexpr char escapeChar{ '\\' };
|
||||
std::string escapeLikeKeyword(std::string_view keywords);
|
||||
std::string escapeForLikeKeyword(std::string_view keywords);
|
||||
|
||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||
|
||||
|
||||
@@ -106,13 +106,13 @@ namespace lms::db
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
|
||||
@@ -40,10 +40,11 @@ namespace lms::db
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.artist.isValid()
|
||||
|| params.release.isValid())
|
||||
|| params.release.isValid()
|
||||
|| params.medium.isValid())
|
||||
{
|
||||
query.join("track t ON t.directory_id = d.id");
|
||||
query.groupBy("d.id");
|
||||
@@ -55,6 +56,9 @@ namespace lms::db
|
||||
if (params.parentDirectory.isValid())
|
||||
query.where("d.parent_directory_id = ?").bind(params.parentDirectory);
|
||||
|
||||
if (params.medium.isValid())
|
||||
query.where("t.medium_id = ?").bind(params.medium);
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
@@ -91,7 +95,7 @@ namespace lms::db
|
||||
case DirectorySortMethod::None:
|
||||
break;
|
||||
case DirectorySortMethod::Name:
|
||||
query.orderBy("name COLLATE NOCASE");
|
||||
query.orderBy("d.name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,20 @@ namespace lms::db
|
||||
if (params.directory.isValid())
|
||||
query.where("i.directory_id = ?").bind(params.directory);
|
||||
if (!params.fileStem.empty())
|
||||
{
|
||||
// if contains a wildcard, replace by % for LIKE
|
||||
if (params.processWildcardsInFileStem.value() && params.fileStem.find('*') != std::string::npos)
|
||||
{
|
||||
std::string fileStem{ params.fileStem };
|
||||
utils::escapeForLikeKeyword(fileStem);
|
||||
std::replace(std::begin(fileStem), std::end(fileStem), '*', '%');
|
||||
query.where("i.stem LIKE ? COLLATE NOCASE ESCAPE '" ESCAPE_CHAR_STR "'").bind(fileStem);
|
||||
}
|
||||
else
|
||||
{
|
||||
query.where("i.stem = ? COLLATE NOCASE").bind(params.fileStem);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -100,13 +100,13 @@ namespace lms::db
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
for (const std::string_view keyword : params.keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
@@ -169,7 +169,7 @@ namespace lms::db
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -227,7 +227,7 @@ namespace lms::db
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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/objects/Medium.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Medium);
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
Medium::Medium(ObjectPtr<Release> release)
|
||||
: _release(getDboPtr(release))
|
||||
{
|
||||
}
|
||||
|
||||
Medium::pointer Medium::create(Session& session, ObjectPtr<Release> release)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<Medium>{ new Medium{ release } });
|
||||
}
|
||||
|
||||
std::size_t Medium::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM medium"));
|
||||
}
|
||||
|
||||
Medium::pointer Medium::find(Session& session, MediumId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m").where("m.id = ?").bind(id));
|
||||
}
|
||||
|
||||
Medium::pointer Medium::find(Session& session, ReleaseId releaseId, std::optional<std::size_t> position)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::optional<int> dbPosition;
|
||||
if (position)
|
||||
dbPosition = static_cast<int>(*position);
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
|
||||
query.where("m.release_id = ?").bind(releaseId);
|
||||
if (position)
|
||||
query.where("m.position = ?").bind(*dbPosition);
|
||||
else
|
||||
query.where("m.position IS NULL");
|
||||
|
||||
return utils::fetchQuerySingleResult(query);
|
||||
}
|
||||
|
||||
void Medium::find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func)
|
||||
{
|
||||
assert(idRange.isValid());
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m").orderBy("m.id").where("m.id BETWEEN ? AND ?").bind(idRange.first).bind(idRange.last) };
|
||||
|
||||
utils::forEachQueryResult(query, [&](const Medium::pointer& medium) {
|
||||
func(medium);
|
||||
});
|
||||
}
|
||||
|
||||
IdRange<MediumId> Medium::findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<std::tuple<MediumId, MediumId>>("SELECT MIN(sub.id) AS first_id, MAX(sub.id) AS last_id FROM (SELECT m.id FROM medium m WHERE m.id > ? ORDER BY m.id LIMIT ?) sub") };
|
||||
query.bind(lastRetrievedId);
|
||||
query.bind(static_cast<int>(count));
|
||||
|
||||
auto res{ utils::fetchQuerySingleResult(query) };
|
||||
return IdRange<MediumId>{ .first = std::get<0>(res), .last = std::get<1>(res) };
|
||||
}
|
||||
|
||||
RangeResults<MediumId> Medium::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
// select the mediums that have no track
|
||||
auto query{ session.getDboSession()->query<MediumId>("select m.id from medium m LEFT OUTER JOIN track t ON m.id = t.medium_id WHERE t.id IS NULL") };
|
||||
return utils::execRangeQuery<MediumId>(query, range);
|
||||
}
|
||||
|
||||
void Medium::updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
if (artworkId.isValid())
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE medium SET preferred_artwork_id = ? WHERE id = ?", artworkId, mediumId);
|
||||
else
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE medium SET preferred_artwork_id = NULL WHERE id = ?", mediumId);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -124,7 +125,7 @@ namespace lms::db
|
||||
query.where("r.name = ?").bind(params.name);
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
@@ -577,28 +578,6 @@ namespace lms::db
|
||||
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params));
|
||||
}
|
||||
|
||||
std::size_t Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res{ utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t").where("t.release_id = ?").bind(getId())) };
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<DiscInfo> Release::getDiscs() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using ResultType = std::tuple<int, std::string>;
|
||||
const auto query{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t").where("t.release_id = ?").bind(getId()).orderBy("disc_number") };
|
||||
|
||||
std::vector<DiscInfo> discs;
|
||||
utils::forEachQueryResult(query, [&](ResultType&& res) {
|
||||
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::move(std::get<std::string>(res)) });
|
||||
});
|
||||
|
||||
return discs;
|
||||
}
|
||||
|
||||
core::PartialDateTime Release::getDate() const
|
||||
{
|
||||
return getDate(false);
|
||||
@@ -753,6 +732,18 @@ namespace lms::db
|
||||
return _preferredArtwork.id();
|
||||
}
|
||||
|
||||
std::vector<ObjectPtr<Medium>> Release::getMediums() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// Select the similar releases using the 5 most used clusters of the release
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
|
||||
query.where("m.release_id = ?").bind(getId());
|
||||
query.orderBy("m.position");
|
||||
|
||||
return utils::fetchQueryResults<Medium::pointer>(query);
|
||||
}
|
||||
|
||||
void Release::clearLabels()
|
||||
{
|
||||
_labels.clear();
|
||||
@@ -794,11 +785,6 @@ namespace lms::db
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
bool Release::hasDiscSubtitle() const
|
||||
{
|
||||
return utils::fetchQuerySingleResult(session()->query<int>("SELECT EXISTS (SELECT 1 FROM track WHERE disc_subtitle IS NOT NULL AND disc_subtitle <> '' AND release_id = ?)").bind(getId()));
|
||||
}
|
||||
|
||||
std::size_t Release::getTrackCount() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -59,7 +61,7 @@ namespace lms::db
|
||||
|
||||
assert(params.keywords.empty() || params.name.empty());
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
if (!params.name.empty())
|
||||
query.where("t.name = ?").bind(params.name);
|
||||
@@ -146,6 +148,9 @@ namespace lms::db
|
||||
query.where("r.name = ?").bind(params.releaseName);
|
||||
}
|
||||
|
||||
if (params.medium.isValid())
|
||||
query.where("t.medium_id = ?").bind(params.medium);
|
||||
|
||||
if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList)
|
||||
{
|
||||
query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
|
||||
@@ -156,8 +161,11 @@ namespace lms::db
|
||||
if (params.trackNumber)
|
||||
query.where("t.track_number = ?").bind(*params.trackNumber);
|
||||
|
||||
if (params.discNumber)
|
||||
query.where("t.disc_number = ?").bind(*params.discNumber);
|
||||
if (params.sortMethod == TrackSortMethod::DateDescAndRelease
|
||||
|| params.sortMethod == TrackSortMethod::Release)
|
||||
{
|
||||
query.join("medium m ON t.medium_id = m.id");
|
||||
}
|
||||
|
||||
if (params.filters.mediaLibrary.isValid())
|
||||
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
|
||||
@@ -213,16 +221,19 @@ namespace lms::db
|
||||
query.orderBy("t.absolute_file_path COLLATE NOCASE");
|
||||
break;
|
||||
case TrackSortMethod::DateDescAndRelease:
|
||||
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number");
|
||||
query.orderBy("t.date DESC,t.release_id,m.position,t.track_number");
|
||||
break;
|
||||
case TrackSortMethod::Release:
|
||||
query.orderBy("t.disc_number,t.track_number");
|
||||
query.orderBy("m.position,t.track_number");
|
||||
break;
|
||||
case TrackSortMethod::TrackList:
|
||||
assert(params.trackList.isValid());
|
||||
query.orderBy("t_l_e.id");
|
||||
break;
|
||||
case TrackSortMethod::TrackNumber:
|
||||
query.orderBy("t.track_number");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -360,7 +371,7 @@ namespace lms::db
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") };
|
||||
auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.mbid") };
|
||||
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ namespace lms::db
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
|
||||
|
||||
if (params.artist.isValid()
|
||||
|| params.discNumber.has_value()
|
||||
if (params.medium.isValid()
|
||||
|| params.track.isValid()
|
||||
|| params.release.isValid()
|
||||
|| params.trackList.isValid()
|
||||
@@ -54,39 +53,27 @@ namespace lms::db
|
||||
{
|
||||
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t_e_i_l.track_id");
|
||||
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||
|
||||
if (!params.trackArtistLinkTypes.empty())
|
||||
{
|
||||
std::string clause{ "t_a_l.type IN (" };
|
||||
for (const auto& type : params.trackArtistLinkTypes)
|
||||
{
|
||||
if (clause.back() != '(')
|
||||
clause += ",";
|
||||
clause += "?";
|
||||
query.bind(type);
|
||||
}
|
||||
clause += ")";
|
||||
query.where(clause);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_e_i_l.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid()
|
||||
|| params.discNumber.has_value()
|
||||
|| params.medium.isValid()
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
|
||||
{
|
||||
assert(!(params.release.isValid() && params.medium.isValid()));
|
||||
|
||||
assert((params.sortMethod != TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc || params.release.isValid()));
|
||||
assert((params.sortMethod != TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc || params.medium.isValid()));
|
||||
|
||||
query.join("track t ON t_e_i_l.track_id = t.id");
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
if (params.discNumber.has_value())
|
||||
query.where("t.disc_number = ?").bind(params.discNumber.value());
|
||||
if (params.medium.isValid())
|
||||
query.where("t.medium_id = ?").bind(params.medium);
|
||||
|
||||
if (params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc)
|
||||
query.join("medium m ON m.id = t.medium_id");
|
||||
}
|
||||
|
||||
if (params.trackList.isValid())
|
||||
@@ -107,7 +94,7 @@ namespace lms::db
|
||||
query.orderBy("t_e_i.size DESC");
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
|
||||
query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC");
|
||||
query.orderBy("m.position, t.track_number, t_e_i.size DESC");
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
|
||||
query.orderBy("t.track_number, t_e_i.size DESC");
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace lms::db
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.label.isValid()
|
||||
|
||||
@@ -119,12 +119,6 @@ namespace lms::db
|
||||
int end{};
|
||||
};
|
||||
|
||||
struct DiscInfo
|
||||
{
|
||||
std::size_t position;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct FileInfo
|
||||
{
|
||||
Wt::WDateTime lastWrittenTime;
|
||||
@@ -163,6 +157,12 @@ namespace lms::db
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class MediumSortMethod
|
||||
{
|
||||
None,
|
||||
PositionAsc,
|
||||
};
|
||||
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
@@ -215,6 +215,7 @@ namespace lms::db
|
||||
DateDescAndRelease,
|
||||
Release, // order by disc/track number
|
||||
TrackList, // order by asc order in tracklist
|
||||
TrackNumber,
|
||||
};
|
||||
|
||||
enum class TrackLyricsSortMethod
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/DirectoryId.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
@@ -50,8 +51,9 @@ namespace lms::db
|
||||
{
|
||||
std::optional<Range> range;
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||
ArtistId artist; // only directory that involve this artist
|
||||
ReleaseId release; // only releases that involve this artist
|
||||
ArtistId artist; // only directoies that involve this artist
|
||||
MediumId medium; // only directories that involve this medium
|
||||
ReleaseId release; // only directories that involve this release
|
||||
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
DirectoryId parentDirectory; // If set, directories that have this parent
|
||||
bool withNoTrack{}; // If set, directories that do not contain any track
|
||||
@@ -74,6 +76,11 @@ namespace lms::db
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setMedium(MediumId _medium)
|
||||
{
|
||||
medium = _medium;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
@@ -107,7 +114,7 @@ namespace lms::db
|
||||
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 RangeResults<Directory::pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func);
|
||||
static void find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func);
|
||||
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId);
|
||||
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/TaggedType.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/DirectoryId.hpp"
|
||||
@@ -43,8 +44,11 @@ namespace lms::db
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
using ProcessWildcards = core::TaggedBool<class FindParametersProcessWildcardsTag>;
|
||||
|
||||
std::optional<Range> range;
|
||||
std::string fileStem; // if set, images with this file stem
|
||||
ProcessWildcards processWildcardsInFileStem{ false }; // if true, replace '*' by '%' for SQL LIKE
|
||||
DirectoryId directory; // if set, images in this directory
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
@@ -52,9 +56,10 @@ namespace lms::db
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setFileStem(std::string_view _fileStem)
|
||||
FindParameters& setFileStem(std::string_view _fileStem, ProcessWildcards processWildcards = ProcessWildcards{ false })
|
||||
{
|
||||
fileStem = _fileStem;
|
||||
processWildcardsInFileStem = processWildcards;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDirectory(DirectoryId _directory)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "database/IdRange.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtworkId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Artwork;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class Medium final : public Object<Medium, MediumId>
|
||||
{
|
||||
public:
|
||||
static const std::size_t maxMediaLength{ 64 };
|
||||
|
||||
Medium() = default;
|
||||
|
||||
// find
|
||||
struct FindParameters
|
||||
{
|
||||
ReleaseId release;
|
||||
MediumSortMethod sortMethod{ MediumSortMethod::None };
|
||||
std::optional<Range> range;
|
||||
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(MediumSortMethod _sortMethod)
|
||||
{
|
||||
sortMethod = _sortMethod;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, MediumId id);
|
||||
static pointer find(Session& session, ReleaseId id, std::optional<std::size_t> position);
|
||||
static void find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func);
|
||||
static IdRange<MediumId> findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count);
|
||||
static RangeResults<MediumId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Updates
|
||||
static void updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId);
|
||||
|
||||
// getters
|
||||
std::string_view getName() const { return _name; }
|
||||
std::optional<std::size_t> getPosition() const { return _position; }
|
||||
std::optional<std::size_t> getTrackCount() const { return _trackCount; } // not necessarily the number of tracks in the medium, but the number of tracks that should be in the medium
|
||||
std::string_view getMedia() const { return _media; }
|
||||
std::optional<float> getReplayGain() const { return _replayGain; }
|
||||
ReleaseId getReleaseId() const { return _release.id(); }
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const { return _preferredArtwork; }
|
||||
ArtworkId getPreferredArtworkId() const { return _preferredArtwork.id(); }
|
||||
|
||||
// setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setPosition(std::optional<std::size_t> position) { _position = position; }
|
||||
void setTrackCount(std::optional<std::size_t> trackCount) { _trackCount = trackCount; }
|
||||
void setMedia(std::string_view media) { _media = media; }
|
||||
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _position, "position");
|
||||
Wt::Dbo::field(a, _trackCount, "track_count");
|
||||
Wt::Dbo::field(a, _media, "media");
|
||||
Wt::Dbo::field(a, _replayGain, "replay_gain");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "medium");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Medium(ObjectPtr<Release> release);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release);
|
||||
|
||||
std::string _name;
|
||||
std::optional<int> _position; // position in the release
|
||||
std::optional<int> _trackCount;
|
||||
std::string _media; // CD, etc.
|
||||
std::optional<float> _replayGain;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // tracks that match this medium
|
||||
};
|
||||
} // 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(MediumId)
|
||||
@@ -51,6 +51,7 @@ namespace lms::db
|
||||
class Artwork;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Medium;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
@@ -282,9 +283,7 @@ namespace lms::db
|
||||
std::string_view getSortName() const { return _sortName; }
|
||||
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
|
||||
std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); }
|
||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||
std::vector<DiscInfo> getDiscs() const;
|
||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } // the number of discs this release should have if complete
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getAddedTime() const;
|
||||
Wt::WDateTime getLastWrittenTime() const;
|
||||
@@ -301,6 +300,7 @@ namespace lms::db
|
||||
std::string_view getComment() const { return _comment; }
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
ArtworkId getPreferredArtworkId() const;
|
||||
std::vector<ObjectPtr<Medium>> getMediums() const;
|
||||
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
@@ -326,7 +326,6 @@ namespace lms::db
|
||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
bool hasDiscSubtitle() const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "database/objects/DirectoryId.hpp"
|
||||
#include "database/objects/Filters.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
@@ -58,6 +59,7 @@ namespace lms::db
|
||||
class Directory;
|
||||
class TrackEmbeddedImageLink;
|
||||
class MediaLibrary;
|
||||
class Medium;
|
||||
class Release;
|
||||
class Session;
|
||||
class TrackArtistLink;
|
||||
@@ -82,11 +84,11 @@ namespace lms::db
|
||||
std::string artistName; // only tracks that involve this artist name
|
||||
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
bool nonRelease{}; // only tracks that do not belong to a release
|
||||
MediumId medium; // matching this medium
|
||||
ReleaseId release; // matching this release
|
||||
std::string releaseName; // matching this release name
|
||||
TrackListId trackList; // matching this trackList
|
||||
std::optional<int> trackNumber; // matching this track number
|
||||
std::optional<int> discNumber; // matching this disc number
|
||||
DirectoryId directory; // if set, tracks in this directory
|
||||
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
|
||||
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
|
||||
@@ -144,6 +146,11 @@ namespace lms::db
|
||||
nonRelease = _nonRelease;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setMedium(MediumId _medium)
|
||||
{
|
||||
medium = _medium;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
@@ -164,11 +171,6 @@ namespace lms::db
|
||||
trackNumber = _trackNumber;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDiscNumber(int _discNumber)
|
||||
{
|
||||
discNumber = _discNumber;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDirectory(DirectoryId _directory)
|
||||
{
|
||||
directory = _directory;
|
||||
@@ -217,9 +219,6 @@ namespace lms::db
|
||||
// Accessors
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
void setDiscNumber(std::optional<int> num) { _discNumber = num; }
|
||||
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
|
||||
void setDiscSubtitle(std::string_view name) { _discSubtitle = name; }
|
||||
void setName(std::string_view name);
|
||||
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||
@@ -237,13 +236,13 @@ namespace lms::db
|
||||
void setCopyright(std::string_view copyright);
|
||||
void setCopyrightURL(std::string_view copyrightURL);
|
||||
void setAdvisory(Advisory advisory) { _advisory = advisory; }
|
||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
|
||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
void setMedium(ObjectPtr<Medium> medium) { _medium = getDboPtr(medium); }
|
||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
|
||||
void clearLyrics();
|
||||
void clearEmbeddedLyrics();
|
||||
@@ -257,9 +256,6 @@ namespace lms::db
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
|
||||
std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::string getName() const { return _name; }
|
||||
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||
long long getFileSize() const { return _fileSize; }
|
||||
@@ -281,8 +277,7 @@ namespace lms::db
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
Advisory getAdvisory() const { return _advisory; }
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
std::optional<float> getReplayGain() const { return _replayGain; }
|
||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
|
||||
@@ -292,6 +287,8 @@ namespace lms::db
|
||||
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
|
||||
ReleaseId getReleaseId() const { return _release.id(); }
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
MediumId getMediumId() const { return _medium.id(); }
|
||||
ObjectPtr<Medium> getMedium() const { return _medium; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
||||
@@ -308,9 +305,6 @@ namespace lms::db
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _discNumber, "disc_number");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||
@@ -328,11 +322,11 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _advisory, "advisory");
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||
Wt::Dbo::field(a, _replayGain, "replay_gain");
|
||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
|
||||
|
||||
Wt::Dbo::belongsTo(a, _medium, "medium", 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, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -353,10 +347,7 @@ namespace lms::db
|
||||
static constexpr std::size_t _maxCopyrightURLLength{ 512 };
|
||||
|
||||
int _scanVersion{};
|
||||
std::optional<int> _trackNumber{};
|
||||
std::optional<int> _discNumber{};
|
||||
std::optional<int> _totalTrack{};
|
||||
std::string _discSubtitle;
|
||||
std::optional<int> _trackNumber;
|
||||
std::string _name;
|
||||
int _bitrate{}; // in bps
|
||||
int _bitsPerSample{};
|
||||
@@ -374,10 +365,10 @@ namespace lms::db
|
||||
std::string _copyright;
|
||||
std::string _copyrightURL;
|
||||
Advisory _advisory{ Advisory::UnSet };
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
std::optional<float> _replayGain;
|
||||
std::string _artistDisplayName;
|
||||
std::string _comment;
|
||||
Wt::Dbo::ptr<Medium> _medium;
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||
Wt::Dbo::ptr<Directory> _directory;
|
||||
|
||||
@@ -26,10 +26,9 @@
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
@@ -49,11 +48,9 @@ namespace lms::db
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ArtistId artist;
|
||||
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes;
|
||||
TrackId track;
|
||||
ReleaseId release;
|
||||
std::optional<int> discNumber;
|
||||
MediumId medium;
|
||||
TrackListId trackList;
|
||||
std::optional<ImageType> imageType;
|
||||
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
|
||||
@@ -63,13 +60,6 @@ namespace lms::db
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
@@ -80,9 +70,9 @@ namespace lms::db
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDiscNumber(std::optional<int> _discNumber)
|
||||
FindParameters& setMedium(MediumId _medium)
|
||||
{
|
||||
discNumber = _discNumber;
|
||||
medium = _medium;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrackList(TrackListId _trackList)
|
||||
|
||||
@@ -10,6 +10,7 @@ add_executable(test-database
|
||||
Directory.cpp
|
||||
Image.cpp
|
||||
Listen.cpp
|
||||
Medium.cpp
|
||||
Migration.cpp
|
||||
PlayListFile.cpp
|
||||
RatedArtist.cpp
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedDirectory = ScopedEntity<db::Directory>;
|
||||
using ScopedMedium = ScopedEntity<db::Medium>;
|
||||
|
||||
TEST_F(DatabaseFixture, Directory)
|
||||
{
|
||||
@@ -285,4 +287,57 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(res.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Directory_findByMedium)
|
||||
{
|
||||
ScopedDirectory dir1{ session, "/root" };
|
||||
ScopedDirectory dir2{ session, "/root" };
|
||||
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
ScopedRelease release1{ session, "Release1" };
|
||||
ScopedMedium medium1{ session, release1.lockAndGet() };
|
||||
|
||||
ScopedRelease release2{ session, "Release2" };
|
||||
ScopedMedium medium2{ session, release1.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Directory::FindParameters params;
|
||||
params.setMedium(medium1.getId());
|
||||
|
||||
bool visited{};
|
||||
Directory::find(session, params, [&](const Directory::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setMedium(medium1.get());
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track1.get().modify()->setDirectory(dir1.get());
|
||||
|
||||
track2.get().modify()->setMedium(medium2.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
track2.get().modify()->setDirectory(dir2.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Directory::FindParameters params;
|
||||
params.setMedium(medium1.getId());
|
||||
|
||||
std::vector<DirectoryId> visitedDirectories;
|
||||
Directory::find(session, params, [&](const Directory::pointer& dir) {
|
||||
visitedDirectories.push_back(dir->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedDirectories.size(), 1);
|
||||
EXPECT_EQ(visitedDirectories[0], dir1.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -122,4 +122,102 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Image_findByFileStem)
|
||||
{
|
||||
ScopedDirectory directory{ session, "/path/to" };
|
||||
ScopedImage image{ session, "/path/to/image" };
|
||||
|
||||
const std::string fileStem{ "image" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
image.get().modify()->setDirectory(directory.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem(fileStem);
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem(fileStem, Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("nonexistent");
|
||||
const auto results{ Image::find(session, params) };
|
||||
EXPECT_EQ(results.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("ima*");
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("ima*", Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("*ge", Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("*g*", Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("*", Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results[0]->getId(), image.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("ima%");
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
Image::FindParameters params;
|
||||
params.setFileStem("ima%", Image::FindParameters::ProcessWildcards{ true });
|
||||
const auto results{ Image::find(session, params) };
|
||||
ASSERT_EQ(results.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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/Types.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedMedium = ScopedEntity<db::Medium>;
|
||||
|
||||
TEST_F(DatabaseFixture, Medium)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Medium::getCount(session), 0);
|
||||
|
||||
Medium::pointer medium{ Medium::find(session, MediumId{}) };
|
||||
ASSERT_EQ(medium, Medium::pointer{});
|
||||
}
|
||||
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Medium::getCount(session), 1);
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
|
||||
ASSERT_NE(foundMedium, Medium::pointer{});
|
||||
EXPECT_EQ(foundMedium->getReleaseId(), release.getId());
|
||||
EXPECT_EQ(foundMedium->getPosition(), std::nullopt);
|
||||
EXPECT_EQ(foundMedium->getMedia(), "");
|
||||
EXPECT_EQ(foundMedium->getName(), "");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
medium.get().modify()->setName("MySubtitle");
|
||||
medium.get().modify()->setPosition(1);
|
||||
medium.get().modify()->setMedia("MyMedia");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
|
||||
ASSERT_NE(foundMedium, Medium::pointer{});
|
||||
EXPECT_EQ(foundMedium->getName(), "MySubtitle");
|
||||
EXPECT_EQ(foundMedium->getPosition(), 1);
|
||||
EXPECT_EQ(foundMedium->getMedia(), "MyMedia");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MediumFindByRelease_noPosition)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
|
||||
ASSERT_EQ(foundMedium, Medium::pointer{});
|
||||
}
|
||||
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
|
||||
ASSERT_NE(foundMedium, Medium::pointer{});
|
||||
EXPECT_EQ(foundMedium->getId(), medium.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MediumFindByRelease_withPosition)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
|
||||
ASSERT_EQ(foundMedium, Medium::pointer{});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
medium.get().modify()->setPosition(1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
|
||||
ASSERT_NE(foundMedium, Medium::pointer{});
|
||||
EXPECT_EQ(foundMedium->getId(), medium.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Medium_findTracksByMedium)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.setMedium(medium.getId());
|
||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
||||
|
||||
bool visited{};
|
||||
db::Track::find(session, params, [&](const db::Track::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.setMedium(medium.getId());
|
||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
||||
|
||||
bool visited{};
|
||||
db::Track::find(session, params, [&](const db::Track::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setMedium(medium.get());
|
||||
track2.get().modify()->setMedium(medium.get());
|
||||
|
||||
track1.get().modify()->setTrackNumber(3);
|
||||
track2.get().modify()->setTrackNumber(1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.setMedium(medium.getId());
|
||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
||||
|
||||
std::vector<db::TrackId> visitedTrackIds;
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
visitedTrackIds.push_back(track->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedTrackIds.size(), 2);
|
||||
EXPECT_EQ(visitedTrackIds[0], track2.getId());
|
||||
EXPECT_EQ(visitedTrackIds[1], track1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lms::db::tests
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "database/objects/AuthToken.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "database/objects/PlayQueue.hpp"
|
||||
#include "database/objects/RatedArtist.hpp"
|
||||
@@ -353,6 +354,7 @@ VALUES
|
||||
EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{}));
|
||||
EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{}));
|
||||
EXPECT_FALSE(Image::find(session, ImageId{}));
|
||||
EXPECT_FALSE(Medium::find(session, MediumId{}));
|
||||
EXPECT_FALSE(Label::find(session, LabelId{}));
|
||||
EXPECT_FALSE(Listen::find(session, ListenId{}));
|
||||
EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{}));
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedCountry = ScopedEntity<db::Country>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
using ScopedLabel = ScopedEntity<db::Label>;
|
||||
using ScopedCountry = ScopedEntity<db::Country>;
|
||||
using ScopedMedium = ScopedEntity<db::Medium>;
|
||||
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
|
||||
|
||||
TEST_F(DatabaseFixture, Release)
|
||||
@@ -438,15 +440,12 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setTotalTrack(36);
|
||||
release1.get().modify()->setTotalDisc(6);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 6);
|
||||
}
|
||||
@@ -456,15 +455,12 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setTotalTrack(37);
|
||||
release1.get().modify()->setTotalDisc(67);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 67);
|
||||
}
|
||||
@@ -481,18 +477,13 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track3.get().modify()->setRelease(release2.get());
|
||||
track3.get().modify()->setTotalTrack(7);
|
||||
release2.get().modify()->setTotalDisc(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
ASSERT_TRUE(release1->getTotalDisc());
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
ASSERT_TRUE(track3->getTotalTrack());
|
||||
EXPECT_EQ(*track3->getTotalTrack(), 7);
|
||||
ASSERT_TRUE(release2->getTotalDisc());
|
||||
EXPECT_EQ(*release2->getTotalDisc(), 5);
|
||||
}
|
||||
@@ -501,7 +492,11 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedMedium medium1A{ session, release1.lockAndGet() };
|
||||
ScopedMedium medium1B{ session, release1.lockAndGet() };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedMedium medium2A{ session, release2.lockAndGet() };
|
||||
ScopedMedium medium2B{ session, release2.lockAndGet() };
|
||||
|
||||
ScopedTrack track1A{ session };
|
||||
ScopedTrack track1B{ session };
|
||||
@@ -519,17 +514,21 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1A.get().modify()->setRelease(release1.get());
|
||||
track1A.get().modify()->setMedium(medium1A.get());
|
||||
|
||||
track1B.get().modify()->setRelease(release1.get());
|
||||
track1B.get().modify()->setMedium(medium1B.get());
|
||||
|
||||
track2A.get().modify()->setRelease(release2.get());
|
||||
track2A.get().modify()->setMedium(medium2A.get());
|
||||
track2B.get().modify()->setRelease(release2.get());
|
||||
track2B.get().modify()->setMedium(medium2B.get());
|
||||
|
||||
track1A.get().modify()->setTrackNumber(1);
|
||||
track1B.get().modify()->setTrackNumber(2);
|
||||
|
||||
track2A.get().modify()->setDiscNumber(2);
|
||||
track2A.get().modify()->setTrackNumber(1);
|
||||
track2B.get().modify()->setTrackNumber(2);
|
||||
track2B.get().modify()->setDiscNumber(1);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -537,14 +536,16 @@ namespace lms::db::tests
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results.front(), track1A.getId());
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track1A.getId());
|
||||
EXPECT_EQ(tracks.results[1], track1B.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results.front(), track2B.getId());
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], track2A.getId());
|
||||
EXPECT_EQ(tracks.results[1], track2B.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -815,52 +816,6 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setDiscNumber(6);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_isCompilation)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
@@ -1549,4 +1504,43 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_mediums)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto mediums{ release->getMediums() };
|
||||
EXPECT_EQ(mediums.size(), 0);
|
||||
}
|
||||
|
||||
ScopedMedium medium2{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto mediums{ release->getMediums() };
|
||||
ASSERT_EQ(mediums.size(), 1);
|
||||
EXPECT_EQ(mediums[0]->getId(), medium2.getId());
|
||||
}
|
||||
|
||||
ScopedMedium medium1{ session, release.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
medium1.get().modify()->setPosition(1);
|
||||
medium2.get().modify()->setPosition(2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto mediums{ release->getMediums() };
|
||||
ASSERT_EQ(mediums.size(), 2);
|
||||
EXPECT_EQ(mediums[0]->getId(), medium1.getId());
|
||||
EXPECT_EQ(mediums[1]->getId(), medium2.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <limits>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
@@ -28,6 +29,7 @@
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedMedium = ScopedEntity<db::Medium>;
|
||||
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
|
||||
using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>;
|
||||
|
||||
@@ -113,6 +115,7 @@ namespace lms::db::tests
|
||||
ScopedTrackEmbeddedImage image{ session };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||
|
||||
{
|
||||
@@ -133,6 +136,7 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setMedium(medium.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -151,13 +155,24 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setTrack(track.getId());
|
||||
params.setMedium(medium.getId());
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setTrack(track.getId());
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts)
|
||||
@@ -169,6 +184,8 @@ namespace lms::db::tests
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMedium medium1{ session, release.lockAndGet() };
|
||||
ScopedMedium medium2{ session, release.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() };
|
||||
@@ -176,7 +193,11 @@ namespace lms::db::tests
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
medium1.get().modify()->setPosition(1);
|
||||
medium2.get().modify()->setPosition(2);
|
||||
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
track1.get().modify()->setMedium(medium1.get());
|
||||
track1.get().modify()->setTrackNumber(2);
|
||||
|
||||
link1.get().modify()->setType(ImageType::FrontCover);
|
||||
@@ -187,6 +208,7 @@ namespace lms::db::tests
|
||||
image3.get().modify()->setSize(2000);
|
||||
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setMedium(medium2.get());
|
||||
track2.get().modify()->setTrackNumber(1);
|
||||
|
||||
link4.get().modify()->setType(ImageType::Media);
|
||||
@@ -213,16 +235,15 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setRelease(release.getId());
|
||||
params.setMedium(medium1.getId());
|
||||
params.setImageType(ImageType::Media);
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||
ASSERT_EQ(visitedIds.size(), 3);
|
||||
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||
ASSERT_EQ(visitedIds.size(), 2);
|
||||
EXPECT_EQ(visitedIds[0], image3.getId());
|
||||
EXPECT_EQ(visitedIds[1], image2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -236,9 +257,9 @@ namespace lms::db::tests
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||
ASSERT_EQ(visitedIds.size(), 3);
|
||||
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||
EXPECT_EQ(visitedIds[0], image3.getId());
|
||||
EXPECT_EQ(visitedIds[1], image2.getId());
|
||||
EXPECT_EQ(visitedIds[2], image4.getId());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -255,59 +276,26 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist)
|
||||
{
|
||||
ScopedTrackEmbeddedImage image{ session };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setArtist(artist.getId());
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist });
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_discNumber)
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_medium)
|
||||
{
|
||||
ScopedTrackEmbeddedImage image{ session };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMedium medium{ session, release.lockAndGet() };
|
||||
ScopedMedium otherMedium{ session, release.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setDiscNumber(1);
|
||||
track.get().modify()->setMedium(medium.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setDiscNumber(1);
|
||||
params.setMedium(medium.getId());
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
@@ -318,7 +306,7 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setDiscNumber(2);
|
||||
params.setMedium(2);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace lms::image::GraphicsMagick
|
||||
catch (Magick::Warning& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||
throw Exception{ std::string{ "Magick read warning: " } + e.what() };
|
||||
throw Exception{ std::string{ "Read warning: " } + e.what() };
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||
throw Exception{ std::string{ "Magick read error: " } + e.what() };
|
||||
throw Exception{ std::string{ "Read error: " } + e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ namespace lms::image::GraphicsMagick
|
||||
catch (Magick::Warning& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||
throw Exception{ std::string{ "Magick read warning: " } + e.what() };
|
||||
throw Exception{ std::string{ "Read warning: " } + e.what() };
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||
throw Exception{ std::string{ "Magick read error: " } + e.what() };
|
||||
throw Exception{ std::string{ "Read error: " } + e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace lms::image::GraphicsMagick
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what());
|
||||
throw Exception{ std::string{ "Magick resize error: " } + e.what() };
|
||||
throw Exception{ std::string{ "Resize error: " } + e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,17 @@
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
StbiException::StbiException(std::string_view desc)
|
||||
: Exception{ std::string{ desc } + ": " + getLastFailureReason() }
|
||||
namespace
|
||||
{
|
||||
}
|
||||
|
||||
std::string StbiException::getLastFailureReason()
|
||||
std::string getLastStbiFailureReason()
|
||||
{
|
||||
const char* failureReason{ ::stbi_failure_reason() };
|
||||
return failureReason ? failureReason : "unknown reason";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
StbiException::StbiException(std::string_view desc)
|
||||
: Exception{ std::string{ desc } + ": " + getLastStbiFailureReason() }
|
||||
{
|
||||
}
|
||||
} // namespace lms::image
|
||||
@@ -29,8 +29,5 @@ namespace lms::image
|
||||
{
|
||||
public:
|
||||
StbiException(std::string_view desc);
|
||||
|
||||
private:
|
||||
static std::string getLastFailureReason();
|
||||
};
|
||||
} // namespace lms::image
|
||||
@@ -46,7 +46,7 @@ namespace lms::auth
|
||||
|
||||
void LoginThrottler::removeOutdatedEntries()
|
||||
{
|
||||
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
|
||||
const Clock::time_point now{ Clock::now() };
|
||||
|
||||
for (auto it{ std::begin(_attemptsInfo) }; it != std::end(_attemptsInfo);)
|
||||
{
|
||||
@@ -60,7 +60,7 @@ namespace lms::auth
|
||||
void LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
|
||||
{
|
||||
const boost::asio::ip::address clientAddress{ getAddressToThrottle(address) };
|
||||
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
|
||||
const Clock::time_point now{ Clock::now() };
|
||||
|
||||
if (_attemptsInfo.size() >= _maxEntries)
|
||||
removeOutdatedEntries();
|
||||
@@ -68,7 +68,7 @@ namespace lms::auth
|
||||
_attemptsInfo.erase(core::random::pickRandom(_attemptsInfo));
|
||||
|
||||
AttemptInfo& attemptInfo{ _attemptsInfo[address] };
|
||||
if (attemptInfo.nextAttempt.isValid())
|
||||
if (attemptInfo.nextAttempt != Clock::time_point{})
|
||||
{
|
||||
assert(attemptInfo.nextAttempt <= now); // should not be called if throttled
|
||||
attemptInfo = {};
|
||||
@@ -79,12 +79,12 @@ namespace lms::auth
|
||||
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
|
||||
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'");
|
||||
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
|
||||
LMS_LOG(AUTH, INFO, "Throttling '" << clientAddress.to_string() << "' for " << std::chrono::duration_cast<std::chrono::seconds>(_throttlingDuration).count() << " seconds");
|
||||
attemptInfo.nextAttempt = now + _throttlingDuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
attemptInfo.nextAttempt = {};
|
||||
attemptInfo.nextAttempt = Clock::time_point{};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,9 @@ namespace lms::auth
|
||||
if (it == _attemptsInfo.end())
|
||||
return false;
|
||||
|
||||
if (!it->second.nextAttempt.isValid())
|
||||
if (it->second.nextAttempt == Clock::time_point{})
|
||||
return false;
|
||||
|
||||
return it->second.nextAttempt > Wt::WDateTime::currentDateTime();
|
||||
return it->second.nextAttempt > Clock::now();
|
||||
}
|
||||
} // namespace lms::auth
|
||||
@@ -22,15 +22,15 @@
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/NetAddress.hpp"
|
||||
#include "core/NetAddress.hpp" // for unordered_map of boost::asio::ip::address
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class LoginThrottler
|
||||
{
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
LoginThrottler(std::size_t maxEntries)
|
||||
: _maxEntries{ maxEntries } {}
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace lms::auth
|
||||
|
||||
struct AttemptInfo
|
||||
{
|
||||
Wt::WDateTime nextAttempt;
|
||||
Clock::time_point nextAttempt{};
|
||||
std::size_t badConsecutiveAttemptCount{};
|
||||
};
|
||||
std::unordered_map<boost::asio::ip::address, AttemptInfo> _attemptsInfo;
|
||||
|
||||
@@ -13,6 +13,7 @@ add_library(lmsscanner STATIC
|
||||
impl/steps/ScanStepArtistReconciliation.cpp
|
||||
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||
impl/steps/ScanStepAssociateMediumImages.cpp
|
||||
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||
impl/steps/ScanStepAssociateTrackImages.cpp
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "steps/ScanStepArtistReconciliation.hpp"
|
||||
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
||||
#include "steps/ScanStepAssociateMediumImages.hpp"
|
||||
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
||||
#include "steps/ScanStepAssociateReleaseImages.hpp"
|
||||
#include "steps/ScanStepAssociateTrackImages.hpp"
|
||||
@@ -506,7 +507,8 @@ namespace lms::scanner
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params)); // must come after ScanStepAssociateReleaseImages
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateTrackImages>(params)); // must come after ScanStepAssociateReleaseImages
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateMediumImages>(params)); // must come after ScanStepAssociateReleaseImages
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateTrackImages>(params)); // must come after ScanStepAssociateMediumImages and ScanStepAssociateReleaseImages
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
@@ -222,6 +223,26 @@ namespace lms::scanner
|
||||
return release;
|
||||
}
|
||||
|
||||
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
|
||||
{
|
||||
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
|
||||
if (!dbMedium)
|
||||
dbMedium = session.create<db::Medium>(release);
|
||||
|
||||
if (dbMedium->getPosition() != medium.position)
|
||||
dbMedium.modify()->setPosition(medium.position);
|
||||
if (dbMedium->getMedia() != medium.media)
|
||||
dbMedium.modify()->setMedia(medium.media);
|
||||
if (dbMedium->getName() != medium.name)
|
||||
dbMedium.modify()->setName(medium.name);
|
||||
if (dbMedium->getTrackCount() != medium.trackCount)
|
||||
dbMedium.modify()->setTrackCount(medium.trackCount);
|
||||
if (dbMedium->getReplayGain() != medium.replayGain)
|
||||
dbMedium.modify()->setReplayGain(medium.replayGain);
|
||||
|
||||
return dbMedium;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
@@ -387,8 +408,6 @@ namespace lms::scanner
|
||||
params.setFileSize(fileSize);
|
||||
if (parsedTrack.medium)
|
||||
{
|
||||
if (parsedTrack.medium->position)
|
||||
params.setDiscNumber(*parsedTrack.medium->position);
|
||||
if (parsedTrack.medium->release)
|
||||
params.setReleaseName(parsedTrack.medium->release->name);
|
||||
}
|
||||
@@ -507,7 +526,7 @@ namespace lms::scanner
|
||||
}
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
addError<EmbeddedImageScanError>(getFilePath(), index);
|
||||
addError<EmbeddedImageScanError>(getFilePath(), index, e.what());
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -691,17 +710,22 @@ namespace lms::scanner
|
||||
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
|
||||
|
||||
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory));
|
||||
{
|
||||
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
|
||||
assert(release);
|
||||
track.modify()->setRelease(release);
|
||||
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
|
||||
}
|
||||
else
|
||||
{
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt);
|
||||
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt);
|
||||
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
|
||||
track.modify()->setMedium({});
|
||||
}
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
|
||||
track.modify()->setDate(_parsedTrack->date);
|
||||
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
|
||||
@@ -719,7 +743,7 @@ namespace lms::scanner
|
||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setTrackReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||
|
||||
track.modify()->clearEmbeddedLyrics();
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace lms::scanner
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
_parsedImageProperties.reset();
|
||||
addError<ImageFileScanError>(getFilePath());
|
||||
addError<ImageFileScanError>(getFilePath(), e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace lms::scanner
|
||||
|
||||
void ScanErrorLogger::visit(const scanner::EmbeddedImageScanError& error)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << error.path << " at index " << error.index);
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << error.path << " at index " << error.index << ": " << error.errorMsg);
|
||||
}
|
||||
|
||||
void ScanErrorLogger::visit(const scanner::NoAudioTrackFoundError& error)
|
||||
@@ -68,7 +68,7 @@ namespace lms::scanner
|
||||
|
||||
void ScanErrorLogger::visit(const scanner::ImageFileScanError& error)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to read image file " << error.path);
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to read image file " << error.path << ": " << error.errorMsg);
|
||||
}
|
||||
|
||||
void ScanErrorLogger::visit(const scanner::LyricsFileScanError& error)
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace lms::scanner
|
||||
{
|
||||
db::Image::FindParameters params;
|
||||
params.setDirectory(directory->getId());
|
||||
params.setFileStem(fileStem);
|
||||
params.setFileStem(fileStem, db::Image::FindParameters::ProcessWildcards{ true }); // no need to sanitize here, user is responsible for providing sanitized file stems in conf file
|
||||
|
||||
db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
|
||||
if (!image)
|
||||
@@ -135,13 +135,11 @@ namespace lms::scanner
|
||||
{
|
||||
// Expect layout like this:
|
||||
// ReleaseArtist/Release/Tracks'
|
||||
// /artist.jpg
|
||||
// /someOtherUserConfiguredArtistFile.jpg
|
||||
// /someUserConfiguredArtistFile.jpg
|
||||
//
|
||||
// Or:
|
||||
// ReleaseArtist/SomeGrouping/Release/Tracks'
|
||||
// /artist.jpg
|
||||
// /someOtherUserConfiguredArtistFile.jpg
|
||||
// /someUserConfiguredArtistFile.jpg
|
||||
//
|
||||
std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
|
||||
while (true)
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 "ScanStepAssociateMediumImages.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include <set>
|
||||
#include <span>
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/IJob.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Path.hpp"
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/ArtistInfo.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/ArtworkId.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "JobQueue.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct MediumArtworkAssociation
|
||||
{
|
||||
db::MediumId mediumId;
|
||||
db::ArtworkId preferredArtworkId;
|
||||
};
|
||||
using MediumArtworkAssociationContainer = std::deque<MediumArtworkAssociation>;
|
||||
|
||||
struct SearchMediumArtworkParams
|
||||
{
|
||||
std::span<const std::string_view> mediumFileNames;
|
||||
};
|
||||
|
||||
db::Image::pointer findImageInDirectory(db::Session& session, const db::Directory::pointer& directory, std::span<const std::string_view> fileStemsToSearch, db::Image::FindParameters::ProcessWildcards processWildcards)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
for (std::string_view fileStem : fileStemsToSearch)
|
||||
{
|
||||
db::Image::FindParameters params;
|
||||
params.setDirectory(directory->getId());
|
||||
params.setFileStem(fileStem, processWildcards);
|
||||
|
||||
db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
|
||||
if (!image)
|
||||
image = foundImg;
|
||||
});
|
||||
|
||||
if (image)
|
||||
break;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer searchImageInDirectories(db::Session& session, const SearchMediumArtworkParams& searchParams, const db::Medium::pointer& medium)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
std::set<std::filesystem::path> mediumPaths;
|
||||
db::Directory::FindParameters params;
|
||||
params.setMedium(medium->getId());
|
||||
|
||||
// Expect layout like this:
|
||||
// Release/Tracks
|
||||
// /NameOfTheDisc.jpg
|
||||
// /someOtherUserConfiguredMediumFile.jpg
|
||||
//
|
||||
// Or:
|
||||
// Release/CD X/Tracks'
|
||||
// /NameOfDisc.jpg.jpg
|
||||
// /someOtherUserConfiguredMediumFile.jpg
|
||||
//
|
||||
// We don't expect mediums to be split across multiple directories, so we can just search for the first directory that matches the medium.
|
||||
db::Directory::find(session, params, [&](const db::Directory::pointer& directory) {
|
||||
if (image)
|
||||
return;
|
||||
|
||||
if (const std::string mediumName{ core::pathUtils::sanitizeFileStem(medium->getName()) }; !mediumName.empty())
|
||||
{
|
||||
std::string_view mediumNameView{ mediumName };
|
||||
image = findImageInDirectory(session, directory, std::span{ &mediumNameView, 1 }, db::Image::FindParameters::ProcessWildcards{ false });
|
||||
}
|
||||
|
||||
if (!image)
|
||||
image = findImageInDirectory(session, directory, searchParams.mediumFileNames, db::Image::FindParameters::ProcessWildcards{ true });
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImage::pointer getArtworkFromTracks(db::Session& session, const db::Medium::pointer& medium)
|
||||
{
|
||||
db::TrackEmbeddedImage::pointer image;
|
||||
|
||||
db::TrackEmbeddedImage::FindParameters params;
|
||||
params.setMedium(medium->getId());
|
||||
params.setImageType(db::ImageType::Media);
|
||||
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& foundImage) {
|
||||
if (image)
|
||||
return;
|
||||
|
||||
image = foundImage;
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Artwork::pointer computePreferredMediumArtwork(db::Session& session, const SearchMediumArtworkParams& searchParams, const db::Medium::pointer& medium)
|
||||
{
|
||||
if (const db::Image::pointer image{ searchImageInDirectories(session, searchParams, medium) })
|
||||
return db::Artwork::find(session, image->getId());
|
||||
|
||||
if (const db::TrackEmbeddedImage::pointer image{ getArtworkFromTracks(session, medium) })
|
||||
return db::Artwork::find(session, image->getId());
|
||||
|
||||
return db::Artwork::pointer{};
|
||||
}
|
||||
|
||||
void updateMediumPreferredArtwork(db::Session& session, const MediumArtworkAssociation& mediumArtworkAssociation)
|
||||
{
|
||||
db::Medium::updatePreferredArtwork(session, mediumArtworkAssociation.mediumId, mediumArtworkAssociation.preferredArtworkId);
|
||||
}
|
||||
|
||||
void updateMediumPreferredArtworks(db::Session& session, MediumArtworkAssociationContainer& imageAssociations, bool forceFullBatch)
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||
{
|
||||
updateMediumPreferredArtwork(session, imageAssociations.front());
|
||||
imageAssociations.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> constructMediumFileNames()
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
core::Service<core::IConfig>::get()->visitStrings("medium-image-file-names",
|
||||
[&res](std::string_view fileName) {
|
||||
res.emplace_back(fileName);
|
||||
},
|
||||
{ "discsubtitle" });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool fetchNextMediumIdRange(db::Session& session, db::MediumId& lastRetrievedId, db::IdRange<db::MediumId>& idRange)
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 100 };
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
idRange = db::Medium::findNextIdRange(session, lastRetrievedId, readBatchSize);
|
||||
lastRetrievedId = idRange.last;
|
||||
|
||||
return idRange.isValid();
|
||||
}
|
||||
|
||||
class ComputeMediumArtworkAssociationsJob : public core::IJob
|
||||
{
|
||||
public:
|
||||
ComputeMediumArtworkAssociationsJob(db::IDb& db, const SearchMediumArtworkParams& searchParams, db::IdRange<db::MediumId> mediumIdRange)
|
||||
: _db{ db }
|
||||
, _searchParams{ searchParams }
|
||||
, _mediumIdRange{ mediumIdRange }
|
||||
{
|
||||
}
|
||||
~ComputeMediumArtworkAssociationsJob() override = default;
|
||||
|
||||
ComputeMediumArtworkAssociationsJob(const ComputeMediumArtworkAssociationsJob&) = delete;
|
||||
ComputeMediumArtworkAssociationsJob& operator=(const ComputeMediumArtworkAssociationsJob&) = delete;
|
||||
|
||||
std::span<const MediumArtworkAssociation> getAssociations() const { return _associations; }
|
||||
std::size_t getProcessedMediumCount() const { return _processedMediumCount; }
|
||||
|
||||
private:
|
||||
core::LiteralString getName() const override { return "Associate Medium Artworks"; }
|
||||
void run() override
|
||||
{
|
||||
auto& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Medium::find(session, _mediumIdRange, [this, &session](const db::Medium::pointer& medium) {
|
||||
const db::Artwork::pointer preferredArtwork{ computePreferredMediumArtwork(session, _searchParams, medium) };
|
||||
|
||||
if (medium->getPreferredArtwork() != preferredArtwork)
|
||||
{
|
||||
_associations.push_back(MediumArtworkAssociation{ medium->getId(), preferredArtwork ? preferredArtwork->getId() : db::ArtworkId{} });
|
||||
|
||||
if (preferredArtwork)
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork for medium '" << medium->getName() << "(from '" << medium->getRelease()->getName() << "') with image in " << preferredArtwork->getAbsoluteFilePath());
|
||||
else
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Removing preferred artwork from medium '" << medium->getName() << "(from '" << medium->getRelease()->getName() << "')");
|
||||
}
|
||||
|
||||
_processedMediumCount++;
|
||||
});
|
||||
}
|
||||
|
||||
db::IDb& _db;
|
||||
const SearchMediumArtworkParams& _searchParams;
|
||||
db::IdRange<db::MediumId> _mediumIdRange;
|
||||
std::vector<MediumArtworkAssociation> _associations;
|
||||
std::size_t _processedMediumCount{};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ScanStepAssociateMediumImages::ScanStepAssociateMediumImages(InitParams& initParams)
|
||||
: ScanStepBase{ initParams }
|
||||
, _mediumFileNames{ constructMediumFileNames() }
|
||||
{
|
||||
}
|
||||
|
||||
bool ScanStepAssociateMediumImages::needProcess(const ScanContext& context) const
|
||||
{
|
||||
return context.stats.getChangesCount() > 0;
|
||||
}
|
||||
|
||||
void ScanStepAssociateMediumImages::process(ScanContext& context)
|
||||
{
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
context.currentStepStats.totalElems = db::Artist::getCount(session);
|
||||
}
|
||||
|
||||
std::vector<std::string_view> mediumFileNames;
|
||||
mediumFileNames.reserve(_mediumFileNames.size());
|
||||
for (const std::string& fileName : _mediumFileNames)
|
||||
mediumFileNames.push_back(fileName);
|
||||
|
||||
const SearchMediumArtworkParams searchParams{
|
||||
.mediumFileNames = mediumFileNames,
|
||||
};
|
||||
|
||||
MediumArtworkAssociationContainer mediumArtworkAssociations;
|
||||
auto processJobsDone = [&](std::span<std::unique_ptr<core::IJob>> jobs) {
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
for (const auto& job : jobs)
|
||||
{
|
||||
const auto& associationJob{ static_cast<const ComputeMediumArtworkAssociationsJob&>(*job) };
|
||||
const auto& artistAssociations{ associationJob.getAssociations() };
|
||||
|
||||
mediumArtworkAssociations.insert(std::end(mediumArtworkAssociations), std::cbegin(artistAssociations), std::cend(artistAssociations));
|
||||
|
||||
context.currentStepStats.processedElems += associationJob.getProcessedMediumCount();
|
||||
}
|
||||
|
||||
updateMediumPreferredArtworks(session, mediumArtworkAssociations, true);
|
||||
_progressCallback(context.currentStepStats);
|
||||
};
|
||||
|
||||
{
|
||||
JobQueue queue{ getJobScheduler(), 20, processJobsDone, 1, 0.85F };
|
||||
|
||||
db::MediumId lastRetrievedMediumId{};
|
||||
db::IdRange<db::MediumId> mediumIdRange;
|
||||
while (fetchNextMediumIdRange(session, lastRetrievedMediumId, mediumIdRange))
|
||||
queue.push(std::make_unique<ComputeMediumArtworkAssociationsJob>(_db, searchParams, mediumIdRange));
|
||||
}
|
||||
|
||||
// process all remaining associations
|
||||
updateMediumPreferredArtworks(session, mediumArtworkAssociations, false);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 ScanStepAssociateMediumImages : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepAssociateMediumImages(InitParams& initParams);
|
||||
~ScanStepAssociateMediumImages() override = default;
|
||||
ScanStepAssociateMediumImages(const ScanStepAssociateMediumImages&) = delete;
|
||||
ScanStepAssociateMediumImages& operator=(const ScanStepAssociateMediumImages&) = delete;
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
|
||||
core::LiteralString getStepName() const override { return "Associate medium images"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
const std::vector<std::string> _mediumFileNames;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -67,7 +67,7 @@ namespace lms::scanner
|
||||
{
|
||||
db::Image::FindParameters params;
|
||||
params.setDirectory(directory->getId());
|
||||
params.setFileStem(fileStem);
|
||||
params.setFileStem(fileStem, db::Image::FindParameters::ProcessWildcards{ true }); // no need to sanitize here, user is responsible for providing sanitized file stems in conf file
|
||||
|
||||
db::Image::find(session, params, [&](const db::Image::pointer& image) {
|
||||
if (!artwork)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -105,24 +106,9 @@ namespace lms::scanner
|
||||
if (res)
|
||||
return res;
|
||||
|
||||
// fallback on another track of the same disc
|
||||
const db::ReleaseId releaseId{ track->getReleaseId() };
|
||||
if (!releaseId.isValid())
|
||||
return res;
|
||||
|
||||
if (const auto discNumber{ track->getDiscNumber() })
|
||||
{
|
||||
db::TrackEmbeddedImage::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
params.setDiscNumber(discNumber);
|
||||
params.setImageType(db::ImageType::Media);
|
||||
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) {
|
||||
if (!res)
|
||||
res = db::Artwork::find(session, image->getId());
|
||||
});
|
||||
}
|
||||
// fallback on the medium's preferred artwork
|
||||
if (const auto medium{ track->getMedium() })
|
||||
res = medium->getPreferredArtwork();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -45,6 +46,7 @@ namespace lms::scanner
|
||||
removeOrphanedClusterTypes(context);
|
||||
removeOrphanedArtists(context);
|
||||
removeOrphanedReleases(context);
|
||||
removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key
|
||||
removeOrphanedReleaseTypes(context);
|
||||
removeOrphanedLabels(context);
|
||||
removeOrphanedCountries(context);
|
||||
@@ -70,6 +72,12 @@ namespace lms::scanner
|
||||
removeOrphanedEntries<db::Artist>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedMediums(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned mediums...");
|
||||
removeOrphanedEntries<db::Medium>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleases(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases...");
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace lms::scanner
|
||||
void removeOrphanedClusters(ScanContext& context);
|
||||
void removeOrphanedClusterTypes(ScanContext& context);
|
||||
void removeOrphanedArtists(ScanContext& context);
|
||||
void removeOrphanedMediums(ScanContext& context);
|
||||
void removeOrphanedReleases(ScanContext& context);
|
||||
void removeOrphanedReleaseTypes(ScanContext& context);
|
||||
void removeOrphanedLabels(ScanContext& context);
|
||||
|
||||
@@ -95,9 +95,12 @@ namespace lms::scanner
|
||||
|
||||
struct EmbeddedImageScanError : public AudioFileScanError
|
||||
{
|
||||
EmbeddedImageScanError(const std::filesystem::path& p, unsigned i)
|
||||
EmbeddedImageScanError(const std::filesystem::path& p, unsigned i, std::string_view e)
|
||||
: AudioFileScanError{ p }
|
||||
, index{ i } {}
|
||||
, index{ i }
|
||||
, errorMsg{ e }
|
||||
{
|
||||
}
|
||||
|
||||
void accept(ScanErrorVisitor& visitor) const override
|
||||
{
|
||||
@@ -105,6 +108,7 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
unsigned index;
|
||||
std::string errorMsg;
|
||||
};
|
||||
|
||||
struct NoAudioTrackFoundError : public AudioFileScanError
|
||||
@@ -149,12 +153,16 @@ namespace lms::scanner
|
||||
|
||||
struct ImageFileScanError : public ScanError
|
||||
{
|
||||
using ScanError::ScanError;
|
||||
ImageFileScanError(const std::filesystem::path& p, std::string_view e)
|
||||
: ScanError{ p }
|
||||
, errorMsg{ e } {}
|
||||
|
||||
void accept(ScanErrorVisitor& visitor) const override
|
||||
{
|
||||
visitor.visit(*this);
|
||||
}
|
||||
|
||||
std::string errorMsg;
|
||||
};
|
||||
|
||||
struct LyricsFileScanError : public ScanError
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "core/StreamLogger.hpp"
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
using namespace lms;
|
||||
// log to stdout
|
||||
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout, core::EnumSet<core::logging::Severity>{ core::logging::Severity::FATAL, core::logging::Severity::ERROR }) };
|
||||
core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::ERROR) };
|
||||
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
@@ -207,10 +208,12 @@ namespace lms::api::subsonic
|
||||
albumNode.addArrayValue("releaseTypes", releaseType);
|
||||
|
||||
albumNode.createEmptyArrayChild("discTitles");
|
||||
for (const DiscInfo& discInfo : release->getDiscs())
|
||||
for (const auto& medium : release->getMediums())
|
||||
{
|
||||
if (!discInfo.name.empty())
|
||||
albumNode.addArrayChild("discTitles", createDiscTitle(discInfo));
|
||||
if (medium->getName().empty())
|
||||
continue;
|
||||
|
||||
albumNode.addArrayChild("discTitles", createDiscTitle(medium));
|
||||
}
|
||||
|
||||
albumNode.createEmptyArrayChild("recordLabels");
|
||||
|
||||
@@ -19,14 +19,16 @@
|
||||
|
||||
#include "responses/DiscTitle.hpp"
|
||||
|
||||
#include "database/objects/Medium.hpp"
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
Response::Node createDiscTitle(const db::DiscInfo& discInfo)
|
||||
Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium)
|
||||
{
|
||||
Response::Node discTitleNode;
|
||||
|
||||
discTitleNode.setAttribute("disc", discInfo.position);
|
||||
discTitleNode.setAttribute("title", discInfo.name);
|
||||
discTitleNode.setAttribute("disc", medium->getPosition() ? *medium->getPosition() : 0);
|
||||
discTitleNode.setAttribute("title", medium->getName());
|
||||
|
||||
return discTitleNode;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/Object.hpp"
|
||||
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Medium;
|
||||
}
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
Response::Node createDiscTitle(const db::DiscInfo& discInfo);
|
||||
Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium);
|
||||
}
|
||||
@@ -19,19 +19,23 @@
|
||||
|
||||
#include "responses/ReplayGain.hpp"
|
||||
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track)
|
||||
Response::Node createReplayGainNode(const db::Track::pointer& track, const db::Medium::pointer& medium)
|
||||
{
|
||||
Response::Node replayGainNode;
|
||||
|
||||
if (const auto trackReplayGain{ track->getTrackReplayGain() })
|
||||
if (const auto trackReplayGain{ track->getReplayGain() })
|
||||
replayGainNode.setAttribute("trackGain", *trackReplayGain);
|
||||
|
||||
if (const auto releaseReplayGain{ track->getReleaseReplayGain() })
|
||||
if (medium)
|
||||
{
|
||||
if (const auto releaseReplayGain{ medium->getReplayGain() })
|
||||
replayGainNode.setAttribute("albumGain", *releaseReplayGain);
|
||||
}
|
||||
|
||||
return replayGainNode;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Medium;
|
||||
class Track;
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track);
|
||||
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track, const db::ObjectPtr<db::Medium>& medium);
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
@@ -78,6 +79,8 @@ namespace lms::api::subsonic
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
|
||||
|
||||
const auto medium{ track->getMedium() };
|
||||
|
||||
Response::Node trackResponse;
|
||||
|
||||
if (!id3)
|
||||
@@ -91,8 +94,8 @@ namespace lms::api::subsonic
|
||||
trackResponse.setAttribute("title", track->getName());
|
||||
if (track->getTrackNumber())
|
||||
trackResponse.setAttribute("track", *track->getTrackNumber());
|
||||
if (track->getDiscNumber())
|
||||
trackResponse.setAttribute("discNumber", *track->getDiscNumber());
|
||||
if (medium && medium->getPosition())
|
||||
trackResponse.setAttribute("discNumber", *medium->getPosition());
|
||||
if (const auto originalYear{ track->getOriginalYear() })
|
||||
trackResponse.setAttribute("year", *originalYear);
|
||||
else if (const auto year{ track->getYear() })
|
||||
@@ -254,7 +257,7 @@ namespace lms::api::subsonic
|
||||
};
|
||||
trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory()));
|
||||
|
||||
trackResponse.addChild("replayGain", createReplayGainNode(track));
|
||||
trackResponse.addChild("replayGain", createReplayGainNode(track, medium));
|
||||
|
||||
return trackResponse;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ add_executable(lms
|
||||
ui/Utils.cpp
|
||||
ui/admin/debug/Database.cpp
|
||||
ui/admin/debug/Tracing.cpp
|
||||
ui/admin/About.cpp
|
||||
ui/admin/DebugToolsView.cpp
|
||||
ui/admin/InitWizardView.cpp
|
||||
ui/admin/MediaLibrariesView.cpp
|
||||
|
||||
+84
-33
@@ -20,18 +20,19 @@
|
||||
#include <thread>
|
||||
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WLogSink.h>
|
||||
#include <Wt/WServer.h>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include "core/IChildProcessManager.hpp"
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/IOContextRunner.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "core/SystemPaths.hpp"
|
||||
#include "core/WtLogger.hpp"
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/IQueryPlanRecorder.hpp"
|
||||
#include "database/Session.hpp"
|
||||
@@ -64,24 +65,6 @@ namespace lms
|
||||
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
||||
}
|
||||
|
||||
core::logging::Severity getLogMinSeverity()
|
||||
{
|
||||
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
|
||||
|
||||
if (minSeverity == "debug")
|
||||
return core::logging::Severity::DEBUG;
|
||||
else if (minSeverity == "info")
|
||||
return core::logging::Severity::INFO;
|
||||
else if (minSeverity == "warning")
|
||||
return core::logging::Severity::WARNING;
|
||||
else if (minSeverity == "error")
|
||||
return core::logging::Severity::ERROR;
|
||||
else if (minSeverity == "fatal")
|
||||
return core::logging::Severity::FATAL;
|
||||
|
||||
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
|
||||
}
|
||||
|
||||
ui::AuthenticationBackend getUIAuthenticationBackend()
|
||||
{
|
||||
const std::string backend{ core::stringUtils::stringToLower(core::Service<core::IConfig>::get()->getString("authentication-backend", "internal")) };
|
||||
@@ -109,15 +92,13 @@ namespace lms
|
||||
throw core::LmsException{ "Invalid config value for 'tracing-level'" };
|
||||
}
|
||||
|
||||
std::vector<std::string> generateWtConfig(std::string execPath, core::logging::Severity minSeverity)
|
||||
std::vector<std::string> generateWtConfig(std::string execPath)
|
||||
{
|
||||
core::IConfig& config{ *core::Service<core::IConfig>::get() };
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" };
|
||||
const std::filesystem::path wtLogFilePath{ config.getPath("log-file", "") };
|
||||
const std::filesystem::path wtAccessLogFilePath{ config.getPath("access-log-file", "") };
|
||||
const std::filesystem::path wtResourcesPath{ config.getPath("wt-resources", "/usr/share/Wt/resources") };
|
||||
|
||||
args.push_back(execPath);
|
||||
@@ -142,19 +123,12 @@ namespace lms
|
||||
args.push_back("--http-address=" + std::string{ config.getString("listen-addr", "0.0.0.0") });
|
||||
}
|
||||
|
||||
if (!wtAccessLogFilePath.empty())
|
||||
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
||||
|
||||
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
||||
|
||||
// Generate the wt_config.xml file
|
||||
boost::property_tree::ptree pt;
|
||||
|
||||
pt.put("server.application-settings.<xmlattr>.location", "*");
|
||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||
|
||||
// log-config
|
||||
pt.put("server.application-settings.log-config", core::logging::WtLogger::computeLogConfig(minSeverity));
|
||||
|
||||
// Reverse proxy
|
||||
if (config.getBool("behind-reverse-proxy", false))
|
||||
@@ -238,6 +212,82 @@ namespace lms
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
core::logging::Severity getLogMinSeverity()
|
||||
{
|
||||
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
|
||||
|
||||
if (minSeverity == "debug")
|
||||
return core::logging::Severity::DEBUG;
|
||||
else if (minSeverity == "info")
|
||||
return core::logging::Severity::INFO;
|
||||
else if (minSeverity == "warning")
|
||||
return core::logging::Severity::WARNING;
|
||||
else if (minSeverity == "error")
|
||||
return core::logging::Severity::ERROR;
|
||||
else if (minSeverity == "fatal")
|
||||
return core::logging::Severity::FATAL;
|
||||
|
||||
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
|
||||
}
|
||||
|
||||
class LmsLogSink : public Wt::WLogSink
|
||||
{
|
||||
public:
|
||||
LmsLogSink(core::logging::ILogger& logger)
|
||||
: _logger{ logger }
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
void log(const std::string& type, const std::string& scope, const std::string& message) const noexcept override
|
||||
{
|
||||
// Some wt code path may go here without testing logging()
|
||||
if (logging(type, scope))
|
||||
{
|
||||
const core::logging::Severity severity{ getSeverity(type, scope) };
|
||||
_logger.processLog(core::logging::Module::WT, severity, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool logging(const std::string& type, const std::string& scope) const noexcept override
|
||||
{
|
||||
const core::logging::Severity severity{ getSeverity(type, scope) };
|
||||
return _logger.isSeverityActive(severity);
|
||||
}
|
||||
|
||||
static core::logging::Severity getSeverity(const std::string& type, const std::string& scope)
|
||||
{
|
||||
return adjustSeverity(getSeverityFromString(type), scope);
|
||||
}
|
||||
|
||||
static core::logging::Severity adjustSeverity(core::logging::Severity initialSeverity, std::string_view scope)
|
||||
{
|
||||
if (initialSeverity == core::logging::Severity::INFO && (scope == "WebRequest" || scope == "wthttp"))
|
||||
return core::logging::Severity::DEBUG;
|
||||
|
||||
return initialSeverity;
|
||||
}
|
||||
|
||||
static core::logging::Severity getSeverityFromString(std::string_view type)
|
||||
{
|
||||
if (type == "debug")
|
||||
return core::logging::Severity::DEBUG;
|
||||
if (type == "info")
|
||||
return core::logging::Severity::INFO;
|
||||
if (type == "warning")
|
||||
return core::logging::Severity::WARNING;
|
||||
if (type == "error")
|
||||
return core::logging::Severity::ERROR;
|
||||
if (type == "fatal")
|
||||
return core::logging::Severity::FATAL;
|
||||
|
||||
return core::logging::Severity::INFO;
|
||||
}
|
||||
|
||||
core::logging::ILogger& _logger;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
@@ -275,8 +325,7 @@ namespace lms
|
||||
close(STDIN_FILENO);
|
||||
|
||||
core::Service<core::IConfig> config{ core::createConfig(configFilePath) };
|
||||
const core::logging::Severity minLogSeverity{ getLogMinSeverity() };
|
||||
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::WtLogger>(minLogSeverity) };
|
||||
core::Service<core::logging::ILogger> logger{ createLogger(getLogMinSeverity(), config->getPath("log-file", "")) };
|
||||
core::Service<core::tracing::ITraceLogger> traceLogger;
|
||||
if (const auto level{ getTracingLevel() })
|
||||
traceLogger.assign(core::tracing::createTraceLogger(level.value(), config->getULong("tracing-buffer-size", core::tracing::MinBufferSizeInMBytes)));
|
||||
@@ -292,7 +341,7 @@ namespace lms
|
||||
std::filesystem::create_directories(config->getPath("working-dir", "/var/lms") / "cache");
|
||||
|
||||
// Construct WT configuration and get the argc/argv back
|
||||
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) };
|
||||
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0]) };
|
||||
|
||||
std::vector<const char*> wtArgv(wtServerArgs.size());
|
||||
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
||||
@@ -301,8 +350,9 @@ namespace lms
|
||||
wtArgv[i] = wtServerArgs[i].c_str();
|
||||
}
|
||||
|
||||
boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop
|
||||
LmsLogSink lmsLogSink{ *logger };
|
||||
Wt::WServer server{ argv[0] };
|
||||
server.setCustomLogger(lmsLogSink);
|
||||
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
|
||||
|
||||
// As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user
|
||||
@@ -314,6 +364,7 @@ namespace lms
|
||||
LMS_LOG(MAIN, INFO, "Starting init web server...");
|
||||
server.start();
|
||||
|
||||
boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop
|
||||
core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" };
|
||||
|
||||
core::Service<db::IQueryPlanRecorder> queryPlanRecorder;
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "NotificationContainer.hpp"
|
||||
#include "PlayQueue.hpp"
|
||||
#include "SettingsView.hpp"
|
||||
#include "admin/About.hpp"
|
||||
#include "admin/DebugToolsView.hpp"
|
||||
#include "admin/InitWizardView.hpp"
|
||||
#include "admin/MediaLibrariesView.hpp"
|
||||
@@ -76,6 +77,7 @@ namespace lms::ui
|
||||
const std::string appRoot{ Wt::WApplication::appRoot() };
|
||||
|
||||
auto res{ std::make_shared<Wt::WMessageResourceBundle>() };
|
||||
res->use(appRoot + "admin-about");
|
||||
res->use(appRoot + "admin-db");
|
||||
res->use(appRoot + "admin-debugtools");
|
||||
res->use(appRoot + "admin-initwizard");
|
||||
@@ -460,6 +462,10 @@ namespace lms::ui
|
||||
if (LmsApp->getUserType() == db::UserType::ADMIN)
|
||||
{
|
||||
navbar->setCondition("if-is-admin", true);
|
||||
auto* about{ navbar->bindNew<Wt::WPushButton>("about", Wt::WString::tr("Lms.Admin.menu-about"), Wt::TextFormat::XHTML) };
|
||||
about->clicked().connect([] {
|
||||
showAboutModal();
|
||||
});
|
||||
navbar->bindNew<Wt::WAnchor>("media-libraries", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/libraries" }, Wt::WString::tr("Lms.Admin.menu-media-libraries"));
|
||||
navbar->bindNew<Wt::WAnchor>("scan-settings", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scan-settings" }, Wt::WString::tr("Lms.Admin.menu-scan-settings"));
|
||||
navbar->bindNew<Wt::WAnchor>("scanner", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scanner" }, Wt::WString::tr("Lms.Admin.menu-scanner"));
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "core/Service.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
@@ -621,14 +622,20 @@ namespace lms::ui
|
||||
return std::nullopt;
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Track:
|
||||
gain = track->getTrackReplayGain();
|
||||
gain = track->getReplayGain();
|
||||
break;
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Release:
|
||||
gain = track->getReleaseReplayGain();
|
||||
{
|
||||
const auto medium{ track->getMedium() };
|
||||
if (medium && medium->getReplayGain())
|
||||
gain = medium->getReplayGain();
|
||||
|
||||
if (!gain)
|
||||
gain = track->getTrackReplayGain();
|
||||
gain = track->getReplayGain();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
|
||||
{
|
||||
@@ -641,13 +648,15 @@ namespace lms::ui
|
||||
if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease())
|
||||
|| (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease()))
|
||||
{
|
||||
gain = track->getReleaseReplayGain();
|
||||
const auto medium{ track->getMedium() };
|
||||
if (medium && medium->getReplayGain())
|
||||
gain = medium->getReplayGain();
|
||||
if (!gain)
|
||||
gain = track->getTrackReplayGain();
|
||||
gain = track->getReplayGain();
|
||||
}
|
||||
else
|
||||
{
|
||||
gain = track->getTrackReplayGain();
|
||||
gain = track->getReplayGain();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+26
-28
@@ -87,8 +87,6 @@ namespace lms::ui
|
||||
};
|
||||
} // namespace
|
||||
|
||||
using namespace db;
|
||||
|
||||
class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
@@ -192,7 +190,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||
|
||||
User::pointer user{ LmsApp->getUser() };
|
||||
db::User::pointer user{ LmsApp->getUser() };
|
||||
|
||||
{
|
||||
const auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) };
|
||||
@@ -307,7 +305,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ LmsApp->getUser() };
|
||||
const db::User::pointer user{ LmsApp->getUser() };
|
||||
|
||||
// UI
|
||||
{
|
||||
@@ -396,7 +394,7 @@ namespace lms::ui
|
||||
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
|
||||
|
||||
{
|
||||
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz };
|
||||
const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz };
|
||||
setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz);
|
||||
validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz);
|
||||
}
|
||||
@@ -472,17 +470,17 @@ namespace lms::ui
|
||||
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never);
|
||||
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported);
|
||||
|
||||
_transcodingOutputBitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
|
||||
visitAllowedAudioBitrates([&](const Bitrate bitrate) {
|
||||
_transcodingOutputBitrateModel = std::make_shared<ValueStringModel<db::Bitrate>>();
|
||||
db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) {
|
||||
_transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
|
||||
});
|
||||
|
||||
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<TranscodingOutputFormat>>();
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), TranscodingOutputFormat::MP3);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), TranscodingOutputFormat::OGG_OPUS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), TranscodingOutputFormat::MATROSKA_OPUS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), TranscodingOutputFormat::OGG_VORBIS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), TranscodingOutputFormat::WEBM_VORBIS);
|
||||
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<db::TranscodingOutputFormat>>();
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), db::TranscodingOutputFormat::MATROSKA_OPUS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS);
|
||||
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), db::TranscodingOutputFormat::WEBM_VORBIS);
|
||||
|
||||
_replayGainModeModel = std::make_shared<ReplayGainModeModel>();
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None);
|
||||
@@ -490,18 +488,18 @@ namespace lms::ui
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track);
|
||||
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release);
|
||||
|
||||
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<SubsonicArtistListMode>>();
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), SubsonicArtistListMode::AllArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), SubsonicArtistListMode::ReleaseArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), SubsonicArtistListMode::TrackArtists);
|
||||
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<db::SubsonicArtistListMode>>();
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), db::SubsonicArtistListMode::AllArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), db::SubsonicArtistListMode::ReleaseArtists);
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), db::SubsonicArtistListMode::TrackArtists);
|
||||
|
||||
_feedbackBackendModel = std::make_shared<ValueStringModel<FeedbackBackend>>();
|
||||
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), FeedbackBackend::Internal);
|
||||
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), FeedbackBackend::ListenBrainz);
|
||||
_feedbackBackendModel = std::make_shared<ValueStringModel<db::FeedbackBackend>>();
|
||||
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::FeedbackBackend::Internal);
|
||||
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::FeedbackBackend::ListenBrainz);
|
||||
|
||||
_scrobblingBackendModel = std::make_shared<ValueStringModel<ScrobblingBackend>>();
|
||||
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), ScrobblingBackend::Internal);
|
||||
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), ScrobblingBackend::ListenBrainz);
|
||||
_scrobblingBackendModel = std::make_shared<ValueStringModel<db::ScrobblingBackend>>();
|
||||
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::ScrobblingBackend::Internal);
|
||||
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::ScrobblingBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
auth::IPasswordService* _authPasswordService{};
|
||||
@@ -512,10 +510,10 @@ namespace lms::ui
|
||||
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
|
||||
std::shared_ptr<ArtistRelationshipsModel> _artistRelationshipsModel;
|
||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
||||
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
||||
std::shared_ptr<ValueStringModel<db::Bitrate>> _transcodingOutputBitrateModel;
|
||||
std::shared_ptr<ValueStringModel<db::TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
||||
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
|
||||
std::shared_ptr<ValueStringModel<SubsonicArtistListMode>> _subsonicArtistListModeModel;
|
||||
std::shared_ptr<ValueStringModel<db::SubsonicArtistListMode>> _subsonicArtistListModeModel;
|
||||
std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel;
|
||||
std::shared_ptr<ScrobblingBackendModel> _scrobblingBackendModel;
|
||||
};
|
||||
@@ -740,8 +738,8 @@ namespace lms::ui
|
||||
}
|
||||
|
||||
auto updateListenBrainzTokenField{ [=] {
|
||||
const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == FeedbackBackend::ListenBrainz
|
||||
|| model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == ScrobblingBackend::ListenBrainz };
|
||||
const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == db::FeedbackBackend::ListenBrainz
|
||||
|| model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == db::ScrobblingBackend::ListenBrainz };
|
||||
|
||||
model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable);
|
||||
model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 "ModalManager.hpp"
|
||||
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "core/Version.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
void showAboutModal()
|
||||
{
|
||||
auto aboutModal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.About.template")) };
|
||||
Wt::WWidget* aboutModalPtr{ aboutModal.get() };
|
||||
aboutModal->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
aboutModal->bindString("version", std::string{ core::getVersion() }, Wt::TextFormat::Plain);
|
||||
aboutModal->bindString("homepage-link", "https://github.com/epoupon/lms");
|
||||
|
||||
Wt::WPushButton* okBtn{ aboutModal->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
|
||||
okBtn->clicked().connect([=] {
|
||||
LmsApp->getModalManager().dispose(aboutModalPtr);
|
||||
});
|
||||
|
||||
LmsApp->getModalManager().show(std::move(aboutModal));
|
||||
}
|
||||
} // namespace lms::ui
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
void showAboutModal();
|
||||
} // namespace lms::ui
|
||||
@@ -83,8 +83,6 @@ namespace lms::ui
|
||||
|
||||
void MediaLibrariesView::showDeleteLibraryModal(db::MediaLibraryId mediaLibraryId, Wt::WTemplate* libraryEntry)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
auto modal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.MediaLibraries.template.delete-library")) };
|
||||
modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
Wt::WWidget* modalPtr{ modal.get() };
|
||||
@@ -94,7 +92,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||
|
||||
db::MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) };
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) };
|
||||
if (mediaLibrary)
|
||||
mediaLibrary.remove();
|
||||
}
|
||||
|
||||
@@ -33,14 +33,12 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
namespace
|
||||
{
|
||||
class LibraryNameValidator : public Wt::WValidator
|
||||
{
|
||||
public:
|
||||
LibraryNameValidator(MediaLibraryId libraryId)
|
||||
LibraryNameValidator(db::MediaLibraryId libraryId)
|
||||
: _libraryId{ libraryId } {}
|
||||
|
||||
private:
|
||||
@@ -66,13 +64,13 @@ namespace lms::ui
|
||||
return result;
|
||||
}
|
||||
|
||||
const MediaLibraryId _libraryId;
|
||||
const db::MediaLibraryId _libraryId;
|
||||
};
|
||||
|
||||
class LibraryRootPathValidator : public Wt::WValidator
|
||||
{
|
||||
public:
|
||||
LibraryRootPathValidator(MediaLibraryId libraryId)
|
||||
LibraryRootPathValidator(db::MediaLibraryId libraryId)
|
||||
: _libraryId{ libraryId } {}
|
||||
|
||||
private:
|
||||
@@ -115,7 +113,7 @@ namespace lms::ui
|
||||
return result;
|
||||
}
|
||||
|
||||
const MediaLibraryId _libraryId;
|
||||
const db::MediaLibraryId _libraryId;
|
||||
};
|
||||
|
||||
class MediaLibraryModel : public Wt::WFormModel
|
||||
@@ -124,7 +122,7 @@ namespace lms::ui
|
||||
static inline constexpr Field NameField{ "name" };
|
||||
static inline constexpr Field DirectoryField{ "directory" };
|
||||
|
||||
MediaLibraryModel(MediaLibraryId libraryId)
|
||||
MediaLibraryModel(db::MediaLibraryId libraryId)
|
||||
: _libraryId{ libraryId }
|
||||
{
|
||||
addField(NameField);
|
||||
@@ -146,7 +144,7 @@ namespace lms::ui
|
||||
loadData();
|
||||
}
|
||||
|
||||
MediaLibraryId saveData()
|
||||
db::MediaLibraryId saveData()
|
||||
{
|
||||
auto& session{ LmsApp->getDbSession() };
|
||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||
@@ -154,10 +152,10 @@ namespace lms::ui
|
||||
std::string name{ valueText(NameField).toUTF8() };
|
||||
std::string path{ valueText(DirectoryField).toUTF8() };
|
||||
|
||||
MediaLibrary::pointer library;
|
||||
db::MediaLibrary::pointer library;
|
||||
if (_libraryId.isValid())
|
||||
{
|
||||
library = MediaLibrary::find(session, _libraryId);
|
||||
library = db::MediaLibrary::find(session, _libraryId);
|
||||
if (library)
|
||||
{
|
||||
library.modify()->setName(name);
|
||||
@@ -166,7 +164,7 @@ namespace lms::ui
|
||||
}
|
||||
else
|
||||
{
|
||||
library = session.create<MediaLibrary>(name, path);
|
||||
library = session.create<db::MediaLibrary>(name, path);
|
||||
}
|
||||
|
||||
return library->getId();
|
||||
@@ -178,17 +176,17 @@ namespace lms::ui
|
||||
auto& session{ LmsApp->getDbSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const MediaLibrary::pointer library{ MediaLibrary::find(session, _libraryId) };
|
||||
const db::MediaLibrary::pointer library{ db::MediaLibrary::find(session, _libraryId) };
|
||||
|
||||
setValue(NameField, std::string{ library->getName() });
|
||||
setValue(DirectoryField, library->getPath().string());
|
||||
}
|
||||
|
||||
const MediaLibraryId _libraryId;
|
||||
const db::MediaLibraryId _libraryId;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
MediaLibraryModal::MediaLibraryModal(MediaLibraryId mediaLibraryId)
|
||||
MediaLibraryModal::MediaLibraryModal(db::MediaLibraryId mediaLibraryId)
|
||||
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.MediaLibrary.template") }
|
||||
{
|
||||
auto model{ std::make_shared<MediaLibraryModel>(mediaLibraryId) };
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace lms::ui
|
||||
class MediaLibraryModal : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
MediaLibraryModal(db::MediaLibraryId mediaLibaryId);
|
||||
MediaLibraryModal(db::MediaLibraryId mediaLibraryId);
|
||||
|
||||
Wt::Signal<db::MediaLibraryId>& saved() { return _saved; };
|
||||
Wt::Signal<>& cancelled() { return _cancelled; }
|
||||
|
||||
@@ -42,8 +42,6 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
namespace
|
||||
{
|
||||
class TagDelimitersValidator : public Wt::WValidator
|
||||
@@ -75,7 +73,7 @@ namespace lms::ui
|
||||
static inline constexpr Field ArtistImageFallbackToReleaseField{ "artist-image-fallback-to-release" };
|
||||
static inline constexpr Field ArtistsToNotSplitField{ "artists-to-not-split" };
|
||||
|
||||
using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
|
||||
using UpdatePeriodModel = ValueStringModel<db::ScanSettings::UpdatePeriod>;
|
||||
|
||||
DatabaseSettingsModel()
|
||||
{
|
||||
@@ -105,7 +103,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const ScanSettings::pointer scanSettings{ ScanSettings::find(LmsApp->getDbSession()) };
|
||||
const db::ScanSettings::pointer scanSettings{ db::ScanSettings::find(LmsApp->getDbSession()) };
|
||||
|
||||
auto periodRow{ _updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod()) };
|
||||
if (periodRow)
|
||||
@@ -115,8 +113,8 @@ namespace lms::ui
|
||||
if (startTimeRow)
|
||||
setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
|
||||
|
||||
if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
|
||||
|| scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
|
||||
if (scanSettings->getUpdatePeriod() == db::ScanSettings::UpdatePeriod::Hourly
|
||||
|| scanSettings->getUpdatePeriod() == db::ScanSettings::UpdatePeriod::Never)
|
||||
{
|
||||
setReadOnly(UpdateStartTimeField, true);
|
||||
}
|
||||
@@ -147,7 +145,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||
|
||||
ScanSettings::pointer scanSettings{ ScanSettings::find(LmsApp->getDbSession()) };
|
||||
db::ScanSettings::pointer scanSettings{ db::ScanSettings::find(LmsApp->getDbSession()) };
|
||||
|
||||
{
|
||||
const auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) };
|
||||
@@ -197,12 +195,12 @@ namespace lms::ui
|
||||
void
|
||||
initializeModels()
|
||||
{
|
||||
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly);
|
||||
_updatePeriodModel = std::make_shared<ValueStringModel<db::ScanSettings::UpdatePeriod>>();
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), db::ScanSettings::UpdatePeriod::Never);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), db::ScanSettings::UpdatePeriod::Hourly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), db::ScanSettings::UpdatePeriod::Daily);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), db::ScanSettings::UpdatePeriod::Weekly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), db::ScanSettings::UpdatePeriod::Monthly);
|
||||
|
||||
_updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>();
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
@@ -211,14 +209,14 @@ namespace lms::ui
|
||||
_updateStartTimeModel->add(time.toString(), time);
|
||||
}
|
||||
|
||||
_similarityEngineTypeModel = std::make_shared<ValueStringModel<ScanSettings::SimilarityEngineType>>();
|
||||
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters);
|
||||
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None);
|
||||
_similarityEngineTypeModel = std::make_shared<ValueStringModel<db::ScanSettings::SimilarityEngineType>>();
|
||||
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), db::ScanSettings::SimilarityEngineType::Clusters);
|
||||
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), db::ScanSettings::SimilarityEngineType::None);
|
||||
}
|
||||
|
||||
std::shared_ptr<UpdatePeriodModel> _updatePeriodModel;
|
||||
std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel;
|
||||
std::shared_ptr<ValueStringModel<ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel;
|
||||
std::shared_ptr<ValueStringModel<db::ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel;
|
||||
};
|
||||
|
||||
class LineEditEntryModel : public Wt::WFormModel
|
||||
@@ -362,8 +360,8 @@ namespace lms::ui
|
||||
auto updatePeriod{ std::make_unique<Wt::WComboBox>() };
|
||||
updatePeriod->setModel(model->updatePeriodModel());
|
||||
updatePeriod->activated().connect([=](int row) {
|
||||
const ScanSettings::UpdatePeriod period{ model->updatePeriodModel()->getValue(row) };
|
||||
model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never);
|
||||
const db::ScanSettings::UpdatePeriod period{ model->updatePeriodModel()->getValue(row) };
|
||||
model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == db::ScanSettings::UpdatePeriod::Hourly || period == db::ScanSettings::UpdatePeriod::Never);
|
||||
t->updateModel(model.get());
|
||||
t->updateView(model.get());
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace lms::ui
|
||||
}
|
||||
void visit(const scanner::EmbeddedImageScanError& error) override
|
||||
{
|
||||
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-embedded-image").arg(error.index).toUTF8() << '\n';
|
||||
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-embedded-image").arg(error.index).arg(Wt::WString::fromUTF8(error.errorMsg)).toUTF8() << '\n';
|
||||
}
|
||||
void visit(const scanner::NoAudioTrackFoundError& error) override
|
||||
{
|
||||
@@ -75,7 +75,7 @@ namespace lms::ui
|
||||
}
|
||||
void visit(const scanner::ImageFileScanError& error) override
|
||||
{
|
||||
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file").toUTF8() << '\n';
|
||||
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file").arg(Wt::WString::fromUTF8(error.errorMsg)).toUTF8() << '\n';
|
||||
}
|
||||
void visit(const scanner::LyricsFileScanError& error) override
|
||||
{
|
||||
|
||||
@@ -42,8 +42,6 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
class UserModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
@@ -51,7 +49,7 @@ namespace lms::ui
|
||||
static inline const Field PasswordField{ "password" };
|
||||
static inline const Field DemoField{ "demo" };
|
||||
|
||||
UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService)
|
||||
UserModel(std::optional<db::UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService)
|
||||
: _userId{ userId }
|
||||
, _authPasswordService{ authPasswordService }
|
||||
, _authTokenService{ authTokenService }
|
||||
@@ -81,7 +79,7 @@ namespace lms::ui
|
||||
if (_userId)
|
||||
{
|
||||
// Update user
|
||||
User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) };
|
||||
db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
|
||||
if (!user)
|
||||
throw UserNotFoundException{};
|
||||
|
||||
@@ -94,16 +92,16 @@ namespace lms::ui
|
||||
else
|
||||
{
|
||||
// Check races with other endpoints (subsonic API...)
|
||||
User::pointer user{ User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
|
||||
db::User::pointer user{ db::User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
|
||||
if (user)
|
||||
throw UserNotAllowedException{};
|
||||
|
||||
// Create user
|
||||
user = LmsApp->getDbSession().create<User>(valueText(LoginField).toUTF8());
|
||||
user = LmsApp->getDbSession().create<db::User>(valueText(LoginField).toUTF8());
|
||||
|
||||
if (Wt::asNumber(value(DemoField)))
|
||||
{
|
||||
user.modify()->setType(UserType::DEMO);
|
||||
user.modify()->setType(db::UserType::DEMO);
|
||||
|
||||
// For demo user, we create the subsonic API auth token now as we have no other mean to create it later
|
||||
core::Service<auth::IAuthTokenService>::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString());
|
||||
@@ -122,24 +120,24 @@ namespace lms::ui
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) };
|
||||
const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
|
||||
if (!user)
|
||||
throw UserNotFoundException{};
|
||||
if (user == LmsApp->getUser())
|
||||
throw UserNotAllowedException{};
|
||||
}
|
||||
|
||||
UserType getUserType() const
|
||||
db::UserType getUserType() const
|
||||
{
|
||||
if (_userId)
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) };
|
||||
const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
|
||||
return user->getType();
|
||||
}
|
||||
|
||||
return Wt::asNumber(value(DemoField)) ? UserType::DEMO : UserType::REGULAR;
|
||||
return Wt::asNumber(value(DemoField)) ? db::UserType::DEMO : db::UserType::REGULAR;
|
||||
}
|
||||
|
||||
std::string getLoginName() const
|
||||
@@ -148,7 +146,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) };
|
||||
const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
|
||||
return user->getLoginName();
|
||||
}
|
||||
|
||||
@@ -163,7 +161,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
|
||||
const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
|
||||
if (user)
|
||||
error = Wt::WString::tr("Lms.Admin.User.user-already-exists");
|
||||
}
|
||||
@@ -171,7 +169,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
if (Wt::asNumber(value(DemoField)) && User::findDemoUser(LmsApp->getDbSession()))
|
||||
if (Wt::asNumber(value(DemoField)) && db::User::findDemoUser(LmsApp->getDbSession()))
|
||||
error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists");
|
||||
}
|
||||
|
||||
@@ -183,7 +181,7 @@ namespace lms::ui
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<UserId> _userId;
|
||||
std::optional<db::UserId> _userId;
|
||||
auth::IPasswordService* _authPasswordService{};
|
||||
auth::IAuthTokenService& _authTokenService;
|
||||
};
|
||||
@@ -202,7 +200,7 @@ namespace lms::ui
|
||||
if (!wApp->internalPathMatches("/admin/user"))
|
||||
return;
|
||||
|
||||
const std::optional<UserId> userId{ core::stringUtils::readAs<UserId::ValueType>(wApp->internalPathNextPart("/admin/user/")) };
|
||||
const std::optional<db::UserId> userId{ core::stringUtils::readAs<db::UserId::ValueType>(wApp->internalPathNextPart("/admin/user/")) };
|
||||
|
||||
clear();
|
||||
|
||||
@@ -220,7 +218,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(LmsApp->getDbSession(), *userId) };
|
||||
const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *userId) };
|
||||
if (!user)
|
||||
throw UserNotFoundException{};
|
||||
|
||||
|
||||
@@ -23,19 +23,14 @@
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
UsersView::UsersView()
|
||||
: Wt::WTemplate{ Wt::WString::tr("Lms.Admin.Users.template") }
|
||||
{
|
||||
@@ -69,9 +64,9 @@ namespace lms::ui
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const UserId currentUserId{ LmsApp->getUserId() };
|
||||
User::find(LmsApp->getDbSession(), User::FindParameters{}, [&](const User::pointer& user) {
|
||||
const UserId userId{ user->getId() };
|
||||
const db::UserId currentUserId{ LmsApp->getUserId() };
|
||||
db::User::find(LmsApp->getDbSession(), db::User::FindParameters{}, [&](const db::User::pointer& user) {
|
||||
const db::UserId userId{ user->getId() };
|
||||
|
||||
Wt::WTemplate* entry{ _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry")) };
|
||||
|
||||
@@ -107,7 +102,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(LmsApp->getDbSession(), userId) };
|
||||
db::User::pointer user{ db::User::find(LmsApp->getDbSession(), userId) };
|
||||
if (user)
|
||||
user.remove();
|
||||
}
|
||||
|
||||
@@ -32,16 +32,14 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
RangeResults<ArtistId> ArtistCollector::get(std::optional<db::Range> requestedRange)
|
||||
db::RangeResults<db::ArtistId> ArtistCollector::get(std::optional<db::Range> requestedRange)
|
||||
{
|
||||
feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() };
|
||||
scrobbling::IScrobblingService& scrobblingService{ *core::Service<scrobbling::IScrobblingService>::get() };
|
||||
|
||||
const Range range{ getActualRange(requestedRange) };
|
||||
|
||||
RangeResults<ArtistId> artists;
|
||||
db::RangeResults<db::ArtistId> artists;
|
||||
|
||||
switch (getMode())
|
||||
{
|
||||
@@ -56,7 +54,7 @@ namespace lms::ui
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::StarredDateDesc);
|
||||
params.setSortMethod(db::ArtistSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
artists = feedbackService.findStarredArtists(params);
|
||||
break;
|
||||
@@ -90,48 +88,48 @@ namespace lms::ui
|
||||
|
||||
case Mode::RecentlyAdded:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
db::Artist::FindParameters params;
|
||||
params.setFilters(getDbFilters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::AddedDesc);
|
||||
params.setSortMethod(db::ArtistSortMethod::AddedDesc);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
artists = Artist::findIds(LmsApp->getDbSession(), params);
|
||||
artists = db::Artist::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Mode::RecentlyModified:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
db::Artist::FindParameters params;
|
||||
params.setFilters(getDbFilters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::LastWrittenDesc);
|
||||
params.setSortMethod(db::ArtistSortMethod::LastWrittenDesc);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
artists = Artist::findIds(LmsApp->getDbSession(), params);
|
||||
artists = db::Artist::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Mode::All:
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
db::Artist::FindParameters params;
|
||||
params.setFilters(getDbFilters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::SortName);
|
||||
params.setSortMethod(db::ArtistSortMethod::SortName);
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
artists = Artist::findIds(LmsApp->getDbSession(), params);
|
||||
artists = db::Artist::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -143,22 +141,22 @@ namespace lms::ui
|
||||
return artists;
|
||||
}
|
||||
|
||||
RangeResults<db::ArtistId> ArtistCollector::getRandomArtists(Range range)
|
||||
db::RangeResults<db::ArtistId> ArtistCollector::getRandomArtists(Range range)
|
||||
{
|
||||
assert(getMode() == Mode::Random);
|
||||
|
||||
if (!_randomArtists)
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
db::Artist::FindParameters params;
|
||||
params.setFilters(getDbFilters());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::Random);
|
||||
params.setRange(Range{ 0, getMaxCount() });
|
||||
params.setSortMethod(db::ArtistSortMethod::Random);
|
||||
params.setRange(db::Range{ 0, getMaxCount() });
|
||||
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
_randomArtists = Artist::findIds(LmsApp->getDbSession(), params);
|
||||
_randomArtists = db::Artist::findIds(LmsApp->getDbSession(), params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +47,9 @@
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
namespace
|
||||
{
|
||||
std::optional<ArtistId> extractArtistIdFromInternalPath()
|
||||
std::optional<db::ArtistId> extractArtistIdFromInternalPath()
|
||||
{
|
||||
if (wApp->internalPathMatches("/artist/mbid/"))
|
||||
{
|
||||
@@ -66,7 +64,7 @@ namespace lms::ui
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return core::stringUtils::readAs<ArtistId::ValueType>(wApp->internalPathNextPart("/artist/"));
|
||||
return core::stringUtils::readAs<db::ArtistId::ValueType>(wApp->internalPathNextPart("/artist/"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -109,7 +107,7 @@ namespace lms::ui
|
||||
if (!artistId)
|
||||
throw ArtistNotFoundException{};
|
||||
|
||||
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, 6) };
|
||||
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist, db::TrackArtistLinkType::ReleaseArtist }, 6) };
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
@@ -131,7 +129,7 @@ namespace lms::ui
|
||||
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
|
||||
|
||||
{
|
||||
auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results };
|
||||
auto clusterTypes{ db::ClusterType::findIds(LmsApp->getDbSession()).results };
|
||||
auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) };
|
||||
|
||||
for (const auto& clusters : clusterGroups)
|
||||
@@ -232,16 +230,16 @@ namespace lms::ui
|
||||
{
|
||||
_releaseContainers.clear();
|
||||
|
||||
Release::FindParameters params;
|
||||
db::Release::FindParameters params;
|
||||
params.setFilters(_filters.getDbFilters());
|
||||
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
|
||||
params.setArtist(_artistId, { db::TrackArtistLinkType::ReleaseArtist }, {});
|
||||
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
|
||||
|
||||
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
|
||||
const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) };
|
||||
if (!releases.results.empty())
|
||||
{
|
||||
// first pass: gather all ids and sort by release type
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
for (const db::ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) };
|
||||
|
||||
@@ -274,27 +272,27 @@ namespace lms::ui
|
||||
|
||||
void Artist::refreshAppearsOnReleases()
|
||||
{
|
||||
constexpr core::EnumSet<TrackArtistLinkType> types{
|
||||
TrackArtistLinkType::Artist,
|
||||
TrackArtistLinkType::Arranger,
|
||||
TrackArtistLinkType::Composer,
|
||||
TrackArtistLinkType::Conductor,
|
||||
TrackArtistLinkType::Lyricist,
|
||||
TrackArtistLinkType::Mixer,
|
||||
TrackArtistLinkType::Performer,
|
||||
TrackArtistLinkType::Producer,
|
||||
TrackArtistLinkType::Remixer,
|
||||
TrackArtistLinkType::Writer,
|
||||
constexpr core::EnumSet<db::TrackArtistLinkType> types{
|
||||
db::TrackArtistLinkType::Artist,
|
||||
db::TrackArtistLinkType::Arranger,
|
||||
db::TrackArtistLinkType::Composer,
|
||||
db::TrackArtistLinkType::Conductor,
|
||||
db::TrackArtistLinkType::Lyricist,
|
||||
db::TrackArtistLinkType::Mixer,
|
||||
db::TrackArtistLinkType::Performer,
|
||||
db::TrackArtistLinkType::Producer,
|
||||
db::TrackArtistLinkType::Remixer,
|
||||
db::TrackArtistLinkType::Writer,
|
||||
};
|
||||
|
||||
_appearsOnReleaseContainer = {};
|
||||
|
||||
Release::FindParameters params;
|
||||
db::Release::FindParameters params;
|
||||
params.setFilters(_filters.getDbFilters());
|
||||
params.setArtist(_artistId, types, { TrackArtistLinkType::ReleaseArtist });
|
||||
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
|
||||
params.setArtist(_artistId, types, { db::TrackArtistLinkType::ReleaseArtist });
|
||||
params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
|
||||
|
||||
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
|
||||
const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) };
|
||||
if (!releases.results.empty())
|
||||
{
|
||||
Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) };
|
||||
@@ -323,7 +321,7 @@ namespace lms::ui
|
||||
setCondition("if-has-non-release-tracks", added);
|
||||
}
|
||||
|
||||
void Artist::refreshSimilarArtists(const std::vector<ArtistId>& similarArtistsId)
|
||||
void Artist::refreshSimilarArtists(const std::vector<db::ArtistId>& similarArtistsId)
|
||||
{
|
||||
if (similarArtistsId.empty())
|
||||
return;
|
||||
@@ -331,7 +329,7 @@ namespace lms::ui
|
||||
setCondition("if-has-similar-artists", true);
|
||||
Wt::WContainerWidget* similarArtistsContainer{ bindNew<Wt::WContainerWidget>("similar-artists") };
|
||||
|
||||
for (const ArtistId artistId : similarArtistsId)
|
||||
for (const db::ArtistId artistId : similarArtistsId)
|
||||
{
|
||||
const db::Artist::pointer similarArtist{ db::Artist::find(LmsApp->getDbSession(), artistId) };
|
||||
if (!similarArtist)
|
||||
@@ -371,19 +369,19 @@ namespace lms::ui
|
||||
{
|
||||
bool areTracksAdded{};
|
||||
|
||||
const Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
|
||||
const db::Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
|
||||
|
||||
Track::FindParameters params;
|
||||
db::Track::FindParameters params;
|
||||
params.setFilters(_filters.getDbFilters());
|
||||
params.setArtist(_artistId);
|
||||
params.setRange(range);
|
||||
params.setSortMethod(TrackSortMethod::Name);
|
||||
params.setSortMethod(db::TrackSortMethod::Name);
|
||||
params.setNonRelease(true);
|
||||
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::find(LmsApp->getDbSession(), params) };
|
||||
for (const Track::pointer& track : tracks.results)
|
||||
const auto tracks{ db::Track::find(LmsApp->getDbSession(), params) };
|
||||
for (const db::Track::pointer& track : tracks.results)
|
||||
{
|
||||
// TODO handle this with range
|
||||
if (_trackContainer->getCount() == _tracksMaxCount)
|
||||
|
||||
@@ -61,14 +61,13 @@ namespace lms::ui
|
||||
|
||||
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<TrackId> res;
|
||||
std::vector<db::TrackId> res;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
for (const ReleaseId releaseId : releasesId)
|
||||
for (const db::ReleaseId releaseId : releasesId)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
@@ -88,39 +87,27 @@ namespace lms::ui
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<db::TrackId> getDiscTracks(db::Session& session, const std::vector<PlayQueueController::Disc>& discs, const Filters& filters, std::size_t maxTrackCount)
|
||||
std::vector<db::TrackId> getMediumTracks(db::Session& session, db::MediumId medium, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
|
||||
std::vector<TrackId> res;
|
||||
std::vector<db::TrackId> res;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
for (const PlayQueueController::Disc& disc : discs)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
params.setRelease(disc.releaseId);
|
||||
params.setSortMethod(db::TrackSortMethod::Release);
|
||||
params.setDiscNumber(disc.discNumber);
|
||||
params.setMedium(medium);
|
||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
||||
params.setFilters(filters.getDbFilters());
|
||||
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
|
||||
params.setRange(db::Range{ .offset = 0, .size = maxTrackCount });
|
||||
|
||||
const auto tracks{ db::Track::findIds(session, params) };
|
||||
|
||||
res.reserve(res.size() + tracks.results.size());
|
||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||
|
||||
if (res.size() == maxTrackCount)
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
return tracks.results;
|
||||
}
|
||||
|
||||
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
|
||||
{
|
||||
using namespace db;
|
||||
assert(maxTrackCount);
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -129,7 +116,7 @@ namespace lms::ui
|
||||
params.setTrackList(trackListId);
|
||||
params.setFilters(filters.getDbFilters());
|
||||
params.setRange(db::Range{ 0, maxTrackCount });
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
params.setSortMethod(db::TrackSortMethod::TrackList);
|
||||
|
||||
return db::Track::findIds(session, params).results;
|
||||
}
|
||||
@@ -179,9 +166,9 @@ namespace lms::ui
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
void PlayQueueController::processCommand(Command command, const std::vector<Disc>& discs)
|
||||
void PlayQueueController::processCommand(Command command, db::MediumId medium)
|
||||
{
|
||||
const std::vector<db::TrackId> tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters, _maxTrackCountToEnqueue) };
|
||||
const std::vector<db::TrackId> tracks{ getMediumTracks(LmsApp->getDbSession(), medium, _filters, _maxTrackCountToEnqueue) };
|
||||
processCommand(command, tracks);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user