From 6d6ea4920b5879b23d123697c5ca90f10f2cb81b Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 24 May 2025 15:02:10 +0200 Subject: [PATCH 01/51] xxhash is just an include dependency --- src/libs/core/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index fa069fba..81f4edcc 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -31,12 +31,12 @@ target_include_directories(lmscore INTERFACE target_include_directories(lmscore PRIVATE include + ${XXHASH_INCLUDE_DIRS} ) target_link_libraries(lmscore PRIVATE PkgConfig::Config++ PkgConfig::Archive - PkgConfig::XXHASH ) target_link_libraries(lmscore PUBLIC From 571c0bc5f4077b1e96f06c4e560c84aa5ef4cdc7 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 26 May 2025 23:33:52 +0200 Subject: [PATCH 02/51] More details for scan error reports #676 --- approot/messages.xml | 6 +- approot/messages_es.xml | 6 +- approot/messages_fr.xml | 6 +- approot/messages_it.xml | 6 +- approot/messages_pl.xml | 6 +- approot/messages_zh.xml | 6 +- src/libs/core/impl/ArchiveZipper.cpp | 16 +- src/libs/core/impl/ChildProcess.cpp | 31 +-- src/libs/core/impl/Path.cpp | 17 +- src/libs/core/impl/String.cpp | 8 - src/libs/core/include/core/Path.hpp | 4 +- src/libs/core/include/core/String.hpp | 2 - src/libs/metadata/CMakeLists.txt | 11 +- src/libs/metadata/impl/ArtistInfo.cpp | 8 +- src/libs/metadata/impl/AudioFileParser.cpp | 108 ++++---- src/libs/metadata/impl/ITagReader.hpp | 1 - src/libs/metadata/impl/Utils.cpp | 2 +- .../{ => avformat}/AvFormatImageReader.cpp | 8 +- .../{ => avformat}/AvFormatImageReader.hpp | 4 +- .../impl/{ => avformat}/AvFormatTagReader.cpp | 8 +- .../impl/{ => avformat}/AvFormatTagReader.hpp | 6 +- src/libs/metadata/impl/avformat/Utils.cpp | 49 ++++ src/libs/metadata/impl/avformat/Utils.hpp | 28 +++ .../metadata/impl/{ => taglib}/TagLibDefs.hpp | 4 + .../impl/{ => taglib}/TagLibImageReader.cpp | 49 ++-- .../impl/{ => taglib}/TagLibImageReader.hpp | 8 +- .../impl/{ => taglib}/TagLibTagReader.cpp | 83 +++---- .../impl/{ => taglib}/TagLibTagReader.hpp | 11 +- src/libs/metadata/impl/taglib/Utils.cpp | 230 ++++++++++++++++++ src/libs/metadata/impl/taglib/Utils.hpp | 39 +++ .../metadata/include/metadata/ArtistInfo.hpp | 2 +- .../metadata/include/metadata/Exception.hpp | 26 +- .../include/metadata/IAudioFileParser.hpp | 1 + src/libs/metadata/test/PlayList.cpp | 1 + src/libs/services/scanner/CMakeLists.txt | 2 + .../services/scanner/impl/ScannerService.cpp | 16 +- .../services/scanner/impl/ScannerStats.cpp | 9 +- .../impl/scanners/ArtistInfoFileScanner.cpp | 129 ++++------ .../impl/scanners/ArtistInfoFileScanner.hpp | 9 +- .../impl/scanners/AudioFileScanOperation.cpp | 126 +++++----- .../impl/scanners/AudioFileScanOperation.hpp | 29 +-- .../impl/scanners/AudioFileScanner.cpp | 65 ++--- .../impl/scanners/AudioFileScanner.hpp | 5 +- .../impl/scanners/FileScanOperationBase.cpp | 34 +++ .../impl/scanners/FileScanOperationBase.hpp | 69 ++++++ .../scanner/impl/scanners/FileToScan.hpp | 7 +- .../impl/scanners/IFileScanOperation.hpp | 23 +- .../scanner/impl/scanners/IFileScanner.hpp | 7 +- .../impl/scanners/ImageFileScanner.cpp | 109 +++------ .../impl/scanners/ImageFileScanner.hpp | 8 +- .../impl/scanners/LyricsFileScanner.cpp | 121 ++++----- .../impl/scanners/LyricsFileScanner.hpp | 8 +- .../impl/scanners/PlayListFileScanner.cpp | 116 ++++----- .../impl/scanners/PlayListFileScanner.hpp | 8 +- .../services/scanner/impl/scanners/Utils.cpp | 56 +---- .../services/scanner/impl/scanners/Utils.hpp | 13 - .../scanner/impl/steps/FileScanQueue.cpp | 2 +- .../services/scanner/impl/steps/IScanStep.hpp | 5 +- .../scanner/impl/steps/ScanErrorLogger.cpp | 93 +++++++ .../scanner/impl/steps/ScanErrorLogger.hpp | 43 ++++ .../steps/ScanStepArtistReconciliation.cpp | 1 + .../steps/ScanStepAssociateArtistImages.cpp | 2 + .../steps/ScanStepAssociateExternalLyrics.cpp | 2 + .../steps/ScanStepAssociatePlayListTracks.cpp | 29 ++- .../steps/ScanStepAssociateReleaseImages.cpp | 2 + .../scanner/impl/steps/ScanStepBase.cpp | 56 +++++ .../scanner/impl/steps/ScanStepBase.hpp | 20 +- .../steps/ScanStepCheckForDuplicatedFiles.cpp | 2 + .../steps/ScanStepCheckForRemovedFiles.cpp | 33 +-- .../steps/ScanStepCheckForRemovedFiles.hpp | 5 +- .../scanner/impl/steps/ScanStepCompact.cpp | 2 + .../steps/ScanStepComputeClusterStats.cpp | 2 + .../impl/steps/ScanStepDiscoverFiles.cpp | 15 +- .../scanner/impl/steps/ScanStepOptimize.cpp | 2 + .../steps/ScanStepRemoveOrphanedDbEntries.cpp | 2 + .../scanner/impl/steps/ScanStepScanFiles.cpp | 86 +++++-- .../scanner/impl/steps/ScanStepScanFiles.hpp | 2 +- .../steps/ScanStepUpdateLibraryFields.cpp | 1 + .../include/services/scanner/ScanErrors.hpp | 205 ++++++++++++++++ .../include/services/scanner/ScannerStats.hpp | 32 +-- src/lms/CMakeLists.txt | 1 + src/lms/ui/LmsApplication.cpp | 2 +- src/lms/ui/admin/ScannerController.cpp | 112 +-------- src/lms/ui/admin/ScannerReportResource.cpp | 168 +++++++++++++ src/lms/ui/admin/ScannerReportResource.hpp | 45 ++++ src/tools/metadata/LmsMetadata.cpp | 2 +- 86 files changed, 1794 insertions(+), 950 deletions(-) rename src/libs/metadata/impl/{ => avformat}/AvFormatImageReader.cpp (92%) rename src/libs/metadata/impl/{ => avformat}/AvFormatImageReader.hpp (94%) rename src/libs/metadata/impl/{ => avformat}/AvFormatTagReader.cpp (98%) rename src/libs/metadata/impl/{ => avformat}/AvFormatTagReader.hpp (93%) create mode 100644 src/libs/metadata/impl/avformat/Utils.cpp create mode 100644 src/libs/metadata/impl/avformat/Utils.hpp rename src/libs/metadata/impl/{ => taglib}/TagLibDefs.hpp (94%) rename src/libs/metadata/impl/{ => taglib}/TagLibImageReader.cpp (95%) rename src/libs/metadata/impl/{ => taglib}/TagLibImageReader.hpp (87%) rename src/libs/metadata/impl/{ => taglib}/TagLibTagReader.cpp (92%) rename src/libs/metadata/impl/{ => taglib}/TagLibTagReader.hpp (90%) create mode 100644 src/libs/metadata/impl/taglib/Utils.cpp create mode 100644 src/libs/metadata/impl/taglib/Utils.hpp create mode 100644 src/libs/services/scanner/impl/scanners/FileScanOperationBase.cpp create mode 100644 src/libs/services/scanner/impl/scanners/FileScanOperationBase.hpp create mode 100644 src/libs/services/scanner/impl/steps/ScanErrorLogger.cpp create mode 100644 src/libs/services/scanner/impl/steps/ScanErrorLogger.hpp create mode 100644 src/libs/services/scanner/include/services/scanner/ScanErrors.hpp create mode 100644 src/lms/ui/admin/ScannerReportResource.cpp create mode 100644 src/lms/ui/admin/ScannerReportResource.hpp diff --git a/approot/messages.xml b/approot/messages.xml index 1f775e67..056dddeb 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -100,9 +100,10 @@ Cannot get track duration +Unable to read the image at index {1} Cannot parse artist info file Cannot parse audio file -Cannot read file +Cannot read file ({1}) Cannot parse image file Cannot parse lyrics file Cannot parse playlist file @@ -115,6 +116,9 @@ Last scan Not available {1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates +Missing artist name +Unable to find track paths +Unable to find the path "{1}" No audio track Duplicated file hash Duplicated track MBID diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 7f47b917..325890c3 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -100,9 +100,10 @@ No ha sido posible obtener la duración de la pista +No se puede leer la imagen en el índice {1} No ha sido posible analizar el fichero de información sobre el artista No ha sido posible analizar el fichero de audio -No ha sido posible leer el fichero +No ha sido posible leer el fichero ({1}) No ha sido posible analizar del fichero de imagen No ha sido posible analizar el fichero de letras No ha sido posible analizar el fichero de la lista de distribución @@ -115,6 +116,9 @@ Último escaneo No disponible {1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados +Falta el nombre del artista +No se pudieron encontrar las rutas de las pistas +No se pudo encontrar la ruta "{1}" No es una pista de audio Hash del fichero duplicado MBID de la pista duplicado diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index de9c8039..5fea473e 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -100,9 +100,10 @@ Impossible de récupérer la durée de la piste +Impossible de lire l'image à l'indice {1} Impossible d'analyser le fichier d'informations sur l'artiste Impossible d'analyser le fichier audio -Impossible de lire le fichier +Impossible de lire le fichier ({1}) Impossible d'analyser le fichier image Impossible d'analyser le fichier de paroles Impossible d'analyser le fichier de liste de lecture @@ -115,6 +116,9 @@ Dernier scan Non disponible {1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas +Nom de l'artiste manquant +Impossible de trouver les chemins des pistes +Impossible de trouver le chemin "{1}" Pas de piste audio Hash dupliqué Track MBID dupliqué diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 6545bb5d..91935ade 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -100,9 +100,10 @@ Non sono stato in grado di determinare la durata della traccia +Impossibile leggere l'immagine all'indice {1} Impossibile analizzare il file delle informazioni sull'artista Impossibile analizzare il file audio -Non in grado di leggere il file +Non in grado di leggere il file ({1}) Impossibile analizzare il file immagine Impossibile analizzare il file dei testi Impossibile analizzare il file della playlist @@ -115,6 +116,9 @@ Ultima scansione Non disponibile {1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati +Nome dell'artista mancante +Impossibile trovare i percorsi delle tracce +Impossibile trovare il percorso "{1}" Nessuna traccia audio Hash doppio Track MBID doppio diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index cb1af168..78c2d88b 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -101,9 +101,10 @@ Nie udało się ustalić długości ścieżki +Nie można odczytać obrazu pod indeksem {1} Nie można przetworzyć pliku z informacjami o artyście" Nie można przeanalizować pliku audio -Nie udało się odczytać pliku +Nie udało się odczytać pliku ({1}) Nie można przeanalizować pliku obrazu Nie można przetworzyć pliku z tekstem Nie można przeanalizować pliku playlisty @@ -124,6 +125,9 @@ Ostatnie skanowanie Niedostępne Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów +Brakuje nazwy artysty +Nie można znaleźć ścieżek utworów +Nie można znaleźć ścieżki „{1}” Brak ścieżki dźwiękowej Zduplikowny hash pliku Zduplikowany MBID pliku diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 7b81cbe3..860bb5d2 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -100,9 +100,10 @@ 无法获得音轨时间 + 无法解析文件 -无法读取文件 +无法读取文件 ({1}) @@ -114,6 +115,9 @@ 最后扫描 不可用 已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项 + + + 没有音轨 相同文件哈希值 相同 MBID diff --git a/src/libs/core/impl/ArchiveZipper.cpp b/src/libs/core/impl/ArchiveZipper.cpp index 15042e87..9cdfb52a 100644 --- a/src/libs/core/impl/ArchiveZipper.cpp +++ b/src/libs/core/impl/ArchiveZipper.cpp @@ -22,12 +22,12 @@ #include #include #include +#include #include #include #include "core/ILogger.hpp" -#include "core/String.hpp" namespace lms::zip { @@ -43,9 +43,13 @@ namespace lms::zip : Exception{ "File '" + p.string() + "': " + std::string{ message } } { } + }; - FileException(const std::filesystem::path& p, std::string_view message, int err) - : Exception{ "File '" + p.string() + "': " + std::string{ message } + ": " + core::stringUtils::systemErrorToString(err) } + class FileStdException : public FileException + { + 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 }; if (!ifs) - throw FileException{ _currentEntry->filePath, "cannot open file", errno }; + throw FileStdException{ _currentEntry->filePath, "cannot open file", errno }; ifs.seekg(0, std::ios::end); const std::uint64_t fileSize{ static_cast(ifs.tellg()) }; @@ -254,10 +258,10 @@ namespace lms::zip // read from file 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(_readBuffer.data()), bytesToRead)) - throw FileException{ _currentEntry->filePath, "read failed", errno }; + throw FileStdException{ _currentEntry->filePath, "read failed", errno }; const std::uint64_t actualBytesRead{ static_cast(ifs.gcount()) }; diff --git a/src/libs/core/impl/ChildProcess.cpp b/src/libs/core/impl/ChildProcess.cpp index 0f7b2ef9..2dc57207 100644 --- a/src/libs/core/impl/ChildProcess.cpp +++ b/src/libs/core/impl/ChildProcess.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,6 @@ #include #include "core/ILogger.hpp" -#include "core/String.hpp" namespace lms::core { @@ -44,8 +44,8 @@ namespace lms::core class SystemException : public ChildProcessException { public: - SystemException(int err, const std::string& errMsg) - : ChildProcessException{ errMsg + ": " + stringUtils::systemErrorToString(err) } + SystemException(std::error_code err, const std::string& errMsg) + : ChildProcessException{ errMsg + ": " + err.message() } { } @@ -69,20 +69,20 @@ namespace lms::core // Use 'pipe' instead of 'pipe2', more portable 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 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) - 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) - 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) - 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) for (const int fd : { pipefd[0], pipefd[1] }) @@ -94,7 +94,7 @@ namespace lms::core if (pipeSizeRes == -1) { 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 { @@ -103,10 +103,10 @@ namespace lms::core #endif if (currentPipeSize < targetPipeSize) { - if (fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1) + if (::fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1) { 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() }; if (res == -1) - throw SystemException{ errno, "fork failed!" }; + throw SystemException{ std::error_code{ errno, std::generic_category() }, "fork failed!" }; if (res == 0) // CHILD { @@ -168,7 +168,10 @@ namespace lms::core // process may already have finished LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process..."); 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) @@ -179,7 +182,7 @@ namespace lms::core const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) }; if (pid == -1) - throw SystemException{ errno, "waitpid failed!" }; + throw SystemException{ std::error_code{ errno, std::generic_category() }, "waitpid failed!" }; if (pid == 0) return false; diff --git a/src/libs/core/impl/Path.cpp b/src/libs/core/impl/Path.cpp index d0f983e7..248042c7 100644 --- a/src/libs/core/impl/Path.cpp +++ b/src/libs/core/impl/Path.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -66,16 +67,24 @@ namespace lms::core::pathUtils 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 { }; - 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 cb, const std::filesystem::path* excludeDirFileName) diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 8258348a..d976d26d 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -20,7 +20,6 @@ #include "core/String.hpp" #include -#include #include #include #include @@ -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) + "]"; } - - std::string systemErrorToString(int err) - { - std::array buffer{}; - ::strerror_r(err, buffer.data(), buffer.size()); - return std::string{ buffer.data() }; - } } // namespace lms::core::stringUtils diff --git a/src/libs/core/include/core/Path.hpp b/src/libs/core/include/core/Path.hpp index 500c9af3..1d9688bd 100644 --- a/src/libs/core/include/core/Path.hpp +++ b/src/libs/core/include/core/Path.hpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include @@ -35,7 +35,7 @@ namespace lms::core::pathUtils bool ensureDirectory(const std::filesystem::path& dir); // 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 bool exploreFilesRecursive(const std::filesystem::path& directory, std::function cb, const std::filesystem::path* excludeDirFileName = {}); diff --git a/src/libs/core/include/core/String.hpp b/src/libs/core/include/core/String.hpp index 7c99a7cc..b65df63a 100644 --- a/src/libs/core/include/core/String.hpp +++ b/src/libs/core/include/core/String.hpp @@ -119,6 +119,4 @@ namespace lms::core::stringUtils // to "[minutes:seconds.milliseconds]" std::string formatTimestamp(std::chrono::milliseconds timestamp); - - std::string systemErrorToString(int err); } // namespace lms::core::stringUtils \ No newline at end of file diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index d5dfc9fd..ea401ef1 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -11,12 +11,14 @@ endif() add_library(lmsmetadata STATIC impl/ArtistInfo.cpp impl/AudioFileParser.cpp - impl/AvFormatImageReader.cpp - impl/AvFormatTagReader.cpp + impl/avformat/AvFormatImageReader.cpp + impl/avformat/AvFormatTagReader.cpp + impl/avformat/Utils.cpp impl/Lyrics.cpp impl/PlayList.cpp - impl/TagLibImageReader.cpp - impl/TagLibTagReader.cpp + impl/taglib/TagLibImageReader.cpp + impl/taglib/TagLibTagReader.cpp + impl/taglib/Utils.cpp impl/Utils.cpp ) @@ -26,6 +28,7 @@ target_include_directories(lmsmetadata INTERFACE target_include_directories(lmsmetadata PRIVATE include + impl ) target_link_libraries(lmsmetadata PRIVATE diff --git a/src/libs/metadata/impl/ArtistInfo.cpp b/src/libs/metadata/impl/ArtistInfo.cpp index 639bbd42..85b4e96b 100644 --- a/src/libs/metadata/impl/ArtistInfo.cpp +++ b/src/libs/metadata/impl/ArtistInfo.cpp @@ -27,10 +27,10 @@ namespace lms::metadata { - std::span getSupportedInfoFileExtensions() + std::span getSupportedArtistInfoFiles() { - static const std::array fileExtensions{ ".nfo" }; - return fileExtensions; + static const std::array files{ "artist.nfo" }; + return files; } ArtistInfo parseArtistInfo(std::istream& is) @@ -56,7 +56,7 @@ namespace lms::metadata } 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() }; } } diff --git a/src/libs/metadata/impl/AudioFileParser.cpp b/src/libs/metadata/impl/AudioFileParser.cpp index a75d7c03..29cfa356 100644 --- a/src/libs/metadata/impl/AudioFileParser.cpp +++ b/src/libs/metadata/impl/AudioFileParser.cpp @@ -28,11 +28,13 @@ #include "core/String.hpp" #include "metadata/Exception.hpp" -#include "AvFormatImageReader.hpp" -#include "AvFormatTagReader.hpp" -#include "TagLibImageReader.hpp" -#include "TagLibTagReader.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 { @@ -338,82 +340,56 @@ namespace lms::metadata std::span AudioFileParser::getSupportedExtensions() const { - // TODO: use backend capability to retrieve supported formats - static const std::array fileExtensions{ - ".aac", - ".alac", - ".aif", - ".aiff", - ".ape", - ".dsf", - ".flac", - ".m4a", - ".m4b", - ".mp3", - ".mpc", - ".oga", - ".ogg", - ".opus", - ".shn", - ".wav", - ".wma", - ".wv", - }; - return fileExtensions; + switch (_params.backend) + { + case ParserBackend::TagLib: + return taglib::utils::getSupportedExtensions(); + break; + + case ParserBackend::AvFormat: + return avformat::utils::getSupportedExtensions(); + break; + } + + return {}; } std::unique_ptr AudioFileParser::parseMetaData(const std::filesystem::path& p) const { - try + std::unique_ptr tagReader; + switch (_params.backend) { - std::unique_ptr tagReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - tagReader = std::make_unique(p, _params.readStyle, _params.debug); - break; + case ParserBackend::TagLib: + tagReader = std::make_unique(p, _params.readStyle, _params.debug); + break; - case ParserBackend::AvFormat: - tagReader = std::make_unique(p, _params.debug); - break; - } - if (!tagReader) - throw ParseException{ "Unhandled parser backend" }; + case ParserBackend::AvFormat: + tagReader = std::make_unique(p, _params.debug); + break; + } + if (!tagReader) + throw AudioFileParsingException{ "Unhandled parser backend" }; - return parseMetaData(*tagReader); - } - catch (const Exception& e) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed"); - throw ParseException{}; - } + return parseMetaData(*tagReader); } void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const { - try + std::unique_ptr imageReader; + switch (_params.backend) { - std::unique_ptr imageReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - imageReader = std::make_unique(p); - break; + case ParserBackend::TagLib: + imageReader = std::make_unique(p); + break; - case ParserBackend::AvFormat: - imageReader = std::make_unique(p); - break; - } - if (!imageReader) - throw ParseException{ "Unhandled parser backend" }; + case ParserBackend::AvFormat: + imageReader = std::make_unique(p); + break; + } + if (!imageReader) + throw AudioFileParsingException{ "Unhandled parser backend" }; - parseImages(*imageReader, std::move(visitor)); - } - catch (const Exception& e) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": image parsing failed"); - throw ParseException{}; - } + parseImages(*imageReader, std::move(visitor)); } std::unique_ptr AudioFileParser::parseMetaData(const ITagReader& tagReader) const diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp index 7526815a..0b4bc8db 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/metadata/impl/ITagReader.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include "metadata/Types.hpp" diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index 1f7158a9..1152b95e 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -108,7 +108,7 @@ namespace lms::metadata::utils return "accurate"; } - throw core::LmsException{ "Unknown read style" }; + throw Exception{ "Unknown read style" }; } PerformerArtist extractPerformerAndRole(std::string_view entry) diff --git a/src/libs/metadata/impl/AvFormatImageReader.cpp b/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp similarity index 92% rename from src/libs/metadata/impl/AvFormatImageReader.cpp rename to src/libs/metadata/impl/avformat/AvFormatImageReader.cpp index a0c4a97a..7ec95d1c 100644 --- a/src/libs/metadata/impl/AvFormatImageReader.cpp +++ b/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp @@ -18,10 +18,12 @@ */ #include "AvFormatImageReader.hpp" + #include "av/IAudioFile.hpp" #include "av/Types.hpp" +#include "metadata/Exception.hpp" -namespace lms::metadata +namespace lms::metadata::avformat { AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p) { @@ -31,7 +33,7 @@ namespace lms::metadata } catch (av::Exception& e) { - throw ParseException{}; + throw AudioFileParsingException{ e.what() }; } } @@ -56,4 +58,4 @@ namespace lms::metadata }); } -} // namespace lms::metadata +} // namespace lms::metadata::avformat diff --git a/src/libs/metadata/impl/AvFormatImageReader.hpp b/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp similarity index 94% rename from src/libs/metadata/impl/AvFormatImageReader.hpp rename to src/libs/metadata/impl/avformat/AvFormatImageReader.hpp index 033d1e45..0658b8a7 100644 --- a/src/libs/metadata/impl/AvFormatImageReader.hpp +++ b/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp @@ -26,7 +26,7 @@ namespace lms::av class IAudioFile; } -namespace lms::metadata +namespace lms::metadata::avformat { class AvFormatImageReader : public IImageReader { @@ -42,4 +42,4 @@ namespace lms::metadata std::unique_ptr _audioFile; }; -} // namespace lms::metadata +} // namespace lms::metadata::avformat diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp similarity index 98% rename from src/libs/metadata/impl/AvFormatTagReader.cpp rename to src/libs/metadata/impl/avformat/AvFormatTagReader.cpp index aa3feb0f..04f50f62 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp @@ -25,7 +25,7 @@ #include "core/String.hpp" #include "metadata/Exception.hpp" -namespace lms::metadata +namespace lms::metadata::avformat { namespace { @@ -165,10 +165,12 @@ namespace lms::metadata } catch (av::Exception& e) { - throw ParseException{}; + throw AudioFileParsingException{ e.what() }; } } + AvFormatTagReader::~AvFormatTagReader() = default; + void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ avFormatTagMapping.find(tag) }; @@ -223,4 +225,4 @@ namespace lms::metadata visitor("", value); }); } -} // namespace lms::metadata +} // namespace lms::metadata::avformat diff --git a/src/libs/metadata/impl/AvFormatTagReader.hpp b/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp similarity index 93% rename from src/libs/metadata/impl/AvFormatTagReader.hpp rename to src/libs/metadata/impl/avformat/AvFormatTagReader.hpp index a6cd7c4f..44b38c38 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.hpp +++ b/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp @@ -25,13 +25,13 @@ #include "ITagReader.hpp" -namespace lms::metadata +namespace lms::metadata::avformat { class AvFormatTagReader : public ITagReader { public: AvFormatTagReader(const std::filesystem::path& path, bool debug); - ~AvFormatTagReader() override = default; + ~AvFormatTagReader() override; AvFormatTagReader(const AvFormatTagReader&) = delete; AvFormatTagReader& operator=(const AvFormatTagReader&) = delete; @@ -46,4 +46,4 @@ namespace lms::metadata av::IAudioFile::MetadataMap _metaDataMap; av::ContainerInfo _containerInfo; }; -} // namespace lms::metadata +} // namespace lms::metadata::avformat diff --git a/src/libs/metadata/impl/avformat/Utils.cpp b/src/libs/metadata/impl/avformat/Utils.cpp new file mode 100644 index 00000000..83055d2f --- /dev/null +++ b/src/libs/metadata/impl/avformat/Utils.cpp @@ -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 . + */ + +#include "Utils.hpp" + +namespace lms::metadata::avformat::utils +{ + std::span getSupportedExtensions() + { + // TODO: use av capability to retrieve supported formats + static const std::array 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 \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/Utils.hpp b/src/libs/metadata/impl/avformat/Utils.hpp new file mode 100644 index 00000000..b4f655fa --- /dev/null +++ b/src/libs/metadata/impl/avformat/Utils.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +namespace lms::metadata::avformat::utils +{ + std::span getSupportedExtensions(); +} // namespace lms::metadata::avformat::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/TagLibDefs.hpp b/src/libs/metadata/impl/taglib/TagLibDefs.hpp similarity index 94% rename from src/libs/metadata/impl/TagLibDefs.hpp rename to src/libs/metadata/impl/taglib/TagLibDefs.hpp index 69e97a8a..4f13bf3e 100644 --- a/src/libs/metadata/impl/TagLibDefs.hpp +++ b/src/libs/metadata/impl/taglib/TagLibDefs.hpp @@ -21,6 +21,10 @@ #include +#if (TAGLIB_MAJOR_VERSION > 2) + #define TAGLIB_HAS_DSF 1 +#endif + // 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)) #define TAGLIB_HAS_MP4_ITEM_TYPE 1 diff --git a/src/libs/metadata/impl/TagLibImageReader.cpp b/src/libs/metadata/impl/taglib/TagLibImageReader.cpp similarity index 95% rename from src/libs/metadata/impl/TagLibImageReader.cpp rename to src/libs/metadata/impl/taglib/TagLibImageReader.cpp index 8dba0b9b..a5d7f940 100644 --- a/src/libs/metadata/impl/TagLibImageReader.cpp +++ b/src/libs/metadata/impl/taglib/TagLibImageReader.cpp @@ -19,6 +19,8 @@ #include "TagLibImageReader.hpp" +#include "TagLibDefs.hpp" + #include #include #include @@ -34,24 +36,16 @@ #include #include #include -#include #include "core/ILogger.hpp" -#include "core/ITraceLogger.hpp" - -#include "core/String.hpp" #include "metadata/Exception.hpp" -#include "TagLibDefs.hpp" +#include "taglib/Utils.hpp" -namespace lms::metadata +namespace lms::metadata::taglib { namespace { - class ImageParsingFailedException : public Exception - { - }; - Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type) { switch (type) @@ -349,44 +343,37 @@ namespace lms::metadata } } // 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) - : _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"); - throw ImageParsingFailedException{}; + throw AudioFileParsingException{}; } } void TagLibImageReader::visitImages(ImageVisitor visitor) const { // MP3 - if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.file()) }) + if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) { if (mp3File->hasID3v2Tag()) visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor)); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) + else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) { visitMP4Images(*mp4File, std::move(visitor)); } // WMA - else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.file()) }) + else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) { if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) visitASFImages(*tag, std::move(visitor)); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.file()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) { if (flacFile->hasID3v2Tag()) // usage discouraged visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor)); @@ -394,38 +381,38 @@ namespace lms::metadata visitFLACImages(flacFile->pictureList(), std::move(visitor)); } // Ogg vorbis - else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.file()) }) + else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.get()) }) { visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor)); } // Ogg Opus - else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.file()) }) + else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.get()) }) { visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor)); } // Aiff - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.file()) }) + else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) { if (aiffFile->hasID3v2Tag()) visitID3V2Images(*aiffFile->tag(), std::move(visitor)); } // Wav - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.file()) }) + else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) { if (wavFile->hasID3v2Tag()) visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor)); } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.file()) }) + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) { if (mpcFile->hasAPETag()) visitAPEImages(*mpcFile->APETag(), std::move(visitor)); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.file()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) { if (wavPackFile->hasAPETag()) visitAPEImages(*wavPackFile->APETag(), std::move(visitor)); } } -} // namespace lms::metadata +} // namespace lms::metadata::taglib diff --git a/src/libs/metadata/impl/TagLibImageReader.hpp b/src/libs/metadata/impl/taglib/TagLibImageReader.hpp similarity index 87% rename from src/libs/metadata/impl/TagLibImageReader.hpp rename to src/libs/metadata/impl/taglib/TagLibImageReader.hpp index 075169af..c0c5bea4 100644 --- a/src/libs/metadata/impl/TagLibImageReader.hpp +++ b/src/libs/metadata/impl/taglib/TagLibImageReader.hpp @@ -21,9 +21,9 @@ #include "IImageReader.hpp" -#include +#include -namespace lms::metadata +namespace lms::metadata::taglib { class TagLibImageReader : public IImageReader { @@ -32,6 +32,6 @@ namespace lms::metadata void visitImages(ImageVisitor visitor) const override; - const TagLib::FileRef _file; + std::unique_ptr _file; }; -} // namespace lms::metadata +} // namespace lms::metadata::taglib diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/taglib/TagLibTagReader.cpp similarity index 92% rename from src/libs/metadata/impl/TagLibTagReader.cpp rename to src/libs/metadata/impl/taglib/TagLibTagReader.cpp index 1ea6e8ea..a7ae70e0 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/taglib/TagLibTagReader.cpp @@ -19,44 +19,49 @@ #include "TagLibTagReader.hpp" -#include #include +#include "TagLibDefs.hpp" + #include +#include #include #include #include -#if TAGLIB_MAJOR_VERSION >= 2 - #include -#endif -#include #include #include #include #include #include +#include #include +#include #include #include #include +#include #include #include #include #include +#if TAGLIB_HAS_DSF + #include +#endif #include "core/ILogger.hpp" -#include "core/ITraceLogger.hpp" #include "core/String.hpp" #include "metadata/Exception.hpp" -#include "TagLibDefs.hpp" +#include "Utils.hpp" -namespace lms::metadata +namespace lms::metadata::taglib { namespace { class TagParsingFailedException : public Exception { + public: + using Exception::Exception; }; // Mapping to internal taglib names and/or common alternative custom names @@ -166,21 +171,6 @@ namespace lms::metadata { 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) { 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 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"); - throw TagParsingFailedException{}; + throw AudioFileParsingException{ "Parsing failed" }; } - if (!_file.audioProperties()) + if (!_file->audioProperties()) { LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties"); - throw TagParsingFailedException{}; + throw AudioFileNoAudioPropertiesException{}; } computeAudioProperties(); - _propertyMap = _file.file()->properties(); + _propertyMap = _file->properties(); if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { @@ -319,7 +300,7 @@ namespace lms::metadata }; // WMA - if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.file()) }) + if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) { if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) { @@ -365,7 +346,7 @@ namespace lms::metadata } } // MP3 - else if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.file()) }) + else if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) { if (mp3File->hasID3v2Tag()) processID3v2Tags(*mp3File->ID3v2Tag()); @@ -373,7 +354,7 @@ namespace lms::metadata getAPETags(mp3File->APETag()); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) + else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) { // Taglib does not expose rtng in properties if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) @@ -406,40 +387,42 @@ namespace lms::metadata } } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.file()) }) + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) { getAPETags(mpcFile->APETag()); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.file()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) { getAPETags(wavPackFile->APETag()); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.file()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) { if (flacFile->hasID3v2Tag()) // discouraged usage processID3v2Tags(*flacFile->ID3v2Tag()); } - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.file()) }) + else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) { if (aiffFile->hasID3v2Tag()) processID3v2Tags(*aiffFile->tag()); } - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.file()) }) + else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) { if (wavFile->hasID3v2Tag()) processID3v2Tags(*wavFile->ID3v2Tag()); } } + TagLibTagReader::~TagLibTagReader() = default; + void TagLibTagReader::computeAudioProperties() { - const TagLib::AudioProperties* properties{ _file.audioProperties() }; + const TagLib::AudioProperties* properties{ _file->audioProperties() }; // Common properties _audioProperties.bitrate = static_cast(properties->bitrate() * 1000); - _audioProperties.channelCount = static_cast(_file.audioProperties()->channels()); + _audioProperties.channelCount = static_cast(_file->audioProperties()->channels()); _audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; _audioProperties.sampleRate = static_cast(properties->sampleRate()); @@ -457,7 +440,7 @@ namespace lms::metadata _audioProperties.bitsPerSample = aiffProperties->bitsPerSample(); else if (const auto* wavProperties{ dynamic_cast(properties) }) _audioProperties.bitsPerSample = wavProperties->bitsPerSample(); -#if TAGLIB_MAJOR_VERSION >= 2 +#if TAGLIB_HAS_DSF else if (const auto* dsfProperties{ dynamic_cast(properties) }) _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); #endif @@ -534,4 +517,4 @@ namespace lms::metadata }); } } -} // namespace lms::metadata +} // namespace lms::metadata::taglib diff --git a/src/libs/metadata/impl/TagLibTagReader.hpp b/src/libs/metadata/impl/taglib/TagLibTagReader.hpp similarity index 90% rename from src/libs/metadata/impl/TagLibTagReader.hpp rename to src/libs/metadata/impl/taglib/TagLibTagReader.hpp index 43d938a7..07034a38 100644 --- a/src/libs/metadata/impl/TagLibTagReader.hpp +++ b/src/libs/metadata/impl/taglib/TagLibTagReader.hpp @@ -21,20 +21,21 @@ #include #include +#include #include -#include +#include #include #include "ITagReader.hpp" -namespace lms::metadata +namespace lms::metadata::taglib { class TagLibTagReader : public ITagReader { public: TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug); - ~TagLibTagReader() = default; + ~TagLibTagReader() override; TagLibTagReader(const TagLibTagReader&) = delete; TagLibTagReader& operator=(const TagLibTagReader&) = delete; @@ -47,9 +48,9 @@ namespace lms::metadata const AudioProperties& getAudioProperties() const override { return _audioProperties; } - const TagLib::FileRef _file; + std::unique_ptr _file; AudioProperties _audioProperties; TagLib::PropertyMap _propertyMap; // case-insensitive keys std::multimap _id3v2Lyrics; }; -} // namespace lms::metadata +} // namespace lms::metadata::taglib diff --git a/src/libs/metadata/impl/taglib/Utils.cpp b/src/libs/metadata/impl/taglib/Utils.cpp new file mode 100644 index 00000000..17e0f93a --- /dev/null +++ b/src/libs/metadata/impl/taglib/Utils.cpp @@ -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 . + */ + +#include "Utils.hpp" + +#include "TagLibDefs.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if TAGLIB_HAS_DSF + #include +#endif + +#include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" + +namespace lms::metadata::taglib::utils +{ + std::span getSupportedExtensions() + { + static const std::vector 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{ 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 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 file; + + // MP3 + if (ext == "MP3" || ext == "MP2" || ext == "AAC") + file = std::make_unique(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + // VORBIS + else if (ext == "OGG") + return std::make_unique(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(stream, readAudioProperties, audioPropertiesStyle); + if (!file->isValid()) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + } + else if (ext == "FLAC") + file = std::make_unique(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + else if (ext == "SPX") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (ext == "OPUS") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // APE + else if (ext == "MPC") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (ext == "WV") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (ext == "APE") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // TRUEAUDIO + else if (ext == "TTA") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // MP4 + else if (ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // ASF + else if (ext == "WMA" || ext == "ASF") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // RIFF + else if (ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (ext == "WAV") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); +#if TAGLIB_HAS_DSF + else if (ext == "DSF") + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (ext == "DFF" || ext == "DSDIFF") + file = std::make_unique(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 parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties) + { + std::unique_ptr file; + + if (TagLib::MPEG::File::isSupported(stream)) + file = std::make_unique(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + // VORBIS + else if (TagLib::Ogg::Vorbis::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::Ogg::FLAC::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::FLAC::File::isSupported(stream)) + file = std::make_unique(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + else if (TagLib::Ogg::Speex::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::Ogg::Opus::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // APE + else if (TagLib::MPC::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::WavPack::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::APE::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // TRUEAUDIO + else if (TagLib::TrueAudio::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // MP4 + else if (TagLib::MP4::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + //_ASF + else if (TagLib::ASF::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + // RIFF + else if (TagLib::RIFF::AIFF::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (TagLib::RIFF::WAV::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); +#if TAGLIB_HAS_DSF + else if (DSF::File::isSupported(stream)) + file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); + else if (DSDIFF::File::isSupported(stream)) + file = std::make_unique(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 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 file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) }; + if (!file) + file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value()); + + return file; + } +} // namespace lms::metadata::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/taglib/Utils.hpp b/src/libs/metadata/impl/taglib/Utils.hpp new file mode 100644 index 00000000..7ddafdbd --- /dev/null +++ b/src/libs/metadata/impl/taglib/Utils.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "core/TaggedType.hpp" +#include "metadata/Types.hpp" + +namespace lms::metadata::taglib::utils +{ + std::span getSupportedExtensions(); + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle); + + using ReadAudioProperties = core::TaggedBool; + std::unique_ptr parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties); +} // namespace lms::metadata::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/include/metadata/ArtistInfo.hpp b/src/libs/metadata/include/metadata/ArtistInfo.hpp index 5b981af1..75af760f 100644 --- a/src/libs/metadata/include/metadata/ArtistInfo.hpp +++ b/src/libs/metadata/include/metadata/ArtistInfo.hpp @@ -51,6 +51,6 @@ namespace lms::metadata using Exception::Exception; }; - std::span getSupportedInfoFileExtensions(); + std::span getSupportedArtistInfoFiles(); ArtistInfo parseArtistInfo(std::istream& is); } // namespace lms::metadata diff --git a/src/libs/metadata/include/metadata/Exception.hpp b/src/libs/metadata/include/metadata/Exception.hpp index 7a878420..2acdda72 100644 --- a/src/libs/metadata/include/metadata/Exception.hpp +++ b/src/libs/metadata/include/metadata/Exception.hpp @@ -19,6 +19,8 @@ #pragma once +#include + #include "core/Exception.hpp" namespace lms::metadata @@ -29,9 +31,31 @@ namespace lms::metadata 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: using Exception::Exception; }; + + class AudioFileNoAudioPropertiesException : public AudioFileParsingException + { + public: + using AudioFileParsingException::AudioFileParsingException; + }; + } // namespace lms::metadata \ No newline at end of file diff --git a/src/libs/metadata/include/metadata/IAudioFileParser.hpp b/src/libs/metadata/include/metadata/IAudioFileParser.hpp index 38eb6694..599773f0 100644 --- a/src/libs/metadata/include/metadata/IAudioFileParser.hpp +++ b/src/libs/metadata/include/metadata/IAudioFileParser.hpp @@ -24,6 +24,7 @@ #include #include +#include "metadata/Exception.hpp" #include "metadata/Types.hpp" namespace lms::metadata diff --git a/src/libs/metadata/test/PlayList.cpp b/src/libs/metadata/test/PlayList.cpp index e11c77ae..165963a5 100644 --- a/src/libs/metadata/test/PlayList.cpp +++ b/src/libs/metadata/test/PlayList.cpp @@ -30,6 +30,7 @@ namespace lms::metadata::tests #PLAYLIST:My super playlist 01-Foo.mp3 + #EXTINF:263,Alice in Chains - Don't Follow 02-FooBar.mp3 diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 7966d1f5..cd28e4b7 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -2,12 +2,14 @@ add_library(lmsscanner STATIC impl/helpers/ArtistHelpers.cpp impl/scanners/ArtistInfoFileScanner.cpp impl/scanners/AudioFileScanOperation.cpp + impl/scanners/FileScanOperationBase.cpp impl/scanners/AudioFileScanner.cpp impl/scanners/ImageFileScanner.cpp impl/scanners/LyricsFileScanner.cpp impl/scanners/PlayListFileScanner.cpp impl/scanners/Utils.cpp impl/steps/FileScanQueue.cpp + impl/steps/ScanErrorLogger.cpp impl/steps/ScanStepArtistReconciliation.cpp impl/steps/ScanStepAssociateArtistImages.cpp impl/steps/ScanStepAssociateExternalLyrics.cpp diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 8b47c6fb..c03cebbf 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -51,14 +51,16 @@ #include "steps/ScanStepScanFiles.hpp" #include "steps/ScanStepUpdateLibraryFields.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { using namespace db; namespace { - static constexpr std::string_view currentSettingsName{ "" }; - static constexpr std::string_view lastScanSettingsName{ "last_scan" }; + constexpr std::string_view currentSettingsName{}; + constexpr std::string_view lastScanSettingsName{ "last_scan" }; Wt::WDate getNextMonday(Wt::WDate current) { @@ -352,7 +354,7 @@ namespace lms::scanner _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) { @@ -433,11 +435,11 @@ namespace lms::scanner } }; _fileScanners.clear(); - _fileScanners.emplace_back(std::make_unique(_settings, _db)); + _fileScanners.emplace_back(std::make_unique(_db, _settings)); _fileScanners.emplace_back(std::make_unique(_db, _settings)); - _fileScanners.emplace_back(std::make_unique(_db)); - _fileScanners.emplace_back(std::make_unique(_db)); - _fileScanners.emplace_back(std::make_unique(_db)); + _fileScanners.emplace_back(std::make_unique(_db, _settings)); + _fileScanners.emplace_back(std::make_unique(_db, _settings)); + _fileScanners.emplace_back(std::make_unique(_db, _settings)); std::vector fileScanners; std::transform(std::cbegin(_fileScanners), std::cend(_fileScanners), std::back_inserter(fileScanners), [](const std::unique_ptr& scanner) { return scanner.get(); }); diff --git a/src/libs/services/scanner/impl/ScannerStats.cpp b/src/libs/services/scanner/impl/ScannerStats.cpp index 38c179f1..37887424 100644 --- a/src/libs/services/scanner/impl/ScannerStats.cpp +++ b/src/libs/services/scanner/impl/ScannerStats.cpp @@ -21,16 +21,9 @@ 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 { - return skips + additions + updates; + return skips + additions + updates + failures; } std::size_t ScanStats::nbChanges() const diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp index 51382c26..718a60eb 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp @@ -20,6 +20,7 @@ #include "ArtistInfoFileScanner.hpp" #include +#include #include "core/ILogger.hpp" #include "core/String.hpp" @@ -29,45 +30,33 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "metadata/ArtistInfo.hpp" +#include "metadata/Types.hpp" +#include "services/scanner/ScanErrors.hpp" -#include "IFileScanOperation.hpp" -#include "ScanContext.hpp" +#include "FileScanOperationBase.hpp" #include "ScannerSettings.hpp" #include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" -#include "metadata/Types.hpp" namespace lms::scanner { namespace { - class ArtistInfoFileScanOperation : public IFileScanOperation + class ArtistInfoFileScanOperation : public FileScanOperationBase { public: - ArtistInfoFileScanOperation(const FileToScan& file, const ScannerSettings& settings, db::Db& db) - : _file{ file.file } - , _mediaLibrary{ file.mediaLibrary } - , _settings{ settings } - , _db{ db } - { - } + using FileScanOperationBase::FileScanOperationBase; ~ArtistInfoFileScanOperation() override = default; ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete; ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete; private: - const std::filesystem::path& getFile() const override { return _file; }; core::LiteralString getName() const override { return "ScanArtistInfoFile"; } void scan() override; - void processResult(ScanContext& context) override; + OperationResult processResult() override; std::string getArtistNameFromArtistInfoFilePath(); - const std::filesystem::path _file; - const MediaLibraryInfo _mediaLibrary; - const ScannerSettings& _settings; - db::Db& _db; - std::optional _parsedArtistInfo; }; @@ -75,91 +64,82 @@ namespace lms::scanner { try { - std::ifstream ifs{ _file }; + std::ifstream ifs{ getFilePath() }; if (!ifs) { - LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file); + const std::error_code ec{ errno, std::generic_category() }; + addError(getFilePath(), ec); return; } _parsedArtistInfo = metadata::parseArtistInfo(ifs); if (_parsedArtistInfo->name.empty()) { - LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set"); + addError(getFilePath()); _parsedArtistInfo.reset(); } } catch (const metadata::ArtistInfoParseException& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read artist info in file " << _file << ": " << e.what()); + addError(getFilePath()); } } - void ArtistInfoFileScanOperation::processResult(ScanContext& context) + ArtistInfoFileScanOperation::OperationResult ArtistInfoFileScanOperation::processResult() { - ScanStats& stats{ context.stats }; - - const std::optional 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) }; + db::Session& dbSession{ getDb().getTLSSession() }; + db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, getFilePath()) }; if (!_parsedArtistInfo) { if (artistInfo) { 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 }; if (!artistInfo) { artistInfo = dbSession.create(); - 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()->setSortName(_parsedArtistInfo->sortName); - artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime); + artistInfo.modify()->setLastWriteTime(getLastWriteTime()); artistInfo.modify()->setType(_parsedArtistInfo->type); artistInfo.modify()->setGender(_parsedArtistInfo->gender); artistInfo.modify()->setDisambiguation(_parsedArtistInfo->disambiguation); 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 - artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary)); + 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, getFilePath().parent_path(), mediaLibrary)); const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional(_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()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID()); if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added artist info file " << _file); - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG, "Updated artist info file " << _file); - stats.updates++; + LMS_LOG(DBUPDATER, DEBUG, "Added artist info file " << getFilePath()); + return OperationResult::Added; } + + LMS_LOG(DBUPDATER, DEBUG, "Updated artist info file " << getFilePath()); + return OperationResult::Updated; } } // namespace - ArtistInfoFileScanner::ArtistInfoFileScanner(const ScannerSettings& settings, db::Db& db) - : _settings{ settings } - , _db{ db } + ArtistInfoFileScanner::ArtistInfoFileScanner(db::Db& db, const ScannerSettings& settings) + : _db{ db } + , _settings{ settings } { } @@ -168,45 +148,34 @@ namespace lms::scanner return "Artist info scanner"; } - std::span ArtistInfoFileScanner::getSupportedExtensions() const + std::span ArtistInfoFileScanner::getSupportedFiles() const { - return metadata::getSupportedInfoFileExtensions(); + return metadata::getSupportedArtistInfoFiles(); } - bool ArtistInfoFileScanner::needsScan(ScanContext& context, const FileToScan& file) const + std::span ArtistInfoFileScanner::getSupportedExtensions() const + { + return {}; + } + + bool ArtistInfoFileScanner::needsScan(const FileToScan& file) const { // 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) - if (!core::stringUtils::stringCaseInsensitiveEqual(file.file.stem().string(), "artist")) + if (!core::stringUtils::stringCaseInsensitiveEqual(file.filePath.stem().string(), "artist")) 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() }; auto transaction{ dbSession.createReadTransaction() }; - db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, file.file) }; - if (artistInfo - && artistInfo->getLastWriteTime() == lastWriteTime - && artistInfo->getScanVersion() == _settings.artistInfoScanVersion) - { - context.stats.skips++; - return false; - } + db::ArtistInfo::pointer artistInfo{ db::ArtistInfo::find(dbSession, file.filePath) }; - return true; + return !artistInfo + || artistInfo->getLastWriteTime() != file.lastWriteTime + || artistInfo->getScanVersion() != _settings.artistInfoScanVersion; } - std::unique_ptr ArtistInfoFileScanner::createScanOperation(const FileToScan& fileToScan) const + std::unique_ptr ArtistInfoFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(fileToScan, _settings, _db); + return std::make_unique(std::move(fileToScan), _db, _settings); } } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp index b94a11e3..4d0af151 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp @@ -36,18 +36,19 @@ namespace lms::scanner class ArtistInfoFileScanner : public IFileScanner { public: - ArtistInfoFileScanner(const ScannerSettings& _settings, db::Db& db); + ArtistInfoFileScanner(db::Db& db, const ScannerSettings& _settings); ~ArtistInfoFileScanner() override = default; ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete; ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete; private: core::LiteralString getName() const override; + std::span getSupportedFiles() const override; std::span getSupportedExtensions() const override; - bool needsScan(ScanContext& context, const FileToScan& file) const override; - std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override; + bool needsScan(const FileToScan& file) const override; + std::unique_ptr createScanOperation(FileToScan&& fileToScan) const override; - const ScannerSettings& _settings; db::Db& _db; + const ScannerSettings& _settings; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp index 8ca0c14e..8a33e817 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp @@ -41,9 +41,10 @@ #include "image/Exception.hpp" #include "image/Image.hpp" #include "metadata/Exception.hpp" +#include "metadata/IAudioFileParser.hpp" +#include "services/scanner/ScanErrors.hpp" #include "IFileScanOperation.hpp" -#include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" @@ -387,7 +388,7 @@ namespace lms::scanner 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; // Add as many fields as possible to limit errors @@ -401,7 +402,7 @@ namespace lms::scanner } if (parsedTrack.position) params.setTrackNumber(*parsedTrack.position); - params.setFileSize(fileInfo.fileSize); + params.setFileSize(fileSize); bool error{}; db::Track::pointer res; @@ -413,7 +414,7 @@ namespace lms::scanner 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; } res = track; @@ -474,6 +475,14 @@ namespace lms::scanner } } // 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() { LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile"); @@ -481,13 +490,13 @@ namespace lms::scanner try { - _parsedTrack = _parser.parseMetaData(_file); + _parsedTrack = _parser.parseMetaData(getFilePath()); // We fill missing artist mbids with mbids found on other artist roles fillMissingMbids(*_parsedTrack); std::size_t index{}; - _parser.parseImages(_file, [&](const metadata::Image& image) { + _parser.parseImages(getFilePath(), [&](const metadata::Image& image) { try { image::ImageProperties properties{ image::probeImage(image.data) }; @@ -508,46 +517,43 @@ namespace lms::scanner } catch (const image::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file); + addError(getFilePath(), index); } index++; }); } + catch (const metadata::AudioFileNoAudioPropertiesException&) + { + addError(getFilePath()); + } + catch (const metadata::IOException& e) + { + addError(getFilePath(), e.getErrorCode()); + } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << _file); + addError(getFilePath()); } } - void AudioFileScanOperation::processResult(ScanContext& context) + AudioFileScanOperation::OperationResult AudioFileScanOperation::processResult() { LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData"); - ScanStats& stats{ context.stats }; - - const std::optional 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) }; - + db::Session& dbSession{ getDb().getTLSSession() }; + db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) }; if (!_parsedTrack) { if (track) { track.remove(); - stats.deletions++; + return OperationResult::Removed; } - context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadAudioFile); - return; + return OperationResult::Skipped; } - if (_parsedTrack->mbid && (!track || _settings.skipDuplicateTrackMBID)) + if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID)) { std::vector duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) }; @@ -558,14 +564,14 @@ namespace lms::scanner std::error_code 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.modify()->setAbsoluteFilePath(_file); + track.modify()->setAbsoluteFilePath(getFilePath()); } } // Skip duplicate track MBID - if (_settings.skipDuplicateTrackMBID) + if (getScannerSettings().skipDuplicateTrackMBID) { for (db::Track::pointer& otherTrack : duplicateTracks) { @@ -574,22 +580,26 @@ namespace lms::scanner continue; // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file - if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries), + auto& mediaLibraries{ getScannerSettings().mediaLibraries }; + if (std::none_of(std::cbegin(mediaLibraries), std::cend(mediaLibraries), [&](const MediaLibraryInfo& libraryInfo) { - return core::pathUtils::isPathInRootPath(_file, libraryInfo.rootDirectory, &excludeDirFileName); + return core::pathUtils::isPathInRootPath(getFilePath(), libraryInfo.rootDirectory, &excludeDirFileName); })) { 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 if (track) { 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) { // maybe the file just moved? - track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo); + track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFileSize(), getRelativeFilePath()); if (track) { - LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath()); - track.modify()->setAbsoluteFilePath(_file); + LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath()); + track.modify()->setAbsoluteFilePath(getFilePath()); } } // We estimate this is an audio file if the duration is not null if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero()) { - LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (duration is 0)"); + addError(getFilePath()); - // If Track exists here, delete it! if (track) { track.remove(); - stats.deletions++; + return OperationResult::Removed; } - stats.errors.emplace_back(_file, ScanErrorType::BadDuration); - return; + return OperationResult::Skipped; } // ***** Title @@ -626,9 +634,9 @@ namespace lms::scanner title = _parsedTrack->title; else { - // TODO parse file name guess track etc. - // For now juste use file name as title - title = _file.filename().string(); + // TODO parse file name to guess track etc. + // For now, we just use file name as title + title = getFilePath().filename().string(); } // If file already exists, update its data @@ -639,13 +647,13 @@ namespace lms::scanner track = dbSession.create(); added = true; - track.modify()->setAbsoluteFilePath(_file); - track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime + track.modify()->setAbsoluteFilePath(getFilePath()); + track.modify()->setAddedTime(getMediaLibrary().firstScan ? getLastWriteTime() : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime } // Track related data assert(track); - track.modify()->setScanVersion(_settings.audioScanVersion); + track.modify()->setScanVersion(getScannerSettings().audioScanVersion); // Audio properties track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate); @@ -654,9 +662,9 @@ namespace lms::scanner track.modify()->setDuration(_parsedTrack->audioProperties.duration); track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate); - track.modify()->setRelativeFilePath(fileInfo->relativePath); - track.modify()->setFileSize(fileInfo->fileSize); - track.modify()->setLastWriteTime(fileInfo->lastWriteTime); + track.modify()->setRelativeFilePath(getRelativeFilePath()); + track.modify()->setFileSize(getFileSize()); + track.modify()->setLastWriteTime(getLastWriteTime()); if (_parsedTrack->encodingTime.isValid()) { @@ -672,14 +680,14 @@ namespace lms::scanner 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); - 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()->clearArtistLinks(); - const helpers::AllowFallbackOnMBIDEntry allowFallback{ _settings.allowArtistMBIDFallback }; + const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback); if (_parsedTrack->medium && _parsedTrack->medium->release) createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback); @@ -732,13 +740,11 @@ namespace lms::scanner if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << _file); - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << _file); - stats.updates++; + LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << getFilePath()); + return OperationResult::Added; } + + LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << getFilePath()); + return OperationResult::Updated; } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp index 3de06544..a6bf3c54 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp @@ -25,16 +25,21 @@ #include #include "image/Types.hpp" -#include "metadata/IAudioFileParser.hpp" +#include "metadata/Types.hpp" +#include "FileScanOperationBase.hpp" #include "FileToScan.hpp" -#include "IFileScanner.hpp" namespace lms::db { class Db; } // namespace lms::db +namespace lms::metadata +{ + class IAudioFileParser; +} // namespace lms::metadata + namespace lms::scanner { struct ImageInfo @@ -48,32 +53,20 @@ namespace lms::scanner std::string description; }; - class AudioFileScanOperation : public IFileScanOperation + class AudioFileScanOperation : public FileScanOperationBase { public: - AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IAudioFileParser& parser, const ScannerSettings& settings) - : _file{ fileToScan.file } - , _mediaLibrary{ fileToScan.mediaLibrary } - , _db{ db } - , _parser{ parser } - , _settings{ settings } - { - } - ~AudioFileScanOperation() override = default; + AudioFileScanOperation(FileToScan&& fileToScan, db::Db& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser); + ~AudioFileScanOperation() override; AudioFileScanOperation(const AudioFileScanOperation&) = delete; AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete; private: - const std::filesystem::path& getFile() const override { return _file; }; core::LiteralString getName() const override { return "ScanAudioFile"; } 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; - const ScannerSettings& _settings; std::unique_ptr _parsedTrack; std::vector _parsedImages; }; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index 14b74efa..476bfea9 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -25,9 +25,9 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "metadata/IAudioFileParser.hpp" #include "AudioFileScanOperation.hpp" -#include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "Utils.hpp" @@ -77,66 +77,29 @@ namespace lms::scanner return "Audio scanner"; } + std::span AudioFileScanner::getSupportedFiles() const + { + return {}; + } + std::span AudioFileScanner::getSupportedExtensions() const { 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() }; + auto transaction{ dbSession.createReadTransaction() }; - { - auto transaction{ dbSession.createReadTransaction() }; - - // Skip file if last write is the same - const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) }; - if (track - && track->getLastWriteTime() == lastWriteTime - && track->getScanVersion() == _settings.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 + const db::Track::pointer track{ db::Track::findByPath(dbSession, file.filePath) }; + return !track + || track->getLastWriteTime() != file.lastWriteTime + || track->getScanVersion() != _settings.audioScanVersion; } - std::unique_ptr AudioFileScanner::createScanOperation(const FileToScan& fileToScan) const + std::unique_ptr AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(fileToScan, _db, *_metadataParser, _settings); + return std::make_unique(std::move(fileToScan), _db, _settings, *_metadataParser); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp index dd6a9247..3fa46cbf 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp @@ -48,9 +48,10 @@ namespace lms::scanner private: core::LiteralString getName() const override; + std::span getSupportedFiles() const override; std::span getSupportedExtensions() const override; - bool needsScan(ScanContext& context, const FileToScan& file) const override; - std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override; + bool needsScan(const FileToScan& file) const override; + std::unique_ptr createScanOperation(FileToScan&& fileToScan) const override; db::Db& _db; const ScannerSettings& _settings; diff --git a/src/libs/services/scanner/impl/scanners/FileScanOperationBase.cpp b/src/libs/services/scanner/impl/scanners/FileScanOperationBase.cpp new file mode 100644 index 00000000..ca3ca660 --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/FileScanOperationBase.cpp @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/FileScanOperationBase.hpp b/src/libs/services/scanner/impl/scanners/FileScanOperationBase.hpp new file mode 100644 index 00000000..6d3b717d --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/FileScanOperationBase.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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 + void addError(CtrArgs&&... args) + { + _errors.emplace_back(std::make_shared(std::forward(args)...)); + } + + const ScanErrorVector& getErrors() override { return _errors; } + + private: + const FileToScan _file; + db::Db& _db; + const ScannerSettings& _settings; + ScanErrorVector _errors; + }; +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/FileToScan.hpp b/src/libs/services/scanner/impl/scanners/FileToScan.hpp index 8a94a0f3..8f44015d 100644 --- a/src/libs/services/scanner/impl/scanners/FileToScan.hpp +++ b/src/libs/services/scanner/impl/scanners/FileToScan.hpp @@ -21,13 +21,18 @@ #include +#include + #include "MediaLibraryInfo.hpp" namespace lms::scanner { struct FileToScan { - std::filesystem::path file; + std::filesystem::path filePath; + std::filesystem::path relativePath; MediaLibraryInfo mediaLibrary; + Wt::WDateTime lastWriteTime; + std::size_t fileSize{}; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/IFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/IFileScanOperation.hpp index cbf828af..d7c2c22c 100644 --- a/src/libs/services/scanner/impl/scanners/IFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/IFileScanOperation.hpp @@ -20,12 +20,14 @@ #pragma once #include +#include +#include #include "core/LiteralString.hpp" namespace lms::scanner { - struct ScanContext; + struct ScanError; class IFileScanOperation { @@ -34,8 +36,23 @@ namespace lms::scanner 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 processResult(ScanContext& context) = 0; + + enum class OperationResult + { + Added, + Removed, + Updated, + Skipped, + }; + virtual OperationResult processResult() = 0; + + using ScanErrorVector = std::vector>; + // list of errors collected during scan/result processing (there might be errors without skipping the file) + virtual const ScanErrorVector& getErrors() = 0; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/IFileScanner.hpp b/src/libs/services/scanner/impl/scanners/IFileScanner.hpp index 381054f2..81c9bdf2 100644 --- a/src/libs/services/scanner/impl/scanners/IFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/IFileScanner.hpp @@ -29,8 +29,6 @@ namespace lms::scanner { class IFileScanOperation; - struct ScanContext; - struct ScannerSettings; class IFileScanner { @@ -38,8 +36,9 @@ namespace lms::scanner virtual ~IFileScanner() = default; virtual core::LiteralString getName() const = 0; + virtual std::span getSupportedFiles() const = 0; virtual std::span getSupportedExtensions() const = 0; - virtual bool needsScan(ScanContext& context, const FileToScan& file) const = 0; - virtual std::unique_ptr createScanOperation(const FileToScan& fileToScan) const = 0; + virtual bool needsScan(const FileToScan& file) const = 0; + virtual std::unique_ptr createScanOperation(FileToScan&& fileToScan) const = 0; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp index d84a3c65..7869e938 100644 --- a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp @@ -27,34 +27,26 @@ #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "image/Exception.hpp" -#include "image/IRawImage.hpp" #include "image/Image.hpp" +#include "services/scanner/ScanErrors.hpp" +#include "FileScanOperationBase.hpp" #include "IFileScanOperation.hpp" -#include "ScanContext.hpp" #include "Utils.hpp" namespace lms::scanner { namespace { - class ImageFileScanOperation : public IFileScanOperation + class ImageFileScanOperation : public FileScanOperationBase { public: - ImageFileScanOperation(const FileToScan& file, db::Db& db) - : _file{ file.file } - , _mediaLibrary{ file.mediaLibrary } - , _db{ db } {} + using FileScanOperationBase::FileScanOperationBase; private: - const std::filesystem::path& getFile() const override { return _file; }; core::LiteralString getName() const override { return "ScanImageFile"; } void scan() override; - void processResult(ScanContext& context) override; - - const std::filesystem::path _file; - const MediaLibraryInfo _mediaLibrary; - db::Db& _db; + OperationResult processResult() override; std::optional _parsedImageProperties; }; @@ -63,67 +55,58 @@ namespace lms::scanner { try { - _parsedImageProperties = image::probeImage(_file); + _parsedImageProperties = image::probeImage(getFilePath()); } catch (const image::Exception& e) { _parsedImageProperties.reset(); - LMS_LOG(DBUPDATER, ERROR, "Cannot read image in file " << _file << ": " << e.what()); + addError(getFilePath()); } } - void ImageFileScanOperation::processResult(ScanContext& context) + ImageFileScanOperation::OperationResult ImageFileScanOperation::processResult() { - ScanStats& stats{ context.stats }; - - const std::optional 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) }; + db::Session& dbSession{ getDb().getTLSSession() }; + db::Image::pointer image{ db::Image::find(dbSession, getFilePath()) }; if (!_parsedImageProperties) { if (image) { 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 }; if (!image) - image = dbSession.create(_file); + image = dbSession.create(getFilePath()); - image.modify()->setLastWriteTime(fileInfo->lastWriteTime); - image.modify()->setFileSize(fileInfo->fileSize); + image.modify()->setLastWriteTime(getLastWriteTime()); + image.modify()->setFileSize(getFileSize()); image.modify()->setHeight(_parsedImageProperties->height); 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 - image.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary)); + 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, getFilePath().parent_path(), mediaLibrary)); if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added image " << _file); - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG, "Updated image " << _file); - stats.updates++; + LMS_LOG(DBUPDATER, DEBUG, "Added image " << getFilePath()); + return OperationResult::Added; } + + LMS_LOG(DBUPDATER, DEBUG, "Updated image " << getFilePath()); + return OperationResult::Updated; } } // namespace - ImageFileScanner::ImageFileScanner(db::Db& db) + ImageFileScanner::ImageFileScanner(db::Db& db, ScannerSettings& settings) : _db{ db } + , _settings{ settings } { } @@ -132,41 +115,27 @@ namespace lms::scanner return "Image scanner"; } + std::span ImageFileScanner::getSupportedFiles() const + { + return {}; + } + std::span ImageFileScanner::getSupportedExtensions() const { 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) }; - // Should rarely fail as we are currently iterating it - if (!lastWriteTime.isValid()) - { - stats.skips++; - return false; - } - - if (!context.scanOptions.fullScan) - { - db::Session& dbSession{ _db.getTLSSession() }; - auto transaction{ _db.getTLSSession().createReadTransaction() }; - - const db::Image::pointer image{ db::Image::find(dbSession, file.file) }; - if (image && image->getLastWriteTime() == lastWriteTime) - { - stats.skips++; - return false; - } - } - - return true; // need to scan + const db::Image::pointer image{ db::Image::find(dbSession, file.filePath) }; + return (!image || image->getLastWriteTime() != file.lastWriteTime); } - std::unique_ptr ImageFileScanner::createScanOperation(const FileToScan& fileToScan) const + std::unique_ptr ImageFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(fileToScan, _db); + return std::make_unique(std::move(fileToScan), _db, _settings); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/ImageFileScanner.hpp b/src/libs/services/scanner/impl/scanners/ImageFileScanner.hpp index a5ade2a6..4e34b0cb 100644 --- a/src/libs/services/scanner/impl/scanners/ImageFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/ImageFileScanner.hpp @@ -36,17 +36,19 @@ namespace lms::scanner class ImageFileScanner : public IFileScanner { public: - ImageFileScanner(db::Db& db); + ImageFileScanner(db::Db& db, ScannerSettings& settings); ~ImageFileScanner() override = default; ImageFileScanner(const ImageFileScanner&) = delete; ImageFileScanner& operator=(const ImageFileScanner&) = delete; private: core::LiteralString getName() const override; + std::span getSupportedFiles() const override; std::span getSupportedExtensions() const override; - bool needsScan(ScanContext& context, const FileToScan& file) const override; - std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override; + bool needsScan(const FileToScan& file) const override; + std::unique_ptr createScanOperation(FileToScan&& fileToScan) const override; db::Db& _db; + ScannerSettings& _settings; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp index ef33f67c..f88e3563 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp @@ -22,38 +22,31 @@ #include #include +#include "FileScanOperationBase.hpp" +#include "ScannerSettings.hpp" #include "core/ILogger.hpp" #include "database/Db.hpp" #include "database/MediaLibrary.hpp" #include "database/Session.hpp" #include "database/TrackLyrics.hpp" #include "metadata/Lyrics.hpp" +#include "services/scanner/ScanErrors.hpp" -#include "IFileScanOperation.hpp" -#include "ScanContext.hpp" #include "Utils.hpp" namespace lms::scanner { namespace { - class LyricsFileScanOperation : public IFileScanOperation + class LyricsFileScanOperation : public FileScanOperationBase { public: - LyricsFileScanOperation(const FileToScan& file, db::Db& db) - : _file{ file.file } - , _mediaLibrary{ file.mediaLibrary } - , _db{ db } {} + using FileScanOperationBase::FileScanOperationBase; private: - const std::filesystem::path& getFile() const override { return _file; }; core::LiteralString getName() const override { return "ScanLyricsFile"; } void scan() override; - void processResult(ScanContext& context) override; - - const std::filesystem::path _file; - const MediaLibraryInfo _mediaLibrary; - db::Db& _db; + OperationResult processResult() override; std::optional _parsedLyrics; }; @@ -62,53 +55,50 @@ namespace lms::scanner { try { - std::ifstream ifs{ _file }; + std::ifstream ifs{ getFilePath() }; if (!ifs) - LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file); - else - _parsedLyrics = metadata::parseLyrics(ifs); + { + const std::error_code ec{ errno, std::generic_category() }; + + addError(getFilePath(), ec); + return; + } + + _parsedLyrics = metadata::parseLyrics(ifs); } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read lyrics in file " << _file << ": " << e.what()); + addError(getFilePath()); } } - void LyricsFileScanOperation::processResult(ScanContext& context) + LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult() { - ScanStats& stats{ context.stats }; - - const std::optional 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) }; + db::Session& dbSession{ getDb().getTLSSession() }; + db::TrackLyrics::pointer trackLyrics{ db::TrackLyrics::find(dbSession, getFilePath()) }; if (!_parsedLyrics) { if (trackLyrics) { 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 }; if (!trackLyrics) { trackLyrics = dbSession.create(); - trackLyrics.modify()->setAbsoluteFilePath(_file); + trackLyrics.modify()->setAbsoluteFilePath(getFilePath()); } - trackLyrics.modify()->setLastWriteTime(fileInfo->lastWriteTime); - trackLyrics.modify()->setFileSize(fileInfo->fileSize); + trackLyrics.modify()->setLastWriteTime(getLastWriteTime()); + trackLyrics.modify()->setFileSize(getFileSize()); trackLyrics.modify()->setLanguage(!_parsedLyrics->language.empty() ? _parsedLyrics->language : "xxx"); trackLyrics.modify()->setOffset(_parsedLyrics->offset); trackLyrics.modify()->setDisplayTitle(_parsedLyrics->displayTitle); @@ -118,24 +108,23 @@ namespace lms::scanner else trackLyrics.modify()->setUnsynchronizedLines(_parsedLyrics->unsynchronizedLines); - db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this - trackLyrics.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary)); + 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, getFilePath().parent_path(), mediaLibrary)); if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics " << _file); - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics " << _file); - stats.updates++; + LMS_LOG(DBUPDATER, DEBUG, "Added external lyrics " << getFilePath()); + return OperationResult::Added; } + + LMS_LOG(DBUPDATER, DEBUG, "Updated external lyrics " << getFilePath()); + return OperationResult::Updated; } } // namespace - LyricsFileScanner::LyricsFileScanner(db::Db& db) + LyricsFileScanner::LyricsFileScanner(db::Db& db, ScannerSettings& _settings) : _db{ db } + , _settings{ _settings } { } @@ -144,41 +133,27 @@ namespace lms::scanner return "Lyrics scanner"; } + std::span LyricsFileScanner::getSupportedFiles() const + { + return {}; + } + std::span LyricsFileScanner::getSupportedExtensions() const { 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) }; - // Should rarely fail as we are currently iterating it - if (!lastWriteTime.isValid()) - { - stats.skips++; - return false; - } - - if (!context.scanOptions.fullScan) - { - db::Session& dbSession{ _db.getTLSSession() }; - auto transaction{ _db.getTLSSession().createReadTransaction() }; - - const db::TrackLyrics::pointer lyrics{ db::TrackLyrics::find(dbSession, file.file) }; - if (lyrics && lyrics->getLastWriteTime() == lastWriteTime) - { - stats.skips++; - return false; - } - } - - return true; // need to scan + const db::TrackLyrics::pointer lyrics{ db::TrackLyrics::find(dbSession, file.filePath) }; + return !lyrics || lyrics->getLastWriteTime() != file.lastWriteTime; } - std::unique_ptr LyricsFileScanner::createScanOperation(const FileToScan& fileToScan) const + std::unique_ptr LyricsFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(fileToScan, _db); + return std::make_unique(std::move(fileToScan), _db, _settings); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp index 44fb2e1d..3bcb4f0d 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp @@ -36,17 +36,19 @@ namespace lms::scanner class LyricsFileScanner : public IFileScanner { public: - LyricsFileScanner(db::Db& db); + LyricsFileScanner(db::Db& db, ScannerSettings& settings); ~LyricsFileScanner() override = default; LyricsFileScanner(const LyricsFileScanner&) = delete; LyricsFileScanner& operator=(const LyricsFileScanner&) = delete; private: core::LiteralString getName() const override; + std::span getSupportedFiles() const override; std::span getSupportedExtensions() const override; - bool needsScan(ScanContext& context, const FileToScan& file) const override; - std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override; + bool needsScan(const FileToScan& file) const override; + std::unique_ptr createScanOperation(FileToScan&& fileToScan) const override; db::Db& _db; + ScannerSettings& _settings; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp index cfef85dd..ca168dab 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp @@ -30,7 +30,7 @@ #include "metadata/Exception.hpp" #include "metadata/PlayList.hpp" -#include "IFileScanOperation.hpp" +#include "FileScanOperationBase.hpp" #include "ScanContext.hpp" #include "Utils.hpp" @@ -38,26 +38,18 @@ namespace lms::scanner { namespace { - class PlayListFileScanOperation : public IFileScanOperation + class PlayListFileScanOperation : public FileScanOperationBase { public: - PlayListFileScanOperation(const FileToScan& file, db::Db& db) - : _file{ file.file } - , _mediaLibrary{ file.mediaLibrary } - , _db{ db } {} + using FileScanOperationBase::FileScanOperationBase; ~PlayListFileScanOperation() override = default; PlayListFileScanOperation(const PlayListFileScanOperation&) = delete; PlayListFileScanOperation& operator=(const PlayListFileScanOperation&) = delete; private: - const std::filesystem::path& getFile() const override { return _file; }; core::LiteralString getName() const override { return "ScanPlayListFile"; } void scan() override; - void processResult(ScanContext& context) override; - - const std::filesystem::path _file; - const MediaLibraryInfo _mediaLibrary; - db::Db& _db; + OperationResult processResult() override; std::optional _parsedPlayList; }; @@ -66,74 +58,72 @@ namespace lms::scanner { try { - std::ifstream ifs{ _file }; + std::ifstream ifs{ getFilePath() }; if (!ifs) - LMS_LOG(DBUPDATER, ERROR, "Cannot open file " << _file); - else - _parsedPlayList = metadata::parsePlayList(ifs); + { + const std::error_code ec{ errno, std::generic_category() }; + + addError(getFilePath(), ec); + return; + } + + _parsedPlayList = metadata::parsePlayList(ifs); } catch (const metadata::Exception& e) { - LMS_LOG(DBUPDATER, ERROR, "Cannot read playlist in file " << _file << ": " << e.what()); + addError(getFilePath()); } } - void PlayListFileScanOperation::processResult(ScanContext& context) + PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult() { - ScanStats& stats{ context.stats }; - - const std::optional 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) }; + db::Session& dbSession{ getDb().getTLSSession() }; + db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, getFilePath()) }; if (!_parsedPlayList) { if (playList) { 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 }; if (!playList) - playList = dbSession.create(_file); + playList = dbSession.create(getFilePath()); - playList.modify()->setLastWriteTime(fileInfo->lastWriteTime); - playList.modify()->setFileSize(fileInfo->fileSize); + playList.modify()->setLastWriteTime(getLastWriteTime()); + playList.modify()->setFileSize(getFileSize()); if (!_parsedPlayList->name.empty()) playList.modify()->setName(_parsedPlayList->name); else - playList.modify()->setName(_file.stem().string()); + playList.modify()->setName(getFilePath().stem().string()); playList.modify()->setFiles(_parsedPlayList->files); - db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this - playList.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary)); + 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, getFilePath().parent_path(), mediaLibrary)); if (added) { - LMS_LOG(DBUPDATER, DEBUG, "Added playlist file " << _file); - stats.additions++; + LMS_LOG(DBUPDATER, DEBUG, "Added playlist file " << getFilePath()); + return OperationResult::Added; } else { - LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << _file); - stats.updates++; + LMS_LOG(DBUPDATER, DEBUG, "Updated playlist file '" << getFilePath()); + return OperationResult::Updated; } } } // namespace - PlayListFileScanner::PlayListFileScanner(db::Db& db) + PlayListFileScanner::PlayListFileScanner(db::Db& db, ScannerSettings& settings) : _db{ db } + , _settings{ settings } { } @@ -142,41 +132,27 @@ namespace lms::scanner return "PlayList scanner"; } + std::span PlayListFileScanner::getSupportedFiles() const + { + return {}; + } + std::span PlayListFileScanner::getSupportedExtensions() const { 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) }; - // Should rarely fail as we are currently iterating it - if (!lastWriteTime.isValid()) - { - stats.skips++; - return false; - } - - if (!context.scanOptions.fullScan) - { - db::Session& dbSession{ _db.getTLSSession() }; - auto transaction{ _db.getTLSSession().createReadTransaction() }; - - const db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, file.file) }; - if (playList && playList->getLastWriteTime() == lastWriteTime) - { - stats.skips++; - return false; - } - } - - return true; // need to scan + const db::PlayListFile::pointer playList{ db::PlayListFile::find(dbSession, file.filePath) }; + return !playList || playList->getLastWriteTime() != file.lastWriteTime; } - std::unique_ptr PlayListFileScanner::createScanOperation(const FileToScan& fileToScan) const + std::unique_ptr PlayListFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(fileToScan, _db); + return std::make_unique(std::move(fileToScan), _db, _settings); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp index d61d7e67..db0f2333 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp @@ -36,17 +36,19 @@ namespace lms::scanner class PlayListFileScanner : public IFileScanner { public: - PlayListFileScanner(db::Db& db); + PlayListFileScanner(db::Db& db, ScannerSettings& settings); ~PlayListFileScanner() override = default; PlayListFileScanner(const PlayListFileScanner&) = delete; PlayListFileScanner& operator=(const PlayListFileScanner&) = delete; private: core::LiteralString getName() const override; + std::span getSupportedFiles() const override; std::span getSupportedExtensions() const override; - bool needsScan(ScanContext& context, const FileToScan& file) const override; - std::unique_ptr createScanOperation(const FileToScan& fileToScan) const override; + bool needsScan(const FileToScan& file) const override; + std::unique_ptr createScanOperation(FileToScan&& fileToScan) const override; db::Db& _db; + ScannerSettings& _settings; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/Utils.cpp b/src/libs/services/scanner/impl/scanners/Utils.cpp index 48e7bbfd..4469a3a1 100644 --- a/src/libs/services/scanner/impl/scanners/Utils.cpp +++ b/src/libs/services/scanner/impl/scanners/Utils.cpp @@ -19,7 +19,8 @@ #include "Utils.hpp" -#include "core/ILogger.hpp" +#include + #include "core/Path.hpp" #include "database/Directory.hpp" #include "database/MediaLibrary.hpp" @@ -27,59 +28,6 @@ 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 retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath) - { - std::optional res; - res.emplace(); - - res->lastWriteTime = retrieveFileGetLastWrite(file); - if (!res->lastWriteTime.isValid()) - { - res.reset(); - return res; - } - - { - std::error_code ec; - res->relativePath = std::filesystem::relative(file, rootPath, ec); - if (ec) - { - LMS_LOG(DBUPDATER, ERROR, "Cannot get relative file path for '" << file.string() << "' from '" << rootPath.string() << "': " << ec.message()); - res.reset(); - return res; - } - } - - { - std::error_code ec; - res->fileSize = std::filesystem::file_size(file, ec); - if (ec) - { - LMS_LOG(DBUPDATER, ERROR, "Cannot get file size for '" << file.string() << "': " << ec.message()); - res.reset(); - return res; - } - } - - return res; - } - db::Directory::pointer getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::MediaLibrary::pointer& mediaLibrary) { db::Directory::pointer directory{ db::Directory::find(session, path) }; diff --git a/src/libs/services/scanner/impl/scanners/Utils.hpp b/src/libs/services/scanner/impl/scanners/Utils.hpp index 2adafd3e..710e3309 100644 --- a/src/libs/services/scanner/impl/scanners/Utils.hpp +++ b/src/libs/services/scanner/impl/scanners/Utils.hpp @@ -20,9 +20,6 @@ #pragma once #include -#include - -#include #include "database/Object.hpp" @@ -35,18 +32,8 @@ namespace lms::db namespace lms::scanner { - struct FileInfo - { - Wt::WDateTime lastWriteTime; - std::filesystem::path relativePath; - std::size_t fileSize{}; - }; - namespace utils { - Wt::WDateTime retrieveFileGetLastWrite(const std::filesystem::path& file); - std::optional retrieveFileInfo(const std::filesystem::path& file, const std::filesystem::path& rootPath); - db::ObjectPtr getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr& mediaLibrary); } // namespace utils } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/FileScanQueue.cpp b/src/libs/services/scanner/impl/steps/FileScanQueue.cpp index 4c71027e..9b61a808 100644 --- a/src/libs/services/scanner/impl/steps/FileScanQueue.cpp +++ b/src/libs/services/scanner/impl/steps/FileScanQueue.cpp @@ -53,7 +53,7 @@ namespace lms::scanner { { 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(); } diff --git a/src/libs/services/scanner/impl/steps/IScanStep.hpp b/src/libs/services/scanner/impl/steps/IScanStep.hpp index 44adb1f7..7f851e3f 100644 --- a/src/libs/services/scanner/impl/steps/IScanStep.hpp +++ b/src/libs/services/scanner/impl/steps/IScanStep.hpp @@ -20,11 +20,12 @@ #pragma once #include "core/LiteralString.hpp" - -#include "ScanContext.hpp" +#include "services/scanner/ScannerStats.hpp" namespace lms::scanner { + struct ScanContext; + class IScanStep { public: diff --git a/src/libs/services/scanner/impl/steps/ScanErrorLogger.cpp b/src/libs/services/scanner/impl/steps/ScanErrorLogger.cpp new file mode 100644 index 00000000..c42b8523 --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ScanErrorLogger.cpp @@ -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 . + */ + +#include "ScanErrorLogger.hpp" + +#include + +#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 \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ScanErrorLogger.hpp b/src/libs/services/scanner/impl/steps/ScanErrorLogger.hpp new file mode 100644 index 00000000..fbf45d75 --- /dev/null +++ b/src/libs/services/scanner/impl/steps/ScanErrorLogger.hpp @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp index 8882649b..c39bc2ba 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp @@ -32,6 +32,7 @@ #include "database/TrackList.hpp" #include "metadata/Types.hpp" +#include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "helpers/ArtistHelpers.hpp" diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index e770f089..96c16532 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -37,6 +37,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { namespace diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateExternalLyrics.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateExternalLyrics.cpp index 19f4a1f3..7765eb58 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateExternalLyrics.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateExternalLyrics.cpp @@ -28,6 +28,8 @@ #include "database/Track.hpp" #include "database/TrackLyrics.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { namespace diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp index 628363e4..7e058cb5 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociatePlayListTracks.cpp @@ -30,7 +30,9 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "services/scanner/ScanErrors.hpp" +#include "ScanContext.hpp" #include "ScannerSettings.hpp" namespace lms::scanner @@ -57,6 +59,7 @@ namespace lms::scanner db::PlayListFileId lastRetrievedPlayListFileId; std::size_t processedPlayListFileCount{}; const ScannerSettings& settings; + std::vector> errors; }; 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; 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 true; - - return false; + return std::all_of(std::cbegin(tracks) + 1, std::cend(tracks), [=](const TrackInfo& trackInfo) { return trackInfo.releaseId == releaseId; }); } bool trackListNeedsUpdate(db::Session& session, std::string_view name, std::span tracks, const db::TrackList::pointer& trackList) @@ -124,17 +124,28 @@ namespace lms::scanner playListAssociation.playListFileIdId = playListFile->getId(); + std::vector> pendingErrors; + const auto files{ playListFile->getFiles() }; for (const std::filesystem::path& file : files) { // TODO optim: no need to fetch the whole track - db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) }; + const db::Track::pointer track{ getMatchingTrack(searchContext.session, file, playListFile->getDirectory()) }; if (track) playListAssociation.tracks.push_back(TrackInfo{ .trackId = track->getId(), .releaseId = track->getReleaseId() }); else - LMS_LOG(DBUPDATER, DEBUG, "Track " << file << " not found in playlist " << playListFile->getAbsoluteFilePath()); + { + pendingErrors.emplace_back(std::make_shared(playListFile->getAbsoluteFilePath(), file)); + } } + if (pendingErrors.size() == files.size()) + { + pendingErrors.clear(); + pendingErrors.emplace_back(std::make_shared(playListFile->getAbsoluteFilePath())); + } + searchContext.errors.insert(std::end(searchContext.errors), std::begin(pendingErrors), std::end(pendingErrors)); + if (playListAssociation.tracks.empty() || (searchContext.settings.skipSingleReleasePlayLists && isSingleReleasePlayList(playListAssociation.tracks))) { @@ -237,6 +248,7 @@ namespace lms::scanner .session = session, .lastRetrievedPlayListFileId = {}, .settings = _settings, + .errors = context.stats.errors }; PlayListFileAssociationContainer playListFileAssociations; @@ -246,7 +258,12 @@ namespace lms::scanner return; updatePlayListFiles(session, playListFileAssociations); + context.currentStepStats.processedElems = searchContext.processedPlayListFileCount; + for (const std::shared_ptr& error : searchContext.errors) + addError(context, error); + searchContext.errors.clear(); + _progressCallback(context.currentStepStats); } } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index 4ec18e89..d758e192 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -34,6 +34,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { namespace diff --git a/src/libs/services/scanner/impl/steps/ScanStepBase.cpp b/src/libs/services/scanner/impl/steps/ScanStepBase.cpp index fa09f4ac..a82d7fb7 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepBase.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepBase.cpp @@ -19,6 +19,11 @@ #include "ScanStepBase.hpp" +#include "core/String.hpp" + +#include "ScanContext.hpp" +#include "scanners/IFileScanner.hpp" + namespace lms::scanner { ScanStepBase::ScanStepBase(InitParams& initParams) @@ -29,7 +34,58 @@ namespace lms::scanner , _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners)) , _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; + + 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& visitor) const + { + for (IFileScanner* scanner : _fileScanners) + visitor(scanner); + } + + void ScanStepBase::addError(ScanContext& context, std::shared_ptr error) + { + error->accept(_scanErrorLogger); + + context.stats.errorsCount++; + + if (context.stats.errors.size() < ScanStats::maxStoredErrorCount) + context.stats.errors.emplace_back(error); + } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepBase.hpp b/src/libs/services/scanner/impl/steps/ScanStepBase.hpp index 0b209415..a467ddab 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepBase.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepBase.hpp @@ -21,9 +21,11 @@ #include #include +#include #include #include "IScanStep.hpp" +#include "ScanErrorLogger.hpp" namespace lms::db { @@ -35,6 +37,7 @@ namespace lms::scanner class IFileScanner; struct ScannerSettings; struct ScanStepStats; + struct ScanContext; class ScanStepBase : public IScanStep { @@ -57,14 +60,29 @@ namespace lms::scanner protected: const ScannerSettings* getLastScanSettings() const { return _lastScanSettings; } + IFileScanner* selectFileScanner(const std::filesystem::path& filePath) const; + void visitFileScanners(const std::function& visitor) const; + + void addError(ScanContext& context, std::shared_ptr error); + + template + void addError(ScanContext& context, CtrArgs&&... args) + { + auto error{ std::make_shared(std::forward(args)...) }; + addError(context, error); + } const ScannerSettings& _settings; ProgressCallback _progressCallback; bool& _abortScan; db::Db& _db; - std::vector _fileScanners; private: + std::unordered_map _scannerByFile; + std::unordered_map _scannerByExtension; + std::vector _fileScanners; + const ScannerSettings* _lastScanSettings{}; + ScanErrorLogger _scanErrorLogger; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp index 0fc8deaa..cea51cc6 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp @@ -24,6 +24,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { bool ScanStepCheckForDuplicatedFiles::needProcess([[maybe_unused]] const ScanContext& context) const diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp index 6f04948b..35a1eeb1 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp @@ -30,8 +30,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackLyrics.hpp" -#include "scanners/IFileScanner.hpp" +#include "ScanContext.hpp" #include "ScannerSettings.hpp" namespace lms::scanner @@ -62,22 +62,15 @@ namespace lms::scanner } LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked..."); - std::vector supportedFileExtensions; - for (IFileScanner* scanner : _fileScanners) - { - for (const std::filesystem::path& extension : scanner->getSupportedExtensions()) - supportedFileExtensions.emplace_back(extension); - } - - checkForRemovedFiles(context, supportedFileExtensions); - checkForRemovedFiles(context, supportedFileExtensions); - checkForRemovedFiles(context, supportedFileExtensions); - checkForRemovedFiles(context, supportedFileExtensions); - checkForRemovedFiles(context, supportedFileExtensions); + checkForRemovedFiles(context); + checkForRemovedFiles(context); + checkForRemovedFiles(context); + checkForRemovedFiles(context); + checkForRemovedFiles(context); } template - void ScanStepCheckForRemovedFiles::checkForRemovedFiles(ScanContext& context, std::span supportedFileExtensions) + void ScanStepCheckForRemovedFiles::checkForRemovedFiles(ScanContext& context) { using namespace db; @@ -110,7 +103,7 @@ namespace lms::scanner return; } - if (!checkFile(object->getAbsoluteFilePath(), supportedFileExtensions)) + if (!checkFile(object->getAbsoluteFilePath())) objectsToRemove.push_back(object); context.currentStepStats.processedElems++; @@ -132,7 +125,7 @@ namespace lms::scanner } } - bool ScanStepCheckForRemovedFiles::checkFile(const std::filesystem::path& p, std::span allowedExtensions) + bool ScanStepCheckForRemovedFiles::checkFile(const std::filesystem::path& p) { try { @@ -140,7 +133,7 @@ namespace lms::scanner // and still belongs to a media directory 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; } @@ -149,13 +142,13 @@ namespace lms::scanner 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; } - 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; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.hpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.hpp index d33bec8c..90d4ceae 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.hpp @@ -20,7 +20,6 @@ #pragma once #include -#include #include "ScanStepBase.hpp" @@ -38,8 +37,8 @@ namespace lms::scanner void process(ScanContext& context) override; template - void checkForRemovedFiles(ScanContext& context, std::span supportedFileExtensions); + void checkForRemovedFiles(ScanContext& context); - bool checkFile(const std::filesystem::path& p, std::span allowedExtensions); + bool checkFile(const std::filesystem::path& p); }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepCompact.cpp b/src/libs/services/scanner/impl/steps/ScanStepCompact.cpp index bc740555..130d3408 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCompact.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCompact.cpp @@ -22,6 +22,8 @@ #include "database/Db.hpp" #include "database/Session.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { bool ScanStepCompact::needProcess(const ScanContext& context) const diff --git a/src/libs/services/scanner/impl/steps/ScanStepComputeClusterStats.cpp b/src/libs/services/scanner/impl/steps/ScanStepComputeClusterStats.cpp index e4344d95..e2fd438a 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepComputeClusterStats.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepComputeClusterStats.cpp @@ -23,6 +23,8 @@ #include "database/Db.hpp" #include "database/Session.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const diff --git a/src/libs/services/scanner/impl/steps/ScanStepDiscoverFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepDiscoverFiles.cpp index 8babbd5b..61056433 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepDiscoverFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepDiscoverFiles.cpp @@ -24,7 +24,8 @@ #include "MediaLibraryInfo.hpp" #include "ScannerSettings.hpp" -#include "scanners/IFileScanner.hpp" + +#include "ScanContext.hpp" namespace lms::scanner { @@ -38,13 +39,6 @@ namespace lms::scanner { context.stats.totalFileCount = 0; - std::vector supportedFileExtensions; - for (IFileScanner* scanner : _fileScanners) - { - for (const std::filesystem::path& extension : scanner->getSupportedExtensions()) - supportedFileExtensions.emplace_back(extension); - } - for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries) { std::size_t currentDirectoryProcessElemsCount{}; @@ -53,7 +47,8 @@ namespace lms::scanner if (_abortScan) 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++; currentDirectoryProcessElemsCount++; @@ -64,7 +59,7 @@ namespace lms::scanner }, &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; diff --git a/src/libs/services/scanner/impl/steps/ScanStepOptimize.cpp b/src/libs/services/scanner/impl/steps/ScanStepOptimize.cpp index 97550d22..576f79d9 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepOptimize.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepOptimize.cpp @@ -23,6 +23,8 @@ #include "database/Db.hpp" #include "database/Session.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { bool ScanStepOptimize::needProcess(const ScanContext& context) const diff --git a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp index 4a5e78ea..360331bd 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp @@ -29,6 +29,8 @@ #include "database/Track.hpp" #include "database/TrackEmbeddedImage.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { bool ScanStepRemoveOrphanedDbEntries::needProcess([[maybe_unused]] const ScanContext& context) const diff --git a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp index 7434c8b3..1ed23520 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.cpp @@ -27,9 +27,12 @@ #include "core/Path.hpp" #include "database/Db.hpp" #include "database/Session.hpp" +#include "scanners/FileToScan.hpp" #include "scanners/IFileScanOperation.hpp" #include "scanners/IFileScanner.hpp" +#include "ScanContext.hpp" + namespace lms::scanner { using namespace db; @@ -45,21 +48,38 @@ namespace lms::scanner 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 ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) : ScanStepBase{ initParams } , _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()) - { - [[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, scanner->getName() << ": supporting file extension " << extension); + }); LMS_LOG(DBUPDATER, INFO, "Using " << _fileScanQueue.getThreadCount() << " thread(s) for scanning file metadata"); } @@ -94,26 +114,28 @@ namespace lms::scanner if (ec) { - LMS_LOG(DBUPDATER, ERROR, "Cannot scan file " << path << ": " << ec.message()); - context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() }); + addError(context, path, ec); + context.stats.skips++; } - else + else if (IFileScanner * scanner{ selectFileScanner(path) }) { - auto itScanner{ _scannerByExtension.find(core::stringUtils::stringToLower(path.extension().c_str())) }; - if (itScanner != std::cend(_scannerByExtension)) + FileToScan fileToScan{ retrieveFileInfo(path, mediaLibrary, ec) }; + if (ec) { - IFileScanner& scanner{ *itScanner->second }; - - FileToScan fileToScan{ .file = path, .mediaLibrary = mediaLibrary }; - if (scanner.needsScan(context, fileToScan)) + addError(context, path, ec); + context.stats.skips++; + } + 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)); } - - context.currentStepStats.processedElems++; - _progressCallback(context.currentStepStats); } + + context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); } while (_fileScanQueue.getResultsCount() > (scanQueueMaxScanRequestCount / 2)) @@ -146,9 +168,27 @@ namespace lms::scanner if (_abortScan) return; - LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for " << scanOperation->getFile()); - scanOperation->processResult(context); + LMS_LOG(DBUPDATER, DEBUG, scanOperation->getName() << ": processing result for " << scanOperation->getFilePath()); + 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++; + + for (const auto& error : scanOperation->getErrors()) + addError(context, error); } } } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp index 8dc9bd70..90e9ce87 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepScanFiles.hpp @@ -40,10 +40,10 @@ namespace lms::scanner core::LiteralString getStepName() const override { return "Scan files"; } bool needProcess(const ScanContext& context) const override; void process(ScanContext& context) override; + void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary); void processFileScanResults(ScanContext& context, std::span> scanOperations); FileScanQueue _fileScanQueue; - std::unordered_map _scannerByExtension; }; } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp index 35dcda97..1e9b52fd 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp @@ -25,6 +25,7 @@ #include "database/Session.hpp" #include "MediaLibraryInfo.hpp" +#include "ScanContext.hpp" #include "ScannerSettings.hpp" namespace lms::scanner diff --git a/src/libs/services/scanner/include/services/scanner/ScanErrors.hpp b/src/libs/services/scanner/include/services/scanner/ScanErrors.hpp new file mode 100644 index 00000000..b57c44f6 --- /dev/null +++ b/src/libs/services/scanner/include/services/scanner/ScanErrors.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +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 diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 614d2a96..13951d34 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -21,40 +21,21 @@ #include -#include +#include #include #include "database/TrackId.hpp" +#include "ScanErrors.hpp" + namespace lms::scanner { - enum class ScanErrorType - { - CannotReadFile, - CannotReadArtistInfoFile, - CannotReadAudioFile, - CannotReadImageFile, - CannotReadLyricsFile, - CannotReadPlayListFile, - NoAudioTrack, - BadDuration, - }; - enum class DuplicateReason { SameHash, 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 { db::TrackId trackId; @@ -105,15 +86,18 @@ namespace lms::scanner std::size_t totalFileCount{}; // Total number of files (estimated) 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 deletions{}; // removed from DB std::size_t updates{}; // updated file in DB + std::size_t failures{}; // scan failure std::size_t featuresFetched{}; // features fetched in DB - std::vector errors; + static constexpr std::size_t maxStoredErrorCount{ 5'000 }; // TODO make this configurable + std::vector> errors; + std::size_t errorsCount{}; // maybe bigger than errors.size() if too many errors std::vector duplicates; std::size_t nbFiles() const; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 19f1790b..b5027141 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(lms ui/admin/MediaLibraryModal.cpp ui/admin/TracingView.cpp ui/admin/ScannerController.cpp + ui/admin/ScannerReportResource.cpp ui/admin/ScanSettingsView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index b5932496..01ecca92 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -542,7 +542,7 @@ namespace lms::ui .arg(static_cast(stats.updates)) .arg(static_cast(stats.deletions)) .arg(static_cast(stats.duplicates.size())) - .arg(static_cast(stats.errors.size()))); + .arg(static_cast(stats.errorsCount))); }); } diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index dff32263..5cefb88a 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -19,21 +19,19 @@ #include "ScannerController.hpp" -#include -#include #include #include #include #include #include +#include #include "core/Service.hpp" #include "core/String.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" #include "services/scanner/IScannerService.hpp" #include "LmsApplication.hpp" +#include "ScannerReportResource.hpp" namespace lms::ui { @@ -45,110 +43,6 @@ namespace lms::ui } } // 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(); - - *_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 _stats; - }; - ScannerController::ScannerController() : WTemplate{ Wt::WString::tr("Lms.Admin.ScannerController.template") } { @@ -225,7 +119,7 @@ namespace lms::ui .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) .arg(status.lastCompleteScanStats->stopTime.date().toString(Wt::WLocale::currentLocale().dateFormat())) .arg(status.lastCompleteScanStats->stopTime.time().toString(Wt::WLocale::currentLocale().timeFormat())) - .arg(status.lastCompleteScanStats->errors.size()) + .arg(status.lastCompleteScanStats->errorsCount) .arg(status.lastCompleteScanStats->duplicates.size())); _reportResource->setScanStats(*status.lastCompleteScanStats); diff --git a/src/lms/ui/admin/ScannerReportResource.cpp b/src/lms/ui/admin/ScannerReportResource.cpp new file mode 100644 index 00000000..e430757f --- /dev/null +++ b/src/lms/ui/admin/ScannerReportResource.cpp @@ -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 . + */ + +#include "ScannerReportResource.hpp" + +#include +#include + +#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(); + + *_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 diff --git a/src/lms/ui/admin/ScannerReportResource.hpp b/src/lms/ui/admin/ScannerReportResource.hpp new file mode 100644 index 00000000..7fa17458 --- /dev/null +++ b/src/lms/ui/admin/ScannerReportResource.hpp @@ -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 . + */ +#pragma once + +#include + +#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 _stats; + }; + +} // namespace lms::ui diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 44904722..f6d15ca4 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -462,7 +462,7 @@ int main(int argc, char* argv[]) { std::filesystem::path file{ inputFile }; - std::cout << "Parsing file '" << file << "'" << std::endl; + std::cout << "Parsing file " << file << std::endl; if (parsers.contains(Parser::Lyrics)) { From 2b9ff2b2e9fda9495bb81cc85bac9eccc423aacd Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 29 May 2025 15:53:42 +0200 Subject: [PATCH 03/51] Added some indexes --- src/libs/database/impl/Session.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 3a7c1432..0511fb97 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -210,6 +210,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_id_idx ON cluster(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); @@ -225,6 +226,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)"); @@ -291,6 +293,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_desc_idx ON track(release_id, file_added DESC)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_id_idx ON tracklist(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_last_modified_date_time_idx ON tracklist(last_modified_date_time)"); @@ -298,6 +301,7 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_idx ON tracklist_entry(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)"); + utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_id_idx ON track_artist_link(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_mbid_matched_artist_idx ON track_artist_link(artist_mbid_matched, artist_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)"); From 291e7ae60c1ea0f16de5f3d7ab49db48d8ef6ab3 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 29 May 2025 21:48:07 +0200 Subject: [PATCH 04/51] Subsonic API: changed return code from 41 to 42 when a token is used --- src/libs/subsonic/impl/SubsonicResource.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 5fc5ad54..8d5e95fe 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -434,7 +434,7 @@ namespace lms::api::subsonic const auto& parameters{ request.getParameterMap() }; if (hasParameter(parameters, "t")) - throw TokenAuthenticationNotSupportedForLDAPUsersError{}; + throw ProvidedAuthenticationMechanismNotSupportedError{}; const auto user{ getParameterAs(parameters, "u") }; const auto password{ getParameterAs(parameters, "p") }; From 5a70ea68ce49849de02169392d7e310a6076a90e Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 29 May 2025 22:54:16 +0200 Subject: [PATCH 05/51] fixed build with wt 4.11.4 on debian, ref #681 --- src/libs/services/scanner/impl/ScannerService.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/services/scanner/impl/ScannerService.hpp b/src/libs/services/scanner/impl/ScannerService.hpp index 4f26ce8f..fd615dc5 100644 --- a/src/libs/services/scanner/impl/ScannerService.hpp +++ b/src/libs/services/scanner/impl/ScannerService.hpp @@ -24,10 +24,11 @@ #include #include +#include + #include #include #include -#include #include "ScannerSettings.hpp" #include "database/Db.hpp" From 8cf2e2b660967025de5c0cd3c6e0f1e165780eea Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 30 May 2025 18:38:53 +0200 Subject: [PATCH 06/51] Removed unused struct --- src/libs/av/include/av/IAudioFile.hpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp index 4a57b0c2..234f8df4 100644 --- a/src/libs/av/include/av/IAudioFile.hpp +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -102,12 +102,6 @@ namespace lms::av std::unique_ptr parseAudioFile(const std::filesystem::path& p); - struct AudioFileFormat - { - std::string mimeType; - std::string format; - }; - std::string_view getMimeType(const std::filesystem::path& fileExtension); } // namespace lms::av From dc52ea4ea5115f361c9366ff445c0af193d52c29 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 30 May 2025 23:27:01 +0200 Subject: [PATCH 07/51] Introduced new transcoding service --- src/libs/av/CMakeLists.txt | 2 - src/libs/av/impl/AudioFile.cpp | 40 +-------- .../av/impl/RawResourceHandlerCreator.cpp | 32 ------- src/libs/av/impl/Transcoder.cpp | 87 ++++++++++--------- src/libs/av/impl/Transcoder.hpp | 38 +++----- .../include/av/{Types.hpp => Exception.hpp} | 0 src/libs/av/include/av/IAudioFile.hpp | 3 - src/libs/av/include/av/ITranscoder.hpp | 71 +++++++++++++++ .../av/TranscodingResourceHandlerCreator.hpp | 32 ------- src/libs/core/CMakeLists.txt | 1 + src/libs/core/impl/FileResourceHandler.cpp | 7 +- src/libs/core/impl/FileResourceHandler.hpp | 4 +- src/libs/core/impl/MimeTypes.cpp | 79 +++++++++++++++++ .../core/FileResourceHandlerCreator.hpp | 4 +- .../core/include/core/IResourceHandler.hpp | 5 +- .../include/core/MimeTypes.hpp} | 10 +-- .../impl/avformat/AvFormatImageReader.cpp | 2 +- .../impl/avformat/AvFormatTagReader.cpp | 2 +- src/libs/services/CMakeLists.txt | 1 + src/libs/services/transcoding/CMakeLists.txt | 17 ++++ .../impl/TranscodingResourceHandler.cpp | 54 +++++++++--- .../impl/TranscodingResourceHandler.hpp | 20 +++-- .../transcoding/impl/TranscodingService.cpp | 48 ++++++++++ .../transcoding/impl/TranscodingService.hpp | 40 +++++++++ .../transcoding/ITranscodingService.hpp} | 34 +++++--- src/libs/subsonic/CMakeLists.txt | 3 +- .../impl/endpoints/MediaRetrieval.cpp | 73 ++++++++-------- src/libs/subsonic/impl/responses/Song.cpp | 5 +- src/lms/CMakeLists.txt | 1 + src/lms/main.cpp | 2 + src/lms/ui/resource/AudioFileResource.cpp | 8 +- .../ui/resource/AudioTranscodingResource.cpp | 63 ++++++-------- 32 files changed, 487 insertions(+), 301 deletions(-) delete mode 100644 src/libs/av/impl/RawResourceHandlerCreator.cpp rename src/libs/av/include/av/{Types.hpp => Exception.hpp} (100%) create mode 100644 src/libs/av/include/av/ITranscoder.hpp delete mode 100644 src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp create mode 100644 src/libs/core/impl/MimeTypes.cpp rename src/libs/{av/include/av/RawResourceHandlerCreator.hpp => core/include/core/MimeTypes.hpp} (77%) create mode 100644 src/libs/services/transcoding/CMakeLists.txt rename src/libs/{av => services/transcoding}/impl/TranscodingResourceHandler.cpp (66%) rename src/libs/{av => services/transcoding}/impl/TranscodingResourceHandler.hpp (81%) create mode 100644 src/libs/services/transcoding/impl/TranscodingService.cpp create mode 100644 src/libs/services/transcoding/impl/TranscodingService.hpp rename src/libs/{av/include/av/TranscodingParameters.hpp => services/transcoding/include/services/transcoding/ITranscodingService.hpp} (50%) diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index 5c63d843..0833bad7 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -2,9 +2,7 @@ pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat) add_library(lmsav STATIC impl/AudioFile.cpp - impl/RawResourceHandlerCreator.cpp impl/Transcoder.cpp - impl/TranscodingResourceHandler.cpp ) target_include_directories(lmsav INTERFACE diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 5432bfc6..614c6cde 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -34,7 +34,7 @@ extern "C" #include "core/ILogger.hpp" #include "core/String.hpp" -#include "av/Types.hpp" +#include "av/Exception.hpp" namespace lms::av { @@ -321,42 +321,4 @@ namespace lms::av return res; } - - std::string_view getMimeType(const std::filesystem::path& fileExtension) - { - // List should be sync with the demuxers shipped in the lms's docker version - // + the _audioFileExtensions in ScanSettings - // std::filesystem::path does not seem to have std::hash specialization on freebsd - static const std::unordered_map entries{ - { ".mp3", "audio/mpeg" }, - { ".ogg", "audio/ogg" }, - { ".oga", "audio/ogg" }, - { ".opus", "audio/opus" }, - { ".aac", "audio/aac" }, - { ".alac", "audio/mp4" }, - { ".m4a", "audio/mp4" }, - { ".m4b", "audio/mp4" }, - { ".flac", "audio/flac" }, - { ".webm", "audio/webm" }, - { ".wav", "audio/x-wav" }, - { ".wma", "audio/x-ms-wma" }, - { ".ape", "audio/x-monkeys-audio" }, - { ".mpc", "audio/x-musepack" }, - { ".shn", "audio/x-shn" }, - { ".aif", "audio/x-aiff" }, - { ".aiff", "audio/x-aiff" }, - { ".m3u", "audio/x-mpegurl" }, - { ".pls", "audio/x-scpls" }, - { ".dsf", "audio/x-dsd" }, - { ".wv", "audio/x-wavpack" }, - { ".wvp", "audio/x-wavpack" }, - { ".mka", "audio/x-matroska" }, - }; - - auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.c_str())) }; - if (it == std::cend(entries)) - return ""; - - return it->second; - } } // namespace lms::av diff --git a/src/libs/av/impl/RawResourceHandlerCreator.cpp b/src/libs/av/impl/RawResourceHandlerCreator.cpp deleted file mode 100644 index 42c200b8..00000000 --- a/src/libs/av/impl/RawResourceHandlerCreator.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2023 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "av/RawResourceHandlerCreator.hpp" - -#include "av/IAudioFile.hpp" -#include "core/FileResourceHandlerCreator.hpp" - -namespace lms::av -{ - std::unique_ptr createRawResourceHandler(const std::filesystem::path& path) - { - std::string_view mimeType{ getMimeType(path.extension()) }; - return createFileResourceHandler(path, mimeType.empty() ? "application/octet-stream" : mimeType); - } -} // namespace lms::av diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index efc0a304..8dc4d59d 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -27,35 +27,20 @@ #include "core/ILogger.hpp" #include "core/Service.hpp" -#include "av/Types.hpp" +#include "av/Exception.hpp" -namespace lms::av::transcoding +namespace lms::av { - #define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message) + std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters) + { + return std::make_unique(inputParameters, outputParameters); + } + static std::atomic globalId{}; static std::filesystem::path ffmpegPath; - std::string_view formatToMimetype(OutputFormat format) - { - switch (format) - { - case OutputFormat::MP3: - return "audio/mpeg"; - case OutputFormat::OGG_OPUS: - return "audio/opus"; - case OutputFormat::MATROSKA_OPUS: - return "audio/x-matroska"; - case OutputFormat::OGG_VORBIS: - return "audio/ogg"; - case OutputFormat::WEBM_VORBIS: - return "audio/webm"; - } - - throw Exception{ "Invalid encoding" }; - } - void Transcoder::init() { ffmpegPath = core::Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); @@ -63,10 +48,10 @@ namespace lms::av::transcoding throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; } - Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters) + Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams) : _debugId{ globalId++ } - , _inputParameters{ inputParameters } - , _outputParameters{ outputParameters } + , _inputParams{ inputParams } + , _outputParams{ outputParams } { start(); } @@ -80,17 +65,18 @@ namespace lms::av::transcoding try { - if (!std::filesystem::exists(_inputParameters.trackPath)) - throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" }; - if (!std::filesystem::is_regular_file(_inputParameters.trackPath)) - throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" }; + if (!std::filesystem::exists(_inputParams.file)) + throw Exception{ "File " + _inputParams.file.string() + " does not exist!" }; + if (!std::filesystem::is_regular_file(_inputParams.file)) + throw Exception{ "File " + _inputParams.file.string() + " is not regular!" }; } catch (const std::filesystem::filesystem_error& e) { - throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() }; + // TODO store/raise e.code() + throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() }; } - LOG(INFO, "Transcoding file " << _inputParameters.trackPath); + LOG(INFO, "Transcoding file " << _inputParams.file); std::vector args; @@ -109,22 +95,22 @@ namespace lms::av::transcoding args.emplace_back("-ss"); std::ostringstream oss; - oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1'000 }); + oss << std::fixed << std::showpoint << std::setprecision(3) << (_inputParams.offset.count() / float{ 1'000 }); args.emplace_back(oss.str()); } // Input file args.emplace_back("-i"); - args.emplace_back(_inputParameters.trackPath.string()); + args.emplace_back(_inputParams.file.string()); // Stream mapping, if set - if (_outputParameters.stream) + if (_inputParams.streamIndex) { args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_outputParameters.stream)); + args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex)); } - if (_outputParameters.stripMetadata) + if (_outputParams.stripMetadata) { // Strip metadata args.emplace_back("-map_metadata"); @@ -136,10 +122,10 @@ namespace lms::av::transcoding // Output bitrates args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_outputParameters.bitrate)); + args.emplace_back(std::to_string(_outputParams.bitrate)); // Codecs and formats - switch (_outputParameters.format) + switch (_outputParams.format) { case OutputFormat::MP3: args.emplace_back("-f"); @@ -175,11 +161,9 @@ namespace lms::av::transcoding break; default: - throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParameters.format)) + ")" }; + throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParams.format)) + ")" }; } - _outputMimeType = formatToMimetype(_outputParameters.format); - args.emplace_back("pipe:1"); LOG(DEBUG, "Dumping args (" << args.size() << ")"); @@ -213,6 +197,25 @@ namespace lms::av::transcoding return _childProcess->readSome(buffer, bufferSize); } + std::string_view Transcoder::getOutputMimeType() const + { + switch (_outputParams.format) + { + case OutputFormat::MP3: + return "audio/mpeg"; + case OutputFormat::OGG_OPUS: + return "audio/opus"; + case OutputFormat::MATROSKA_OPUS: + return "audio/x-matroska"; + case OutputFormat::OGG_VORBIS: + return "audio/ogg"; + case OutputFormat::WEBM_VORBIS: + return "audio/webm"; + } + + return "application/octet-stream"; // default, should not happen + } + bool Transcoder::finished() const { assert(_childProcess); @@ -220,4 +223,4 @@ namespace lms::av::transcoding return _childProcess->finished(); } -} // namespace lms::av::transcoding +} // namespace lms::av diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp index 3b226dff..685d18b0 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/av/impl/Transcoder.hpp @@ -19,47 +19,37 @@ #pragma once -#include - -#include "av/TranscodingParameters.hpp" +#include "av/ITranscoder.hpp" namespace lms::core { class IChildProcess; } -namespace lms::av::transcoding +namespace lms::av { - class Transcoder + class Transcoder : public ITranscoder { public: Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); - ~Transcoder(); + ~Transcoder() override; Transcoder(const Transcoder&) = delete; Transcoder& operator=(const Transcoder&) = delete; - Transcoder(Transcoder&&) = delete; - Transcoder& operator=(Transcoder&&) = delete; - - // non blocking calls - using ReadCallback = std::function; - void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); - std::size_t readSome(std::byte* buffer, std::size_t bufferSize); - - const std::string& getOutputMimeType() const { return _outputMimeType; } - const OutputParameters& getOutputParameters() const { return _outputParameters; } - - bool finished() const; private: - static void init(); + void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback) override; + std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override; + std::string_view getOutputMimeType() const override; + const OutputParameters& getOutputParameters() const override { return _outputParams; } + + bool finished() const override; + static void init(); void start(); const std::size_t _debugId{}; - const InputParameters _inputParameters; - const OutputParameters _outputParameters; - std::string _outputMimeType; - + const InputParameters _inputParams; + const OutputParameters _outputParams; std::unique_ptr _childProcess; }; -} // namespace lms::av::transcoding \ No newline at end of file +} // namespace lms::av \ No newline at end of file diff --git a/src/libs/av/include/av/Types.hpp b/src/libs/av/include/av/Exception.hpp similarity index 100% rename from src/libs/av/include/av/Types.hpp rename to src/libs/av/include/av/Exception.hpp diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp index 234f8df4..63087058 100644 --- a/src/libs/av/include/av/IAudioFile.hpp +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -101,7 +101,4 @@ namespace lms::av }; std::unique_ptr parseAudioFile(const std::filesystem::path& p); - - std::string_view getMimeType(const std::filesystem::path& fileExtension); - } // namespace lms::av diff --git a/src/libs/av/include/av/ITranscoder.hpp b/src/libs/av/include/av/ITranscoder.hpp new file mode 100644 index 00000000..165858b0 --- /dev/null +++ b/src/libs/av/include/av/ITranscoder.hpp @@ -0,0 +1,71 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace lms::av +{ + struct InputParameters + { + std::filesystem::path file; // Path to the input file + std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from + std::optional streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set) + }; + + enum class OutputFormat + { + MP3, + OGG_OPUS, + MATROSKA_OPUS, + OGG_VORBIS, + WEBM_VORBIS, + }; + + struct OutputParameters + { + OutputFormat format; + std::size_t bitrate{ 128'000 }; + bool stripMetadata{ true }; + }; + + class ITranscoder + { + public: + virtual ~ITranscoder() = default; + + // non blocking calls + using ReadCallback = std::function; + virtual void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback callback) = 0; + virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0; + + virtual std::string_view getOutputMimeType() const = 0; + virtual const OutputParameters& getOutputParameters() const = 0; + + virtual bool finished() const = 0; + }; + + std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); +} // namespace lms::av \ No newline at end of file diff --git a/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp deleted file mode 100644 index 553ab5ea..00000000 --- a/src/libs/av/include/av/TranscodingResourceHandlerCreator.hpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include - -#include "core/IResourceHandler.hpp" - -namespace lms::av::transcoding -{ - struct InputParameters; - struct OutputParameters; - - std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength); -} // namespace lms::av::transcoding diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index 81f4edcc..8ce177b5 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(lmscore STATIC impl/FileResourceHandler.cpp impl/IOContextRunner.cpp impl/Logger.cpp + impl/MimeTypes.cpp impl/NetAddress.cpp impl/PartialDateTime.cpp impl/Path.cpp diff --git a/src/libs/core/impl/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp index 130671dc..7db88efc 100644 --- a/src/libs/core/impl/FileResourceHandler.cpp +++ b/src/libs/core/impl/FileResourceHandler.cpp @@ -22,12 +22,13 @@ #include #include "core/ILogger.hpp" +#include "core/MimeTypes.hpp" -namespace lms +namespace lms::core { std::unique_ptr createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) { - return std::make_unique(path, mimeType); + return std::make_unique(path, mimeType.empty() ? getMimeType(path.extension()) : mimeType); } FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) @@ -134,4 +135,4 @@ namespace lms LMS_LOG(UTILS, DEBUG, "Job complete!"); return nullptr; } -} // namespace lms \ No newline at end of file +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 6b79d4e5..7e541038 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -25,7 +25,7 @@ #include "core/IResourceHandler.hpp" -namespace lms +namespace lms::core { class FileResourceHandler final : public IResourceHandler { @@ -43,4 +43,4 @@ namespace lms ::uint64_t _beyondLastByte{}; ::uint64_t _offset{}; }; -} // namespace lms \ No newline at end of file +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/core/impl/MimeTypes.cpp b/src/libs/core/impl/MimeTypes.cpp new file mode 100644 index 00000000..337800ac --- /dev/null +++ b/src/libs/core/impl/MimeTypes.cpp @@ -0,0 +1,79 @@ +/* + * 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 . + */ + +#include "core/MimeTypes.hpp" + +#include + +#include "core/String.hpp" + +namespace lms::core +{ + std::string_view getMimeType(const std::filesystem::path& fileExtension) + { + static const std::unordered_map entries{ + // audio + { ".aac", "audio/aac" }, + { ".ac3", "audio/ac3" }, + { ".aif", "audio/x-aiff" }, + { ".aiff", "audio/x-aiff" }, + { ".alac", "audio/mp4" }, + { ".ape", "audio/x-monkeys-audio" }, + { ".dff", "audio/x-dsd-dff" }, + { ".dsdiff", "audio/x-dsd-diff" }, + { ".dsf", "audio/x-dsd" }, + { ".dsf", "audio/x-dsd-dsf" }, + { ".dts", "audio/vnd.dts" }, + { ".dtshd", "audio/vnd.dts.hd" }, + { ".eac3", "audio/eac3" }, + { ".flac", "audio/flac" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4b", "audio/mp4" }, + { ".mka", "audio/x-matroska" }, + { ".mka", "audio/x-matroska" }, + { ".mp3", "audio/mpeg" }, + { ".mpc", "audio/x-musepack" }, + { ".oga", "audio/ogg" }, + { ".ogg", "audio/ogg" }, + { ".opus", "audio/opus" }, + { ".pls", "audio/x-scpls" }, + { ".shn", "audio/x-shn" }, + { ".wav", "audio/x-wav" }, + { ".webm", "audio/webm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wv", "audio/x-wavpack" }, + { ".wvp", "audio/x-wavpack" }, + + // image + { ".bmp", "image/bmp" }, + { ".gif", "image/gif" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".png", "image/png" }, + { ".webp", "image/webp" }, + }; + + auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.c_str())) }; + if (it == std::cend(entries)) + return "application/octet-stream"; + + return it->second; + } +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/core/include/core/FileResourceHandlerCreator.hpp b/src/libs/core/include/core/FileResourceHandlerCreator.hpp index 2ea0cb3d..6e9c755c 100644 --- a/src/libs/core/include/core/FileResourceHandlerCreator.hpp +++ b/src/libs/core/include/core/FileResourceHandlerCreator.hpp @@ -25,7 +25,7 @@ #include "core/IResourceHandler.hpp" -namespace lms +namespace lms::core { - std::unique_ptr createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType); + std::unique_ptr createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType = ""); } \ No newline at end of file diff --git a/src/libs/core/include/core/IResourceHandler.hpp b/src/libs/core/include/core/IResourceHandler.hpp index 49433b65..cdf04969 100644 --- a/src/libs/core/include/core/IResourceHandler.hpp +++ b/src/libs/core/include/core/IResourceHandler.hpp @@ -22,8 +22,7 @@ #include #include -// TODO, move elsewhere -namespace lms +namespace lms::core { // Helper class to serve a resource (must be saved as continuation data if not complete) class IResourceHandler @@ -34,4 +33,4 @@ namespace lms [[nodiscard]] virtual Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0; virtual void abort() = 0; }; -} // namespace lms \ No newline at end of file +} // namespace lms::core \ No newline at end of file diff --git a/src/libs/av/include/av/RawResourceHandlerCreator.hpp b/src/libs/core/include/core/MimeTypes.hpp similarity index 77% rename from src/libs/av/include/av/RawResourceHandlerCreator.hpp rename to src/libs/core/include/core/MimeTypes.hpp index e2282de2..fde19596 100644 --- a/src/libs/av/include/av/RawResourceHandlerCreator.hpp +++ b/src/libs/core/include/core/MimeTypes.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * @@ -20,11 +20,9 @@ #pragma once #include -#include +#include -#include "core/IResourceHandler.hpp" - -namespace lms::av +namespace lms::core { - std::unique_ptr createRawResourceHandler(const std::filesystem::path& path); + std::string_view getMimeType(const std::filesystem::path& fileExtension); } \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp b/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp index 7ec95d1c..8a420534 100644 --- a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp +++ b/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp @@ -19,8 +19,8 @@ #include "AvFormatImageReader.hpp" +#include "av/Exception.hpp" #include "av/IAudioFile.hpp" -#include "av/Types.hpp" #include "metadata/Exception.hpp" namespace lms::metadata::avformat diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp b/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp index 04f50f62..1ac145a0 100644 --- a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp @@ -19,8 +19,8 @@ #include "AvFormatTagReader.hpp" +#include "av/Exception.hpp" #include "av/IAudioFile.hpp" -#include "av/Types.hpp" #include "core/ILogger.hpp" #include "core/String.hpp" #include "metadata/Exception.hpp" diff --git a/src/libs/services/CMakeLists.txt b/src/libs/services/CMakeLists.txt index 9cb1f7ce..02b6c6aa 100644 --- a/src/libs/services/CMakeLists.txt +++ b/src/libs/services/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(feedback) add_subdirectory(recommendation) add_subdirectory(scanner) add_subdirectory(scrobbling) +add_subdirectory(transcoding) diff --git a/src/libs/services/transcoding/CMakeLists.txt b/src/libs/services/transcoding/CMakeLists.txt new file mode 100644 index 00000000..2c0f5843 --- /dev/null +++ b/src/libs/services/transcoding/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library(lmstranscoding STATIC + impl/TranscodingResourceHandler.cpp + impl/TranscodingService.cpp + ) + +target_include_directories(lmstranscoding INTERFACE + include + ) + +target_include_directories(lmstranscoding PRIVATE + include + impl + ) + +target_link_libraries(lmstranscoding PRIVATE + lmsav + ) diff --git a/src/libs/av/impl/TranscodingResourceHandler.cpp b/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp similarity index 66% rename from src/libs/av/impl/TranscodingResourceHandler.cpp rename to src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp index 611c87c8..0fda5f9e 100644 --- a/src/libs/av/impl/TranscodingResourceHandler.cpp +++ b/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp @@ -18,9 +18,12 @@ */ #include "TranscodingResourceHandler.hpp" + +#include "av/Exception.hpp" +#include "av/ITranscoder.hpp" #include "core/ILogger.hpp" -namespace lms::av::transcoding +namespace lms::transcoding { namespace { @@ -29,9 +32,20 @@ namespace lms::av::transcoding const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputParameters.duration).count()) / 1000 }; return estimatedContentLength; } + + av::InputParameters toAv(const InputParameters& in) + { + return { .file = in.file, .offset = in.offset, .streamIndex = in.streamIndex }; + } + + av::OutputParameters toAv(const OutputParameters& out) + { + return { .format = static_cast(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata }; + } + } // namespace - std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) { return std::make_unique(inputParameters, outputParameters, estimateContentLength); } @@ -40,20 +54,36 @@ namespace lms::av::transcoding TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) : _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt } - , _transcoder{ inputParameters, outputParameters } { - if (_estimatedContentLength) - LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); - else - LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length"); + try + { + _transcoder = av::createTranscoder(toAv(inputParameters), toAv(outputParameters)); + + if (_estimatedContentLength) + LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); + else + LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length"); + } + catch (av::Exception& e) + { + LMS_LOG(TRANSCODING, ERROR, "Failed to create transcoder: " << e.what()); + } } + TranscodingResourceHandler::~TranscodingResourceHandler() = default; + Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { + if (!_transcoder) + { + response.setStatus(404); + return {}; + } + if (_estimatedContentLength) response.setContentLength(*_estimatedContentLength); - response.setMimeType(_transcoder.getOutputMimeType()); - LMS_LOG(TRANSCODING, DEBUG, "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType()); + response.setMimeType(std::string{ _transcoder->getOutputMimeType() }); + LMS_LOG(TRANSCODING, DEBUG, "Transcoder finished = " << _transcoder->finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder->getOutputMimeType()); if (_bytesReadyCount > 0) { @@ -64,11 +94,11 @@ namespace lms::av::transcoding _bytesReadyCount = 0; } - if (!_transcoder.finished()) + if (!_transcoder->finished()) { Wt::Http::ResponseContinuation* continuation{ response.createContinuation() }; continuation->waitForMoreData(); - _transcoder.asyncRead(_buffer.data(), _buffer.size(), [this, continuation](std::size_t nbBytesRead) { + _transcoder->asyncRead(_buffer.data(), _buffer.size(), [this, continuation](std::size_t nbBytesRead) { LMS_LOG(TRANSCODING, DEBUG, "Have " << nbBytesRead << " more bytes to send back"); assert(_bytesReadyCount == 0); @@ -96,4 +126,4 @@ namespace lms::av::transcoding return {}; } -} // namespace lms::av::transcoding +} // namespace lms::transcoding diff --git a/src/libs/av/impl/TranscodingResourceHandler.hpp b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp similarity index 81% rename from src/libs/av/impl/TranscodingResourceHandler.hpp rename to src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp index 6e5aa516..26465b1b 100644 --- a/src/libs/av/impl/TranscodingResourceHandler.hpp +++ b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp @@ -20,20 +20,24 @@ #pragma once #include +#include #include -#include "av/TranscodingParameters.hpp" #include "core/IResourceHandler.hpp" +#include "services/transcoding/ITranscodingService.hpp" -#include "Transcoder.hpp" - -namespace lms::av::transcoding +namespace lms::av { - class TranscodingResourceHandler final : public IResourceHandler + class ITranscoder; +} + +namespace lms::transcoding +{ + class TranscodingResourceHandler final : public core::IResourceHandler { public: TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength); - ~TranscodingResourceHandler() override = default; + ~TranscodingResourceHandler() override; TranscodingResourceHandler(const TranscodingResourceHandler&) = delete; TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete; @@ -47,6 +51,6 @@ namespace lms::av::transcoding std::array _buffer; std::size_t _bytesReadyCount{}; std::size_t _totalServedByteCount{}; - Transcoder _transcoder; + std::unique_ptr _transcoder; }; -} // namespace lms::av::transcoding +} // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodingService.cpp new file mode 100644 index 00000000..57a2f57e --- /dev/null +++ b/src/libs/services/transcoding/impl/TranscodingService.cpp @@ -0,0 +1,48 @@ +/* + * 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 . + */ + +#include "TranscodingService.hpp" + +#include "core/ILogger.hpp" + +#include "TranscodingResourceHandler.hpp" + +namespace lms::transcoding +{ + std::unique_ptr createTranscodingService(core::IChildProcessManager& childProcessManager) + { + return std::make_unique(childProcessManager); + } + + TranscodingService::TranscodingService(core::IChildProcessManager& childProcessManager) + : _childProcessManager(childProcessManager) + { + LMS_LOG(TRANSCODING, INFO, "Service started!"); + } + + TranscodingService::~TranscodingService() + { + LMS_LOG(TRANSCODING, INFO, "Service stopped!"); + } + + std::unique_ptr TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + { + return std::make_unique(inputParameters, outputParameters, estimateContentLength); + } +} // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.hpp b/src/libs/services/transcoding/impl/TranscodingService.hpp new file mode 100644 index 00000000..d3c55aee --- /dev/null +++ b/src/libs/services/transcoding/impl/TranscodingService.hpp @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/transcoding/ITranscodingService.hpp" + +namespace lms::transcoding +{ + class TranscodingService : public ITranscodingService + { + public: + explicit TranscodingService(core::IChildProcessManager& childProcessManager); + ~TranscodingService() override; + + TranscodingService(const TranscodingService&) = delete; + TranscodingService& operator=(const TranscodingService&) = delete; + + private: + std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override; + + core::IChildProcessManager& _childProcessManager; + }; +} // namespace lms::transcoding diff --git a/src/libs/av/include/av/TranscodingParameters.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp similarity index 50% rename from src/libs/av/include/av/TranscodingParameters.hpp rename to src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp index a2f9516b..3c383f14 100644 --- a/src/libs/av/include/av/TranscodingParameters.hpp +++ b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * @@ -19,16 +19,24 @@ #pragma once -#include #include +#include #include -namespace lms::av::transcoding +namespace lms::core +{ + class IChildProcessManager; + class IResourceHandler; +} // namespace lms::core + +namespace lms::transcoding { struct InputParameters { - std::filesystem::path trackPath; - std::chrono::milliseconds duration; // used to estimate content length + std::filesystem::path file; // Path to the input file + std::chrono::milliseconds duration; // Offset in the input file to start transcoding from + std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from + std::optional streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set) }; enum class OutputFormat @@ -40,14 +48,20 @@ namespace lms::av::transcoding WEBM_VORBIS, }; - std::string_view toMimetype(OutputFormat format); - struct OutputParameters { OutputFormat format; std::size_t bitrate{ 128'000 }; - std::optional stream; // Id of the stream to be transcoded (auto detect by default) - std::chrono::milliseconds offset{ 0 }; bool stripMetadata{ true }; }; -} // namespace lms::av::transcoding + + class ITranscodingService + { + public: + virtual ~ITranscodingService() = default; + + virtual std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0; + }; + + std::unique_ptr createTranscodingService(core::IChildProcessManager& childProcessManager); +} // namespace lms::transcoding diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 3149d14f..e1e9fe23 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -44,6 +44,7 @@ target_include_directories(lmssubsonic PRIVATE ) target_link_libraries(lmssubsonic PRIVATE + lmsartwork lmsauth lmsav lmsdatabase @@ -51,7 +52,7 @@ target_link_libraries(lmssubsonic PRIVATE lmsrecommendation lmsscanner lmsscrobbling - lmsartwork + lmstranscoding lmscore std::filesystem ) diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index b3e764ae..d756ea1e 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -19,11 +19,9 @@ #include "MediaRetrieval.hpp" +#include "av/Exception.hpp" #include "av/IAudioFile.hpp" -#include "av/RawResourceHandlerCreator.hpp" -#include "av/TranscodingParameters.hpp" -#include "av/TranscodingResourceHandlerCreator.hpp" -#include "av/Types.hpp" +#include "core/FileResourceHandlerCreator.hpp" #include "core/ILogger.hpp" #include "core/IResourceHandler.hpp" #include "core/String.hpp" @@ -34,6 +32,7 @@ #include "database/TrackLyrics.hpp" #include "database/User.hpp" #include "services/artwork/IArtworkService.hpp" +#include "services/transcoding/ITranscodingService.hpp" #include "CoverArtId.hpp" #include "ParameterParsing.hpp" @@ -47,12 +46,12 @@ namespace lms::api::subsonic namespace { - std::optional subsonicStreamFormatToAvOutputFormat(std::string_view format) + std::optional subsonicStreamFormatToAvOutputFormat(std::string_view format) { - for (const auto& [str, avFormat] : std::initializer_list>{ - { "mp3", av::transcoding::OutputFormat::MP3 }, - { "opus", av::transcoding::OutputFormat::OGG_OPUS }, - { "vorbis", av::transcoding::OutputFormat::OGG_VORBIS }, + for (const auto& [str, avFormat] : std::initializer_list>{ + { "mp3", transcoding::OutputFormat::MP3 }, + { "opus", transcoding::OutputFormat::OGG_OPUS }, + { "vorbis", transcoding::OutputFormat::OGG_VORBIS }, }) { if (core::stringUtils::stringCaseInsensitiveEqual(str, format)) @@ -61,37 +60,37 @@ namespace lms::api::subsonic return std::nullopt; } - av::transcoding::OutputFormat userTranscodeFormatToAvFormat(db::TranscodingOutputFormat format) + transcoding::OutputFormat userTranscodeFormatToAvFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return av::transcoding::OutputFormat::MP3; + return transcoding::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return av::transcoding::OutputFormat::OGG_OPUS; + return transcoding::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return av::transcoding::OutputFormat::MATROSKA_OPUS; + return transcoding::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return av::transcoding::OutputFormat::OGG_VORBIS; + return transcoding::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return av::transcoding::OutputFormat::WEBM_VORBIS; + return transcoding::OutputFormat::WEBM_VORBIS; } - return av::transcoding::OutputFormat::OGG_OPUS; + return transcoding::OutputFormat::OGG_OPUS; } - bool isCodecCompatibleWithOutputFormat(av::DecodingCodec codec, av::transcoding::OutputFormat outputFormat) + bool isCodecCompatibleWithOutputFormat(av::DecodingCodec codec, transcoding::OutputFormat outputFormat) { switch (outputFormat) { - case av::transcoding::OutputFormat::MP3: + case transcoding::OutputFormat::MP3: return codec == av::DecodingCodec::MP3; - case av::transcoding::OutputFormat::OGG_OPUS: - case av::transcoding::OutputFormat::MATROSKA_OPUS: + case transcoding::OutputFormat::OGG_OPUS: + case transcoding::OutputFormat::MATROSKA_OPUS: return codec == av::DecodingCodec::OPUS; - case av::transcoding::OutputFormat::OGG_VORBIS: - case av::transcoding::OutputFormat::WEBM_VORBIS: + case transcoding::OutputFormat::OGG_VORBIS: + case transcoding::OutputFormat::WEBM_VORBIS: return codec == av::DecodingCodec::VORBIS; } @@ -100,13 +99,14 @@ namespace lms::api::subsonic struct StreamParameters { - av::transcoding::InputParameters inputParameters; - std::optional outputParameters; + transcoding::InputParameters inputParameters; + std::optional outputParameters; bool estimateContentLength{}; }; - bool isOutputFormatCompatible(const std::filesystem::path& trackPath, av::transcoding::OutputFormat outputFormat) + bool isOutputFormatCompatible(const std::filesystem::path& trackPath, transcoding::OutputFormat outputFormat) { + // TODO: put this information in db during scan try { const auto audioFile{ av::parseAudioFile(trackPath) }; @@ -143,14 +143,15 @@ namespace lms::api::subsonic if (!track) throw RequestedDataNotFoundError{}; - parameters.inputParameters.trackPath = track->getAbsoluteFilePath(); + parameters.inputParameters.file = track->getAbsoluteFilePath(); parameters.inputParameters.duration = track->getDuration(); + parameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; parameters.estimateContentLength = estimateContentLength; if (format == "raw") // raw => no transcoding return parameters; - std::optional requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) }; + std::optional requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) }; if (!requestedFormat) { if (context.user->getSubsonicEnableTranscodingByDefault()) @@ -185,10 +186,8 @@ namespace lms::api::subsonic if (maxBitRate) bitrate = std::min(bitrate, maxBitRate); - av::transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; - + transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - outputParameters.offset = std::chrono::seconds{ timeOffset }; outputParameters.format = *requestedFormat; outputParameters.bitrate = bitrate; @@ -266,7 +265,7 @@ namespace lms::api::subsonic void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) { - std::shared_ptr resourceHandler; + std::shared_ptr resourceHandler; Wt::Http::ResponseContinuation* continuation{ request.continuation() }; if (!continuation) @@ -285,11 +284,11 @@ namespace lms::api::subsonic trackPath = track->getAbsoluteFilePath(); } - resourceHandler = av::createRawResourceHandler(trackPath); + resourceHandler = core::createFileResourceHandler(trackPath); } else { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } continuation = resourceHandler->processRequest(request, response); @@ -299,7 +298,7 @@ namespace lms::api::subsonic void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) { - std::shared_ptr resourceHandler; + std::shared_ptr resourceHandler; try { @@ -308,13 +307,13 @@ namespace lms::api::subsonic { StreamParameters streamParameters{ getStreamParameters(context) }; if (streamParameters.outputParameters) - resourceHandler = av::transcoding::createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); + resourceHandler = core::Service::get()->createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); else - resourceHandler = av::createRawResourceHandler(streamParameters.inputParameters.trackPath); + resourceHandler = core::createFileResourceHandler(streamParameters.inputParameters.file); } else { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } continuation = resourceHandler->processRequest(request, response); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index fb6f37b3..830a11a5 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -23,6 +23,7 @@ #include "av/IAudioFile.hpp" #include "core/ITraceLogger.hpp" +#include "core/MimeTypes.hpp" #include "core/Service.hpp" #include "core/String.hpp" #include "database/Artist.hpp" @@ -107,7 +108,7 @@ namespace lms::api::subsonic { const std::string fileSuffix{ formatToSuffix(context.user->getSubsonicDefaultTranscodingOutputFormat()) }; trackResponse.setAttribute("transcodedSuffix", fileSuffix); - trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix })); + trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } const Release::pointer release{ track->getRelease() }; @@ -156,7 +157,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000)); trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getAddedTime())); - trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension())); + trackResponse.setAttribute("contentType", core::getMimeType(track->getAbsoluteFilePath().extension())); if (const auto rating{ core::Service::get()->getRating(context.user->getId(), track->getId()) }) trackResponse.setAttribute("userRating", *rating); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index b5027141..9db7d25a 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -73,6 +73,7 @@ target_link_libraries(lms PRIVATE lmsscrobbling lmsartwork lmssubsonic + lmstranscoding lmscore ) diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 39cacc87..fd9a7f12 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -44,6 +44,7 @@ #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "services/scrobbling/IScrobblingService.hpp" +#include "services/transcoding/ITranscodingService.hpp" #include "subsonic/SubsonicResource.hpp" #include "ui/Auth.hpp" #include "ui/LmsApplication.hpp" @@ -373,6 +374,7 @@ namespace lms core::Service recommendationService{ recommendation::createRecommendationService(database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; core::Service scannerService{ scanner::createScannerService(database) }; + core::Service transcodingService{ transcoding::createTranscodingService(*childProcessManagerService.get()) }; scannerService->getEvents().scanComplete.connect([&] { // Flush cover cache even if no changes: diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 2ab9d51d..1a90142a 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -21,7 +21,7 @@ #include -#include "av/RawResourceHandlerCreator.hpp" +#include "core/FileResourceHandlerCreator.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/String.hpp" @@ -85,7 +85,7 @@ namespace lms::ui { LMS_SCOPED_TRACE_OVERVIEW("UI", "HandleAudioFileRequest"); - std::shared_ptr fileResourceHandler; + std::shared_ptr fileResourceHandler; if (!request.continuation()) { @@ -93,11 +93,11 @@ namespace lms::ui if (!trackPath) return; - fileResourceHandler = av::createRawResourceHandler(*trackPath); + fileResourceHandler = core::createFileResourceHandler(*trackPath); } else { - fileResourceHandler = Wt::cpp17::any_cast>(request.continuation()->data()); + fileResourceHandler = Wt::cpp17::any_cast>(request.continuation()->data()); } auto* continuation{ fileResourceHandler->processRequest(request, response) }; diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index 37498a47..5cf7f8ee 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -23,14 +23,14 @@ #include -#include "av/TranscodingParameters.hpp" -#include "av/TranscodingResourceHandlerCreator.hpp" -#include "av/Types.hpp" #include "core/ILogger.hpp" +#include "core/IResourceHandler.hpp" +#include "core/Service.hpp" #include "core/String.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" +#include "services/transcoding/ITranscodingService.hpp" #include "LmsApplication.hpp" @@ -72,20 +72,20 @@ namespace lms::ui { namespace { - std::optional AudioFormatToAvFormat(db::TranscodingOutputFormat format) + std::optional AudioFormatToAvFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return av::transcoding::OutputFormat::MP3; + return transcoding::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return av::transcoding::OutputFormat::OGG_OPUS; + return transcoding::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return av::transcoding::OutputFormat::MATROSKA_OPUS; + return transcoding::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return av::transcoding::OutputFormat::OGG_VORBIS; + return transcoding::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return av::transcoding::OutputFormat::WEBM_VORBIS; + return transcoding::OutputFormat::WEBM_VORBIS; } TRANSCODE_LOG(ERROR, "Cannot convert from audio format to AV format"); @@ -112,8 +112,8 @@ namespace lms::ui struct TranscodingParameters { - av::transcoding::InputParameters inputParameters; - av::transcoding::OutputParameters outputParameters; + transcoding::InputParameters inputParameters; + transcoding::OutputParameters outputParameters; }; std::optional readTranscodingParameters(const Wt::Http::Request& request) @@ -134,7 +134,7 @@ namespace lms::ui return std::nullopt; } - const std::optional avFormat{ AudioFormatToAvFormat(*format) }; + const std::optional avFormat{ AudioFormatToAvFormat(*format) }; if (!avFormat) return std::nullopt; @@ -152,14 +152,14 @@ namespace lms::ui return std::nullopt; } - parameters.inputParameters.trackPath = track->getAbsoluteFilePath(); + parameters.inputParameters.file = track->getAbsoluteFilePath(); parameters.inputParameters.duration = track->getDuration(); + parameters.inputParameters.offset = std::chrono::seconds{ offset }; } parameters.outputParameters.stripMetadata = true; parameters.outputParameters.format = *avFormat; parameters.outputParameters.bitrate = *bitrate; - parameters.outputParameters.offset = std::chrono::seconds{ offset }; return parameters; } @@ -177,31 +177,24 @@ namespace lms::ui void AudioTranscodingResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - std::shared_ptr resourceHandler; + std::shared_ptr resourceHandler; - try + Wt::Http::ResponseContinuation* continuation{ request.continuation() }; + if (!continuation) { - Wt::Http::ResponseContinuation* continuation{ request.continuation() }; - if (!continuation) - { - if (const auto& parameters{ readTranscodingParameters(request) }) - resourceHandler = av::transcoding::createResourceHandler(parameters->inputParameters, parameters->outputParameters, false /* estimate content length */); - } - else - { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); - } - - if (resourceHandler) - { - continuation = resourceHandler->processRequest(request, response); - if (continuation) - continuation->setData(resourceHandler); - } + if (const auto& parameters{ readTranscodingParameters(request) }) + resourceHandler = core::Service::get()->createResourceHandler(parameters->inputParameters, parameters->outputParameters, false /* estimate content length */); } - catch (const av::Exception& e) + else { - TRANSCODE_LOG(ERROR, "Caught Av exception: " << e.what()); + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + } + + if (resourceHandler) + { + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); } } From 2557402367267109e050a0b2fba2e9e9e812629f Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 31 May 2025 10:27:53 +0200 Subject: [PATCH 08/51] Using track id for the transcoding service --- src/libs/services/auth/CMakeLists.txt | 7 +-- .../services/recommendation/CMakeLists.txt | 6 ++- .../recommendation/IRecommendationService.hpp | 2 + src/libs/services/transcoding/CMakeLists.txt | 5 ++ .../impl/TranscodingResourceHandler.cpp | 28 ++--------- .../impl/TranscodingResourceHandler.hpp | 9 +--- .../transcoding/impl/TranscodingService.cpp | 47 +++++++++++++++++-- .../transcoding/impl/TranscodingService.hpp | 3 +- .../transcoding/ITranscodingService.hpp | 25 ++++++---- .../impl/endpoints/MediaRetrieval.cpp | 7 +-- src/lms/main.cpp | 2 +- .../ui/resource/AudioTranscodingResource.cpp | 18 +------ 12 files changed, 88 insertions(+), 71 deletions(-) diff --git a/src/libs/services/auth/CMakeLists.txt b/src/libs/services/auth/CMakeLists.txt index 3558812d..c664ca98 100644 --- a/src/libs/services/auth/CMakeLists.txt +++ b/src/libs/services/auth/CMakeLists.txt @@ -18,14 +18,11 @@ target_include_directories(lmsauth PRIVATE impl ) -target_link_libraries(lmsauth PRIVATE - lmscore - lmsdatabase - ) - target_link_libraries(lmsauth PUBLIC Boost::system Wt::Wt + lmscore + lmsdatabase ) # PAM diff --git a/src/libs/services/recommendation/CMakeLists.txt b/src/libs/services/recommendation/CMakeLists.txt index b31cf280..cd170732 100644 --- a/src/libs/services/recommendation/CMakeLists.txt +++ b/src/libs/services/recommendation/CMakeLists.txt @@ -21,7 +21,11 @@ target_include_directories(lmsrecommendation PRIVATE ) target_link_libraries(lmsrecommendation PRIVATE - lmsdatabase + lmssom std::filesystem ) + +target_link_libraries(lmsrecommendation PUBLIC + lmsdatabase + ) diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp index 89ef69fc..0872cc69 100644 --- a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -23,6 +23,8 @@ #include #include "core/EnumSet.hpp" +#include "database/ArtistId.hpp" +#include "database/ReleaseId.hpp" #include "database/TrackListId.hpp" #include "database/Types.hpp" #include "services/recommendation/Types.hpp" diff --git a/src/libs/services/transcoding/CMakeLists.txt b/src/libs/services/transcoding/CMakeLists.txt index 2c0f5843..e57e0b5f 100644 --- a/src/libs/services/transcoding/CMakeLists.txt +++ b/src/libs/services/transcoding/CMakeLists.txt @@ -15,3 +15,8 @@ target_include_directories(lmstranscoding PRIVATE target_link_libraries(lmstranscoding PRIVATE lmsav ) + +target_link_libraries(lmstranscoding PUBLIC + lmsdatabase + ) + diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp b/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp index 0fda5f9e..8c21b93f 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp +++ b/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp @@ -25,39 +25,19 @@ namespace lms::transcoding { - namespace - { - std::size_t doEstimateContentLength(const InputParameters& inputParameters, const OutputParameters& outputParameters) - { - const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast(std::chrono::duration_cast(inputParameters.duration).count()) / 1000 }; - return estimatedContentLength; - } - - av::InputParameters toAv(const InputParameters& in) - { - return { .file = in.file, .offset = in.offset, .streamIndex = in.streamIndex }; - } - - av::OutputParameters toAv(const OutputParameters& out) - { - return { .format = static_cast(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata }; - } - - } // namespace - - std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + std::unique_ptr createResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, bool estimateContentLength) { return std::make_unique(inputParameters, outputParameters, estimateContentLength); } // TODO set some nice HTTP return code - TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) - : _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt } + TranscodingResourceHandler::TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength) + : _estimatedContentLength{ estimatedContentLength } { try { - _transcoder = av::createTranscoder(toAv(inputParameters), toAv(outputParameters)); + _transcoder = av::createTranscoder(inputParameters, outputParameters); if (_estimatedContentLength) LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp index 26465b1b..89f6d3ce 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp +++ b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp @@ -23,20 +23,15 @@ #include #include +#include "av/ITranscoder.hpp" #include "core/IResourceHandler.hpp" -#include "services/transcoding/ITranscodingService.hpp" - -namespace lms::av -{ - class ITranscoder; -} namespace lms::transcoding { class TranscodingResourceHandler final : public core::IResourceHandler { public: - TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength); + TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength); ~TranscodingResourceHandler() override; TranscodingResourceHandler(const TranscodingResourceHandler&) = delete; diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodingService.cpp index 57a2f57e..1599d3a1 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodingService.cpp @@ -19,19 +19,38 @@ #include "TranscodingService.hpp" +#include "av/ITranscoder.hpp" #include "core/ILogger.hpp" +#include "database/Db.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" #include "TranscodingResourceHandler.hpp" namespace lms::transcoding { - std::unique_ptr createTranscodingService(core::IChildProcessManager& childProcessManager) + namespace { - return std::make_unique(childProcessManager); + av::OutputParameters toAv(const OutputParameters& out) + { + return { .format = static_cast(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata }; + } + + std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) + { + const std::size_t estimatedContentLength{ (bitrate / 8 * duration.count()) / 1000 }; + return estimatedContentLength; + } + } // namespace + + std::unique_ptr createTranscodingService(db::Db& db, core::IChildProcessManager& childProcessManager) + { + return std::make_unique(db, childProcessManager); } - TranscodingService::TranscodingService(core::IChildProcessManager& childProcessManager) - : _childProcessManager(childProcessManager) + TranscodingService::TranscodingService(db::Db& db, core::IChildProcessManager& childProcessManager) + : _db{ db } + , _childProcessManager(childProcessManager) { LMS_LOG(TRANSCODING, INFO, "Service started!"); } @@ -43,6 +62,24 @@ namespace lms::transcoding std::unique_ptr TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) { - return std::make_unique(inputParameters, outputParameters, estimateContentLength); + av::InputParameters avInputParams; + std::optional estimatedContentLength; + { + auto& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + db::Track::pointer track{ db::Track::find(session, inputParameters.trackId) }; + if (!track) + return nullptr; + + avInputParams.file = track->getAbsoluteFilePath(); + avInputParams.offset = inputParameters.offset; + avInputParams.streamIndex = inputParameters.streamIndex; + + if (estimateContentLength) + estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, track->getDuration()); + } + + return std::make_unique(avInputParams, toAv(outputParameters), estimatedContentLength); } } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.hpp b/src/libs/services/transcoding/impl/TranscodingService.hpp index d3c55aee..cfa45eaf 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.hpp +++ b/src/libs/services/transcoding/impl/TranscodingService.hpp @@ -26,7 +26,7 @@ namespace lms::transcoding class TranscodingService : public ITranscodingService { public: - explicit TranscodingService(core::IChildProcessManager& childProcessManager); + explicit TranscodingService(db::Db& db, core::IChildProcessManager& childProcessManager); ~TranscodingService() override; TranscodingService(const TranscodingService&) = delete; @@ -35,6 +35,7 @@ namespace lms::transcoding private: std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override; + db::Db& _db; core::IChildProcessManager& _childProcessManager; }; } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp index 3c383f14..c6fccd01 100644 --- a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp +++ b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp @@ -23,19 +23,28 @@ #include #include -namespace lms::core +#include "database/TrackId.hpp" + +namespace lms { - class IChildProcessManager; - class IResourceHandler; -} // namespace lms::core + namespace core + { + class IChildProcessManager; + class IResourceHandler; + } // namespace core + + namespace db + { + class Db; + } +} // namespace lms namespace lms::transcoding { struct InputParameters { - std::filesystem::path file; // Path to the input file - std::chrono::milliseconds duration; // Offset in the input file to start transcoding from - std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from + db::TrackId trackId; + std::chrono::milliseconds offset{}; // Offset in the track file to start transcoding from std::optional streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set) }; @@ -63,5 +72,5 @@ namespace lms::transcoding virtual std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0; }; - std::unique_ptr createTranscodingService(core::IChildProcessManager& childProcessManager); + std::unique_ptr createTranscodingService(db::Db& db, core::IChildProcessManager& childProcessManager); } // namespace lms::transcoding diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index d756ea1e..4b5d40c1 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -101,6 +101,7 @@ namespace lms::api::subsonic { transcoding::InputParameters inputParameters; std::optional outputParameters; + std::filesystem::path trackPath; bool estimateContentLength{}; }; @@ -143,10 +144,10 @@ namespace lms::api::subsonic if (!track) throw RequestedDataNotFoundError{}; - parameters.inputParameters.file = track->getAbsoluteFilePath(); - parameters.inputParameters.duration = track->getDuration(); + parameters.inputParameters.trackId = id; parameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; parameters.estimateContentLength = estimateContentLength; + parameters.trackPath = track->getAbsoluteFilePath(); if (format == "raw") // raw => no transcoding return parameters; @@ -309,7 +310,7 @@ namespace lms::api::subsonic if (streamParameters.outputParameters) resourceHandler = core::Service::get()->createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); else - resourceHandler = core::createFileResourceHandler(streamParameters.inputParameters.file); + resourceHandler = core::createFileResourceHandler(streamParameters.trackPath); } else { diff --git a/src/lms/main.cpp b/src/lms/main.cpp index fd9a7f12..de5e4364 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -374,7 +374,7 @@ namespace lms core::Service recommendationService{ recommendation::createRecommendationService(database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; core::Service scannerService{ scanner::createScannerService(database) }; - core::Service transcodingService{ transcoding::createTranscodingService(*childProcessManagerService.get()) }; + core::Service transcodingService{ transcoding::createTranscodingService(database, *childProcessManagerService.get()) }; scannerService->getEvents().scanComplete.connect([&] { // Flush cover cache even if no changes: diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index 5cf7f8ee..926985cc 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -141,22 +141,8 @@ namespace lms::ui // optional parameter std::size_t offset{ readParameterAs(request, "offset").value_or(0) }; - std::filesystem::path trackPath; - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), *trackId) }; - if (!track) - { - TRANSCODE_LOG(ERROR, "Missing track"); - return std::nullopt; - } - - parameters.inputParameters.file = track->getAbsoluteFilePath(); - parameters.inputParameters.duration = track->getDuration(); - parameters.inputParameters.offset = std::chrono::seconds{ offset }; - } - + parameters.inputParameters.trackId = *trackId; + parameters.inputParameters.offset = std::chrono::seconds{ offset }; parameters.outputParameters.stripMetadata = true; parameters.outputParameters.format = *avFormat; parameters.outputParameters.bitrate = *bitrate; From 54771e6ddf3ec0fe258405bc32bb311dc10a2527 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 29 May 2025 21:36:42 +0200 Subject: [PATCH 09/51] Minor fixes to ChildProcess - Don't set FD_CLOEXEC, but close manually before fork: Since we don't use pipe2() to set the flag atomically, we're prone to races with concurrent fork+execs either way. As ChildProcess uses a mutex here anyways, as long as there is no other part in lms that would fork+exec, we're not any more or less safe now, but the code is shorter and we cannot fail the fcntl. - Don't make the write-end of the pipe non-blocking. Usually programs do not expect this, i.e. they either never check the return code of a write to stdout (potential data loss), or if they do, they just bail out on any error, and don't handle EAGAIN. ffmpeg seemed to handle this fine though (or we were just lucky and always read the data faster than ffmpeg could produce it). - Do not close stdin and stderr. Again ffmpeg seems to handle this, at least regarding stdin thanks to -nostdin, but in general if a process tries to write to stderr and fd 2 is not open, it might just bail out. Even worse, the process might have opened some file it wants to work with, and that file got assigned fd 2 (as that was the next free fd) - the program would corrupt whatever file it opened there whenever it tries to write to stderr. Try to open /dev/null instead for stdin and stderr, and if that fails, keep whatever lms inherited open, which should be safer than relying on the child process to handle this case properly. - exec() does not return on success, no need to check return code, any return from it is a failure. - Wrong error message in error path when assigning fd to boost stream. - F_SETPIPE_SZ requires and int, not size_t. This worked on little endian systems since the value passed was < 2^32, but on big endian systems you'd effectively pass "0" if using a 64bit type. - Address a few clang-tidy complaints (constness) --- src/libs/core/impl/ChildProcess.cpp | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/libs/core/impl/ChildProcess.cpp b/src/libs/core/impl/ChildProcess.cpp index 2dc57207..157b7709 100644 --- a/src/libs/core/impl/ChildProcess.cpp +++ b/src/libs/core/impl/ChildProcess.cpp @@ -63,32 +63,23 @@ namespace lms::core { // make sure only one thread is executing this part of code static std::mutex mutex; - std::unique_lock lock{ mutex }; + const std::lock_guard lock{ mutex }; int pipefd[2]; // Use 'pipe' instead of 'pipe2', more portable - if (pipe(pipefd) < 0) + if (pipe(pipefd) == -1) 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 + // Only set O_NONBLOCK on read end - usually programs don't expect stdout to be non-blocking if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" }; - if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1) - throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" }; - - if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1) - throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set FD_CLOEXEC!" }; - - if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) - throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set FD_CLOEXEC!" }; - #if defined(__linux__) && defined(F_SETPIPE_SZ) for (const int fd : { pipefd[0], pipefd[1] }) { - constexpr std::size_t targetPipeSize{ static_cast(65'536) * 4 }; - std::size_t currentPipeSize{ 65'536 }; // common default value + constexpr int targetPipeSize{ 65'536 * 4 }; + int currentPipeSize{ 65'536 }; // common default value #if defined(F_GETPIPE_SZ) const int pipeSizeRes{ fcntl(fd, F_GETPIPE_SZ) }; if (pipeSizeRes == -1) @@ -112,27 +103,36 @@ namespace lms::core } #endif - int res{ fork() }; + const int res{ fork() }; if (res == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "fork failed!" }; if (res == 0) // CHILD { - close(pipefd[0]); - close(STDIN_FILENO); - close(STDERR_FILENO); + // Never close stdin/out/err, most programs expect these to exist; + // rather connect them to /dev/null if unwanted + const int nullFd{ open("/dev/null", O_RDWR) }; + // Ignore errors, worst thing is stderr writes to the same fd as lms + if (nullFd != -1) + { + dup2(nullFd, STDIN_FILENO); + dup2(nullFd, STDERR_FILENO); + close(nullFd); + } // Replace stdout with pipe write if (dup2(pipefd[1], STDOUT_FILENO) == -1) exit(-1); + // Close pipe: read end not needed, write end was dup2ed + close(pipefd[0]); + close(pipefd[1]); std::vector execArgs; std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); }); execArgs.push_back(nullptr); - res = execv(path.string().c_str(), (char* const*)&execArgs[0]); - if (res == -1) - exit(-1); + execv(path.string().c_str(), (char* const*)&execArgs[0]); + exit(-1); } else // PARENT { @@ -141,7 +141,7 @@ namespace lms::core boost::system::error_code assignError; _childStdout.assign(pipefd[0], assignError); if (assignError) - throw SystemException{ assignError, "fork failed!" }; + throw SystemException{ assignError, "assigning read end of pipe to asio stream failed!" }; } _childPID = res; } From 5b0a136542b5f67be8df5bfbc013daa6732caf88 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sat, 24 May 2025 15:49:47 +0200 Subject: [PATCH 10/51] FileResourceHandler: Open file once on init, more error handling Keeping the file open allows better prefetching/caching on both the application layer (ifstream) and OS level (vfs layer) as the access pattern is sequential and predictable. We also save three syscalls (open, seek, close) for every chunk we send to the client (256kb). --- src/libs/core/impl/FileResourceHandler.cpp | 75 ++++++++++------------ src/libs/core/impl/FileResourceHandler.hpp | 4 +- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/libs/core/impl/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp index 7db88efc..561fda0f 100644 --- a/src/libs/core/impl/FileResourceHandler.cpp +++ b/src/libs/core/impl/FileResourceHandler.cpp @@ -19,8 +19,6 @@ #include "FileResourceHandler.hpp" -#include - #include "core/ILogger.hpp" #include "core/MimeTypes.hpp" @@ -32,38 +30,37 @@ namespace lms::core } FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) - : _path{ path } - , _mimeType{ mimeType } + : _mimeType{ mimeType } + , _ifs{ path, std::ios::in | std::ios::binary } { + if (!_ifs) + LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << path); + else + { + _ifs.seekg(0, std::ios::end); + if (!_ifs.fail()) + _fileSize = static_cast<::uint64_t>(_ifs.tellg()); + LMS_LOG(UTILS, DEBUG, "File " << path << ", fileSize = " << _fileSize); + } } Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - ::uint64_t startByte{ _offset }; - std::ifstream ifs{ _path, std::ios::in | std::ios::binary }; - - if (startByte == 0) + if (_offset == 0) { - if (!ifs) + if (!_ifs) { - LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << _path); response.setStatus(404); return {}; } - ifs.seekg(0, std::ios::end); - const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) }; - ifs.seekg(0, std::ios::beg); - - LMS_LOG(UTILS, DEBUG, "File " << _path << ", fileSize = " << fileSize); - response.addHeader("Accept-Ranges", "bytes"); - const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) }; + const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(_fileSize) }; if (!ranges.isSatisfiable()) { std::ostringstream contentRange; - contentRange << "bytes */" << fileSize; + contentRange << "bytes */" << _fileSize; response.setStatus(416); // Requested range not satisfiable response.addHeader("Content-Range", contentRange.str()); @@ -76,57 +73,51 @@ namespace lms::core LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte()); response.setStatus(206); - startByte = ranges[0].firstByte(); + _offset = ranges[0].firstByte(); _beyondLastByte = ranges[0].lastByte() + 1; std::ostringstream contentRange; - contentRange << "bytes " << startByte << "-" - << _beyondLastByte - 1 << "/" << fileSize; + contentRange << "bytes " << _offset << "-" + << _beyondLastByte - 1 << "/" << _fileSize; response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(_beyondLastByte - startByte); + response.setContentLength(_beyondLastByte - _offset); } else { LMS_LOG(UTILS, DEBUG, "No range requested"); response.setStatus(200); - _beyondLastByte = fileSize; + _beyondLastByte = _fileSize; response.setContentLength(_beyondLastByte); } LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'"); response.setMimeType(_mimeType); - } - else if (!ifs) - { - LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for " << _path); - return {}; - } - ifs.seekg(static_cast(startByte)); + _ifs.seekg(static_cast(_offset)); + } // end initial response setup - std::vector buf; - buf.resize(_chunkSize); + ::uint64_t restSize = _beyondLastByte - _offset; + ::uint64_t pieceSize = std::min(restSize, _chunkSize); - ::uint64_t restSize = _beyondLastByte - startByte; - ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); + std::vector buf(pieceSize); - ifs.read(&buf[0], pieceSize); - const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) }; + _ifs.read(buf.data(), buf.size()); + const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(_ifs.gcount()) }; if (actualPieceSize > 0) { - response.out().write(&buf[0], actualPieceSize); - LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << ""); + response.out().write(buf.data(), actualPieceSize); + LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << _offset << "-" << _offset + actualPieceSize - 1 << ""); } else - { LMS_LOG(UTILS, DEBUG, "Written 0 byte"); - } - if (ifs.good() && actualPieceSize < restSize) + if (!_ifs.good()) + LMS_LOG(UTILS, WARNING, "Error reading from file!"); + else if (actualPieceSize < restSize) { - _offset = startByte + actualPieceSize; + _offset += actualPieceSize; LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1); return response.createContinuation(); diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 7e541038..5c239842 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "core/IResourceHandler.hpp" @@ -38,9 +39,10 @@ namespace lms::core static constexpr std::size_t _chunkSize{ 262'144 }; - std::filesystem::path _path; std::string _mimeType; ::uint64_t _beyondLastByte{}; ::uint64_t _offset{}; + ::uint64_t _fileSize{}; + std::ifstream _ifs; }; } // namespace lms::core \ No newline at end of file From 8bcda2a96f19c06546327303d3be57f34a3280d9 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Jun 2025 21:10:01 +0200 Subject: [PATCH 11/51] Fixed warning --- src/libs/services/transcoding/impl/TranscodingService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodingService.cpp index 1599d3a1..09270a19 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodingService.cpp @@ -38,7 +38,7 @@ namespace lms::transcoding std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) { - const std::size_t estimatedContentLength{ (bitrate / 8 * duration.count()) / 1000 }; + const std::size_t estimatedContentLength{ static_cast((bitrate / 8 * duration.count()) / 1000) }; return estimatedContentLength; } } // namespace From 221daf5222c3aa2d21589b3818beadfb133af35a Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Jun 2025 21:10:01 +0200 Subject: [PATCH 12/51] Fixed warning --- src/libs/services/transcoding/impl/TranscodingService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodingService.cpp index 1599d3a1..09270a19 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodingService.cpp @@ -38,7 +38,7 @@ namespace lms::transcoding std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) { - const std::size_t estimatedContentLength{ (bitrate / 8 * duration.count()) / 1000 }; + const std::size_t estimatedContentLength{ static_cast((bitrate / 8 * duration.count()) / 1000) }; return estimatedContentLength; } } // namespace From 4e7bc9c7cc7ef932086fede5fd2a064b9494b75a Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Jun 2025 21:38:19 +0200 Subject: [PATCH 13/51] fixed format --- src/libs/core/impl/FileResourceHandler.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 5c239842..34beecc4 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -20,9 +20,9 @@ #pragma once #include +#include #include #include -#include #include "core/IResourceHandler.hpp" From 1e7fbcad1590d91bef87ca4dfcd013baaa893d6b Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Jun 2025 21:10:01 +0200 Subject: [PATCH 14/51] Fixed warning --- src/libs/services/transcoding/impl/TranscodingService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodingService.cpp index 1599d3a1..09270a19 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodingService.cpp @@ -38,7 +38,7 @@ namespace lms::transcoding std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) { - const std::size_t estimatedContentLength{ (bitrate / 8 * duration.count()) / 1000 }; + const std::size_t estimatedContentLength{ static_cast((bitrate / 8 * duration.count()) / 1000) }; return estimatedContentLength; } } // namespace From b2471cccfb6511f9ec336df0fad84317e8be2125 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sat, 24 May 2025 15:49:47 +0200 Subject: [PATCH 15/51] FileResourceHandler: Open file once on init, more error handling Keeping the file open allows better prefetching/caching on both the application layer (ifstream) and OS level (vfs layer) as the access pattern is sequential and predictable. We also save three syscalls (open, seek, close) for every chunk we send to the client (256kb). --- src/libs/core/impl/FileResourceHandler.cpp | 75 ++++++++++------------ src/libs/core/impl/FileResourceHandler.hpp | 4 +- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/libs/core/impl/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp index 7db88efc..561fda0f 100644 --- a/src/libs/core/impl/FileResourceHandler.cpp +++ b/src/libs/core/impl/FileResourceHandler.cpp @@ -19,8 +19,6 @@ #include "FileResourceHandler.hpp" -#include - #include "core/ILogger.hpp" #include "core/MimeTypes.hpp" @@ -32,38 +30,37 @@ namespace lms::core } FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) - : _path{ path } - , _mimeType{ mimeType } + : _mimeType{ mimeType } + , _ifs{ path, std::ios::in | std::ios::binary } { + if (!_ifs) + LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << path); + else + { + _ifs.seekg(0, std::ios::end); + if (!_ifs.fail()) + _fileSize = static_cast<::uint64_t>(_ifs.tellg()); + LMS_LOG(UTILS, DEBUG, "File " << path << ", fileSize = " << _fileSize); + } } Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - ::uint64_t startByte{ _offset }; - std::ifstream ifs{ _path, std::ios::in | std::ios::binary }; - - if (startByte == 0) + if (_offset == 0) { - if (!ifs) + if (!_ifs) { - LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << _path); response.setStatus(404); return {}; } - ifs.seekg(0, std::ios::end); - const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) }; - ifs.seekg(0, std::ios::beg); - - LMS_LOG(UTILS, DEBUG, "File " << _path << ", fileSize = " << fileSize); - response.addHeader("Accept-Ranges", "bytes"); - const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) }; + const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(_fileSize) }; if (!ranges.isSatisfiable()) { std::ostringstream contentRange; - contentRange << "bytes */" << fileSize; + contentRange << "bytes */" << _fileSize; response.setStatus(416); // Requested range not satisfiable response.addHeader("Content-Range", contentRange.str()); @@ -76,57 +73,51 @@ namespace lms::core LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte()); response.setStatus(206); - startByte = ranges[0].firstByte(); + _offset = ranges[0].firstByte(); _beyondLastByte = ranges[0].lastByte() + 1; std::ostringstream contentRange; - contentRange << "bytes " << startByte << "-" - << _beyondLastByte - 1 << "/" << fileSize; + contentRange << "bytes " << _offset << "-" + << _beyondLastByte - 1 << "/" << _fileSize; response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(_beyondLastByte - startByte); + response.setContentLength(_beyondLastByte - _offset); } else { LMS_LOG(UTILS, DEBUG, "No range requested"); response.setStatus(200); - _beyondLastByte = fileSize; + _beyondLastByte = _fileSize; response.setContentLength(_beyondLastByte); } LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'"); response.setMimeType(_mimeType); - } - else if (!ifs) - { - LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for " << _path); - return {}; - } - ifs.seekg(static_cast(startByte)); + _ifs.seekg(static_cast(_offset)); + } // end initial response setup - std::vector buf; - buf.resize(_chunkSize); + ::uint64_t restSize = _beyondLastByte - _offset; + ::uint64_t pieceSize = std::min(restSize, _chunkSize); - ::uint64_t restSize = _beyondLastByte - startByte; - ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); + std::vector buf(pieceSize); - ifs.read(&buf[0], pieceSize); - const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) }; + _ifs.read(buf.data(), buf.size()); + const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(_ifs.gcount()) }; if (actualPieceSize > 0) { - response.out().write(&buf[0], actualPieceSize); - LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << ""); + response.out().write(buf.data(), actualPieceSize); + LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << _offset << "-" << _offset + actualPieceSize - 1 << ""); } else - { LMS_LOG(UTILS, DEBUG, "Written 0 byte"); - } - if (ifs.good() && actualPieceSize < restSize) + if (!_ifs.good()) + LMS_LOG(UTILS, WARNING, "Error reading from file!"); + else if (actualPieceSize < restSize) { - _offset = startByte + actualPieceSize; + _offset += actualPieceSize; LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1); return response.createContinuation(); diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 7e541038..5c239842 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "core/IResourceHandler.hpp" @@ -38,9 +39,10 @@ namespace lms::core static constexpr std::size_t _chunkSize{ 262'144 }; - std::filesystem::path _path; std::string _mimeType; ::uint64_t _beyondLastByte{}; ::uint64_t _offset{}; + ::uint64_t _fileSize{}; + std::ifstream _ifs; }; } // namespace lms::core \ No newline at end of file From c9e4ec1eb5bcd4bfddc3780723d5ceb0561543f6 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Jun 2025 21:38:19 +0200 Subject: [PATCH 16/51] fixed format --- src/libs/core/impl/FileResourceHandler.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 5c239842..34beecc4 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -20,9 +20,9 @@ #pragma once #include +#include #include #include -#include #include "core/IResourceHandler.hpp" From 161d7b6c152b935de064233829d38b73439d0e1b Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 6 Jun 2025 08:45:34 +0200 Subject: [PATCH 17/51] Minor clean --- src/libs/core/impl/ChildProcess.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libs/core/impl/ChildProcess.cpp b/src/libs/core/impl/ChildProcess.cpp index 157b7709..dfcb532f 100644 --- a/src/libs/core/impl/ChildProcess.cpp +++ b/src/libs/core/impl/ChildProcess.cpp @@ -19,18 +19,17 @@ #include "ChildProcess.hpp" -#include -#include #include #include #include #include -#include #include #include -#include +#include +#include #include +#include #include #include @@ -63,7 +62,7 @@ namespace lms::core { // make sure only one thread is executing this part of code static std::mutex mutex; - const std::lock_guard lock{ mutex }; + const std::scoped_lock lock{ mutex }; int pipefd[2]; From 4837e1959f6cfa26cd35fc145af48e483175babe Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 12 Jun 2025 22:44:14 +0200 Subject: [PATCH 18/51] search3: minor optim --- src/libs/database/impl/Release.cpp | 26 +++++++++++++++---- .../database/include/database/Release.hpp | 1 + src/libs/subsonic/impl/responses/Album.cpp | 6 ++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4c2575c1..d06dad36 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -867,14 +867,11 @@ namespace lms::db for (const std::string& bindArg : where.getBindArgs()) query.bind(bindArg); - auto queryRes{ query.resultList() }; - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { + utils::forEachQueryResult(query, [&](const Wt::Dbo::ptr& cluster) { if (clustersByType[cluster->getType()->getId()].size() < size) clustersByType[cluster->getType()->getId()].push_back(cluster); - } + }); std::vector> res; for (const auto& [clusterTypeId, clusters] : clustersByType) @@ -882,4 +879,23 @@ namespace lms::db return res; } + + std::vector> Release::getClusters(ClusterTypeId clusterTypeId, std::size_t maxCount) const + { + assert(session()); + + auto query{ session()->query>("SELECT c FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") + .where("t.release_id = ?").bind(getId()) + .where("c.cluster_type_id = ?").bind(clusterTypeId.toString()) + .groupBy("c.id") + .orderBy("COUNT(c.id) DESC") + .limit(static_cast(maxCount)) }; + + std::vector> res; + utils::forEachQueryResult(query, [&](const Wt::Dbo::ptr& cluster) { + res.push_back(cluster); + }); + + return res; + } } // namespace lms::db diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index f6df0773..885b9672 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -258,6 +258,7 @@ namespace lms::db // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) // size is the max number of cluster per cluster type std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const; + std::vector> getClusters(ClusterTypeId clusterTypeId, std::size_t maxCount) const; // Utility functions (if all tracks have the same values, which is legit to not be the case) core::PartialDateTime getDate() const; diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index eacefb0f..2344a18b 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -131,9 +131,9 @@ namespace lms::api::subsonic const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; if (genreClusterType) { - auto clusters{ release->getClusterGroups({ genreClusterType->getId() }, 1) }; - if (!clusters.empty() && !clusters.front().empty()) - albumNode.setAttribute("genre", clusters.front().front()->getName()); + const auto clusters{ release->getClusters(genreClusterType->getId(), 1) }; + if (!clusters.empty()) + albumNode.setAttribute("genre", clusters.front()->getName()); } if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid()) From d4d14c39a5830d7e2d90a3109cd71d63beb79b37 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 12 Jun 2025 22:52:44 +0200 Subject: [PATCH 19/51] Fixed format --- src/libs/database/impl/Release.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index d06dad36..4d367881 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -884,12 +884,12 @@ namespace lms::db { assert(session()); - auto query{ session()->query>("SELECT c FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") - .where("t.release_id = ?").bind(getId()) - .where("c.cluster_type_id = ?").bind(clusterTypeId.toString()) - .groupBy("c.id") - .orderBy("COUNT(c.id) DESC") - .limit(static_cast(maxCount)) }; + auto query{ session()->query>("SELECT c FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") }; + query.where("t.release_id = ?").bind(getId()); + query.where("c.cluster_type_id = ?").bind(clusterTypeId); + query.groupBy("c.id"); + query.orderBy("COUNT(c.id) DESC"); + query.limit(static_cast(maxCount)); std::vector> res; utils::forEachQueryResult(query, [&](const Wt::Dbo::ptr& cluster) { From 085210f3647e41bc989a13cd7b413bbadadc119c Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Jun 2025 13:06:41 +0200 Subject: [PATCH 20/51] Subsonic API: report original year for albums, fixes #672 --- src/libs/subsonic/impl/responses/Album.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 2344a18b..cec71030 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -105,7 +105,9 @@ namespace lms::api::subsonic albumNode.setAttribute("coverArt", idToString(coverArtId)); }); } - if (const auto year{ release->getYear() }) + if (const auto originalYear{ release->getOriginalYear() }) + albumNode.setAttribute("year", *originalYear); + else if (const auto year{ release->getYear() }) albumNode.setAttribute("year", *year); auto artists{ release->getReleaseArtists() }; From 4d3201c1a0b7f91eaa974b165b58b1ae1adeb685 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Jun 2025 13:34:19 +0200 Subject: [PATCH 21/51] Use the folder name as artist name if no name is provided in the artist.nfo file. fixes #688 --- README.md | 2 +- .../services/scanner/impl/scanners/ArtistInfoFileScanner.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87e52361..5743db76 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ _LMS_ supports an Artist information folder to manage metadata and images for ar The folder must follow a structure defined by Kodi, as detailed [here](https://kodi.wiki/view/Artist_information_folder). `artist.nfo` files are used to define additional artist information such as biography, sort name, and MusicBrainz ArtistID. See the format [here](https://kodi.wiki/view/NFO_files/Artists). -__Note__: `artist.nfo` files must include both `name` and `musicBrainzArtistID` fields to be recognized. +__Note__: If no name is provided in the artist.nfo file, the name of the containing folder is used. ### Filtering It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings. diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp index 718a60eb..21469e80 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp @@ -75,8 +75,8 @@ namespace lms::scanner _parsedArtistInfo = metadata::parseArtistInfo(ifs); if (_parsedArtistInfo->name.empty()) { - addError(getFilePath()); - _parsedArtistInfo.reset(); + _parsedArtistInfo->name = getFilePath().parent_path().filename(); + LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'"); } } catch (const metadata::ArtistInfoParseException& e) From a008ccfacc1537bc3ebb249e53a239c659e105c0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 15 Jun 2025 14:34:14 +0200 Subject: [PATCH 22/51] Increased the artist info scan version to force rescan. ref #688 --- src/libs/database/impl/Migration.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index a0318ed8..47fbbde9 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 90 }; + static constexpr Version LMS_DATABASE_VERSION{ 91 }; } VersionInfo::VersionInfo() @@ -1206,6 +1206,13 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1"); } + void migrateFromV90(Session& session) + { + // Added a fallback for missing name, using parent directory name + // Just increment the scan version of the settings to make the next scan rescan everything + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1272,6 +1279,7 @@ FROM tracklist)"); { 87, migrateFromV87 }, { 88, migrateFromV88 }, { 89, migrateFromV89 }, + { 90, migrateFromV90 }, }; bool migrationPerformed{}; From df8dd91c4d0731248e177ae0134bdd0fa51b8f7e Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 9 May 2025 12:51:23 +0200 Subject: [PATCH 23/51] Centralized artwork lookup logic --- src/libs/database/impl/Artist.cpp | 5 + src/libs/database/impl/Release.cpp | 5 + src/libs/database/include/database/Artist.hpp | 2 + .../database/include/database/Release.hpp | 2 + .../services/artwork/impl/ArtworkService.cpp | 82 ++++++++++++++ .../services/artwork/impl/ArtworkService.hpp | 4 + .../services/artwork/IArtworkService.hpp | 11 ++ src/libs/subsonic/impl/responses/Album.cpp | 30 +++-- src/libs/subsonic/impl/responses/Artist.cpp | 15 ++- src/libs/subsonic/impl/responses/Song.cpp | 25 ++--- src/lms/ui/resource/ArtworkResource.cpp | 106 +++++------------- 11 files changed, 173 insertions(+), 114 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index ee61e067..d7906600 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -320,6 +320,11 @@ AND NOT EXISTS ( return ObjectPtr{ _image }; } + ImageId Artist::getImageId() const + { + return _image.id(); + } + RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const { assert(session()); diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 4d367881..d14174b8 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -706,6 +706,11 @@ namespace lms::db return ObjectPtr{ _image }; } + ImageId Release::getImageId() const + { + return _image.id(); + } + void Release::clearLabels() { _labels.clear(); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index d2b24cdf..39ee300d 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -33,6 +33,7 @@ #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" #include "database/Filters.hpp" +#include "database/ImageId.hpp" #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" #include "database/ReleaseId.hpp" @@ -137,6 +138,7 @@ namespace lms::db std::optional getMBID() const; bool hasMBID() const; ObjectPtr getImage() const; + ImageId getImageId() const; void visitLinks(std::function& link)> visitor) const; // No artistLinkTypes means get them all diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 885b9672..03ed0b8f 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -34,6 +34,7 @@ #include "database/CountryId.hpp" #include "database/DirectoryId.hpp" #include "database/Filters.hpp" +#include "database/ImageId.hpp" #include "database/LabelId.hpp" #include "database/MediaLibraryId.hpp" #include "database/Object.hpp" @@ -292,6 +293,7 @@ namespace lms::db std::string_view getBarcode() const { return _barcode; } std::string_view getComment() const { return _comment; } ObjectPtr getImage() const; + ImageId getImageId() const; // Setters void setName(std::string_view name) { _name = name; } diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index a1b83427..5d34e9d2 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -25,10 +25,13 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Utils.hpp" +#include "database/Artist.hpp" #include "database/Db.hpp" #include "database/Image.hpp" +#include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" +#include "database/TrackEmbeddedImage.hpp" #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" @@ -178,6 +181,85 @@ namespace lms::cover return {}; } + ArtworkService::ImageFindResult ArtworkService::findArtistImage(db::ArtistId artistId) + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + ImageFindResult res; + + if (const db::Artist::pointer artist{ db::Artist::find(session, artistId) }) + { + if (const db::ImageId imageId{ artist->getImageId() }; imageId.isValid()) + res = imageId; + + // TODO fallback on first release? + } + + return res; + } + + ArtworkService::ImageFindResult ArtworkService::findPreferredTrackImage(db::TrackId trackId) + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + ImageFindResult res; + + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(trackId); + params.setIsPreferred(true); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { + res = image->getId(); + }); + + if (res.index() == 0) + { + if (db::Track::pointer track{ db::Track::find(session, trackId) }) + { + if (const db::Release::pointer release{ track->getRelease() }) + { + if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid()) + res = imageId; + } + } + } + + return res; + } + + ArtworkService::ImageFindResult ArtworkService::findReleaseImage(db::ReleaseId releaseId) + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + ImageFindResult res; + + const db::Release::pointer release{ db::Release::find(session, releaseId) }; + if (release) + { + if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid()) + { + res = imageId; + } + else + { + db::TrackEmbeddedImage::FindParameters params; + params.setRelease(releaseId); + params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { + res = image->getId(); + }); + } + } + + return res; + } + std::shared_ptr ArtworkService::getImage(db::ImageId imageId, std::optional width) { const ImageCache::EntryDesc cacheEntryDesc{ imageId, width }; diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index addfdca5..91b80177 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -47,6 +47,10 @@ namespace lms::cover ArtworkService& operator=(const ArtworkService&) = delete; private: + ImageFindResult findArtistImage(db::ArtistId artistId) override; + ImageFindResult findPreferredTrackImage(db::TrackId trackId) override; + ImageFindResult findReleaseImage(db::ReleaseId releaseId) override; + std::shared_ptr getImage(db::ImageId imageId, std::optional width) override; std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) override; diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index 5da77f4a..d9dbb161 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -22,9 +22,13 @@ #include #include #include +#include +#include "database/ArtistId.hpp" #include "database/ImageId.hpp" +#include "database/ReleaseId.hpp" #include "database/TrackEmbeddedImageId.hpp" +#include "database/TrackId.hpp" #include "image/IEncodedImage.hpp" namespace lms::db @@ -39,6 +43,13 @@ namespace lms::cover public: virtual ~IArtworkService() = default; + // Helpers to find artworks + using ImageFindResult = std::variant; + virtual ImageFindResult findArtistImage(db::ArtistId artistId) = 0; + virtual ImageFindResult findPreferredTrackImage(db::TrackId trackId) = 0; + virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0; + + // Image retrieval virtual std::shared_ptr getImage(db::ImageId imageId, std::optional width) = 0; virtual std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) = 0; diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index cec71030..2ee75e42 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -28,9 +28,9 @@ #include "database/Image.hpp" #include "database/Release.hpp" #include "database/Track.hpp" -#include "database/TrackEmbeddedImage.hpp" #include "database/Types.hpp" #include "database/User.hpp" +#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -87,23 +87,19 @@ namespace lms::api::subsonic } albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime())); - if (const auto image{ release->getImage() }) - { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; - albumNode.setAttribute("coverArt", idToString(coverArtId)); - } - else - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(release->getId()); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); - params.setRange(db::Range{ 0, 1 }); - db::TrackEmbeddedImage::find(context.dbSession, params, [&](const db::TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - albumNode.setAttribute("coverArt", idToString(coverArtId)); - }); + { + const auto imageResult{ core::Service::get()->findReleaseImage(release->getId()) }; + if (const db::ImageId * imageId{ std::get_if(&imageResult) }) + { + if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) + { + const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; + albumNode.setAttribute("coverArt", idToString(coverArtId)); + } + } + else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) + albumNode.setAttribute("coverArt", idToString(*embeddedImageId)); } if (const auto originalYear{ release->getOriginalYear() }) albumNode.setAttribute("year", *originalYear); diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 632e3548..053b7dcc 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -27,6 +27,7 @@ #include "database/Release.hpp" #include "database/TrackArtistLink.hpp" #include "database/User.hpp" +#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "CoverArtId.hpp" @@ -95,10 +96,18 @@ namespace lms::api::subsonic artistNode.setAttribute("id", idToString(artist->getId())); artistNode.setAttribute("name", artist->getName()); - if (const auto image{ artist->getImage() }) { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; - artistNode.setAttribute("coverArt", idToString(coverArtId)); + const auto imageResult{ core::Service::get()->findArtistImage(artist->getId()) }; + if (const db::ImageId * imageId{ std::get_if(&imageResult) }) + { + if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) + { + const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; + artistNode.setAttribute("coverArt", idToString(coverArtId)); + } + } + else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) + artistNode.setAttribute("coverArt", idToString(*embeddedImageId)); } const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) }; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 830a11a5..876721b4 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -33,9 +33,9 @@ #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackArtistLink.hpp" -#include "database/TrackEmbeddedImage.hpp" #include "database/Types.hpp" #include "database/User.hpp" +#include "services/artwork/IArtworkService.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -111,28 +111,18 @@ namespace lms::api::subsonic trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } - const Release::pointer release{ track->getRelease() }; - { - TrackEmbeddedImage::FindParameters params; - params.setTrack(track->getId()); - params.setIsPreferred(true); - params.setRange(Range{ .offset = 0, .size = 1 }); - - bool hasEmbeddedImage{}; - TrackEmbeddedImage::find(context.dbSession, params, [&](const TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - }); - - if (!hasEmbeddedImage && release) + const auto imageResult{ core::Service::get()->findPreferredTrackImage(track->getId()) }; + if (const db::ImageId * imageId{ std::get_if(&imageResult) }) { - if (const db::Image::pointer image{ release->getImage() }) + if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) { - const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() }; + const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; trackResponse.setAttribute("coverArt", idToString(coverArtId)); } } + else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) + trackResponse.setAttribute("coverArt", idToString(*embeddedImageId)); } const std::vector& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; @@ -147,6 +137,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); } + const Release::pointer release{ track->getRelease() }; if (release) { trackResponse.setAttribute("album", release->getName()); diff --git a/src/lms/ui/resource/ArtworkResource.cpp b/src/lms/ui/resource/ArtworkResource.cpp index b190b819..8cb14e9a 100644 --- a/src/lms/ui/resource/ArtworkResource.cpp +++ b/src/lms/ui/resource/ArtworkResource.cpp @@ -26,13 +26,6 @@ #include "core/ITraceLogger.hpp" #include "core/Service.hpp" #include "core/String.hpp" -#include "database/Artist.hpp" -#include "database/Image.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackEmbeddedImage.hpp" -#include "database/Types.hpp" #include "services/artwork/IArtworkService.hpp" #include "LmsApplication.hpp" @@ -58,19 +51,15 @@ namespace lms::ui { std::string url; - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const db::Artist::pointer artist{ db::Artist::find(LmsApp->getDbSession(), artistId) }; - if (artist) - { - if (const db::Image::pointer image{ artist->getImage() }) - url = getImageUrl(image->getId(), size, "artist"); - } - } - - if (url.empty()) - url = getDefaultArtistImageUrl(); + const auto imageResult{ core::Service::get()->findArtistImage(artistId) }; + std::visit([&](const auto& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + url = getDefaultArtistImageUrl(); + else + url = getImageUrl(arg, size, "artist"); + }, + imageResult); return url; } @@ -79,33 +68,15 @@ namespace lms::ui { std::string url; - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) }; - if (release) - { - if (const db::Image::pointer image{ release->getImage() }) - { - url = getImageUrl(image->getId(), size, "release"); - } - else - { - db::TrackEmbeddedImage::FindParameters params; - params.setRelease(releaseId); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); - params.setRange(db::Range{ 0, 1 }); - - db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) { - url = getImageUrl(image->getId(), size, "release"); - }); - } - } - } - - if (url.empty()) - url = getDefaultReleaseCoverUrl(); + const auto imageResult{ core::Service::get()->findReleaseImage(releaseId) }; + std::visit([&](const auto& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + url = getDefaultReleaseCoverUrl(); + else + url = getImageUrl(arg, size, "release"); + }, + imageResult); return url; } @@ -114,34 +85,15 @@ namespace lms::ui { std::string url; - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - db::TrackEmbeddedImage::FindParameters params; - params.setTrack(trackId); - params.setIsPreferred(true); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) { - url = getImageUrl(image->getId(), size, "release"); - }); - - if (url.empty()) - { - db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) }; - if (track) - { - if (const db::Release::pointer release{ track->getRelease() }) - { - if (const db::Image::pointer image{ release->getImage() }) - url = getImageUrl(image->getId(), size, "release"); - } - } - } - } - - if (url.empty()) - url = getDefaultReleaseCoverUrl(); + const auto imageResult{ core::Service::get()->findPreferredTrackImage(trackId) }; + std::visit([&](const auto& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + url = getDefaultReleaseCoverUrl(); + else + url = getImageUrl(arg, size, "release"); + }, + imageResult); return url; } @@ -155,9 +107,9 @@ namespace lms::ui return res; } - std::string ArtworkResource::getImageUrl(db::TrackEmbeddedImageId trackId, std::optional size, std::string_view type) const + std::string ArtworkResource::getImageUrl(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional size, std::string_view type) const { - std::string res{ url() + "&trimageid=" + trackId.toString() + "&type=" + std::string{ type } }; + std::string res{ url() + "&trimageid=" + trackEmbeddedImageId.toString() + "&type=" + std::string{ type } }; if (size) res += "&size=" + std::to_string(static_cast(*size)); return res; From d1314ddf0da38f1aec613dac24f29eb315f49776 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 9 May 2025 13:25:17 +0200 Subject: [PATCH 24/51] Centralized artwork lookup logic for playlists --- .../services/artwork/impl/ArtworkService.cpp | 20 +++++++++++++ .../services/artwork/impl/ArtworkService.hpp | 1 + .../services/artwork/IArtworkService.hpp | 2 ++ .../subsonic/impl/endpoints/Playlists.cpp | 6 ++-- src/libs/subsonic/impl/responses/Playlist.cpp | 30 +++++++++++-------- src/libs/subsonic/impl/responses/Playlist.hpp | 7 +++-- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 5d34e9d2..a65a19f6 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -260,6 +260,26 @@ namespace lms::cover return res; } + ArtworkService::ImageFindResult ArtworkService::findTrackListImage(db::TrackListId trackListId) + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + ImageFindResult res; + + db::TrackEmbeddedImage::FindParameters params; + params.setTrackList(trackListId); + params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { + res = image->getId(); + }); + + return res; + } + std::shared_ptr ArtworkService::getImage(db::ImageId imageId, std::optional width) { const ImageCache::EntryDesc cacheEntryDesc{ imageId, width }; diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index 91b80177..e1b77906 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -50,6 +50,7 @@ namespace lms::cover ImageFindResult findArtistImage(db::ArtistId artistId) override; ImageFindResult findPreferredTrackImage(db::TrackId trackId) override; ImageFindResult findReleaseImage(db::ReleaseId releaseId) override; + ImageFindResult findTrackListImage(db::TrackListId trackListId) override; std::shared_ptr getImage(db::ImageId imageId, std::optional width) override; std::shared_ptr getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional width) override; diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index d9dbb161..259474bc 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -29,6 +29,7 @@ #include "database/ReleaseId.hpp" #include "database/TrackEmbeddedImageId.hpp" #include "database/TrackId.hpp" +#include "database/TrackListId.hpp" #include "image/IEncodedImage.hpp" namespace lms::db @@ -48,6 +49,7 @@ namespace lms::cover virtual ImageFindResult findArtistImage(db::ArtistId artistId) = 0; virtual ImageFindResult findPreferredTrackImage(db::TrackId trackId) = 0; virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0; + virtual ImageFindResult findTrackListImage(db::TrackListId trackListId) = 0; // Image retrieval virtual std::shared_ptr getImage(db::ImageId imageId, std::optional width) = 0; diff --git a/src/libs/subsonic/impl/endpoints/Playlists.cpp b/src/libs/subsonic/impl/endpoints/Playlists.cpp index 5224b988..a3d2e4bb 100644 --- a/src/libs/subsonic/impl/endpoints/Playlists.cpp +++ b/src/libs/subsonic/impl/endpoints/Playlists.cpp @@ -59,7 +59,7 @@ namespace lms::api::subsonic Response::Node& playlistsNode{ response.createNode("playlists") }; auto addTrackList{ [&](const db::TrackList::pointer& trackList) { - playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession)); + playlistsNode.addArrayChild("playlist", createPlaylistNode(context, trackList)); } }; // First add user's playlists @@ -104,7 +104,7 @@ namespace lms::api::subsonic throw RequestedDataNotFoundError{}; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) }; + Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) @@ -157,7 +157,7 @@ namespace lms::api::subsonic } Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) }; + Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; for (const TrackListEntry::pointer& entry : entries.results) diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index e6a01ff4..4b5e54cb 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -19,17 +19,20 @@ #include "Playlist.hpp" +#include "core/Service.hpp" #include "core/String.hpp" -#include "database/TrackEmbeddedImage.hpp" +#include "database/Image.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" +#include "services/artwork/IArtworkService.hpp" #include "CoverArtId.hpp" +#include "RequestContext.hpp" #include "SubsonicId.hpp" namespace lms::api::subsonic { - Response::Node createPlaylistNode(const db::TrackList::pointer& tracklist, db::Session& session) + Response::Node createPlaylistNode(RequestContext& context, const db::TrackList::pointer& tracklist) { Response::Node playlistNode; @@ -43,16 +46,19 @@ namespace lms::api::subsonic if (const db::User::pointer user{ tracklist->getUser() }) playlistNode.setAttribute("owner", user->getLoginName()); - db::TrackEmbeddedImage::FindParameters params; - params.setTrackList(tracklist->getId()); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); - params.setRange(db::Range{ .offset = 0, .size = 1 }); - - db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { - const CoverArtId coverArtId{ image->getId() }; - playlistNode.setAttribute("coverArt", idToString(coverArtId)); - }); + { + const auto imageResult{ core::Service::get()->findTrackListImage(tracklist->getId()) }; + if (const db::ImageId * imageId{ std::get_if(&imageResult) }) + { + if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) + { + const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() }; + playlistNode.setAttribute("coverArt", idToString(coverArtId)); + } + } + else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if(&imageResult) }) + playlistNode.setAttribute("coverArt", idToString(*embeddedImageId)); + } return playlistNode; } diff --git a/src/libs/subsonic/impl/responses/Playlist.hpp b/src/libs/subsonic/impl/responses/Playlist.hpp index 07246674..6c6c8e00 100644 --- a/src/libs/subsonic/impl/responses/Playlist.hpp +++ b/src/libs/subsonic/impl/responses/Playlist.hpp @@ -26,10 +26,11 @@ namespace lms::db { class TrackList; - class Session; } // namespace lms::db namespace lms::api::subsonic { - Response::Node createPlaylistNode(const db::ObjectPtr& tracklist, db::Session& session); -} \ No newline at end of file + struct RequestContext; + + Response::Node createPlaylistNode(RequestContext& context, const db::ObjectPtr& tracklist); +} // namespace lms::api::subsonic \ No newline at end of file From d5aead9daf66aaa8c96817f9b2fe842430e1ed1d Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 10 May 2025 13:31:27 +0200 Subject: [PATCH 25/51] Now prefer 'disc' artworks for tracks, ref #604 --- src/libs/database/impl/Migration.cpp | 8 +- src/libs/database/impl/Session.cpp | 1 - src/libs/database/impl/TrackEmbeddedImage.cpp | 16 +-- .../database/impl/TrackEmbeddedImageLink.cpp | 8 ++ .../include/database/TrackEmbeddedImage.hpp | 6 -- .../database/TrackEmbeddedImageLink.hpp | 6 +- src/libs/database/include/database/Types.hpp | 3 +- src/libs/database/test/TrackEmbeddedImage.cpp | 79 ++++++++++++-- .../services/artwork/impl/ArtworkService.cpp | 100 +++++------------- .../services/artwork/impl/ArtworkService.hpp | 6 +- .../services/artwork/IArtworkService.hpp | 6 +- .../impl/scanners/AudioFileScanOperation.cpp | 12 --- src/libs/subsonic/impl/responses/Song.cpp | 2 +- src/lms/ui/MediaPlayer.cpp | 4 +- src/lms/ui/Utils.cpp | 2 +- src/lms/ui/resource/ArtworkResource.cpp | 4 +- src/lms/ui/resource/ArtworkResource.hpp | 2 +- 17 files changed, 136 insertions(+), 129 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 47fbbde9..529a802c 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 91 }; + static constexpr Version LMS_DATABASE_VERSION{ 92 }; } VersionInfo::VersionInfo() @@ -1213,6 +1213,11 @@ FROM tracklist)"); utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1"); } + void migrateFromV91(Session& session) + { + utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1280,6 +1285,7 @@ FROM tracklist)"); { 88, migrateFromV88 }, { 89, migrateFromV89 }, { 90, migrateFromV90 }, + { 91, migrateFromV91 } }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 0511fb97..28a47531 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -229,7 +229,6 @@ namespace lms::db utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)"); - utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)"); diff --git a/src/libs/database/impl/TrackEmbeddedImage.cpp b/src/libs/database/impl/TrackEmbeddedImage.cpp index c802d4b8..0bf1df18 100644 --- a/src/libs/database/impl/TrackEmbeddedImage.cpp +++ b/src/libs/database/impl/TrackEmbeddedImage.cpp @@ -38,17 +38,14 @@ namespace lms::db auto query{ session.getDboSession()->query>("SELECT t_e_i FROM track_embedded_image t_e_i") }; - if (params.isPreferred - || params.track.isValid() + if (params.track.isValid() || params.release.isValid() || params.trackList.isValid() - || params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize) + || params.sortMethod == TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize + || params.sortMethod == TrackEmbeddedImageSortMethod::FrontTypeThenSize) { query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id"); - if (params.isPreferred) - query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value()); - if (params.track.isValid()) query.where("t_e_i_l.track_id = ?").bind(params.track); @@ -69,8 +66,11 @@ namespace lms::db { case TrackEmbeddedImageSortMethod::None: break; - case TrackEmbeddedImageSortMethod::FrontCoverAndSize: - query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover); + case TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize: + query.orderBy("CASE t_e_i_l.type WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END, t_e_i.size DESC").bind(ImageType::Media).bind(ImageType::FrontCover); + break; + case TrackEmbeddedImageSortMethod::FrontTypeThenSize: + query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size DESC").bind(ImageType::FrontCover); break; } diff --git a/src/libs/database/impl/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/TrackEmbeddedImageLink.cpp index cecc6f2d..300d2ed3 100644 --- a/src/libs/database/impl/TrackEmbeddedImageLink.cpp +++ b/src/libs/database/impl/TrackEmbeddedImageLink.cpp @@ -54,6 +54,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } + void TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function visitor) + { + auto query{ session.getDboSession()->find() }; + query.where("track_embedded_image_id = ?").bind(trackEmbeddedImageId); + + return utils::forEachQueryResult(query, visitor); + } + ObjectPtr TrackEmbeddedImageLink::getTrack() const { return _track; diff --git a/src/libs/database/include/database/TrackEmbeddedImage.hpp b/src/libs/database/include/database/TrackEmbeddedImage.hpp index b90ed6fc..a5efb896 100644 --- a/src/libs/database/include/database/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/TrackEmbeddedImage.hpp @@ -49,7 +49,6 @@ namespace lms::db TrackId track; ReleaseId release; TrackListId trackList; - std::optional isPreferred; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; FindParameters& setRange(std::optional _range) @@ -72,11 +71,6 @@ namespace lms::db trackList = _trackList; return *this; } - FindParameters& setIsPreferred(std::optional _isPreferred) - { - isPreferred = _isPreferred; - return *this; - } FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod) { sortMethod = _sortMethod; diff --git a/src/libs/database/include/database/TrackEmbeddedImageLink.hpp b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp index e8b66534..77dd0d2e 100644 --- a/src/libs/database/include/database/TrackEmbeddedImageLink.hpp +++ b/src/libs/database/include/database/TrackEmbeddedImageLink.hpp @@ -25,6 +25,7 @@ #include #include "database/Object.hpp" +#include "database/TrackEmbeddedImageId.hpp" #include "database/TrackEmbeddedImageLinkId.hpp" #include "database/Types.hpp" @@ -44,18 +45,17 @@ namespace lms::db // find static std::size_t getCount(Session& session); static pointer find(Session& session, TrackEmbeddedImageLinkId id); + static void find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function visitor); // getters ObjectPtr getTrack() const; ObjectPtr getImage() const; std::size_t getIndex() const { return _index; } - bool isPreferred() const { return _isPreferred; } ImageType getType() const { return _type; } std::string_view getDescription() const { return _description; } // setters void setIndex(std::size_t index) { _index = static_cast(index); } - void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; } void setType(ImageType type) { _type = type; } void setDescription(std::string_view description) { _description = description; } @@ -63,7 +63,6 @@ namespace lms::db void persist(Action& a) { Wt::Dbo::field(a, _index, "index"); - Wt::Dbo::field(a, _isPreferred, "is_preferred"); Wt::Dbo::field(a, _type, "type"); Wt::Dbo::field(a, _description, "description"); @@ -78,7 +77,6 @@ namespace lms::db static pointer create(Session& session, ObjectPtr track, ObjectPtr image); int _index{}; // index within the track - bool _isPreferred{}; ImageType _type{ ImageType::Unknown }; std::string _description; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 043ade8e..a126c12e 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -171,7 +171,8 @@ namespace lms::db enum class TrackEmbeddedImageSortMethod { None, - FrontCoverAndSize, + MediaTypeThenFrontTypeThenSize, + FrontTypeThenSize, }; enum class TrackListSortMethod diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp index 3ba25be0..0a06fd24 100644 --- a/src/libs/database/test/TrackEmbeddedImage.cpp +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -118,23 +118,22 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); - EXPECT_FALSE(visited); + EXPECT_TRUE(visited); } { auto transaction{ session.createWriteTransaction() }; - link.get().modify()->setIsPreferred(true); + link.get().modify()->setType(ImageType::FrontCover); } { auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -145,8 +144,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); params.setRelease(release.getId()); + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -162,9 +161,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); params.setRelease(release.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -175,9 +173,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setIsPreferred(true); params.setTrack(track.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -185,6 +182,55 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts) + { + ScopedTrackEmbeddedImage image1{ session }; + ScopedTrackEmbeddedImage image2{ session }; + ScopedTrackEmbeddedImage image3{ session }; + ScopedTrack track{ session }; + ScopedRelease release{ session, "MyRelease" }; + ScopedTrackEmbeddedImageLink link1{ session, track.lockAndGet(), image1.lockAndGet() }; + ScopedTrackEmbeddedImageLink link2{ session, track.lockAndGet(), image2.lockAndGet() }; + ScopedTrackEmbeddedImageLink link3{ session, track.lockAndGet(), image3.lockAndGet() }; + + { + auto transaction{ session.createWriteTransaction() }; + link1.get().modify()->setType(ImageType::FrontCover); + link2.get().modify()->setType(ImageType::Media); + image2.get().modify()->setSize(1000); + link3.get().modify()->setType(ImageType::Media); + image3.get().modify()->setSize(2000); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image1.getId()); + EXPECT_EQ(visitedIds[1], image3.getId()); + EXPECT_EQ(visitedIds[2], image2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image3.getId()); + EXPECT_EQ(visitedIds[1], image2.getId()); + EXPECT_EQ(visitedIds[2], image1.getId()); + } + } + TEST_F(DatabaseFixture, Track_findByEmbeddedImage) { ScopedTrackEmbeddedImage image{ session }; @@ -279,5 +325,20 @@ namespace lms::db::tests EXPECT_EQ(img->getType(), ImageType::FrontCover); EXPECT_EQ(img->getDescription(), "MyDesc"); } + + { + auto transaction{ session.createReadTransaction() }; + + bool visited{}; + TrackEmbeddedImageLink::find(session, image->getId(), [&](const TrackEmbeddedImageLink::pointer& link) { + EXPECT_EQ(link->getIndex(), 2); + EXPECT_EQ(link->getType(), ImageType::FrontCover); + EXPECT_EQ(link->getDescription(), "MyDesc"); + EXPECT_EQ(link->getTrack(), track.get()); + + visited = true; + }); + EXPECT_TRUE(visited); + } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index a65a19f6..4c4da83d 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -19,9 +19,6 @@ #include "ArtworkService.hpp" -#include -#include - #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Utils.hpp" @@ -32,6 +29,8 @@ #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackEmbeddedImage.hpp" +#include "database/TrackEmbeddedImageLink.hpp" +#include "database/Types.hpp" #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" @@ -39,14 +38,6 @@ namespace lms::cover { - namespace - { - bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions) - { - return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); - } - } // namespace - std::unique_ptr createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath) { return std::make_unique(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath); @@ -105,49 +96,18 @@ namespace lms::cover return _defaultArtistImage; } - bool ArtworkService::checkImageFile(const std::filesystem::path& filePath) + std::unique_ptr ArtworkService::getTrackImage(const std::filesystem::path& p, std::size_t index, std::optional width) const { - std::error_code ec; - - if (!isFileSupported(filePath, _fileExtensions)) - return false; - - if (!std::filesystem::exists(filePath, ec)) - return false; - - if (!std::filesystem::is_regular_file(filePath, ec)) - return false; - - return true; - } - - std::unique_ptr ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional width) const - { - struct CandidateImage - { - std::unique_ptr image; - bool isFront{}; - std::size_t index; - - // > means is better candidate - bool operator>(const CandidateImage& other) const - { - if (!isFront && other.isFront) - return false; - if (isFront && !other.isFront) - return true; - - return index < other.index; - } - }; - - std::vector candidateImages; - std::size_t pictureIndex{}; + std::unique_ptr image; try { + std::size_t currentIndex{}; + _audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) { - std::unique_ptr image; + if (currentIndex++ != index) + return; + try { if (!width) @@ -165,8 +125,6 @@ namespace lms::cover { LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what()); } - - candidateImages.emplace_back(CandidateImage{ .image = std::move(image), .isFront = parsedImage.type == metadata::Image::Type::FrontCover, .index = pictureIndex++ }); }); } catch (const metadata::Exception& e) @@ -174,11 +132,7 @@ namespace lms::cover LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what()); } - std::stable_sort(std::begin(candidateImages), std::end(candidateImages), std::greater<>()); - if (!candidateImages.empty()) - return std::move(candidateImages.front().image); - - return {}; + return image; } ArtworkService::ImageFindResult ArtworkService::findArtistImage(db::ArtistId artistId) @@ -199,7 +153,7 @@ namespace lms::cover return res; } - ArtworkService::ImageFindResult ArtworkService::findPreferredTrackImage(db::TrackId trackId) + ArtworkService::ImageFindResult ArtworkService::findTrackImage(db::TrackId trackId) { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; @@ -207,16 +161,17 @@ namespace lms::cover db::TrackEmbeddedImage::FindParameters params; params.setTrack(trackId); - params.setIsPreferred(true); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); }); + // No embedded image found, fallback on release image if (res.index() == 0) { - if (db::Track::pointer track{ db::Track::find(session, trackId) }) + if (const db::Track::pointer track{ db::Track::find(session, trackId) }) { if (const db::Release::pointer release{ track->getRelease() }) { @@ -247,8 +202,7 @@ namespace lms::cover { db::TrackEmbeddedImage::FindParameters params; params.setRelease(releaseId); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontTypeThenSize); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { @@ -269,8 +223,7 @@ namespace lms::cover db::TrackEmbeddedImage::FindParameters params; params.setTrackList(trackListId); - params.setIsPreferred(true); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { @@ -309,27 +262,24 @@ namespace lms::cover { const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width }; - std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; - if (cover) - return cover; + std::shared_ptr image{ _cache.getImage(cacheEntryDesc) }; + if (image) + return image; - std::filesystem::path trackFile; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - db::Track::FindParameters params; - params.setEmbeddedImage(trackEmbeddedImageId); - db::Track::find(session, params, [&](const db::Track::pointer& track) { - if (!cover) - cover = getTrackImage(track->getAbsoluteFilePath(), width); + db::TrackEmbeddedImageLink::find(session, trackEmbeddedImageId, [&](const db::TrackEmbeddedImageLink::pointer& link) { + if (!image) + image = getTrackImage(link->getTrack()->getAbsoluteFilePath(), link->getIndex(), width); }); } - if (cover) - _cache.addImage(cacheEntryDesc, cover); + if (image) + _cache.addImage(cacheEntryDesc, image); - return cover; + return image; } void ArtworkService::flushCache() diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index e1b77906..9d90d1a7 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -48,7 +48,7 @@ namespace lms::cover private: ImageFindResult findArtistImage(db::ArtistId artistId) override; - ImageFindResult findPreferredTrackImage(db::TrackId trackId) override; + ImageFindResult findTrackImage(db::TrackId trackId) override; ImageFindResult findReleaseImage(db::ReleaseId releaseId) override; ImageFindResult findTrackListImage(db::TrackListId trackListId) override; @@ -62,9 +62,7 @@ namespace lms::cover void setJpegQuality(unsigned quality) override; std::unique_ptr getFromImageFile(const std::filesystem::path& p, std::optional width) const; - std::unique_ptr getTrackImage(const std::filesystem::path& path, std::optional width) const; - - static bool checkImageFile(const std::filesystem::path& filePath); + std::unique_ptr getTrackImage(const std::filesystem::path& path, std::size_t index, std::optional width) const; db::Db& _db; diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index 259474bc..90a8319c 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -47,7 +47,11 @@ namespace lms::cover // Helpers to find artworks using ImageFindResult = std::variant; virtual ImageFindResult findArtistImage(db::ArtistId artistId) = 0; - virtual ImageFindResult findPreferredTrackImage(db::TrackId trackId) = 0; + + // Will get Disc/Media artwork if available, otherwise, will fallback on release artwork + virtual ImageFindResult findTrackImage(db::TrackId trackId) = 0; + + // Will get Release if available, otherwise, will fallback on embedded artworks virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0; virtual ImageFindResult findTrackListImage(db::TrackListId trackListId) = 0; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp index 8a33e817..e5fcf43b 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp @@ -350,24 +350,12 @@ namespace lms::scanner void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span images) { - db::TrackEmbeddedImageLink::pointer preferredImageLink; - track.modify()->clearEmbeddedImageLinks(); for (const ImageInfo& imageInfo : images) { db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) }; track.modify()->addEmbeddedImageLink(link); - - if (!preferredImageLink - || (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover) - || (preferredImageLink->getImage()->getSize() < link->getImage()->getSize())) - { - preferredImageLink = link; - } } - - if (preferredImageLink) - preferredImageLink.modify()->setIsPreferred(true); } db::Advisory getAdvisory(std::optional advisory) diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 876721b4..cdf55651 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -112,7 +112,7 @@ namespace lms::api::subsonic } { - const auto imageResult{ core::Service::get()->findPreferredTrackImage(track->getId()) }; + const auto imageResult{ core::Service::get()->findTrackImage(track->getId()) }; if (const db::ImageId * imageId{ std::get_if(&imageResult) }) { if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) }) diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 22f4b984..87a76c35 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -253,8 +253,8 @@ namespace lms::ui << " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," << " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\"," << " artwork: [" - << " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," - << " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," + << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" }," + << " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" }," << " ]" << "};"; // Update 'sizes' above to match this: diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 0cbfe325..858c533f 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -97,7 +97,7 @@ namespace lms::ui::utils std::unique_ptr createTrackImage(db::TrackId trackId, ArtworkResource::Size size) { auto cover{ std::make_unique() }; - cover->setImageLink(LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, size)); + cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size)); cover->setStyleClass("Lms-cover img-fluid"); // HACK cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK diff --git a/src/lms/ui/resource/ArtworkResource.cpp b/src/lms/ui/resource/ArtworkResource.cpp index 8cb14e9a..424824a4 100644 --- a/src/lms/ui/resource/ArtworkResource.cpp +++ b/src/lms/ui/resource/ArtworkResource.cpp @@ -81,11 +81,11 @@ namespace lms::ui return url; } - std::string ArtworkResource::getPreferredTrackImageUrl(db::TrackId trackId, std::optional size) const + std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, std::optional size) const { std::string url; - const auto imageResult{ core::Service::get()->findPreferredTrackImage(trackId) }; + const auto imageResult{ core::Service::get()->findTrackImage(trackId) }; std::visit([&](const auto& arg) { using T = std::decay_t; if constexpr (std::is_same_v) diff --git a/src/lms/ui/resource/ArtworkResource.hpp b/src/lms/ui/resource/ArtworkResource.hpp index d54ddfa3..dda8b5e1 100644 --- a/src/lms/ui/resource/ArtworkResource.hpp +++ b/src/lms/ui/resource/ArtworkResource.hpp @@ -47,7 +47,7 @@ namespace lms::ui std::string getArtistImageUrl(db::ArtistId artistId, std::optional size = std::nullopt) const; std::string getReleaseCoverUrl(db::ReleaseId releaseId, std::optional size = std::nullopt) const; - std::string getPreferredTrackImageUrl(db::TrackId trackId, std::optional size = std::nullopt) const; + std::string getTrackImageUrl(db::TrackId trackId, std::optional size = std::nullopt) const; private: std::string getImageUrl(db::ImageId imageId, std::optional size, std::string_view type) const; From 4b3e8032cbcbb7a8d9bd42c8a2d74bae7dd3882a Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 10 May 2025 13:44:49 +0200 Subject: [PATCH 26/51] Added missing drop index call --- src/libs/database/impl/Migration.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 529a802c..f6ba2b4c 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -1215,6 +1215,7 @@ FROM tracklist)"); void migrateFromV91(Session& session) { + dropIndexes(session); utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred"); } From f906212fc4855cf4a989879409a9f49fd66aeba6 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 10 May 2025 14:04:46 +0200 Subject: [PATCH 27/51] Added a helper to get the disc cover for a given track --- src/libs/database/impl/TrackEmbeddedImage.cpp | 15 ++++-- .../include/database/TrackEmbeddedImage.hpp | 6 +++ src/libs/database/include/database/Types.hpp | 5 +- src/libs/database/test/TrackEmbeddedImage.cpp | 51 ++++++++++++++++--- .../services/artwork/impl/ArtworkService.cpp | 25 +++++++-- .../services/artwork/impl/ArtworkService.hpp | 1 + .../services/artwork/IArtworkService.hpp | 5 +- 7 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/libs/database/impl/TrackEmbeddedImage.cpp b/src/libs/database/impl/TrackEmbeddedImage.cpp index 0bf1df18..c6116b5f 100644 --- a/src/libs/database/impl/TrackEmbeddedImage.cpp +++ b/src/libs/database/impl/TrackEmbeddedImage.cpp @@ -41,8 +41,9 @@ namespace lms::db if (params.track.isValid() || params.release.isValid() || params.trackList.isValid() - || params.sortMethod == TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize - || params.sortMethod == TrackEmbeddedImageSortMethod::FrontTypeThenSize) + || params.imageType.has_value() + || params.sortMethod == TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc + || params.sortMethod == TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc) { query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id"); @@ -60,16 +61,22 @@ namespace lms::db query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id"); query.where("t_l_e.tracklist_id = ?").bind(params.trackList); } + + if (params.imageType.has_value()) + query.where("t_e_i_l.type = ?").bind(params.imageType.value()); } switch (params.sortMethod) { case TrackEmbeddedImageSortMethod::None: break; - case TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize: + case TrackEmbeddedImageSortMethod::SizeDesc: + query.orderBy("t_e_i.size DESC"); + break; + case TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc: query.orderBy("CASE t_e_i_l.type WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END, t_e_i.size DESC").bind(ImageType::Media).bind(ImageType::FrontCover); break; - case TrackEmbeddedImageSortMethod::FrontTypeThenSize: + case TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc: query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size DESC").bind(ImageType::FrontCover); break; } diff --git a/src/libs/database/include/database/TrackEmbeddedImage.hpp b/src/libs/database/include/database/TrackEmbeddedImage.hpp index a5efb896..e323e093 100644 --- a/src/libs/database/include/database/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/TrackEmbeddedImage.hpp @@ -49,6 +49,7 @@ namespace lms::db TrackId track; ReleaseId release; TrackListId trackList; + std::optional imageType; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; FindParameters& setRange(std::optional _range) @@ -71,6 +72,11 @@ namespace lms::db trackList = _trackList; return *this; } + FindParameters& setImageType(std::optional _imageType) + { + imageType = _imageType; + return *this; + } FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod) { sortMethod = _sortMethod; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index a126c12e..74bee666 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -171,8 +171,9 @@ namespace lms::db enum class TrackEmbeddedImageSortMethod { None, - MediaTypeThenFrontTypeThenSize, - FrontTypeThenSize, + SizeDesc, + MediaTypeThenFrontTypeThenSizeDescDesc, + FrontTypeThenSizeDesc, }; enum class TrackListSortMethod diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp index 0a06fd24..c33d9ffb 100644 --- a/src/libs/database/test/TrackEmbeddedImage.cpp +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -133,19 +133,43 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); EXPECT_TRUE(visited); } + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setImageType(ImageType::FrontCover); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_TRUE(visited); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setImageType(ImageType::Media); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_FALSE(visited); + } + { auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; params.setRelease(release.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -162,7 +186,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setRelease(release.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -174,7 +198,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setTrack(track.getId()); - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); bool visited{}; TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); @@ -196,6 +220,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; link1.get().modify()->setType(ImageType::FrontCover); + image1.get().modify()->setSize(750); link2.get().modify()->setType(ImageType::Media); image2.get().modify()->setSize(1000); link3.get().modify()->setType(ImageType::Media); @@ -206,7 +231,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); std::vector visitedIds; TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); @@ -220,7 +245,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; TrackEmbeddedImage::FindParameters params; - params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + params.setSortMethod(TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc); std::vector visitedIds; TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); @@ -229,6 +254,20 @@ namespace lms::db::tests EXPECT_EQ(visitedIds[1], image2.getId()); EXPECT_EQ(visitedIds[2], image1.getId()); } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setSortMethod(TrackEmbeddedImageSortMethod::SizeDesc); + + std::vector visitedIds; + TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); + ASSERT_EQ(visitedIds.size(), 3); + EXPECT_EQ(visitedIds[0], image3.getId()); + EXPECT_EQ(visitedIds[2], image1.getId()); + EXPECT_EQ(visitedIds[1], image2.getId()); + } } TEST_F(DatabaseFixture, Track_findByEmbeddedImage) diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 4c4da83d..006d21d5 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -161,7 +161,7 @@ namespace lms::cover db::TrackEmbeddedImage::FindParameters params; params.setTrack(trackId); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { @@ -184,6 +184,25 @@ namespace lms::cover return res; } + ArtworkService::ImageFindResult ArtworkService::findTrackMediaImage(db::TrackId trackId) + { + db::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + ImageFindResult res; + + db::TrackEmbeddedImage::FindParameters params; + params.setTrack(trackId); + params.setImageType(db::ImageType::Media); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { + res = image->getId(); + }); + + return res; + } + ArtworkService::ImageFindResult ArtworkService::findReleaseImage(db::ReleaseId releaseId) { db::Session& session{ _db.getTLSSession() }; @@ -202,7 +221,7 @@ namespace lms::cover { db::TrackEmbeddedImage::FindParameters params; params.setRelease(releaseId); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontTypeThenSize); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontTypeThenSizeDesc); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { @@ -223,7 +242,7 @@ namespace lms::cover db::TrackEmbeddedImage::FindParameters params; params.setTrackList(trackListId); - params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSize); + params.setSortMethod(db::TrackEmbeddedImageSortMethod::MediaTypeThenFrontTypeThenSizeDescDesc); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index 9d90d1a7..a5dd7a4d 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -49,6 +49,7 @@ namespace lms::cover private: ImageFindResult findArtistImage(db::ArtistId artistId) override; ImageFindResult findTrackImage(db::TrackId trackId) override; + ImageFindResult findTrackMediaImage(db::TrackId trackId) override; ImageFindResult findReleaseImage(db::ReleaseId releaseId) override; ImageFindResult findTrackListImage(db::TrackListId trackListId) override; diff --git a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp index 90a8319c..220bde91 100644 --- a/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp +++ b/src/libs/services/artwork/include/services/artwork/IArtworkService.hpp @@ -51,7 +51,10 @@ namespace lms::cover // Will get Disc/Media artwork if available, otherwise, will fallback on release artwork virtual ImageFindResult findTrackImage(db::TrackId trackId) = 0; - // Will get Release if available, otherwise, will fallback on embedded artworks + // Will get Disc/Media artwork if available, no fallback + virtual ImageFindResult findTrackMediaImage(db::TrackId trackId) = 0; + + // Will get Release if available, otherwise, will fallback on embedded artworks virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0; virtual ImageFindResult findTrackListImage(db::TrackListId trackListId) = 0; From dd57275858ad446f695c4281dcb32f1cb7413739 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 10 May 2025 14:41:59 +0200 Subject: [PATCH 28/51] Added media related image in the release view, if found, ref #604 --- approot/release.xml | 9 ++++++--- src/lms/ui/Utils.cpp | 14 ++++++++++++++ src/lms/ui/Utils.hpp | 2 ++ src/lms/ui/explore/ReleaseView.cpp | 17 ++++++++++++++--- src/lms/ui/resource/ArtworkResource.cpp | 15 +++++++++++++++ src/lms/ui/resource/ArtworkResource.hpp | 1 + 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/approot/release.xml b/approot/release.xml index 0dbbc8ed..caf2b811 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -54,8 +54,11 @@ -
-

${disc-title}

+

+ ${} +
${image class="Lms-cursor-pointer"}
+ ${
} +
${disc-title}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
-

+ ${tracks class="d-grid gap-1 Lms-row-container"}
diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 858c533f..1e8191f4 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -104,6 +104,20 @@ namespace lms::ui::utils return cover; } + std::unique_ptr createTrackMediaImage(db::TrackId trackId, ArtworkResource::Size size) + { + std::string url{ LmsApp->getArtworkResource()->getTrackMediaImageUrl(trackId, size) }; + if (url.empty()) + return nullptr; + + auto cover{ std::make_unique() }; + cover->setImageLink(std::move(url)); + cover->setStyleClass("Lms-cover img-fluid"); // HACK + cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK + + return cover; + } + std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete) { auto res{ std::make_unique(Wt::WString{ canDelete ? " " : "" } + name, Wt::TextFormat::UnsafeXHTML) }; diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 2039db95..c29077e2 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -34,6 +34,7 @@ #include "database/ReleaseId.hpp" #include "database/TrackId.hpp" +#include "database/Types.hpp" #include "resource/ArtworkResource.hpp" namespace lms::db @@ -58,6 +59,7 @@ namespace lms::ui::utils std::unique_ptr createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size); std::unique_ptr createTrackImage(db::TrackId trackId, ArtworkResource::Size size); + std::unique_ptr createTrackMediaImage(db::TrackId trackId, ArtworkResource::Size size); // return nullptr if no image std::unique_ptr createArtistImage(db::ArtistId artistId, ArtworkResource::Size size); std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 8fd13d70..9241d1d0 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -391,13 +391,24 @@ namespace lms::ui // Expect to be called in asc order std::map trackContainers; - auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget* { + auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle, db::TrackId trackId) -> Wt::WContainerWidget* { if (auto it{ trackContainers.find(discNumber) }; it != std::cend(trackContainers)) return it->second; Template* disc{ rootContainer->addNew