Added playlist import (and sync)from m3u files, ref #391
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ 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;ObjectPtr"
|
value: "shared_ptr;ObjectPtr;.*::pointer"
|
||||||
- key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted
|
- key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted
|
||||||
value: '1'
|
value: '1'
|
||||||
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
|
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 @@ 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") };
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -57,6 +58,9 @@ namespace lms::db
|
|||||||
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 +112,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 +312,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
|
||||||
|
|||||||
@@ -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,6 +53,12 @@ namespace lms::db
|
|||||||
public:
|
public:
|
||||||
TrackList() = default;
|
TrackList() = default;
|
||||||
|
|
||||||
|
enum class Visibility
|
||||||
|
{
|
||||||
|
Private = 0,
|
||||||
|
Public = 1,
|
||||||
|
};
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
@@ -61,6 +68,7 @@ namespace lms::db
|
|||||||
UserId user; // only tracklists owned by this user
|
UserId user; // only tracklists 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)
|
||||||
{
|
{
|
||||||
@@ -92,6 +100,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 +114,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 +151,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 +182,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
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
|
|
||||||
add_library(lmsscanner SHARED
|
add_library(lmsscanner SHARED
|
||||||
impl/FileScanQueue.cpp
|
impl/scanners/AudioFileScanner.cpp
|
||||||
|
impl/scanners/ImageFileScanner.cpp
|
||||||
|
impl/scanners/LyricsFileScanner.cpp
|
||||||
|
impl/scanners/PlayListFileScanner.cpp
|
||||||
|
impl/scanners/Utils.cpp
|
||||||
|
impl/steps/FileScanQueue.cpp
|
||||||
|
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||||
|
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||||
|
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||||
|
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||||
|
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||||
|
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||||
|
impl/steps/ScanStepCompact.cpp
|
||||||
|
impl/steps/ScanStepComputeClusterStats.cpp
|
||||||
|
impl/steps/ScanStepDiscoverFiles.cpp
|
||||||
|
impl/steps/ScanStepOptimize.cpp
|
||||||
|
impl/steps/ScanStepRemoveOrphanedDbEntries.cpp
|
||||||
|
impl/steps/ScanStepScanFiles.cpp
|
||||||
|
impl/steps/ScanStepUpdateLibraryFields.cpp
|
||||||
impl/ScannerService.cpp
|
impl/ScannerService.cpp
|
||||||
impl/ScannerStats.cpp
|
impl/ScannerStats.cpp
|
||||||
impl/ScanStepAssociateArtistImages.cpp
|
|
||||||
impl/ScanStepAssociateExternalLyrics.cpp
|
|
||||||
impl/ScanStepAssociateReleaseImages.cpp
|
|
||||||
impl/ScanStepCheckForDuplicatedFiles.cpp
|
|
||||||
impl/ScanStepCheckForRemovedFiles.cpp
|
|
||||||
impl/ScanStepCompact.cpp
|
|
||||||
impl/ScanStepComputeClusterStats.cpp
|
|
||||||
impl/ScanStepDiscoverFiles.cpp
|
|
||||||
impl/ScanStepOptimize.cpp
|
|
||||||
impl/ScanStepRemoveOrphanedDbEntries.cpp
|
|
||||||
impl/ScanStepScanFiles.cpp
|
|
||||||
impl/ScanStepUpdateLibraryFields.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmsscanner INTERFACE
|
target_include_directories(lmsscanner INTERFACE
|
||||||
@@ -23,6 +28,7 @@ target_include_directories(lmsscanner INTERFACE
|
|||||||
|
|
||||||
target_include_directories(lmsscanner PRIVATE
|
target_include_directories(lmsscanner PRIVATE
|
||||||
include
|
include
|
||||||
|
impl
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsscanner PRIVATE
|
target_link_libraries(lmsscanner PRIVATE
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FileScanQueue.hpp"
|
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "image/Exception.hpp"
|
|
||||||
#include "image/Image.hpp"
|
|
||||||
#include "metadata/Exception.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
|
||||||
{
|
|
||||||
FileScanQueue::FileScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort)
|
|
||||||
: _metadataParser{ parser }
|
|
||||||
, _scanContextRunner{ _scanContext, threadCount, "FileScan" }
|
|
||||||
, _abort{ abort }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileScanQueue::pushScanRequest(const std::filesystem::path& path, ScanRequestType type)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::scoped_lock lock{ _mutex };
|
|
||||||
_ongoingScanCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_scanContext.post([=, this] {
|
|
||||||
if (_abort)
|
|
||||||
{
|
|
||||||
std::scoped_lock lock{ _mutex };
|
|
||||||
_ongoingScanCount -= 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FileScanResult result;
|
|
||||||
result.path = path;
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case ScanRequestType::AudioFile:
|
|
||||||
result.scanData = scanAudioFile(path);
|
|
||||||
break;
|
|
||||||
case ScanRequestType::ImageFile:
|
|
||||||
result.scanData = scanImageFile(path);
|
|
||||||
break;
|
|
||||||
case ScanRequestType::LyricsFile:
|
|
||||||
result.scanData = scanLyricsFile(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::scoped_lock lock{ _mutex };
|
|
||||||
|
|
||||||
_scanResults.emplace_back(std::move(result));
|
|
||||||
_ongoingScanCount -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_condVar.notify_all();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioFileScanData FileScanQueue::scanAudioFile(const std::filesystem::path& path)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
|
||||||
std::unique_ptr<metadata::Track> track;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
track = _metadataParser.parse(path);
|
|
||||||
}
|
|
||||||
catch (const metadata::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, INFO, "Failed to parse audio file '" << path.string() << "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageFileScanData FileScanQueue::scanImageFile(const std::filesystem::path& path)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanImageFile");
|
|
||||||
|
|
||||||
std::optional<ImageInfo> optInfo;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::unique_ptr<image::IRawImage> rawImage{ image::decodeImage(path) };
|
|
||||||
ImageInfo& imageInfo{ optInfo.emplace() };
|
|
||||||
imageInfo.width = rawImage->getWidth();
|
|
||||||
imageInfo.height = rawImage->getHeight();
|
|
||||||
}
|
|
||||||
catch (const image::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file '" << path.string() << "': " << e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return optInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
LyricsFileScanData FileScanQueue::scanLyricsFile(const std::filesystem::path& path)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanLyricsFile");
|
|
||||||
|
|
||||||
LyricsFileScanData lyrics;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::ifstream ifs{ path.string() };
|
|
||||||
if (!ifs)
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot open file '" << path.string() << "'");
|
|
||||||
else
|
|
||||||
lyrics = metadata::parseLyrics(ifs);
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file '" << path.string() << "': " << e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return lyrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t FileScanQueue::getResultsCount() const
|
|
||||||
{
|
|
||||||
std::scoped_lock lock{ _mutex };
|
|
||||||
return _scanResults.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t FileScanQueue::popResults(std::vector<FileScanResult>& results, std::size_t maxCount)
|
|
||||||
{
|
|
||||||
results.clear();
|
|
||||||
results.reserve(maxCount);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::scoped_lock lock{ _mutex };
|
|
||||||
|
|
||||||
while (results.size() < maxCount && !_scanResults.empty())
|
|
||||||
{
|
|
||||||
results.push_back(std::move(_scanResults.front()));
|
|
||||||
_scanResults.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileScanQueue::wait(std::size_t maxScanRequestCount)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
|
|
||||||
|
|
||||||
std::unique_lock lock{ _mutex };
|
|
||||||
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace lms::scanner
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <deque>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <mutex>
|
|
||||||
#include <variant>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "core/IOContextRunner.hpp"
|
|
||||||
#include "metadata/IParser.hpp"
|
|
||||||
#include "metadata/Lyrics.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
|
||||||
{
|
|
||||||
struct ImageInfo
|
|
||||||
{
|
|
||||||
std::size_t height{};
|
|
||||||
std::size_t width{};
|
|
||||||
};
|
|
||||||
|
|
||||||
using AudioFileScanData = std::unique_ptr<metadata::Track>;
|
|
||||||
using ImageFileScanData = std::optional<ImageInfo>;
|
|
||||||
using LyricsFileScanData = std::optional<metadata::Lyrics>;
|
|
||||||
struct FileScanResult
|
|
||||||
{
|
|
||||||
std::filesystem::path path;
|
|
||||||
std::variant<std::monostate, AudioFileScanData, ImageFileScanData, LyricsFileScanData> scanData;
|
|
||||||
};
|
|
||||||
|
|
||||||
class FileScanQueue
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
FileScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort);
|
|
||||||
|
|
||||||
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
|
|
||||||
|
|
||||||
enum ScanRequestType
|
|
||||||
{
|
|
||||||
AudioFile,
|
|
||||||
ImageFile,
|
|
||||||
LyricsFile,
|
|
||||||
};
|
|
||||||
void pushScanRequest(const std::filesystem::path& path, ScanRequestType type);
|
|
||||||
|
|
||||||
std::size_t getResultsCount() const;
|
|
||||||
size_t popResults(std::vector<FileScanResult>& results, std::size_t maxCount);
|
|
||||||
|
|
||||||
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
|
|
||||||
|
|
||||||
private:
|
|
||||||
AudioFileScanData scanAudioFile(const std::filesystem::path& path);
|
|
||||||
ImageFileScanData scanImageFile(const std::filesystem::path& path);
|
|
||||||
LyricsFileScanData scanLyricsFile(const std::filesystem::path& path);
|
|
||||||
|
|
||||||
metadata::IParser& _metadataParser;
|
|
||||||
boost::asio::io_context _scanContext;
|
|
||||||
core::IOContextRunner _scanContextRunner;
|
|
||||||
|
|
||||||
mutable std::mutex _mutex;
|
|
||||||
std::size_t _ongoingScanCount{};
|
|
||||||
std::deque<FileScanResult> _scanResults;
|
|
||||||
std::condition_variable _condVar;
|
|
||||||
bool& _abort;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace lms::scanner
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "database/MediaLibraryId.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct MediaLibraryInfo
|
||||||
|
{
|
||||||
|
db::MediaLibraryId id;
|
||||||
|
std::filesystem::path rootDirectory;
|
||||||
|
|
||||||
|
auto operator<=>(const MediaLibraryInfo& other) const = default;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/scanner/ScannerOptions.hpp"
|
||||||
|
#include "services/scanner/ScannerStats.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScanContext
|
||||||
|
{
|
||||||
|
ScanOptions scanOptions;
|
||||||
|
ScanStats stats;
|
||||||
|
ScanStepStats currentStepStats;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -1,973 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ScanStepScanFiles.hpp"
|
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
|
||||||
#include "core/IConfig.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "core/Path.hpp"
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Cluster.hpp"
|
|
||||||
#include "database/Db.hpp"
|
|
||||||
#include "database/Directory.hpp"
|
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/MediaLibrary.hpp"
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "database/TrackArtistLink.hpp"
|
|
||||||
#include "database/TrackFeatures.hpp"
|
|
||||||
#include "database/TrackLyrics.hpp"
|
|
||||||
#include "metadata/IParser.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
|
||||||
{
|
|
||||||
using namespace db;
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
struct FileInfo
|
|
||||||
{
|
|
||||||
Wt::WDateTime lastWriteTime;
|
|
||||||
std::filesystem::path relativePath;
|
|
||||||
std::size_t fileSize{};
|
|
||||||
};
|
|
||||||
|
|
||||||
Wt::WDateTime retrieveFileGetLastWrite(const std::filesystem::path& file)
|
|
||||||
{
|
|
||||||
Wt::WDateTime res;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
res = core::pathUtils::getLastWriteTime(file);
|
|
||||||
}
|
|
||||||
catch (core::LmsException& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot get last write time: " << e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<FileInfo> retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath)
|
|
||||||
{
|
|
||||||
std::optional<FileInfo> res;
|
|
||||||
res.emplace();
|
|
||||||
|
|
||||||
res->lastWriteTime = retrieveFileGetLastWrite(file);
|
|
||||||
if (!res->lastWriteTime.isValid())
|
|
||||||
{
|
|
||||||
res.reset();
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::error_code ec;
|
|
||||||
res->relativePath = std::filesystem::relative(file, rootPath, ec);
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot get relative file path for '" << file.string() << "' from '" << rootPath.string() << "': " << ec.message());
|
|
||||||
res.reset();
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::error_code ec;
|
|
||||||
res->fileSize = std::filesystem::file_size(file, ec);
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot get file size for '" << file.string() << "': " << ec.message());
|
|
||||||
res.reset();
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const MediaLibrary::pointer& mediaLibrary)
|
|
||||||
{
|
|
||||||
Directory::pointer directory{ Directory::find(session, path) };
|
|
||||||
if (!directory)
|
|
||||||
{
|
|
||||||
Directory::pointer parentDirectory;
|
|
||||||
if (path != mediaLibrary->getPath())
|
|
||||||
parentDirectory = getOrCreateDirectory(session, path.parent_path(), mediaLibrary);
|
|
||||||
|
|
||||||
directory = session.create<Directory>(path);
|
|
||||||
directory.modify()->setParent(parentDirectory);
|
|
||||||
directory.modify()->setMediaLibrary(mediaLibrary);
|
|
||||||
}
|
|
||||||
// Don't update library if it does not match, will be updated elsewhere
|
|
||||||
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
db::TrackLyrics::pointer createLyrics(Session& session, const metadata::Lyrics& lyricsInfo)
|
|
||||||
{
|
|
||||||
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
|
||||||
|
|
||||||
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
|
||||||
lyrics.modify()->setOffset(lyricsInfo.offset);
|
|
||||||
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
|
||||||
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
|
||||||
if (!lyricsInfo.synchronizedLines.empty())
|
|
||||||
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
|
||||||
else
|
|
||||||
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
|
||||||
|
|
||||||
return lyrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
Artist::pointer createArtist(Session& session, const metadata::Artist& artistInfo)
|
|
||||||
{
|
|
||||||
Artist::pointer artist{ session.create<Artist>(artistInfo.name) };
|
|
||||||
|
|
||||||
if (artistInfo.mbid)
|
|
||||||
artist.modify()->setMBID(artistInfo.mbid);
|
|
||||||
if (artistInfo.sortName)
|
|
||||||
artist.modify()->setSortName(*artistInfo.sortName);
|
|
||||||
|
|
||||||
return artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
|
|
||||||
{
|
|
||||||
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateArtistIfNeeded(Artist::pointer artist, const metadata::Artist& artistInfo)
|
|
||||||
{
|
|
||||||
// Name may have been updated
|
|
||||||
if (artist->getName() != artistInfo.name)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'");
|
|
||||||
artist.modify()->setName(artistInfo.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortname may have been updated
|
|
||||||
// As the sort name is quite often not filled in, we update it only if already set (for now?)
|
|
||||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'");
|
|
||||||
artist.modify()->setSortName(*artistInfo.sortName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Artist::pointer> getOrCreateArtists(Session& session, const std::vector<metadata::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
|
||||||
{
|
|
||||||
std::vector<Artist::pointer> artists;
|
|
||||||
|
|
||||||
for (const metadata::Artist& artistInfo : artistsInfo)
|
|
||||||
{
|
|
||||||
Artist::pointer artist;
|
|
||||||
|
|
||||||
// First try to get by MBID
|
|
||||||
if (artistInfo.mbid)
|
|
||||||
{
|
|
||||||
artist = Artist::find(session, *artistInfo.mbid);
|
|
||||||
if (!artist)
|
|
||||||
artist = createArtist(session, artistInfo);
|
|
||||||
else
|
|
||||||
updateArtistIfNeeded(artist, artistInfo);
|
|
||||||
|
|
||||||
artists.emplace_back(std::move(artist));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back on artist name (collisions may occur)
|
|
||||||
if (!artistInfo.name.empty())
|
|
||||||
{
|
|
||||||
for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name))
|
|
||||||
{
|
|
||||||
// Do not fallback on artist that is correctly tagged
|
|
||||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
artist = sameNamedArtist;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No Artist found with the same name and without MBID -> creating
|
|
||||||
if (!artist)
|
|
||||||
artist = createArtist(session, artistInfo);
|
|
||||||
else
|
|
||||||
updateArtistIfNeeded(artist, artistInfo);
|
|
||||||
|
|
||||||
artists.emplace_back(std::move(artist));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artists;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseType::pointer getOrCreateReleaseType(Session& session, std::string_view name)
|
|
||||||
{
|
|
||||||
ReleaseType::pointer releaseType{ ReleaseType::find(session, name) };
|
|
||||||
if (!releaseType)
|
|
||||||
releaseType = session.create<ReleaseType>(name);
|
|
||||||
|
|
||||||
return releaseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label::pointer getOrCreateLabel(Session& session, std::string_view name)
|
|
||||||
{
|
|
||||||
Label::pointer label{ Label::find(session, name) };
|
|
||||||
if (!label)
|
|
||||||
label = session.create<Label>(name);
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateReleaseIfNeeded(Session& session, Release::pointer release, const metadata::Release& releaseInfo)
|
|
||||||
{
|
|
||||||
if (release->getName() != releaseInfo.name)
|
|
||||||
release.modify()->setName(releaseInfo.name);
|
|
||||||
if (release->getSortName() != releaseInfo.sortName)
|
|
||||||
release.modify()->setSortName(releaseInfo.sortName);
|
|
||||||
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
|
||||||
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
|
||||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
|
||||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
|
||||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
|
||||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
|
||||||
if (release->isCompilation() != releaseInfo.isCompilation)
|
|
||||||
release.modify()->setCompilation(releaseInfo.isCompilation);
|
|
||||||
if (release->getBarcode() != releaseInfo.barcode)
|
|
||||||
release.modify()->setBarcode(releaseInfo.barcode);
|
|
||||||
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
|
||||||
{
|
|
||||||
release.modify()->clearReleaseTypes();
|
|
||||||
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
|
||||||
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (release->getLabelNames() != releaseInfo.labels)
|
|
||||||
{
|
|
||||||
release.modify()->clearLabels();
|
|
||||||
for (std::string_view label : releaseInfo.labels)
|
|
||||||
release.modify()->addLabel(getOrCreateLabel(session, label));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare release level info
|
|
||||||
bool isReleaseMatching(const Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
|
||||||
{
|
|
||||||
// TODO: add more criterias?
|
|
||||||
return candidateRelease->getName() == releaseInfo.name
|
|
||||||
&& candidateRelease->getSortName() == releaseInfo.sortName
|
|
||||||
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
|
||||||
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
|
||||||
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
|
||||||
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
|
||||||
}
|
|
||||||
|
|
||||||
Release::pointer getOrCreateRelease(Session& session, const metadata::Release& releaseInfo, const Directory::pointer& currentDirectory)
|
|
||||||
{
|
|
||||||
Release::pointer release;
|
|
||||||
|
|
||||||
// First try to get by MBID: fastest, safest
|
|
||||||
if (releaseInfo.mbid)
|
|
||||||
{
|
|
||||||
release = Release::find(session, *releaseInfo.mbid);
|
|
||||||
if (!release)
|
|
||||||
release = session.create<Release>(releaseInfo.name, releaseInfo.mbid);
|
|
||||||
}
|
|
||||||
else if (releaseInfo.name.empty())
|
|
||||||
{
|
|
||||||
// No release name (only mbid) -> nothing to do
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back on release name (collisions may occur)
|
|
||||||
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
|
||||||
const DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
|
||||||
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
|
||||||
{
|
|
||||||
Release::FindParameters params;
|
|
||||||
params.setParentDirectory(parentDirectoryId);
|
|
||||||
params.setName(releaseInfo.name);
|
|
||||||
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
|
|
||||||
// Already found a candidate
|
|
||||||
if (release)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Do not fallback on properly tagged releases
|
|
||||||
if (candidateRelease->getMBID().has_value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
|
||||||
return;
|
|
||||||
|
|
||||||
release = candidateRelease;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lastly try in the current directory: we do this at last to have
|
|
||||||
// opportunities to merge releases in case of migration / rescan
|
|
||||||
if (!release)
|
|
||||||
{
|
|
||||||
Release::FindParameters params;
|
|
||||||
params.setDirectory(currentDirectory->getId());
|
|
||||||
params.setName(releaseInfo.name);
|
|
||||||
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
|
|
||||||
// Already found a candidate
|
|
||||||
if (release)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Do not fallback on properly tagged releases
|
|
||||||
if (candidateRelease->getMBID().has_value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
|
||||||
return;
|
|
||||||
|
|
||||||
release = candidateRelease;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!release)
|
|
||||||
release = session.create<Release>(releaseInfo.name);
|
|
||||||
|
|
||||||
updateReleaseIfNeeded(session, release, releaseInfo);
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Cluster::pointer> getOrCreateClusters(Session& session, const metadata::Track& track)
|
|
||||||
{
|
|
||||||
std::vector<Cluster::pointer> clusters;
|
|
||||||
|
|
||||||
auto getOrCreateClusters{ [&](std::string_view tag, std::span<const std::string> values) {
|
|
||||||
auto clusterType = ClusterType::find(session, tag);
|
|
||||||
if (!clusterType)
|
|
||||||
clusterType = session.create<ClusterType>(tag);
|
|
||||||
|
|
||||||
for (const auto& value : values)
|
|
||||||
{
|
|
||||||
auto cluster{ clusterType->getCluster(value) };
|
|
||||||
if (!cluster)
|
|
||||||
cluster = session.create<Cluster>(clusterType, value);
|
|
||||||
|
|
||||||
clusters.push_back(cluster);
|
|
||||||
}
|
|
||||||
} };
|
|
||||||
|
|
||||||
// TODO: migrate these fields in dedicated tables in DB
|
|
||||||
getOrCreateClusters("GENRE", track.genres);
|
|
||||||
getOrCreateClusters("MOOD", track.moods);
|
|
||||||
getOrCreateClusters("LANGUAGE", track.languages);
|
|
||||||
getOrCreateClusters("GROUPING", track.groupings);
|
|
||||||
|
|
||||||
for (const auto& [tag, values] : track.userExtraTags)
|
|
||||||
getOrCreateClusters(tag, values);
|
|
||||||
|
|
||||||
return clusters;
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata::ParserReadStyle getParserReadStyle()
|
|
||||||
{
|
|
||||||
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
|
||||||
|
|
||||||
if (readStyle == "fast")
|
|
||||||
return metadata::ParserReadStyle::Fast;
|
|
||||||
if (readStyle == "average")
|
|
||||||
return metadata::ParserReadStyle::Average;
|
|
||||||
if (readStyle == "accurate")
|
|
||||||
return metadata::ParserReadStyle::Accurate;
|
|
||||||
|
|
||||||
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t getScanMetaDataThreadCount()
|
|
||||||
{
|
|
||||||
std::size_t threadCount{ core::Service<core::IConfig>::get()->getULong("scanner-metadata-thread-count", 0) };
|
|
||||||
|
|
||||||
if (threadCount == 0)
|
|
||||||
threadCount = std::max<std::size_t>(std::thread::hardware_concurrency() / 2, 1);
|
|
||||||
|
|
||||||
return threadCount;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
|
||||||
: ScanStepBase{ initParams }
|
|
||||||
, _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib
|
|
||||||
, _fileScanQueue{ *_metadataParser, getScanMetaDataThreadCount(), _abortScan }
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::process(ScanContext& context)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::vector<std::string> tagsToParse{ _extraTagsToParse };
|
|
||||||
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
|
|
||||||
_metadataParser->setUserExtraTags(tagsToParse);
|
|
||||||
_metadataParser->setArtistTagDelimiters(_settings.artistTagDelimiters);
|
|
||||||
_metadataParser->setDefaultTagDelimiters(_settings.defaultTagDelimiters);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.currentStepStats.totalElems = context.stats.totalFileCount;
|
|
||||||
|
|
||||||
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
|
||||||
process(context, mediaLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::process(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary)
|
|
||||||
{
|
|
||||||
const std::size_t scanQueueMaxScanRequestCount{ 100 * _fileScanQueue.getThreadCount() };
|
|
||||||
const std::size_t processFileResultsBatchSize{ 5 };
|
|
||||||
|
|
||||||
std::vector<FileScanResult> scanResults;
|
|
||||||
|
|
||||||
core::pathUtils::exploreFilesRecursive(
|
|
||||||
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
|
|
||||||
|
|
||||||
if (_abortScan)
|
|
||||||
return false; // stop iterating
|
|
||||||
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot scan file '" << path.string() << "': " << ec.message());
|
|
||||||
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool fileMatched{};
|
|
||||||
if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions))
|
|
||||||
{
|
|
||||||
fileMatched = true;
|
|
||||||
if (checkAudioFileNeedScan(context, path, mediaLibrary))
|
|
||||||
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::AudioFile);
|
|
||||||
}
|
|
||||||
else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions))
|
|
||||||
{
|
|
||||||
fileMatched = true;
|
|
||||||
if (checkImageFileNeedScan(context, path))
|
|
||||||
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::ImageFile);
|
|
||||||
}
|
|
||||||
else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedLyricsFileExtensions))
|
|
||||||
{
|
|
||||||
fileMatched = true;
|
|
||||||
if (checkLyricsFileNeedScan(context, path))
|
|
||||||
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::LyricsFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileMatched)
|
|
||||||
{
|
|
||||||
context.currentStepStats.processedElems++;
|
|
||||||
_progressCallback(context.currentStepStats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2))
|
|
||||||
{
|
|
||||||
_fileScanQueue.popResults(scanResults, processFileResultsBatchSize);
|
|
||||||
processFileScanResults(context, scanResults, mediaLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileScanQueue.wait(scanQueueMaxScanRequestCount);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
&excludeDirFileName);
|
|
||||||
|
|
||||||
_fileScanQueue.wait();
|
|
||||||
|
|
||||||
while (!_abortScan && _fileScanQueue.popResults(scanResults, processFileResultsBatchSize) > 0)
|
|
||||||
processFileScanResults(context, scanResults, mediaLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScanStepScanFiles::checkAudioFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
|
||||||
{
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) };
|
|
||||||
// Should rarely fail as we are currently iterating it
|
|
||||||
if (!lastWriteTime.isValid())
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.scanOptions.fullScan)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
bool needUpdateLibrary{};
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ dbSession.createReadTransaction() };
|
|
||||||
|
|
||||||
// Skip file if last write is the same
|
|
||||||
const Track::pointer track{ Track::findByPath(dbSession, file) };
|
|
||||||
if (track
|
|
||||||
&& track->getLastWriteTime() == lastWriteTime
|
|
||||||
&& track->getScanVersion() == _settings.scanVersion)
|
|
||||||
{
|
|
||||||
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
|
|
||||||
const auto trackMediaLibrary{ track->getMediaLibrary() };
|
|
||||||
if (trackMediaLibrary && trackMediaLibrary->getId() == libraryInfo.id)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
needUpdateLibrary = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needUpdateLibrary)
|
|
||||||
{
|
|
||||||
auto transaction{ dbSession.createWriteTransaction() };
|
|
||||||
|
|
||||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
|
||||||
assert(track);
|
|
||||||
track.modify()->setMediaLibrary(db::MediaLibrary::find(dbSession, libraryInfo.id)); // may be null, will be handled in the next scan anyway
|
|
||||||
stats.updates++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // need to scan
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScanStepScanFiles::checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file)
|
|
||||||
{
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) };
|
|
||||||
// Should rarely fail as we are currently iterating it
|
|
||||||
if (!lastWriteTime.isValid())
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.scanOptions.fullScan)
|
|
||||||
{
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
|
||||||
|
|
||||||
const db::Image::pointer image{ db::Image::find(dbSession, file) };
|
|
||||||
if (image && image->getLastWriteTime() == lastWriteTime)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // need to scan
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScanStepScanFiles::checkLyricsFileNeedScan(ScanContext& context, const std::filesystem::path& file)
|
|
||||||
{
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ retrieveFileGetLastWrite(file) };
|
|
||||||
// Should rarely fail as we are currently iterating it
|
|
||||||
if (!lastWriteTime.isValid())
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.scanOptions.fullScan)
|
|
||||||
{
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
|
||||||
|
|
||||||
const db::TrackLyrics::pointer lyrics{ db::TrackLyrics::find(dbSession, file) };
|
|
||||||
if (lyrics && lyrics->getLastWriteTime() == lastWriteTime)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // need to scan
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults");
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
auto transaction{ dbSession.createWriteTransaction() };
|
|
||||||
|
|
||||||
for (const FileScanResult& scanResult : scanResults)
|
|
||||||
{
|
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (const AudioFileScanData * scanData{ std::get_if<AudioFileScanData>(&scanResult.scanData) })
|
|
||||||
{
|
|
||||||
context.stats.scans++;
|
|
||||||
processAudioFileScanData(context, scanResult.path, scanData->get(), libraryInfo);
|
|
||||||
}
|
|
||||||
else if (const ImageFileScanData * scanData{ std::get_if<ImageFileScanData>(&scanResult.scanData) })
|
|
||||||
{
|
|
||||||
context.stats.scans++;
|
|
||||||
processImageFileScanData(context, scanResult.path, scanData->has_value() ? &scanData->value() : nullptr, libraryInfo);
|
|
||||||
}
|
|
||||||
else if (const LyricsFileScanData * scanData{ std::get_if<LyricsFileScanData>(&scanResult.scanData) })
|
|
||||||
{
|
|
||||||
context.stats.scans++;
|
|
||||||
processLyricsFileScanData(context, scanResult.path, scanData->has_value() ? &scanData->value() : nullptr, libraryInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::processAudioFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Track* trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
|
||||||
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) };
|
|
||||||
if (!fileInfo)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
|
||||||
|
|
||||||
if (!trackMetadata)
|
|
||||||
{
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
track.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
context.stats.errors.emplace_back(file, ScanErrorType::CannotReadAudioFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackMetadata->mbid && (!track || _settings.skipDuplicateMBID))
|
|
||||||
{
|
|
||||||
std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackMetadata->mbid) };
|
|
||||||
|
|
||||||
// find for an existing track MBID as the file may have just been moved
|
|
||||||
if (!track && duplicateTracks.size() == 1)
|
|
||||||
{
|
|
||||||
Track::pointer otherTrack{ duplicateTracks.front() };
|
|
||||||
std::error_code ec;
|
|
||||||
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getAbsoluteFilePath() << "'");
|
|
||||||
track = otherTrack;
|
|
||||||
track.modify()->setAbsoluteFilePath(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip duplicate track MBID
|
|
||||||
if (_settings.skipDuplicateMBID)
|
|
||||||
{
|
|
||||||
for (Track::pointer& otherTrack : duplicateTracks)
|
|
||||||
{
|
|
||||||
// Skip ourselves
|
|
||||||
if (track && track->getId() == otherTrack->getId())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
|
|
||||||
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
|
||||||
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
|
|
||||||
return core::pathUtils::isPathInRootPath(file, libraryInfo.rootDirectory, &excludeDirFileName);
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getAbsoluteFilePath().string() << "')");
|
|
||||||
// As this MBID already exists, just remove what we just scanned
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
track.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We estimate this is an audio file if the duration is not null
|
|
||||||
if (trackMetadata->audioProperties.duration == std::chrono::milliseconds::zero())
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
|
||||||
|
|
||||||
// If Track exists here, delete it!
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
track.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
stats.errors.emplace_back(file, ScanErrorType::BadDuration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ***** Title
|
|
||||||
std::string title;
|
|
||||||
if (!trackMetadata->title.empty())
|
|
||||||
title = trackMetadata->title;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO parse file name guess track etc.
|
|
||||||
// For now juste use file name as title
|
|
||||||
title = file.filename().string();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If file already exists, update its data
|
|
||||||
// Otherwise, create it
|
|
||||||
bool added{};
|
|
||||||
if (!track)
|
|
||||||
{
|
|
||||||
track = dbSession.create<Track>();
|
|
||||||
track.modify()->setAbsoluteFilePath(file);
|
|
||||||
added = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track related data
|
|
||||||
assert(track);
|
|
||||||
|
|
||||||
// Audio properties
|
|
||||||
track.modify()->setBitrate(trackMetadata->audioProperties.bitrate);
|
|
||||||
track.modify()->setBitsPerSample(trackMetadata->audioProperties.bitsPerSample);
|
|
||||||
track.modify()->setChannelCount(trackMetadata->audioProperties.channelCount);
|
|
||||||
track.modify()->setDuration(trackMetadata->audioProperties.duration);
|
|
||||||
track.modify()->setSampleRate(trackMetadata->audioProperties.sampleRate);
|
|
||||||
|
|
||||||
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
|
||||||
track.modify()->setFileSize(fileInfo->fileSize);
|
|
||||||
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
|
||||||
|
|
||||||
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
|
|
||||||
track.modify()->setMediaLibrary(mediaLibrary);
|
|
||||||
Directory::pointer directory{ getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary) };
|
|
||||||
track.modify()->setDirectory(directory);
|
|
||||||
|
|
||||||
track.modify()->clearArtistLinks();
|
|
||||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
|
||||||
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata->artists, false))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
|
||||||
|
|
||||||
if (trackMetadata->medium && trackMetadata->medium->release)
|
|
||||||
{
|
|
||||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackMetadata->medium->release->artists, false))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
|
||||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
|
||||||
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackMetadata->conductorArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
|
||||||
|
|
||||||
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackMetadata->composerArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
|
|
||||||
|
|
||||||
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackMetadata->lyricistArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
|
||||||
|
|
||||||
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackMetadata->mixerArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
|
||||||
|
|
||||||
for (const auto& [role, performers] : trackMetadata->performerArtists)
|
|
||||||
{
|
|
||||||
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackMetadata->producerArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
|
|
||||||
|
|
||||||
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackMetadata->remixerArtists, true))
|
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
|
||||||
|
|
||||||
track.modify()->setScanVersion(_settings.scanVersion);
|
|
||||||
if (trackMetadata->medium && trackMetadata->medium->release)
|
|
||||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata->medium->release, directory));
|
|
||||||
else
|
|
||||||
track.modify()->setRelease({});
|
|
||||||
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
|
|
||||||
track.modify()->setReleaseReplayGain(trackMetadata->medium ? trackMetadata->medium->replayGain : std::nullopt);
|
|
||||||
track.modify()->setDiscSubtitle(trackMetadata->medium ? trackMetadata->medium->name : "");
|
|
||||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *trackMetadata));
|
|
||||||
track.modify()->setName(title);
|
|
||||||
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
|
||||||
track.modify()->setTrackNumber(trackMetadata->position);
|
|
||||||
track.modify()->setDiscNumber(trackMetadata->medium ? trackMetadata->medium->position : std::nullopt);
|
|
||||||
track.modify()->setDate(trackMetadata->date);
|
|
||||||
track.modify()->setYear(trackMetadata->year);
|
|
||||||
track.modify()->setOriginalDate(trackMetadata->originalDate);
|
|
||||||
track.modify()->setOriginalYear(trackMetadata->originalYear);
|
|
||||||
|
|
||||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
|
||||||
if (!trackMetadata->date.isValid() && trackMetadata->originalDate.isValid())
|
|
||||||
track.modify()->setDate(trackMetadata->originalDate);
|
|
||||||
|
|
||||||
// If a file has an OriginalYear but no Year, set it to ease filtering
|
|
||||||
if (!trackMetadata->year && trackMetadata->originalYear)
|
|
||||||
track.modify()->setYear(trackMetadata->originalYear);
|
|
||||||
|
|
||||||
track.modify()->setRecordingMBID(trackMetadata->recordingMBID);
|
|
||||||
track.modify()->setTrackMBID(trackMetadata->mbid);
|
|
||||||
if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
|
|
||||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
|
||||||
track.modify()->setHasCover(trackMetadata->hasCover);
|
|
||||||
track.modify()->setCopyright(trackMetadata->copyright);
|
|
||||||
track.modify()->setCopyrightURL(trackMetadata->copyrightURL);
|
|
||||||
track.modify()->setComment(!trackMetadata->comments.empty() ? trackMetadata->comments.front() : ""); // only take the first one for now
|
|
||||||
track.modify()->setTrackReplayGain(trackMetadata->replayGain);
|
|
||||||
track.modify()->setArtistDisplayName(trackMetadata->artistDisplayName);
|
|
||||||
|
|
||||||
track.modify()->clearEmbeddedLyrics();
|
|
||||||
for (const metadata::Lyrics& lyricsInfo : trackMetadata->lyrics)
|
|
||||||
{
|
|
||||||
db::TrackLyrics::pointer lyrics{ createLyrics(dbSession, lyricsInfo) };
|
|
||||||
track.modify()->addLyrics(lyrics);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added audio file '" << file.string() << "'");
|
|
||||||
stats.additions++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file '" << file.string() << "'");
|
|
||||||
stats.updates++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::processImageFileScanData(ScanContext& context, const std::filesystem::path& file, const ImageInfo* imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessImageScanData");
|
|
||||||
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) };
|
|
||||||
if (!fileInfo)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
db::Image::pointer image{ db::Image::find(dbSession, file) };
|
|
||||||
|
|
||||||
if (!imageInfo)
|
|
||||||
{
|
|
||||||
if (image)
|
|
||||||
{
|
|
||||||
image.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
context.stats.errors.emplace_back(file, ScanErrorType::CannotReadImageFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool added{ !image };
|
|
||||||
if (!image)
|
|
||||||
image = dbSession.create<db::Image>(file);
|
|
||||||
|
|
||||||
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
|
||||||
image.modify()->setFileSize(fileInfo->fileSize);
|
|
||||||
image.modify()->setHeight(imageInfo->height);
|
|
||||||
image.modify()->setWidth(imageInfo->width);
|
|
||||||
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
|
|
||||||
image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary));
|
|
||||||
|
|
||||||
if (added)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added image '" << file.string() << "'");
|
|
||||||
stats.additions++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated image '" << file.string() << "'");
|
|
||||||
stats.updates++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScanStepScanFiles::processLyricsFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Lyrics* lyricsInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessImageScanData");
|
|
||||||
|
|
||||||
ScanStats& stats{ context.stats };
|
|
||||||
|
|
||||||
const std::optional<FileInfo> fileInfo{ retrieveFileInfo(file, libraryInfo.rootDirectory) };
|
|
||||||
if (!fileInfo)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
db::TrackLyrics::pointer trackLyrics{ db::TrackLyrics::find(dbSession, file) };
|
|
||||||
|
|
||||||
if (!lyricsInfo)
|
|
||||||
{
|
|
||||||
if (trackLyrics)
|
|
||||||
{
|
|
||||||
trackLyrics.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
context.stats.errors.emplace_back(file, ScanErrorType::CannotReadLyricsFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool added{ !trackLyrics };
|
|
||||||
if (!trackLyrics)
|
|
||||||
{
|
|
||||||
trackLyrics = dbSession.create<db::TrackLyrics>();
|
|
||||||
trackLyrics.modify()->setAbsoluteFilePath(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackLyrics.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
|
||||||
trackLyrics.modify()->setFileSize(fileInfo->fileSize);
|
|
||||||
trackLyrics.modify()->setLanguage(!lyricsInfo->language.empty() ? lyricsInfo->language : "xxx");
|
|
||||||
trackLyrics.modify()->setOffset(lyricsInfo->offset);
|
|
||||||
trackLyrics.modify()->setDisplayTitle(lyricsInfo->displayTitle);
|
|
||||||
trackLyrics.modify()->setDisplayArtist(lyricsInfo->displayArtist);
|
|
||||||
if (!lyricsInfo->synchronizedLines.empty())
|
|
||||||
trackLyrics.modify()->setSynchronizedLines(lyricsInfo->synchronizedLines);
|
|
||||||
else
|
|
||||||
trackLyrics.modify()->setUnsynchronizedLines(lyricsInfo->unsynchronizedLines);
|
|
||||||
|
|
||||||
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
|
|
||||||
trackLyrics.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary));
|
|
||||||
|
|
||||||
if (added)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics '" << file.string() << "'");
|
|
||||||
stats.additions++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics '" << file.string() << "'");
|
|
||||||
stats.updates++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace lms::scanner
|
|
||||||
@@ -21,26 +21,32 @@
|
|||||||
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
|
||||||
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
#include "database/TrackFeatures.hpp"
|
|
||||||
#include "image/Image.hpp"
|
|
||||||
|
|
||||||
#include "ScanStepAssociateArtistImages.hpp"
|
#include "scanners/AudioFileScanner.hpp"
|
||||||
#include "ScanStepAssociateExternalLyrics.hpp"
|
#include "scanners/ImageFileScanner.hpp"
|
||||||
#include "ScanStepAssociateReleaseImages.hpp"
|
#include "scanners/LyricsFileScanner.hpp"
|
||||||
#include "ScanStepCheckForDuplicatedFiles.hpp"
|
#include "scanners/PlayListFileScanner.hpp"
|
||||||
#include "ScanStepCheckForRemovedFiles.hpp"
|
|
||||||
#include "ScanStepCompact.hpp"
|
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||||
#include "ScanStepComputeClusterStats.hpp"
|
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
||||||
#include "ScanStepDiscoverFiles.hpp"
|
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
||||||
#include "ScanStepOptimize.hpp"
|
#include "steps/ScanStepAssociateReleaseImages.hpp"
|
||||||
#include "ScanStepRemoveOrphanedDbEntries.hpp"
|
#include "steps/ScanStepCheckForDuplicatedFiles.hpp"
|
||||||
#include "ScanStepScanFiles.hpp"
|
#include "steps/ScanStepCheckForRemovedFiles.hpp"
|
||||||
#include "ScanStepUpdateLibraryFields.hpp"
|
#include "steps/ScanStepCompact.hpp"
|
||||||
|
#include "steps/ScanStepComputeClusterStats.hpp"
|
||||||
|
#include "steps/ScanStepDiscoverFiles.hpp"
|
||||||
|
#include "steps/ScanStepOptimize.hpp"
|
||||||
|
#include "steps/ScanStepRemoveOrphanedDbEntries.hpp"
|
||||||
|
#include "steps/ScanStepScanFiles.hpp"
|
||||||
|
#include "steps/ScanStepUpdateLibraryFields.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -270,7 +276,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|
||||||
IScanStep::ScanContext scanContext;
|
ScanContext scanContext;
|
||||||
scanContext.scanOptions = scanOptions;
|
scanContext.scanOptions = scanOptions;
|
||||||
ScanStats& stats{ scanContext.stats };
|
ScanStats& stats{ scanContext.stats };
|
||||||
stats.startTime = Wt::WDateTime::currentDateTime();
|
stats.startTime = Wt::WDateTime::currentDateTime();
|
||||||
@@ -341,27 +347,38 @@ namespace lms::scanner
|
|||||||
notifyInProgressIfNeeded(stats);
|
notifyInProgressIfNeeded(stats);
|
||||||
} };
|
} };
|
||||||
|
|
||||||
|
_fileScanners.clear();
|
||||||
|
_fileScanners.emplace_back(std::make_unique<AudioFileScanner>(_db, _settings));
|
||||||
|
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db));
|
||||||
|
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db));
|
||||||
|
_fileScanners.emplace_back(std::make_unique<PlayListFileScanner>(_db));
|
||||||
|
|
||||||
|
std::vector<IFileScanner*> fileScanners;
|
||||||
|
std::transform(std::cbegin(_fileScanners), std::cend(_fileScanners), std::back_inserter(fileScanners), [](const std::unique_ptr<IFileScanner>& scanner) { return scanner.get(); });
|
||||||
|
|
||||||
ScanStepBase::InitParams params{
|
ScanStepBase::InitParams params{
|
||||||
_settings,
|
.settings = _settings,
|
||||||
cbFunc,
|
.progressCallback = cbFunc,
|
||||||
_abortScan,
|
.abortScan = _abortScan,
|
||||||
_db
|
.db = _db,
|
||||||
|
.fileScanners = fileScanners,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Order is important: steps are sequential
|
// Order is important: steps are sequential
|
||||||
_scanSteps.clear();
|
_scanSteps.clear();
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepScanFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociatePlayListTracks>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepOptimize>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepComputeClusterStats>(params));
|
||||||
|
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScannerSettings ScannerService::readSettings()
|
ScannerSettings ScannerService::readSettings()
|
||||||
@@ -378,29 +395,8 @@ namespace lms::scanner
|
|||||||
newSettings.startTime = scanSettings->getUpdateStartTime();
|
newSettings.startTime = scanSettings->getUpdateStartTime();
|
||||||
newSettings.updatePeriod = scanSettings->getUpdatePeriod();
|
newSettings.updatePeriod = scanSettings->getUpdatePeriod();
|
||||||
|
|
||||||
{
|
|
||||||
const auto audioFileExtensions{ scanSettings->getAudioFileExtensions() };
|
|
||||||
newSettings.supportedAudioFileExtensions.reserve(audioFileExtensions.size());
|
|
||||||
std::transform(std::cbegin(audioFileExtensions), std::cend(audioFileExtensions), std::back_inserter(newSettings.supportedAudioFileExtensions),
|
|
||||||
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const auto imageFileExtensions{ image::getSupportedFileExtensions() };
|
|
||||||
newSettings.supportedImageFileExtensions.reserve(imageFileExtensions.size());
|
|
||||||
std::transform(std::cbegin(imageFileExtensions), std::cend(imageFileExtensions), std::back_inserter(newSettings.supportedImageFileExtensions),
|
|
||||||
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const auto lyricsFileExtensions{ metadata::getSupportedLyricsFileExtensions() };
|
|
||||||
newSettings.supportedLyricsFileExtensions.reserve(lyricsFileExtensions.size());
|
|
||||||
std::transform(std::cbegin(lyricsFileExtensions), std::cend(lyricsFileExtensions), std::back_inserter(newSettings.supportedLyricsFileExtensions),
|
|
||||||
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) {
|
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) {
|
||||||
newSettings.mediaLibraries.push_back(ScannerSettings::MediaLibraryInfo{ mediaLibrary->getId(), mediaLibrary->getPath().lexically_normal() });
|
newSettings.mediaLibraries.push_back(MediaLibraryInfo{ .id = mediaLibrary->getId(), .rootDirectory = mediaLibrary->getPath().lexically_normal() });
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,14 +29,16 @@
|
|||||||
#include <Wt/WSignal.h>
|
#include <Wt/WSignal.h>
|
||||||
#include <boost/asio/system_timer.hpp>
|
#include <boost/asio/system_timer.hpp>
|
||||||
|
|
||||||
#include "IScanStep.hpp"
|
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "services/scanner/IScannerService.hpp"
|
#include "services/scanner/IScannerService.hpp"
|
||||||
|
#include "steps/IScanStep.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
class IFileScanner;
|
||||||
|
|
||||||
class ScannerService : public IScannerService
|
class ScannerService : public IScannerService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -73,6 +75,7 @@ namespace lms::scanner
|
|||||||
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
||||||
void notifyInProgress(const ScanStepStats& stats);
|
void notifyInProgress(const ScanStepStats& stats);
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<IFileScanner>> _fileScanners;
|
||||||
std::vector<std::unique_ptr<IScanStep>> _scanSteps;
|
std::vector<std::unique_ptr<IScanStep>> _scanSteps;
|
||||||
|
|
||||||
std::mutex _controlMutex;
|
std::mutex _controlMutex;
|
||||||
|
|||||||
@@ -25,31 +25,23 @@
|
|||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "database/MediaLibraryId.hpp"
|
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
|
|
||||||
|
#include "MediaLibraryInfo.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
static inline const std::filesystem::path excludeDirFileName{ ".lmsignore" };
|
||||||
|
|
||||||
struct ScannerSettings
|
struct ScannerSettings
|
||||||
{
|
{
|
||||||
std::size_t scanVersion{};
|
std::size_t scanVersion{};
|
||||||
Wt::WTime startTime;
|
Wt::WTime startTime;
|
||||||
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
|
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
|
||||||
std::vector<std::filesystem::path> supportedAudioFileExtensions;
|
|
||||||
std::vector<std::filesystem::path> supportedImageFileExtensions;
|
|
||||||
std::vector<std::filesystem::path> supportedLyricsFileExtensions;
|
|
||||||
bool skipDuplicateMBID{};
|
bool skipDuplicateMBID{};
|
||||||
std::vector<std::string> extraTags;
|
std::vector<std::string> extraTags;
|
||||||
std::vector<std::string> artistTagDelimiters;
|
std::vector<std::string> artistTagDelimiters;
|
||||||
std::vector<std::string> defaultTagDelimiters;
|
std::vector<std::string> defaultTagDelimiters;
|
||||||
|
|
||||||
struct MediaLibraryInfo
|
|
||||||
{
|
|
||||||
db::MediaLibraryId id;
|
|
||||||
std::filesystem::path rootDirectory;
|
|
||||||
|
|
||||||
auto operator<=>(const MediaLibraryInfo& other) const = default;
|
|
||||||
};
|
|
||||||
std::vector<MediaLibraryInfo> mediaLibraries;
|
std::vector<MediaLibraryInfo> mediaLibraries;
|
||||||
|
|
||||||
bool operator==(const ScannerSettings& rhs) const = default;
|
bool operator==(const ScannerSettings& rhs) const = default;
|
||||||
|
|||||||
@@ -0,0 +1,668 @@
|
|||||||
|
/*
|
||||||
|
* 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 "AudioFileScanner.hpp"
|
||||||
|
|
||||||
|
#include "core/IConfig.hpp"
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "core/Service.hpp"
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackArtistLink.hpp"
|
||||||
|
#include "database/TrackFeatures.hpp"
|
||||||
|
#include "database/TrackLyrics.hpp"
|
||||||
|
#include "metadata/Exception.hpp"
|
||||||
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
|
#include "IFileScanOperation.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
|
||||||
|
{
|
||||||
|
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
|
||||||
|
|
||||||
|
if (artistInfo.mbid)
|
||||||
|
artist.modify()->setMBID(artistInfo.mbid);
|
||||||
|
if (artistInfo.sortName)
|
||||||
|
artist.modify()->setSortName(*artistInfo.sortName);
|
||||||
|
|
||||||
|
return artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
|
||||||
|
{
|
||||||
|
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
|
||||||
|
{
|
||||||
|
// Name may have been updated
|
||||||
|
if (artist->getName() != artistInfo.name)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'");
|
||||||
|
artist.modify()->setName(artistInfo.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortname may have been updated
|
||||||
|
// As the sort name is quite often not filled in, we update it only if already set (for now?)
|
||||||
|
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'");
|
||||||
|
artist.modify()->setSortName(*artistInfo.sortName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<db::Artist::pointer> getOrCreateArtists(db::Session& session, const std::vector<metadata::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
||||||
|
{
|
||||||
|
std::vector<db::Artist::pointer> artists;
|
||||||
|
|
||||||
|
for (const metadata::Artist& artistInfo : artistsInfo)
|
||||||
|
{
|
||||||
|
db::Artist::pointer artist;
|
||||||
|
|
||||||
|
// First try to get by MBID
|
||||||
|
if (artistInfo.mbid)
|
||||||
|
{
|
||||||
|
artist = db::Artist::find(session, *artistInfo.mbid);
|
||||||
|
if (!artist)
|
||||||
|
artist = createArtist(session, artistInfo);
|
||||||
|
else
|
||||||
|
updateArtistIfNeeded(artist, artistInfo);
|
||||||
|
|
||||||
|
artists.emplace_back(std::move(artist));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back on artist name (collisions may occur)
|
||||||
|
if (!artistInfo.name.empty())
|
||||||
|
{
|
||||||
|
for (const db::Artist::pointer& sameNamedArtist : db::Artist::find(session, artistInfo.name))
|
||||||
|
{
|
||||||
|
// Do not fallback on artist that is correctly tagged
|
||||||
|
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
artist = sameNamedArtist;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Artist found with the same name and without MBID -> creating
|
||||||
|
if (!artist)
|
||||||
|
artist = createArtist(session, artistInfo);
|
||||||
|
else
|
||||||
|
updateArtistIfNeeded(artist, artistInfo);
|
||||||
|
|
||||||
|
artists.emplace_back(std::move(artist));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return artists;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name)
|
||||||
|
{
|
||||||
|
db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) };
|
||||||
|
if (!releaseType)
|
||||||
|
releaseType = session.create<db::ReleaseType>(name);
|
||||||
|
|
||||||
|
return releaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Label::pointer getOrCreateLabel(db::Session& session, std::string_view name)
|
||||||
|
{
|
||||||
|
db::Label::pointer label{ db::Label::find(session, name) };
|
||||||
|
if (!label)
|
||||||
|
label = session.create<db::Label>(name);
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
|
||||||
|
{
|
||||||
|
if (release->getName() != releaseInfo.name)
|
||||||
|
release.modify()->setName(releaseInfo.name);
|
||||||
|
if (release->getSortName() != releaseInfo.sortName)
|
||||||
|
release.modify()->setSortName(releaseInfo.sortName);
|
||||||
|
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
||||||
|
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
||||||
|
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||||
|
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||||
|
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||||
|
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||||
|
if (release->isCompilation() != releaseInfo.isCompilation)
|
||||||
|
release.modify()->setCompilation(releaseInfo.isCompilation);
|
||||||
|
if (release->getBarcode() != releaseInfo.barcode)
|
||||||
|
release.modify()->setBarcode(releaseInfo.barcode);
|
||||||
|
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
||||||
|
{
|
||||||
|
release.modify()->clearReleaseTypes();
|
||||||
|
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
||||||
|
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (release->getLabelNames() != releaseInfo.labels)
|
||||||
|
{
|
||||||
|
release.modify()->clearLabels();
|
||||||
|
for (std::string_view label : releaseInfo.labels)
|
||||||
|
release.modify()->addLabel(getOrCreateLabel(session, label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare release level info
|
||||||
|
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||||
|
{
|
||||||
|
// TODO: add more criterias?
|
||||||
|
return candidateRelease->getName() == releaseInfo.name
|
||||||
|
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||||
|
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||||
|
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
||||||
|
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
||||||
|
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
|
||||||
|
{
|
||||||
|
db::Release::pointer release;
|
||||||
|
|
||||||
|
// First try to get by MBID: fastest, safest
|
||||||
|
if (releaseInfo.mbid)
|
||||||
|
{
|
||||||
|
release = db::Release::find(session, *releaseInfo.mbid);
|
||||||
|
if (!release)
|
||||||
|
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
|
||||||
|
}
|
||||||
|
else if (releaseInfo.name.empty())
|
||||||
|
{
|
||||||
|
// No release name (only mbid) -> nothing to do
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back on release name (collisions may occur)
|
||||||
|
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||||
|
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||||
|
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||||
|
{
|
||||||
|
db::Release::FindParameters params;
|
||||||
|
params.setParentDirectory(parentDirectoryId);
|
||||||
|
params.setName(releaseInfo.name);
|
||||||
|
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||||
|
// Already found a candidate
|
||||||
|
if (release)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Do not fallback on properly tagged releases
|
||||||
|
if (candidateRelease->getMBID().has_value())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||||
|
return;
|
||||||
|
|
||||||
|
release = candidateRelease;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lastly try in the current directory: we do this at last to have
|
||||||
|
// opportunities to merge releases in case of migration / rescan
|
||||||
|
if (!release)
|
||||||
|
{
|
||||||
|
db::Release::FindParameters params;
|
||||||
|
params.setDirectory(currentDirectory->getId());
|
||||||
|
params.setName(releaseInfo.name);
|
||||||
|
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||||
|
// Already found a candidate
|
||||||
|
if (release)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Do not fallback on properly tagged releases
|
||||||
|
if (candidateRelease->getMBID().has_value())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||||
|
return;
|
||||||
|
|
||||||
|
release = candidateRelease;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!release)
|
||||||
|
release = session.create<db::Release>(releaseInfo.name);
|
||||||
|
|
||||||
|
updateReleaseIfNeeded(session, release, releaseInfo);
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||||
|
{
|
||||||
|
std::vector<db::Cluster::pointer> clusters;
|
||||||
|
|
||||||
|
auto getOrCreateClusters{ [&](std::string_view tag, std::span<const std::string> values) {
|
||||||
|
auto clusterType = db::ClusterType::find(session, tag);
|
||||||
|
if (!clusterType)
|
||||||
|
clusterType = session.create<db::ClusterType>(tag);
|
||||||
|
|
||||||
|
for (const auto& value : values)
|
||||||
|
{
|
||||||
|
auto cluster{ clusterType->getCluster(value) };
|
||||||
|
if (!cluster)
|
||||||
|
cluster = session.create<db::Cluster>(clusterType, value);
|
||||||
|
|
||||||
|
clusters.push_back(cluster);
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
|
||||||
|
// TODO: migrate these fields in dedicated tables in DB
|
||||||
|
getOrCreateClusters("GENRE", track.genres);
|
||||||
|
getOrCreateClusters("MOOD", track.moods);
|
||||||
|
getOrCreateClusters("LANGUAGE", track.languages);
|
||||||
|
getOrCreateClusters("GROUPING", track.groupings);
|
||||||
|
|
||||||
|
for (const auto& [tag, values] : track.userExtraTags)
|
||||||
|
getOrCreateClusters(tag, values);
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
|
||||||
|
{
|
||||||
|
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
||||||
|
|
||||||
|
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
||||||
|
lyrics.modify()->setOffset(lyricsInfo.offset);
|
||||||
|
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
||||||
|
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
||||||
|
if (!lyricsInfo.synchronizedLines.empty())
|
||||||
|
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
||||||
|
else
|
||||||
|
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
||||||
|
|
||||||
|
return lyrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioFileScanOperation : public IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IParser& parser, const ScannerSettings& settings)
|
||||||
|
: _file{ fileToScan.file }
|
||||||
|
, _mediaLibrary{ fileToScan.mediaLibrary }
|
||||||
|
, _db{ db }
|
||||||
|
, _parser{ parser }
|
||||||
|
, _settings{ settings }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
~AudioFileScanOperation() override = default;
|
||||||
|
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||||
|
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::LiteralString getName() const override { return "ScanAudioFile"; }
|
||||||
|
void scan() override;
|
||||||
|
void processResult(ScanContext& context) override;
|
||||||
|
|
||||||
|
const std::filesystem::path _file;
|
||||||
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
|
db::Db& _db;
|
||||||
|
metadata::IParser& _parser;
|
||||||
|
const ScannerSettings& _settings;
|
||||||
|
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AudioFileScanOperation::scan()
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
||||||
|
std::unique_ptr<metadata::Track> track;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_parsedTrack = _parser.parse(_file);
|
||||||
|
}
|
||||||
|
catch (const metadata::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, INFO, "Failed to parse audio file '" << _file.string() << "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioFileScanOperation::processResult(ScanContext& context)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||||
|
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||||
|
if (!fileInfo)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
db::Track::pointer track{ db::Track::findByPath(dbSession, _file) };
|
||||||
|
|
||||||
|
if (!_parsedTrack)
|
||||||
|
{
|
||||||
|
if (track)
|
||||||
|
{
|
||||||
|
track.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadAudioFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_parsedTrack->mbid && (!track || _settings.skipDuplicateMBID))
|
||||||
|
{
|
||||||
|
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||||
|
|
||||||
|
// find for an existing track MBID as the file may have just been moved
|
||||||
|
if (!track && duplicateTracks.size() == 1)
|
||||||
|
{
|
||||||
|
db::Track::pointer otherTrack{ duplicateTracks.front() };
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << _file.string() << "' moved from '" << otherTrack->getAbsoluteFilePath() << "'");
|
||||||
|
track = otherTrack;
|
||||||
|
track.modify()->setAbsoluteFilePath(_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip duplicate track MBID
|
||||||
|
if (_settings.skipDuplicateMBID)
|
||||||
|
{
|
||||||
|
for (db::Track::pointer& otherTrack : duplicateTracks)
|
||||||
|
{
|
||||||
|
// Skip ourselves
|
||||||
|
if (track && track->getId() == otherTrack->getId())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
|
||||||
|
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
||||||
|
[&](const MediaLibraryInfo& libraryInfo) {
|
||||||
|
return core::pathUtils::isPathInRootPath(_file, libraryInfo.rootDirectory, &excludeDirFileName);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << _file.string() << "' (similar MBID in '" << otherTrack->getAbsoluteFilePath().string() << "')");
|
||||||
|
// As this MBID already exists, just remove what we just scanned
|
||||||
|
if (track)
|
||||||
|
{
|
||||||
|
track.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We estimate this is an audio file if the duration is not null
|
||||||
|
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << _file.string() << "' (duration is 0)");
|
||||||
|
|
||||||
|
// If Track exists here, delete it!
|
||||||
|
if (track)
|
||||||
|
{
|
||||||
|
track.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
stats.errors.emplace_back(_file, ScanErrorType::BadDuration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***** Title
|
||||||
|
std::string title;
|
||||||
|
if (!_parsedTrack->title.empty())
|
||||||
|
title = _parsedTrack->title;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO parse file name guess track etc.
|
||||||
|
// For now juste use file name as title
|
||||||
|
title = _file.filename().string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file already exists, update its data
|
||||||
|
// Otherwise, create it
|
||||||
|
bool added{};
|
||||||
|
if (!track)
|
||||||
|
{
|
||||||
|
track = dbSession.create<db::Track>();
|
||||||
|
track.modify()->setAbsoluteFilePath(_file);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track related data
|
||||||
|
assert(track);
|
||||||
|
|
||||||
|
// Audio properties
|
||||||
|
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||||
|
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
|
||||||
|
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
|
||||||
|
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||||
|
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||||
|
|
||||||
|
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
||||||
|
track.modify()->setFileSize(fileInfo->fileSize);
|
||||||
|
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
|
|
||||||
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
|
track.modify()->setMediaLibrary(mediaLibrary);
|
||||||
|
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary) };
|
||||||
|
track.modify()->setDirectory(directory);
|
||||||
|
|
||||||
|
track.modify()->clearArtistLinks();
|
||||||
|
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||||
|
for (const db::Artist::pointer& artist : getOrCreateArtists(dbSession, _parsedTrack->artists, false))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, artist, db::TrackArtistLinkType::Artist));
|
||||||
|
|
||||||
|
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||||
|
{
|
||||||
|
for (const db::Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, _parsedTrack->medium->release->artists, false))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, releaseArtist, db::TrackArtistLinkType::ReleaseArtist));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||||
|
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||||
|
for (const db::Artist::pointer& conductor : getOrCreateArtists(dbSession, _parsedTrack->conductorArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, conductor, db::TrackArtistLinkType::Conductor));
|
||||||
|
|
||||||
|
for (const db::Artist::pointer& composer : getOrCreateArtists(dbSession, _parsedTrack->composerArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, composer, db::TrackArtistLinkType::Composer));
|
||||||
|
|
||||||
|
for (const db::Artist::pointer& lyricist : getOrCreateArtists(dbSession, _parsedTrack->lyricistArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, lyricist, db::TrackArtistLinkType::Lyricist));
|
||||||
|
|
||||||
|
for (const db::Artist::pointer& mixer : getOrCreateArtists(dbSession, _parsedTrack->mixerArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, mixer, db::TrackArtistLinkType::Mixer));
|
||||||
|
|
||||||
|
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||||
|
{
|
||||||
|
for (const db::Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, performer, db::TrackArtistLinkType::Performer, role));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const db::Artist::pointer& producer : getOrCreateArtists(dbSession, _parsedTrack->producerArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, producer, db::TrackArtistLinkType::Producer));
|
||||||
|
|
||||||
|
for (const db::Artist::pointer& remixer : getOrCreateArtists(dbSession, _parsedTrack->remixerArtists, true))
|
||||||
|
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, remixer, db::TrackArtistLinkType::Remixer));
|
||||||
|
|
||||||
|
track.modify()->setScanVersion(_settings.scanVersion);
|
||||||
|
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||||
|
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory));
|
||||||
|
else
|
||||||
|
track.modify()->setRelease({});
|
||||||
|
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt);
|
||||||
|
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt);
|
||||||
|
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
|
||||||
|
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||||
|
track.modify()->setName(title);
|
||||||
|
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
||||||
|
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||||
|
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
|
||||||
|
track.modify()->setDate(_parsedTrack->date);
|
||||||
|
track.modify()->setYear(_parsedTrack->year);
|
||||||
|
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||||
|
track.modify()->setOriginalYear(_parsedTrack->originalYear);
|
||||||
|
|
||||||
|
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||||
|
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
|
||||||
|
track.modify()->setDate(_parsedTrack->originalDate);
|
||||||
|
|
||||||
|
// If a file has an OriginalYear but no Year, set it to ease filtering
|
||||||
|
if (!_parsedTrack->year && _parsedTrack->originalYear)
|
||||||
|
track.modify()->setYear(_parsedTrack->originalYear);
|
||||||
|
|
||||||
|
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
|
||||||
|
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||||
|
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||||
|
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||||
|
track.modify()->setHasCover(_parsedTrack->hasCover);
|
||||||
|
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||||
|
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||||
|
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||||
|
track.modify()->setTrackReplayGain(_parsedTrack->replayGain);
|
||||||
|
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||||
|
|
||||||
|
track.modify()->clearEmbeddedLyrics();
|
||||||
|
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
|
||||||
|
{
|
||||||
|
db::TrackLyrics::pointer lyrics{ createLyrics(dbSession, lyricsInfo) };
|
||||||
|
track.modify()->addLyrics(lyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Added audio file '" << _file.string() << "'");
|
||||||
|
stats.additions++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file '" << _file.string() << "'");
|
||||||
|
stats.updates++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata::ParserReadStyle getParserReadStyle()
|
||||||
|
{
|
||||||
|
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||||
|
|
||||||
|
if (readStyle == "fast")
|
||||||
|
return metadata::ParserReadStyle::Fast;
|
||||||
|
if (readStyle == "average")
|
||||||
|
return metadata::ParserReadStyle::Average;
|
||||||
|
if (readStyle == "accurate")
|
||||||
|
return metadata::ParserReadStyle::Accurate;
|
||||||
|
|
||||||
|
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
||||||
|
: _db{ db }
|
||||||
|
, _settings{ settings }
|
||||||
|
, _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib
|
||||||
|
{
|
||||||
|
std::vector<std::string> tagsToParse{ _extraTagsToParse };
|
||||||
|
tagsToParse.insert(std::end(tagsToParse), std::cbegin(settings.extraTags), std::cend(settings.extraTags));
|
||||||
|
_metadataParser->setUserExtraTags(tagsToParse);
|
||||||
|
_metadataParser->setArtistTagDelimiters(settings.artistTagDelimiters);
|
||||||
|
_metadataParser->setDefaultTagDelimiters(settings.defaultTagDelimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileScanner::~AudioFileScanner() = default;
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return _metadataParser->getSupportedExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
||||||
|
// Should rarely fail as we are currently iterating it
|
||||||
|
if (!lastWriteTime.isValid())
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.scanOptions.fullScan)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
bool needUpdateLibrary{};
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
// Skip file if last write is the same
|
||||||
|
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
||||||
|
if (track
|
||||||
|
&& track->getLastWriteTime() == lastWriteTime
|
||||||
|
&& track->getScanVersion() == _settings.scanVersion)
|
||||||
|
{
|
||||||
|
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
|
||||||
|
const auto trackMediaLibrary{ track->getMediaLibrary() };
|
||||||
|
if (trackMediaLibrary && trackMediaLibrary->getId() == file.mediaLibrary.id)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
needUpdateLibrary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needUpdateLibrary)
|
||||||
|
{
|
||||||
|
auto transaction{ dbSession.createWriteTransaction() };
|
||||||
|
|
||||||
|
db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
||||||
|
assert(track);
|
||||||
|
track.modify()->setMediaLibrary(db::MediaLibrary::find(dbSession, file.mediaLibrary.id)); // may be null, will be handled in the next scan anyway
|
||||||
|
stats.updates++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // need to scan
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
||||||
|
{
|
||||||
|
return std::make_unique<AudioFileScanOperation>(fileToScan, _db, *_metadataParser, _settings);
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "IFileScanner.hpp"
|
||||||
|
|
||||||
|
namespace lms
|
||||||
|
{
|
||||||
|
namespace db
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace metadata
|
||||||
|
{
|
||||||
|
class IParser;
|
||||||
|
}
|
||||||
|
} // namespace lms
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
class AudioFileScanner : public IFileScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileScanner(db::Db& db, const ScannerSettings& settings);
|
||||||
|
~AudioFileScanner() override;
|
||||||
|
AudioFileScanner(const AudioFileScanner&) = delete;
|
||||||
|
AudioFileScanner& operator=(const AudioFileScanner&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||||
|
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||||
|
|
||||||
|
db::Db& _db;
|
||||||
|
const ScannerSettings& _settings;
|
||||||
|
std::unique_ptr<metadata::IParser> _metadataParser;
|
||||||
|
const std::vector<std::string> _extraTagsToParse;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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 "core/LiteralString.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanContext;
|
||||||
|
|
||||||
|
class IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IFileScanOperation() = default;
|
||||||
|
|
||||||
|
virtual core::LiteralString getName() const = 0;
|
||||||
|
virtual void scan() = 0;
|
||||||
|
virtual void processResult(ScanContext& context) = 0;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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 "MediaLibraryInfo.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanContext;
|
||||||
|
class IFileScanOperation;
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
struct FileToScan
|
||||||
|
{
|
||||||
|
std::filesystem::path file;
|
||||||
|
MediaLibraryInfo mediaLibrary;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IFileScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IFileScanner() = default;
|
||||||
|
|
||||||
|
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||||
|
virtual bool needsScan(ScanContext& context, const FileToScan& file) const = 0;
|
||||||
|
virtual std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const = 0;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ImageFileScanner.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
|
#include "IFileScanOperation.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class ImageFileScanOperation : public IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImageFileScanOperation(const FileToScan& file, db::Db& db)
|
||||||
|
: _file{ file.file }
|
||||||
|
, _mediaLibrary{ file.mediaLibrary }
|
||||||
|
, _db{ db } {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::LiteralString getName() const override { return "ScanImageFile"; }
|
||||||
|
void scan() override;
|
||||||
|
void processResult(ScanContext& context) override;
|
||||||
|
|
||||||
|
const std::filesystem::path _file;
|
||||||
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
|
db::Db& _db;
|
||||||
|
|
||||||
|
struct ImageInfo
|
||||||
|
{
|
||||||
|
std::size_t height{};
|
||||||
|
std::size_t width{};
|
||||||
|
};
|
||||||
|
std::optional<ImageInfo> _parsedImageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ImageFileScanOperation::scan()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::unique_ptr<image::IRawImage> rawImage{ image::decodeImage(_file) };
|
||||||
|
ImageInfo& imageInfo{ _parsedImageInfo.emplace() };
|
||||||
|
imageInfo.width = rawImage->getWidth();
|
||||||
|
imageInfo.height = rawImage->getHeight();
|
||||||
|
}
|
||||||
|
catch (const image::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file '" << _file.string() << "': " << e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageFileScanOperation::processResult(ScanContext& context)
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||||
|
if (!fileInfo)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
db::Image::pointer image{ db::Image::find(dbSession, _file) };
|
||||||
|
|
||||||
|
if (!_parsedImageInfo)
|
||||||
|
{
|
||||||
|
if (image)
|
||||||
|
{
|
||||||
|
image.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadImageFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool added{ !image };
|
||||||
|
if (!image)
|
||||||
|
image = dbSession.create<db::Image>(_file);
|
||||||
|
|
||||||
|
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
|
image.modify()->setFileSize(fileInfo->fileSize);
|
||||||
|
image.modify()->setHeight(_parsedImageInfo->height);
|
||||||
|
image.modify()->setWidth(_parsedImageInfo->width);
|
||||||
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
|
image.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Added image '" << _file.string() << "'");
|
||||||
|
stats.additions++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated image '" << _file.string() << "'");
|
||||||
|
stats.updates++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ImageFileScanner::ImageFileScanner(db::Db& db)
|
||||||
|
: _db{ db }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> ImageFileScanner::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return image::getSupportedFileExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
||||||
|
// Should rarely fail as we are currently iterating it
|
||||||
|
if (!lastWriteTime.isValid())
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.scanOptions.fullScan)
|
||||||
|
{
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Image::pointer image{ db::Image::find(dbSession, file.file) };
|
||||||
|
if (image && image->getLastWriteTime() == lastWriteTime)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // need to scan
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IFileScanOperation> ImageFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
||||||
|
{
|
||||||
|
return std::make_unique<ImageFileScanOperation>(fileToScan, _db);
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 "IFileScanner.hpp"
|
||||||
|
|
||||||
|
namespace lms
|
||||||
|
{
|
||||||
|
namespace db
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
} // namespace lms
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
class ImageFileScanner : public IFileScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImageFileScanner(db::Db& db);
|
||||||
|
~ImageFileScanner() override = default;
|
||||||
|
ImageFileScanner(const ImageFileScanner&) = delete;
|
||||||
|
ImageFileScanner& operator=(const ImageFileScanner&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||||
|
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||||
|
|
||||||
|
db::Db& _db;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* 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 "LyricsFileScanner.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/TrackLyrics.hpp"
|
||||||
|
#include "metadata/Lyrics.hpp"
|
||||||
|
|
||||||
|
#include "IFileScanOperation.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class LyricsFileScanOperation : public IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LyricsFileScanOperation(const FileToScan& file, db::Db& db)
|
||||||
|
: _file{ file.file }
|
||||||
|
, _mediaLibrary{ file.mediaLibrary }
|
||||||
|
, _db{ db } {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::LiteralString getName() const override { return "ScanLyricsFile"; }
|
||||||
|
void scan() override;
|
||||||
|
void processResult(ScanContext& context) override;
|
||||||
|
|
||||||
|
const std::filesystem::path _file;
|
||||||
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
|
db::Db& _db;
|
||||||
|
|
||||||
|
std::optional<metadata::Lyrics> _parsedLyrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
void LyricsFileScanOperation::scan()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::ifstream ifs{ _file.string() };
|
||||||
|
if (!ifs)
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot open file '" << _file.string() << "'");
|
||||||
|
else
|
||||||
|
_parsedLyrics = metadata::parseLyrics(ifs);
|
||||||
|
}
|
||||||
|
catch (const metadata::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file '" << _file.string() << "': " << e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsFileScanOperation::processResult(ScanContext& context)
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||||
|
if (!fileInfo)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
db::TrackLyrics::pointer trackLyrics{ db::TrackLyrics::find(dbSession, _file) };
|
||||||
|
|
||||||
|
if (!_parsedLyrics)
|
||||||
|
{
|
||||||
|
if (trackLyrics)
|
||||||
|
{
|
||||||
|
trackLyrics.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadLyricsFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool added{ !trackLyrics };
|
||||||
|
if (!trackLyrics)
|
||||||
|
{
|
||||||
|
trackLyrics = dbSession.create<db::TrackLyrics>();
|
||||||
|
trackLyrics.modify()->setAbsoluteFilePath(_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackLyrics.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
|
trackLyrics.modify()->setFileSize(fileInfo->fileSize);
|
||||||
|
trackLyrics.modify()->setLanguage(!_parsedLyrics->language.empty() ? _parsedLyrics->language : "xxx");
|
||||||
|
trackLyrics.modify()->setOffset(_parsedLyrics->offset);
|
||||||
|
trackLyrics.modify()->setDisplayTitle(_parsedLyrics->displayTitle);
|
||||||
|
trackLyrics.modify()->setDisplayArtist(_parsedLyrics->displayArtist);
|
||||||
|
if (!_parsedLyrics->synchronizedLines.empty())
|
||||||
|
trackLyrics.modify()->setSynchronizedLines(_parsedLyrics->synchronizedLines);
|
||||||
|
else
|
||||||
|
trackLyrics.modify()->setUnsynchronizedLines(_parsedLyrics->unsynchronizedLines);
|
||||||
|
|
||||||
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
|
trackLyrics.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics '" << _file.string() << "'");
|
||||||
|
stats.additions++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics '" << _file.string() << "'");
|
||||||
|
stats.updates++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
LyricsFileScanner::LyricsFileScanner(db::Db& db)
|
||||||
|
: _db{ db }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return metadata::getSupportedLyricsFileExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LyricsFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
||||||
|
// Should rarely fail as we are currently iterating it
|
||||||
|
if (!lastWriteTime.isValid())
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.scanOptions.fullScan)
|
||||||
|
{
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::TrackLyrics::pointer lyrics{ db::TrackLyrics::find(dbSession, file.file) };
|
||||||
|
if (lyrics && lyrics->getLastWriteTime() == lastWriteTime)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // need to scan
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IFileScanOperation> LyricsFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
||||||
|
{
|
||||||
|
return std::make_unique<LyricsFileScanOperation>(fileToScan, _db);
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 "IFileScanner.hpp"
|
||||||
|
|
||||||
|
namespace lms
|
||||||
|
{
|
||||||
|
namespace db
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
} // namespace lms
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
class LyricsFileScanner : public IFileScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LyricsFileScanner(db::Db& db);
|
||||||
|
~LyricsFileScanner() override = default;
|
||||||
|
LyricsFileScanner(const LyricsFileScanner&) = delete;
|
||||||
|
LyricsFileScanner& operator=(const LyricsFileScanner&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||||
|
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||||
|
|
||||||
|
db::Db& _db;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* 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 "PlayListFileScanner.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "metadata/Exception.hpp"
|
||||||
|
#include "metadata/PlayList.hpp"
|
||||||
|
|
||||||
|
#include "IFileScanOperation.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class PlayListFileScanOperation : public IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PlayListFileScanOperation(const FileToScan& file, db::Db& db)
|
||||||
|
: _file{ file.file }
|
||||||
|
, _mediaLibrary{ file.mediaLibrary }
|
||||||
|
, _db{ db } {}
|
||||||
|
~PlayListFileScanOperation() override = default;
|
||||||
|
PlayListFileScanOperation(const PlayListFileScanOperation&) = delete;
|
||||||
|
PlayListFileScanOperation& operator=(const PlayListFileScanOperation&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::LiteralString getName() const override { return "ScanPlayListFile"; }
|
||||||
|
void scan() override;
|
||||||
|
void processResult(ScanContext& context) override;
|
||||||
|
|
||||||
|
const std::filesystem::path _file;
|
||||||
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
|
db::Db& _db;
|
||||||
|
|
||||||
|
std::optional<metadata::PlayList> _parsedPlayList;
|
||||||
|
};
|
||||||
|
|
||||||
|
void PlayListFileScanOperation::scan()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::ifstream ifs{ _file.string() };
|
||||||
|
if (!ifs)
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot open file '" << _file.string() << "'");
|
||||||
|
else
|
||||||
|
_parsedPlayList = metadata::parsePlayList(ifs);
|
||||||
|
}
|
||||||
|
catch (const metadata::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot read playlist in file '" << _file.string() << "': " << e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayListFileScanOperation::processResult(ScanContext& context)
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||||
|
if (!fileInfo)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, _file) };
|
||||||
|
|
||||||
|
if (!_parsedPlayList)
|
||||||
|
{
|
||||||
|
if (playList)
|
||||||
|
{
|
||||||
|
playList.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadPlayListFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool added{ !playList };
|
||||||
|
if (!playList)
|
||||||
|
playList = dbSession.create<db::PlayListFile>(_file);
|
||||||
|
|
||||||
|
playList.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
|
playList.modify()->setFileSize(fileInfo->fileSize);
|
||||||
|
if (!_parsedPlayList->name.empty())
|
||||||
|
playList.modify()->setName(_parsedPlayList->name);
|
||||||
|
else
|
||||||
|
playList.modify()->setName(_file.stem().string());
|
||||||
|
playList.modify()->setFiles(_parsedPlayList->files);
|
||||||
|
|
||||||
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
|
playList.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Added playlist file '" << _file.string() << "'");
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "db playlist file = '" << playList->getAbsoluteFilePath().string() << "'");
|
||||||
|
stats.additions++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << _file.string() << "'");
|
||||||
|
stats.updates++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PlayListFileScanner::PlayListFileScanner(db::Db& db)
|
||||||
|
: _db{ db }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return metadata::getSupportedPlayListFileExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlayListFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
||||||
|
{
|
||||||
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
|
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
||||||
|
// Should rarely fail as we are currently iterating it
|
||||||
|
if (!lastWriteTime.isValid())
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.scanOptions.fullScan)
|
||||||
|
{
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, file.file) };
|
||||||
|
if (playList && playList->getLastWriteTime() == lastWriteTime)
|
||||||
|
{
|
||||||
|
stats.skips++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // need to scan
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IFileScanOperation> PlayListFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
||||||
|
{
|
||||||
|
return std::make_unique<PlayListFileScanOperation>(fileToScan, _db);
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 "IFileScanner.hpp"
|
||||||
|
|
||||||
|
namespace lms
|
||||||
|
{
|
||||||
|
namespace db
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
} // namespace lms
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
class PlayListFileScanner : public IFileScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PlayListFileScanner(db::Db& db);
|
||||||
|
~PlayListFileScanner() override = default;
|
||||||
|
PlayListFileScanner(const PlayListFileScanner&) = delete;
|
||||||
|
PlayListFileScanner& operator=(const PlayListFileScanner&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||||
|
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||||
|
|
||||||
|
db::Db& _db;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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 "Utils.hpp"
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner::utils
|
||||||
|
{
|
||||||
|
Wt::WDateTime retrieveFileGetLastWrite(const std::filesystem::path& file)
|
||||||
|
{
|
||||||
|
Wt::WDateTime res;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
res = core::pathUtils::getLastWriteTime(file);
|
||||||
|
}
|
||||||
|
catch (core::LmsException& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot get last write time: " << e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<FileInfo> retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath)
|
||||||
|
{
|
||||||
|
std::optional<FileInfo> res;
|
||||||
|
res.emplace();
|
||||||
|
|
||||||
|
res->lastWriteTime = retrieveFileGetLastWrite(file);
|
||||||
|
if (!res->lastWriteTime.isValid())
|
||||||
|
{
|
||||||
|
res.reset();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
res->relativePath = std::filesystem::relative(file, rootPath, ec);
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot get relative file path for '" << file.string() << "' from '" << rootPath.string() << "': " << ec.message());
|
||||||
|
res.reset();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
res->fileSize = std::filesystem::file_size(file, ec);
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot get file size for '" << file.string() << "': " << ec.message());
|
||||||
|
res.reset();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Directory::pointer getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::MediaLibrary::pointer& mediaLibrary)
|
||||||
|
{
|
||||||
|
db::Directory::pointer directory{ db::Directory::find(session, path) };
|
||||||
|
if (!directory)
|
||||||
|
{
|
||||||
|
db::Directory::pointer parentDirectory;
|
||||||
|
if (path != mediaLibrary->getPath())
|
||||||
|
parentDirectory = getOrCreateDirectory(session, path.parent_path(), mediaLibrary);
|
||||||
|
|
||||||
|
directory = session.create<db::Directory>(path);
|
||||||
|
directory.modify()->setParent(parentDirectory);
|
||||||
|
directory.modify()->setMediaLibrary(mediaLibrary);
|
||||||
|
}
|
||||||
|
// Don't update library if it does not match, will be updated elsewhere
|
||||||
|
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner::utils
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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 <optional>
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Directory;
|
||||||
|
class MediaLibrary;
|
||||||
|
class Session;
|
||||||
|
} // namespace lms::db
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct FileInfo
|
||||||
|
{
|
||||||
|
Wt::WDateTime lastWriteTime;
|
||||||
|
std::filesystem::path relativePath;
|
||||||
|
std::size_t fileSize{};
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace utils
|
||||||
|
{
|
||||||
|
Wt::WDateTime retrieveFileGetLastWrite(const std::filesystem::path& file);
|
||||||
|
std::optional<FileInfo> retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath);
|
||||||
|
|
||||||
|
db::ObjectPtr<db::Directory> getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr<db::MediaLibrary>& mediaLibrary);
|
||||||
|
} // namespace utils
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FileScanQueue.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
|
||||||
|
#include "scanners/IFileScanOperation.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
FileScanQueue::FileScanQueue(std::size_t threadCount, bool& abort)
|
||||||
|
: _scanContextRunner{ _scanIoContext, threadCount, "FileScan" }
|
||||||
|
, _abort{ abort }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileScanQueue::~FileScanQueue() = default;
|
||||||
|
|
||||||
|
void FileScanQueue::pushScanRequest(std::unique_ptr<IFileScanOperation> operation)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
_ongoingScanCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto operationHandler{ [operation = std::move(operation), this]() mutable {
|
||||||
|
if (_abort)
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
_ongoingScanCount -= 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", operation->getName());
|
||||||
|
operation->scan();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
|
||||||
|
_scanResults.emplace_back(std::move(operation));
|
||||||
|
_ongoingScanCount -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_condVar.notify_all();
|
||||||
|
} };
|
||||||
|
|
||||||
|
boost::asio::post(_scanIoContext, std::move(operationHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t FileScanQueue::getResultsCount() const
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
return _scanResults.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t FileScanQueue::popResults(std::vector<std::unique_ptr<IFileScanOperation>>& results, std::size_t maxCount)
|
||||||
|
{
|
||||||
|
results.clear();
|
||||||
|
results.reserve(maxCount);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{ _mutex };
|
||||||
|
|
||||||
|
while (results.size() < maxCount && !_scanResults.empty())
|
||||||
|
{
|
||||||
|
results.push_back(std::move(_scanResults.front()));
|
||||||
|
_scanResults.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileScanQueue::wait(std::size_t maxScanRequestCount)
|
||||||
|
{
|
||||||
|
if (_ongoingScanCount <= maxScanRequestCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "WaitParseResults");
|
||||||
|
|
||||||
|
std::unique_lock lock{ _mutex };
|
||||||
|
_condVar.wait(lock, [=, this] { return _ongoingScanCount <= maxScanRequestCount; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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 <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/IOContextRunner.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class IFileScanOperation;
|
||||||
|
|
||||||
|
class FileScanQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FileScanQueue(std::size_t threadCount, bool& abort);
|
||||||
|
~FileScanQueue();
|
||||||
|
FileScanQueue(const FileScanQueue&) = delete;
|
||||||
|
FileScanQueue& operator=(const FileScanQueue&) = delete;
|
||||||
|
|
||||||
|
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
|
||||||
|
|
||||||
|
void pushScanRequest(std::unique_ptr<IFileScanOperation> operation);
|
||||||
|
|
||||||
|
std::size_t getResultsCount() const;
|
||||||
|
size_t popResults(std::vector<std::unique_ptr<IFileScanOperation>>& results, std::size_t maxCount);
|
||||||
|
|
||||||
|
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::asio::io_context _scanIoContext;
|
||||||
|
core::IOContextRunner _scanContextRunner;
|
||||||
|
|
||||||
|
mutable std::mutex _mutex;
|
||||||
|
std::atomic<std::size_t> _ongoingScanCount{};
|
||||||
|
std::deque<std::unique_ptr<IFileScanOperation>> _scanResults;
|
||||||
|
std::condition_variable _condVar;
|
||||||
|
bool& _abort;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
+2
-9
@@ -20,8 +20,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/LiteralString.hpp"
|
#include "core/LiteralString.hpp"
|
||||||
#include "services/scanner/ScannerOptions.hpp"
|
|
||||||
#include "services/scanner/ScannerStats.hpp"
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -32,13 +32,6 @@ namespace lms::scanner
|
|||||||
|
|
||||||
virtual ScanStep getStep() const = 0;
|
virtual ScanStep getStep() const = 0;
|
||||||
virtual core::LiteralString getStepName() const = 0;
|
virtual core::LiteralString getStepName() const = 0;
|
||||||
|
|
||||||
struct ScanContext
|
|
||||||
{
|
|
||||||
ScanOptions scanOptions;
|
|
||||||
ScanStats stats;
|
|
||||||
ScanStepStats currentStepStats;
|
|
||||||
};
|
|
||||||
virtual void process(ScanContext& context) = 0;
|
virtual void process(ScanContext& context) = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
+2
-2
@@ -59,9 +59,9 @@ namespace lms::scanner
|
|||||||
assert(!lyrics->getFileStem().empty());
|
assert(!lyrics->getFileStem().empty());
|
||||||
|
|
||||||
params.setDirectory(lyrics->getDirectory()->getId());
|
params.setDirectory(lyrics->getDirectory()->getId());
|
||||||
params.setStem(stem);
|
params.setFileStem(stem);
|
||||||
|
|
||||||
db::Track::find(session, params, [&](const db::Track::pointer track) {
|
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||||
if (matchingTrack)
|
if (matchingTrack)
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "External lyrics '" << lyrics->getAbsoluteFilePath() << "' already matched with '" << matchingTrack->getAbsoluteFilePath() << "', replaced by '" << track->getAbsoluteFilePath() << "'");
|
LMS_LOG(DBUPDATER, DEBUG, "External lyrics '" << lyrics->getAbsoluteFilePath() << "' already matched with '" << matchingTrack->getAbsoluteFilePath() << "', replaced by '" << track->getAbsoluteFilePath() << "'");
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ScanStepAssociatePlayListTracks.hpp"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 20 };
|
||||||
|
constexpr std::size_t writeBatchSize{ 5 };
|
||||||
|
|
||||||
|
struct PlayListFileAssociation
|
||||||
|
{
|
||||||
|
db::PlayListFileId playListFileIdId;
|
||||||
|
std::vector<db::TrackId> trackIds;
|
||||||
|
};
|
||||||
|
using PlayListFileAssociationContainer = std::deque<PlayListFileAssociation>;
|
||||||
|
|
||||||
|
struct SearchPlayListFileContext
|
||||||
|
{
|
||||||
|
db::Session& session;
|
||||||
|
db::PlayListFileId lastRetrievedPlayListFileId;
|
||||||
|
std::size_t processedPlayListFileCount{};
|
||||||
|
};
|
||||||
|
|
||||||
|
db::Track::pointer getMatchingTrack(db::Session& session, const std::filesystem::path& filePath, const db::Directory::pointer& playListDirectory)
|
||||||
|
{
|
||||||
|
db::Track::pointer matchingTrack;
|
||||||
|
if (filePath.is_absolute())
|
||||||
|
{
|
||||||
|
matchingTrack = db::Track::findByPath(session, filePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::filesystem::path absolutePath{ playListDirectory->getAbsolutePath() / filePath };
|
||||||
|
matchingTrack = db::Track::findByPath(session, absolutePath.lexically_normal());
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span<const db::TrackId> trackIds, const db::TrackList::pointer& trackList)
|
||||||
|
{
|
||||||
|
if (trackList->getName() != name)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
db::TrackListEntry::FindParameters params;
|
||||||
|
params.setTrackList(trackList->getId());
|
||||||
|
|
||||||
|
bool needUpdate{};
|
||||||
|
std::size_t currentIndex{};
|
||||||
|
db::TrackListEntry::find(session, params, [&](const db::TrackListEntry::pointer& entry) {
|
||||||
|
if (currentIndex > trackIds.size() || trackIds[currentIndex] != entry->getTrackId())
|
||||||
|
needUpdate = true;
|
||||||
|
|
||||||
|
currentIndex += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentIndex != trackIds.size())
|
||||||
|
needUpdate = true;
|
||||||
|
|
||||||
|
return needUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchNextPlayListFilesToUpdate(SearchPlayListFileContext& searchContext, PlayListFileAssociationContainer& playListFileAssociations)
|
||||||
|
{
|
||||||
|
const db::PlayListFileId playListFileIdId{ searchContext.lastRetrievedPlayListFileId };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
|
db::PlayListFile::find(searchContext.session, searchContext.lastRetrievedPlayListFileId, readBatchSize, [&](const db::PlayListFile::pointer& playListFile) {
|
||||||
|
PlayListFileAssociation playListAssociation;
|
||||||
|
|
||||||
|
playListAssociation.playListFileIdId = playListFile->getId();
|
||||||
|
|
||||||
|
const auto files{ playListFile->getFiles() };
|
||||||
|
for (const std::filesystem::path& file : files)
|
||||||
|
{
|
||||||
|
// TODO optim: no need to fetch the whole track
|
||||||
|
db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) };
|
||||||
|
if (track)
|
||||||
|
playListAssociation.trackIds.push_back(track->getId());
|
||||||
|
else
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Track '" << file.string() << "' not found in playlist '" << playListFile->getAbsoluteFilePath().string() << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needUpdate{ true };
|
||||||
|
if (const db::TrackList::pointer trackList{ playListFile->getTrackList() })
|
||||||
|
needUpdate = trackListNeedsUpdate(searchContext.session, playListFile->getName(), playListAssociation.trackIds, trackList);
|
||||||
|
|
||||||
|
if (needUpdate)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updating PlayList '" << playListFile->getAbsoluteFilePath().string() << "' (" << playListAssociation.trackIds.size() << " files)");
|
||||||
|
playListFileAssociations.emplace_back(std::move(playListAssociation));
|
||||||
|
}
|
||||||
|
searchContext.processedPlayListFileCount++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return playListFileIdId != searchContext.lastRetrievedPlayListFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePlayListFile(db::Session& session, const PlayListFileAssociation& playListFileAssociation)
|
||||||
|
{
|
||||||
|
db::PlayListFile::pointer playListFile{ db::PlayListFile::find(session, playListFileAssociation.playListFileIdId) };
|
||||||
|
assert(playListFile);
|
||||||
|
|
||||||
|
db::TrackList::pointer trackList{ playListFile->getTrackList() };
|
||||||
|
if (!trackList)
|
||||||
|
{
|
||||||
|
trackList = session.create<db::TrackList>(playListFile->getName(), db::TrackListType::PlayList);
|
||||||
|
playListFile.modify()->setTrackList(trackList);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackList.modify()->setVisibility(db::TrackList::Visibility::Public);
|
||||||
|
trackList.modify()->setLastModifiedDateTime(playListFile->getLastWriteTime());
|
||||||
|
trackList.modify()->setName(playListFile->getName());
|
||||||
|
|
||||||
|
trackList.modify()->clear();
|
||||||
|
for (const db::TrackId trackId : playListFileAssociation.trackIds)
|
||||||
|
{
|
||||||
|
if (db::Track::pointer track{ db::Track::find(session, trackId) })
|
||||||
|
session.create<db::TrackListEntry>(track, trackList, playListFile->getLastWriteTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePlayListFiles(db::Session& session, PlayListFileAssociationContainer& playListFileAssociations)
|
||||||
|
{
|
||||||
|
while (!playListFileAssociations.empty())
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
for (std::size_t i{}; !playListFileAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
|
{
|
||||||
|
updatePlayListFile(session, playListFileAssociations.front());
|
||||||
|
playListFileAssociations.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ScanStepAssociatePlayListTracks::process(ScanContext& context)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (context.stats.nbChanges() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
context.currentStepStats.totalElems = db::PlayListFile::getCount(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPlayListFileContext searchContext{
|
||||||
|
.session = session,
|
||||||
|
.lastRetrievedPlayListFileId = {},
|
||||||
|
};
|
||||||
|
|
||||||
|
PlayListFileAssociationContainer playListFileAssociations;
|
||||||
|
while (fetchNextPlayListFilesToUpdate(searchContext, playListFileAssociations))
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
updatePlayListFiles(session, playListFileAssociations);
|
||||||
|
context.currentStepStats.processedElems = searchContext.processedPlayListFileCount;
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ScanStepBase.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanStepAssociatePlayListTracks : public ScanStepBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ScanStepBase::ScanStepBase;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScanStep getStep() const override { return ScanStep::AssociatePlayListTracks; }
|
||||||
|
core::LiteralString getStepName() const override { return "Associate playlist tracks"; }
|
||||||
|
void process(ScanContext& context) override;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
+11
-4
@@ -20,11 +20,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <span>
|
||||||
#include "services/scanner/ScannerStats.hpp"
|
#include <vector>
|
||||||
|
|
||||||
#include "IScanStep.hpp"
|
#include "IScanStep.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
@@ -33,10 +32,13 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
class IFileScanner;
|
||||||
|
struct ScannerSettings;
|
||||||
|
struct ScanStepStats;
|
||||||
|
|
||||||
class ScanStepBase : public IScanStep
|
class ScanStepBase : public IScanStep
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static inline const std::filesystem::path excludeDirFileName{ ".lmsignore" };
|
|
||||||
using ProgressCallback = std::function<void(const ScanStepStats& stats)>;
|
using ProgressCallback = std::function<void(const ScanStepStats& stats)>;
|
||||||
|
|
||||||
struct InitParams
|
struct InitParams
|
||||||
@@ -45,21 +47,26 @@ namespace lms::scanner
|
|||||||
ProgressCallback progressCallback;
|
ProgressCallback progressCallback;
|
||||||
bool& abortScan;
|
bool& abortScan;
|
||||||
db::Db& db;
|
db::Db& db;
|
||||||
|
std::span<IFileScanner*> fileScanners;
|
||||||
};
|
};
|
||||||
ScanStepBase(InitParams& initParams)
|
ScanStepBase(InitParams& initParams)
|
||||||
: _settings{ initParams.settings }
|
: _settings{ initParams.settings }
|
||||||
, _progressCallback{ initParams.progressCallback }
|
, _progressCallback{ initParams.progressCallback }
|
||||||
, _abortScan{ initParams.abortScan }
|
, _abortScan{ initParams.abortScan }
|
||||||
, _db{ initParams.db }
|
, _db{ initParams.db }
|
||||||
|
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
~ScanStepBase() override = default;
|
~ScanStepBase() override = default;
|
||||||
|
ScanStepBase(const ScanStepBase&) = delete;
|
||||||
|
ScanStepBase& operator=(const ScanStepBase&) = delete;
|
||||||
|
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
ProgressCallback _progressCallback;
|
ProgressCallback _progressCallback;
|
||||||
bool& _abortScan;
|
bool& _abortScan;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
std::vector<IFileScanner*> _fileScanners;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
+18
-4
@@ -19,13 +19,18 @@
|
|||||||
|
|
||||||
#include "ScanStepCheckForRemovedFiles.hpp"
|
#include "ScanStepCheckForRemovedFiles.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
|
#include "database/PlayListFile.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackLyrics.hpp"
|
#include "database/TrackLyrics.hpp"
|
||||||
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -47,12 +52,21 @@ namespace lms::scanner
|
|||||||
context.currentStepStats.totalElems += db::Track::getCount(session);
|
context.currentStepStats.totalElems += db::Track::getCount(session);
|
||||||
context.currentStepStats.totalElems += db::Image::getCount(session);
|
context.currentStepStats.totalElems += db::Image::getCount(session);
|
||||||
context.currentStepStats.totalElems += db::TrackLyrics::getExternalLyricsCount(session);
|
context.currentStepStats.totalElems += db::TrackLyrics::getExternalLyricsCount(session);
|
||||||
|
context.currentStepStats.totalElems += db::PlayListFile::getCount(session);
|
||||||
}
|
}
|
||||||
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
|
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
|
||||||
|
|
||||||
checkForRemovedFiles<db::Track>(context, _settings.supportedAudioFileExtensions);
|
std::vector<std::filesystem::path> supportedFileExtensions;
|
||||||
checkForRemovedFiles<db::Image>(context, _settings.supportedImageFileExtensions);
|
for (IFileScanner* scanner : _fileScanners)
|
||||||
checkForRemovedFiles<db::TrackLyrics>(context, _settings.supportedLyricsFileExtensions);
|
{
|
||||||
|
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
||||||
|
supportedFileExtensions.emplace_back(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForRemovedFiles<db::Track>(context, supportedFileExtensions);
|
||||||
|
checkForRemovedFiles<db::Image>(context, supportedFileExtensions);
|
||||||
|
checkForRemovedFiles<db::TrackLyrics>(context, supportedFileExtensions);
|
||||||
|
checkForRemovedFiles<db::PlayListFile>(context, supportedFileExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename Object>
|
template<typename Object>
|
||||||
@@ -124,7 +138,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
||||||
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
|
[&](const MediaLibraryInfo& libraryInfo) {
|
||||||
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
|
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
+12
-9
@@ -22,21 +22,24 @@
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
|
|
||||||
|
#include "MediaLibraryInfo.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
void ScanStepDiscoverFiles::process(ScanContext& context)
|
void ScanStepDiscoverFiles::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
context.stats.totalFileCount = 0;
|
context.stats.totalFileCount = 0;
|
||||||
|
|
||||||
std::vector<std::filesystem::path> supportedExtensions;
|
std::vector<std::filesystem::path> supportedFileExtensions;
|
||||||
for (const auto& extension : _settings.supportedAudioFileExtensions)
|
for (IFileScanner* scanner : _fileScanners)
|
||||||
supportedExtensions.emplace_back(extension);
|
{
|
||||||
for (const auto& extension : _settings.supportedImageFileExtensions)
|
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
||||||
supportedExtensions.emplace_back(extension);
|
supportedFileExtensions.emplace_back(extension);
|
||||||
for (const auto& extension : _settings.supportedLyricsFileExtensions)
|
}
|
||||||
supportedExtensions.emplace_back(extension);
|
|
||||||
|
|
||||||
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
{
|
{
|
||||||
std::size_t currentDirectoryProcessElemsCount{};
|
std::size_t currentDirectoryProcessElemsCount{};
|
||||||
core::pathUtils::exploreFilesRecursive(
|
core::pathUtils::exploreFilesRecursive(
|
||||||
@@ -44,7 +47,7 @@ namespace lms::scanner
|
|||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!ec && core::pathUtils::hasFileAnyExtension(path, supportedExtensions))
|
if (!ec && core::pathUtils::hasFileAnyExtension(path, supportedFileExtensions))
|
||||||
{
|
{
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
currentDirectoryProcessElemsCount++;
|
currentDirectoryProcessElemsCount++;
|
||||||
+1
-1
@@ -29,7 +29,7 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
ScanStats& stats{ context.stats };
|
||||||
|
|
||||||
if (context.scanOptions.forceOptimize || (stats.nbChanges() > (stats.nbFiles() / 5)))
|
if (context.scanOptions.forceOptimize || (stats.nbChanges() > (stats.nbFiles() / 10)))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO, "Database analyze started");
|
LMS_LOG(DBUPDATER, INFO, "Database analyze started");
|
||||||
|
|
||||||
+1
-1
@@ -84,7 +84,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedEntries(ScanStepRemoveOrphanedDbEntries::ScanContext& context)
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedEntries(ScanContext& context)
|
||||||
{
|
{
|
||||||
constexpr std::size_t batchSize = 100;
|
constexpr std::size_t batchSize = 100;
|
||||||
|
|
||||||
+1
-1
@@ -42,6 +42,6 @@ namespace lms::scanner
|
|||||||
void removeOrphanedDirectories(ScanContext& context);
|
void removeOrphanedDirectories(ScanContext& context);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void removeOrphanedEntries(ScanStepRemoveOrphanedDbEntries::ScanContext& context);
|
void removeOrphanedEntries(ScanContext& context);
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ScanStepScanFiles.hpp"
|
||||||
|
|
||||||
|
#include "FileScanQueue.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
|
#include "core/IConfig.hpp"
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "scanners/IFileScanOperation.hpp"
|
||||||
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
using namespace db;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::size_t getScanMetaDataThreadCount()
|
||||||
|
{
|
||||||
|
std::size_t threadCount{ core::Service<core::IConfig>::get()->getULong("scanner-metadata-thread-count", 0) };
|
||||||
|
|
||||||
|
if (threadCount == 0)
|
||||||
|
threadCount = std::max<std::size_t>(std::thread::hardware_concurrency() / 2, 1);
|
||||||
|
|
||||||
|
return threadCount;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
||||||
|
: ScanStepBase{ initParams }
|
||||||
|
, _fileScanQueue{ getScanMetaDataThreadCount(), _abortScan }
|
||||||
|
{
|
||||||
|
for (IFileScanner* scanner : _fileScanners)
|
||||||
|
{
|
||||||
|
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
||||||
|
{
|
||||||
|
[[maybe_unused]] auto [it, inserted]{ _scannerByExtension.emplace(extension, scanner) };
|
||||||
|
assert(inserted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::process(ScanContext& context)
|
||||||
|
{
|
||||||
|
context.currentStepStats.totalElems = context.stats.totalFileCount;
|
||||||
|
|
||||||
|
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
|
process(context, mediaLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::process(ScanContext& context, const MediaLibraryInfo& mediaLibrary)
|
||||||
|
{
|
||||||
|
const std::size_t scanQueueMaxScanRequestCount{ 100 * _fileScanQueue.getThreadCount() };
|
||||||
|
const std::size_t processFileResultsBatchSize{ 5 };
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<IFileScanOperation>> scanOperations;
|
||||||
|
|
||||||
|
core::pathUtils::exploreFilesRecursive(
|
||||||
|
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
|
||||||
|
|
||||||
|
if (_abortScan)
|
||||||
|
return false; // stop iterating
|
||||||
|
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Cannot scan file '" << path.string() << "': " << ec.message());
|
||||||
|
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto itScanner{ _scannerByExtension.find(core::stringUtils::stringToLower(path.extension().string())) };
|
||||||
|
if (itScanner != std::cend(_scannerByExtension))
|
||||||
|
{
|
||||||
|
IFileScanner& scanner{ *itScanner->second };
|
||||||
|
|
||||||
|
FileToScan fileToScan{ .file = path, .mediaLibrary = mediaLibrary };
|
||||||
|
if (scanner.needsScan(context, fileToScan))
|
||||||
|
{
|
||||||
|
auto scanOperation{ scanner.createScanOperation(fileToScan) };
|
||||||
|
_fileScanQueue.pushScanRequest(std::move(scanOperation));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.currentStepStats.processedElems++;
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2))
|
||||||
|
{
|
||||||
|
_fileScanQueue.popResults(scanOperations, processFileResultsBatchSize);
|
||||||
|
processFileScanResults(context, scanOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileScanQueue.wait(scanQueueMaxScanRequestCount);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
&excludeDirFileName);
|
||||||
|
|
||||||
|
_fileScanQueue.wait();
|
||||||
|
|
||||||
|
while (!_abortScan && _fileScanQueue.popResults(scanOperations, processFileResultsBatchSize) > 0)
|
||||||
|
processFileScanResults(context, scanOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepScanFiles::processFileScanResults(ScanContext& context, std::span<std::unique_ptr<IFileScanOperation>> scanOperations)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults");
|
||||||
|
|
||||||
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ dbSession.createWriteTransaction() };
|
||||||
|
|
||||||
|
for (auto& scanOperation : scanOperations)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
scanOperation->processResult(context);
|
||||||
|
context.stats.scans++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
+6
-17
@@ -21,16 +21,15 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "metadata/IParser.hpp"
|
|
||||||
|
|
||||||
#include "FileScanQueue.hpp"
|
#include "FileScanQueue.hpp"
|
||||||
#include "ScanStepBase.hpp"
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
class IFileScanner;
|
||||||
|
class MediaLibraryInfo;
|
||||||
|
|
||||||
class ScanStepScanFiles : public ScanStepBase
|
class ScanStepScanFiles : public ScanStepBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -40,20 +39,10 @@ namespace lms::scanner
|
|||||||
ScanStep getStep() const override { return ScanStep::ScanFiles; }
|
ScanStep getStep() const override { return ScanStep::ScanFiles; }
|
||||||
core::LiteralString getStepName() const override { return "Scan files"; }
|
core::LiteralString getStepName() const override { return "Scan files"; }
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
void process(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary);
|
void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary);
|
||||||
|
void processFileScanResults(ScanContext& context, std::span<std::unique_ptr<IFileScanOperation>> scanOperations);
|
||||||
bool checkAudioFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
|
||||||
bool checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file);
|
|
||||||
bool checkLyricsFileNeedScan(ScanContext& context, const std::filesystem::path& file);
|
|
||||||
|
|
||||||
void processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
|
||||||
void processAudioFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Track* trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
|
||||||
void processImageFileScanData(ScanContext& context, const std::filesystem::path& file, const ImageInfo* imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
|
||||||
void processLyricsFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Lyrics* lyrics, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
|
||||||
|
|
||||||
std::unique_ptr<metadata::IParser> _metadataParser;
|
|
||||||
const std::vector<std::string> _extraTagsToParse;
|
|
||||||
|
|
||||||
FileScanQueue _fileScanQueue;
|
FileScanQueue _fileScanQueue;
|
||||||
|
std::unordered_map<std::filesystem::path, IFileScanner*> _scannerByExtension;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
+5
-2
@@ -24,6 +24,9 @@
|
|||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
#include "MediaLibraryInfo.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -34,7 +37,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
void ScanStepUpdateLibraryFields::processDirectories(ScanContext& context)
|
void ScanStepUpdateLibraryFields::processDirectories(ScanContext& context)
|
||||||
{
|
{
|
||||||
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
break;
|
break;
|
||||||
@@ -43,7 +46,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepUpdateLibraryFields::processDirectory(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary)
|
void ScanStepUpdateLibraryFields::processDirectory(ScanContext& context, const MediaLibraryInfo& mediaLibrary)
|
||||||
{
|
{
|
||||||
db::Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
+3
-1
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
class MediaLibraryInfo;
|
||||||
|
|
||||||
class ScanStepUpdateLibraryFields : public ScanStepBase
|
class ScanStepUpdateLibraryFields : public ScanStepBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -34,6 +36,6 @@ namespace lms::scanner
|
|||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
void processDirectories(ScanContext& context);
|
void processDirectories(ScanContext& context);
|
||||||
void processDirectory(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary);
|
void processDirectory(ScanContext& context, const MediaLibraryInfo& mediaLibrary);
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -30,12 +30,13 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
enum class ScanErrorType
|
enum class ScanErrorType
|
||||||
{
|
{
|
||||||
CannotReadFile, // cannot read file
|
CannotReadFile,
|
||||||
CannotReadAudioFile, // cannot parse audio file
|
CannotReadAudioFile,
|
||||||
CannotReadImageFile, // cannot parse image file
|
CannotReadImageFile,
|
||||||
CannotReadLyricsFile, // cannot parse lyrics file
|
CannotReadLyricsFile,
|
||||||
NoAudioTrack, // no audio track found
|
CannotReadPlayListFile,
|
||||||
BadDuration, // bad duration
|
NoAudioTrack,
|
||||||
|
BadDuration,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class DuplicateReason
|
enum class DuplicateReason
|
||||||
@@ -64,6 +65,7 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
AssociateArtistImages,
|
AssociateArtistImages,
|
||||||
AssociateExternalLyrics,
|
AssociateExternalLyrics,
|
||||||
|
AssociatePlayListTracks,
|
||||||
AssociateReleaseImages,
|
AssociateReleaseImages,
|
||||||
CheckForDuplicatedFiles,
|
CheckForDuplicatedFiles,
|
||||||
CheckForRemovedFiles,
|
CheckForRemovedFiles,
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <list>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
@@ -37,24 +37,22 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
TLSMonotonicMemoryResource()
|
TLSMonotonicMemoryResource()
|
||||||
{
|
{
|
||||||
allocateNewBlock();
|
allocateNewBlock(defaultBlockSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::byte* allocate(std::size_t byteCount, std::size_t alignment)
|
[[nodiscard]] std::byte* allocate(std::size_t byteCount, std::size_t alignment)
|
||||||
{
|
{
|
||||||
|
if (byteCount == 0)
|
||||||
|
byteCount = 1;
|
||||||
|
|
||||||
std::byte* currentAddrAligned{ computeAlignedAddr(_currentAddr, alignment) };
|
std::byte* currentAddrAligned{ computeAlignedAddr(_currentAddr, alignment) };
|
||||||
|
|
||||||
if (currentAddrAligned + byteCount > &_currentBlock->back() + 1)
|
if (!_currentBlock->fitsInBlock(currentAddrAligned, byteCount))
|
||||||
{
|
{
|
||||||
allocateNewBlock();
|
allocateNewBlock(std::max(defaultBlockSize, byteCount + alignment /* worst case */));
|
||||||
currentAddrAligned = computeAlignedAddr(_currentAddr, alignment);
|
currentAddrAligned = computeAlignedAddr(_currentAddr, alignment);
|
||||||
}
|
}
|
||||||
|
assert(_currentBlock->fitsInBlock(currentAddrAligned, byteCount));
|
||||||
// Requested too many bytes for blockSize!
|
|
||||||
if (currentAddrAligned + byteCount > &_currentBlock->back() + 1)
|
|
||||||
throw std::bad_alloc{};
|
|
||||||
|
|
||||||
assert(currentAddrAligned >= &_currentBlock->front());
|
|
||||||
|
|
||||||
std::byte* res{ currentAddrAligned };
|
std::byte* res{ currentAddrAligned };
|
||||||
_currentAddr = currentAddrAligned + byteCount;
|
_currentAddr = currentAddrAligned + byteCount;
|
||||||
@@ -62,26 +60,34 @@ namespace lms::api::subsonic
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void deallocate(std::byte*)
|
void deallocate(std::byte* /* ptr */)
|
||||||
{
|
{
|
||||||
// nothing to do!
|
// nothing to do!
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset()
|
void reset()
|
||||||
{
|
{
|
||||||
// always keep at least one block
|
// First: remove blocks that are not default-sized
|
||||||
|
std::erase_if(_blocks, [](const Block& _block) { return _block.size != defaultBlockSize; });
|
||||||
|
|
||||||
|
// Then always keep at least one block
|
||||||
if (_blocks.size() > 1)
|
if (_blocks.size() > 1)
|
||||||
_blocks.erase(std::next(std::cbegin(_blocks), 1), std::cend(_blocks));
|
_blocks.erase(std::next(std::cbegin(_blocks), 1), std::cend(_blocks));
|
||||||
|
|
||||||
_currentBlock = &_blocks.front();
|
_currentBlock = &_blocks.front();
|
||||||
_currentAddr = _currentBlock->data();
|
_currentAddr = _currentBlock->data.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void allocateNewBlock()
|
void allocateNewBlock(std::size_t size)
|
||||||
{
|
{
|
||||||
_currentBlock = &_blocks.emplace_back();
|
Block block{
|
||||||
_currentAddr = _currentBlock->data();
|
.data = std::make_unique_for_overwrite<std::byte[]>(size),
|
||||||
|
.size = size,
|
||||||
|
};
|
||||||
|
|
||||||
|
_currentAddr = block.data.get();
|
||||||
|
_currentBlock = &_blocks.emplace_back(std::move(block));
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::byte* computeAlignedAddr(std::byte* addr, std::size_t alignment) noexcept
|
static std::byte* computeAlignedAddr(std::byte* addr, std::size_t alignment) noexcept
|
||||||
@@ -93,11 +99,20 @@ namespace lms::api::subsonic
|
|||||||
return reinterpret_cast<std::byte*>(addr + adjustment);
|
return reinterpret_cast<std::byte*>(addr + adjustment);
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr std::size_t blockSize{ static_cast<std::size_t>(1 * 1024 * 1024) };
|
static constexpr std::size_t defaultBlockSize{ static_cast<std::size_t>(1 * 1024 * 1024) };
|
||||||
using BlockType = std::array<std::byte, blockSize>;
|
struct Block
|
||||||
|
{
|
||||||
|
std::unique_ptr<std::byte[]> data;
|
||||||
|
std::size_t size;
|
||||||
|
|
||||||
std::list<BlockType> _blocks;
|
bool fitsInBlock(std::byte* addr, std::size_t rangeSize) const
|
||||||
BlockType* _currentBlock{};
|
{
|
||||||
|
return data && rangeSize && addr >= data.get() && addr + rangeSize <= data.get() + size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Block> _blocks;
|
||||||
|
Block* _currentBlock{};
|
||||||
std::byte* _currentAddr{};
|
std::byte* _currentAddr{};
|
||||||
};
|
};
|
||||||
} // namespace lms::api::subsonic
|
} // namespace lms::api::subsonic
|
||||||
@@ -40,15 +40,31 @@ namespace lms::api::subsonic
|
|||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node& playlistsNode{ response.createNode("playlists") };
|
Response::Node& playlistsNode{ response.createNode("playlists") };
|
||||||
|
|
||||||
TrackList::FindParameters params;
|
auto addTrackList{ [&](const db::TrackList::pointer& trackList) {
|
||||||
params.setUser(context.user->getId());
|
|
||||||
params.setType(TrackListType::Playlist);
|
|
||||||
|
|
||||||
auto tracklistIds{ TrackList::find(context.dbSession, params) };
|
|
||||||
for (const TrackListId trackListId : tracklistIds.results)
|
|
||||||
{
|
|
||||||
const TrackList::pointer trackList{ TrackList::find(context.dbSession, trackListId) };
|
|
||||||
playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession));
|
playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession));
|
||||||
|
} };
|
||||||
|
|
||||||
|
// First add user's playlists
|
||||||
|
{
|
||||||
|
TrackList::FindParameters params;
|
||||||
|
params.setUser(context.user->getId());
|
||||||
|
params.setType(TrackListType::PlayList);
|
||||||
|
|
||||||
|
db::TrackList::find(context.dbSession, params, [&](const db::TrackList::pointer& trackList) {
|
||||||
|
addTrackList(trackList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add others public playlists
|
||||||
|
{
|
||||||
|
TrackList::FindParameters params;
|
||||||
|
params.setVisibility(TrackList::Visibility::Public);
|
||||||
|
params.setType(TrackListType::PlayList);
|
||||||
|
|
||||||
|
db::TrackList::find(context.dbSession, params, [&](const db::TrackList::pointer& trackList) {
|
||||||
|
if (trackList->getUserId() != context.user->getId()) // skip already reported
|
||||||
|
addTrackList(trackList);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -96,17 +112,22 @@ namespace lms::api::subsonic
|
|||||||
tracklist = TrackList::find(context.dbSession, *id);
|
tracklist = TrackList::find(context.dbSession, *id);
|
||||||
if (!tracklist
|
if (!tracklist
|
||||||
|| tracklist->getUser() != context.user
|
|| tracklist->getUser() != context.user
|
||||||
|| tracklist->getType() != TrackListType::Playlist)
|
|| tracklist->getType() != TrackListType::PlayList)
|
||||||
{
|
{
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name)
|
if (name)
|
||||||
tracklist.modify()->setName(*name);
|
tracklist.modify()->setName(*name);
|
||||||
|
|
||||||
|
tracklist.modify()->clear();
|
||||||
|
tracklist.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, context.user);
|
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::PlayList);
|
||||||
|
tracklist.modify()->setUser(context.user);
|
||||||
|
tracklist.modify()->setVisibility(TrackList::Visibility::Private);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const TrackId trackId : trackIds)
|
for (const TrackId trackId : trackIds)
|
||||||
@@ -147,7 +168,7 @@ namespace lms::api::subsonic
|
|||||||
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||||
if (!tracklist
|
if (!tracklist
|
||||||
|| tracklist->getUser() != context.user
|
|| tracklist->getUser() != context.user
|
||||||
|| tracklist->getType() != TrackListType::Playlist)
|
|| tracklist->getType() != TrackListType::PlayList)
|
||||||
{
|
{
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
}
|
}
|
||||||
@@ -155,8 +176,8 @@ namespace lms::api::subsonic
|
|||||||
if (name)
|
if (name)
|
||||||
tracklist.modify()->setName(*name);
|
tracklist.modify()->setName(*name);
|
||||||
|
|
||||||
if (isPublic)
|
tracklist.modify()->setVisibility(isPublic ? db::TrackList::Visibility::Public : db::TrackList::Visibility::Private);
|
||||||
tracklist.modify()->setIsPublic(*isPublic);
|
tracklist.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime());
|
||||||
|
|
||||||
{
|
{
|
||||||
// Remove from end to make indexes stable
|
// Remove from end to make indexes stable
|
||||||
@@ -192,7 +213,7 @@ namespace lms::api::subsonic
|
|||||||
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||||
if (!tracklist
|
if (!tracklist
|
||||||
|| tracklist->getUser() != context.user
|
|| tracklist->getUser() != context.user
|
||||||
|| tracklist->getType() != TrackListType::Playlist)
|
|| tracklist->getType() != TrackListType::PlayList)
|
||||||
{
|
{
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "Playlist.hpp"
|
#include "Playlist.hpp"
|
||||||
|
|
||||||
|
#include "core/String.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackList.hpp"
|
#include "database/TrackList.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
@@ -28,11 +29,7 @@
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
using namespace db;
|
Response::Node createPlaylistNode(const db::TrackList::pointer& tracklist, db::Session& session)
|
||||||
|
|
||||||
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
|
||||||
|
|
||||||
Response::Node createPlaylistNode(const TrackList::pointer& tracklist, Session& session)
|
|
||||||
{
|
{
|
||||||
Response::Node playlistNode;
|
Response::Node playlistNode;
|
||||||
|
|
||||||
@@ -40,15 +37,17 @@ namespace lms::api::subsonic
|
|||||||
playlistNode.setAttribute("name", tracklist->getName());
|
playlistNode.setAttribute("name", tracklist->getName());
|
||||||
playlistNode.setAttribute("songCount", tracklist->getCount());
|
playlistNode.setAttribute("songCount", tracklist->getCount());
|
||||||
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
||||||
playlistNode.setAttribute("public", tracklist->isPublic());
|
playlistNode.setAttribute("public", tracklist->getVisibility() == db::TrackList::Visibility::Public);
|
||||||
playlistNode.setAttribute("created", reportedDummyDate);
|
playlistNode.setAttribute("changed", core::stringUtils::toISO8601String(tracklist->getLastModifiedDateTime()));
|
||||||
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
playlistNode.setAttribute("created", core::stringUtils::toISO8601String(tracklist->getCreationDateTime()));
|
||||||
|
if (const db::User::pointer user{ tracklist->getUser() })
|
||||||
|
playlistNode.setAttribute("owner", user->getLoginName());
|
||||||
|
|
||||||
db::Track::FindParameters params;
|
db::Track::FindParameters params;
|
||||||
params.setTrackList(tracklist->getId());
|
params.setTrackList(tracklist->getId());
|
||||||
params.setHasEmbeddedImage(true);
|
params.setHasEmbeddedImage(true);
|
||||||
params.setRange(db::Range{ 0, 1 });
|
params.setRange(db::Range{ 0, 1 });
|
||||||
params.setSortMethod(TrackSortMethod::TrackList);
|
params.setSortMethod(db::TrackSortMethod::TrackList);
|
||||||
|
|
||||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||||
const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() };
|
const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() };
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ namespace lms::ui
|
|||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
TrackList::FindParameters params;
|
TrackList::FindParameters params;
|
||||||
params.setType(TrackListType::Playlist);
|
params.setType(TrackListType::PlayList);
|
||||||
params.setUser(LmsApp->getUserId());
|
params.setUser(LmsApp->getUserId());
|
||||||
params.setSortMethod(TrackListSortMethod::Name);
|
params.setSortMethod(TrackListSortMethod::Name);
|
||||||
|
|
||||||
@@ -327,12 +327,18 @@ namespace lms::ui
|
|||||||
static const std::string queueName{ "__queued_tracks__" };
|
static const std::string queueName{ "__queued_tracks__" };
|
||||||
queue = db::TrackList::find(LmsApp->getDbSession(), queueName, db::TrackListType::Internal, LmsApp->getUserId());
|
queue = db::TrackList::find(LmsApp->getDbSession(), queueName, db::TrackListType::Internal, LmsApp->getUserId());
|
||||||
if (!queue)
|
if (!queue)
|
||||||
queue = LmsApp->getDbSession().create<db::TrackList>(queueName, db::TrackListType::Internal, false, LmsApp->getUser());
|
{
|
||||||
|
queue = LmsApp->getDbSession().create<db::TrackList>(queueName, db::TrackListType::Internal);
|
||||||
|
queue.modify()->setVisibility(db::TrackList::Visibility::Private);
|
||||||
|
queue.modify()->setUser(LmsApp->getUser());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
static const std::string queueName{ "__temp_queue__" };
|
static const std::string queueName{ "__temp_queue__" };
|
||||||
queue = LmsApp->getDbSession().create<db::TrackList>(queueName, db::TrackListType::Internal, false, LmsApp->getUser());
|
queue = LmsApp->getDbSession().create<db::TrackList>(queueName, db::TrackListType::Internal);
|
||||||
|
queue.modify()->setVisibility(db::TrackList::Visibility::Private);
|
||||||
|
queue.modify()->setUser(LmsApp->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
_queueId = queue->getId();
|
_queueId = queue->getId();
|
||||||
@@ -735,7 +741,9 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
Session& session{ LmsApp->getDbSession() };
|
Session& session{ LmsApp->getDbSession() };
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
TrackList::pointer trackList{ session.create<TrackList>(name.toUTF8(), TrackListType::Playlist, false, LmsApp->getUser()) };
|
TrackList::pointer trackList{ session.create<TrackList>(name.toUTF8(), TrackListType::PlayList) };
|
||||||
|
trackList.modify()->setVisibility(TrackList::Visibility::Private);
|
||||||
|
trackList.modify()->setUser(LmsApp->getUser());
|
||||||
trackListId = trackList->getId();
|
trackListId = trackList->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,6 +759,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
TrackList::pointer trackList{ TrackList::find(LmsApp->getDbSession(), trackListId) };
|
TrackList::pointer trackList{ TrackList::find(LmsApp->getDbSession(), trackListId) };
|
||||||
trackList.modify()->clear();
|
trackList.modify()->clear();
|
||||||
|
trackList.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime());
|
||||||
|
|
||||||
Track::FindParameters params;
|
Track::FindParameters params;
|
||||||
params.setTrackList(_queueId);
|
params.setTrackList(_queueId);
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ namespace lms::ui
|
|||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file");
|
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file");
|
||||||
case scanner::ScanErrorType::CannotReadLyricsFile:
|
case scanner::ScanErrorType::CannotReadLyricsFile:
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-lyrics-file");
|
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-lyrics-file");
|
||||||
|
case scanner::ScanErrorType::CannotReadPlayListFile:
|
||||||
|
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-playlist-file");
|
||||||
case scanner::ScanErrorType::NoAudioTrack:
|
case scanner::ScanErrorType::NoAudioTrack:
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
|
return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
|
||||||
case scanner::ScanErrorType::BadDuration:
|
case scanner::ScanErrorType::BadDuration:
|
||||||
@@ -274,6 +276,11 @@ namespace lms::ui
|
|||||||
.arg(stepStats.progress()));
|
.arg(stepStats.progress()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ScanStep::AssociatePlayListTracks:
|
||||||
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-playlist-tracks")
|
||||||
|
.arg(stepStats.progress()));
|
||||||
|
break;
|
||||||
|
|
||||||
case ScanStep::AssociateReleaseImages:
|
case ScanStep::AssociateReleaseImages:
|
||||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-release-images")
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-release-images")
|
||||||
.arg(stepStats.progress()));
|
.arg(stepStats.progress()));
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ namespace lms::ui
|
|||||||
params.setClusters(_filters.getClusters());
|
params.setClusters(_filters.getClusters());
|
||||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||||
params.setUser(LmsApp->getUserId());
|
params.setUser(LmsApp->getUserId());
|
||||||
params.setType(TrackListType::Playlist);
|
params.setType(TrackListType::PlayList);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
switch (_mode)
|
switch (_mode)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user