Merge branch 'develop' for release v3.62.0
This commit is contained in:
+5
-1
@@ -12,5 +12,9 @@ CheckOptions:
|
|||||||
- key: cppcoreguidelines-avoid-do-while.IgnoreMacros
|
- key: cppcoreguidelines-avoid-do-while.IgnoreMacros
|
||||||
value: '1'
|
value: '1'
|
||||||
- key: performance-unnecessary-value-param.AllowedTypes
|
- key: performance-unnecessary-value-param.AllowedTypes
|
||||||
value: "shared_ptr"
|
value: "shared_ptr;ObjectPtr;.*::pointer"
|
||||||
|
- key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted
|
||||||
|
value: '1'
|
||||||
|
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
|
||||||
|
value: '1'
|
||||||
...
|
...
|
||||||
|
|||||||
+2
-1
@@ -3,4 +3,5 @@
|
|||||||
CMakeCache.txt
|
CMakeCache.txt
|
||||||
CMakeFiles/
|
CMakeFiles/
|
||||||
build/
|
build/
|
||||||
.vscode/
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -95,9 +95,10 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio 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</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</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Cannot parse lyrics file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Cannot parse lyrics file</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read 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>
|
<message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message>
|
||||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message>
|
<message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} errors:</message>
|
<message id="Lms.Admin.ScannerController.errors-header">{1} errors:</message>
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associating playlist tracks: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release images: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release images: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
||||||
@@ -267,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Delete playlist?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Delete playlist?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">My Playlists</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Shared playlists</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||||
|
|||||||
@@ -95,9 +95,10 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</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</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</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-lyrics-file">Impossible d'analyser le fichier de paroles</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</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>
|
<message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message>
|
||||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message>
|
<message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} erreurs :</message>
|
<message id="Lms.Admin.ScannerController.errors-header">{1} erreurs :</message>
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
||||||
@@ -267,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Supprimer la liste de lecture ?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Supprimer la liste de lecture ?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Mes listes</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Listes partagées</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||||
|
|||||||
@@ -95,9 +95,10 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</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</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</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossibile analizzare il file dei testi</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossibile analizzare il file dei testi</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</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>
|
<message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message>
|
||||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message>
|
<message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} errori:</message>
|
<message id="Lms.Admin.ScannerController.errors-header">{1} errori:</message>
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associando brani della playlist: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
||||||
@@ -267,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Eliminare la playlist?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Eliminare la playlist?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Le mie liste</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Liste condivise</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
|
||||||
|
|||||||
@@ -96,9 +96,10 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</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</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</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-lyrics-file">Nie można przetworzyć pliku z tekstem</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku</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>
|
<message id="Lms.Admin.ScannerController.compact">Sprasuj bazę danych. <strong>Uwaga:</strong> może to trochę zająć, a cała aplikacja będzie w tym czasie zablokowana!</message>
|
||||||
<message id="Lms.Admin.ScannerController.duplicates-header">
|
<message id="Lms.Admin.ScannerController.duplicates-header">
|
||||||
<plural case="0">{1} zduplikowany plik:</plural>
|
<plural case="0">{1} zduplikowany plik:</plural>
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Kojarzenie utworów z playlisty: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
||||||
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
||||||
@@ -293,6 +295,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Usunąć listę odtwarzania?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Usunąć listę odtwarzania?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Moje listy</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Udostępnione listy</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Szukaj...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Szukaj...</message>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message>
|
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message>
|
<message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message>
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
||||||
|
|
||||||
|
|
||||||
@@ -268,6 +270,8 @@
|
|||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">搜索...</message>
|
<message id="Lms.Explore.Search.search-placeholder">搜索...</message>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
<li>${play-shuffled class="dropdown-item"}</li>
|
<li>${play-shuffled class="dropdown-item"}</li>
|
||||||
<li>${play-last class="dropdown-item"}</li>
|
<li>${play-last class="dropdown-item"}</li>
|
||||||
<li>${download class="dropdown-item"}</li>
|
<li>${download class="dropdown-item"}</li>
|
||||||
|
${<if-has-delete>}
|
||||||
<li>${delete class="dropdown-item"}</li>
|
<li>${delete class="dropdown-item"}</li>
|
||||||
|
${</if-has-delete>}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-2
@@ -3,12 +3,28 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.Explore.TrackLists.template">
|
<message id="Lms.Explore.TrackLists.template">
|
||||||
<div class="d-flex flex-row-reverse">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
${sort-mode}
|
${tracklist-type class="me-1"}
|
||||||
|
<div class="d-flex">
|
||||||
|
${search class="form-control form-control-sm me-1" type="search"}
|
||||||
|
${sort-mode}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${tracklists}
|
${tracklists}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.TrackLists.template.type-selector">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
${selected-item}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>${owned class="dropdown-item"}</li>
|
||||||
|
<li>${shared class="dropdown-item"}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.TrackLists.template.sort-mode">
|
<message id="Lms.Explore.TrackLists.template.sort-mode">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
|||||||
@@ -79,9 +79,6 @@ api-subsonic-support-user-password-auth = true;
|
|||||||
# Main usage is to make auto detections for the 'p' (password) parameter work
|
# Main usage is to make auto detections for the 'p' (password) parameter work
|
||||||
api-subsonic-old-server-protocol-clients = ("DSub");
|
api-subsonic-old-server-protocol-clients = ("DSub");
|
||||||
|
|
||||||
# List of clients for whom a default cover is served (as they do not have their own)
|
|
||||||
api-subsonic-default-cover-clients = ("DSub", "substreamer");
|
|
||||||
|
|
||||||
# List of clients for whom open subsonic extensions and extra fields are disabled
|
# List of clients for whom open subsonic extensions and extra fields are disabled
|
||||||
api-open-subsonic-disabled-clients = ("DSub");
|
api-open-subsonic-disabled-clients = ("DSub");
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ extern "C"
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
|
#include "av/Types.hpp"
|
||||||
|
|
||||||
namespace lms::av
|
namespace lms::av
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
@@ -43,9 +45,9 @@ namespace lms::av
|
|||||||
std::array<char, 128> buf = { 0 };
|
std::array<char, 128> buf = { 0 };
|
||||||
|
|
||||||
if (::av_strerror(error, buf.data(), buf.size()) == 0)
|
if (::av_strerror(error, buf.data(), buf.size()) == 0)
|
||||||
return &buf[0];
|
return buf.data();
|
||||||
else
|
|
||||||
return "Unknown error";
|
return "Unknown error";
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioFileException : public Exception
|
class AudioFileException : public Exception
|
||||||
@@ -158,7 +160,7 @@ namespace lms::av
|
|||||||
{
|
{
|
||||||
ContainerInfo info;
|
ContainerInfo info;
|
||||||
info.bitrate = _context->bit_rate;
|
info.bitrate = _context->bit_rate;
|
||||||
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1000 };
|
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
|
||||||
info.name = _context->iformat->name;
|
info.name = _context->iformat->name;
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@@ -237,14 +239,13 @@ namespace lms::av
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
|
||||||
{
|
{
|
||||||
static const std::unordered_map<int, std::string> codecMimeMap{
|
static const std::unordered_map<int, std::string> codecMimeMap{
|
||||||
{ AV_CODEC_ID_BMP, "image/x-bmp" },
|
{ AV_CODEC_ID_BMP, "image/bmp" },
|
||||||
{ AV_CODEC_ID_GIF, "image/gif" },
|
{ AV_CODEC_ID_GIF, "image/gif" },
|
||||||
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
|
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
|
||||||
{ AV_CODEC_ID_PNG, "image/png" },
|
{ AV_CODEC_ID_PNG, "image/png" },
|
||||||
{ AV_CODEC_ID_PNG, "image/x-png" },
|
|
||||||
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,6 +263,9 @@ namespace lms::av
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MetadataMap metadata;
|
||||||
|
getMetaDataFromDictionnary(avstream->metadata, metadata);
|
||||||
|
|
||||||
Picture picture;
|
Picture picture;
|
||||||
|
|
||||||
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
||||||
@@ -277,10 +281,8 @@ namespace lms::av
|
|||||||
|
|
||||||
const AVPacket& pkt{ avstream->attached_pic };
|
const AVPacket& pkt{ avstream->attached_pic };
|
||||||
|
|
||||||
picture.data = reinterpret_cast<const std::byte*>(pkt.data);
|
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
|
||||||
picture.dataSize = pkt.size;
|
func(picture, metadata);
|
||||||
|
|
||||||
func(picture);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ struct AVFormatContext;
|
|||||||
|
|
||||||
namespace lms::av
|
namespace lms::av
|
||||||
{
|
{
|
||||||
|
|
||||||
class AudioFile final : public IAudioFile
|
class AudioFile final : public IAudioFile
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioFile(const std::filesystem::path& p);
|
AudioFile(const std::filesystem::path& p);
|
||||||
~AudioFile();
|
~AudioFile() override;
|
||||||
|
AudioFile(const AudioFile&) = delete;
|
||||||
|
AudioFile& operator=(const AudioFile&) = delete;
|
||||||
|
|
||||||
const std::filesystem::path& getPath() const override;
|
const std::filesystem::path& getPath() const override;
|
||||||
ContainerInfo getContainerInfo() const override;
|
ContainerInfo getContainerInfo() const override;
|
||||||
@@ -39,16 +40,12 @@ namespace lms::av
|
|||||||
std::optional<StreamInfo> getBestStreamInfo() const override;
|
std::optional<StreamInfo> getBestStreamInfo() const override;
|
||||||
std::optional<std::size_t> getBestStreamIndex() const override;
|
std::optional<std::size_t> getBestStreamIndex() const override;
|
||||||
bool hasAttachedPictures() const override;
|
bool hasAttachedPictures() const override;
|
||||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AudioFile(const AudioFile&) = delete;
|
|
||||||
AudioFile& operator=(const AudioFile&) = delete;
|
|
||||||
|
|
||||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||||
|
|
||||||
const std::filesystem::path _p;
|
const std::filesystem::path _p;
|
||||||
AVFormatContext* _context{};
|
AVFormatContext* _context{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lms::av
|
} // namespace lms::av
|
||||||
|
|||||||
@@ -25,9 +25,10 @@
|
|||||||
#include "core/IChildProcessManager.hpp"
|
#include "core/IChildProcessManager.hpp"
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Path.hpp"
|
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
|
|
||||||
|
#include "av/Types.hpp"
|
||||||
|
|
||||||
namespace lms::av::transcoding
|
namespace lms::av::transcoding
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ namespace lms::av::transcoding
|
|||||||
{
|
{
|
||||||
if (!std::filesystem::exists(_inputParameters.trackPath))
|
if (!std::filesystem::exists(_inputParameters.trackPath))
|
||||||
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
|
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
|
||||||
else if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
|
if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
|
||||||
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
|
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
|
||||||
}
|
}
|
||||||
catch (const std::filesystem::filesystem_error& e)
|
catch (const std::filesystem::filesystem_error& e)
|
||||||
@@ -108,7 +109,7 @@ namespace lms::av::transcoding
|
|||||||
args.emplace_back("-ss");
|
args.emplace_back("-ss");
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 });
|
oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1'000 });
|
||||||
args.emplace_back(oss.str());
|
args.emplace_back(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "av/TranscodingParameters.hpp"
|
#include "av/TranscodingParameters.hpp"
|
||||||
#include "av/Types.hpp"
|
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
@@ -37,7 +35,6 @@ namespace lms::av::transcoding
|
|||||||
public:
|
public:
|
||||||
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
||||||
~Transcoder();
|
~Transcoder();
|
||||||
|
|
||||||
Transcoder(const Transcoder&) = delete;
|
Transcoder(const Transcoder&) = delete;
|
||||||
Transcoder& operator=(const Transcoder&) = delete;
|
Transcoder& operator=(const Transcoder&) = delete;
|
||||||
Transcoder(Transcoder&&) = delete;
|
Transcoder(Transcoder&&) = delete;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace lms::av::transcoding
|
|||||||
{
|
{
|
||||||
LMS_LOG(TRANSCODING, DEBUG, "Writing " << _bytesReadyCount << " bytes back to client");
|
LMS_LOG(TRANSCODING, DEBUG, "Writing " << _bytesReadyCount << " bytes back to client");
|
||||||
|
|
||||||
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
|
response.out().write(reinterpret_cast<const char*>(_buffer.data()), _bytesReadyCount);
|
||||||
_totalServedByteCount += _bytesReadyCount;
|
_totalServedByteCount += _bytesReadyCount;
|
||||||
_bytesReadyCount = 0;
|
_bytesReadyCount = 0;
|
||||||
}
|
}
|
||||||
@@ -78,24 +78,22 @@ namespace lms::av::transcoding
|
|||||||
|
|
||||||
return continuation;
|
return continuation;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// pad with 0 if necessary as duration may not be accurate
|
||||||
|
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
|
||||||
{
|
{
|
||||||
// pad with 0 if necessary as duration may not be accurate
|
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
|
||||||
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
|
|
||||||
{
|
|
||||||
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
|
|
||||||
|
|
||||||
LMS_LOG(TRANSCODING, DEBUG, "Adding " << padSize << " padding bytes");
|
LMS_LOG(TRANSCODING, DEBUG, "Adding " << padSize << " padding bytes");
|
||||||
|
|
||||||
for (std::size_t i{}; i < padSize; ++i)
|
for (std::size_t i{}; i < padSize; ++i)
|
||||||
response.out().put(0);
|
response.out().put(0);
|
||||||
|
|
||||||
_totalServedByteCount += padSize;
|
_totalServedByteCount += padSize;
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(TRANSCODING, DEBUG, "Transcoding finished. Total served byte count = " << _totalServedByteCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(TRANSCODING, DEBUG, "Transcoding finished. Total served byte count = " << _totalServedByteCount);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
} // namespace lms::av::transcoding
|
} // namespace lms::av::transcoding
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "av/TranscodingParameters.hpp"
|
#include "av/TranscodingParameters.hpp"
|
||||||
@@ -34,9 +33,13 @@ namespace lms::av::transcoding
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
|
TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
|
||||||
|
~TranscodingResourceHandler() override = default;
|
||||||
|
|
||||||
|
TranscodingResourceHandler(const TranscodingResourceHandler&) = delete;
|
||||||
|
TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
void abort() override{};
|
void abort() override{};
|
||||||
|
|
||||||
static constexpr std::size_t _chunkSize{ 262'144 };
|
static constexpr std::size_t _chunkSize{ 262'144 };
|
||||||
|
|||||||
@@ -23,13 +23,12 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Types.hpp"
|
|
||||||
|
|
||||||
namespace lms::av
|
namespace lms::av
|
||||||
{
|
{
|
||||||
// List should be sync with the codecs shipped in the lms's docker version
|
// List should be sync with the codecs shipped in the lms's docker version
|
||||||
@@ -62,14 +61,13 @@ namespace lms::av
|
|||||||
struct Picture
|
struct Picture
|
||||||
{
|
{
|
||||||
std::string mimeType;
|
std::string mimeType;
|
||||||
const std::byte* data{};
|
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||||
std::size_t dataSize{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ContainerInfo
|
struct ContainerInfo
|
||||||
{
|
{
|
||||||
std::size_t bitrate{};
|
std::size_t bitrate{};
|
||||||
std::string name{};
|
std::string name;
|
||||||
std::chrono::milliseconds duration{};
|
std::chrono::milliseconds duration{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ namespace lms::av
|
|||||||
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
||||||
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
|
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
|
||||||
virtual bool hasAttachedPictures() const = 0;
|
virtual bool hasAttachedPictures() const = 0;
|
||||||
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
|
virtual void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap& metadata)> func) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
|
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "Types.hpp"
|
|
||||||
|
|
||||||
namespace lms::av::transcoding
|
namespace lms::av::transcoding
|
||||||
{
|
{
|
||||||
struct InputParameters
|
struct InputParameters
|
||||||
@@ -47,7 +45,7 @@ namespace lms::av::transcoding
|
|||||||
struct OutputParameters
|
struct OutputParameters
|
||||||
{
|
{
|
||||||
OutputFormat format;
|
OutputFormat format;
|
||||||
std::size_t bitrate{ 128000 };
|
std::size_t bitrate{ 128'000 };
|
||||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||||
std::chrono::milliseconds offset{ 0 };
|
std::chrono::milliseconds offset{ 0 };
|
||||||
bool stripMetadata{ true };
|
bool stripMetadata{ true };
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
// The trace logger is meant to built/destroyed once
|
// The trace logger is meant to built/destroyed once
|
||||||
Service<logging::ILogger> logger{ std::make_unique<logging::StreamLogger>(std::cout, logging::StreamLogger::allSeverities) };
|
const Service<logging::ILogger> logger{ std::make_unique<logging::StreamLogger>(std::cout, logging::StreamLogger::allSeverities) };
|
||||||
Service<tracing::ITraceLogger> traceLogger{ tracing::createTraceLogger(tracing::Level::Overview) };
|
const Service<tracing::ITraceLogger> traceLogger{ tracing::createTraceLogger(tracing::Level::Overview) };
|
||||||
|
|
||||||
static void BM_TraceLogger_Overview(benchmark::State& state)
|
static void BM_TraceLogger_Overview(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -217,14 +217,14 @@ namespace lms::zip
|
|||||||
if (!std::filesystem::is_regular_file(entry.filePath))
|
if (!std::filesystem::is_regular_file(entry.filePath))
|
||||||
throw FileException{ entry.filePath, "not a regular file" };
|
throw FileException{ entry.filePath, "not a regular file" };
|
||||||
|
|
||||||
ArchiveEntryPtr archiveEntry{ archive_entry_new() };
|
ArchiveEntryPtr archiveEntry{ ::archive_entry_new() };
|
||||||
if (!archiveEntry)
|
if (!archiveEntry)
|
||||||
throw Exception{ "Cannot create archive entry control struct" };
|
throw Exception{ "Cannot create archive entry control struct" };
|
||||||
|
|
||||||
archive_entry_set_pathname(archiveEntry.get(), entry.fileName.c_str());
|
::archive_entry_set_pathname(archiveEntry.get(), entry.fileName.c_str());
|
||||||
archive_entry_set_size(archiveEntry.get(), std::filesystem::file_size(entry.filePath));
|
::archive_entry_set_size(archiveEntry.get(), std::filesystem::file_size(entry.filePath));
|
||||||
archive_entry_set_mode(archiveEntry.get(), permsToMode(std::filesystem::status(entry.filePath).permissions()));
|
::archive_entry_set_mode(archiveEntry.get(), permsToMode(std::filesystem::status(entry.filePath).permissions()));
|
||||||
archive_entry_set_filetype(archiveEntry.get(), AE_IFREG);
|
::archive_entry_set_filetype(archiveEntry.get(), AE_IFREG);
|
||||||
|
|
||||||
return archiveEntry;
|
return archiveEntry;
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@ namespace lms::zip
|
|||||||
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
||||||
throw FileException{ _currentEntry->filePath, "seek failed", errno };
|
throw FileException{ _currentEntry->filePath, "seek failed", errno };
|
||||||
|
|
||||||
if (!ifs.read(reinterpret_cast<char*>(&_readBuffer[0]), bytesToRead))
|
if (!ifs.read(reinterpret_cast<char*>(_readBuffer.data()), bytesToRead))
|
||||||
throw FileException{ _currentEntry->filePath, "read failed", errno };
|
throw FileException{ _currentEntry->filePath, "read failed", errno };
|
||||||
|
|
||||||
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
||||||
@@ -266,7 +266,7 @@ namespace lms::zip
|
|||||||
std::uint64_t remainingBytesToWrite{ actualBytesRead };
|
std::uint64_t remainingBytesToWrite{ actualBytesRead };
|
||||||
while (remainingBytesToWrite > 0)
|
while (remainingBytesToWrite > 0)
|
||||||
{
|
{
|
||||||
const auto writtenBytes{ archive_write_data(_archive.get(), &_readBuffer[actualBytesRead - remainingBytesToWrite], remainingBytesToWrite) };
|
const auto writtenBytes{ ::archive_write_data(_archive.get(), &_readBuffer[actualBytesRead - remainingBytesToWrite], remainingBytesToWrite) };
|
||||||
if (writtenBytes < 0)
|
if (writtenBytes < 0)
|
||||||
throw ArchiveException{ _archive.get() };
|
throw ArchiveException{ _archive.get() };
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ namespace lms::zip
|
|||||||
{
|
{
|
||||||
if (!_currentOutputStream)
|
if (!_currentOutputStream)
|
||||||
{
|
{
|
||||||
archive_set_error(_archive.get(), EIO, "IO error: operation cancelled");
|
::archive_set_error(_archive.get(), EIO, "IO error: operation cancelled");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ namespace lms::zip
|
|||||||
class ArchiveZipper : public IZipper
|
class ArchiveZipper : public IZipper
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArchiveZipper(const EntryContainer& files);
|
ArchiveZipper(const EntryContainer& entries);
|
||||||
|
~ArchiveZipper() = default;
|
||||||
ArchiveZipper(const ArchiveZipper&) = delete;
|
ArchiveZipper(const ArchiveZipper&) = delete;
|
||||||
ArchiveZipper& operator=(const ArchiveZipper&) = delete;
|
ArchiveZipper& operator=(const ArchiveZipper&) = delete;
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ namespace lms::core
|
|||||||
class ChildProcess : public IChildProcess
|
class ChildProcess : public IChildProcess
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
~ChildProcess();
|
|
||||||
ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args);
|
ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args);
|
||||||
|
~ChildProcess() override;
|
||||||
|
|
||||||
|
ChildProcess(const ChildProcess&) = delete;
|
||||||
|
ChildProcess& operator=(const ChildProcess&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override;
|
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ namespace lms::core
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ChildProcessManager(boost::asio::io_context& ioContext);
|
ChildProcessManager(boost::asio::io_context& ioContext);
|
||||||
~ChildProcessManager() = default;
|
~ChildProcessManager() override = default;
|
||||||
|
|
||||||
ChildProcessManager(const ChildProcessManager&) = delete;
|
ChildProcessManager(const ChildProcessManager&) = delete;
|
||||||
ChildProcessManager(ChildProcessManager&&) = delete;
|
ChildProcessManager(ChildProcessManager&&) = delete;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include "Config.hpp"
|
#include "Config.hpp"
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
#include "core/Exception.hpp"
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ namespace lms::core
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Config(const std::filesystem::path& p);
|
Config(const std::filesystem::path& p);
|
||||||
~Config() = default;
|
~Config() override = default;
|
||||||
|
|
||||||
Config(const Config&) = delete;
|
Config(const Config&) = delete;
|
||||||
Config& operator=(const Config&) = delete;
|
Config& operator=(const Config&) = delete;
|
||||||
Config(Config&&) = delete;
|
Config(Config&&) = delete;
|
||||||
Config& operator=(Config&&) = delete;
|
Config& operator=(Config&&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
// Default values are returned in case of setting not found
|
// Default values are returned in case of setting not found
|
||||||
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
||||||
void visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs) override;
|
void visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs) override;
|
||||||
@@ -44,7 +45,6 @@ namespace lms::core
|
|||||||
long getLong(std::string_view setting, long def = 0) override;
|
long getLong(std::string_view setting, long def = 0) override;
|
||||||
bool getBool(std::string_view setting, bool def = false) override;
|
bool getBool(std::string_view setting, bool def = false) override;
|
||||||
|
|
||||||
private:
|
|
||||||
libconfig::Config _config;
|
libconfig::Config _config;
|
||||||
};
|
};
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount, std::string_view name)
|
IOContextRunner::IOContextRunner(boost::asio::io_context& ioContext, std::size_t threadCount, std::string_view name)
|
||||||
: _ioService{ ioService }
|
: _ioContext{ ioContext }
|
||||||
, _work{ ioService }
|
, _work{ ioContext }
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ namespace lms::core
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ioService.run();
|
_ioContext.run();
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
@@ -65,7 +65,7 @@ namespace lms::core
|
|||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
||||||
_work.reset();
|
_work.reset();
|
||||||
_ioService.stop();
|
_ioContext.stop();
|
||||||
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -321,6 +321,18 @@ namespace lms::core::stringUtils
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string_view::size_type stringCaseInsensitiveContains(std::string_view str, std::string_view strtoFind)
|
||||||
|
{
|
||||||
|
if (str.empty() && strtoFind.empty())
|
||||||
|
return true; // same as std
|
||||||
|
|
||||||
|
const auto it{ std::search(
|
||||||
|
std::cbegin(str), std::cend(str),
|
||||||
|
std::cbegin(strtoFind), std::cend(strtoFind),
|
||||||
|
[](char chA, char chB) { return std::tolower(chA) == std::tolower(chB); }) };
|
||||||
|
return (it != std::cend(str));
|
||||||
|
}
|
||||||
|
|
||||||
void capitalize(std::string& str)
|
void capitalize(std::string& str)
|
||||||
{
|
{
|
||||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ namespace lms::core::tracing
|
|||||||
private:
|
private:
|
||||||
CurrentThreadUnregisterer(const CurrentThreadUnregisterer&) = delete;
|
CurrentThreadUnregisterer(const CurrentThreadUnregisterer&) = delete;
|
||||||
CurrentThreadUnregisterer& operator=(const CurrentThreadUnregisterer&) = delete;
|
CurrentThreadUnregisterer& operator=(const CurrentThreadUnregisterer&) = delete;
|
||||||
|
CurrentThreadUnregisterer(CurrentThreadUnregisterer&&) = delete;
|
||||||
|
CurrentThreadUnregisterer& operator=(CurrentThreadUnregisterer&&) = delete;
|
||||||
|
|
||||||
TraceLogger* _logger;
|
TraceLogger* _logger;
|
||||||
};
|
};
|
||||||
@@ -295,7 +297,7 @@ namespace lms::core::tracing
|
|||||||
oss << threadId;
|
oss << threadId;
|
||||||
|
|
||||||
std::istringstream iss{ oss.str() };
|
std::istringstream iss{ oss.str() };
|
||||||
std::uint64_t id;
|
std::uint64_t id{};
|
||||||
iss >> id;
|
iss >> id;
|
||||||
|
|
||||||
return static_cast<std::uint32_t>(id);
|
return static_cast<std::uint32_t>(id);
|
||||||
|
|||||||
@@ -22,25 +22,24 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include <boost/asio/io_service.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
class IOContextRunner
|
class IOContextRunner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount, std::string_view name);
|
IOContextRunner(boost::asio::io_context& ioContext, std::size_t threadCount, std::string_view name);
|
||||||
~IOContextRunner();
|
~IOContextRunner();
|
||||||
|
IOContextRunner(const IOContextRunner&) = delete;
|
||||||
|
IOContextRunner& operator=(const IOContextRunner&) = delete;
|
||||||
|
|
||||||
void stop();
|
void stop();
|
||||||
std::size_t getThreadCount() const;
|
std::size_t getThreadCount() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
IOContextRunner(const IOContextRunner&) = delete;
|
boost::asio::io_context& _ioContext;
|
||||||
IOContextRunner& operator=(const IOContextRunner&) = delete;
|
std::optional<boost::asio::io_context::work> _work;
|
||||||
|
|
||||||
boost::asio::io_service& _ioService;
|
|
||||||
std::optional<boost::asio::io_service::work> _work;
|
|
||||||
std::vector<std::thread> _threads;
|
std::vector<std::thread> _threads;
|
||||||
};
|
};
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -62,6 +62,7 @@ namespace lms::core::stringUtils
|
|||||||
[[nodiscard]] std::string bufferToString(std::span<const unsigned char> data);
|
[[nodiscard]] std::string bufferToString(std::span<const unsigned char> data);
|
||||||
|
|
||||||
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB);
|
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB);
|
||||||
|
[[nodiscard]] std::string_view::size_type stringCaseInsensitiveContains(std::string_view str, std::string_view strtoFind);
|
||||||
|
|
||||||
void capitalize(std::string& str);
|
void capitalize(std::string& str);
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ namespace lms::core::stringUtils::tests
|
|||||||
std::string expectedOutput;
|
std::string expectedOutput;
|
||||||
};
|
};
|
||||||
|
|
||||||
TestCase tests[]{
|
const TestCase tests[]{
|
||||||
{ { "" }, ';', '\\', "" },
|
{ { "" }, ';', '\\', "" },
|
||||||
{ { ";" }, ';', '\\', "\\;" },
|
{ { ";" }, ';', '\\', "\\;" },
|
||||||
{ { ";;" }, ';', '\\', "\\;\\;" },
|
{ { ";;" }, ';', '\\', "\\;\\;" },
|
||||||
@@ -322,4 +322,16 @@ namespace lms::core::stringUtils::tests
|
|||||||
EXPECT_FALSE(stringEndsWith("FooBar", "1FooBar"));
|
EXPECT_FALSE(stringEndsWith("FooBar", "1FooBar"));
|
||||||
EXPECT_FALSE(stringEndsWith("FooBar", "R"));
|
EXPECT_FALSE(stringEndsWith("FooBar", "R"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(StringUtils, stringCaseInsensitiveContains)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "Bar"));
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "bar"));
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "Foo"));
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "foo"));
|
||||||
|
EXPECT_FALSE(stringCaseInsensitiveContains("something", "foo"));
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", ""));
|
||||||
|
EXPECT_TRUE(stringCaseInsensitiveContains("", ""));
|
||||||
|
EXPECT_FALSE(stringCaseInsensitiveContains("", "Foo"));
|
||||||
|
}
|
||||||
} // namespace lms::core::stringUtils::tests
|
} // namespace lms::core::stringUtils::tests
|
||||||
@@ -8,6 +8,7 @@ add_library(lmsdatabase SHARED
|
|||||||
impl/Listen.cpp
|
impl/Listen.cpp
|
||||||
impl/MediaLibrary.cpp
|
impl/MediaLibrary.cpp
|
||||||
impl/Migration.cpp
|
impl/Migration.cpp
|
||||||
|
impl/PlayListFile.cpp
|
||||||
impl/PlayQueue.cpp
|
impl/PlayQueue.cpp
|
||||||
impl/TrackArtistLink.cpp
|
impl/TrackArtistLink.cpp
|
||||||
impl/TrackFeatures.cpp
|
impl/TrackFeatures.cpp
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
|
||||||
#include "EnumSetTraits.hpp"
|
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
@@ -182,16 +181,16 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Artist::Artist(const std::string& name, const std::optional<core::UUID>& MBID)
|
Artist::Artist(const std::string& name, const std::optional<core::UUID>& mbid)
|
||||||
: _MBID{ MBID ? MBID->getAsString() : "" }
|
: _mbid{ mbid ? mbid->getAsString() : "" }
|
||||||
{
|
{
|
||||||
setName(name);
|
setName(name);
|
||||||
_sortName = _name;
|
_sortName = _name;
|
||||||
}
|
}
|
||||||
|
|
||||||
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
|
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& mbid)
|
||||||
{
|
{
|
||||||
return session.getDboSession()->add(std::unique_ptr<Artist>{ new Artist{ name, MBID } });
|
return session.getDboSession()->add(std::unique_ptr<Artist>{ new Artist{ name, mbid } });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t Artist::getCount(Session& session)
|
std::size_t Artist::getCount(Session& session)
|
||||||
@@ -322,7 +321,7 @@ namespace lms::db
|
|||||||
return utils::execRangeQuery<ArtistId>(query, range);
|
return utils::execRangeQuery<ArtistId>(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
|
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -354,6 +353,7 @@ namespace lms::db
|
|||||||
});
|
});
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>> res;
|
std::vector<std::vector<Cluster::pointer>> res;
|
||||||
|
res.reserve(clustersByType.size());
|
||||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||||
res.push_back(clusters);
|
res.push_back(clusters);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
|||||||
@@ -170,10 +170,12 @@ namespace lms::db
|
|||||||
query.leftJoin("track t ON d.id = t.directory_id");
|
query.leftJoin("track t ON d.id = t.directory_id");
|
||||||
query.leftJoin("image i ON d.id = i.directory_id");
|
query.leftJoin("image i ON d.id = i.directory_id");
|
||||||
query.leftJoin("track_lyrics l_lrc ON d.id = l_lrc.directory_id");
|
query.leftJoin("track_lyrics l_lrc ON d.id = l_lrc.directory_id");
|
||||||
|
query.leftJoin("playlist_file pl_f ON d.id = pl_f.directory_id");
|
||||||
query.where("d_child.id IS NULL");
|
query.where("d_child.id IS NULL");
|
||||||
query.where("t.directory_id IS NULL");
|
query.where("t.directory_id IS NULL");
|
||||||
query.where("i.directory_id IS NULL");
|
query.where("i.directory_id IS NULL");
|
||||||
query.where("l_lrc.directory_id IS NULL");
|
query.where("l_lrc.directory_id IS NULL");
|
||||||
|
query.where("pl_f.directory_id IS NULL");
|
||||||
|
|
||||||
return utils::execRangeQuery<DirectoryId>(query, range);
|
return utils::execRangeQuery<DirectoryId>(query, range);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ namespace lms::db
|
|||||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.id = ?").bind(id));
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.id = ?").bind(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Image::pointer Image::find(Session& session, const std::filesystem::path& path)
|
Image::pointer Image::find(Session& session, const std::filesystem::path& file)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
|
|
||||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.absolute_file_path = ?").bind(path));
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Image>>("SELECT i from image i").where("i.absolute_file_path = ?").bind(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Image::find(Session& session, ImageId& lastRetrievedImage, std::size_t count, const std::function<void(const Image::pointer&)>& func)
|
void Image::find(Session& session, ImageId& lastRetrievedImage, std::size_t count, const std::function<void(const Image::pointer&)>& func)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 76 };
|
static constexpr Version LMS_DATABASE_VERSION{ 77 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -127,7 +127,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
void migrateFromV36(Session& session)
|
void migrateFromV36(Session& session)
|
||||||
{
|
{
|
||||||
// Increased precision for track durations (now in milliseconds instead of secodns)
|
// Increased precision for track durations (now in milliseconds instead of secodns)
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
// Support Performer tags (via subtypes)
|
// Support Performer tags (via subtypes)
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_artist_link ADD subtype TEXT");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_artist_link ADD subtype TEXT");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD primary_type INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD primary_type INTEGER");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD secondary_types INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD secondary_types INTEGER");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE cluster ADD track_count INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE cluster ADD track_count INTEGER");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE cluster ADD release_count INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE cluster ADD release_count INTEGER");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
// add bitrate
|
// add bitrate
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
|
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,14 +299,14 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))");
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))");
|
||||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))");
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
void migrateFromV48(Session& session)
|
void migrateFromV48(Session& session)
|
||||||
{
|
{
|
||||||
// Regression for the extra tags not being parsed
|
// Regression for the extra tags not being parsed
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD year INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD year INTEGER");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD original_year INTEGER");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD original_year INTEGER");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ SELECT
|
|||||||
// Add sort name for releases
|
// Add sort name for releases
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ SELECT
|
|||||||
// Add release group mbid
|
// Add release group mbid
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,7 +494,7 @@ SELECT
|
|||||||
constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
|
constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
|
||||||
))");
|
))");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +625,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE image");
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE image");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE image_backup RENAME TO image");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE image_backup RENAME TO image");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE directory");
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE directory");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE directory_backup RENAME TO directory");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE directory_backup RENAME TO directory");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,7 +669,7 @@ SELECT
|
|||||||
// Add a new column comment
|
// Add a new column comment
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD comment TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD comment TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +732,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_label_label" on "release_label" ("label_id"))");
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_label_label" on "release_label" ("label_id"))");
|
||||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_label_release" on "release_label" ("release_id"))");
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "release_label_release" on "release_label" ("release_id"))");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +740,7 @@ SELECT
|
|||||||
{
|
{
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD is_compilation BOOLEAN NOT NULL DEFAULT(false)");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD is_compilation BOOLEAN NOT NULL DEFAULT(false)");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +805,7 @@ SELECT
|
|||||||
for (const auto& indexName : indexeNames)
|
for (const auto& indexName : indexeNames)
|
||||||
utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName);
|
utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName);
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,7 +868,7 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE artist");
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE artist");
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,7 +903,7 @@ SELECT
|
|||||||
constraint "fk_track_lyrics_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
|
constraint "fk_track_lyrics_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
|
||||||
))");
|
))");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,7 +937,7 @@ SELECT
|
|||||||
// Add catalog number
|
// Add catalog number
|
||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD barcode TEXT NOT NULL DEFAULT ''");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release ADD barcode TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,6 +968,67 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD bcrypt_round_count INTEGER NOT NULL DEFAULT(7)");
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD bcrypt_round_count INTEGER NOT NULL DEFAULT(7)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV76(Session& session)
|
||||||
|
{
|
||||||
|
// Rename public -> visibility (false is Private(0) and true is Public(1))
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE tracklist RENAME COLUMN public TO visibility");
|
||||||
|
|
||||||
|
// Supported extensions are now runtime
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings DROP COLUMN audio_file_extensions");
|
||||||
|
|
||||||
|
// Add PlayListFile
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "playlist_file" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"absolute_file_path" text not null,
|
||||||
|
"file_stem" text not null,
|
||||||
|
"file_size" bigint not null,
|
||||||
|
"file_last_write" text,
|
||||||
|
"name" text not null,
|
||||||
|
"entries" text not null,
|
||||||
|
"media_library_id" bigint,
|
||||||
|
"directory_id" bigint,
|
||||||
|
constraint "fk_playlist_file_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
|
||||||
|
constraint "fk_playlist_file_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
// Add a link into tracklist to ease cleanup
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "tracklist_backup" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"name" text not null,
|
||||||
|
"type" integer not null,
|
||||||
|
"visibility" integer not null,
|
||||||
|
"creation_date_time" text,
|
||||||
|
"last_modified_date_time" text,
|
||||||
|
"user_id" bigint,
|
||||||
|
"playlist_file_id" bigint,
|
||||||
|
constraint "fk_tracklist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_tracklist_playlist_file" foreign key ("playlist_file_id") references "playlist_file" ("id") on delete cascade deferrable initially deferred))");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO tracklist_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
visibility,
|
||||||
|
creation_date_time,
|
||||||
|
last_modified_date_time,
|
||||||
|
user_id,
|
||||||
|
NULL AS playlist_file_id
|
||||||
|
FROM tracklist)");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(DROP TABLE tracklist)");
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE tracklist_backup RENAME TO tracklist)");
|
||||||
|
|
||||||
|
// Add a file name in tracks
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track ADD COLUMN file_name TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -1020,6 +1081,7 @@ SELECT
|
|||||||
{ 73, migrateFromV73 },
|
{ 73, migrateFromV73 },
|
||||||
{ 74, migrateFromV74 },
|
{ 74, migrateFromV74 },
|
||||||
{ 75, migrateFromV75 },
|
{ 75, migrateFromV75 },
|
||||||
|
{ 76, migrateFromV76 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
|
#include "database/User.hpp"
|
||||||
|
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "PathTraits.hpp"
|
||||||
|
#include "StringViewTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
PlayListFile::PlayListFile(const std::filesystem::path& file)
|
||||||
|
{
|
||||||
|
setAbsoluteFilePath(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayListFile::pointer PlayListFile::create(Session& session, const std::filesystem::path& file)
|
||||||
|
{
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<PlayListFile>{ new PlayListFile{ file } });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t PlayListFile::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM playlist_file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayListFile::pointer PlayListFile::find(Session& session, const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<PlayListFile>>("SELECT pl_f from playlist_file pl_f").where("pl_f.absolute_file_path = ?").bind(p.string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::find(Session& session, PlayListFileId& lastRetrievedId, std::size_t count, const std::function<void(const pointer&)>& func)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<PlayListFile>>("SELECT pl_f from playlist_file pl_f").orderBy("pl_f.id").where("pl_f.id > ?").bind(lastRetrievedId).limit(static_cast<int>(count)) };
|
||||||
|
|
||||||
|
utils::forEachQueryResult(query, [&](const PlayListFile::pointer& playList) {
|
||||||
|
func(playList);
|
||||||
|
lastRetrievedId = playList->getId();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayListFile::pointer PlayListFile::find(Session& session, PlayListFileId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<PlayListFile>>("SELECT pl_f from playlist_file pl_f").where("pl_f.id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> PlayListFile::getFiles() const
|
||||||
|
{
|
||||||
|
std::vector<std::filesystem::path> files;
|
||||||
|
{
|
||||||
|
Wt::Json::Object root;
|
||||||
|
Wt::Json::parse(_entries, root);
|
||||||
|
|
||||||
|
assert(root.type("files") == Wt::Json::Type::Array);
|
||||||
|
const Wt::Json::Array& filesArray = root.get("files");
|
||||||
|
for (const Wt::Json::Value& file : filesArray)
|
||||||
|
files.push_back(static_cast<std::string>(file.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackList::pointer PlayListFile::getTrackList() const
|
||||||
|
{
|
||||||
|
return _trackList.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory::pointer PlayListFile::getDirectory() const
|
||||||
|
{
|
||||||
|
return _directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
||||||
|
{
|
||||||
|
assert(filePath.is_absolute());
|
||||||
|
_absoluteFilePath = filePath;
|
||||||
|
_fileStem = filePath.stem();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::setDirectory(ObjectPtr<Directory> directory)
|
||||||
|
{
|
||||||
|
_directory = getDboPtr(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::setTrackList(ObjectPtr<TrackList> trackList)
|
||||||
|
{
|
||||||
|
_trackList = getDboPtr(trackList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::setName(std::string_view name)
|
||||||
|
{
|
||||||
|
_name = std::string{ name, 0, _maxNameLength };
|
||||||
|
if (name.size() > _maxNameLength)
|
||||||
|
LMS_LOG(DB, WARNING, "PlaylistFile name too long, truncated to '" << _name << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFile::setFiles(std::span<const std::filesystem::path> files)
|
||||||
|
{
|
||||||
|
Wt::Json::Object root;
|
||||||
|
|
||||||
|
Wt::Json::Array fileArray;
|
||||||
|
for (const auto& file : files)
|
||||||
|
fileArray.push_back(Wt::Json::Value{ file.string() });
|
||||||
|
|
||||||
|
root["files"] = std::move(fileArray);
|
||||||
|
_entries = Wt::Json::serialize(root);
|
||||||
|
}
|
||||||
|
} // namespace lms::db
|
||||||
@@ -46,17 +46,6 @@ namespace lms::db
|
|||||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>());
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
|
|
||||||
{
|
|
||||||
const auto extensions{ core::stringUtils::splitString(_audioFileExtensions, ' ') };
|
|
||||||
|
|
||||||
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
|
|
||||||
std::sort(std::begin(res), std::end(res));
|
|
||||||
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||||
{
|
{
|
||||||
std::vector<std::string_view> tags{ core::stringUtils::splitString(_extraTagsToScan, ';') };
|
std::vector<std::string_view> tags{ core::stringUtils::splitString(_extraTagsToScan, ';') };
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
#include "database/Listen.hpp"
|
#include "database/Listen.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
#include "database/PlayQueue.hpp"
|
#include "database/PlayQueue.hpp"
|
||||||
#include "database/RatedArtist.hpp"
|
#include "database/RatedArtist.hpp"
|
||||||
#include "database/RatedRelease.hpp"
|
#include "database/RatedRelease.hpp"
|
||||||
@@ -105,6 +106,7 @@ namespace lms::db
|
|||||||
_session.mapClass<Label>("label");
|
_session.mapClass<Label>("label");
|
||||||
_session.mapClass<Listen>("listen");
|
_session.mapClass<Listen>("listen");
|
||||||
_session.mapClass<MediaLibrary>("media_library");
|
_session.mapClass<MediaLibrary>("media_library");
|
||||||
|
_session.mapClass<PlayListFile>("playlist_file");
|
||||||
_session.mapClass<PlayQueue>("playqueue");
|
_session.mapClass<PlayQueue>("playqueue");
|
||||||
_session.mapClass<RatedArtist>("rated_artist");
|
_session.mapClass<RatedArtist>("rated_artist");
|
||||||
_session.mapClass<RatedRelease>("rated_release");
|
_session.mapClass<RatedRelease>("rated_release");
|
||||||
@@ -181,98 +183,107 @@ namespace lms::db
|
|||||||
LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation");
|
LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation");
|
||||||
LMS_LOG(DB, INFO, "Creating indexes... This may take a while...");
|
LMS_LOG(DB, INFO, "Creating indexes... This may take a while...");
|
||||||
|
|
||||||
auto transaction{ createWriteTransaction() };
|
{
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
auto transaction{ createWriteTransaction() };
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_date_time_idx ON listen(user_id, backend, date_time DESC)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_date_time_idx ON listen(user_id, backend, date_time DESC)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)");
|
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_file_stem_idx ON track(directory_id, file_stem);");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||||
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 release_type_name_idx ON release_type(name)");
|
||||||
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_mbid_idx ON track(mbid)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
|
|
||||||
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_original_year_idx ON track(original_year)");
|
|
||||||
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_idx ON track(release_id)");
|
|
||||||
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_year_idx ON track(release_id, year)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
|
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_file_stem_idx ON track(directory_id, file_stem);");
|
||||||
|
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_mbid_idx ON track(mbid)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
|
||||||
|
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_original_year_idx ON track(original_year)");
|
||||||
|
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_idx ON track(release_id)");
|
||||||
|
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_year_idx ON track(release_id, year)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_last_modified_date_time_idx ON tracklist(last_modified_date_time)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_type_track_artist_idx ON track_artist_link(type, track_id, artist_id)");
|
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_idx ON tracklist_entry(id)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_type_track_artist_idx ON track_artist_link(type, track_id, artist_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
|
||||||
|
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
|
||||||
|
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
|
||||||
|
}
|
||||||
|
|
||||||
LMS_LOG(DB, INFO, "Indexes created!");
|
LMS_LOG(DB, INFO, "Indexes created!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ namespace lms::db
|
|||||||
for (std::string_view keyword : params.keywords)
|
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::escapeLikeKeyword(keyword) + "%");
|
||||||
|
|
||||||
if (!params.stem.empty())
|
if (!params.fileStem.empty())
|
||||||
query.where("t.file_stem = ?").bind(params.stem);
|
query.where("t.file_stem = ?").bind(params.fileStem);
|
||||||
|
|
||||||
|
if (!params.fileName.empty())
|
||||||
|
query.where("t.file_name = ?").bind(params.fileName);
|
||||||
|
|
||||||
if (!params.name.empty())
|
if (!params.name.empty())
|
||||||
query.where("t.name = ?").bind(params.name);
|
query.where("t.name = ?").bind(params.name);
|
||||||
@@ -375,8 +378,10 @@ namespace lms::db
|
|||||||
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
||||||
{
|
{
|
||||||
assert(filePath.is_absolute());
|
assert(filePath.is_absolute());
|
||||||
|
|
||||||
_absoluteFilePath = filePath;
|
_absoluteFilePath = filePath;
|
||||||
_fileStem = filePath.stem();
|
_fileStem = filePath.stem();
|
||||||
|
_fileName = filePath.filename();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Track::setRelativeFilePath(const std::filesystem::path& filePath)
|
void Track::setRelativeFilePath(const std::filesystem::path& filePath)
|
||||||
@@ -385,6 +390,7 @@ namespace lms::db
|
|||||||
|
|
||||||
assert(_absoluteFilePath.filename() == filePath.filename()); // must be compatible with previous setAbsoluteFilePath call
|
assert(_absoluteFilePath.filename() == filePath.filename()); // must be compatible with previous setAbsoluteFilePath call
|
||||||
_fileStem = filePath.stem(); // lazy migration (_fileStem added later, could be set only with setAbsoluteFilePath)
|
_fileStem = filePath.stem(); // lazy migration (_fileStem added later, could be set only with setAbsoluteFilePath)
|
||||||
|
_fileName = filePath.filename(); // lazy migration (_fileName added later, could be set only with setAbsoluteFilePath)
|
||||||
_relativeFilePath = filePath;
|
_relativeFilePath = filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,8 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/PlayListFile.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
@@ -40,10 +38,18 @@ namespace lms::db
|
|||||||
template<typename ResultType>
|
template<typename ResultType>
|
||||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params)
|
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params)
|
||||||
{
|
{
|
||||||
auto query{ session.getDboSession()->query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
assert(!params.user.isValid() || !params.excludedUser.isValid());
|
||||||
|
|
||||||
|
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
||||||
|
|
||||||
if (!params.clusters.empty() || params.mediaLibrary.isValid())
|
if (!params.clusters.empty() || params.mediaLibrary.isValid())
|
||||||
|
{
|
||||||
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
|
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
|
||||||
|
query.groupBy("t_l.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::string_view keyword : params.keywords)
|
||||||
|
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||||
|
|
||||||
if (params.mediaLibrary.isValid())
|
if (params.mediaLibrary.isValid())
|
||||||
query.join("track t ON t.id = t_l_e.track_id");
|
query.join("track t ON t.id = t_l_e.track_id");
|
||||||
@@ -53,10 +59,15 @@ namespace lms::db
|
|||||||
|
|
||||||
if (params.user.isValid())
|
if (params.user.isValid())
|
||||||
query.where("t_l.user_id = ?").bind(params.user);
|
query.where("t_l.user_id = ?").bind(params.user);
|
||||||
|
else if (params.excludedUser.isValid())
|
||||||
|
query.where("t_l.user_id <> ? OR t_l.user_id IS NULL").bind(params.excludedUser);
|
||||||
|
|
||||||
if (params.type)
|
if (params.type)
|
||||||
query.where("t_l.type = ?").bind(*params.type);
|
query.where("t_l.type = ?").bind(*params.type);
|
||||||
|
|
||||||
|
if (params.visibility)
|
||||||
|
query.where("t_l.visibility = ?").bind(*params.visibility);
|
||||||
|
|
||||||
if (!params.clusters.empty())
|
if (!params.clusters.empty())
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
@@ -108,20 +119,17 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
TrackList::TrackList(std::string_view name, TrackListType type)
|
||||||
: _name{ name }
|
: _name{ name }
|
||||||
, _type{ type }
|
, _type{ type }
|
||||||
, _isPublic{ isPublic }
|
|
||||||
, _creationDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
, _creationDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||||
, _lastModifiedDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
, _lastModifiedDateTime{ utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
|
||||||
, _user{ getDboPtr(user) }
|
|
||||||
{
|
{
|
||||||
assert(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type)
|
||||||
{
|
{
|
||||||
return session.getDboSession()->add(std::unique_ptr<TrackList>{ new TrackList{ name, type, isPublic, user } });
|
return session.getDboSession()->add(std::unique_ptr<TrackList>{ new TrackList{ name, type } });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t TrackList::getCount(Session& session)
|
std::size_t TrackList::getCount(Session& session)
|
||||||
@@ -311,20 +319,24 @@ namespace lms::db
|
|||||||
return session.getDboSession()->add(std::unique_ptr<TrackListEntry>{ new TrackListEntry{ track, tracklist, dateTime } });
|
return session.getDboSession()->add(std::unique_ptr<TrackListEntry>{ new TrackListEntry{ track, tracklist, dateTime } });
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackListEntry::onPostCreated()
|
|
||||||
{
|
|
||||||
_tracklist.modify()->setLastModifiedDateTime(utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TrackListEntry::onPreRemove()
|
|
||||||
{
|
|
||||||
_tracklist.modify()->setLastModifiedDateTime(utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
|
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
|
|
||||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackListEntry>().where("id = ?").bind(id));
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackListEntry>().where("id = ?").bind(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TrackListEntry::find(Session& session, const FindParameters& params, const std::function<void(const TrackListEntry::pointer&)>& func)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackListEntry>>("SELECT t_l_e FROM tracklist_entry t_l_e") };
|
||||||
|
|
||||||
|
if (params.trackList.isValid())
|
||||||
|
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
|
||||||
|
query.orderBy("t_l_e.id");
|
||||||
|
|
||||||
|
utils::forEachQueryRangeResult(query, params.range, func);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -130,16 +130,16 @@ namespace lms::db
|
|||||||
static pointer find(Session& session, ArtistId id);
|
static pointer find(Session& session, ArtistId id);
|
||||||
static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field
|
static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field
|
||||||
static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function<void(const Artist::pointer&)>& func, MediaLibraryId library = {});
|
static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function<void(const Artist::pointer&)>& func, MediaLibraryId library = {});
|
||||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
|
||||||
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& params);
|
||||||
static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
|
static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
|
||||||
static bool exists(Session& session, ArtistId id);
|
static bool exists(Session& session, ArtistId id);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
const std::string& getSortName() const { return _sortName; }
|
const std::string& getSortName() const { return _sortName; }
|
||||||
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
|
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_mbid); }
|
||||||
ObjectPtr<Image> getImage() const;
|
ObjectPtr<Image> getImage() const;
|
||||||
|
|
||||||
// No artistLinkTypes means get them all
|
// No artistLinkTypes means get them all
|
||||||
@@ -148,10 +148,10 @@ namespace lms::db
|
|||||||
// Get the cluster of the tracks made by this artist
|
// Get the cluster of the tracks made by this artist
|
||||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||||
// size is the max number of cluster per cluster type
|
// size is the max number of cluster per cluster type
|
||||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
||||||
|
|
||||||
void setName(std::string_view name);
|
void setName(std::string_view name);
|
||||||
void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid ? mbid->getAsString() : ""; }
|
||||||
void setSortName(std::string_view sortName);
|
void setSortName(std::string_view sortName);
|
||||||
void setImage(ObjectPtr<Image> image);
|
void setImage(ObjectPtr<Image> image);
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||||
Wt::Dbo::field(a, _MBID, "mbid");
|
Wt::Dbo::field(a, _mbid, "mbid");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||||
@@ -173,11 +173,11 @@ namespace lms::db
|
|||||||
friend class Session;
|
friend class Session;
|
||||||
// Create
|
// Create
|
||||||
Artist(const std::string& name, const std::optional<core::UUID>& MBID = {});
|
Artist(const std::string& name, const std::optional<core::UUID>& MBID = {});
|
||||||
static pointer create(Session& session, const std::string& name, const std::optional<core::UUID>& UUID = {});
|
static pointer create(Session& session, const std::string& name, const std::optional<core::UUID>& mbid = std::nullopt);
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
std::string _sortName;
|
std::string _sortName;
|
||||||
std::string _MBID; // Musicbrainz Identifier
|
std::string _mbid; // Musicbrainz Identifier
|
||||||
|
|
||||||
Wt::Dbo::ptr<Image> _image;
|
Wt::Dbo::ptr<Image> _image;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ namespace lms::db
|
|||||||
TransactionChecker::checkWriteTransaction(*_obj.session());
|
TransactionChecker::checkWriteTransaction(*_obj.session());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (_obj->hasOnPreRemove())
|
|
||||||
_obj.modify()->onPreRemove();
|
|
||||||
_obj.remove();
|
_obj.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +85,6 @@ namespace lms::db
|
|||||||
template<typename>
|
template<typename>
|
||||||
friend class ObjectPtr;
|
friend class ObjectPtr;
|
||||||
|
|
||||||
virtual bool hasOnPreRemove() const { return false; }
|
|
||||||
virtual void onPreRemove() {}
|
|
||||||
|
|
||||||
virtual bool hasOnPostCreated() const { return false; }
|
|
||||||
virtual void onPostCreated() {}
|
|
||||||
|
|
||||||
// Can get raw dbo ptr only from Objects
|
// Can get raw dbo ptr only from Objects
|
||||||
template<typename SomeObject>
|
template<typename SomeObject>
|
||||||
static Wt::Dbo::ptr<SomeObject> getDboPtr(const ObjectPtr<SomeObject>& ptr)
|
static Wt::Dbo::ptr<SomeObject> getDboPtr(const ObjectPtr<SomeObject>& ptr)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "database/DirectoryId.hpp"
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(PlayListFileId)
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
class Directory;
|
||||||
|
class MediaLibrary;
|
||||||
|
class TrackList;
|
||||||
|
|
||||||
|
class PlayListFile final : public Object<PlayListFile, PlayListFileId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PlayListFile() = default;
|
||||||
|
|
||||||
|
// find
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, PlayListFileId id);
|
||||||
|
static pointer find(Session& session, const std::filesystem::path& path);
|
||||||
|
static void find(Session& session, PlayListFileId& lastRetrievedId, std::size_t count, const std::function<void(const pointer&)>& func);
|
||||||
|
|
||||||
|
// getters
|
||||||
|
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||||
|
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
|
||||||
|
std::size_t getFileSize() const { return _fileSize; }
|
||||||
|
std::string_view getName() const { return _name; }
|
||||||
|
std::vector<std::filesystem::path> getFiles() const;
|
||||||
|
ObjectPtr<TrackList> getTrackList() const;
|
||||||
|
ObjectPtr<Directory> getDirectory() const;
|
||||||
|
DirectoryId getDirectoryId() const { return _directory.id(); }
|
||||||
|
|
||||||
|
// setters
|
||||||
|
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||||
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
|
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||||
|
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
|
||||||
|
void setDirectory(ObjectPtr<Directory> directory);
|
||||||
|
void setTrackList(ObjectPtr<TrackList> trackList);
|
||||||
|
void setName(std::string_view name);
|
||||||
|
void setFiles(std::span<const std::filesystem::path> files);
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
||||||
|
Wt::Dbo::field(a, _fileStem, "file_stem");
|
||||||
|
Wt::Dbo::field(a, _fileSize, "file_size");
|
||||||
|
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||||
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
Wt::Dbo::field(a, _entries, "entries");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete playlist on media library removal, we want to wait for the next scan to have a chance to migrate files
|
||||||
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::hasOne(a, _trackList, "playlist_file");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Session;
|
||||||
|
PlayListFile(const std::filesystem::path& file);
|
||||||
|
static pointer create(Session& session, const std::filesystem::path& file);
|
||||||
|
|
||||||
|
static constexpr std::size_t _maxNameLength{ 512 };
|
||||||
|
|
||||||
|
std::filesystem::path _absoluteFilePath;
|
||||||
|
std::string _fileStem;
|
||||||
|
Wt::WDateTime _fileLastWrite;
|
||||||
|
Wt::WDateTime _fileAdded;
|
||||||
|
long long _fileSize{};
|
||||||
|
|
||||||
|
std::string _name;
|
||||||
|
std::string _entries; // A json encoded list of files
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||||
|
Wt::Dbo::ptr<Directory> _directory;
|
||||||
|
Wt::Dbo::weak_ptr<TrackList> _trackList;
|
||||||
|
};
|
||||||
|
} // namespace lms::db
|
||||||
@@ -67,7 +67,6 @@ namespace lms::db
|
|||||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
|
||||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||||
std::vector<std::string> getArtistTagDelimiters() const;
|
std::vector<std::string> getArtistTagDelimiters() const;
|
||||||
std::vector<std::string> getDefaultTagDelimiters() const;
|
std::vector<std::string> getDefaultTagDelimiters() const;
|
||||||
@@ -87,7 +86,6 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||||
Wt::Dbo::field(a, _startTime, "start_time");
|
Wt::Dbo::field(a, _startTime, "start_time");
|
||||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
|
||||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||||
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
||||||
@@ -99,7 +97,6 @@ namespace lms::db
|
|||||||
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
||||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv .dsf" };
|
|
||||||
std::string _extraTagsToScan;
|
std::string _extraTagsToScan;
|
||||||
std::string _artistTagDelimiters;
|
std::string _artistTagDelimiters;
|
||||||
std::string _defaultTagDelimiters;
|
std::string _defaultTagDelimiters;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/RecursiveSharedMutex.hpp"
|
#include "core/RecursiveSharedMutex.hpp"
|
||||||
#include "database/Object.hpp"
|
|
||||||
#include "database/TransactionChecker.hpp"
|
#include "database/TransactionChecker.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -69,7 +68,10 @@ namespace lms::db
|
|||||||
class Session
|
class Session
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Session(Db& database);
|
Session(Db& db);
|
||||||
|
~Session() = default;
|
||||||
|
Session(const Session&) = delete;
|
||||||
|
Session& operator=(const Session&) = delete;
|
||||||
|
|
||||||
[[nodiscard]] WriteTransaction createWriteTransaction();
|
[[nodiscard]] WriteTransaction createWriteTransaction();
|
||||||
[[nodiscard]] ReadTransaction createReadTransaction();
|
[[nodiscard]] ReadTransaction createReadTransaction();
|
||||||
@@ -119,16 +121,10 @@ namespace lms::db
|
|||||||
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
|
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
|
||||||
getDboSession()->flush();
|
getDboSession()->flush();
|
||||||
|
|
||||||
if (res->hasOnPostCreated())
|
|
||||||
res.modify()->onPostCreated();
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Session(const Session&) = delete;
|
|
||||||
Session& operator=(const Session&) = delete;
|
|
||||||
|
|
||||||
Db& _db;
|
Db& _db;
|
||||||
Wt::Dbo::Session _session;
|
Wt::Dbo::Session _session;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
@@ -67,8 +66,9 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
||||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
std::string name; // if non empty, must match this name
|
std::string name; // if non empty, must match this name (title)
|
||||||
std::string stem; // if non empty, must match this stem
|
std::string fileStem; // if non empty, must match this file stem
|
||||||
|
std::string fileName; // if non empty, must match this file name
|
||||||
TrackSortMethod sortMethod{ TrackSortMethod::None };
|
TrackSortMethod sortMethod{ TrackSortMethod::None };
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
Wt::WDateTime writtenAfter;
|
Wt::WDateTime writtenAfter;
|
||||||
@@ -102,11 +102,17 @@ namespace lms::db
|
|||||||
name = _name;
|
name = _name;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
FindParameters& setStem(std::string_view _stem)
|
FindParameters& setFileStem(std::string_view _fileStem)
|
||||||
{
|
{
|
||||||
stem = _stem;
|
fileStem = _fileStem;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setFileName(std::string_view _fileName)
|
||||||
|
{
|
||||||
|
fileName = _fileName;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
FindParameters& setSortMethod(TrackSortMethod _method)
|
FindParameters& setSortMethod(TrackSortMethod _method)
|
||||||
{
|
{
|
||||||
sortMethod = _method;
|
sortMethod = _method;
|
||||||
@@ -317,6 +323,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
||||||
Wt::Dbo::field(a, _relativeFilePath, "relative_file_path");
|
Wt::Dbo::field(a, _relativeFilePath, "relative_file_path");
|
||||||
Wt::Dbo::field(a, _fileStem, "file_stem");
|
Wt::Dbo::field(a, _fileStem, "file_stem");
|
||||||
|
Wt::Dbo::field(a, _fileName, "file_name");
|
||||||
Wt::Dbo::field(a, _fileSize, "file_size");
|
Wt::Dbo::field(a, _fileSize, "file_size");
|
||||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||||
Wt::Dbo::field(a, _fileAdded, "file_added");
|
Wt::Dbo::field(a, _fileAdded, "file_added");
|
||||||
@@ -364,6 +371,7 @@ namespace lms::db
|
|||||||
std::filesystem::path _absoluteFilePath; // full path
|
std::filesystem::path _absoluteFilePath; // full path
|
||||||
std::filesystem::path _relativeFilePath; // relative to root (that may be deleted)
|
std::filesystem::path _relativeFilePath; // relative to root (that may be deleted)
|
||||||
std::filesystem::path _fileStem;
|
std::filesystem::path _fileStem;
|
||||||
|
std::filesystem::path _fileName;
|
||||||
long long _fileSize{};
|
long long _fileSize{};
|
||||||
Wt::WDateTime _fileLastWrite;
|
Wt::WDateTime _fileLastWrite;
|
||||||
Wt::WDateTime _fileAdded;
|
Wt::WDateTime _fileAdded;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace lms::db
|
|||||||
class Artist;
|
class Artist;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
|
class PlayListFile;
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
@@ -52,21 +53,35 @@ namespace lms::db
|
|||||||
public:
|
public:
|
||||||
TrackList() = default;
|
TrackList() = default;
|
||||||
|
|
||||||
|
enum class Visibility
|
||||||
|
{
|
||||||
|
Private = 0,
|
||||||
|
Public = 1,
|
||||||
|
};
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
||||||
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
std::optional<TrackListType> type;
|
std::optional<TrackListType> type;
|
||||||
UserId user; // only tracklists owned by this user
|
UserId user; // only tracklists owned by this user
|
||||||
|
UserId excludedUser; // only tracklists *not* owned by this user
|
||||||
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
|
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
|
||||||
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
||||||
|
std::optional<Visibility> visibility;
|
||||||
|
|
||||||
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
||||||
{
|
{
|
||||||
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
|
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
|
||||||
|
{
|
||||||
|
keywords = _keywords;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
FindParameters& setRange(std::optional<Range> _range)
|
FindParameters& setRange(std::optional<Range> _range)
|
||||||
{
|
{
|
||||||
range = _range;
|
range = _range;
|
||||||
@@ -82,6 +97,12 @@ namespace lms::db
|
|||||||
user = _user;
|
user = _user;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setExcludedUser(UserId _user)
|
||||||
|
{
|
||||||
|
excludedUser = _user;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
||||||
{
|
{
|
||||||
mediaLibrary = _mediaLibrary;
|
mediaLibrary = _mediaLibrary;
|
||||||
@@ -92,6 +113,11 @@ namespace lms::db
|
|||||||
sortMethod = _sortMethod;
|
sortMethod = _sortMethod;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setVisibility(std::optional<Visibility> _visibility)
|
||||||
|
{
|
||||||
|
visibility = _visibility;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
|
static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
|
||||||
@@ -101,13 +127,17 @@ namespace lms::db
|
|||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
std::string_view getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
bool isPublic() const { return _isPublic; }
|
Visibility getVisibility() const { return _visibility; }
|
||||||
TrackListType getType() const { return _type; }
|
TrackListType getType() const { return _type; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
UserId getUserId() const { return _user.id(); }
|
||||||
|
Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; }
|
||||||
|
Wt::WDateTime getCreationDateTime() const { return _creationDateTime; }
|
||||||
|
|
||||||
// Modifiers
|
// Modifiers
|
||||||
void setName(const std::string& name) { _name = name; }
|
void setUser(ObjectPtr<User> user) { _user = getDboPtr(user); }
|
||||||
void setIsPublic(bool isPublic) { _isPublic = isPublic; }
|
void setName(std::string_view name) { _name = name; }
|
||||||
|
void setVisibility(Visibility visibility) { _visibility = visibility; }
|
||||||
void clear() { _entries.clear(); }
|
void clear() { _entries.clear(); }
|
||||||
|
|
||||||
// Get tracks, ordered by position
|
// Get tracks, ordered by position
|
||||||
@@ -134,26 +164,29 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
Wt::Dbo::field(a, _type, "type");
|
Wt::Dbo::field(a, _type, "type");
|
||||||
Wt::Dbo::field(a, _isPublic, "public");
|
Wt::Dbo::field(a, _visibility, "visibility");
|
||||||
Wt::Dbo::field(a, _creationDateTime, "creation_date_time");
|
Wt::Dbo::field(a, _creationDateTime, "creation_date_time");
|
||||||
Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time");
|
Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); // optional
|
||||||
|
Wt::Dbo::belongsTo(a, _playListFile, "playlist_file", Wt::Dbo::OnDeleteCascade); // optional
|
||||||
|
|
||||||
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
|
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
|
TrackList(std::string_view name, TrackListType type);
|
||||||
static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
|
static pointer create(Session& session, std::string_view name, TrackListType type);
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
TrackListType _type{ TrackListType::Playlist };
|
TrackListType _type{ TrackListType::PlayList };
|
||||||
bool _isPublic{ false };
|
Visibility _visibility{ Visibility::Private };
|
||||||
Wt::WDateTime _creationDateTime;
|
Wt::WDateTime _creationDateTime;
|
||||||
Wt::WDateTime _lastModifiedDateTime;
|
Wt::WDateTime _lastModifiedDateTime;
|
||||||
|
|
||||||
Wt::Dbo::ptr<User> _user;
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
Wt::Dbo::ptr<PlayListFile> _playListFile;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,16 +195,29 @@ namespace lms::db
|
|||||||
public:
|
public:
|
||||||
TrackListEntry() = default;
|
TrackListEntry() = default;
|
||||||
|
|
||||||
bool hasOnPostCreated() const override { return true; }
|
|
||||||
void onPostCreated() override;
|
|
||||||
|
|
||||||
bool hasOnPreRemove() const override { return true; }
|
|
||||||
void onPreRemove() override;
|
|
||||||
|
|
||||||
// find utility
|
// find utility
|
||||||
|
// Search utility
|
||||||
|
struct FindParameters
|
||||||
|
{
|
||||||
|
TrackListId trackList;
|
||||||
|
std::optional<Range> range;
|
||||||
|
|
||||||
|
FindParameters& setTrackList(TrackListId _trackList)
|
||||||
|
{
|
||||||
|
trackList = _trackList;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
FindParameters& setRange(std::optional<Range> _range)
|
||||||
|
{
|
||||||
|
range = _range;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
static pointer getById(Session& session, TrackListEntryId id);
|
static pointer getById(Session& session, TrackListEntryId id);
|
||||||
|
static void find(Session& session, const FindParameters& params, const std::function<void(const TrackListEntry::pointer&)>& func);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
|
TrackId getTrackId() const { return _track.id(); }
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
|
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ namespace lms::db
|
|||||||
|
|
||||||
enum class TrackListType
|
enum class TrackListType
|
||||||
{
|
{
|
||||||
Playlist, // user controlled playlists
|
PlayList = 0, // user controlled playlists
|
||||||
Internal, // internal usage (current playqueue, history, ...)
|
Internal = 1, // internal usage (current playqueue, history, ...)
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ add_executable(test-database
|
|||||||
Image.cpp
|
Image.cpp
|
||||||
Listen.cpp
|
Listen.cpp
|
||||||
Migration.cpp
|
Migration.cpp
|
||||||
|
PlayListFile.cpp
|
||||||
RatedArtist.cpp
|
RatedArtist.cpp
|
||||||
RatedRelease.cpp
|
RatedRelease.cpp
|
||||||
RatedTrack.cpp
|
RatedTrack.cpp
|
||||||
|
|||||||
@@ -718,8 +718,7 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
std::list<ScopedTrack> tracks;
|
std::list<ScopedTrack> tracks;
|
||||||
@@ -743,15 +742,14 @@ namespace lms::db::tests
|
|||||||
const auto similarTracks{ trackList->getSimilarTracks() };
|
const auto similarTracks{ trackList->getSimilarTracks() };
|
||||||
EXPECT_EQ(similarTracks.size(), 5);
|
EXPECT_EQ(similarTracks.size(), 5);
|
||||||
|
|
||||||
for (auto similarTrack : similarTracks)
|
for (const auto& similarTrack : similarTracks)
|
||||||
EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::cend(tracks), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); }));
|
EXPECT_TRUE(std::any_of(std::next(std::cbegin(tracks), 5), std::cend(tracks), [similarTrack](const ScopedTrack& track) { return track.getId() == similarTrack->getId(); }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
#include "database/PlayQueue.hpp"
|
#include "database/PlayQueue.hpp"
|
||||||
#include "database/RatedArtist.hpp"
|
#include "database/RatedArtist.hpp"
|
||||||
#include "database/RatedRelease.hpp"
|
#include "database/RatedRelease.hpp"
|
||||||
@@ -345,6 +346,7 @@ VALUES
|
|||||||
EXPECT_FALSE(Image::find(session, ImageId{}));
|
EXPECT_FALSE(Image::find(session, ImageId{}));
|
||||||
EXPECT_FALSE(Label::find(session, LabelId{}));
|
EXPECT_FALSE(Label::find(session, LabelId{}));
|
||||||
EXPECT_FALSE(Listen::find(session, ListenId{}));
|
EXPECT_FALSE(Listen::find(session, ListenId{}));
|
||||||
|
EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{}));
|
||||||
EXPECT_FALSE(PlayQueue::find(session, PlayQueueId{}));
|
EXPECT_FALSE(PlayQueue::find(session, PlayQueueId{}));
|
||||||
EXPECT_FALSE(RatedArtist::find(session, RatedArtistId{}));
|
EXPECT_FALSE(RatedArtist::find(session, RatedArtistId{}));
|
||||||
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
|
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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/PlayListFile.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
|
|
||||||
|
namespace lms::db::tests
|
||||||
|
{
|
||||||
|
using ScopedPlayListFile = ScopedEntity<db::PlayListFile>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, PlayListFile)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(PlayListFile::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedPlayListFile playlist{ session, "/tmp/foo.m3u" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(PlayListFile::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const PlayListFile::pointer dbPlayList{ playlist.get() };
|
||||||
|
|
||||||
|
EXPECT_EQ(dbPlayList->getAbsoluteFilePath(), "/tmp/foo.m3u");
|
||||||
|
EXPECT_EQ(dbPlayList->getLastWriteTime(), Wt::WDateTime{});
|
||||||
|
EXPECT_EQ(dbPlayList->getFileSize(), 0);
|
||||||
|
EXPECT_EQ(dbPlayList->getName(), "");
|
||||||
|
EXPECT_EQ(dbPlayList->getTrackList(), TrackList::pointer{});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::PlayList };
|
||||||
|
|
||||||
|
// Now change some values
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
PlayListFile::pointer dbPlayList{ playlist.get() };
|
||||||
|
|
||||||
|
dbPlayList.modify()->setAbsoluteFilePath("/tmp/bar.m3u");
|
||||||
|
dbPlayList.modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2024, 30, 1 }, Wt::WTime{ 12, 58, 29 } });
|
||||||
|
dbPlayList.modify()->setFileSize(1234);
|
||||||
|
dbPlayList.modify()->setName("My playlist");
|
||||||
|
dbPlayList.modify()->setTrackList(trackList.get());
|
||||||
|
dbPlayList.modify()->setFiles({ { std::filesystem::path{ "/foo/foo.mp3" }, std::filesystem::path{ "/foo/bar.mp3" } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check values are reflected
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const PlayListFile::pointer dbPlayList{ playlist.get() };
|
||||||
|
|
||||||
|
EXPECT_EQ(dbPlayList->getAbsoluteFilePath(), "/tmp/bar.m3u");
|
||||||
|
EXPECT_EQ(dbPlayList->getLastWriteTime(), (Wt::WDateTime{ Wt::WDate{ 2024, 30, 1 }, Wt::WTime{ 12, 58, 29 } }));
|
||||||
|
EXPECT_EQ(dbPlayList->getFileSize(), 1234);
|
||||||
|
EXPECT_EQ(dbPlayList->getName(), "My playlist");
|
||||||
|
EXPECT_EQ(dbPlayList->getTrackList(), trackList.get());
|
||||||
|
const auto files{ dbPlayList->getFiles() };
|
||||||
|
ASSERT_EQ(files.size(), 2);
|
||||||
|
EXPECT_EQ(files[0], "/foo/foo.mp3");
|
||||||
|
EXPECT_EQ(files[1], "/foo/bar.mp3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, PlayListFile_deleteTrackList)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
ScopedPlayListFile playlist{ session, "/tmp/foo.m3u" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
TrackList::pointer trackList{ session.create<TrackList>("MyTrackList", TrackListType::PlayList) };
|
||||||
|
playlist.get().modify()->setTrackList(trackList);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
ASSERT_EQ(TrackList::getCount(session), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
ASSERT_EQ(PlayListFile::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
ASSERT_EQ(TrackList::getCount(session), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::db::tests
|
||||||
@@ -25,13 +25,12 @@ namespace lms::db::tests
|
|||||||
{
|
{
|
||||||
TEST_F(DatabaseFixture, SingleTrackList)
|
TEST_F(DatabaseFixture, SingleTrackList)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
ScopedTrackList trackList{ session, "MytrackList", TrackListType::PlayList };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
@@ -41,9 +40,8 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedTrack track{ session };
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -76,9 +74,8 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, TrackList_SortMethod)
|
TEST_F(DatabaseFixture, TrackList_SortMethod)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedTrack track{ session };
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -125,8 +122,7 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList{ session, "MytrackList", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
std::list<ScopedTrack> tracks;
|
std::list<ScopedTrack> tracks;
|
||||||
|
|
||||||
for (std::size_t i{}; i < 10; ++i)
|
for (std::size_t i{}; i < 10; ++i)
|
||||||
@@ -153,9 +149,8 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, TrackList_MediaLibrary)
|
TEST_F(DatabaseFixture, TrackList_MediaLibrary)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList1{ session, "MytrackList1", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList1{ session, "MytrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
ScopedTrackList trackList2{ session, "MytrackList2", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList2{ session, "MytrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedTrack track1{ session };
|
ScopedTrack track1{ session };
|
||||||
ScopedTrack track2{ session };
|
ScopedTrack track2{ session };
|
||||||
ScopedMediaLibrary library{ session, "MyLibrary", "/root" };
|
ScopedMediaLibrary library{ session, "MyLibrary", "/root" };
|
||||||
@@ -191,9 +186,8 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
|
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
ScopedTrack track{ session };
|
ScopedTrack track{ session };
|
||||||
@@ -223,8 +217,7 @@ namespace lms::db::tests
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackList_getEntries)
|
TEST_F(DatabaseFixture, SingleTrackList_getEntries)
|
||||||
{
|
{
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::PlayList };
|
||||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
|
||||||
ScopedTrack track1{ session };
|
ScopedTrack track1{ session };
|
||||||
ScopedTrack track2{ session };
|
ScopedTrack track2{ session };
|
||||||
|
|
||||||
@@ -260,4 +253,30 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(entries.results[0]->getTrack()->getId(), track2.getId());
|
EXPECT_EQ(entries.results[0]->getTrack()->getId(), track2.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, SingleTrackList_visitEntries)
|
||||||
|
{
|
||||||
|
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::PlayList };
|
||||||
|
ScopedTrack track1{ session };
|
||||||
|
ScopedTrack track2{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||||
|
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
std::vector<TrackId> visitedTrackIds;
|
||||||
|
TrackListEntry::find(session, TrackListEntry::FindParameters{}.setTrackList(trackList.getId()), [&](const TrackListEntry::pointer& entry) {
|
||||||
|
visitedTrackIds.push_back(entry->getTrackId());
|
||||||
|
});
|
||||||
|
ASSERT_EQ(visitedTrackIds.size(), 2);
|
||||||
|
EXPECT_EQ(visitedTrackIds[0], track1.getId());
|
||||||
|
EXPECT_EQ(visitedTrackIds[1], track2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
add_library(lmsimage SHARED
|
add_library(lmsimage SHARED
|
||||||
impl/SvgImage.cpp
|
impl/EncodedImage.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmsimage INTERFACE
|
target_include_directories(lmsimage INTERFACE
|
||||||
@@ -26,7 +26,6 @@ if (${LMS_IMAGE_BACKEND} STREQUAL "stb")
|
|||||||
|
|
||||||
target_sources(lmsimage PRIVATE
|
target_sources(lmsimage PRIVATE
|
||||||
impl/stb/Image.cpp
|
impl/stb/Image.cpp
|
||||||
impl/stb/JPEGImage.cpp
|
|
||||||
impl/stb/RawImage.cpp
|
impl/stb/RawImage.cpp
|
||||||
)
|
)
|
||||||
target_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}")
|
target_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}")
|
||||||
@@ -38,7 +37,6 @@ elseif (${LMS_IMAGE_BACKEND} STREQUAL "graphicsmagick")
|
|||||||
|
|
||||||
target_sources(lmsimage PRIVATE
|
target_sources(lmsimage PRIVATE
|
||||||
impl/graphicsmagick/Image.cpp
|
impl/graphicsmagick/Image.cpp
|
||||||
impl/graphicsmagick/JPEGImage.cpp
|
|
||||||
impl/graphicsmagick/RawImage.cpp
|
impl/graphicsmagick/RawImage.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(lmsimage PRIVATE PkgConfig::GraphicsMagick++)
|
target_link_libraries(lmsimage PRIVATE PkgConfig::GraphicsMagick++)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "EncodedImage.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
|
||||||
|
namespace lms::image
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string_view extensionToMimeType(const std::filesystem::path& extension)
|
||||||
|
{
|
||||||
|
static const std::unordered_map<std::string_view, std::string_view> mimeTypesByExtension{
|
||||||
|
{ ".bmp", "image/bmp" },
|
||||||
|
{ ".gif", "image/gif" },
|
||||||
|
{ ".jpeg", "image/jpeg" },
|
||||||
|
{ ".jpg", "image/jpeg" },
|
||||||
|
{ ".png", "image/png" },
|
||||||
|
{ ".ppm", "image/x-portable-pixmap" },
|
||||||
|
{ ".svg", "image/svg+xml" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto it{ mimeTypesByExtension.find(core::stringUtils::stringToLower(extension.c_str())) };
|
||||||
|
if (it == std::cend(mimeTypesByExtension))
|
||||||
|
throw Exception{ "Unhandled image extension '" + extension.string() + "'" };
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::byte> fileToBuffer(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "ReadFile");
|
||||||
|
|
||||||
|
std::ifstream ifs{ p.string(), std::ios::binary };
|
||||||
|
if (!ifs.is_open())
|
||||||
|
throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" };
|
||||||
|
|
||||||
|
std::vector<std::byte> data;
|
||||||
|
// read file content
|
||||||
|
ifs.seekg(0, std::ios::end);
|
||||||
|
std::streamsize size = ifs.tellg();
|
||||||
|
if (size < 0)
|
||||||
|
throw Exception{ "Cannot determine file size for '" + p.string() + "'" };
|
||||||
|
|
||||||
|
ifs.seekg(0, std::ios::beg);
|
||||||
|
data.resize(size);
|
||||||
|
if (!ifs.read(reinterpret_cast<char*>(data.data()), size))
|
||||||
|
throw Exception{ "Cannot read file content for '" + p.string() + "'" };
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> readImage(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
return std::make_unique<EncodedImage>(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> readImage(std::span<const std::byte> encodedData, std::string_view mimeType)
|
||||||
|
{
|
||||||
|
return std::make_unique<EncodedImage>(encodedData, mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
EncodedImage::EncodedImage(std::vector<std::byte>&& data, std::string_view mimeType)
|
||||||
|
: _data{ std::move(data) }
|
||||||
|
, _mimeType(mimeType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EncodedImage::EncodedImage(std::span<const std::byte> data, std::string_view mimeType)
|
||||||
|
: _data(std::cbegin(data), std::cend(data))
|
||||||
|
, _mimeType(mimeType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EncodedImage::EncodedImage(const std::filesystem::path& p)
|
||||||
|
: EncodedImage::EncodedImage{ fileToBuffer(p), extensionToMimeType(p.extension()) }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
} // namespace lms::image
|
||||||
@@ -20,24 +20,27 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
{
|
{
|
||||||
class SvgImage : public IEncodedImage
|
class EncodedImage : public IEncodedImage
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SvgImage(std::vector<std::byte>&& data)
|
EncodedImage(const std::filesystem::path& path);
|
||||||
: _data{ std::move(data) } {}
|
EncodedImage(std::vector<std::byte>&& data, std::string_view mimeType);
|
||||||
|
EncodedImage(std::span<const std::byte> data, std::string_view mimeType);
|
||||||
|
~EncodedImage() override = default;
|
||||||
|
EncodedImage(const EncodedImage&) = delete;
|
||||||
|
EncodedImage& operator=(const EncodedImage&) = delete;
|
||||||
|
|
||||||
const std::byte* getData() const { return &_data.front(); }
|
std::span<const std::byte> getData() const override { return _data; }
|
||||||
std::size_t getDataSize() const { return _data.size(); }
|
std::string_view getMimeType() const override { return _mimeType; }
|
||||||
std::string_view getMimeType() const { return "image/svg+xml"; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<std::byte> _data;
|
const std::vector<std::byte> _data;
|
||||||
|
const std::string _mimeType;
|
||||||
};
|
};
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "SvgImage.hpp"
|
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "image/Exception.hpp"
|
|
||||||
|
|
||||||
namespace lms::image
|
|
||||||
{
|
|
||||||
std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& p)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "ReadSVG");
|
|
||||||
|
|
||||||
if (p.extension() != ".svg")
|
|
||||||
throw Exception{ "Unexpected file extension: '" + p.extension().string() + "', expected .svg" };
|
|
||||||
|
|
||||||
std::ifstream ifs{ p.string(), std::ios::binary };
|
|
||||||
if (!ifs.is_open())
|
|
||||||
throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" };
|
|
||||||
|
|
||||||
std::vector<std::byte> data;
|
|
||||||
// read file content
|
|
||||||
ifs.seekg(0, std::ios::end);
|
|
||||||
std::streamsize size = ifs.tellg();
|
|
||||||
if (size < 0)
|
|
||||||
throw Exception{ "Cannot determine file size for '" + p.string() + "'" };
|
|
||||||
|
|
||||||
ifs.seekg(0, std::ios::beg);
|
|
||||||
data.resize(size);
|
|
||||||
if (!ifs.read(reinterpret_cast<char*>(data.data()), size))
|
|
||||||
throw Exception{ "Cannot read file content for '" + p.string() + "'" };
|
|
||||||
|
|
||||||
return std::make_unique<SvgImage>(std::move(data));
|
|
||||||
}
|
|
||||||
} // namespace lms::image
|
|
||||||
@@ -19,26 +19,17 @@
|
|||||||
|
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
#include <array>
|
#include <memory>
|
||||||
|
|
||||||
#include "RawImage.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
|
||||||
|
#include "EncodedImage.hpp"
|
||||||
|
#include "RawImage.hpp"
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
{
|
{
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
|
|
||||||
return std::make_unique<GraphicsMagick::RawImage>(encodedData, encodedDataSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
|
|
||||||
return std::make_unique<GraphicsMagick::RawImage>(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void init(const std::filesystem::path& path)
|
void init(const std::filesystem::path& path)
|
||||||
{
|
{
|
||||||
Magick::InitializeMagick(path.string().c_str());
|
Magick::InitializeMagick(path.string().c_str());
|
||||||
@@ -61,4 +52,39 @@ namespace lms::image
|
|||||||
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
|
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
|
||||||
return fileExtensions;
|
return fileExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
|
||||||
|
return std::make_unique<GraphicsMagick::RawImage>(encodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
|
||||||
|
return std::make_unique<GraphicsMagick::RawImage>(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> encodeToJPEG(const IRawImage& rawImage, unsigned quality)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Magick::Image image{ static_cast<const GraphicsMagick::RawImage&>(rawImage).getMagickImage() };
|
||||||
|
image.magick("JPEG");
|
||||||
|
image.quality(quality);
|
||||||
|
|
||||||
|
Magick::Blob blob;
|
||||||
|
image.write(&blob);
|
||||||
|
|
||||||
|
return std::make_unique<EncodedImage>(std::span{ static_cast<const std::byte*>(blob.data()), blob.length() }, "image/jpeg");
|
||||||
|
}
|
||||||
|
catch (Magick::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||||
|
throw Exception{ std::string{ "Magick read error: " } + e.what() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 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 "JPEGImage.hpp"
|
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "image/Exception.hpp"
|
|
||||||
|
|
||||||
#include "RawImage.hpp"
|
|
||||||
|
|
||||||
namespace lms::image::GraphicsMagick
|
|
||||||
{
|
|
||||||
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Magick::Image image{ rawImage.getMagickImage() };
|
|
||||||
image.magick("JPEG");
|
|
||||||
image.quality(quality);
|
|
||||||
image.write(&_blob);
|
|
||||||
}
|
|
||||||
catch (Magick::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
|
||||||
throw Exception{ std::string{ "Magick read error: " } + e.what() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::byte* JPEGImage::getData() const
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::byte*>(_blob.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t JPEGImage::getDataSize() const
|
|
||||||
{
|
|
||||||
return _blob.length();
|
|
||||||
}
|
|
||||||
} // namespace lms::image::GraphicsMagick
|
|
||||||
@@ -19,24 +19,19 @@
|
|||||||
|
|
||||||
#include "RawImage.hpp"
|
#include "RawImage.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
|
|
||||||
#include <magick/resource.h>
|
#include <magick/resource.h>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
|
|
||||||
#include "JPEGImage.hpp"
|
|
||||||
|
|
||||||
namespace lms::image::GraphicsMagick
|
namespace lms::image::GraphicsMagick
|
||||||
{
|
{
|
||||||
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
RawImage::RawImage(std::span<const std::byte> encodedData)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Magick::Blob blob{ encodedData, encodedDataSize };
|
Magick::Blob blob{ encodedData.data(), encodedData.size() };
|
||||||
_image.read(blob);
|
_image.read(blob);
|
||||||
}
|
}
|
||||||
catch (Magick::WarningCoder& e)
|
catch (Magick::WarningCoder& e)
|
||||||
@@ -102,14 +97,8 @@ namespace lms::image::GraphicsMagick
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> RawImage::encodeToJPEG(unsigned quality) const
|
|
||||||
{
|
|
||||||
return std::make_unique<JPEGImage>(*this, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
Magick::Image RawImage::getMagickImage() const
|
Magick::Image RawImage::getMagickImage() const
|
||||||
{
|
{
|
||||||
return _image;
|
return _image;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::image::GraphicsMagick
|
} // namespace lms::image::GraphicsMagick
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include <Magick++.h>
|
#include <Magick++.h>
|
||||||
|
|
||||||
#include "image/IEncodedImage.hpp"
|
|
||||||
#include "image/IRawImage.hpp"
|
#include "image/IRawImage.hpp"
|
||||||
|
|
||||||
namespace lms::image::GraphicsMagick
|
namespace lms::image::GraphicsMagick
|
||||||
@@ -32,19 +32,17 @@ namespace lms::image::GraphicsMagick
|
|||||||
class RawImage : public IRawImage
|
class RawImage : public IRawImage
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
|
RawImage(std::span<const std::byte> encodedData);
|
||||||
RawImage(const std::filesystem::path& path);
|
RawImage(const std::filesystem::path& path);
|
||||||
|
|
||||||
ImageSize getWidth() const override;
|
ImageSize getWidth() const override;
|
||||||
ImageSize getHeight() const override;
|
ImageSize getHeight() const override;
|
||||||
|
|
||||||
void resize(ImageSize width) override;
|
void resize(ImageSize width) override;
|
||||||
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class JPEGImage;
|
|
||||||
Magick::Image getMagickImage() const;
|
Magick::Image getMagickImage() const;
|
||||||
|
|
||||||
|
private:
|
||||||
Magick::Image _image;
|
Magick::Image _image;
|
||||||
};
|
};
|
||||||
} // namespace lms::image::GraphicsMagick
|
} // namespace lms::image::GraphicsMagick
|
||||||
|
|||||||
@@ -21,24 +21,18 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
#include "RawImage.hpp"
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include <stb_image_write.h>
|
||||||
|
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
|
|
||||||
|
#include "EncodedImage.hpp"
|
||||||
|
#include "RawImage.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
{
|
{
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
void init(const std::filesystem::path& /*unused*/)
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
|
|
||||||
return std::make_unique<STB::RawImage>(encodedData, encodedDataSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
|
|
||||||
return std::make_unique<STB::RawImage>(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void init(const std::filesystem::path&)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,4 +41,35 @@ namespace lms::image
|
|||||||
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
|
static const std::array<std::filesystem::path, 4> fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" };
|
||||||
return fileExtensions;
|
return fileExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
|
||||||
|
return std::make_unique<STB::RawImage>(encodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
|
||||||
|
return std::make_unique<STB::RawImage>(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> encodeToJPEG(const IRawImage& rawImage, unsigned quality)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
|
||||||
|
|
||||||
|
std::vector<std::byte> encodedData;
|
||||||
|
|
||||||
|
auto writeCb{ [](void* ctx, void* writeData, int writeSize) {
|
||||||
|
auto& output{ *reinterpret_cast<std::vector<std::byte>*>(ctx) };
|
||||||
|
const std::size_t currentOutputSize{ output.size() };
|
||||||
|
output.resize(currentOutputSize + writeSize);
|
||||||
|
std::copy(reinterpret_cast<const std::byte*>(writeData), reinterpret_cast<const std::byte*>(writeData) + writeSize, output.data() + currentOutputSize);
|
||||||
|
} };
|
||||||
|
|
||||||
|
if (::stbi_write_jpg_to_func(writeCb, &encodedData, rawImage.getWidth(), rawImage.getHeight(), 3, static_cast<const STB::RawImage&>(rawImage).getData(), quality) == 0)
|
||||||
|
throw Exception{ "Failed to export in jpeg format!" };
|
||||||
|
|
||||||
|
return std::make_unique<EncodedImage>(std::move(encodedData), "image/jpeg");
|
||||||
|
}
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 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 "JPEGImage.hpp"
|
|
||||||
|
|
||||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
||||||
#include <stb_image_write.h>
|
|
||||||
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "image/Exception.hpp"
|
|
||||||
|
|
||||||
#include "RawImage.hpp"
|
|
||||||
|
|
||||||
namespace lms::image::STB
|
|
||||||
{
|
|
||||||
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
|
|
||||||
|
|
||||||
auto writeCb{ [](void* ctx, void* writeData, int writeSize) {
|
|
||||||
auto& output{ *reinterpret_cast<std::vector<std::byte>*>(ctx) };
|
|
||||||
const std::size_t currentOutputSize{ output.size() };
|
|
||||||
output.resize(currentOutputSize + writeSize);
|
|
||||||
std::copy(reinterpret_cast<const std::byte*>(writeData), reinterpret_cast<const std::byte*>(writeData) + writeSize, output.data() + currentOutputSize);
|
|
||||||
} };
|
|
||||||
|
|
||||||
if (stbi_write_jpg_to_func(writeCb, &_data, rawImage.getWidth(), rawImage.getHeight(), 3, rawImage.getData(), quality) == 0)
|
|
||||||
{
|
|
||||||
_data.clear();
|
|
||||||
throw Exception{ "Failed to export in jpeg format!" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::byte*
|
|
||||||
JPEGImage::getData() const
|
|
||||||
{
|
|
||||||
if (_data.empty())
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
return &_data.front();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
|
||||||
JPEGImage::getDataSize() const
|
|
||||||
{
|
|
||||||
return _data.size();
|
|
||||||
}
|
|
||||||
} // namespace lms::image::STB
|
|
||||||
@@ -39,21 +39,19 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
|
|
||||||
#include "JPEGImage.hpp"
|
|
||||||
|
|
||||||
namespace lms::image::STB
|
namespace lms::image::STB
|
||||||
{
|
{
|
||||||
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
RawImage::RawImage(std::span<const std::byte> encodedData)
|
||||||
{
|
{
|
||||||
int n;
|
int n{};
|
||||||
_data = UniquePtrFree{ ::stbi_load_from_memory(reinterpret_cast<const stbi_uc*>(encodedData), encodedDataSize, &_width, &_height, &n, 3), std::free };
|
_data = UniquePtrFree{ ::stbi_load_from_memory(reinterpret_cast<const stbi_uc*>(encodedData.data()), encodedData.size(), &_width, &_height, &n, 3), std::free };
|
||||||
if (!_data)
|
if (!_data)
|
||||||
throw Exception{ "Cannot load image from memory: " + std::string{ ::stbi_failure_reason() } };
|
throw Exception{ "Cannot load image from memory: " + std::string{ ::stbi_failure_reason() } };
|
||||||
}
|
}
|
||||||
|
|
||||||
RawImage::RawImage(const std::filesystem::path& p)
|
RawImage::RawImage(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
int n;
|
int n{};
|
||||||
_data = UniquePtrFree{ stbi_load(p.string().c_str(), &_width, &_height, &n, 3), std::free };
|
_data = UniquePtrFree{ stbi_load(p.string().c_str(), &_width, &_height, &n, 3), std::free };
|
||||||
if (!_data)
|
if (!_data)
|
||||||
throw Exception{ "Cannot load image from file: " + std::string{ ::stbi_failure_reason() } };
|
throw Exception{ "Cannot load image from file: " + std::string{ ::stbi_failure_reason() } };
|
||||||
@@ -63,7 +61,7 @@ namespace lms::image::STB
|
|||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Image", "Resize");
|
LMS_SCOPED_TRACE_DETAILED("Image", "Resize");
|
||||||
|
|
||||||
size_t height;
|
size_t height{};
|
||||||
if (_width == _height)
|
if (_width == _height)
|
||||||
{
|
{
|
||||||
height = width;
|
height = width;
|
||||||
@@ -78,7 +76,7 @@ namespace lms::image::STB
|
|||||||
width = (size_t)((float)height / _height * _width);
|
width = (size_t)((float)height / _height * _width);
|
||||||
}
|
}
|
||||||
|
|
||||||
UniquePtrFree resizedData{ reinterpret_cast<unsigned char*>(malloc(width * height * 3)), std::free };
|
UniquePtrFree resizedData{ static_cast<unsigned char*>(malloc(width * height * 3)), std::free };
|
||||||
if (!resizedData)
|
if (!resizedData)
|
||||||
throw Exception{ "Cannot allocate memory for resized image!" };
|
throw Exception{ "Cannot allocate memory for resized image!" };
|
||||||
|
|
||||||
@@ -104,11 +102,6 @@ namespace lms::image::STB
|
|||||||
_width = width;
|
_width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> RawImage::encodeToJPEG(unsigned quality) const
|
|
||||||
{
|
|
||||||
return std::make_unique<JPEGImage>(*this, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageSize RawImage::getWidth() const
|
ImageSize RawImage::getWidth() const
|
||||||
{
|
{
|
||||||
return _width;
|
return _width;
|
||||||
@@ -121,9 +114,7 @@ namespace lms::image::STB
|
|||||||
|
|
||||||
const std::byte* RawImage::getData() const
|
const std::byte* RawImage::getData() const
|
||||||
{
|
{
|
||||||
if (!_data)
|
assert(_data);
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
return reinterpret_cast<const std::byte*>(_data.get());
|
return reinterpret_cast<const std::byte*>(_data.get());
|
||||||
}
|
}
|
||||||
} // namespace lms::image::STB
|
} // namespace lms::image::STB
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include "image/IEncodedImage.hpp"
|
|
||||||
#include "image/IRawImage.hpp"
|
#include "image/IRawImage.hpp"
|
||||||
|
|
||||||
namespace lms::image::STB
|
namespace lms::image::STB
|
||||||
@@ -30,20 +30,23 @@ namespace lms::image::STB
|
|||||||
class RawImage : public IRawImage
|
class RawImage : public IRawImage
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
|
RawImage(std::span<const std::byte> encodedData);
|
||||||
RawImage(const std::filesystem::path& path);
|
RawImage(const std::filesystem::path& path);
|
||||||
|
|
||||||
|
~RawImage() override = default;
|
||||||
|
RawImage(const RawImage&) = delete;
|
||||||
|
RawImage& operator=(const RawImage&) = delete;
|
||||||
|
|
||||||
ImageSize getWidth() const override;
|
ImageSize getWidth() const override;
|
||||||
ImageSize getHeight() const override;
|
ImageSize getHeight() const override;
|
||||||
|
|
||||||
void resize(ImageSize width) override;
|
void resize(ImageSize width) override;
|
||||||
std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const override;
|
|
||||||
|
|
||||||
const std::byte* getData() const;
|
const std::byte* getData() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int _width;
|
int _width{};
|
||||||
int _height;
|
int _height{};
|
||||||
using UniquePtrFree = std::unique_ptr<unsigned char, decltype(&std::free)>;
|
using UniquePtrFree = std::unique_ptr<unsigned char, decltype(&std::free)>;
|
||||||
UniquePtrFree _data{ nullptr, std::free };
|
UniquePtrFree _data{ nullptr, std::free };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <span>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
@@ -31,8 +32,7 @@ namespace lms::image
|
|||||||
public:
|
public:
|
||||||
virtual ~IEncodedImage() = default;
|
virtual ~IEncodedImage() = default;
|
||||||
|
|
||||||
virtual const std::byte* getData() const = 0;
|
virtual std::span<const std::byte> getData() const = 0;
|
||||||
virtual std::size_t getDataSize() const = 0;
|
|
||||||
virtual std::string_view getMimeType() const = 0;
|
virtual std::string_view getMimeType() const = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/Types.hpp"
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
{
|
{
|
||||||
@@ -32,6 +32,5 @@ namespace lms::image
|
|||||||
virtual ImageSize getHeight() const = 0;
|
virtual ImageSize getHeight() const = 0;
|
||||||
|
|
||||||
virtual void resize(ImageSize width) = 0;
|
virtual void resize(ImageSize width) = 0;
|
||||||
virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0;
|
|
||||||
};
|
};
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ namespace lms::image
|
|||||||
{
|
{
|
||||||
void init(const std::filesystem::path& path);
|
void init(const std::filesystem::path& path);
|
||||||
std::span<const std::filesystem::path> getSupportedFileExtensions();
|
std::span<const std::filesystem::path> getSupportedFileExtensions();
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize);
|
|
||||||
|
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData);
|
||||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
|
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
|
||||||
std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& path);
|
|
||||||
|
std::unique_ptr<IEncodedImage> readImage(std::span<const std::byte> encodedData, std::string_view mimeType);
|
||||||
|
std::unique_ptr<IEncodedImage> readImage(const std::filesystem::path& path);
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> encodeToJPEG(const IRawImage& rawImage, unsigned quality);
|
||||||
} // namespace lms::image
|
} // namespace lms::image
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cstddef>
|
||||||
|
|
||||||
|
namespace lms::image
|
||||||
|
{
|
||||||
|
using ImageSize = std::size_t;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ add_library(lmsmetadata SHARED
|
|||||||
impl/AvFormatTagReader.cpp
|
impl/AvFormatTagReader.cpp
|
||||||
impl/Lyrics.cpp
|
impl/Lyrics.cpp
|
||||||
impl/Parser.cpp
|
impl/Parser.cpp
|
||||||
|
impl/PlayList.cpp
|
||||||
impl/TagLibTagReader.cpp
|
impl/TagLibTagReader.cpp
|
||||||
impl/Utils.cpp
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,16 +19,12 @@
|
|||||||
|
|
||||||
#include "AvFormatTagReader.hpp"
|
#include "AvFormatTagReader.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
#include "av/IAudioFile.hpp"
|
||||||
|
#include "av/Types.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
#include "av/IAudioFile.hpp"
|
||||||
#include "metadata/IParser.hpp"
|
|
||||||
|
|
||||||
#include "ITagReader.hpp"
|
#include "ITagReader.hpp"
|
||||||
|
|
||||||
@@ -32,11 +31,11 @@ namespace lms::metadata
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
||||||
|
~AvFormatTagReader() override = default;
|
||||||
private:
|
|
||||||
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
||||||
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||||
|
|||||||
@@ -269,6 +269,32 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> Parser::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
// TODO: use backend capability to retrieve supported formats
|
||||||
|
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||||
|
".aac",
|
||||||
|
".alac",
|
||||||
|
".aif",
|
||||||
|
".aiff",
|
||||||
|
".ape",
|
||||||
|
".dsf",
|
||||||
|
".flac",
|
||||||
|
".m4a",
|
||||||
|
".m4b",
|
||||||
|
".mp3",
|
||||||
|
".mpc",
|
||||||
|
".oga",
|
||||||
|
".ogg",
|
||||||
|
".opus",
|
||||||
|
".shn",
|
||||||
|
".wav",
|
||||||
|
".wma",
|
||||||
|
".wv",
|
||||||
|
};
|
||||||
|
return fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Track> Parser::parse(const std::filesystem::path& p, bool debug)
|
std::unique_ptr<Track> Parser::parse(const std::filesystem::path& p, bool debug)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace lms::metadata
|
|||||||
std::unique_ptr<Track> parse(const ITagReader& reader);
|
std::unique_ptr<Track> parse(const ITagReader& reader);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
void setUserExtraTags(std::span<const std::string> extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); }
|
void setUserExtraTags(std::span<const std::string> extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); }
|
||||||
void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
|
void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
|
||||||
void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
|
void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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 "metadata/PlayList.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "core/String.hpp"
|
||||||
|
|
||||||
|
namespace lms::metadata
|
||||||
|
{
|
||||||
|
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
|
||||||
|
{
|
||||||
|
static const std::array<std::filesystem::path, 2> fileExtensions{ ".m3u", "m3u8" };
|
||||||
|
return fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct Comment
|
||||||
|
{
|
||||||
|
std::string_view directive;
|
||||||
|
std::string_view parameter;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<Comment> parseComment(std::string_view line)
|
||||||
|
{
|
||||||
|
if (line.empty() || line.front() != '#')
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
Comment comment;
|
||||||
|
const std::string_view::size_type parameterSeparator{ line.find(':') };
|
||||||
|
if (parameterSeparator == std::string_view::npos)
|
||||||
|
{
|
||||||
|
comment.directive = line;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
comment.directive = line.substr(0, parameterSeparator + 1);
|
||||||
|
comment.parameter = line.substr(parameterSeparator + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PlayList parsePlayList(std::istream& is)
|
||||||
|
{
|
||||||
|
PlayList playlist;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(is, line))
|
||||||
|
{
|
||||||
|
const std::string_view trimmedLine{ core::stringUtils::stringTrim(line) };
|
||||||
|
if (trimmedLine.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Don't enforce #EXTM3U as first line: be permissive
|
||||||
|
if (const std::optional<Comment> comment{ parseComment(trimmedLine) })
|
||||||
|
{
|
||||||
|
if (comment->directive == "#PLAYLIST:")
|
||||||
|
playlist.name = comment->parameter;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
|
||||||
|
// Consider an entry with a ':' is actually an url, as filenames are not supposed to have ':' on windows
|
||||||
|
if (trimmedLine.find(':') != std::string_view::npos)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const std::filesystem::path path{ std::cbegin(trimmedLine), std::cend(trimmedLine) };
|
||||||
|
playlist.files.emplace_back(path.lexically_normal());
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
} // namespace lms::metadata
|
||||||
@@ -34,6 +34,7 @@ namespace lms::metadata
|
|||||||
|
|
||||||
virtual std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
virtual std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
||||||
|
|
||||||
|
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||||
virtual void setUserExtraTags(std::span<const std::string> extraTags) = 0;
|
virtual void setUserExtraTags(std::span<const std::string> extraTags) = 0;
|
||||||
virtual void setArtistTagDelimiters(std::span<const std::string> delimiters) = 0;
|
virtual void setArtistTagDelimiters(std::span<const std::string> delimiters) = 0;
|
||||||
virtual void setDefaultTagDelimiters(std::span<const std::string> delimiters) = 0;
|
virtual void setDefaultTagDelimiters(std::span<const std::string> delimiters) = 0;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iosfwd>
|
#include <iosfwd>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iosfwd>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace lms::metadata
|
||||||
|
{
|
||||||
|
struct PlayList
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::vector<std::filesystem::path> files;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
|
||||||
|
PlayList parsePlayList(std::istream& is);
|
||||||
|
} // namespace lms::metadata
|
||||||
@@ -4,6 +4,7 @@ add_executable(test-metadata
|
|||||||
Lyrics.cpp
|
Lyrics.cpp
|
||||||
Metadata.cpp
|
Metadata.cpp
|
||||||
Parser.cpp
|
Parser.cpp
|
||||||
|
PlayList.cpp
|
||||||
Utils.cpp
|
Utils.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* 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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "metadata/PlayList.hpp"
|
||||||
|
|
||||||
|
namespace lms::metadata::tests
|
||||||
|
{
|
||||||
|
TEST(PlayList, basic)
|
||||||
|
{
|
||||||
|
std::istringstream is{ R"(#EXTM3U
|
||||||
|
#PLAYLIST:My super playlist
|
||||||
|
01-Foo.mp3
|
||||||
|
|
||||||
|
|
||||||
|
#EXTINF:263,Alice in Chains - Don't Follow
|
||||||
|
02-FooBar.mp3
|
||||||
|
#EXTALB:Album Title (2009)
|
||||||
|
03-Bar.mp3
|
||||||
|
/this is/a test with a long/path and some spaces/foo.mp3
|
||||||
|
and another one/with relative path/foo.mp3
|
||||||
|
one to be/../one to be/normalized/foo.mp3)" };
|
||||||
|
|
||||||
|
const PlayList playlist{ parsePlayList(is) };
|
||||||
|
|
||||||
|
EXPECT_EQ(playlist.name, "My super playlist");
|
||||||
|
ASSERT_EQ(playlist.files.size(), 6);
|
||||||
|
EXPECT_EQ(playlist.files[0], "01-Foo.mp3");
|
||||||
|
EXPECT_EQ(playlist.files[1], "02-FooBar.mp3");
|
||||||
|
EXPECT_EQ(playlist.files[2], "03-Bar.mp3");
|
||||||
|
EXPECT_EQ(playlist.files[3], "/this is/a test with a long/path and some spaces/foo.mp3");
|
||||||
|
EXPECT_EQ(playlist.files[4], "and another one/with relative path/foo.mp3");
|
||||||
|
EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3");
|
||||||
|
}
|
||||||
|
} // namespace lms::metadata::tests
|
||||||
@@ -19,20 +19,21 @@
|
|||||||
|
|
||||||
#include "ArtworkService.hpp"
|
#include "ArtworkService.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
#include "av/IAudioFile.hpp"
|
||||||
|
#include "av/Types.hpp"
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Path.hpp"
|
|
||||||
#include "core/Random.hpp"
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "core/Utils.hpp"
|
#include "core/Utils.hpp"
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
|
#include "image/IEncodedImage.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::cover
|
||||||
@@ -50,8 +51,6 @@ namespace lms::cover
|
|||||||
return std::make_unique<ArtworkService>(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath);
|
return std::make_unique<ArtworkService>(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
using namespace image;
|
|
||||||
|
|
||||||
ArtworkService::ArtworkService(db::Db& db,
|
ArtworkService::ArtworkService(db::Db& db,
|
||||||
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
||||||
const std::filesystem::path& defaultArtistImageSvgPath)
|
const std::filesystem::path& defaultArtistImageSvgPath)
|
||||||
@@ -63,42 +62,82 @@ namespace lms::cover
|
|||||||
LMS_LOG(COVER, INFO, "Default release cover path = '" << defaultReleaseCoverSvgPath.string() << "'");
|
LMS_LOG(COVER, INFO, "Default release cover path = '" << defaultReleaseCoverSvgPath.string() << "'");
|
||||||
LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
|
LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
|
||||||
|
|
||||||
_defaultReleaseCover = image::readSvgFile(defaultReleaseCoverSvgPath); // may throw
|
_defaultReleaseCover = image::readImage(defaultReleaseCoverSvgPath); // may throw
|
||||||
_defaultArtistImage = image::readSvgFile(defaultArtistImageSvgPath); // may throw
|
_defaultArtistImage = image::readImage(defaultArtistImageSvgPath); // may throw
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const
|
std::unique_ptr<image::IEncodedImage> ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, std::optional<image::ImageSize> width) const
|
||||||
{
|
{
|
||||||
std::unique_ptr<IEncodedImage> image;
|
struct CandidatePicture
|
||||||
|
{
|
||||||
|
av::Picture picture;
|
||||||
|
bool isFront{};
|
||||||
|
std::size_t index;
|
||||||
|
|
||||||
input.visitAttachedPictures([&](const av::Picture& picture) {
|
// > means is better candidate
|
||||||
if (image)
|
bool operator>(const CandidatePicture& other) const
|
||||||
return;
|
{
|
||||||
|
if (!isFront && other.isFront)
|
||||||
|
return false;
|
||||||
|
if (isFront && !other.isFront)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return index < other.index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto metadataHasFrontKeyword{ [](const av::IAudioFile::MetadataMap& metadata) {
|
||||||
|
return std::any_of(std::cbegin(metadata), std::cend(metadata), [](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, "front"); });
|
||||||
|
} };
|
||||||
|
|
||||||
|
std::vector<CandidatePicture> candidatePictures;
|
||||||
|
std::size_t pictureIndex{};
|
||||||
|
input.visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metadata) {
|
||||||
|
candidatePictures.emplace_back(picture, metadataHasFrontKeyword(metadata), pictureIndex++);
|
||||||
|
});
|
||||||
|
std::stable_sort(std::begin(candidatePictures), std::end(candidatePictures), std::greater<>());
|
||||||
|
|
||||||
|
std::unique_ptr<image::IEncodedImage> image;
|
||||||
|
for (const CandidatePicture& candidatePicture : candidatePictures)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
|
if (!width)
|
||||||
rawImage->resize(width);
|
{
|
||||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
image = image::readImage(candidatePicture.picture.data, candidatePicture.picture.mimeType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto rawImage{ image::decodeImage(candidatePicture.picture.data) };
|
||||||
|
rawImage->resize(*width);
|
||||||
|
image = image::encodeToJPEG(*rawImage, _jpegQuality);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (const image::Exception& e)
|
catch (const image::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> ArtworkService::getFromImageFile(const std::filesystem::path& p, ImageSize width) const
|
std::unique_ptr<image::IEncodedImage> ArtworkService::getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
|
||||||
{
|
{
|
||||||
std::unique_ptr<IEncodedImage> image;
|
std::unique_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
|
if (!width)
|
||||||
rawImage->resize(width);
|
{
|
||||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
image = image::readImage(p);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto rawImage{ image::decodeImage(p) };
|
||||||
|
rawImage->resize(*width);
|
||||||
|
image = image::encodeToJPEG(*rawImage, _jpegQuality);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (const image::Exception& e)
|
catch (const image::Exception& e)
|
||||||
{
|
{
|
||||||
@@ -108,17 +147,17 @@ namespace lms::cover
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> ArtworkService::getDefaultReleaseCover()
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseCover()
|
||||||
{
|
{
|
||||||
return _defaultReleaseCover;
|
return _defaultReleaseCover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> ArtworkService::getDefaultArtistImage()
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistImage()
|
||||||
{
|
{
|
||||||
return _defaultArtistImage;
|
return _defaultArtistImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ArtworkService::checkImageFile(const std::filesystem::path& filePath) const
|
bool ArtworkService::checkImageFile(const std::filesystem::path& filePath)
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
|
|
||||||
@@ -134,9 +173,9 @@ namespace lms::cover
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, ImageSize width) const
|
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
|
||||||
{
|
{
|
||||||
std::unique_ptr<IEncodedImage> image;
|
std::unique_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -150,81 +189,54 @@ namespace lms::cover
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, ImageSize width)
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
|
||||||
{
|
{
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
|
const ImageCache::EntryDesc cacheEntryDesc{ imageId, width };
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
||||||
if (cover)
|
if (cover)
|
||||||
return cover;
|
return cover;
|
||||||
|
|
||||||
|
std::filesystem::path imageFile;
|
||||||
{
|
{
|
||||||
db::Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
const db::Track::pointer track{ db::Track::find(session, trackId) };
|
const db::Image::pointer image{ db::Image::find(session, imageId) };
|
||||||
if (track && track->hasCover())
|
if (image)
|
||||||
cover = getTrackImage(track->getAbsoluteFilePath(), width);
|
imageFile = image->getAbsoluteFilePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cover = getFromImageFile(imageFile, width);
|
||||||
if (cover)
|
if (cover)
|
||||||
_cache.addImage(cacheEntryDesc, cover);
|
_cache.addImage(cacheEntryDesc, cover);
|
||||||
|
|
||||||
return cover;
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> ArtworkService::getReleaseCover(db::ReleaseId releaseId, ImageSize width)
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width)
|
||||||
{
|
{
|
||||||
using namespace db;
|
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
|
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
||||||
if (image)
|
if (cover)
|
||||||
return image;
|
return cover;
|
||||||
|
|
||||||
|
std::filesystem::path trackFile;
|
||||||
{
|
{
|
||||||
Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
const db::Release::pointer release{ db::Release::find(session, releaseId) };
|
const db::Track::pointer track{ db::Track::find(session, trackId) };
|
||||||
if (release)
|
if (track && track->hasCover())
|
||||||
{
|
trackFile = track->getAbsoluteFilePath();
|
||||||
if (const db::Image::pointer dbImage{ release->getImage() })
|
|
||||||
image = getFromImageFile(dbImage->getAbsoluteFilePath(), width);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image)
|
cover = getTrackImage(trackFile, width);
|
||||||
_cache.addImage(cacheEntryDesc, image);
|
if (cover)
|
||||||
|
_cache.addImage(cacheEntryDesc, cover);
|
||||||
|
|
||||||
return image;
|
return cover;
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> ArtworkService::getArtistImage(db::ArtistId artistId, ImageSize width)
|
|
||||||
{
|
|
||||||
using namespace db;
|
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ artistId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> artistImage{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (artistImage)
|
|
||||||
return artistImage;
|
|
||||||
|
|
||||||
{
|
|
||||||
Session& session{ _db.getTLSSession() };
|
|
||||||
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
if (const Artist::pointer artist{ Artist::find(session, artistId) })
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ artist->getImage() })
|
|
||||||
artistImage = getFromImageFile(image->getAbsoluteFilePath(), width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (artistImage)
|
|
||||||
_cache.addImage(cacheEntryDesc, artistImage);
|
|
||||||
|
|
||||||
return artistImage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArtworkService::flushCache()
|
void ArtworkService::flushCache()
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
#include "services/artwork/IArtworkService.hpp"
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
@@ -43,28 +42,25 @@ namespace lms::cover
|
|||||||
class ArtworkService : public IArtworkService
|
class ArtworkService : public IArtworkService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtworkService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
ArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
|
~ArtworkService() override = default;
|
||||||
private:
|
|
||||||
ArtworkService(const ArtworkService&) = delete;
|
ArtworkService(const ArtworkService&) = delete;
|
||||||
ArtworkService& operator=(const ArtworkService&) = delete;
|
ArtworkService& operator=(const ArtworkService&) = delete;
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, image::ImageSize width) override;
|
private:
|
||||||
std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, image::ImageSize width) override;
|
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
|
||||||
std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, image::ImageSize width) override;
|
std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width) override;
|
||||||
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
|
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
|
||||||
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
|
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
|
||||||
|
|
||||||
void flushCache() override;
|
void flushCache() override;
|
||||||
void setJpegQuality(unsigned quality) override;
|
void setJpegQuality(unsigned quality) override;
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> getTrackImage(db::Session& dbSession, db::TrackId trackId, image::ImageSize width, bool allowReleaseFallback);
|
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, std::optional<image::ImageSize> width) const;
|
||||||
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, image::ImageSize width) const;
|
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
|
||||||
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, image::ImageSize width) const;
|
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::optional<image::ImageSize> width) const;
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, image::ImageSize width) const;
|
static bool checkImageFile(const std::filesystem::path& filePath);
|
||||||
|
|
||||||
bool checkImageFile(const std::filesystem::path& filePath) const;
|
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
|
||||||
|
|||||||
@@ -33,21 +33,29 @@ namespace lms::cover
|
|||||||
|
|
||||||
void ImageCache::addImage(const EntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image)
|
void ImageCache::addImage(const EntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image)
|
||||||
{
|
{
|
||||||
|
// cache only resized files
|
||||||
|
if (!entryDesc.size)
|
||||||
|
return;
|
||||||
|
|
||||||
const std::unique_lock lock{ _mutex };
|
const std::unique_lock lock{ _mutex };
|
||||||
|
|
||||||
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
|
while (_cacheSize + image->getData().size() > _maxCacheSize && !_cache.empty())
|
||||||
{
|
{
|
||||||
auto itRandom{ core::random::pickRandom(_cache) };
|
auto itRandom{ core::random::pickRandom(_cache) };
|
||||||
_cacheSize -= itRandom->second->getDataSize();
|
_cacheSize -= itRandom->second->getData().size();
|
||||||
_cache.erase(itRandom);
|
_cache.erase(itRandom);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cacheSize += image->getDataSize();
|
_cacheSize += image->getData().size();
|
||||||
_cache[entryDesc] = image;
|
_cache[entryDesc] = image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> ImageCache::getImage(const EntryDesc& entryDesc) const
|
std::shared_ptr<image::IEncodedImage> ImageCache::getImage(const EntryDesc& entryDesc) const
|
||||||
{
|
{
|
||||||
|
// cache only resized files
|
||||||
|
if (!entryDesc.size)
|
||||||
|
return {};
|
||||||
|
|
||||||
const std::shared_lock lock{ _mutex };
|
const std::shared_lock lock{ _mutex };
|
||||||
|
|
||||||
const auto it{ _cache.find(entryDesc) };
|
const auto it{ _cache.find(entryDesc) };
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <optional>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ImageId.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ namespace lms::cover
|
|||||||
|
|
||||||
struct EntryDesc
|
struct EntryDesc
|
||||||
{
|
{
|
||||||
using VariantType = std::variant<db::ArtistId, db::ReleaseId, db::TrackId>;
|
using VariantType = std::variant<db::TrackId, db::ImageId>;
|
||||||
VariantType id;
|
VariantType id;
|
||||||
std::size_t size;
|
std::optional<std::size_t> size;
|
||||||
|
|
||||||
bool operator==(const EntryDesc& other) const = default;
|
bool operator==(const EntryDesc& other) const = default;
|
||||||
};
|
};
|
||||||
@@ -60,7 +60,8 @@ namespace lms::cover
|
|||||||
{
|
{
|
||||||
std::size_t operator()(const EntryDesc& entry) const
|
std::size_t operator()(const EntryDesc& entry) const
|
||||||
{
|
{
|
||||||
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(entry.size);
|
assert(entry.size); // should not cache unresized images
|
||||||
|
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ImageId.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
@@ -39,13 +39,10 @@ namespace lms::cover
|
|||||||
public:
|
public:
|
||||||
virtual ~IArtworkService() = default;
|
virtual ~IArtworkService() = default;
|
||||||
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, image::ImageSize width) = 0;
|
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;
|
||||||
|
|
||||||
// no logic to fallback to release here
|
// no logic to fallback to release here
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, image::ImageSize width) = 0;
|
virtual std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width) = 0;
|
||||||
|
|
||||||
// no logic to fallback to track here
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, image::ImageSize width) = 0;
|
|
||||||
|
|
||||||
// Svg images dont have image "size"
|
// Svg images dont have image "size"
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
|
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
|
||||||
@@ -56,6 +53,6 @@ namespace lms::cover
|
|||||||
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
|
|
||||||
} // namespace lms::cover
|
} // namespace lms::cover
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ namespace lms::auth
|
|||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
AuthServiceBase(db::Db& db);
|
AuthServiceBase(db::Db& db);
|
||||||
|
~AuthServiceBase() = default;
|
||||||
|
AuthServiceBase(const AuthServiceBase&) = delete;
|
||||||
|
AuthServiceBase& operator=(const AuthServiceBase&) = delete;
|
||||||
|
|
||||||
db::UserId getOrCreateUser(std::string_view loginName);
|
db::UserId getOrCreateUser(std::string_view loginName);
|
||||||
void onUserAuthenticated(db::UserId userId);
|
void onUserAuthenticated(db::UserId userId);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include <Wt/Auth/HashFunction.h>
|
#include <Wt/Auth/HashFunction.h>
|
||||||
#include <Wt/WRandom.h>
|
#include <Wt/WRandom.h>
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/AuthToken.hpp"
|
#include "database/AuthToken.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
@@ -126,7 +125,7 @@ namespace lms::auth
|
|||||||
std::shared_lock lock{ _mutex };
|
std::shared_lock lock{ _mutex };
|
||||||
|
|
||||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
|
return AuthTokenProcessResult{ .state = AuthTokenProcessResult::State::Throttled, .authTokenInfo = std::nullopt };
|
||||||
}
|
}
|
||||||
|
|
||||||
auto res{ processAuthToken(domain, tokenValue) };
|
auto res{ processAuthToken(domain, tokenValue) };
|
||||||
@@ -134,17 +133,17 @@ namespace lms::auth
|
|||||||
std::unique_lock lock{ _mutex };
|
std::unique_lock lock{ _mutex };
|
||||||
|
|
||||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
|
return AuthTokenProcessResult{ .state = AuthTokenProcessResult::State::Throttled, .authTokenInfo = std::nullopt };
|
||||||
|
|
||||||
if (!res)
|
if (!res)
|
||||||
{
|
{
|
||||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Denied };
|
return AuthTokenProcessResult{ .state = AuthTokenProcessResult::State::Denied, .authTokenInfo = std::nullopt };
|
||||||
}
|
}
|
||||||
|
|
||||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||||
onUserAuthenticated(res->userId);
|
onUserAuthenticated(res->userId);
|
||||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, res };
|
return AuthTokenProcessResult{ .state = AuthTokenProcessResult::State::Granted, .authTokenInfo = res };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace lms::auth
|
|||||||
public:
|
public:
|
||||||
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
|
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
|
||||||
|
|
||||||
|
~AuthTokenService() override = default;
|
||||||
AuthTokenService(const AuthTokenService&) = delete;
|
AuthTokenService(const AuthTokenService&) = delete;
|
||||||
AuthTokenService& operator=(const AuthTokenService&) = delete;
|
AuthTokenService& operator=(const AuthTokenService&) = delete;
|
||||||
AuthTokenService(AuthTokenService&&) = delete;
|
AuthTokenService(AuthTokenService&&) = delete;
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
{
|
{
|
||||||
std::unique_ptr<IEnvService>
|
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, db::Db& db)
|
||||||
createEnvService(std::string_view backendName, db::Db& db)
|
|
||||||
{
|
{
|
||||||
if (backendName == "http-headers")
|
if (backendName == "http-headers")
|
||||||
return std::make_unique<HttpHeadersEnvService>(db);
|
return std::make_unique<HttpHeadersEnvService>(db);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
|
||||||
#include "core/NetAddress.hpp"
|
#include "core/NetAddress.hpp"
|
||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
@@ -35,6 +34,10 @@ namespace lms::auth
|
|||||||
LoginThrottler(std::size_t maxEntries)
|
LoginThrottler(std::size_t maxEntries)
|
||||||
: _maxEntries{ maxEntries } {}
|
: _maxEntries{ maxEntries } {}
|
||||||
|
|
||||||
|
~LoginThrottler() = default;
|
||||||
|
LoginThrottler(const LoginThrottler&) = delete;
|
||||||
|
LoginThrottler& operator=(const LoginThrottler&) = delete;
|
||||||
|
|
||||||
// user must lock these calls to avoid races
|
// user must lock these calls to avoid races
|
||||||
bool isClientThrottled(const boost::asio::ip::address& address) const;
|
bool isClientThrottled(const boost::asio::ip::address& address) const;
|
||||||
void onBadClientAttempt(const boost::asio::ip::address& address);
|
void onBadClientAttempt(const boost::asio::ip::address& address);
|
||||||
|
|||||||
@@ -27,10 +27,8 @@
|
|||||||
#include "pam/PAMPasswordService.hpp"
|
#include "pam/PAMPasswordService.hpp"
|
||||||
#endif // LMS_SUPPORT_PAM
|
#endif // LMS_SUPPORT_PAM
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/User.hpp"
|
|
||||||
#include "services/auth/Types.hpp"
|
#include "services/auth/Types.hpp"
|
||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
@@ -63,7 +61,7 @@ namespace lms::auth
|
|||||||
std::shared_lock lock{ _mutex };
|
std::shared_lock lock{ _mutex };
|
||||||
|
|
||||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||||
return { CheckResult::State::Throttled };
|
return CheckResult{ .state = CheckResult::State::Throttled, .userId = {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool match{ checkUserPassword(loginName, password) };
|
const bool match{ checkUserPassword(loginName, password) };
|
||||||
@@ -71,7 +69,7 @@ namespace lms::auth
|
|||||||
std::unique_lock lock{ _mutex };
|
std::unique_lock lock{ _mutex };
|
||||||
|
|
||||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||||
return { CheckResult::State::Throttled };
|
return CheckResult{ .state = CheckResult::State::Throttled, .userId = {} };
|
||||||
|
|
||||||
if (match)
|
if (match)
|
||||||
{
|
{
|
||||||
@@ -79,13 +77,11 @@ namespace lms::auth
|
|||||||
|
|
||||||
const db::UserId userId{ getOrCreateUser(loginName) };
|
const db::UserId userId{ getOrCreateUser(loginName) };
|
||||||
onUserAuthenticated(userId);
|
onUserAuthenticated(userId);
|
||||||
return { CheckResult::State::Granted, userId };
|
return CheckResult{ .state = CheckResult::State::Granted, .userId = userId };
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
|
||||||
return { CheckResult::State::Denied };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||||
|
return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::auth
|
} // namespace lms::auth
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace lms::auth
|
|||||||
public:
|
public:
|
||||||
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries);
|
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries);
|
||||||
|
|
||||||
|
~PasswordServiceBase() override = default;
|
||||||
PasswordServiceBase(const PasswordServiceBase&) = delete;
|
PasswordServiceBase(const PasswordServiceBase&) = delete;
|
||||||
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
|
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
|
||||||
PasswordServiceBase(PasswordServiceBase&&) = delete;
|
PasswordServiceBase(PasswordServiceBase&&) = delete;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace lms::auth
|
|||||||
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
|
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
|
||||||
|
|
||||||
bool canSetPasswords() const override;
|
bool canSetPasswords() const override;
|
||||||
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
|
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const override;
|
||||||
void setPassword(db::UserId userId, std::string_view newPassword) override;
|
void setPassword(db::UserId userId, std::string_view newPassword) override;
|
||||||
|
|
||||||
db::User::PasswordHash hashPassword(std::string_view password) const;
|
db::User::PasswordHash hashPassword(std::string_view password) const;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
#include <security/pam_appl.h>
|
#include <security/pam_appl.h>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "services/auth/Types.hpp"
|
#include "services/auth/Types.hpp"
|
||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
@@ -192,7 +191,7 @@ namespace lms::auth
|
|||||||
throw NotImplementedException{};
|
throw NotImplementedException{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void PAMPasswordService::setPassword(db::UserId, std::string_view)
|
void PAMPasswordService::setPassword(db::UserId /*userId*/, std::string_view /*newPassword*/)
|
||||||
{
|
{
|
||||||
throw NotImplementedException{};
|
throw NotImplementedException{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <shared_mutex>
|
|
||||||
|
|
||||||
#include "PasswordServiceBase.hpp"
|
#include "PasswordServiceBase.hpp"
|
||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
@@ -62,7 +61,7 @@ namespace lms::auth
|
|||||||
};
|
};
|
||||||
|
|
||||||
State state{ State::Denied };
|
State state{ State::Denied };
|
||||||
std::optional<AuthTokenInfo> authTokenInfo{};
|
std::optional<AuthTokenInfo> authTokenInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DomainParameters
|
struct DomainParameters
|
||||||
|
|||||||
@@ -19,8 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <string_view>
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "database/UserId.hpp"
|
#include "database/UserId.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include <Wt/Dbo/ptr.h>
|
#include <Wt/Dbo/ptr.h>
|
||||||
@@ -52,7 +51,6 @@ namespace lms::auth
|
|||||||
};
|
};
|
||||||
State state{ State::Denied };
|
State state{ State::Denied };
|
||||||
db::UserId userId{};
|
db::UserId userId{};
|
||||||
std::optional<Wt::WDateTime> expiry{};
|
|
||||||
};
|
};
|
||||||
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
|
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
|
||||||
std::string_view loginName,
|
std::string_view loginName,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user