+
${image class="shadow-sm"}
${name class="d-block text-center text-truncate text-decoration-none link-secondary"}
diff --git a/approot/messages.xml b/approot/messages.xml
index 1f775e67..d3df21c2 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -74,6 +74,7 @@
Allow merging artists without an MBID to those with one
+
Use an album artwork if no artist image is available
Delimiters to be used for splitting artist tags
Artists to not split using the delimiters (one artist per line)
Daily
@@ -100,9 +101,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 +117,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
@@ -129,6 +134,7 @@
Associating external lyrics: {1}%...
Associating playlist tracks: {1}%...
Associating release images: {1}%...
+
Associating track images: {1}%...
Checking for duplicate files... {1} files
Checking for removed files... {1}%
Compacting database...
diff --git a/approot/messages_es.xml b/approot/messages_es.xml
index 7f47b917..2811c7bd 100644
--- a/approot/messages_es.xml
+++ b/approot/messages_es.xml
@@ -74,6 +74,7 @@
Permitir la fusión de artistas sin MBID con aquellos que sí lo tienen
+
Usar una imagen del álbum si no hay una imagen disponible del artista
Delimitadores usados para separar las etiquetas de los artistas
Artistas que no se deben separar usando los delimitadores (un artista por línea)
Diariamente
@@ -100,9 +101,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 +117,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
@@ -129,6 +134,7 @@
Association des paroles externes: {1}%...
Association des pistes des listes de lectures: {1}%...
Asignando imágenes de los álbumes: {1}%...
+
Asignando imágenes de las pistas: {1}%...
Comprobando ficheros duplicados ... {1} ficheros
Comprobando ficheros eliminados... {1}%
Compactando la base de datos...
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index de9c8039..54a216c9 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -74,6 +74,7 @@
Permettre la fusion des artistes sans MBID avec ceux qui en ont un
+
Utilizza un'immagine dell'album se non è disponibile un'immagine dell'artista
Délimiteurs à utiliser pour séparer les tags d'artistes
Artistes à ne pas séparer en utilisant les délimiteurs (un artiste par ligne)
Tous les jours
@@ -100,9 +101,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 +117,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é
@@ -129,6 +134,7 @@
Association des paroles externes: {1}%...
Association des pistes des listes de lectures: {1}%...
Association des images des albums: {1}%...
+
Association des images des pistes: {1}%...
Vérification des fichiers dupliqués... {1} fichiers
Vérification des fichiers supprimés... {1}%
Compactage de la base de données...
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 6545bb5d..95638714 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -74,6 +74,7 @@
Consentire la fusione degli artisti senza MBID con quelli che ne hanno uno
+
Usa l'immagine dell'album se non è disponibile un'immagine dell'artista
Delimitatori da utilizzare per separare i tag degli artisti
Artisti da non suddividere utilizzando i delimitatori (un artista per riga)
Giornaliera
@@ -100,9 +101,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 +117,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
@@ -129,6 +134,7 @@
Associazione dei testi esterni: {1}%...
Associando brani della playlist: {1}%...
Associando immagini degli album: {1}%...
+
Associando immagini delle tracce: {1}%...
Controllo duplicati... {1} files
Controllo file... {1}%
Compattazione del database...
diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml
index cb1af168..8a117a43 100644
--- a/approot/messages_pl.xml
+++ b/approot/messages_pl.xml
@@ -75,6 +75,7 @@
Pozwól na łączenie artystów bez MBID z tymi, którzy go mają
+
Użyj okładki albumu, jeśli zdjęcie artysty nie jest dostępne
Znaki rozdzielające artystów
Artyści, których nie należy dzielić przy użyciu separatorów (jeden artysta na linię)
Codziennie
@@ -101,9 +102,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 +126,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
@@ -138,6 +143,7 @@
Kojarzenie zewnętrznych tekstów: {1}%...
Kojarzenie utworów z playlisty: {1}%...
Kojarzenie obrazów albumów: {1}%...
+
Kojarzenie obrazów ścieżek: {1}%...
Sprawdzanie duplikatów... {1} plik
Sprawdzanie duplikatów... {1} pliki
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index 7b81cbe3..4825aea7 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -75,6 +75,7 @@
+
每日
@@ -100,9 +101,10 @@
无法获得音轨时间
+
无法解析文件
-无法读取文件
+无法读取文件 ({1})
@@ -114,6 +116,9 @@
最后扫描
不可用
已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项
+
+
+
没有音轨
相同文件哈希值
相同 MBID
diff --git a/approot/release.xml b/approot/release.xml
index 0dbbc8ed..92160133 100644
--- a/approot/release.xml
+++ b/approot/release.xml
@@ -9,7 +9,7 @@
- ${cover class="Lms-cursor-pointer rounded d-block mx-auto"}
+ ${artwork class="rounded d-block mx-auto"}
${name}${}[${comment}]${}
@@ -54,8 +54,11 @@
-
-
${disc-title}
+
+ ${}
+ ${artwork class="Lms-cursor-pointer"}
+ ${}
+
${disc-title}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
@@ -68,7 +71,7 @@
-
+
${tracks class="d-grid gap-1 Lms-row-container"}
diff --git a/conf/lms.conf b/conf/lms.conf
index eaa1921f..1c2dfa12 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -11,7 +11,11 @@ ffmpeg-file = "/usr/bin/ffmpeg";
log-file = "";
access-log-file = "";
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
+# "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application
log-min-severity = "info";
+# Database consistency check to run at startup.
+# Can be "none", "quick", or "full"
+db-integrity-check = "quick";
# Output db queries on stdout
db-show-queries = false;
diff --git a/docroot/css/lms.css b/docroot/css/lms.css
index b9eed06b..23c2e981 100644
--- a/docroot/css/lms.css
+++ b/docroot/css/lms.css
@@ -110,6 +110,15 @@ body {
cursor: pointer;
}
+.Lms-image-artist {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
#lms-mp-progress {
transition: none;
}
@@ -184,6 +193,18 @@ body {
width: 0;
}
+.Lms-responsive-circle {
+ width: 100%;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.Lms-responsive-circle:after {
+ content: "";
+ display: block;
+ padding-bottom: 100%;
+}
+
.Lms-responsive-square {
width: 100%;
}
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/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 4a57b0c2..63087058 100644
--- a/src/libs/av/include/av/IAudioFile.hpp
+++ b/src/libs/av/include/av/IAudioFile.hpp
@@ -101,13 +101,4 @@ 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
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/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt
index fa069fba..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
@@ -31,12 +32,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
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..dfcb532f 100644
--- a/src/libs/core/impl/ChildProcess.cpp
+++ b/src/libs/core/impl/ChildProcess.cpp
@@ -19,8 +19,6 @@
#include "ChildProcess.hpp"
-#include
-#include
#include
#include
#include
@@ -28,14 +26,15 @@
#include
#include
-#include
+#include
+#include
#include
+#include
#include
#include
#include "core/ILogger.hpp"
-#include "core/String.hpp"
namespace lms::core
{
@@ -44,8 +43,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() }
{
}
@@ -63,38 +62,29 @@ 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::scoped_lock lock{ mutex };
int pipefd[2];
// Use 'pipe' instead of 'pipe2', more portable
- if (pipe(pipefd) < 0)
- throw SystemException{ errno, "pipe failed!" };
+ 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{ errno, "fcntl failed to set O_NONBLOCK!" };
-
- if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1)
- throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
-
- if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1)
- throw SystemException{ errno, "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 O_NONBLOCK!" };
#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)
{
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,36 +93,45 @@ 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()));
}
}
}
#endif
- int res{ fork() };
+ const 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
{
- 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 +140,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;
}
@@ -168,7 +167,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 +181,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/FileResourceHandler.cpp b/src/libs/core/impl/FileResourceHandler.cpp
index 130671dc..fd23bdd0 100644
--- a/src/libs/core/impl/FileResourceHandler.cpp
+++ b/src/libs/core/impl/FileResourceHandler.cpp
@@ -19,50 +19,50 @@
#include "FileResourceHandler.hpp"
-#include
+#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)
- : _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());
@@ -75,57 +75,51 @@ namespace lms
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, static_cast<::uint64_t>(_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();
@@ -134,4 +128,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..34beecc4 100644
--- a/src/libs/core/impl/FileResourceHandler.hpp
+++ b/src/libs/core/impl/FileResourceHandler.hpp
@@ -20,12 +20,13 @@
#pragma once
#include
+#include
#include
#include
#include "core/IResourceHandler.hpp"
-namespace lms
+namespace lms::core
{
class FileResourceHandler final : public IResourceHandler
{
@@ -38,9 +39,10 @@ namespace lms
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
\ 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/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/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/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/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt
index 87d95d63..29b07d8b 100644
--- a/src/libs/database/CMakeLists.txt
+++ b/src/libs/database/CMakeLists.txt
@@ -1,6 +1,7 @@
add_library(lmsdatabase STATIC
impl/Artist.cpp
impl/ArtistInfo.cpp
+ impl/Artwork.cpp
impl/AuthToken.cpp
impl/Cluster.cpp
impl/Db.cpp
diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp
index ee61e067..57ff3c50 100644
--- a/src/libs/database/impl/Artist.cpp
+++ b/src/libs/database/impl/Artist.cpp
@@ -21,9 +21,9 @@
#include
#include "core/ILogger.hpp"
+#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
-#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
@@ -315,9 +315,14 @@ AND NOT EXISTS (
return getMBID().has_value();
}
- ObjectPtr Artist::getImage() const
+ ObjectPtr Artist::getPreferredArtwork() const
{
- return ObjectPtr{ _image };
+ return ObjectPtr{ _preferredArtwork };
+ }
+
+ ArtworkId Artist::getPreferredArtworkId() const
+ {
+ return _preferredArtwork.id();
}
RangeResults Artist::findSimilarArtistIds(core::EnumSet artistLinkTypes, std::optional range) const
@@ -416,8 +421,8 @@ AND NOT EXISTS (
LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'");
}
- void Artist::setImage(ObjectPtr image)
+ void Artist::setPreferredArtwork(ObjectPtr artwork)
{
- _image = getDboPtr(image);
+ _preferredArtwork = getDboPtr(artwork);
}
} // namespace lms::db
diff --git a/src/libs/database/impl/Artwork.cpp b/src/libs/database/impl/Artwork.cpp
new file mode 100644
index 00000000..e9264f06
--- /dev/null
+++ b/src/libs/database/impl/Artwork.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "database/Artwork.hpp"
+
+#include
+
+#include "database/Image.hpp"
+#include "database/Session.hpp"
+#include "database/TrackEmbeddedImage.hpp"
+
+#include "Utils.hpp"
+#include "traits/IdTypeTraits.hpp"
+
+namespace lms::db
+{
+ Artwork::Artwork(ObjectPtr trackEmbeddedImage)
+ : _trackEmbeddedImage{ getDboPtr(trackEmbeddedImage) }
+ {
+ }
+
+ Artwork::Artwork(ObjectPtr image)
+ : _image{ getDboPtr(image) }
+ {
+ }
+
+ Artwork::pointer Artwork::create(Session& session, ObjectPtr trackEmbeddedImage)
+ {
+ session.checkWriteTransaction();
+ return session.getDboSession()->add(std::unique_ptr{ new Artwork{ trackEmbeddedImage } });
+ }
+
+ Artwork::pointer Artwork::create(Session& session, ObjectPtr image)
+ {
+ session.checkWriteTransaction();
+ return session.getDboSession()->add(std::unique_ptr{ new Artwork{ image } });
+ }
+
+ std::size_t Artwork::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM artwork"));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, ArtworkId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.id = ?").bind(id));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, TrackEmbeddedImageId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.track_embedded_image_id = ?").bind(id));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, ImageId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.image_id = ?").bind(id));
+ }
+
+ Wt::WDateTime Artwork::getLastWrittenTime() const
+ {
+ auto query{ session()->query("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") };
+ query.leftJoin("image ON artwork.image_id = image.id");
+ query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id");
+ query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id");
+ query.leftJoin("track ON track.id = track_embedded_image_link.track_id");
+ query.where("artwork.id = ?").bind(getId());
+
+ return utils::fetchQuerySingleResult(query);
+ }
+} // namespace lms::db
diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp
index 44841f4f..720e040e 100644
--- a/src/libs/database/impl/Cluster.cpp
+++ b/src/libs/database/impl/Cluster.cpp
@@ -20,6 +20,7 @@
#include "database/Cluster.hpp"
#include "database/Artist.hpp"
+#include "database/Artwork.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp"
#include "database/Release.hpp"
diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp
index ff696b7a..0bb7e46a 100644
--- a/src/libs/database/impl/Db.cpp
+++ b/src/libs/database/impl/Db.cpp
@@ -19,6 +19,9 @@
#include "database/Db.hpp"
+#include
+#include
+
#include
#include
@@ -70,21 +73,98 @@ namespace lms::db
std::filesystem::path _dbPath;
};
+
+ enum class IntegrityCheckType
+ {
+ Quick,
+ Full
+ };
+ bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function errorCallback)
+ {
+ bool integrityCheckPassed{};
+
+ auto statement = connection.prepareStatement(checkType == IntegrityCheckType::Full ? "PRAGMA integrity_check" : "PRAGMA quick_check");
+ statement->execute();
+
+ std::string result;
+ result.reserve(32);
+ while (statement->nextRow())
+ {
+ result.clear();
+ statement->getResult(0, &result, result.capacity());
+
+ if (result == "ok")
+ {
+ integrityCheckPassed = true;
+ break;
+ }
+
+ errorCallback(result);
+ }
+
+ return integrityCheckPassed;
+ }
+
+ bool checkDbForeignKeyConstraints(Wt::Dbo::SqlConnection& connection, std::function errorCallback)
+ {
+ bool foreignKeyConstraintsPassed{ true };
+
+ auto statement = connection.prepareStatement("PRAGMA foreign_key_check");
+ statement->execute();
+
+ std::string table;
+ std::string foreignTable;
+ // see https://www.sqlite.org/pragma.html#pragma_foreign_key_check for exepcted result
+ while (statement->nextRow())
+ {
+ foreignKeyConstraintsPassed = false;
+
+ table.clear();
+ foreignTable.clear();
+ long long rowId{};
+
+ statement->getResult(0, &table, static_cast(table.capacity()));
+ statement->getResult(1, &rowId);
+ statement->getResult(2, &foreignTable, static_cast(foreignTable.capacity()));
+
+ errorCallback(table, rowId, foreignTable);
+ }
+
+ return foreignKeyConstraintsPassed;
+ }
} // namespace
// Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
{
+ std::string checkType{ "quick" };
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath);
auto connection{ std::make_unique(dbPath) };
if (core::IConfig * config{ core::Service::get() }) // may not be here on testU
+ {
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
+ checkType = config->getString("db-integrity-check", "quick");
+ }
auto connectionPool{ std::make_unique(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 });
_connectionPool = std::move(connectionPool);
+
+ if (checkType == "quick")
+ {
+ performQuickCheck();
+ }
+ else if (checkType == "full")
+ {
+ performIntegrityCheck();
+ performForeignKeyConstraintsCheck();
+ }
+ else if (checkType != "none")
+ {
+ throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'.");
+ }
}
void Db::executeSql(const std::string& sql)
@@ -114,6 +194,55 @@ namespace lms::db
return *tlsSession;
}
+ void Db::performQuickCheck()
+ {
+ ScopedConnection connection{ *_connectionPool };
+
+ LMS_LOG(DB, INFO, "Performing quick database check...");
+
+ // Quick check is just a simple integrity check
+ bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) {
+ LMS_LOG(DB, ERROR, "Quick check error: " << error);
+ }) };
+
+ if (quickCheckPassed)
+ LMS_LOG(DB, INFO, "Quick database check passed!");
+ else
+ LMS_LOG(DB, ERROR, "Quick database check done with errors!");
+ }
+
+ void Db::performIntegrityCheck()
+ {
+ ScopedConnection connection{ *_connectionPool };
+
+ LMS_LOG(DB, INFO, "Checking database integrity...");
+
+ bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) {
+ LMS_LOG(DB, ERROR, "Integrity check error: " << error);
+ }) };
+
+ if (integrityCheckPassed)
+ LMS_LOG(DB, INFO, "Database integrity check passed!");
+ else
+ LMS_LOG(DB, ERROR, "Database integrity check done with errors!");
+ }
+
+ void Db::performForeignKeyConstraintsCheck()
+ {
+ ScopedConnection connection{ *_connectionPool };
+
+ LMS_LOG(DB, INFO, "Checking foreign key constraints...");
+
+ bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) {
+ LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'");
+ }) };
+
+ if (!foreignKeyConstraintsPassed)
+ throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database.");
+
+ LMS_LOG(DB, INFO, "Foreign key constraints check passed!");
+ }
+
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
: _connectionPool{ pool }
, _connection{ _connectionPool.getConnection() }
diff --git a/src/libs/database/impl/Image.cpp b/src/libs/database/impl/Image.cpp
index 1b010d09..e0d43c4d 100644
--- a/src/libs/database/impl/Image.cpp
+++ b/src/libs/database/impl/Image.cpp
@@ -23,7 +23,6 @@
#include "database/Artist.hpp"
#include "database/Directory.hpp"
-#include "database/Release.hpp"
#include "database/Session.hpp"
#include "Utils.hpp"
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index a0318ed8..31532b5f 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{ 94 };
}
VersionInfo::VersionInfo()
@@ -411,7 +411,7 @@ SELECT
copyright_url,
track_replay_gain,
release_replay_gain,
- COALESCE(artist_display_name, ""),
+ artist_display_name,
release_id,
1
FROM track)");
@@ -1206,6 +1206,186 @@ 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");
+ }
+
+ void migrateFromV91(Session& session)
+ {
+ dropIndexes(session);
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred");
+ }
+
+ void migrateFromV92(Session& session)
+ {
+ // Create the new artwork table
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artwork" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "track_embedded_image_id" bigint,
+ "image_id" bigint,
+ constraint "fk_artwork_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_artwork_image" foreign key ("image_id") references "image" ("id") on delete cascade deferrable initially deferred))");
+
+ // Replaced image by artwork for release
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "name" text not null,
+ "sort_name" text not null,
+ "mbid" text not null,
+ "group_mbid" text not null,
+ "total_disc" integer,
+ "artist_display_name" text not null,
+ "is_compilation" boolean not null,
+ "barcode" text not null,
+ "comment" text not null,
+ "preferred_artwork_id" bigint,
+ constraint "fk_release_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))");
+ // Migrate data, with the new preferred_artwork_id field set to null
+ utils::executeCommand(*session.getDboSession(), R"(INSERT INTO release_backup
+SELECT
+ id,
+ version,
+ name,
+ sort_name,
+ mbid,
+ group_mbid,
+ total_disc,
+ COALESCE(artist_display_name, ''),
+ is_compilation,
+ barcode,
+ comment,
+ NULL as preferred_artwork_id
+FROM release)");
+
+ utils::executeCommand(*session.getDboSession(), "DROP TABLE release");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release");
+
+ // Replaced image by artwork for track
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "scan_version" integer not null,
+ "track_number" integer,
+ "disc_number" integer,
+ "total_track" integer,
+ "disc_subtitle" text not null,
+ "name" text not null,
+ "duration" integer,
+ "bitrate" integer not null,
+ "bits_per_sample" integer not null,
+ "channel_count" integer not null,
+ "sample_rate" integer not null,
+ "date" text,
+ "original_date" text,
+ "absolute_file_path" text not null,
+ "relative_file_path" text not null,
+ "file_stem" text not null,
+ "file_name" text not null,
+ "file_size" bigint not null,
+ "file_last_write" text,
+ "file_added" text,
+ "mbid" text not null,
+ "recording_mbid" text not null,
+ "copyright" text not null,
+ "copyright_url" text not null,
+ "advisory" integer not null,
+ "track_replay_gain" real,
+ "release_replay_gain" real,
+ "artist_display_name" text not null,
+ "comment" text not null,
+ "release_id" bigint,
+ "media_library_id" bigint,
+ "directory_id" bigint,
+ "preferred_artwork_id" bigint,
+ "preferred_media_artwork_id" bigint,
+ constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
+ constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred,
+ constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
+ ))");
+ // Migrate data, with the new preferred_artwork_id and preferred_media_artwork_id fields set to null
+ utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
+SELECT
+ id,
+ version,
+ scan_version,
+ track_number,
+ disc_number,
+ total_track,
+ disc_subtitle,
+ name,
+ duration,
+ bitrate,
+ bits_per_sample,
+ channel_count,
+ sample_rate,
+ date,
+ original_date,
+ absolute_file_path,
+ relative_file_path,
+ file_stem,
+ file_name,
+ file_size,
+ file_last_write,
+ file_added,
+ mbid,
+ recording_mbid,
+ copyright,
+ copyright_url,
+ advisory,
+ track_replay_gain,
+ release_replay_gain,
+ artist_display_name,
+ comment,
+ release_id,
+ media_library_id,
+ directory_id,
+ NULL as preferred_artwork_id,
+ NULL as preferred_media_artwork_id
+FROM track)");
+ utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
+
+ // Replaced image by artwork for artist
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "name" text not null,
+ "sort_name" text not null,
+ "mbid" text not null,
+ "preferred_artwork_id" bigint,
+ constraint "fk_artist_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
+ ))");
+ // Migrate data, with the new preferred_artwork_id field set to null
+ utils::executeCommand(*session.getDboSession(), R"(INSERT INTO artist_backup
+SELECT
+ id,
+ version,
+ name,
+ sort_name,
+ mbid,
+ NULL as preferred_artwork_id
+FROM artist)");
+
+ utils::executeCommand(*session.getDboSession(), "DROP TABLE artist");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist");
+
+ // Just increment the scan version of the settings to make the next scan rescan everything
+ utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
+ }
+
+ void migrateFromV93(Session& session)
+ {
+ // add artist_image_fallback_to_release in ScanSettings
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN artist_image_fallback_to_release BOOLEAN NOT NULL DEFAULT(false)");
+ }
+
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1272,6 +1452,10 @@ FROM tracklist)");
{ 87, migrateFromV87 },
{ 88, migrateFromV88 },
{ 89, migrateFromV89 },
+ { 90, migrateFromV90 },
+ { 91, migrateFromV91 },
+ { 92, migrateFromV92 },
+ { 93, migrateFromV93 },
};
bool migrationPerformed{};
diff --git a/src/libs/database/impl/PlayQueue.cpp b/src/libs/database/impl/PlayQueue.cpp
index cd1af8f3..d5941f20 100644
--- a/src/libs/database/impl/PlayQueue.cpp
+++ b/src/libs/database/impl/PlayQueue.cpp
@@ -21,6 +21,7 @@
#include
+#include "database/Artwork.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp"
#include "database/Release.hpp"
diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp
index 4c2575c1..12c85d6b 100644
--- a/src/libs/database/impl/Release.cpp
+++ b/src/libs/database/impl/Release.cpp
@@ -23,9 +23,9 @@
#include "core/PartialDateTime.hpp"
#include "database/Artist.hpp"
+#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
-#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/Types.hpp"
@@ -701,9 +701,14 @@ namespace lms::db
return utils::fetchQueryResults(query);
}
- ObjectPtr Release::getImage() const
+ ObjectPtr Release::getPreferredArtwork() const
{
- return ObjectPtr{ _image };
+ return ObjectPtr{ _preferredArtwork };
+ }
+
+ ArtworkId Release::getPreferredArtworkId() const
+ {
+ return _preferredArtwork.id();
}
void Release::clearLabels()
@@ -736,9 +741,9 @@ namespace lms::db
_releaseTypes.insert(getDboPtr(releaseType));
}
- void Release::setImage(ObjectPtr image)
+ void Release::setPreferredArtwork(ObjectPtr artwork)
{
- _image = getDboPtr(image);
+ _preferredArtwork = getDboPtr(artwork);
}
bool Release::hasVariousArtists() const
@@ -867,14 +872,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 +884,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") };
+ 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) {
+ res.push_back(cluster);
+ });
+
+ return res;
+ }
} // namespace lms::db
diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp
index 3a4f7a8a..f38ee204 100644
--- a/src/libs/database/impl/ScanSettings.cpp
+++ b/src/libs/database/impl/ScanSettings.cpp
@@ -121,14 +121,17 @@ namespace lms::db
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
{
- if (_skipSingleReleasePlayLists != value)
- _skipSingleReleasePlayLists = value;
+ _skipSingleReleasePlayLists = value;
}
void ScanSettings::setAllowMBIDArtistMerge(bool value)
{
- if (_allowMBIDArtistMerge != value)
- _allowMBIDArtistMerge = value;
+ _allowMBIDArtistMerge = value;
+ }
+
+ void ScanSettings::setArtistImageFallbackToReleaseField(bool value)
+ {
+ _artistImageFallbackToReleaseField = value;
}
void ScanSettings::incAudioScanVersion()
diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp
index 3a7c1432..deac39c9 100644
--- a/src/libs/database/impl/Session.cpp
+++ b/src/libs/database/impl/Session.cpp
@@ -23,6 +23,7 @@
#include "core/ITraceLogger.hpp"
#include "database/Artist.hpp"
#include "database/ArtistInfo.hpp"
+#include "database/Artwork.hpp"
#include "database/AuthToken.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -102,6 +103,7 @@ namespace lms::db
_session.mapClass("artist");
_session.mapClass("artist_info");
+ _session.mapClass("artwork");
_session.mapClass("auth_token");
_session.mapClass("cluster");
_session.mapClass("cluster_type");
@@ -196,7 +198,6 @@ namespace lms::db
{
auto transaction{ createWriteTransaction() };
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
- utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
@@ -206,10 +207,15 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)");
+
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_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,9 +231,10 @@ 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)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_type_track_embedded_image_id_track_id_idx ON track_embedded_image_link(type,track_embedded_image_id,track_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)");
@@ -254,7 +261,6 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
- utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
@@ -291,6 +297,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 +305,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)");
@@ -393,6 +401,16 @@ namespace lms::db
LMS_LOG(DB, INFO, "Analyze complete!");
}
+ bool Session::areAllTablesEmpty()
+ {
+ const std::vector entryList{ utils::fetchQueryResults(_session.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")) };
+
+ return std::all_of(entryList.cbegin(), entryList.cend(), [this](const std::string& entry) {
+ const auto count{ utils::fetchQuerySingleResult(_session.query("SELECT COUNT(*) FROM " + entry)) };
+ return count == 0;
+ });
+ }
+
void Session::retrieveEntriesToAnalyze(std::vector& entryList)
{
auto transaction{ createReadTransaction() };
diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp
index ab631042..ae308b53 100644
--- a/src/libs/database/impl/Track.cpp
+++ b/src/libs/database/impl/Track.cpp
@@ -23,6 +23,7 @@
#include "core/ILogger.hpp"
#include "database/Artist.hpp"
+#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/MediaLibrary.hpp"
@@ -324,6 +325,26 @@ namespace lms::db
return utils::execRangeQuery(query, range);
}
+ void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
+ {
+ session.checkWriteTransaction();
+
+ if (artworkId.isValid())
+ utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = ? WHERE id = ?", artworkId, trackId);
+ else
+ utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = NULL WHERE id = ?", trackId);
+ }
+
+ void Track::updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
+ {
+ session.checkWriteTransaction();
+
+ if (artworkId.isValid())
+ utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = ? WHERE id = ?", artworkId, trackId);
+ else
+ utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = NULL WHERE id = ?", trackId);
+ }
+
std::vector Track::getClusters() const
{
return utils::fetchQueryResults(_clusters.find());
@@ -338,6 +359,36 @@ namespace lms::db
return utils::fetchQueryResults(query);
}
+ ObjectPtr Track::getMediaLibrary() const
+ {
+ return _mediaLibrary;
+ }
+
+ ObjectPtr Track::getDirectory() const
+ {
+ return _directory;
+ }
+
+ ObjectPtr Track::getPreferredArtwork() const
+ {
+ return _preferredArtwork;
+ }
+
+ ArtworkId Track::getPreferredArtworkId() const
+ {
+ return _preferredArtwork.id();
+ }
+
+ ObjectPtr Track::getPreferredMediaArtwork() const
+ {
+ return _preferredMediaArtwork;
+ }
+
+ ArtworkId Track::getPreferredMediaArtworkId() const
+ {
+ return _preferredMediaArtwork.id();
+ }
+
RangeResults Track::findIds(Session& session, const FindParameters& parameters)
{
session.checkReadTransaction();
@@ -484,6 +535,26 @@ namespace lms::db
_embeddedImageLinks.insert(getDboPtr(image));
}
+ void Track::setMediaLibrary(ObjectPtr mediaLibrary)
+ {
+ _mediaLibrary = getDboPtr(mediaLibrary);
+ }
+
+ void Track::setDirectory(ObjectPtr directory)
+ {
+ _directory = getDboPtr(directory);
+ }
+
+ void Track::setPreferredArtwork(ObjectPtr artwork)
+ {
+ _preferredArtwork = getDboPtr(artwork);
+ }
+
+ void Track::setPreferredMediaArtwork(ObjectPtr artwork)
+ {
+ _preferredMediaArtwork = getDboPtr(artwork);
+ }
+
std::optional Track::getYear() const
{
return _date.getYear();
diff --git a/src/libs/database/impl/TrackEmbeddedImage.cpp b/src/libs/database/impl/TrackEmbeddedImage.cpp
index c802d4b8..9b844b8d 100644
--- a/src/libs/database/impl/TrackEmbeddedImage.cpp
+++ b/src/libs/database/impl/TrackEmbeddedImage.cpp
@@ -38,24 +38,50 @@ namespace lms::db
auto query{ session.getDboSession()->query>("SELECT t_e_i FROM track_embedded_image t_e_i") };
- if (params.isPreferred
+ if (params.artist.isValid()
+ || params.discNumber.has_value()
|| params.track.isValid()
|| params.release.isValid()
|| params.trackList.isValid()
- || params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize)
+ || !params.imageTypes.empty()
+ || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
+ || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
{
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.artist.isValid())
+ {
+ query.join("track_artist_link t_a_l ON t_a_l.track_id = t_e_i_l.track_id");
+ query.where("t_a_l.artist_id = ?").bind(params.artist);
+
+ if (!params.trackArtistLinkTypes.empty())
+ {
+ std::string clause{ "t_a_l.type IN (" };
+ for (const auto& type : params.trackArtistLinkTypes)
+ {
+ if (clause.back() != '(')
+ clause += ",";
+ clause += "?";
+ query.bind(type);
+ }
+ clause += ")";
+ query.where(clause);
+ }
+ }
if (params.track.isValid())
query.where("t_e_i_l.track_id = ?").bind(params.track);
- if (params.release.isValid())
+ if (params.release.isValid()
+ || params.discNumber.has_value()
+ || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
+ || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
{
query.join("track t ON t_e_i_l.track_id = t.id");
- query.where("t.release_id = ?").bind(params.release);
+ if (params.release.isValid())
+ query.where("t.release_id = ?").bind(params.release);
+ if (params.discNumber.has_value())
+ query.where("t.disc_number = ?").bind(params.discNumber.value());
}
if (params.trackList.isValid())
@@ -63,14 +89,38 @@ 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.imageTypes.empty())
+ {
+ std::string clause{ "t_e_i_l.type IN (" };
+ for (const auto& type : params.imageTypes)
+ {
+ if (clause.back() != '(')
+ clause += ",";
+ clause += "?";
+ query.bind(type);
+ }
+ clause += ")";
+ query.where(clause);
+ }
}
switch (params.sortMethod)
{
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::SizeDesc:
+ query.orderBy("t_e_i.size DESC");
+ break;
+ case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
+ query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC");
+ break;
+ case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
+ query.orderBy("t.track_number, t_e_i.size DESC");
+ break;
+ case TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc:
+ assert(params.trackList.isValid());
+ query.orderBy("t_l_e.id, t_e_i.size DESC");
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