More details for scan error reports #676
This commit is contained in:
@@ -100,9 +100,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.bad-embedded-image">Unable to read the image at index {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-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-file">Cannot read file ({1})</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</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-playlist-file">Cannot parse playlist file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Cannot parse playlist file</message>
|
||||||
@@ -115,6 +116,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.missing-artist-name">Missing artist name</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Unable to find track paths</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-path-missing">Unable to find the path "{1}"</message>
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">No audio track</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">No audio track</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
|
<message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated track MBID</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated track MBID</message>
|
||||||
|
|||||||
@@ -100,9 +100,10 @@
|
|||||||
|
|
||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message>
|
||||||
@@ -115,6 +116,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.missing-artist-name">Falta el nombre del artista</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">No se pudieron encontrar las rutas de las pistas</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-path-missing">No se pudo encontrar la ruta "{1}"</message>
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">No es una pista de audio</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">No es una pista de audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">Hash del fichero duplicado</message>
|
<message id="Lms.Admin.ScannerController.same-hash">Hash del fichero duplicado</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">MBID de la pista duplicado</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">MBID de la pista duplicado</message>
|
||||||
|
|||||||
@@ -100,9 +100,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.bad-embedded-image">Impossible de lire l'image à l'indice {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-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-file">Impossible de lire le fichier ({1})</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</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-playlist-file">Impossible d'analyser le fichier de liste de lecture</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossible d'analyser le fichier de liste de lecture</message>
|
||||||
@@ -115,6 +116,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.missing-artist-name">Nom de l'artiste manquant</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossible de trouver les chemins des pistes</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossible de trouver le chemin "{1}"</message>
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">Pas de piste audio</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">Pas de piste audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
|
<message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID dupliqué</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID dupliqué</message>
|
||||||
|
|||||||
@@ -100,9 +100,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.bad-embedded-image">Impossibile leggere l'immagine all'indice {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-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-file">Non in grado di leggere il file ({1})</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</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-playlist-file">Impossibile analizzare il file della playlist</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossibile analizzare il file della playlist</message>
|
||||||
@@ -115,6 +116,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.missing-artist-name">Nome dell'artista mancante</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossibile trovare i percorsi delle tracce</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossibile trovare il percorso "{1}"</message>
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">Nessuna traccia audio</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">Nessuna traccia audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message>
|
<message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID doppio</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID doppio</message>
|
||||||
|
|||||||
@@ -101,9 +101,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.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-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-file">Nie udało się odczytać pliku ({1})</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</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-playlist-file">Nie można przeanalizować pliku playlisty</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Nie można przeanalizować pliku playlisty</message>
|
||||||
@@ -124,6 +125,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.missing-artist-name">Brakuje nazwy artysty</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Nie można znaleźć ścieżek utworów</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.playlist-path-missing">Nie można znaleźć ścieżki „{1}”</message>
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">Brak ścieżki dźwiękowej</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">Brak ścieżki dźwiękowej</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">Zduplikowny hash pliku</message>
|
<message id="Lms.Admin.ScannerController.same-hash">Zduplikowny hash pliku</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">Zduplikowany MBID pliku</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">Zduplikowany MBID pliku</message>
|
||||||
|
|||||||
@@ -100,9 +100,10 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件 ({1})</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +115,9 @@
|
|||||||
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
|
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message>
|
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message>
|
<message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message>
|
<message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message>
|
||||||
<message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message>
|
<message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message>
|
||||||
|
|||||||
@@ -22,12 +22,12 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#include <archive.h>
|
#include <archive.h>
|
||||||
#include <archive_entry.h>
|
#include <archive_entry.h>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
|
||||||
|
|
||||||
namespace lms::zip
|
namespace lms::zip
|
||||||
{
|
{
|
||||||
@@ -43,9 +43,13 @@ namespace lms::zip
|
|||||||
: Exception{ "File '" + p.string() + "': " + std::string{ message } }
|
: Exception{ "File '" + p.string() + "': " + std::string{ message } }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
FileException(const std::filesystem::path& p, std::string_view message, int err)
|
class FileStdException : public FileException
|
||||||
: Exception{ "File '" + p.string() + "': " + std::string{ message } + ": " + core::stringUtils::systemErrorToString(err) }
|
{
|
||||||
|
public:
|
||||||
|
FileStdException(const std::filesystem::path& p, std::string_view message, int err)
|
||||||
|
: FileException{ p, std::string{ message } + "': " + std::error_code{ err, std::generic_category() }.message() }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -240,7 +244,7 @@ namespace lms::zip
|
|||||||
|
|
||||||
std::ifstream ifs{ _currentEntry->filePath, std::ios_base::binary };
|
std::ifstream ifs{ _currentEntry->filePath, std::ios_base::binary };
|
||||||
if (!ifs)
|
if (!ifs)
|
||||||
throw FileException{ _currentEntry->filePath, "cannot open file", errno };
|
throw FileStdException{ _currentEntry->filePath, "cannot open file", errno };
|
||||||
|
|
||||||
ifs.seekg(0, std::ios::end);
|
ifs.seekg(0, std::ios::end);
|
||||||
const std::uint64_t fileSize{ static_cast<std::uint64_t>(ifs.tellg()) };
|
const std::uint64_t fileSize{ static_cast<std::uint64_t>(ifs.tellg()) };
|
||||||
@@ -254,10 +258,10 @@ namespace lms::zip
|
|||||||
|
|
||||||
// read from file
|
// read from file
|
||||||
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
||||||
throw FileException{ _currentEntry->filePath, "seek failed", errno };
|
throw FileStdException{ _currentEntry->filePath, "seek failed", errno };
|
||||||
|
|
||||||
if (!ifs.read(reinterpret_cast<char*>(_readBuffer.data()), bytesToRead))
|
if (!ifs.read(reinterpret_cast<char*>(_readBuffer.data()), bytesToRead))
|
||||||
throw FileException{ _currentEntry->filePath, "read failed", errno };
|
throw FileStdException{ _currentEntry->filePath, "read failed", errno };
|
||||||
|
|
||||||
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
#include <system_error>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -35,7 +36,6 @@
|
|||||||
#include <boost/asio/read.hpp>
|
#include <boost/asio/read.hpp>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
@@ -44,8 +44,8 @@ namespace lms::core
|
|||||||
class SystemException : public ChildProcessException
|
class SystemException : public ChildProcessException
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SystemException(int err, const std::string& errMsg)
|
SystemException(std::error_code err, const std::string& errMsg)
|
||||||
: ChildProcessException{ errMsg + ": " + stringUtils::systemErrorToString(err) }
|
: ChildProcessException{ errMsg + ": " + err.message() }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,20 +69,20 @@ namespace lms::core
|
|||||||
|
|
||||||
// Use 'pipe' instead of 'pipe2', more portable
|
// Use 'pipe' instead of 'pipe2', more portable
|
||||||
if (pipe(pipefd) < 0)
|
if (pipe(pipefd) < 0)
|
||||||
throw SystemException{ errno, "pipe failed!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "pipe failed!" };
|
||||||
|
|
||||||
// Manually set the O_NONBLOCK and O_CLOEXEC flags for both ends of the pipe
|
// Manually set the O_NONBLOCK and O_CLOEXEC flags for both ends of the pipe
|
||||||
if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1)
|
if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1)
|
||||||
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" };
|
||||||
|
|
||||||
if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1)
|
if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1)
|
||||||
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" };
|
||||||
|
|
||||||
if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1)
|
if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1)
|
||||||
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set FD_CLOEXEC!" };
|
||||||
|
|
||||||
if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1)
|
if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1)
|
||||||
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set FD_CLOEXEC!" };
|
||||||
|
|
||||||
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
||||||
for (const int fd : { pipefd[0], pipefd[1] })
|
for (const int fd : { pipefd[0], pipefd[1] })
|
||||||
@@ -94,7 +94,7 @@ namespace lms::core
|
|||||||
if (pipeSizeRes == -1)
|
if (pipeSizeRes == -1)
|
||||||
{
|
{
|
||||||
const int err{ errno };
|
const int err{ errno };
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
|
LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -103,10 +103,10 @@ namespace lms::core
|
|||||||
#endif
|
#endif
|
||||||
if (currentPipeSize < targetPipeSize)
|
if (currentPipeSize < targetPipeSize)
|
||||||
{
|
{
|
||||||
if (fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1)
|
if (::fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1)
|
||||||
{
|
{
|
||||||
const int err{ errno };
|
const int err{ errno };
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
|
LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ namespace lms::core
|
|||||||
|
|
||||||
int res{ fork() };
|
int res{ fork() };
|
||||||
if (res == -1)
|
if (res == -1)
|
||||||
throw SystemException{ errno, "fork failed!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fork failed!" };
|
||||||
|
|
||||||
if (res == 0) // CHILD
|
if (res == 0) // CHILD
|
||||||
{
|
{
|
||||||
@@ -168,7 +168,10 @@ namespace lms::core
|
|||||||
// process may already have finished
|
// process may already have finished
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
|
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
|
||||||
if (::kill(_childPID, SIGKILL) == -1)
|
if (::kill(_childPID, SIGKILL) == -1)
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << stringUtils::systemErrorToString(errno));
|
{
|
||||||
|
const int err{ errno };
|
||||||
|
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChildProcess::wait(bool block)
|
bool ChildProcess::wait(bool block)
|
||||||
@@ -179,7 +182,7 @@ namespace lms::core
|
|||||||
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
|
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
|
||||||
|
|
||||||
if (pid == -1)
|
if (pid == -1)
|
||||||
throw SystemException{ errno, "waitpid failed!" };
|
throw SystemException{ std::error_code{ errno, std::generic_category() }, "waitpid failed!" };
|
||||||
if (pid == 0)
|
if (pid == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <system_error>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <boost/tokenizer.hpp>
|
#include <boost/tokenizer.hpp>
|
||||||
@@ -66,16 +67,24 @@ namespace lms::core::pathUtils
|
|||||||
return std::filesystem::create_directory(dir);
|
return std::filesystem::create_directory(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file)
|
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file, std::error_code& ec)
|
||||||
{
|
{
|
||||||
|
Wt::WDateTime res;
|
||||||
|
|
||||||
struct stat sb
|
struct stat sb
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stat(file.c_str(), &sb) == -1)
|
if (stat(file.c_str(), &sb) == -1)
|
||||||
throw LmsException("Failed to get stats on file '" + file.string() + "'");
|
{
|
||||||
|
ec = std::error_code{ errno, std::generic_category() };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ec = std::error_code{};
|
||||||
|
res = Wt::WDateTime::fromTime_t(sb.st_mtime);
|
||||||
|
}
|
||||||
|
|
||||||
return Wt::WDateTime::fromTime_t(sb.st_mtime);
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -509,11 +508,4 @@ namespace lms::core::stringUtils
|
|||||||
|
|
||||||
return "[" + std::to_string(mins) + ":" + (secs < 10 ? "0" : "") + std::to_string(secs) + "." + (millis < 100 ? (millis < 10 ? "00" : "0") : "") + std::to_string(millis) + "]";
|
return "[" + std::to_string(mins) + ":" + (secs < 10 ? "0" : "") + std::to_string(secs) + "." + (millis < 100 ? (millis < 10 ? "00" : "0") : "") + std::to_string(millis) + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string systemErrorToString(int err)
|
|
||||||
{
|
|
||||||
std::array<char, 128> buffer{};
|
|
||||||
::strerror_r(err, buffer.data(), buffer.size());
|
|
||||||
return std::string{ buffer.data() };
|
|
||||||
}
|
|
||||||
} // namespace lms::core::stringUtils
|
} // namespace lms::core::stringUtils
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <system_error>
|
||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ namespace lms::core::pathUtils
|
|||||||
bool ensureDirectory(const std::filesystem::path& dir);
|
bool ensureDirectory(const std::filesystem::path& dir);
|
||||||
|
|
||||||
// Get the last write time since Epoch
|
// Get the last write time since Epoch
|
||||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& dir);
|
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file, std::error_code& ec);
|
||||||
|
|
||||||
// returns false if aborted by user
|
// returns false if aborted by user
|
||||||
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName = {});
|
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName = {});
|
||||||
|
|||||||
@@ -119,6 +119,4 @@ namespace lms::core::stringUtils
|
|||||||
|
|
||||||
// to "[minutes:seconds.milliseconds]"
|
// to "[minutes:seconds.milliseconds]"
|
||||||
std::string formatTimestamp(std::chrono::milliseconds timestamp);
|
std::string formatTimestamp(std::chrono::milliseconds timestamp);
|
||||||
|
|
||||||
std::string systemErrorToString(int err);
|
|
||||||
} // namespace lms::core::stringUtils
|
} // namespace lms::core::stringUtils
|
||||||
@@ -11,12 +11,14 @@ endif()
|
|||||||
add_library(lmsmetadata STATIC
|
add_library(lmsmetadata STATIC
|
||||||
impl/ArtistInfo.cpp
|
impl/ArtistInfo.cpp
|
||||||
impl/AudioFileParser.cpp
|
impl/AudioFileParser.cpp
|
||||||
impl/AvFormatImageReader.cpp
|
impl/avformat/AvFormatImageReader.cpp
|
||||||
impl/AvFormatTagReader.cpp
|
impl/avformat/AvFormatTagReader.cpp
|
||||||
|
impl/avformat/Utils.cpp
|
||||||
impl/Lyrics.cpp
|
impl/Lyrics.cpp
|
||||||
impl/PlayList.cpp
|
impl/PlayList.cpp
|
||||||
impl/TagLibImageReader.cpp
|
impl/taglib/TagLibImageReader.cpp
|
||||||
impl/TagLibTagReader.cpp
|
impl/taglib/TagLibTagReader.cpp
|
||||||
|
impl/taglib/Utils.cpp
|
||||||
impl/Utils.cpp
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ target_include_directories(lmsmetadata INTERFACE
|
|||||||
|
|
||||||
target_include_directories(lmsmetadata PRIVATE
|
target_include_directories(lmsmetadata PRIVATE
|
||||||
include
|
include
|
||||||
|
impl
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsmetadata PRIVATE
|
target_link_libraries(lmsmetadata PRIVATE
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
std::span<const std::filesystem::path> getSupportedInfoFileExtensions()
|
std::span<const std::filesystem::path> getSupportedArtistInfoFiles()
|
||||||
{
|
{
|
||||||
static const std::array<std::filesystem::path, 1> fileExtensions{ ".nfo" };
|
static const std::array<std::filesystem::path, 1> files{ "artist.nfo" };
|
||||||
return fileExtensions;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistInfo parseArtistInfo(std::istream& is)
|
ArtistInfo parseArtistInfo(std::istream& is)
|
||||||
@@ -56,7 +56,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read artist xml info: " << error.what());
|
LMS_LOG(METADATA, ERROR, "Cannot read artist xml info: " << error.what());
|
||||||
throw ArtistInfoParseException{ error.what() };
|
throw ArtistInfoParseException{ error.what() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,13 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
#include "AvFormatImageReader.hpp"
|
|
||||||
#include "AvFormatTagReader.hpp"
|
|
||||||
#include "TagLibImageReader.hpp"
|
|
||||||
#include "TagLibTagReader.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "avformat/AvFormatImageReader.hpp"
|
||||||
|
#include "avformat/AvFormatTagReader.hpp"
|
||||||
|
#include "avformat/Utils.hpp"
|
||||||
|
#include "taglib/TagLibImageReader.hpp"
|
||||||
|
#include "taglib/TagLibTagReader.hpp"
|
||||||
|
#include "taglib/Utils.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
@@ -338,82 +340,56 @@ namespace lms::metadata
|
|||||||
|
|
||||||
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
|
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
// TODO: use backend capability to retrieve supported formats
|
switch (_params.backend)
|
||||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
{
|
||||||
".aac",
|
case ParserBackend::TagLib:
|
||||||
".alac",
|
return taglib::utils::getSupportedExtensions();
|
||||||
".aif",
|
break;
|
||||||
".aiff",
|
|
||||||
".ape",
|
case ParserBackend::AvFormat:
|
||||||
".dsf",
|
return avformat::utils::getSupportedExtensions();
|
||||||
".flac",
|
break;
|
||||||
".m4a",
|
}
|
||||||
".m4b",
|
|
||||||
".mp3",
|
return {};
|
||||||
".mpc",
|
|
||||||
".oga",
|
|
||||||
".ogg",
|
|
||||||
".opus",
|
|
||||||
".shn",
|
|
||||||
".wav",
|
|
||||||
".wma",
|
|
||||||
".wv",
|
|
||||||
};
|
|
||||||
return fileExtensions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
|
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
|
||||||
{
|
{
|
||||||
try
|
std::unique_ptr<ITagReader> tagReader;
|
||||||
|
switch (_params.backend)
|
||||||
{
|
{
|
||||||
std::unique_ptr<ITagReader> tagReader;
|
case ParserBackend::TagLib:
|
||||||
switch (_params.backend)
|
tagReader = std::make_unique<taglib::TagLibTagReader>(p, _params.readStyle, _params.debug);
|
||||||
{
|
break;
|
||||||
case ParserBackend::TagLib:
|
|
||||||
tagReader = std::make_unique<TagLibTagReader>(p, _params.readStyle, _params.debug);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ParserBackend::AvFormat:
|
case ParserBackend::AvFormat:
|
||||||
tagReader = std::make_unique<AvFormatTagReader>(p, _params.debug);
|
tagReader = std::make_unique<avformat::AvFormatTagReader>(p, _params.debug);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!tagReader)
|
if (!tagReader)
|
||||||
throw ParseException{ "Unhandled parser backend" };
|
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||||
|
|
||||||
return parseMetaData(*tagReader);
|
return parseMetaData(*tagReader);
|
||||||
}
|
|
||||||
catch (const Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed");
|
|
||||||
throw ParseException{};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const
|
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const
|
||||||
{
|
{
|
||||||
try
|
std::unique_ptr<IImageReader> imageReader;
|
||||||
|
switch (_params.backend)
|
||||||
{
|
{
|
||||||
std::unique_ptr<IImageReader> imageReader;
|
case ParserBackend::TagLib:
|
||||||
switch (_params.backend)
|
imageReader = std::make_unique<taglib::TagLibImageReader>(p);
|
||||||
{
|
break;
|
||||||
case ParserBackend::TagLib:
|
|
||||||
imageReader = std::make_unique<TagLibImageReader>(p);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ParserBackend::AvFormat:
|
case ParserBackend::AvFormat:
|
||||||
imageReader = std::make_unique<AvFormatImageReader>(p);
|
imageReader = std::make_unique<avformat::AvFormatImageReader>(p);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!imageReader)
|
if (!imageReader)
|
||||||
throw ParseException{ "Unhandled parser backend" };
|
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||||
|
|
||||||
parseImages(*imageReader, std::move(visitor));
|
parseImages(*imageReader, std::move(visitor));
|
||||||
}
|
|
||||||
catch (const Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": image parsing failed");
|
|
||||||
throw ParseException{};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
|
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "metadata/Types.hpp"
|
#include "metadata/Types.hpp"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace lms::metadata::utils
|
|||||||
return "accurate";
|
return "accurate";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw core::LmsException{ "Unknown read style" };
|
throw Exception{ "Unknown read style" };
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||||
|
|||||||
+5
-3
@@ -18,10 +18,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "AvFormatImageReader.hpp"
|
#include "AvFormatImageReader.hpp"
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
#include "av/IAudioFile.hpp"
|
||||||
#include "av/Types.hpp"
|
#include "av/Types.hpp"
|
||||||
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::avformat
|
||||||
{
|
{
|
||||||
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
|
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
@@ -31,7 +33,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
catch (av::Exception& e)
|
catch (av::Exception& e)
|
||||||
{
|
{
|
||||||
throw ParseException{};
|
throw AudioFileParsingException{ e.what() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,4 +58,4 @@ namespace lms::metadata
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::avformat
|
||||||
+2
-2
@@ -26,7 +26,7 @@ namespace lms::av
|
|||||||
class IAudioFile;
|
class IAudioFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::avformat
|
||||||
{
|
{
|
||||||
class AvFormatImageReader : public IImageReader
|
class AvFormatImageReader : public IImageReader
|
||||||
{
|
{
|
||||||
@@ -42,4 +42,4 @@ namespace lms::metadata
|
|||||||
|
|
||||||
std::unique_ptr<av::IAudioFile> _audioFile;
|
std::unique_ptr<av::IAudioFile> _audioFile;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::avformat
|
||||||
+5
-3
@@ -25,7 +25,7 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::avformat
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -165,10 +165,12 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
catch (av::Exception& e)
|
catch (av::Exception& e)
|
||||||
{
|
{
|
||||||
throw ParseException{};
|
throw AudioFileParsingException{ e.what() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvFormatTagReader::~AvFormatTagReader() = default;
|
||||||
|
|
||||||
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||||
{
|
{
|
||||||
auto itTagNames{ avFormatTagMapping.find(tag) };
|
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||||
@@ -223,4 +225,4 @@ namespace lms::metadata
|
|||||||
visitor("", value);
|
visitor("", value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::avformat
|
||||||
+3
-3
@@ -25,13 +25,13 @@
|
|||||||
|
|
||||||
#include "ITagReader.hpp"
|
#include "ITagReader.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::avformat
|
||||||
{
|
{
|
||||||
class AvFormatTagReader : public ITagReader
|
class AvFormatTagReader : public ITagReader
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
||||||
~AvFormatTagReader() override = default;
|
~AvFormatTagReader() override;
|
||||||
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
||||||
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
||||||
|
|
||||||
@@ -46,4 +46,4 @@ namespace lms::metadata
|
|||||||
av::IAudioFile::MetadataMap _metaDataMap;
|
av::IAudioFile::MetadataMap _metaDataMap;
|
||||||
av::ContainerInfo _containerInfo;
|
av::ContainerInfo _containerInfo;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::avformat
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::metadata::avformat::utils
|
||||||
|
{
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||||
|
{
|
||||||
|
// TODO: use av 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;
|
||||||
|
}
|
||||||
|
} // namespace lms::metadata::avformat::utils
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace lms::metadata::avformat::utils
|
||||||
|
{
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||||
|
} // namespace lms::metadata::avformat::utils
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
|
|
||||||
#include <taglib/taglib.h>
|
#include <taglib/taglib.h>
|
||||||
|
|
||||||
|
#if (TAGLIB_MAJOR_VERSION > 2)
|
||||||
|
#define TAGLIB_HAS_DSF 1
|
||||||
|
#endif
|
||||||
|
|
||||||
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
||||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
|
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
|
||||||
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
|
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
|
||||||
+18
-31
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "TagLibImageReader.hpp"
|
#include "TagLibImageReader.hpp"
|
||||||
|
|
||||||
|
#include "TagLibDefs.hpp"
|
||||||
|
|
||||||
#include <taglib/aifffile.h>
|
#include <taglib/aifffile.h>
|
||||||
#include <taglib/apetag.h>
|
#include <taglib/apetag.h>
|
||||||
#include <taglib/asffile.h>
|
#include <taglib/asffile.h>
|
||||||
@@ -34,24 +36,16 @@
|
|||||||
#include <taglib/vorbisfile.h>
|
#include <taglib/vorbisfile.h>
|
||||||
#include <taglib/wavfile.h>
|
#include <taglib/wavfile.h>
|
||||||
#include <taglib/wavpackfile.h>
|
#include <taglib/wavpackfile.h>
|
||||||
#include <tbytevector.h>
|
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
|
|
||||||
#include "core/String.hpp"
|
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
#include "TagLibDefs.hpp"
|
#include "taglib/Utils.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::taglib
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ImageParsingFailedException : public Exception
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
@@ -349,44 +343,37 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TagLib::FileRef parseFile(const std::filesystem::path& p)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForImages");
|
|
||||||
|
|
||||||
return TagLib::FileRef{ p.c_str(), false };
|
|
||||||
}
|
|
||||||
|
|
||||||
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
|
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
|
||||||
: _file{ parseFile(p) }
|
: _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) }
|
||||||
{
|
{
|
||||||
if (_file.isNull())
|
if (!_file)
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||||
throw ImageParsingFailedException{};
|
throw AudioFileParsingException{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TagLibImageReader::visitImages(ImageVisitor visitor) const
|
void TagLibImageReader::visitImages(ImageVisitor visitor) const
|
||||||
{
|
{
|
||||||
// MP3
|
// MP3
|
||||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
|
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (mp3File->hasID3v2Tag())
|
if (mp3File->hasID3v2Tag())
|
||||||
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
|
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// MP4
|
// MP4
|
||||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
|
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
visitMP4Images(*mp4File, std::move(visitor));
|
visitMP4Images(*mp4File, std::move(visitor));
|
||||||
}
|
}
|
||||||
// WMA
|
// WMA
|
||||||
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
|
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||||
visitASFImages(*tag, std::move(visitor));
|
visitASFImages(*tag, std::move(visitor));
|
||||||
}
|
}
|
||||||
// FLAC
|
// FLAC
|
||||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (flacFile->hasID3v2Tag()) // usage discouraged
|
if (flacFile->hasID3v2Tag()) // usage discouraged
|
||||||
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
|
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
|
||||||
@@ -394,38 +381,38 @@ namespace lms::metadata
|
|||||||
visitFLACImages(flacFile->pictureList(), std::move(visitor));
|
visitFLACImages(flacFile->pictureList(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// Ogg vorbis
|
// Ogg vorbis
|
||||||
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
|
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
|
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// Ogg Opus
|
// Ogg Opus
|
||||||
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
|
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
|
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// Aiff
|
// Aiff
|
||||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (aiffFile->hasID3v2Tag())
|
if (aiffFile->hasID3v2Tag())
|
||||||
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
|
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// Wav
|
// Wav
|
||||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
|
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (wavFile->hasID3v2Tag())
|
if (wavFile->hasID3v2Tag())
|
||||||
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
|
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// MPC
|
// MPC
|
||||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
|
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (mpcFile->hasAPETag())
|
if (mpcFile->hasAPETag())
|
||||||
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
|
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
|
||||||
}
|
}
|
||||||
// WavPack
|
// WavPack
|
||||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
|
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (wavPackFile->hasAPETag())
|
if (wavPackFile->hasAPETag())
|
||||||
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
|
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::taglib
|
||||||
+4
-4
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
#include "IImageReader.hpp"
|
#include "IImageReader.hpp"
|
||||||
|
|
||||||
#include <taglib/fileref.h>
|
#include <taglib/tfile.h>
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::taglib
|
||||||
{
|
{
|
||||||
class TagLibImageReader : public IImageReader
|
class TagLibImageReader : public IImageReader
|
||||||
{
|
{
|
||||||
@@ -32,6 +32,6 @@ namespace lms::metadata
|
|||||||
|
|
||||||
void visitImages(ImageVisitor visitor) const override;
|
void visitImages(ImageVisitor visitor) const override;
|
||||||
|
|
||||||
const TagLib::FileRef _file;
|
std::unique_ptr<TagLib::File> _file;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::taglib
|
||||||
+33
-50
@@ -19,44 +19,49 @@
|
|||||||
|
|
||||||
#include "TagLibTagReader.hpp"
|
#include "TagLibTagReader.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "TagLibDefs.hpp"
|
||||||
|
|
||||||
#include <taglib/aifffile.h>
|
#include <taglib/aifffile.h>
|
||||||
|
#include <taglib/apefile.h>
|
||||||
#include <taglib/apeproperties.h>
|
#include <taglib/apeproperties.h>
|
||||||
#include <taglib/apetag.h>
|
#include <taglib/apetag.h>
|
||||||
#include <taglib/asffile.h>
|
#include <taglib/asffile.h>
|
||||||
#if TAGLIB_MAJOR_VERSION >= 2
|
|
||||||
#include <taglib/dsffile.h>
|
|
||||||
#endif
|
|
||||||
#include <taglib/fileref.h>
|
|
||||||
#include <taglib/flacfile.h>
|
#include <taglib/flacfile.h>
|
||||||
#include <taglib/id3v2tag.h>
|
#include <taglib/id3v2tag.h>
|
||||||
#include <taglib/mp4file.h>
|
#include <taglib/mp4file.h>
|
||||||
#include <taglib/mpcfile.h>
|
#include <taglib/mpcfile.h>
|
||||||
#include <taglib/mpegfile.h>
|
#include <taglib/mpegfile.h>
|
||||||
|
#include <taglib/oggflacfile.h>
|
||||||
#include <taglib/opusfile.h>
|
#include <taglib/opusfile.h>
|
||||||
|
#include <taglib/speexfile.h>
|
||||||
#include <taglib/synchronizedlyricsframe.h>
|
#include <taglib/synchronizedlyricsframe.h>
|
||||||
#include <taglib/tag.h>
|
#include <taglib/tag.h>
|
||||||
#include <taglib/tpropertymap.h>
|
#include <taglib/tpropertymap.h>
|
||||||
|
#include <taglib/trueaudiofile.h>
|
||||||
#include <taglib/unsynchronizedlyricsframe.h>
|
#include <taglib/unsynchronizedlyricsframe.h>
|
||||||
#include <taglib/vorbisfile.h>
|
#include <taglib/vorbisfile.h>
|
||||||
#include <taglib/wavfile.h>
|
#include <taglib/wavfile.h>
|
||||||
#include <taglib/wavpackfile.h>
|
#include <taglib/wavpackfile.h>
|
||||||
|
#if TAGLIB_HAS_DSF
|
||||||
|
#include <taglib/dsffile.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
#include "TagLibDefs.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::taglib
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class TagParsingFailedException : public Exception
|
class TagParsingFailedException : public Exception
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
using Exception::Exception;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mapping to internal taglib names and/or common alternative custom names
|
// Mapping to internal taglib names and/or common alternative custom names
|
||||||
@@ -166,21 +171,6 @@ namespace lms::metadata
|
|||||||
{ TagType::Writer, { "WRITER" } },
|
{ TagType::Writer, { "WRITER" } },
|
||||||
};
|
};
|
||||||
|
|
||||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
|
||||||
{
|
|
||||||
switch (readStyle)
|
|
||||||
{
|
|
||||||
case ParserReadStyle::Fast:
|
|
||||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
|
||||||
case ParserReadStyle::Average:
|
|
||||||
return TagLib::AudioProperties::ReadStyle::Average;
|
|
||||||
case ParserReadStyle::Accurate:
|
|
||||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw core::LmsException{ "Cannot convert read style" };
|
|
||||||
}
|
|
||||||
|
|
||||||
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
|
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
|
||||||
{
|
{
|
||||||
for (auto&& [tag, values] : src)
|
for (auto&& [tag, values] : src)
|
||||||
@@ -211,35 +201,26 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForMetaData");
|
|
||||||
|
|
||||||
return TagLib::FileRef{ p.c_str(), true // read audio properties
|
|
||||||
,
|
|
||||||
readStyleToTagLibReadStyle(parserReadStyle) };
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
|
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
|
||||||
: _file{ parseFile(p, parserReadStyle) }
|
: _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) }
|
||||||
{
|
{
|
||||||
if (_file.isNull())
|
if (!_file)
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||||
throw TagParsingFailedException{};
|
throw AudioFileParsingException{ "Parsing failed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_file.audioProperties())
|
if (!_file->audioProperties())
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||||
throw TagParsingFailedException{};
|
throw AudioFileNoAudioPropertiesException{};
|
||||||
}
|
}
|
||||||
|
|
||||||
computeAudioProperties();
|
computeAudioProperties();
|
||||||
|
|
||||||
_propertyMap = _file.file()->properties();
|
_propertyMap = _file->properties();
|
||||||
|
|
||||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||||
{
|
{
|
||||||
@@ -319,7 +300,7 @@ namespace lms::metadata
|
|||||||
};
|
};
|
||||||
|
|
||||||
// WMA
|
// WMA
|
||||||
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
|
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||||
{
|
{
|
||||||
@@ -365,7 +346,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MP3
|
// MP3
|
||||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
|
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (mp3File->hasID3v2Tag())
|
if (mp3File->hasID3v2Tag())
|
||||||
processID3v2Tags(*mp3File->ID3v2Tag());
|
processID3v2Tags(*mp3File->ID3v2Tag());
|
||||||
@@ -373,7 +354,7 @@ namespace lms::metadata
|
|||||||
getAPETags(mp3File->APETag());
|
getAPETags(mp3File->APETag());
|
||||||
}
|
}
|
||||||
// MP4
|
// MP4
|
||||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
|
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
// Taglib does not expose rtng in properties
|
// Taglib does not expose rtng in properties
|
||||||
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||||
@@ -406,40 +387,42 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MPC
|
// MPC
|
||||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
|
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
getAPETags(mpcFile->APETag());
|
getAPETags(mpcFile->APETag());
|
||||||
}
|
}
|
||||||
// WavPack
|
// WavPack
|
||||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
|
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
getAPETags(wavPackFile->APETag());
|
getAPETags(wavPackFile->APETag());
|
||||||
}
|
}
|
||||||
// FLAC
|
// FLAC
|
||||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (flacFile->hasID3v2Tag()) // discouraged usage
|
if (flacFile->hasID3v2Tag()) // discouraged usage
|
||||||
processID3v2Tags(*flacFile->ID3v2Tag());
|
processID3v2Tags(*flacFile->ID3v2Tag());
|
||||||
}
|
}
|
||||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (aiffFile->hasID3v2Tag())
|
if (aiffFile->hasID3v2Tag())
|
||||||
processID3v2Tags(*aiffFile->tag());
|
processID3v2Tags(*aiffFile->tag());
|
||||||
}
|
}
|
||||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
|
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||||
{
|
{
|
||||||
if (wavFile->hasID3v2Tag())
|
if (wavFile->hasID3v2Tag())
|
||||||
processID3v2Tags(*wavFile->ID3v2Tag());
|
processID3v2Tags(*wavFile->ID3v2Tag());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagLibTagReader::~TagLibTagReader() = default;
|
||||||
|
|
||||||
void TagLibTagReader::computeAudioProperties()
|
void TagLibTagReader::computeAudioProperties()
|
||||||
{
|
{
|
||||||
const TagLib::AudioProperties* properties{ _file.audioProperties() };
|
const TagLib::AudioProperties* properties{ _file->audioProperties() };
|
||||||
|
|
||||||
// Common properties
|
// Common properties
|
||||||
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||||
_audioProperties.channelCount = static_cast<std::size_t>(_file.audioProperties()->channels());
|
_audioProperties.channelCount = static_cast<std::size_t>(_file->audioProperties()->channels());
|
||||||
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||||
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
||||||
|
|
||||||
@@ -457,7 +440,7 @@ namespace lms::metadata
|
|||||||
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
|
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
|
||||||
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
|
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
|
||||||
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
|
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
|
||||||
#if TAGLIB_MAJOR_VERSION >= 2
|
#if TAGLIB_HAS_DSF
|
||||||
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
||||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||||
#endif
|
#endif
|
||||||
@@ -534,4 +517,4 @@ namespace lms::metadata
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::taglib
|
||||||
+6
-5
@@ -21,20 +21,21 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <taglib/fileref.h>
|
#include <taglib/tfile.h>
|
||||||
#include <taglib/tpropertymap.h>
|
#include <taglib/tpropertymap.h>
|
||||||
|
|
||||||
#include "ITagReader.hpp"
|
#include "ITagReader.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata::taglib
|
||||||
{
|
{
|
||||||
class TagLibTagReader : public ITagReader
|
class TagLibTagReader : public ITagReader
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
||||||
~TagLibTagReader() = default;
|
~TagLibTagReader() override;
|
||||||
TagLibTagReader(const TagLibTagReader&) = delete;
|
TagLibTagReader(const TagLibTagReader&) = delete;
|
||||||
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
||||||
|
|
||||||
@@ -47,9 +48,9 @@ namespace lms::metadata
|
|||||||
|
|
||||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||||
|
|
||||||
const TagLib::FileRef _file;
|
std::unique_ptr<TagLib::File> _file;
|
||||||
AudioProperties _audioProperties;
|
AudioProperties _audioProperties;
|
||||||
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||||
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata::taglib
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
#include "TagLibDefs.hpp"
|
||||||
|
|
||||||
|
#include <taglib/aifffile.h>
|
||||||
|
#include <taglib/apefile.h>
|
||||||
|
#include <taglib/asffile.h>
|
||||||
|
#include <taglib/audioproperties.h>
|
||||||
|
#include <taglib/flacfile.h>
|
||||||
|
#include <taglib/id3v2framefactory.h>
|
||||||
|
#include <taglib/mp4file.h>
|
||||||
|
#include <taglib/mpcfile.h>
|
||||||
|
#include <taglib/mpegfile.h>
|
||||||
|
#include <taglib/oggflacfile.h>
|
||||||
|
#include <taglib/opusfile.h>
|
||||||
|
#include <taglib/speexfile.h>
|
||||||
|
#include <taglib/tfile.h>
|
||||||
|
#include <taglib/tfilestream.h>
|
||||||
|
#include <taglib/trueaudiofile.h>
|
||||||
|
#include <taglib/vorbisfile.h>
|
||||||
|
#include <taglib/wavfile.h>
|
||||||
|
#include <taglib/wavpackfile.h>
|
||||||
|
#if TAGLIB_HAS_DSF
|
||||||
|
#include <taglib/dsffile.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
|
||||||
|
namespace lms::metadata::taglib::utils
|
||||||
|
{
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||||
|
{
|
||||||
|
static const std::vector<std::filesystem::path> supportedExtensions
|
||||||
|
{
|
||||||
|
".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus",
|
||||||
|
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
|
||||||
|
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
|
||||||
|
".wav",
|
||||||
|
#if TAGLIB_HAS_DSF
|
||||||
|
".dsf", ".dff", ".dsdiff"
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||||
|
}
|
||||||
|
|
||||||
|
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||||
|
{
|
||||||
|
switch (readStyle)
|
||||||
|
{
|
||||||
|
case ParserReadStyle::Fast:
|
||||||
|
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||||
|
case ParserReadStyle::Average:
|
||||||
|
return TagLib::AudioProperties::ReadStyle::Average;
|
||||||
|
case ParserReadStyle::Accurate:
|
||||||
|
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception{ "Cannot convert read style" };
|
||||||
|
}
|
||||||
|
|
||||||
|
TagLib::FileStream createFileStream(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
FILE* file{ std::fopen(p.c_str(), "r") };
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
|
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
||||||
|
throw IOException{ "fopen failed", ec };
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd{ ::fileno(file) };
|
||||||
|
if (fd == -1)
|
||||||
|
{
|
||||||
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
|
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
|
||||||
|
throw IOException{ "fileno failed", ec };
|
||||||
|
}
|
||||||
|
|
||||||
|
return TagLib::FileStream{ fd, true };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||||
|
{
|
||||||
|
const std::string ext{ core::stringUtils::stringToUpper(extension.string().substr(1)) };
|
||||||
|
|
||||||
|
std::unique_ptr<TagLib::File> file;
|
||||||
|
|
||||||
|
// MP3
|
||||||
|
if (ext == "MP3" || ext == "MP2" || ext == "AAC")
|
||||||
|
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||||
|
// VORBIS
|
||||||
|
else if (ext == "OGG")
|
||||||
|
return std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "OGA")
|
||||||
|
{
|
||||||
|
/* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */
|
||||||
|
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
if (!file->isValid())
|
||||||
|
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
}
|
||||||
|
else if (ext == "FLAC")
|
||||||
|
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "SPX")
|
||||||
|
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "OPUS")
|
||||||
|
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// APE
|
||||||
|
else if (ext == "MPC")
|
||||||
|
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "WV")
|
||||||
|
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "APE")
|
||||||
|
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// TRUEAUDIO
|
||||||
|
else if (ext == "TTA")
|
||||||
|
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// MP4
|
||||||
|
else if (ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V")
|
||||||
|
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// ASF
|
||||||
|
else if (ext == "WMA" || ext == "ASF")
|
||||||
|
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// RIFF
|
||||||
|
else if (ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC")
|
||||||
|
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "WAV")
|
||||||
|
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
#if TAGLIB_HAS_DSF
|
||||||
|
else if (ext == "DSF")
|
||||||
|
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (ext == "DFF" || ext == "DSDIFF")
|
||||||
|
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (file && !file->isValid())
|
||||||
|
{
|
||||||
|
LMS_LOG(METADATA, DEBUG, "File " << file << ": failed to parse by extension");
|
||||||
|
file.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||||
|
{
|
||||||
|
std::unique_ptr<TagLib::File> file;
|
||||||
|
|
||||||
|
if (TagLib::MPEG::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||||
|
// VORBIS
|
||||||
|
else if (TagLib::Ogg::Vorbis::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::Ogg::FLAC::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::FLAC::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::Ogg::Speex::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::Ogg::Opus::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// APE
|
||||||
|
else if (TagLib::MPC::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::WavPack::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::APE::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// TRUEAUDIO
|
||||||
|
else if (TagLib::TrueAudio::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// MP4
|
||||||
|
else if (TagLib::MP4::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
//_ASF
|
||||||
|
else if (TagLib::ASF::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
// RIFF
|
||||||
|
else if (TagLib::RIFF::AIFF::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (TagLib::RIFF::WAV::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
#if TAGLIB_HAS_DSF
|
||||||
|
else if (DSF::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
else if (DSDIFF::File::isSupported(stream))
|
||||||
|
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (file && !file->isValid())
|
||||||
|
{
|
||||||
|
LMS_LOG(METADATA, DEBUG, "File " << file << ": failed to parse by content");
|
||||||
|
file.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||||
|
|
||||||
|
TagLib::FileStream fileStream{ createFileStream(p) };
|
||||||
|
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) };
|
||||||
|
if (!file)
|
||||||
|
file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value());
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} // namespace lms::metadata::taglib::utils
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include <taglib/audioproperties.h>
|
||||||
|
#include <taglib/tfile.h>
|
||||||
|
|
||||||
|
#include "core/TaggedType.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
|
namespace lms::metadata::taglib::utils
|
||||||
|
{
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||||
|
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle);
|
||||||
|
|
||||||
|
using ReadAudioProperties = core::TaggedBool<struct ReadAudioPropertiesTag>;
|
||||||
|
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties);
|
||||||
|
} // namespace lms::metadata::taglib::utils
|
||||||
@@ -51,6 +51,6 @@ namespace lms::metadata
|
|||||||
using Exception::Exception;
|
using Exception::Exception;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::span<const std::filesystem::path> getSupportedInfoFileExtensions();
|
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
|
||||||
ArtistInfo parseArtistInfo(std::istream& is);
|
ArtistInfo parseArtistInfo(std::istream& is);
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
#include "core/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
@@ -29,9 +31,31 @@ namespace lms::metadata
|
|||||||
using LmsException::LmsException;
|
using LmsException::LmsException;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ParseException : public Exception
|
class IOException : public Exception
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IOException(std::string_view message, std::error_code err)
|
||||||
|
: Exception{ std::string{ message } + ": " + err.message() }
|
||||||
|
, _err{ err }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code getErrorCode() const { return _err; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::error_code _err;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioFileParsingException : public Exception
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using Exception::Exception;
|
using Exception::Exception;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using AudioFileParsingException::AudioFileParsingException;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
|
#include "metadata/Exception.hpp"
|
||||||
#include "metadata/Types.hpp"
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ namespace lms::metadata::tests
|
|||||||
#PLAYLIST:My super playlist
|
#PLAYLIST:My super playlist
|
||||||
01-Foo.mp3
|
01-Foo.mp3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#EXTINF:263,Alice in Chains - Don't Follow
|
#EXTINF:263,Alice in Chains - Don't Follow
|
||||||
02-FooBar.mp3
|
02-FooBar.mp3
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ add_library(lmsscanner STATIC
|
|||||||
impl/helpers/ArtistHelpers.cpp
|
impl/helpers/ArtistHelpers.cpp
|
||||||
impl/scanners/ArtistInfoFileScanner.cpp
|
impl/scanners/ArtistInfoFileScanner.cpp
|
||||||
impl/scanners/AudioFileScanOperation.cpp
|
impl/scanners/AudioFileScanOperation.cpp
|
||||||
|
impl/scanners/FileScanOperationBase.cpp
|
||||||
impl/scanners/AudioFileScanner.cpp
|
impl/scanners/AudioFileScanner.cpp
|
||||||
impl/scanners/ImageFileScanner.cpp
|
impl/scanners/ImageFileScanner.cpp
|
||||||
impl/scanners/LyricsFileScanner.cpp
|
impl/scanners/LyricsFileScanner.cpp
|
||||||
impl/scanners/PlayListFileScanner.cpp
|
impl/scanners/PlayListFileScanner.cpp
|
||||||
impl/scanners/Utils.cpp
|
impl/scanners/Utils.cpp
|
||||||
impl/steps/FileScanQueue.cpp
|
impl/steps/FileScanQueue.cpp
|
||||||
|
impl/steps/ScanErrorLogger.cpp
|
||||||
impl/steps/ScanStepArtistReconciliation.cpp
|
impl/steps/ScanStepArtistReconciliation.cpp
|
||||||
impl/steps/ScanStepAssociateArtistImages.cpp
|
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||||
|
|||||||
@@ -51,14 +51,16 @@
|
|||||||
#include "steps/ScanStepScanFiles.hpp"
|
#include "steps/ScanStepScanFiles.hpp"
|
||||||
#include "steps/ScanStepUpdateLibraryFields.hpp"
|
#include "steps/ScanStepUpdateLibraryFields.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr std::string_view currentSettingsName{ "" };
|
constexpr std::string_view currentSettingsName{};
|
||||||
static constexpr std::string_view lastScanSettingsName{ "last_scan" };
|
constexpr std::string_view lastScanSettingsName{ "last_scan" };
|
||||||
|
|
||||||
Wt::WDate getNextMonday(Wt::WDate current)
|
Wt::WDate getNextMonday(Wt::WDate current)
|
||||||
{
|
{
|
||||||
@@ -352,7 +354,7 @@ namespace lms::scanner
|
|||||||
_currentScanStepStats.reset(); // must be sync with _curState
|
_currentScanStepStats.reset(); // must be sync with _curState
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << ", failures = " << stats.failures << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errorsCount << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
||||||
|
|
||||||
if (!_abortScan)
|
if (!_abortScan)
|
||||||
{
|
{
|
||||||
@@ -433,11 +435,11 @@ namespace lms::scanner
|
|||||||
} };
|
} };
|
||||||
|
|
||||||
_fileScanners.clear();
|
_fileScanners.clear();
|
||||||
_fileScanners.emplace_back(std::make_unique<ArtistInfoFileScanner>(_settings, _db));
|
_fileScanners.emplace_back(std::make_unique<ArtistInfoFileScanner>(_db, _settings));
|
||||||
_fileScanners.emplace_back(std::make_unique<AudioFileScanner>(_db, _settings));
|
_fileScanners.emplace_back(std::make_unique<AudioFileScanner>(_db, _settings));
|
||||||
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db));
|
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db, _settings));
|
||||||
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db));
|
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db, _settings));
|
||||||
_fileScanners.emplace_back(std::make_unique<PlayListFileScanner>(_db));
|
_fileScanners.emplace_back(std::make_unique<PlayListFileScanner>(_db, _settings));
|
||||||
|
|
||||||
std::vector<IFileScanner*> fileScanners;
|
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(); });
|
std::transform(std::cbegin(_fileScanners), std::cend(_fileScanners), std::back_inserter(fileScanners), [](const std::unique_ptr<IFileScanner>& scanner) { return scanner.get(); });
|
||||||
|
|||||||
@@ -21,16 +21,9 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
ScanError::ScanError(const std::filesystem::path& _file, ScanErrorType _error, const std::string& _systemError)
|
|
||||||
: file{ _file }
|
|
||||||
, error{ _error }
|
|
||||||
, systemError{ _systemError }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t ScanStats::nbFiles() const
|
std::size_t ScanStats::nbFiles() const
|
||||||
{
|
{
|
||||||
return skips + additions + updates;
|
return skips + additions + updates + failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t ScanStats::nbChanges() const
|
std::size_t ScanStats::nbChanges() const
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "ArtistInfoFileScanner.hpp"
|
#include "ArtistInfoFileScanner.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
@@ -29,45 +30,33 @@
|
|||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "metadata/ArtistInfo.hpp"
|
#include "metadata/ArtistInfo.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
#include "IFileScanOperation.hpp"
|
#include "FileScanOperationBase.hpp"
|
||||||
#include "ScanContext.hpp"
|
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "helpers/ArtistHelpers.hpp"
|
#include "helpers/ArtistHelpers.hpp"
|
||||||
#include "metadata/Types.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ArtistInfoFileScanOperation : public IFileScanOperation
|
class ArtistInfoFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtistInfoFileScanOperation(const FileToScan& file, const ScannerSettings& settings, db::Db& db)
|
using FileScanOperationBase::FileScanOperationBase;
|
||||||
: _file{ file.file }
|
|
||||||
, _mediaLibrary{ file.mediaLibrary }
|
|
||||||
, _settings{ settings }
|
|
||||||
, _db{ db }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
~ArtistInfoFileScanOperation() override = default;
|
~ArtistInfoFileScanOperation() override = default;
|
||||||
ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
|
ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
|
||||||
ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
|
ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path& getFile() const override { return _file; };
|
|
||||||
core::LiteralString getName() const override { return "ScanArtistInfoFile"; }
|
core::LiteralString getName() const override { return "ScanArtistInfoFile"; }
|
||||||
void scan() override;
|
void scan() override;
|
||||||
void processResult(ScanContext& context) override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
std::string getArtistNameFromArtistInfoFilePath();
|
std::string getArtistNameFromArtistInfoFilePath();
|
||||||
|
|
||||||
const std::filesystem::path _file;
|
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
|
||||||
const ScannerSettings& _settings;
|
|
||||||
db::Db& _db;
|
|
||||||
|
|
||||||
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,91 +64,82 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::ifstream ifs{ _file };
|
std::ifstream ifs{ getFilePath() };
|
||||||
if (!ifs)
|
if (!ifs)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file);
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
|
addError<IOScanError>(getFilePath(), ec);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
||||||
if (_parsedArtistInfo->name.empty())
|
if (_parsedArtistInfo->name.empty())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set");
|
addError<MissingArtistNameError>(getFilePath());
|
||||||
_parsedArtistInfo.reset();
|
_parsedArtistInfo.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const metadata::ArtistInfoParseException& e)
|
catch (const metadata::ArtistInfoParseException& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read artist info in file " << _file << ": " << e.what());
|
addError<ArtistInfoFileScanError>(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArtistInfoFileScanOperation::processResult(ScanContext& context)
|
ArtistInfoFileScanOperation::OperationResult ArtistInfoFileScanOperation::processResult()
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ getDb().getTLSSession() };
|
||||||
|
db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, getFilePath()) };
|
||||||
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
|
||||||
if (!fileInfo)
|
|
||||||
{
|
|
||||||
stats.skips++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, _file) };
|
|
||||||
if (!_parsedArtistInfo)
|
if (!_parsedArtistInfo)
|
||||||
{
|
{
|
||||||
if (artistInfo)
|
if (artistInfo)
|
||||||
{
|
{
|
||||||
artistInfo.remove();
|
artistInfo.remove();
|
||||||
stats.deletions++;
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removed artist info file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Removed artist info file " << getFilePath());
|
||||||
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadArtistInfoFile);
|
|
||||||
return;
|
return OperationResult::Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool added{ !artistInfo };
|
const bool added{ !artistInfo };
|
||||||
if (!artistInfo)
|
if (!artistInfo)
|
||||||
{
|
{
|
||||||
artistInfo = dbSession.create<db::ArtistInfo>();
|
artistInfo = dbSession.create<db::ArtistInfo>();
|
||||||
artistInfo.modify()->setAbsoluteFilePath(_file);
|
artistInfo.modify()->setAbsoluteFilePath(getFilePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
artistInfo.modify()->setScanVersion(_settings.artistInfoScanVersion);
|
artistInfo.modify()->setScanVersion(getScannerSettings().artistInfoScanVersion);
|
||||||
artistInfo.modify()->setName(_parsedArtistInfo->name);
|
artistInfo.modify()->setName(_parsedArtistInfo->name);
|
||||||
artistInfo.modify()->setSortName(_parsedArtistInfo->sortName);
|
artistInfo.modify()->setSortName(_parsedArtistInfo->sortName);
|
||||||
artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
artistInfo.modify()->setLastWriteTime(getLastWriteTime());
|
||||||
artistInfo.modify()->setType(_parsedArtistInfo->type);
|
artistInfo.modify()->setType(_parsedArtistInfo->type);
|
||||||
artistInfo.modify()->setGender(_parsedArtistInfo->gender);
|
artistInfo.modify()->setGender(_parsedArtistInfo->gender);
|
||||||
artistInfo.modify()->setDisambiguation(_parsedArtistInfo->disambiguation);
|
artistInfo.modify()->setDisambiguation(_parsedArtistInfo->disambiguation);
|
||||||
artistInfo.modify()->setBiography(_parsedArtistInfo->biography);
|
artistInfo.modify()->setBiography(_parsedArtistInfo->biography);
|
||||||
|
|
||||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||||
|
|
||||||
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ _settings.allowArtistMBIDFallback }) };
|
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) };
|
||||||
artistInfo.modify()->setArtist(artist);
|
artistInfo.modify()->setArtist(artist);
|
||||||
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID());
|
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID());
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added artist info file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Added artist info file " << getFilePath());
|
||||||
stats.additions++;
|
return OperationResult::Added;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated artist info file " << _file);
|
|
||||||
stats.updates++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated artist info file " << getFilePath());
|
||||||
|
return OperationResult::Updated;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ArtistInfoFileScanner::ArtistInfoFileScanner(const ScannerSettings& settings, db::Db& db)
|
ArtistInfoFileScanner::ArtistInfoFileScanner(db::Db& db, const ScannerSettings& settings)
|
||||||
: _settings{ settings }
|
: _db{ db }
|
||||||
, _db{ db }
|
, _settings{ settings }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,45 +148,34 @@ namespace lms::scanner
|
|||||||
return "Artist info scanner";
|
return "Artist info scanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
|
||||||
{
|
{
|
||||||
return metadata::getSupportedInfoFileExtensions();
|
return metadata::getSupportedArtistInfoFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ArtistInfoFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtistInfoFileScanner::needsScan(const FileToScan& file) const
|
||||||
{
|
{
|
||||||
// Special case: only files named "artist.nfo" are compatible with this scanner
|
// Special case: only files named "artist.nfo" are compatible with this scanner
|
||||||
// Hack here since the scanner framework only handle extensions (the discover count is not accurate)
|
// Hack here since the scanner framework only handle extensions (the discover count is not accurate)
|
||||||
if (!core::stringUtils::stringCaseInsensitiveEqual(file.file.stem().string(), "artist"))
|
if (!core::stringUtils::stringCaseInsensitiveEqual(file.filePath.stem().string(), "artist"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
|
||||||
// Should rarely fail as we are currently iterating it
|
|
||||||
if (!lastWriteTime.isValid())
|
|
||||||
{
|
|
||||||
context.stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.scanOptions.fullScan)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
auto transaction{ dbSession.createReadTransaction() };
|
auto transaction{ dbSession.createReadTransaction() };
|
||||||
db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, file.file) };
|
db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, file.filePath) };
|
||||||
if (artistInfo
|
|
||||||
&& artistInfo->getLastWriteTime() == lastWriteTime
|
|
||||||
&& artistInfo->getScanVersion() == _settings.artistInfoScanVersion)
|
|
||||||
{
|
|
||||||
context.stats.skips++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return !artistInfo
|
||||||
|
|| artistInfo->getLastWriteTime() != file.lastWriteTime
|
||||||
|
|| artistInfo->getScanVersion() != _settings.artistInfoScanVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IFileScanOperation> ArtistInfoFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
std::unique_ptr<IFileScanOperation> ArtistInfoFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<ArtistInfoFileScanOperation>(fileToScan, _settings, _db);
|
return std::make_unique<ArtistInfoFileScanOperation>(std::move(fileToScan), _db, _settings);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -36,18 +36,19 @@ namespace lms::scanner
|
|||||||
class ArtistInfoFileScanner : public IFileScanner
|
class ArtistInfoFileScanner : public IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtistInfoFileScanner(const ScannerSettings& _settings, db::Db& db);
|
ArtistInfoFileScanner(db::Db& db, const ScannerSettings& _settings);
|
||||||
~ArtistInfoFileScanner() override = default;
|
~ArtistInfoFileScanner() override = default;
|
||||||
ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
|
ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
|
||||||
ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
|
ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
core::LiteralString getName() const override;
|
core::LiteralString getName() const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedFiles() const override;
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const override;
|
||||||
|
|
||||||
const ScannerSettings& _settings;
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
const ScannerSettings& _settings;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -41,9 +41,10 @@
|
|||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
#include "IFileScanOperation.hpp"
|
#include "IFileScanOperation.hpp"
|
||||||
#include "ScanContext.hpp"
|
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "helpers/ArtistHelpers.hpp"
|
#include "helpers/ArtistHelpers.hpp"
|
||||||
@@ -387,7 +388,7 @@ namespace lms::scanner
|
|||||||
return db::Advisory::UnSet;
|
return db::Advisory::UnSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo)
|
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, size_t fileSize, const std::filesystem::path& relativePath)
|
||||||
{
|
{
|
||||||
db::Track::FindParameters params;
|
db::Track::FindParameters params;
|
||||||
// Add as many fields as possible to limit errors
|
// Add as many fields as possible to limit errors
|
||||||
@@ -401,7 +402,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
if (parsedTrack.position)
|
if (parsedTrack.position)
|
||||||
params.setTrackNumber(*parsedTrack.position);
|
params.setTrackNumber(*parsedTrack.position);
|
||||||
params.setFileSize(fileInfo.fileSize);
|
params.setFileSize(fileSize);
|
||||||
|
|
||||||
bool error{};
|
bool error{};
|
||||||
db::Track::pointer res;
|
db::Track::pointer res;
|
||||||
@@ -413,7 +414,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
if (res)
|
if (res)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Found too many candidates for file move. New file = " << fileInfo.relativePath << ", candidate = " << track->getAbsoluteFilePath() << ", previous candidate = " << res->getAbsoluteFilePath());
|
LMS_LOG(DBUPDATER, DEBUG, "Found too many candidates for file move. New file = " << relativePath << ", candidate = " << track->getAbsoluteFilePath() << ", previous candidate = " << res->getAbsoluteFilePath());
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
res = track;
|
res = track;
|
||||||
@@ -474,6 +475,14 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::Db& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser)
|
||||||
|
: FileScanOperationBase{ std::move(fileToScan), db, settings }
|
||||||
|
, _parser{ parser }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileScanOperation::~AudioFileScanOperation() = default;
|
||||||
|
|
||||||
void AudioFileScanOperation::scan()
|
void AudioFileScanOperation::scan()
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
||||||
@@ -481,13 +490,13 @@ namespace lms::scanner
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_parsedTrack = _parser.parseMetaData(_file);
|
_parsedTrack = _parser.parseMetaData(getFilePath());
|
||||||
|
|
||||||
// We fill missing artist mbids with mbids found on other artist roles
|
// We fill missing artist mbids with mbids found on other artist roles
|
||||||
fillMissingMbids(*_parsedTrack);
|
fillMissingMbids(*_parsedTrack);
|
||||||
|
|
||||||
std::size_t index{};
|
std::size_t index{};
|
||||||
_parser.parseImages(_file, [&](const metadata::Image& image) {
|
_parser.parseImages(getFilePath(), [&](const metadata::Image& image) {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||||
@@ -508,46 +517,43 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
catch (const image::Exception& e)
|
catch (const image::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file);
|
addError<EmbeddedImageScanError>(getFilePath(), index);
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (const metadata::AudioFileNoAudioPropertiesException&)
|
||||||
|
{
|
||||||
|
addError<NoAudioTrackFoundError>(getFilePath());
|
||||||
|
}
|
||||||
|
catch (const metadata::IOException& e)
|
||||||
|
{
|
||||||
|
addError<IOScanError>(getFilePath(), e.getErrorCode());
|
||||||
|
}
|
||||||
catch (const metadata::Exception& e)
|
catch (const metadata::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << _file);
|
addError<AudioFileScanError>(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioFileScanOperation::processResult(ScanContext& context)
|
AudioFileScanOperation::OperationResult AudioFileScanOperation::processResult()
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||||
|
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ getDb().getTLSSession() };
|
||||||
|
db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) };
|
||||||
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 (!_parsedTrack)
|
||||||
{
|
{
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
track.remove();
|
track.remove();
|
||||||
stats.deletions++;
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadAudioFile);
|
return OperationResult::Skipped;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_parsedTrack->mbid && (!track || _settings.skipDuplicateTrackMBID))
|
if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
|
||||||
{
|
{
|
||||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||||
|
|
||||||
@@ -558,14 +564,14 @@ namespace lms::scanner
|
|||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << otherTrack->getAbsoluteFilePath());
|
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << otherTrack->getAbsoluteFilePath());
|
||||||
track = otherTrack;
|
track = otherTrack;
|
||||||
track.modify()->setAbsoluteFilePath(_file);
|
track.modify()->setAbsoluteFilePath(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip duplicate track MBID
|
// Skip duplicate track MBID
|
||||||
if (_settings.skipDuplicateTrackMBID)
|
if (getScannerSettings().skipDuplicateTrackMBID)
|
||||||
{
|
{
|
||||||
for (db::Track::pointer& otherTrack : duplicateTracks)
|
for (db::Track::pointer& otherTrack : duplicateTracks)
|
||||||
{
|
{
|
||||||
@@ -574,22 +580,26 @@ namespace lms::scanner
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
|
// 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),
|
auto& mediaLibraries{ getScannerSettings().mediaLibraries };
|
||||||
|
if (std::none_of(std::cbegin(mediaLibraries), std::cend(mediaLibraries),
|
||||||
[&](const MediaLibraryInfo& libraryInfo) {
|
[&](const MediaLibraryInfo& libraryInfo) {
|
||||||
return core::pathUtils::isPathInRootPath(_file, libraryInfo.rootDirectory, &excludeDirFileName);
|
return core::pathUtils::isPathInRootPath(getFilePath(), libraryInfo.rootDirectory, &excludeDirFileName);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (similar MBID in " << otherTrack->getAbsoluteFilePath() << ")");
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << getFilePath() << ": same MBID already found in " << otherTrack->getAbsoluteFilePath());
|
||||||
// As this MBID already exists, just remove what we just scanned
|
// As this MBID already exists, just remove what we just scanned
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
track.remove();
|
track.remove();
|
||||||
stats.deletions++;
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Removed " << getFilePath() << ": same MBID already found in " << otherTrack->getAbsoluteFilePath());
|
||||||
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
|
return OperationResult::Skipped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,27 +607,25 @@ namespace lms::scanner
|
|||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
// maybe the file just moved?
|
// maybe the file just moved?
|
||||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
|
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFileSize(), getRelativeFilePath());
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
|
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath());
|
||||||
track.modify()->setAbsoluteFilePath(_file);
|
track.modify()->setAbsoluteFilePath(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We estimate this is an audio file if the duration is not null
|
// We estimate this is an audio file if the duration is not null
|
||||||
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (duration is 0)");
|
addError<BadAudioDurationError>(getFilePath());
|
||||||
|
|
||||||
// If Track exists here, delete it!
|
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
track.remove();
|
track.remove();
|
||||||
stats.deletions++;
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
stats.errors.emplace_back(_file, ScanErrorType::BadDuration);
|
return OperationResult::Skipped;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ***** Title
|
// ***** Title
|
||||||
@@ -626,9 +634,9 @@ namespace lms::scanner
|
|||||||
title = _parsedTrack->title;
|
title = _parsedTrack->title;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO parse file name guess track etc.
|
// TODO parse file name to guess track etc.
|
||||||
// For now juste use file name as title
|
// For now, we just use file name as title
|
||||||
title = _file.filename().string();
|
title = getFilePath().filename().string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If file already exists, update its data
|
// If file already exists, update its data
|
||||||
@@ -639,13 +647,13 @@ namespace lms::scanner
|
|||||||
track = dbSession.create<db::Track>();
|
track = dbSession.create<db::Track>();
|
||||||
added = true;
|
added = true;
|
||||||
|
|
||||||
track.modify()->setAbsoluteFilePath(_file);
|
track.modify()->setAbsoluteFilePath(getFilePath());
|
||||||
track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime
|
track.modify()->setAddedTime(getMediaLibrary().firstScan ? getLastWriteTime() : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track related data
|
// Track related data
|
||||||
assert(track);
|
assert(track);
|
||||||
track.modify()->setScanVersion(_settings.audioScanVersion);
|
track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
|
||||||
|
|
||||||
// Audio properties
|
// Audio properties
|
||||||
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||||
@@ -654,9 +662,9 @@ namespace lms::scanner
|
|||||||
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||||
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||||
|
|
||||||
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
track.modify()->setRelativeFilePath(getRelativeFilePath());
|
||||||
track.modify()->setFileSize(fileInfo->fileSize);
|
track.modify()->setFileSize(getFileSize());
|
||||||
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
track.modify()->setLastWriteTime(getLastWriteTime());
|
||||||
|
|
||||||
if (_parsedTrack->encodingTime.isValid())
|
if (_parsedTrack->encodingTime.isValid())
|
||||||
{
|
{
|
||||||
@@ -672,14 +680,14 @@ namespace lms::scanner
|
|||||||
track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date });
|
track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date });
|
||||||
}
|
}
|
||||||
|
|
||||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
track.modify()->setMediaLibrary(mediaLibrary);
|
track.modify()->setMediaLibrary(mediaLibrary);
|
||||||
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary) };
|
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary) };
|
||||||
track.modify()->setDirectory(directory);
|
track.modify()->setDirectory(directory);
|
||||||
|
|
||||||
track.modify()->clearArtistLinks();
|
track.modify()->clearArtistLinks();
|
||||||
|
|
||||||
const helpers::AllowFallbackOnMBIDEntry allowFallback{ _settings.allowArtistMBIDFallback };
|
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
|
||||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
|
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
|
||||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
|
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
|
||||||
@@ -732,13 +740,11 @@ namespace lms::scanner
|
|||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << getFilePath());
|
||||||
stats.additions++;
|
return OperationResult::Added;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << _file);
|
|
||||||
stats.updates++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << getFilePath());
|
||||||
|
return OperationResult::Updated;
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -25,16 +25,21 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "image/Types.hpp"
|
#include "image/Types.hpp"
|
||||||
#include "metadata/IAudioFileParser.hpp"
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
|
#include "FileScanOperationBase.hpp"
|
||||||
#include "FileToScan.hpp"
|
#include "FileToScan.hpp"
|
||||||
#include "IFileScanner.hpp"
|
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|
||||||
|
namespace lms::metadata
|
||||||
|
{
|
||||||
|
class IAudioFileParser;
|
||||||
|
} // namespace lms::metadata
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
struct ImageInfo
|
struct ImageInfo
|
||||||
@@ -48,32 +53,20 @@ namespace lms::scanner
|
|||||||
std::string description;
|
std::string description;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AudioFileScanOperation : public IFileScanOperation
|
class AudioFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IAudioFileParser& parser, const ScannerSettings& settings)
|
AudioFileScanOperation(FileToScan&& fileToScan, db::Db& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser);
|
||||||
: _file{ fileToScan.file }
|
~AudioFileScanOperation() override;
|
||||||
, _mediaLibrary{ fileToScan.mediaLibrary }
|
|
||||||
, _db{ db }
|
|
||||||
, _parser{ parser }
|
|
||||||
, _settings{ settings }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
~AudioFileScanOperation() override = default;
|
|
||||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path& getFile() const override { return _file; };
|
|
||||||
core::LiteralString getName() const override { return "ScanAudioFile"; }
|
core::LiteralString getName() const override { return "ScanAudioFile"; }
|
||||||
void scan() override;
|
void scan() override;
|
||||||
void processResult(ScanContext& context) override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
const std::filesystem::path _file;
|
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
|
||||||
db::Db& _db;
|
|
||||||
metadata::IAudioFileParser& _parser;
|
metadata::IAudioFileParser& _parser;
|
||||||
const ScannerSettings& _settings;
|
|
||||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||||
std::vector<ImageInfo> _parsedImages;
|
std::vector<ImageInfo> _parsedImages;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
|
||||||
#include "AudioFileScanOperation.hpp"
|
#include "AudioFileScanOperation.hpp"
|
||||||
#include "ScanContext.hpp"
|
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -77,66 +77,29 @@ namespace lms::scanner
|
|||||||
return "Audio scanner";
|
return "Audio scanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> AudioFileScanner::getSupportedFiles() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
return _metadataParser->getSupportedExtensions();
|
return _metadataParser->getSupportedExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
bool AudioFileScanner::needsScan(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() };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ dbSession.createReadTransaction() };
|
||||||
|
|
||||||
{
|
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.filePath) };
|
||||||
auto transaction{ dbSession.createReadTransaction() };
|
return !track
|
||||||
|
|| track->getLastWriteTime() != file.lastWriteTime
|
||||||
// Skip file if last write is the same
|
|| track->getScanVersion() != _settings.audioScanVersion;
|
||||||
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
|
||||||
if (track
|
|
||||||
&& track->getLastWriteTime() == lastWriteTime
|
|
||||||
&& track->getScanVersion() == _settings.audioScanVersion)
|
|
||||||
{
|
|
||||||
// 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
|
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<AudioFileScanOperation>(fileToScan, _db, *_metadataParser, _settings);
|
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ namespace lms::scanner
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
core::LiteralString getName() const override;
|
core::LiteralString getName() const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedFiles() const override;
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const override;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 "FileScanOperationBase.hpp"
|
||||||
|
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
FileScanOperationBase::FileScanOperationBase(FileToScan&& fileToScan, db::Db& db, const ScannerSettings& settings)
|
||||||
|
: _file{ std::move(fileToScan) }
|
||||||
|
, _db{ db }
|
||||||
|
, _settings{ settings }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FileScanOperationBase::~FileScanOperationBase() = default;
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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 <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FileToScan.hpp"
|
||||||
|
#include "IFileScanOperation.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
} // namespace lms::db
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct ScanError;
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
|
class FileScanOperationBase : public IFileScanOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FileScanOperationBase(FileToScan&& fileToScan, db::Db& db, const ScannerSettings& settings);
|
||||||
|
~FileScanOperationBase() override;
|
||||||
|
FileScanOperationBase(const FileScanOperationBase&) = delete;
|
||||||
|
FileScanOperationBase& operator=(const FileScanOperationBase&) = delete;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const std::filesystem::path& getFilePath() const override { return _file.filePath; }
|
||||||
|
const MediaLibraryInfo& getMediaLibrary() const { return _file.mediaLibrary; }
|
||||||
|
db::Db& getDb() { return _db; }
|
||||||
|
const ScannerSettings& getScannerSettings() const { return _settings; }
|
||||||
|
Wt::WDateTime getLastWriteTime() const { return _file.lastWriteTime; }
|
||||||
|
std::size_t getFileSize() const { return _file.fileSize; }
|
||||||
|
const std::filesystem::path& getRelativeFilePath() const { return _file.relativePath; }
|
||||||
|
|
||||||
|
template<typename T, typename... CtrArgs>
|
||||||
|
void addError(CtrArgs&&... args)
|
||||||
|
{
|
||||||
|
_errors.emplace_back(std::make_shared<T>(std::forward<CtrArgs>(args)...));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScanErrorVector& getErrors() override { return _errors; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FileToScan _file;
|
||||||
|
db::Db& _db;
|
||||||
|
const ScannerSettings& _settings;
|
||||||
|
ScanErrorVector _errors;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -21,13 +21,18 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "MediaLibraryInfo.hpp"
|
#include "MediaLibraryInfo.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
struct FileToScan
|
struct FileToScan
|
||||||
{
|
{
|
||||||
std::filesystem::path file;
|
std::filesystem::path filePath;
|
||||||
|
std::filesystem::path relativePath;
|
||||||
MediaLibraryInfo mediaLibrary;
|
MediaLibraryInfo mediaLibrary;
|
||||||
|
Wt::WDateTime lastWriteTime;
|
||||||
|
std::size_t fileSize{};
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "core/LiteralString.hpp"
|
#include "core/LiteralString.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
struct ScanContext;
|
struct ScanError;
|
||||||
|
|
||||||
class IFileScanOperation
|
class IFileScanOperation
|
||||||
{
|
{
|
||||||
@@ -34,8 +36,23 @@ namespace lms::scanner
|
|||||||
|
|
||||||
virtual core::LiteralString getName() const = 0;
|
virtual core::LiteralString getName() const = 0;
|
||||||
|
|
||||||
virtual const std::filesystem::path& getFile() const = 0;
|
virtual const std::filesystem::path& getFilePath() const = 0;
|
||||||
|
|
||||||
|
// scan() is called asynchronously by a pool of threads
|
||||||
|
// processResult() is called sequentially by a single thread
|
||||||
virtual void scan() = 0;
|
virtual void scan() = 0;
|
||||||
virtual void processResult(ScanContext& context) = 0;
|
|
||||||
|
enum class OperationResult
|
||||||
|
{
|
||||||
|
Added,
|
||||||
|
Removed,
|
||||||
|
Updated,
|
||||||
|
Skipped,
|
||||||
|
};
|
||||||
|
virtual OperationResult processResult() = 0;
|
||||||
|
|
||||||
|
using ScanErrorVector = std::vector<std::shared_ptr<ScanError>>;
|
||||||
|
// list of errors collected during scan/result processing (there might be errors without skipping the file)
|
||||||
|
virtual const ScanErrorVector& getErrors() = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -29,8 +29,6 @@
|
|||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
class IFileScanOperation;
|
class IFileScanOperation;
|
||||||
struct ScanContext;
|
|
||||||
struct ScannerSettings;
|
|
||||||
|
|
||||||
class IFileScanner
|
class IFileScanner
|
||||||
{
|
{
|
||||||
@@ -38,8 +36,9 @@ namespace lms::scanner
|
|||||||
virtual ~IFileScanner() = default;
|
virtual ~IFileScanner() = default;
|
||||||
|
|
||||||
virtual core::LiteralString getName() const = 0;
|
virtual core::LiteralString getName() const = 0;
|
||||||
|
virtual std::span<const std::filesystem::path> getSupportedFiles() const = 0;
|
||||||
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||||
virtual bool needsScan(ScanContext& context, const FileToScan& file) const = 0;
|
virtual bool needsScan(const FileToScan& file) const = 0;
|
||||||
virtual std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const = 0;
|
virtual std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -27,34 +27,26 @@
|
|||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/IRawImage.hpp"
|
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
|
#include "FileScanOperationBase.hpp"
|
||||||
#include "IFileScanOperation.hpp"
|
#include "IFileScanOperation.hpp"
|
||||||
#include "ScanContext.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ImageFileScanOperation : public IFileScanOperation
|
class ImageFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ImageFileScanOperation(const FileToScan& file, db::Db& db)
|
using FileScanOperationBase::FileScanOperationBase;
|
||||||
: _file{ file.file }
|
|
||||||
, _mediaLibrary{ file.mediaLibrary }
|
|
||||||
, _db{ db } {}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path& getFile() const override { return _file; };
|
|
||||||
core::LiteralString getName() const override { return "ScanImageFile"; }
|
core::LiteralString getName() const override { return "ScanImageFile"; }
|
||||||
void scan() override;
|
void scan() override;
|
||||||
void processResult(ScanContext& context) override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
const std::filesystem::path _file;
|
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
|
||||||
db::Db& _db;
|
|
||||||
|
|
||||||
std::optional<image::ImageProperties> _parsedImageProperties;
|
std::optional<image::ImageProperties> _parsedImageProperties;
|
||||||
};
|
};
|
||||||
@@ -63,67 +55,58 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_parsedImageProperties = image::probeImage(_file);
|
_parsedImageProperties = image::probeImage(getFilePath());
|
||||||
}
|
}
|
||||||
catch (const image::Exception& e)
|
catch (const image::Exception& e)
|
||||||
{
|
{
|
||||||
_parsedImageProperties.reset();
|
_parsedImageProperties.reset();
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file " << _file << ": " << e.what());
|
addError<ImageFileScanError>(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageFileScanOperation::processResult(ScanContext& context)
|
ImageFileScanOperation::OperationResult ImageFileScanOperation::processResult()
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ getDb().getTLSSession() };
|
||||||
|
db::Image::pointer image{ db::Image::find(dbSession, getFilePath()) };
|
||||||
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 (!_parsedImageProperties)
|
if (!_parsedImageProperties)
|
||||||
{
|
{
|
||||||
if (image)
|
if (image)
|
||||||
{
|
{
|
||||||
image.remove();
|
image.remove();
|
||||||
stats.deletions++;
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removed image " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Removed image " << getFilePath());
|
||||||
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadImageFile);
|
|
||||||
return;
|
return OperationResult::Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool added{ !image };
|
const bool added{ !image };
|
||||||
if (!image)
|
if (!image)
|
||||||
image = dbSession.create<db::Image>(_file);
|
image = dbSession.create<db::Image>(getFilePath());
|
||||||
|
|
||||||
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
image.modify()->setLastWriteTime(getLastWriteTime());
|
||||||
image.modify()->setFileSize(fileInfo->fileSize);
|
image.modify()->setFileSize(getFileSize());
|
||||||
image.modify()->setHeight(_parsedImageProperties->height);
|
image.modify()->setHeight(_parsedImageProperties->height);
|
||||||
image.modify()->setWidth(_parsedImageProperties->width);
|
image.modify()->setWidth(_parsedImageProperties->width);
|
||||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
image.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
image.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added image " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Added image " << getFilePath());
|
||||||
stats.additions++;
|
return OperationResult::Added;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated image " << _file);
|
|
||||||
stats.updates++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated image " << getFilePath());
|
||||||
|
return OperationResult::Updated;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ImageFileScanner::ImageFileScanner(db::Db& db)
|
ImageFileScanner::ImageFileScanner(db::Db& db, ScannerSettings& settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
|
, _settings{ settings }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,41 +115,27 @@ namespace lms::scanner
|
|||||||
return "Image scanner";
|
return "Image scanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> ImageFileScanner::getSupportedFiles() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::span<const std::filesystem::path> ImageFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> ImageFileScanner::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
return image::getSupportedFileExtensions();
|
return image::getSupportedFileExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ImageFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
bool ImageFileScanner::needsScan(const FileToScan& file) const
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
const db::Image::pointer image{ db::Image::find(dbSession, file.filePath) };
|
||||||
// Should rarely fail as we are currently iterating it
|
return (!image || image->getLastWriteTime() != file.lastWriteTime);
|
||||||
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
|
std::unique_ptr<IFileScanOperation> ImageFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<ImageFileScanOperation>(fileToScan, _db);
|
return std::make_unique<ImageFileScanOperation>(std::move(fileToScan), _db, _settings);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -36,17 +36,19 @@ namespace lms::scanner
|
|||||||
class ImageFileScanner : public IFileScanner
|
class ImageFileScanner : public IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ImageFileScanner(db::Db& db);
|
ImageFileScanner(db::Db& db, ScannerSettings& settings);
|
||||||
~ImageFileScanner() override = default;
|
~ImageFileScanner() override = default;
|
||||||
ImageFileScanner(const ImageFileScanner&) = delete;
|
ImageFileScanner(const ImageFileScanner&) = delete;
|
||||||
ImageFileScanner& operator=(const ImageFileScanner&) = delete;
|
ImageFileScanner& operator=(const ImageFileScanner&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
core::LiteralString getName() const override;
|
core::LiteralString getName() const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedFiles() const override;
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const override;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
ScannerSettings& _settings;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -22,38 +22,31 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "FileScanOperationBase.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/TrackLyrics.hpp"
|
#include "database/TrackLyrics.hpp"
|
||||||
#include "metadata/Lyrics.hpp"
|
#include "metadata/Lyrics.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
#include "IFileScanOperation.hpp"
|
|
||||||
#include "ScanContext.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class LyricsFileScanOperation : public IFileScanOperation
|
class LyricsFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LyricsFileScanOperation(const FileToScan& file, db::Db& db)
|
using FileScanOperationBase::FileScanOperationBase;
|
||||||
: _file{ file.file }
|
|
||||||
, _mediaLibrary{ file.mediaLibrary }
|
|
||||||
, _db{ db } {}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path& getFile() const override { return _file; };
|
|
||||||
core::LiteralString getName() const override { return "ScanLyricsFile"; }
|
core::LiteralString getName() const override { return "ScanLyricsFile"; }
|
||||||
void scan() override;
|
void scan() override;
|
||||||
void processResult(ScanContext& context) override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
const std::filesystem::path _file;
|
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
|
||||||
db::Db& _db;
|
|
||||||
|
|
||||||
std::optional<metadata::Lyrics> _parsedLyrics;
|
std::optional<metadata::Lyrics> _parsedLyrics;
|
||||||
};
|
};
|
||||||
@@ -62,53 +55,50 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::ifstream ifs{ _file };
|
std::ifstream ifs{ getFilePath() };
|
||||||
if (!ifs)
|
if (!ifs)
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file);
|
{
|
||||||
else
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
_parsedLyrics = metadata::parseLyrics(ifs);
|
|
||||||
|
addError<IOScanError>(getFilePath(), ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parsedLyrics = metadata::parseLyrics(ifs);
|
||||||
}
|
}
|
||||||
catch (const metadata::Exception& e)
|
catch (const metadata::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file " << _file << ": " << e.what());
|
addError<LyricsFileScanError>(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LyricsFileScanOperation::processResult(ScanContext& context)
|
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ getDb().getTLSSession() };
|
||||||
|
db::TrackLyrics::pointer trackLyrics{ db::TrackLyrics::find(dbSession, getFilePath()) };
|
||||||
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 (!_parsedLyrics)
|
||||||
{
|
{
|
||||||
if (trackLyrics)
|
if (trackLyrics)
|
||||||
{
|
{
|
||||||
trackLyrics.remove();
|
trackLyrics.remove();
|
||||||
stats.deletions++;
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removed lyrics file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Removed lyrics file " << getFilePath());
|
||||||
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadLyricsFile);
|
|
||||||
return;
|
return OperationResult::Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool added{ !trackLyrics };
|
const bool added{ !trackLyrics };
|
||||||
if (!trackLyrics)
|
if (!trackLyrics)
|
||||||
{
|
{
|
||||||
trackLyrics = dbSession.create<db::TrackLyrics>();
|
trackLyrics = dbSession.create<db::TrackLyrics>();
|
||||||
trackLyrics.modify()->setAbsoluteFilePath(_file);
|
trackLyrics.modify()->setAbsoluteFilePath(getFilePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
trackLyrics.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
trackLyrics.modify()->setLastWriteTime(getLastWriteTime());
|
||||||
trackLyrics.modify()->setFileSize(fileInfo->fileSize);
|
trackLyrics.modify()->setFileSize(getFileSize());
|
||||||
trackLyrics.modify()->setLanguage(!_parsedLyrics->language.empty() ? _parsedLyrics->language : "xxx");
|
trackLyrics.modify()->setLanguage(!_parsedLyrics->language.empty() ? _parsedLyrics->language : "xxx");
|
||||||
trackLyrics.modify()->setOffset(_parsedLyrics->offset);
|
trackLyrics.modify()->setOffset(_parsedLyrics->offset);
|
||||||
trackLyrics.modify()->setDisplayTitle(_parsedLyrics->displayTitle);
|
trackLyrics.modify()->setDisplayTitle(_parsedLyrics->displayTitle);
|
||||||
@@ -118,24 +108,23 @@ namespace lms::scanner
|
|||||||
else
|
else
|
||||||
trackLyrics.modify()->setUnsynchronizedLines(_parsedLyrics->unsynchronizedLines);
|
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
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
trackLyrics.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
trackLyrics.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics " << getFilePath());
|
||||||
stats.additions++;
|
return OperationResult::Added;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics " << _file);
|
|
||||||
stats.updates++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics " << getFilePath());
|
||||||
|
return OperationResult::Updated;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
LyricsFileScanner::LyricsFileScanner(db::Db& db)
|
LyricsFileScanner::LyricsFileScanner(db::Db& db, ScannerSettings& _settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
|
, _settings{ _settings }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,41 +133,27 @@ namespace lms::scanner
|
|||||||
return "Lyrics scanner";
|
return "Lyrics scanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedFiles() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
return metadata::getSupportedLyricsFileExtensions();
|
return metadata::getSupportedLyricsFileExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LyricsFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
bool LyricsFileScanner::needsScan(const FileToScan& file) const
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
const db::TrackLyrics::pointer lyrics{ db::TrackLyrics::find(dbSession, file.filePath) };
|
||||||
// Should rarely fail as we are currently iterating it
|
return !lyrics || lyrics->getLastWriteTime() != file.lastWriteTime;
|
||||||
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
|
std::unique_ptr<IFileScanOperation> LyricsFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<LyricsFileScanOperation>(fileToScan, _db);
|
return std::make_unique<LyricsFileScanOperation>(std::move(fileToScan), _db, _settings);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -36,17 +36,19 @@ namespace lms::scanner
|
|||||||
class LyricsFileScanner : public IFileScanner
|
class LyricsFileScanner : public IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LyricsFileScanner(db::Db& db);
|
LyricsFileScanner(db::Db& db, ScannerSettings& settings);
|
||||||
~LyricsFileScanner() override = default;
|
~LyricsFileScanner() override = default;
|
||||||
LyricsFileScanner(const LyricsFileScanner&) = delete;
|
LyricsFileScanner(const LyricsFileScanner&) = delete;
|
||||||
LyricsFileScanner& operator=(const LyricsFileScanner&) = delete;
|
LyricsFileScanner& operator=(const LyricsFileScanner&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
core::LiteralString getName() const override;
|
core::LiteralString getName() const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedFiles() const override;
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const override;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
ScannerSettings& _settings;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
#include "metadata/PlayList.hpp"
|
#include "metadata/PlayList.hpp"
|
||||||
|
|
||||||
#include "IFileScanOperation.hpp"
|
#include "FileScanOperationBase.hpp"
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -38,26 +38,18 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class PlayListFileScanOperation : public IFileScanOperation
|
class PlayListFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PlayListFileScanOperation(const FileToScan& file, db::Db& db)
|
using FileScanOperationBase::FileScanOperationBase;
|
||||||
: _file{ file.file }
|
|
||||||
, _mediaLibrary{ file.mediaLibrary }
|
|
||||||
, _db{ db } {}
|
|
||||||
~PlayListFileScanOperation() override = default;
|
~PlayListFileScanOperation() override = default;
|
||||||
PlayListFileScanOperation(const PlayListFileScanOperation&) = delete;
|
PlayListFileScanOperation(const PlayListFileScanOperation&) = delete;
|
||||||
PlayListFileScanOperation& operator=(const PlayListFileScanOperation&) = delete;
|
PlayListFileScanOperation& operator=(const PlayListFileScanOperation&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path& getFile() const override { return _file; };
|
|
||||||
core::LiteralString getName() const override { return "ScanPlayListFile"; }
|
core::LiteralString getName() const override { return "ScanPlayListFile"; }
|
||||||
void scan() override;
|
void scan() override;
|
||||||
void processResult(ScanContext& context) override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
const std::filesystem::path _file;
|
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
|
||||||
db::Db& _db;
|
|
||||||
|
|
||||||
std::optional<metadata::PlayList> _parsedPlayList;
|
std::optional<metadata::PlayList> _parsedPlayList;
|
||||||
};
|
};
|
||||||
@@ -66,74 +58,72 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::ifstream ifs{ _file };
|
std::ifstream ifs{ getFilePath() };
|
||||||
if (!ifs)
|
if (!ifs)
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file);
|
{
|
||||||
else
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
_parsedPlayList = metadata::parsePlayList(ifs);
|
|
||||||
|
addError<IOScanError>(getFilePath(), ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parsedPlayList = metadata::parsePlayList(ifs);
|
||||||
}
|
}
|
||||||
catch (const metadata::Exception& e)
|
catch (const metadata::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot read playlist in file " << _file << ": " << e.what());
|
addError<PlayListFileScanError>(getFilePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayListFileScanOperation::processResult(ScanContext& context)
|
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ getDb().getTLSSession() };
|
||||||
|
db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, getFilePath()) };
|
||||||
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 (!_parsedPlayList)
|
||||||
{
|
{
|
||||||
if (playList)
|
if (playList)
|
||||||
{
|
{
|
||||||
playList.remove();
|
playList.remove();
|
||||||
stats.deletions++;
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removed playlist file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Removed playlist file " << getFilePath());
|
||||||
|
return OperationResult::Removed;
|
||||||
}
|
}
|
||||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadPlayListFile);
|
|
||||||
return;
|
return OperationResult::Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool added{ !playList };
|
const bool added{ !playList };
|
||||||
if (!playList)
|
if (!playList)
|
||||||
playList = dbSession.create<db::PlayListFile>(_file);
|
playList = dbSession.create<db::PlayListFile>(getFilePath());
|
||||||
|
|
||||||
playList.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
playList.modify()->setLastWriteTime(getLastWriteTime());
|
||||||
playList.modify()->setFileSize(fileInfo->fileSize);
|
playList.modify()->setFileSize(getFileSize());
|
||||||
if (!_parsedPlayList->name.empty())
|
if (!_parsedPlayList->name.empty())
|
||||||
playList.modify()->setName(_parsedPlayList->name);
|
playList.modify()->setName(_parsedPlayList->name);
|
||||||
else
|
else
|
||||||
playList.modify()->setName(_file.stem().string());
|
playList.modify()->setName(getFilePath().stem().string());
|
||||||
playList.modify()->setFiles(_parsedPlayList->files);
|
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
|
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
playList.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
playList.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Added playlist file " << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Added playlist file " << getFilePath());
|
||||||
stats.additions++;
|
return OperationResult::Added;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << _file);
|
LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << getFilePath());
|
||||||
stats.updates++;
|
return OperationResult::Updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PlayListFileScanner::PlayListFileScanner(db::Db& db)
|
PlayListFileScanner::PlayListFileScanner(db::Db& db, ScannerSettings& settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
|
, _settings{ settings }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,41 +132,27 @@ namespace lms::scanner
|
|||||||
return "PlayList scanner";
|
return "PlayList scanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedFiles() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
return metadata::getSupportedPlayListFileExtensions();
|
return metadata::getSupportedPlayListFileExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlayListFileScanner::needsScan(ScanContext& context, const FileToScan& file) const
|
bool PlayListFileScanner::needsScan(const FileToScan& file) const
|
||||||
{
|
{
|
||||||
ScanStats& stats{ context.stats };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||||
|
|
||||||
const Wt::WDateTime lastWriteTime{ utils::retrieveFileGetLastWrite(file.file) };
|
const db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, file.filePath) };
|
||||||
// Should rarely fail as we are currently iterating it
|
return !playList || playList->getLastWriteTime() != file.lastWriteTime;
|
||||||
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
|
std::unique_ptr<IFileScanOperation> PlayListFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<PlayListFileScanOperation>(fileToScan, _db);
|
return std::make_unique<PlayListFileScanOperation>(std::move(fileToScan), _db, _settings);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -36,17 +36,19 @@ namespace lms::scanner
|
|||||||
class PlayListFileScanner : public IFileScanner
|
class PlayListFileScanner : public IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PlayListFileScanner(db::Db& db);
|
PlayListFileScanner(db::Db& db, ScannerSettings& settings);
|
||||||
~PlayListFileScanner() override = default;
|
~PlayListFileScanner() override = default;
|
||||||
PlayListFileScanner(const PlayListFileScanner&) = delete;
|
PlayListFileScanner(const PlayListFileScanner&) = delete;
|
||||||
PlayListFileScanner& operator=(const PlayListFileScanner&) = delete;
|
PlayListFileScanner& operator=(const PlayListFileScanner&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
core::LiteralString getName() const override;
|
core::LiteralString getName() const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedFiles() const override;
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(FileToScan&& fileToScan) const override;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
ScannerSettings& _settings;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include <system_error>
|
||||||
|
|
||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
@@ -27,59 +28,6 @@
|
|||||||
|
|
||||||
namespace lms::scanner::utils
|
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 getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::MediaLibrary::pointer& mediaLibrary)
|
||||||
{
|
{
|
||||||
db::Directory::pointer directory{ db::Directory::find(session, path) };
|
db::Directory::pointer directory{ db::Directory::find(session, path) };
|
||||||
|
|||||||
@@ -20,9 +20,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
|
||||||
|
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
|
|
||||||
@@ -35,18 +32,8 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
struct FileInfo
|
|
||||||
{
|
|
||||||
Wt::WDateTime lastWriteTime;
|
|
||||||
std::filesystem::path relativePath;
|
|
||||||
std::size_t fileSize{};
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace utils
|
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);
|
db::ObjectPtr<db::Directory> getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr<db::MediaLibrary>& mediaLibrary);
|
||||||
} // namespace utils
|
} // namespace utils
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -53,7 +53,7 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", operation->getName());
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", operation->getName());
|
||||||
LMS_LOG(DBUPDATER, DEBUG, operation->getName() << ": scanning file " << operation->getFile());
|
LMS_LOG(DBUPDATER, DEBUG, operation->getName() << ": scanning file " << operation->getFilePath());
|
||||||
operation->scan();
|
operation->scan();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/LiteralString.hpp"
|
#include "core/LiteralString.hpp"
|
||||||
|
#include "services/scanner/ScannerStats.hpp"
|
||||||
#include "ScanContext.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
struct ScanContext;
|
||||||
|
|
||||||
class IScanStep
|
class IScanStep
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ScanErrorLogger.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
void ScanErrorLogger::visit([[maybe_unused]] const scanner::ScanError& error)
|
||||||
|
{
|
||||||
|
// There should never be a ScanError without a specific type
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::IOScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to open file " << error.path << ": " << error.err.message());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::AudioFileScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::EmbeddedImageScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << error.path << " at index " << error.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::NoAudioTrackFoundError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << error.path << ": no audio track found");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::BadAudioDurationError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << error.path << ": duration is 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::ArtistInfoFileScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to read artist info file " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::MissingArtistNameError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to read artist info file " << error.path << ": missing name");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::ImageFileScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to read image file " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::LyricsFileScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to read lyrics file " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::PlayListFileScanError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to read playlist file " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::PlayListFilePathMissingError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Track " << error.entry << " not found in playlist " << error.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanErrorLogger::visit(const scanner::PlayListFileAllPathesMissingError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to parse playlist " << error.path << ": all entries are missing");
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanErrorLogger : public scanner::ScanErrorVisitor
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
void visit(const scanner::ScanError&) override;
|
||||||
|
void visit(const scanner::IOScanError& error) override;
|
||||||
|
void visit(const scanner::AudioFileScanError& error) override;
|
||||||
|
void visit(const scanner::EmbeddedImageScanError& error) override;
|
||||||
|
void visit(const scanner::NoAudioTrackFoundError& error) override;
|
||||||
|
void visit(const scanner::BadAudioDurationError& error) override;
|
||||||
|
void visit(const scanner::ArtistInfoFileScanError& error) override;
|
||||||
|
void visit(const scanner::MissingArtistNameError& error) override;
|
||||||
|
void visit(const scanner::ImageFileScanError& error) override;
|
||||||
|
void visit(const scanner::LyricsFileScanError& error) override;
|
||||||
|
void visit(const scanner::PlayListFileScanError& error) override;
|
||||||
|
void visit(const scanner::PlayListFilePathMissingError& error) override;
|
||||||
|
void visit(const scanner::PlayListFileAllPathesMissingError& error) override;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "database/TrackList.hpp"
|
#include "database/TrackList.hpp"
|
||||||
#include "metadata/Types.hpp"
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "helpers/ArtistHelpers.hpp"
|
#include "helpers/ArtistHelpers.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackLyrics.hpp"
|
#include "database/TrackLyrics.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackList.hpp"
|
#include "database/TrackList.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
@@ -57,6 +59,7 @@ namespace lms::scanner
|
|||||||
db::PlayListFileId lastRetrievedPlayListFileId;
|
db::PlayListFileId lastRetrievedPlayListFileId;
|
||||||
std::size_t processedPlayListFileCount{};
|
std::size_t processedPlayListFileCount{};
|
||||||
const ScannerSettings& settings;
|
const ScannerSettings& settings;
|
||||||
|
std::vector<std::shared_ptr<ScanError>> errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
db::Track::pointer getMatchingTrack(db::Session& session, const std::filesystem::path& filePath, const db::Directory::pointer& playListDirectory)
|
db::Track::pointer getMatchingTrack(db::Session& session, const std::filesystem::path& filePath, const db::Directory::pointer& playListDirectory)
|
||||||
@@ -81,10 +84,7 @@ namespace lms::scanner
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
const db::ReleaseId releaseId{ tracks.front().releaseId };
|
const db::ReleaseId releaseId{ tracks.front().releaseId };
|
||||||
if (std::all_of(std::cbegin(tracks) + 1, std::cend(tracks), [=](const TrackInfo& trackInfo) { return trackInfo.releaseId == releaseId; }))
|
return std::all_of(std::cbegin(tracks) + 1, std::cend(tracks), [=](const TrackInfo& trackInfo) { return trackInfo.releaseId == releaseId; });
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span<const TrackInfo> tracks, const db::TrackList::pointer& trackList)
|
bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span<const TrackInfo> tracks, const db::TrackList::pointer& trackList)
|
||||||
@@ -124,17 +124,28 @@ namespace lms::scanner
|
|||||||
|
|
||||||
playListAssociation.playListFileIdId = playListFile->getId();
|
playListAssociation.playListFileIdId = playListFile->getId();
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<ScanError>> pendingErrors;
|
||||||
|
|
||||||
const auto files{ playListFile->getFiles() };
|
const auto files{ playListFile->getFiles() };
|
||||||
for (const std::filesystem::path& file : files)
|
for (const std::filesystem::path& file : files)
|
||||||
{
|
{
|
||||||
// TODO optim: no need to fetch the whole track
|
// TODO optim: no need to fetch the whole track
|
||||||
db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) };
|
const db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) };
|
||||||
if (track)
|
if (track)
|
||||||
playListAssociation.tracks.push_back(TrackInfo{ .trackId = track->getId(), .releaseId = track->getReleaseId() });
|
playListAssociation.tracks.push_back(TrackInfo{ .trackId = track->getId(), .releaseId = track->getReleaseId() });
|
||||||
else
|
else
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Track " << file << " not found in playlist " << playListFile->getAbsoluteFilePath());
|
{
|
||||||
|
pendingErrors.emplace_back(std::make_shared<PlayListFilePathMissingError>(playListFile->getAbsoluteFilePath(), file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingErrors.size() == files.size())
|
||||||
|
{
|
||||||
|
pendingErrors.clear();
|
||||||
|
pendingErrors.emplace_back(std::make_shared<PlayListFileAllPathesMissingError>(playListFile->getAbsoluteFilePath()));
|
||||||
|
}
|
||||||
|
searchContext.errors.insert(std::end(searchContext.errors), std::begin(pendingErrors), std::end(pendingErrors));
|
||||||
|
|
||||||
if (playListAssociation.tracks.empty()
|
if (playListAssociation.tracks.empty()
|
||||||
|| (searchContext.settings.skipSingleReleasePlayLists && isSingleReleasePlayList(playListAssociation.tracks)))
|
|| (searchContext.settings.skipSingleReleasePlayLists && isSingleReleasePlayList(playListAssociation.tracks)))
|
||||||
{
|
{
|
||||||
@@ -237,6 +248,7 @@ namespace lms::scanner
|
|||||||
.session = session,
|
.session = session,
|
||||||
.lastRetrievedPlayListFileId = {},
|
.lastRetrievedPlayListFileId = {},
|
||||||
.settings = _settings,
|
.settings = _settings,
|
||||||
|
.errors = context.stats.errors
|
||||||
};
|
};
|
||||||
|
|
||||||
PlayListFileAssociationContainer playListFileAssociations;
|
PlayListFileAssociationContainer playListFileAssociations;
|
||||||
@@ -246,7 +258,12 @@ namespace lms::scanner
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
updatePlayListFiles(session, playListFileAssociations);
|
updatePlayListFiles(session, playListFileAssociations);
|
||||||
|
|
||||||
context.currentStepStats.processedElems = searchContext.processedPlayListFileCount;
|
context.currentStepStats.processedElems = searchContext.processedPlayListFileCount;
|
||||||
|
for (const std::shared_ptr<ScanError>& error : searchContext.errors)
|
||||||
|
addError(context, error);
|
||||||
|
searchContext.errors.clear();
|
||||||
|
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
|
|
||||||
#include "ScanStepBase.hpp"
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
|
#include "core/String.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
ScanStepBase::ScanStepBase(InitParams& initParams)
|
ScanStepBase::ScanStepBase(InitParams& initParams)
|
||||||
@@ -29,7 +34,58 @@ namespace lms::scanner
|
|||||||
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
||||||
, _lastScanSettings{ initParams.lastScanSettings }
|
, _lastScanSettings{ initParams.lastScanSettings }
|
||||||
{
|
{
|
||||||
|
for (IFileScanner* scanner : _fileScanners)
|
||||||
|
{
|
||||||
|
for (const std::filesystem::path& file : scanner->getSupportedFiles())
|
||||||
|
{
|
||||||
|
[[maybe_unused]] auto [it, inserted]{ _scannerByFile.emplace(file, scanner) };
|
||||||
|
assert(inserted);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
||||||
|
{
|
||||||
|
[[maybe_unused]] auto [it, inserted]{ _scannerByExtension.emplace(extension, scanner) };
|
||||||
|
assert(inserted);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanStepBase::~ScanStepBase() = default;
|
ScanStepBase::~ScanStepBase() = default;
|
||||||
|
|
||||||
|
IFileScanner* ScanStepBase::selectFileScanner(const std::filesystem::path& filePath) const
|
||||||
|
{
|
||||||
|
{
|
||||||
|
const std::string fileName{ core::stringUtils::stringToLower(filePath.filename().string()) };
|
||||||
|
|
||||||
|
auto itScanner{ _scannerByFile.find(fileName) };
|
||||||
|
if (itScanner != std::cend(_scannerByFile))
|
||||||
|
return itScanner->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const std::string extension{ core::stringUtils::stringToLower(filePath.extension().string()) };
|
||||||
|
|
||||||
|
auto itScanner{ _scannerByExtension.find(extension) };
|
||||||
|
if (itScanner != std::cend(_scannerByExtension))
|
||||||
|
return itScanner->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepBase::visitFileScanners(const std::function<void(IFileScanner*)>& visitor) const
|
||||||
|
{
|
||||||
|
for (IFileScanner* scanner : _fileScanners)
|
||||||
|
visitor(scanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepBase::addError(ScanContext& context, std::shared_ptr<ScanError> error)
|
||||||
|
{
|
||||||
|
error->accept(_scanErrorLogger);
|
||||||
|
|
||||||
|
context.stats.errorsCount++;
|
||||||
|
|
||||||
|
if (context.stats.errors.size() < ScanStats::maxStoredErrorCount)
|
||||||
|
context.stats.errors.emplace_back(error);
|
||||||
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "IScanStep.hpp"
|
#include "IScanStep.hpp"
|
||||||
|
#include "ScanErrorLogger.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,7 @@ namespace lms::scanner
|
|||||||
class IFileScanner;
|
class IFileScanner;
|
||||||
struct ScannerSettings;
|
struct ScannerSettings;
|
||||||
struct ScanStepStats;
|
struct ScanStepStats;
|
||||||
|
struct ScanContext;
|
||||||
|
|
||||||
class ScanStepBase : public IScanStep
|
class ScanStepBase : public IScanStep
|
||||||
{
|
{
|
||||||
@@ -57,14 +60,29 @@ namespace lms::scanner
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
const ScannerSettings* getLastScanSettings() const { return _lastScanSettings; }
|
const ScannerSettings* getLastScanSettings() const { return _lastScanSettings; }
|
||||||
|
IFileScanner* selectFileScanner(const std::filesystem::path& filePath) const;
|
||||||
|
void visitFileScanners(const std::function<void(IFileScanner*)>& visitor) const;
|
||||||
|
|
||||||
|
void addError(ScanContext& context, std::shared_ptr<ScanError> error);
|
||||||
|
|
||||||
|
template<typename T, typename... CtrArgs>
|
||||||
|
void addError(ScanContext& context, CtrArgs&&... args)
|
||||||
|
{
|
||||||
|
auto error{ std::make_shared<T>(std::forward<CtrArgs>(args)...) };
|
||||||
|
addError(context, error);
|
||||||
|
}
|
||||||
|
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
ProgressCallback _progressCallback;
|
ProgressCallback _progressCallback;
|
||||||
bool& _abortScan;
|
bool& _abortScan;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
std::vector<IFileScanner*> _fileScanners;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::unordered_map<std::filesystem::path, IFileScanner*> _scannerByFile;
|
||||||
|
std::unordered_map<std::filesystem::path, IFileScanner*> _scannerByExtension;
|
||||||
|
std::vector<IFileScanner*> _fileScanners;
|
||||||
|
|
||||||
const ScannerSettings* _lastScanSettings{};
|
const ScannerSettings* _lastScanSettings{};
|
||||||
|
ScanErrorLogger _scanErrorLogger;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
bool ScanStepCheckForDuplicatedFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
bool ScanStepCheckForDuplicatedFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
#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"
|
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
@@ -62,22 +62,15 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
|
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
|
||||||
|
|
||||||
std::vector<std::filesystem::path> supportedFileExtensions;
|
checkForRemovedFiles<db::Track>(context);
|
||||||
for (IFileScanner* scanner : _fileScanners)
|
checkForRemovedFiles<db::Image>(context);
|
||||||
{
|
checkForRemovedFiles<db::TrackLyrics>(context);
|
||||||
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
checkForRemovedFiles<db::PlayListFile>(context);
|
||||||
supportedFileExtensions.emplace_back(extension);
|
checkForRemovedFiles<db::ArtistInfo>(context);
|
||||||
}
|
|
||||||
|
|
||||||
checkForRemovedFiles<db::Track>(context, supportedFileExtensions);
|
|
||||||
checkForRemovedFiles<db::Image>(context, supportedFileExtensions);
|
|
||||||
checkForRemovedFiles<db::TrackLyrics>(context, supportedFileExtensions);
|
|
||||||
checkForRemovedFiles<db::PlayListFile>(context, supportedFileExtensions);
|
|
||||||
checkForRemovedFiles<db::ArtistInfo>(context, supportedFileExtensions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename Object>
|
template<typename Object>
|
||||||
void ScanStepCheckForRemovedFiles::checkForRemovedFiles(ScanContext& context, std::span<const std::filesystem::path> supportedFileExtensions)
|
void ScanStepCheckForRemovedFiles::checkForRemovedFiles(ScanContext& context)
|
||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
@@ -110,7 +103,7 @@ namespace lms::scanner
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkFile(object->getAbsoluteFilePath(), supportedFileExtensions))
|
if (!checkFile(object->getAbsoluteFilePath()))
|
||||||
objectsToRemove.push_back(object);
|
objectsToRemove.push_back(object);
|
||||||
|
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
@@ -132,7 +125,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScanStepCheckForRemovedFiles::checkFile(const std::filesystem::path& p, std::span<const std::filesystem::path> allowedExtensions)
|
bool ScanStepCheckForRemovedFiles::checkFile(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -140,7 +133,7 @@ namespace lms::scanner
|
|||||||
// and still belongs to a media directory
|
// and still belongs to a media directory
|
||||||
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
|
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removing '" << p.string() << "': missing");
|
LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": missing");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,13 +142,13 @@ namespace lms::scanner
|
|||||||
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
|
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removing '" << p.string() << "': out of media directory");
|
LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": out of media directory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!core::pathUtils::hasFileAnyExtension(p, allowedExtensions))
|
if (!selectFileScanner(p))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Removing '" << p.string() << "': file format no longer handled");
|
LMS_LOG(DBUPDATER, DEBUG, "Removing " << p.string() << ": file format no longer handled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <span>
|
|
||||||
|
|
||||||
#include "ScanStepBase.hpp"
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
@@ -38,8 +37,8 @@ namespace lms::scanner
|
|||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
template<typename Object>
|
template<typename Object>
|
||||||
void checkForRemovedFiles(ScanContext& context, std::span<const std::filesystem::path> supportedFileExtensions);
|
void checkForRemovedFiles(ScanContext& context);
|
||||||
|
|
||||||
bool checkFile(const std::filesystem::path& p, std::span<const std::filesystem::path> allowedExtensions);
|
bool checkFile(const std::filesystem::path& p);
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
bool ScanStepCompact::needProcess(const ScanContext& context) const
|
bool ScanStepCompact::needProcess(const ScanContext& context) const
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const
|
bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
|
|
||||||
#include "MediaLibraryInfo.hpp"
|
#include "MediaLibraryInfo.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "scanners/IFileScanner.hpp"
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -38,13 +39,6 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
context.stats.totalFileCount = 0;
|
context.stats.totalFileCount = 0;
|
||||||
|
|
||||||
std::vector<std::filesystem::path> supportedFileExtensions;
|
|
||||||
for (IFileScanner* scanner : _fileScanners)
|
|
||||||
{
|
|
||||||
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
|
||||||
supportedFileExtensions.emplace_back(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
{
|
{
|
||||||
std::size_t currentDirectoryProcessElemsCount{};
|
std::size_t currentDirectoryProcessElemsCount{};
|
||||||
@@ -53,7 +47,8 @@ namespace lms::scanner
|
|||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!ec && core::pathUtils::hasFileAnyExtension(path, supportedFileExtensions))
|
// we don't report errors here (done in the actual scan step)
|
||||||
|
if (!ec && selectFileScanner(path))
|
||||||
{
|
{
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
currentDirectoryProcessElemsCount++;
|
currentDirectoryProcessElemsCount++;
|
||||||
@@ -64,7 +59,7 @@ namespace lms::scanner
|
|||||||
},
|
},
|
||||||
&excludeDirFileName);
|
&excludeDirFileName);
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << currentDirectoryProcessElemsCount << " files in '" << mediaLibrary.rootDirectory << "'");
|
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << currentDirectoryProcessElemsCount << " files in " << mediaLibrary.rootDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.stats.totalFileCount = context.currentStepStats.processedElems;
|
context.stats.totalFileCount = context.currentStepStats.processedElems;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
bool ScanStepOptimize::needProcess(const ScanContext& context) const
|
bool ScanStepOptimize::needProcess(const ScanContext& context) const
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
bool ScanStepRemoveOrphanedDbEntries::needProcess([[maybe_unused]] const ScanContext& context) const
|
bool ScanStepRemoveOrphanedDbEntries::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||||
|
|||||||
@@ -27,9 +27,12 @@
|
|||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
#include "scanners/FileToScan.hpp"
|
||||||
#include "scanners/IFileScanOperation.hpp"
|
#include "scanners/IFileScanOperation.hpp"
|
||||||
#include "scanners/IFileScanner.hpp"
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
@@ -45,21 +48,38 @@ namespace lms::scanner
|
|||||||
|
|
||||||
return threadCount;
|
return threadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileToScan retrieveFileInfo(const std::filesystem::path& file, const MediaLibraryInfo& mediaLibrary, std::error_code& ec)
|
||||||
|
{
|
||||||
|
FileToScan res;
|
||||||
|
|
||||||
|
res.lastWriteTime = core::pathUtils::getLastWriteTime(file, ec);
|
||||||
|
if (!ec)
|
||||||
|
res.relativePath = std::filesystem::relative(file, mediaLibrary.rootDirectory, ec);
|
||||||
|
if (!ec)
|
||||||
|
res.fileSize = std::filesystem::file_size(file, ec);
|
||||||
|
|
||||||
|
if (!ec)
|
||||||
|
{
|
||||||
|
res.filePath = file;
|
||||||
|
res.mediaLibrary = mediaLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
||||||
: ScanStepBase{ initParams }
|
: ScanStepBase{ initParams }
|
||||||
, _fileScanQueue{ getScanMetaDataThreadCount(), _abortScan }
|
, _fileScanQueue{ getScanMetaDataThreadCount(), _abortScan }
|
||||||
{
|
{
|
||||||
for (IFileScanner* scanner : _fileScanners)
|
visitFileScanners([](IFileScanner* scanner) {
|
||||||
{
|
for (const std::filesystem::path& file : scanner->getSupportedFiles())
|
||||||
|
LMS_LOG(DBUPDATER, INFO, scanner->getName() << ": supporting file " << file);
|
||||||
|
|
||||||
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
for (const std::filesystem::path& extension : scanner->getSupportedExtensions())
|
||||||
{
|
LMS_LOG(DBUPDATER, INFO, scanner->getName() << ": supporting file extension " << extension);
|
||||||
[[maybe_unused]] auto [it, inserted]{ _scannerByExtension.emplace(extension, scanner) };
|
});
|
||||||
assert(inserted);
|
|
||||||
LMS_LOG(DBUPDATER, INFO, "Registered extension " << extension << " for " << scanner->getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
|
LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
|
||||||
}
|
}
|
||||||
@@ -94,26 +114,28 @@ namespace lms::scanner
|
|||||||
|
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR, "Cannot scan file " << path << ": " << ec.message());
|
addError<IOScanError>(context, path, ec);
|
||||||
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
context.stats.skips++;
|
||||||
}
|
}
|
||||||
else
|
else if (IFileScanner * scanner{ selectFileScanner(path) })
|
||||||
{
|
{
|
||||||
auto itScanner{ _scannerByExtension.find(core::stringUtils::stringToLower(path.extension().c_str())) };
|
FileToScan fileToScan{ retrieveFileInfo(path, mediaLibrary, ec) };
|
||||||
if (itScanner != std::cend(_scannerByExtension))
|
if (ec)
|
||||||
{
|
{
|
||||||
IFileScanner& scanner{ *itScanner->second };
|
addError<IOScanError>(context, path, ec);
|
||||||
|
context.stats.skips++;
|
||||||
FileToScan fileToScan{ .file = path, .mediaLibrary = mediaLibrary };
|
}
|
||||||
if (scanner.needsScan(context, fileToScan))
|
else
|
||||||
|
{
|
||||||
|
if (context.scanOptions.fullScan || scanner->needsScan(fileToScan))
|
||||||
{
|
{
|
||||||
auto scanOperation{ scanner.createScanOperation(fileToScan) };
|
auto scanOperation{ scanner->createScanOperation(std::move(fileToScan)) };
|
||||||
_fileScanQueue.pushScanRequest(std::move(scanOperation));
|
_fileScanQueue.pushScanRequest(std::move(scanOperation));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.currentStepStats.processedElems++;
|
|
||||||
_progressCallback(context.currentStepStats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.currentStepStats.processedElems++;
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2))
|
while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2))
|
||||||
@@ -146,9 +168,27 @@ namespace lms::scanner
|
|||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for " << scanOperation->getFile());
|
LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for " << scanOperation->getFilePath());
|
||||||
scanOperation->processResult(context);
|
const IFileScanOperation::OperationResult res{ scanOperation->processResult() };
|
||||||
|
switch (res)
|
||||||
|
{
|
||||||
|
case IFileScanOperation::OperationResult::Added:
|
||||||
|
context.stats.additions++;
|
||||||
|
break;
|
||||||
|
case IFileScanOperation::OperationResult::Removed:
|
||||||
|
context.stats.deletions++;
|
||||||
|
break;
|
||||||
|
case IFileScanOperation::OperationResult::Skipped:
|
||||||
|
context.stats.failures++;
|
||||||
|
break;
|
||||||
|
case IFileScanOperation::OperationResult::Updated:
|
||||||
|
context.stats.updates++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
context.stats.scans++;
|
context.stats.scans++;
|
||||||
|
|
||||||
|
for (const auto& error : scanOperation->getErrors())
|
||||||
|
addError(context, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -40,10 +40,10 @@ namespace lms::scanner
|
|||||||
core::LiteralString getStepName() const override { return "Scan files"; }
|
core::LiteralString getStepName() const override { return "Scan files"; }
|
||||||
bool needProcess(const ScanContext& context) const override;
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary);
|
void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary);
|
||||||
void processFileScanResults(ScanContext& context, std::span<std::unique_ptr<IFileScanOperation>> scanOperations);
|
void processFileScanResults(ScanContext& context, std::span<std::unique_ptr<IFileScanOperation>> scanOperations);
|
||||||
|
|
||||||
FileScanQueue _fileScanQueue;
|
FileScanQueue _fileScanQueue;
|
||||||
std::unordered_map<std::filesystem::path, IFileScanner*> _scannerByExtension;
|
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "MediaLibraryInfo.hpp"
|
#include "MediaLibraryInfo.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
// Forward declarations of all error types
|
||||||
|
struct ScanError;
|
||||||
|
struct IOScanError;
|
||||||
|
struct AudioFileScanError;
|
||||||
|
struct EmbeddedImageScanError;
|
||||||
|
struct NoAudioTrackFoundError;
|
||||||
|
struct BadAudioDurationError;
|
||||||
|
struct ArtistInfoFileScanError;
|
||||||
|
struct MissingArtistNameError;
|
||||||
|
struct ImageFileScanError;
|
||||||
|
struct LyricsFileScanError;
|
||||||
|
struct PlayListFileScanError;
|
||||||
|
struct PlayListFilePathMissingError;
|
||||||
|
struct PlayListFileAllPathesMissingError;
|
||||||
|
|
||||||
|
// Visitor interface
|
||||||
|
struct ScanErrorVisitor
|
||||||
|
{
|
||||||
|
virtual ~ScanErrorVisitor() = default;
|
||||||
|
|
||||||
|
virtual void visit(const ScanError&) = 0;
|
||||||
|
virtual void visit(const IOScanError&) = 0;
|
||||||
|
virtual void visit(const AudioFileScanError&) = 0;
|
||||||
|
virtual void visit(const EmbeddedImageScanError&) = 0;
|
||||||
|
virtual void visit(const NoAudioTrackFoundError&) = 0;
|
||||||
|
virtual void visit(const BadAudioDurationError&) = 0;
|
||||||
|
virtual void visit(const ArtistInfoFileScanError&) = 0;
|
||||||
|
virtual void visit(const MissingArtistNameError&) = 0;
|
||||||
|
virtual void visit(const ImageFileScanError&) = 0;
|
||||||
|
virtual void visit(const LyricsFileScanError&) = 0;
|
||||||
|
virtual void visit(const PlayListFileScanError&) = 0;
|
||||||
|
virtual void visit(const scanner::PlayListFilePathMissingError& error) = 0;
|
||||||
|
virtual void visit(const scanner::PlayListFileAllPathesMissingError& error) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScanError
|
||||||
|
{
|
||||||
|
ScanError(const std::filesystem::path& p)
|
||||||
|
: path{ p } {}
|
||||||
|
virtual ~ScanError() = default;
|
||||||
|
virtual void accept(ScanErrorVisitor&) const = 0;
|
||||||
|
|
||||||
|
std::filesystem::path path; // Error that occurs on this path
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IOScanError : public ScanError
|
||||||
|
{
|
||||||
|
IOScanError(const std::filesystem::path& p, std::error_code e)
|
||||||
|
: ScanError{ p }
|
||||||
|
, err{ e } {}
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code err;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AudioFileScanError : public ScanError
|
||||||
|
{
|
||||||
|
using ScanError::ScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EmbeddedImageScanError : public AudioFileScanError
|
||||||
|
{
|
||||||
|
EmbeddedImageScanError(const std::filesystem::path& p, unsigned i)
|
||||||
|
: AudioFileScanError{ p }
|
||||||
|
, index{ i } {}
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned index;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NoAudioTrackFoundError : public AudioFileScanError
|
||||||
|
{
|
||||||
|
using AudioFileScanError::AudioFileScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BadAudioDurationError : public AudioFileScanError
|
||||||
|
{
|
||||||
|
using AudioFileScanError::AudioFileScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArtistInfoFileScanError : public ScanError
|
||||||
|
{
|
||||||
|
using ScanError::ScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MissingArtistNameError : public ArtistInfoFileScanError
|
||||||
|
{
|
||||||
|
using ArtistInfoFileScanError::ArtistInfoFileScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImageFileScanError : public ScanError
|
||||||
|
{
|
||||||
|
using ScanError::ScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LyricsFileScanError : public ScanError
|
||||||
|
{
|
||||||
|
using ScanError::ScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayListFileScanError : public ScanError
|
||||||
|
{
|
||||||
|
using ScanError::ScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayListFilePathMissingError : public PlayListFileScanError
|
||||||
|
{
|
||||||
|
PlayListFilePathMissingError(const std::filesystem::path& p, const std::filesystem::path& e)
|
||||||
|
: PlayListFileScanError{ p }
|
||||||
|
, entry{ e }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayListFileAllPathesMissingError : public PlayListFileScanError
|
||||||
|
{
|
||||||
|
using PlayListFileScanError::PlayListFileScanError;
|
||||||
|
|
||||||
|
void accept(ScanErrorVisitor& visitor) const override
|
||||||
|
{
|
||||||
|
visitor.visit(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -21,40 +21,21 @@
|
|||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
|
|
||||||
|
#include "ScanErrors.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
enum class ScanErrorType
|
|
||||||
{
|
|
||||||
CannotReadFile,
|
|
||||||
CannotReadArtistInfoFile,
|
|
||||||
CannotReadAudioFile,
|
|
||||||
CannotReadImageFile,
|
|
||||||
CannotReadLyricsFile,
|
|
||||||
CannotReadPlayListFile,
|
|
||||||
NoAudioTrack,
|
|
||||||
BadDuration,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class DuplicateReason
|
enum class DuplicateReason
|
||||||
{
|
{
|
||||||
SameHash,
|
SameHash,
|
||||||
SameTrackMBID,
|
SameTrackMBID,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ScanError
|
|
||||||
{
|
|
||||||
std::filesystem::path file;
|
|
||||||
ScanErrorType error;
|
|
||||||
std::string systemError;
|
|
||||||
|
|
||||||
ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = "");
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ScanDuplicate
|
struct ScanDuplicate
|
||||||
{
|
{
|
||||||
db::TrackId trackId;
|
db::TrackId trackId;
|
||||||
@@ -105,15 +86,18 @@ namespace lms::scanner
|
|||||||
std::size_t totalFileCount{}; // Total number of files (estimated)
|
std::size_t totalFileCount{}; // Total number of files (estimated)
|
||||||
|
|
||||||
std::size_t skips{}; // no change since last scan
|
std::size_t skips{}; // no change since last scan
|
||||||
std::size_t scans{}; // actually scanned filed
|
std::size_t scans{}; // count of scanned files
|
||||||
|
|
||||||
std::size_t additions{}; // added in DB
|
std::size_t additions{}; // added in DB
|
||||||
std::size_t deletions{}; // removed from DB
|
std::size_t deletions{}; // removed from DB
|
||||||
std::size_t updates{}; // updated file in DB
|
std::size_t updates{}; // updated file in DB
|
||||||
|
std::size_t failures{}; // scan failure
|
||||||
|
|
||||||
std::size_t featuresFetched{}; // features fetched in DB
|
std::size_t featuresFetched{}; // features fetched in DB
|
||||||
|
|
||||||
std::vector<ScanError> errors;
|
static constexpr std::size_t maxStoredErrorCount{ 5'000 }; // TODO make this configurable
|
||||||
|
std::vector<std::shared_ptr<ScanError>> errors;
|
||||||
|
std::size_t errorsCount{}; // maybe bigger than errors.size() if too many errors
|
||||||
std::vector<ScanDuplicate> duplicates;
|
std::vector<ScanDuplicate> duplicates;
|
||||||
|
|
||||||
std::size_t nbFiles() const;
|
std::size_t nbFiles() const;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ add_executable(lms
|
|||||||
ui/admin/MediaLibraryModal.cpp
|
ui/admin/MediaLibraryModal.cpp
|
||||||
ui/admin/TracingView.cpp
|
ui/admin/TracingView.cpp
|
||||||
ui/admin/ScannerController.cpp
|
ui/admin/ScannerController.cpp
|
||||||
|
ui/admin/ScannerReportResource.cpp
|
||||||
ui/admin/ScanSettingsView.cpp
|
ui/admin/ScanSettingsView.cpp
|
||||||
ui/admin/UserView.cpp
|
ui/admin/UserView.cpp
|
||||||
ui/admin/UsersView.cpp
|
ui/admin/UsersView.cpp
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ namespace lms::ui
|
|||||||
.arg(static_cast<unsigned>(stats.updates))
|
.arg(static_cast<unsigned>(stats.updates))
|
||||||
.arg(static_cast<unsigned>(stats.deletions))
|
.arg(static_cast<unsigned>(stats.deletions))
|
||||||
.arg(static_cast<unsigned>(stats.duplicates.size()))
|
.arg(static_cast<unsigned>(stats.duplicates.size()))
|
||||||
.arg(static_cast<unsigned>(stats.errors.size())));
|
.arg(static_cast<unsigned>(stats.errorsCount)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,21 +19,19 @@
|
|||||||
|
|
||||||
#include "ScannerController.hpp"
|
#include "ScannerController.hpp"
|
||||||
|
|
||||||
#include <Wt/Http/Response.h>
|
|
||||||
#include <Wt/Utils.h>
|
|
||||||
#include <Wt/WCheckBox.h>
|
#include <Wt/WCheckBox.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
#include <Wt/WLocale.h>
|
#include <Wt/WLocale.h>
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
#include <Wt/WResource.h>
|
#include <Wt/WResource.h>
|
||||||
|
#include <Wt/WTime.h>
|
||||||
|
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "services/scanner/IScannerService.hpp"
|
#include "services/scanner/IScannerService.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
#include "ScannerReportResource.hpp"
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
@@ -45,110 +43,6 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class ScannerReportResource : public Wt::WResource
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ScannerReportResource() = default;
|
|
||||||
~ScannerReportResource() override
|
|
||||||
{
|
|
||||||
beingDeleted();
|
|
||||||
}
|
|
||||||
ScannerReportResource(const ScannerReportResource&) = delete;
|
|
||||||
ScannerReportResource& operator=(const ScannerReportResource&) = delete;
|
|
||||||
|
|
||||||
void setScanStats(const scanner::ScanStats& stats)
|
|
||||||
{
|
|
||||||
if (!_stats)
|
|
||||||
_stats = std::make_unique<scanner::ScanStats>();
|
|
||||||
|
|
||||||
*_stats = stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) override
|
|
||||||
{
|
|
||||||
if (!_stats)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto encodeHttpHeaderField = [](const std::string& fieldName, const std::string& fieldValue) {
|
|
||||||
// This implements RFC 5987
|
|
||||||
return fieldName + "*=UTF-8''" + Wt::Utils::urlEncode(fieldValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const std::string cdp{ encodeHttpHeaderField("filename", "LMS_scan_report_" + core::stringUtils::toISO8601String(_stats->startTime) + ".txt") };
|
|
||||||
response.addHeader("Content-Disposition", "attachment; " + cdp);
|
|
||||||
|
|
||||||
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats->errors.size()).toUTF8() << std::endl;
|
|
||||||
|
|
||||||
for (const auto& error : _stats->errors)
|
|
||||||
{
|
|
||||||
response.out() << error.file.string() << " - " << errorTypeToWString(error.error).toUTF8();
|
|
||||||
if (!error.systemError.empty())
|
|
||||||
response.out() << ": " << error.systemError;
|
|
||||||
response.out() << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.out() << std::endl;
|
|
||||||
|
|
||||||
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats->duplicates.size()).toUTF8() << std::endl;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
|
||||||
|
|
||||||
for (const auto& duplicate : _stats->duplicates)
|
|
||||||
{
|
|
||||||
const auto& track{ db::Track::find(LmsApp->getDbSession(), duplicate.trackId) };
|
|
||||||
if (!track)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
response.out() << track->getAbsoluteFilePath().string();
|
|
||||||
if (auto mbid{ track->getTrackMBID() })
|
|
||||||
response.out() << " (Track MBID " << mbid->getAsString() << ")";
|
|
||||||
|
|
||||||
response.out() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static Wt::WString errorTypeToWString(scanner::ScanErrorType error)
|
|
||||||
{
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case scanner::ScanErrorType::CannotReadFile:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file");
|
|
||||||
case scanner::ScanErrorType::CannotReadArtistInfoFile:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-artist-info-file");
|
|
||||||
case scanner::ScanErrorType::CannotReadAudioFile:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-audio-file");
|
|
||||||
case scanner::ScanErrorType::CannotReadImageFile:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file");
|
|
||||||
case scanner::ScanErrorType::CannotReadLyricsFile:
|
|
||||||
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:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
|
|
||||||
case scanner::ScanErrorType::BadDuration:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.bad-duration");
|
|
||||||
}
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
static Wt::WString duplicateReasonToWString(scanner::DuplicateReason reason)
|
|
||||||
{
|
|
||||||
switch (reason)
|
|
||||||
{
|
|
||||||
case scanner::DuplicateReason::SameHash:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.same-hash");
|
|
||||||
case scanner::DuplicateReason::SameTrackMBID:
|
|
||||||
return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid");
|
|
||||||
}
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<scanner::ScanStats> _stats;
|
|
||||||
};
|
|
||||||
|
|
||||||
ScannerController::ScannerController()
|
ScannerController::ScannerController()
|
||||||
: WTemplate{ Wt::WString::tr("Lms.Admin.ScannerController.template") }
|
: WTemplate{ Wt::WString::tr("Lms.Admin.ScannerController.template") }
|
||||||
{
|
{
|
||||||
@@ -225,7 +119,7 @@ namespace lms::ui
|
|||||||
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
|
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
|
||||||
.arg(status.lastCompleteScanStats->stopTime.date().toString(Wt::WLocale::currentLocale().dateFormat()))
|
.arg(status.lastCompleteScanStats->stopTime.date().toString(Wt::WLocale::currentLocale().dateFormat()))
|
||||||
.arg(status.lastCompleteScanStats->stopTime.time().toString(Wt::WLocale::currentLocale().timeFormat()))
|
.arg(status.lastCompleteScanStats->stopTime.time().toString(Wt::WLocale::currentLocale().timeFormat()))
|
||||||
.arg(status.lastCompleteScanStats->errors.size())
|
.arg(status.lastCompleteScanStats->errorsCount)
|
||||||
.arg(status.lastCompleteScanStats->duplicates.size()));
|
.arg(status.lastCompleteScanStats->duplicates.size()));
|
||||||
|
|
||||||
_reportResource->setScanStats(*status.lastCompleteScanStats);
|
_reportResource->setScanStats(*status.lastCompleteScanStats);
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ScannerReportResource.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Http/Response.h>
|
||||||
|
#include <Wt/Utils.h>
|
||||||
|
|
||||||
|
#include "core/String.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
#include "services/scanner/ScanErrors.hpp"
|
||||||
|
|
||||||
|
namespace lms::ui
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class ErrorFormatter : public scanner::ScanErrorVisitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ErrorFormatter(std::ostream& os)
|
||||||
|
: _os{ os } {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void visit(const scanner::ScanError&) override
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(const scanner::IOScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file").arg(Wt::WString::fromUTF8(error.err.message())).toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(const scanner::AudioFileScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-audio-file").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::EmbeddedImageScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-embedded-image").arg(error.index).toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::NoAudioTrackFoundError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::BadAudioDurationError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-duration").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::ArtistInfoFileScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-artist-info-file").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::MissingArtistNameError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.missing-artist-name").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::ImageFileScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::LyricsFileScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-lyrics-file").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::PlayListFileScanError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-playlist-file").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::PlayListFilePathMissingError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.playlist-path-missing").arg(Wt::WString::fromUTF8(error.entry)).toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
void visit(const scanner::PlayListFileAllPathesMissingError& error) override
|
||||||
|
{
|
||||||
|
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.playlist-all-pathes-missing").toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& _os;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScannerReportResource::ScannerReportResource() = default;
|
||||||
|
ScannerReportResource::~ScannerReportResource()
|
||||||
|
{
|
||||||
|
beingDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScannerReportResource::setScanStats(const scanner::ScanStats& stats)
|
||||||
|
{
|
||||||
|
if (!_stats)
|
||||||
|
_stats = std::make_unique<scanner::ScanStats>();
|
||||||
|
|
||||||
|
*_stats = stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScannerReportResource::handleRequest(const Wt::Http::Request&, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
if (!_stats)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto encodeHttpHeaderField = [](const std::string& fieldName, const std::string& fieldValue) {
|
||||||
|
// This implements RFC 5987
|
||||||
|
return fieldName + "*=UTF-8''" + Wt::Utils::urlEncode(fieldValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string cdp{ encodeHttpHeaderField("filename", "LMS_scan_report_" + core::stringUtils::toISO8601String(_stats->startTime) + ".txt") };
|
||||||
|
response.addHeader("Content-Disposition", "attachment; " + cdp);
|
||||||
|
|
||||||
|
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats->errorsCount).toUTF8() << std::endl;
|
||||||
|
|
||||||
|
ErrorFormatter errorFormatter{ response.out() };
|
||||||
|
for (const auto& error : _stats->errors)
|
||||||
|
error->accept(errorFormatter);
|
||||||
|
|
||||||
|
response.out() << std::endl;
|
||||||
|
|
||||||
|
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats->duplicates.size()).toUTF8() << std::endl;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
|
for (const auto& duplicate : _stats->duplicates)
|
||||||
|
{
|
||||||
|
const auto& track{ db::Track::find(LmsApp->getDbSession(), duplicate.trackId) };
|
||||||
|
if (!track)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
response.out() << track->getAbsoluteFilePath().string();
|
||||||
|
if (auto mbid{ track->getTrackMBID() })
|
||||||
|
response.out() << " (Track MBID " << mbid->getAsString() << ")";
|
||||||
|
|
||||||
|
response.out() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WString ScannerReportResource::duplicateReasonToWString(scanner::DuplicateReason reason)
|
||||||
|
{
|
||||||
|
switch (reason)
|
||||||
|
{
|
||||||
|
case scanner::DuplicateReason::SameHash:
|
||||||
|
return Wt::WString::tr("Lms.Admin.ScannerController.same-hash");
|
||||||
|
case scanner::DuplicateReason::SameTrackMBID:
|
||||||
|
return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid");
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::ui
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WResource.h>
|
||||||
|
|
||||||
|
#include "services/scanner/ScannerStats.hpp"
|
||||||
|
|
||||||
|
namespace lms::ui
|
||||||
|
{
|
||||||
|
|
||||||
|
class ScannerReportResource : public Wt::WResource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScannerReportResource();
|
||||||
|
~ScannerReportResource() override;
|
||||||
|
ScannerReportResource(const ScannerReportResource&) = delete;
|
||||||
|
ScannerReportResource& operator=(const ScannerReportResource&) = delete;
|
||||||
|
|
||||||
|
void setScanStats(const scanner::ScanStats& stats);
|
||||||
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static Wt::WString formatScanError(const scanner::ScanError* error);
|
||||||
|
static Wt::WString duplicateReasonToWString(scanner::DuplicateReason reason);
|
||||||
|
std::unique_ptr<scanner::ScanStats> _stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lms::ui
|
||||||
@@ -462,7 +462,7 @@ int main(int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
std::filesystem::path file{ inputFile };
|
std::filesystem::path file{ inputFile };
|
||||||
|
|
||||||
std::cout << "Parsing file '" << file << "'" << std::endl;
|
std::cout << "Parsing file " << file << std::endl;
|
||||||
|
|
||||||
if (parsers.contains(Parser::Lyrics))
|
if (parsers.contains(Parser::Lyrics))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user