diff --git a/CMakeLists.txt b/CMakeLists.txt
index 29844248..bc0ce574 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12)
-project(lms VERSION 3.72.1)
+project(lms VERSION 3.73.0)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
diff --git a/Dockerfile-release b/Dockerfile-release
index 1bf40d82..585c76db 100644
--- a/Dockerfile-release
+++ b/Dockerfile-release
@@ -67,11 +67,12 @@ RUN \
--enable-libopenjpeg \
--enable-libopus \
--enable-libvorbis \
+ --enable-zlib \
--disable-everything \
- --enable-decoder=aac*,ac3*,alac,als,dsd*,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \
- --enable-encoder=libmp3lame,libopus,libvorbis \
- --enable-demuxer=aac,aiff,asf,dsf,flac,ipod,ogg,matroska,mov,mp3,mp4,wav,wv,webm \
- --enable-muxer=ogg,matroska,mp3,webm \
+ --enable-decoder=aac*,ac3*,alac,als,ape,asf,dsd*,flac,libopus,pcm*,libvorbis,mp3*,mpc7,mpc8,shorten,tta,wavpack,wma*,libopenjpg,png \
+ --enable-encoder=flac,libmp3lame,libopus,libvorbis \
+ --enable-demuxer=aac,aiff,ape,asf,dsf,flac,m4a,mp3,mov,mpc,mpc8,ogg,shn,tta,wav,wv \
+ --enable-muxer=flac,mp3,ogg \
--enable-protocol=file,pipe \
--enable-filter=aresample \
--enable-lto \
diff --git a/README.md b/README.md
index d4344ff7..8534b2f0 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ The folder must follow a structure defined by Kodi, as detailed [here](https://k
__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.
+It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, `codec`, and by music library. More tags, including custom ones, can be added in the database administration settings.
__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_.
diff --git a/approot/artist.xml b/approot/artist.xml
index 13d24a6a..78195b00 100644
--- a/approot/artist.xml
+++ b/approot/artist.xml
@@ -24,7 +24,9 @@
${}
${tr:Lms.Explore.musicbrainz-artist}
${}
+ ${}
${download class="dropdown-item"}
+ ${}
diff --git a/approot/messages.xml b/approot/messages.xml
index 9a4fd79f..00d92c46 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -371,11 +371,9 @@
Never
Transcoding output bitrate
Transcoding output format
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
User Interface
diff --git a/approot/messages_es.xml b/approot/messages_es.xml
index b60112a2..5800e983 100644
--- a/approot/messages_es.xml
+++ b/approot/messages_es.xml
@@ -370,11 +370,9 @@
Nunca
Bitrate de transcodificación
Formato de transcodificación
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
Interfaz de usuario
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index ed0a2b30..4ca0b8a5 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -370,11 +370,9 @@
Jamais
Bitrate du transcodage
Format du transcodage
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
Interface utilisateur
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 13e74ee0..a1856a6b 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -370,11 +370,9 @@
Mai
Bitrate transcodifica
Formato di transcodifica
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
Interfaccia utente
diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml
index 2687adc6..6e8ff867 100644
--- a/approot/messages_pl.xml
+++ b/approot/messages_pl.xml
@@ -396,11 +396,9 @@
Nigdy
Przepływność transkodowania
Format wyjściowy transkodowania
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
Interfejs użytkownika
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index e5f4cda8..37f11a77 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -367,11 +367,9 @@
从不
转码比特率
转码格式
-Matroska/Opus
MP3
Ogg/Opus
Ogg/Vorbis
-WebM/Vorbis
diff --git a/approot/playqueue.xml b/approot/playqueue.xml
index c372f8df..2f6f2453 100644
--- a/approot/playqueue.xml
+++ b/approot/playqueue.xml
@@ -65,7 +65,9 @@
diff --git a/approot/release.xml b/approot/release.xml
index 0d856885..d19cd070 100644
--- a/approot/release.xml
+++ b/approot/release.xml
@@ -27,7 +27,9 @@
${}
${tr:Lms.Explore.musicbrainz-release}
${}
+ ${}
${download class="dropdown-item"}
+ ${}
${release-info class="dropdown-item"}
@@ -109,7 +111,9 @@
${play-next class="dropdown-item"}
${play-last class="dropdown-item"}
${star class="dropdown-item"}
+ ${}
${download class="dropdown-item"}
+ ${}
${track-info class="dropdown-item"}
${}${track-lyrics class="dropdown-item"}${}
diff --git a/approot/tracklist.xml b/approot/tracklist.xml
index 5a2ec74b..9f4fc8ca 100644
--- a/approot/tracklist.xml
+++ b/approot/tracklist.xml
@@ -13,7 +13,9 @@
diff --git a/conf/lms.conf b/conf/lms.conf
index 269ae960..3a22444d 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -113,9 +113,6 @@ artist-image-file-names = ("artist");
# Note: files named after the disc itself are always searched before the names in this list.
medium-image-file-names = ("discsubtitle");
-# Playqueue max entry count
-playqueue-max-entry-count = 1000;
-
# Internal tracing for profiling purposes. Enable only if necessary, as it incurs some runtime overhead!
# Possible values are "disabled", "overview" or "detailed".
# If enabled, data has to be dumped in the tracing view located in the admin menu
@@ -140,3 +137,9 @@ podcast-auto-download-episodes = true;
# Max age in days for auto-downloaded episodes before deletion
podcast-auto-download-episodes-max-age-days = 30;
+
+# Playqueue max entry count
+ui-playqueue-max-entry-count = 1000;
+
+# Allow downloads
+ui-allow-downloads = true;
\ No newline at end of file
diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt
index 48f071bb..0979b975 100644
--- a/src/libs/audio/CMakeLists.txt
+++ b/src/libs/audio/CMakeLists.txt
@@ -4,17 +4,17 @@ pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
add_library(lmsaudio STATIC
impl/ffmpeg/AudioFile.cpp
impl/ffmpeg/AudioFileInfo.cpp
+ impl/ffmpeg/AudioFileInfoParser.cpp
impl/ffmpeg/ImageReader.cpp
impl/ffmpeg/TagReader.cpp
impl/ffmpeg/Transcoder.cpp
impl/ffmpeg/Utils.cpp
impl/taglib/AudioFileInfo.cpp
+ impl/taglib/AudioFileInfoParser.cpp
impl/taglib/ImageReader.cpp
impl/taglib/TagReader.cpp
impl/taglib/Utils.cpp
- impl/AudioTypes.cpp
- impl/ImageReader.cpp
- impl/ParseAudioFileInfo.cpp
+ impl/AudioFileInfoParser.cpp
impl/TagReader.cpp
)
diff --git a/src/libs/audio/impl/AudioFileInfoParser.cpp b/src/libs/audio/impl/AudioFileInfoParser.cpp
new file mode 100644
index 00000000..069399d7
--- /dev/null
+++ b/src/libs/audio/impl/AudioFileInfoParser.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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
+
+#include "audio/IAudioFileInfoParser.hpp"
+
+#include "ffmpeg/AudioFileInfoParser.hpp"
+#include "taglib/AudioFileInfoParser.hpp"
+
+namespace lms::audio
+{
+ std::unique_ptr createAudioFileInfoParser(AudioFileInfoParserBackend backend)
+ {
+ switch (backend)
+ {
+ case AudioFileInfoParserBackend::TagLib:
+ return std::make_unique();
+ case AudioFileInfoParserBackend::FFmpeg:
+ return std::make_unique();
+ }
+
+ return nullptr;
+ }
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/audio/impl/AudioTypes.cpp
deleted file mode 100644
index d0a5930b..00000000
--- a/src/libs/audio/impl/AudioTypes.cpp
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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 "audio/AudioTypes.hpp"
-
-namespace lms::audio
-{
- core::LiteralString containerTypeToString(ContainerType type)
- {
- switch (type)
- {
- case ContainerType::AIFF:
- return "AIFF";
- case ContainerType::APE:
- return "APE";
- case ContainerType::ASF:
- return "ASF";
- case ContainerType::DSF:
- return "DSF";
- case ContainerType::FLAC:
- return "FLAC";
- case ContainerType::MP4:
- return "MP4";
- case ContainerType::MPC:
- return "MPC";
- case ContainerType::MPEG:
- return "MPEG";
- case ContainerType::Ogg:
- return "Ogg";
- case ContainerType::Shorten:
- return "Shorten";
- case ContainerType::TrueAudio:
- return "TrueAudio";
- case ContainerType::WAV:
- return "WAV";
- case ContainerType::WavPack:
- return "WavPack";
- }
-
- return "";
- }
-
- core::LiteralString codecTypeToString(CodecType type)
- {
- switch (type)
- {
- case CodecType::AAC:
- return "AAC";
- case CodecType::ALAC:
- return "ALAC";
- case CodecType::APE:
- return "APE";
- case CodecType::DSD:
- return "DSD";
- case CodecType::FLAC:
- return "FLAC";
- case CodecType::MP3:
- return "MP3";
- case CodecType::MP4ALS:
- return "MP4ALS";
- case CodecType::MPC7:
- return "MPC7";
- case CodecType::MPC8:
- return "MPC8";
- case CodecType::Opus:
- return "Opus";
- case CodecType::PCM:
- return "PCM";
- case CodecType::Shorten:
- return "Shorten";
- case CodecType::TrueAudio:
- return "TrueAudio";
- case CodecType::Vorbis:
- return "Vorbis";
- case CodecType::WavPack:
- return "WavPack";
- case CodecType::WMA1:
- return "WMA1";
- case CodecType::WMA2:
- return "WMA2";
- case CodecType::WMA9Pro:
- return "WMA9Pro";
- case CodecType::WMA9Lossless:
- return "WMA9Lossless";
- }
-
- return "";
- }
-} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/ParseAudioFileInfo.cpp b/src/libs/audio/impl/ParseAudioFileInfo.cpp
deleted file mode 100644
index 321d46a7..00000000
--- a/src/libs/audio/impl/ParseAudioFileInfo.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2015 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include
-
-#include "audio/IAudioFileInfo.hpp"
-
-#include "ffmpeg/AudioFileInfo.hpp"
-#include "ffmpeg/Utils.hpp"
-#include "taglib/AudioFileInfo.hpp"
-#include "taglib/Utils.hpp"
-
-namespace lms::audio
-{
- std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
- {
- switch (parserOptions.parser)
- {
- case ParserOptions::Parser::TagLib:
- return std::make_unique(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
- case ParserOptions::Parser::FFmpeg:
- return std::make_unique(p, parserOptions.enableExtraDebugLogs);
- }
-
- return {};
- }
-
- std::span getSupportedExtensions(ParserOptions::Parser parser)
- {
- switch (parser)
- {
- case ParserOptions::Parser::TagLib:
- return taglib::utils::getSupportedExtensions();
- case ParserOptions::Parser::FFmpeg:
- return ffmpeg::utils::getSupportedExtensions();
- }
-
- return {};
- }
-
-} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp
index ad1861a7..1583eb73 100644
--- a/src/libs/audio/impl/ffmpeg/AudioFile.cpp
+++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp
@@ -20,21 +20,22 @@
#include "AudioFile.hpp"
#include
+#include
#include
extern "C"
{
-#define __STDC_CONSTANT_MACROS
#include
#include
#include
+#include
}
#include "core/ILogger.hpp"
+#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
-#include "audio/AudioTypes.hpp"
-#include "audio/IAudioFileInfo.hpp"
+#include "audio/Exception.hpp"
namespace lms::audio::ffmpeg
{
@@ -50,115 +51,169 @@ namespace lms::audio::ffmpeg
return "Unknown error";
}
- class AudioFileException : public AudioFileParsingException
+ class AvException : public Exception
{
public:
- AudioFileException(int avError)
- : AudioFileParsingException{ averror_to_string(avError) }
+ AvException(int avError)
+ : Exception{ averror_to_string(avError) }
{
}
};
- void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
+ void extractMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
{
if (!dictionnary)
return;
- AVDictionaryEntry* tag = NULL;
- while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
- {
+ const AVDictionaryEntry* tag{ NULL };
+ while ((tag = av_dict_iterate(dictionnary, tag)))
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
- }
}
- std::optional avdemuxerToContainerType(std::string_view name)
+ std::optional avdemuxerToContainerType(std::string_view name)
{
if (name == "aiff")
- return ContainerType::AIFF;
+ return core::media::Container::AIFF;
if (name == "ape")
- return ContainerType::APE;
+ return core::media::Container::APE;
if (name.starts_with("asf"))
- return ContainerType::ASF;
+ return core::media::Container::ASF;
if (name == "dsf")
- return ContainerType::DSF;
+ return core::media::Container::DSF;
if (name == "flac")
- return ContainerType::FLAC;
+ return core::media::Container::FLAC;
if (name.find("mp4") != std::string_view::npos)
- return ContainerType::MP4;
+ return core::media::Container::MP4;
if (name.starts_with("mpc"))
- return ContainerType::MPC;
+ return core::media::Container::MPC;
if (name == "mp3")
- return ContainerType::MPEG;
+ return core::media::Container::MPEG;
if (name == "ogg")
- return ContainerType::Ogg;
+ return core::media::Container::Ogg;
if (name == "shn")
- return ContainerType::Shorten;
+ return core::media::Container::Shorten;
if (name == "tta")
- return ContainerType::TrueAudio;
+ return core::media::Container::TrueAudio;
if (name == "wav")
- return ContainerType::WAV;
+ return core::media::Container::WAV;
if (name == "wv")
- return ContainerType::WavPack;
+ return core::media::Container::WavPack;
return std::nullopt;
}
- std::optional avcodecToCodecType(AVCodecID codec)
+ std::optional avcodecToCodecType(AVCodecID codec)
{
switch (codec)
{
- case AV_CODEC_ID_MP3:
- return CodecType::MP3;
case AV_CODEC_ID_AAC:
- return CodecType::AAC;
- case AV_CODEC_ID_VORBIS:
- return CodecType::Vorbis;
- case AV_CODEC_ID_WMAV1:
- return CodecType::WMA1;
- case AV_CODEC_ID_WMAV2:
- return CodecType::WMA2;
- case AV_CODEC_ID_WMAPRO:
- return CodecType::WMA9Pro;
- case AV_CODEC_ID_WMALOSSLESS:
- return CodecType::WMA9Lossless;
- case AV_CODEC_ID_FLAC:
- return CodecType::FLAC;
+ return core::media::Codec::AAC;
+ case AV_CODEC_ID_AC3:
+ return core::media::Codec::AC3;
case AV_CODEC_ID_ALAC:
- return CodecType::ALAC;
- case AV_CODEC_ID_WAVPACK:
- return CodecType::WavPack;
- case AV_CODEC_ID_MUSEPACK7:
- return CodecType::MPC7;
- case AV_CODEC_ID_MUSEPACK8:
- return CodecType::MPC8;
+ return core::media::Codec::ALAC;
case AV_CODEC_ID_APE:
- return CodecType::APE;
- case AV_CODEC_ID_MP4ALS:
- return CodecType::MP4ALS;
- case AV_CODEC_ID_OPUS:
- return CodecType::Opus;
- case AV_CODEC_ID_SHORTEN:
- return CodecType::Shorten;
+ return core::media::Codec::APE;
case AV_CODEC_ID_DSD_LSBF:
case AV_CODEC_ID_DSD_LSBF_PLANAR:
case AV_CODEC_ID_DSD_MSBF:
case AV_CODEC_ID_DSD_MSBF_PLANAR:
- return CodecType::DSD;
+ return core::media::Codec::DSD;
+ case AV_CODEC_ID_EAC3:
+ return core::media::Codec::EAC3;
+ case AV_CODEC_ID_FLAC:
+ return core::media::Codec::FLAC;
+ case AV_CODEC_ID_MP3:
+ return core::media::Codec::MP3;
+ case AV_CODEC_ID_MP4ALS:
+ return core::media::Codec::MP4ALS;
+ case AV_CODEC_ID_MUSEPACK7:
+ return core::media::Codec::MPC7;
+ case AV_CODEC_ID_MUSEPACK8:
+ return core::media::Codec::MPC8;
+ case AV_CODEC_ID_OPUS:
+ return core::media::Codec::Opus;
+ case AV_CODEC_ID_SHORTEN:
+ return core::media::Codec::Shorten;
+ case AV_CODEC_ID_VORBIS:
+ return core::media::Codec::Vorbis;
+ case AV_CODEC_ID_WAVPACK:
+ return core::media::Codec::WavPack;
+ case AV_CODEC_ID_WMALOSSLESS:
+ return core::media::Codec::WMA9Lossless;
+ case AV_CODEC_ID_WMAPRO:
+ return core::media::Codec::WMA9Pro;
+ case AV_CODEC_ID_WMAV1:
+ return core::media::Codec::WMA1;
+ case AV_CODEC_ID_WMAV2:
+ return core::media::Codec::WMA2;
default:
return std::nullopt;
}
}
+
+ core::LiteralString avLogLevelToStr(int level)
+ {
+ switch (level)
+ {
+ case AV_LOG_TRACE:
+ return "trace";
+ case AV_LOG_DEBUG:
+ return "debug";
+ case AV_LOG_VERBOSE:
+ return "verbose";
+ case AV_LOG_INFO:
+ return "info";
+ case AV_LOG_WARNING:
+ return "warning";
+ case AV_LOG_ERROR:
+ return "error";
+ case AV_LOG_FATAL:
+ return "fatal";
+ case AV_LOG_PANIC:
+ return "panic";
+ default:
+ return "unknown";
+ }
+ }
+
+ void avLogCallback(void*, int level, const char* fmt, va_list vl)
+ {
+ if (!core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
+ return;
+
+ if (level > AV_LOG_WARNING)
+ return;
+
+ std::array buffer{ 0 };
+ std::vsnprintf(buffer.data(), buffer.size(), fmt, vl);
+
+ LMS_LOG(AUDIO, DEBUG, "FFmpeg [" << avLogLevelToStr(level) << "] " << buffer.data());
+ }
+
+ class AvInitializer
+ {
+ public:
+ AvInitializer()
+ {
+ ::av_log_set_callback(avLogCallback);
+ }
+ };
} // namespace
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
+ LMS_SCOPED_TRACE_DETAILED("MetaData", "FFmpegParseFile");
+
+ static AvInitializer init;
+
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
- throw AudioFileException{ error };
+ throw AvException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
@@ -166,7 +221,7 @@ namespace lms::audio::ffmpeg
{
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
avformat_close_input(&_context);
- throw AudioFileException{ error };
+ throw AvException{ error };
}
}
@@ -187,17 +242,18 @@ namespace lms::audio::ffmpeg
info.container = avdemuxerToContainerType(_context->iformat->name);
info.containerName = _context->iformat->name;
- info.bitrate = _context->bit_rate;
+ if (_context->bit_rate > 0)
+ info.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
return info;
}
- AudioFile::MetadataMap AudioFile::getMetaData() const
+ AudioFile::MetadataMap AudioFile::extractMetaData() const
{
MetadataMap res;
- getMetaDataFromDictionnary(_context->metadata, res);
+ extractMetaDataFromDictionnary(_context->metadata, res);
// HACK for OGG files
// If we did not find tags, search metadata in streams
@@ -205,7 +261,7 @@ namespace lms::audio::ffmpeg
{
for (std::size_t i{}; i < _context->nb_streams; ++i)
{
- getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
+ extractMetaDataFromDictionnary(_context->streams[i]->metadata, res);
if (!res.empty())
break;
@@ -266,7 +322,7 @@ namespace lms::audio::ffmpeg
return false;
}
- void AudioFile::visitAttachedPictures(std::function func) const
+ void AudioFile::visitAttachedPictures(std::function func) const
{
static const std::unordered_map codecMimeMap{
{ AV_CODEC_ID_BMP, "image/bmp" },
@@ -286,14 +342,14 @@ namespace lms::audio::ffmpeg
if (avstream->codecpar == nullptr)
{
- LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
+ LMS_LOG(AUDIO, WARNING, "Skipping stream " << i << " since no codecpar is set");
continue;
}
MetadataMap metadata;
- getMetaDataFromDictionnary(avstream->metadata, metadata);
+ extractMetaDataFromDictionnary(avstream->metadata, metadata);
- Picture picture;
+ PictureView picture;
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end())
@@ -303,10 +359,10 @@ namespace lms::audio::ffmpeg
else
{
picture.mimeType = "application/octet-stream";
- LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
+ LMS_LOG(AUDIO, WARNING, "AVCodecID" << avstream->codecpar->codec_id << " (" << ::avcodec_get_name(avstream->codecpar->codec_id) << ") not handled in mime type conversion");
}
- const AVPacket& pkt{ avstream->attached_pic };
+ const ::AVPacket& pkt{ avstream->attached_pic };
picture.data = std::span{ reinterpret_cast(pkt.data), static_cast(pkt.size) };
func(picture, metadata);
@@ -325,7 +381,7 @@ namespace lms::audio::ffmpeg
if (!avstream->codecpar)
{
- LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
+ LMS_LOG(AUDIO, WARNING, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp
index 2af7fb1b..fdf0ac65 100644
--- a/src/libs/audio/impl/ffmpeg/AudioFile.hpp
+++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp
@@ -27,7 +27,8 @@
#include
#include
-#include "audio/AudioTypes.hpp"
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
extern "C"
{
@@ -36,7 +37,7 @@ extern "C"
namespace lms::audio::ffmpeg
{
- struct Picture
+ struct PictureView
{
std::string mimeType;
std::span data; // valid as long as IAudioFile exists
@@ -44,17 +45,17 @@ namespace lms::audio::ffmpeg
struct ContainerInfo
{
- std::optional container;
+ std::optional container;
std::string containerName;
- std::size_t bitrate{};
+ std::optional bitrate;
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
- std::optional codec;
+ std::optional codec;
std::string codecName;
std::optional bitrate;
@@ -75,12 +76,12 @@ namespace lms::audio::ffmpeg
const std::filesystem::path& getPath() const;
ContainerInfo getContainerInfo() const;
- MetadataMap getMetaData() const;
+ MetadataMap extractMetaData() const;
std::vector getStreamInfo() const;
std::optional getBestStreamInfo() const;
std::optional getBestStreamIndex() const;
bool hasAttachedPictures() const;
- void visitAttachedPictures(std::function func) const;
+ void visitAttachedPictures(std::function func) const;
private:
std::optional getStreamInfo(std::size_t streamIndex) const;
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp
index fe0793f9..e987412e 100644
--- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp
@@ -19,7 +19,12 @@
#include "AudioFileInfo.hpp"
-#include "audio/AudioTypes.hpp"
+#include
+#include
+
+#include "core/ILogger.hpp"
+
+#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "AudioFile.hpp"
@@ -30,65 +35,108 @@ namespace lms::audio::ffmpeg
{
namespace
{
- AudioProperties computeAudioProperties(const AudioFile& audioFile)
+ std::optional computeAudioProperties(const AudioFile& audioFile)
{
- AudioProperties audioProperties;
+ std::optional audioProperties;
const auto containerInfo{ audioFile.getContainerInfo() };
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
if (!bestStreamInfo)
- throw AudioFileParsingException{ "Cannot find best audio stream" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot find best audio stream in " << audioFile.getPath());
+ return audioProperties;
+ }
+
+ if (containerInfo.duration.count() == 0)
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << audioFile.getPath());
+ return audioProperties;
+ }
if (!containerInfo.container)
- throw AudioFileParsingException{ "Unhandled container type '" + containerInfo.containerName + "'" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Unhandled container '" << containerInfo.containerName << "' in " << audioFile.getPath());
+ return audioProperties;
+ }
if (!bestStreamInfo->codec)
- throw AudioFileParsingException{ "Unhandled codec type '" + bestStreamInfo->codecName + "'" };
-
- if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0)
- throw AudioFileParsingException{ "Cannot determine bitrate" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Unhandled codec '" << bestStreamInfo->codecName << "' in " << audioFile.getPath());
+ return audioProperties;
+ }
if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0)
- throw AudioFileParsingException{ "Cannot determine channel count" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << audioFile.getPath());
+ return audioProperties;
+ }
if (!bestStreamInfo->sampleRate || *bestStreamInfo->sampleRate == 0)
- throw AudioFileParsingException{ "Cannot determine sample rate" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << audioFile.getPath());
+ return audioProperties;
+ }
- audioProperties.container = *containerInfo.container;
- audioProperties.duration = containerInfo.duration;
- audioProperties.codec = *bestStreamInfo->codec;
- audioProperties.bitrate = *bestStreamInfo->bitrate;
- audioProperties.channelCount = *bestStreamInfo->channelCount;
- audioProperties.sampleRate = *bestStreamInfo->sampleRate;
- audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
+ audioProperties.emplace();
+
+ audioProperties->container = *containerInfo.container;
+ audioProperties->duration = containerInfo.duration;
+ audioProperties->codec = *bestStreamInfo->codec;
+
+ if (bestStreamInfo->bitrate)
+ audioProperties->bitrate = *bestStreamInfo->bitrate;
+ else if (containerInfo.bitrate)
+ audioProperties->bitrate = *containerInfo.bitrate;
+ else // Fallback on a bitrate based on duration/size
+ {
+ std::error_code ec;
+ const auto fileSize{ std::filesystem::file_size(audioFile.getPath(), ec) };
+ if (ec)
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine file size for " << audioFile.getPath() << ": " << ec.message());
+ audioProperties.reset();
+ return audioProperties;
+ }
+
+ audioProperties->bitrate = static_cast(std::chrono::duration_cast>(containerInfo.duration).count() / fileSize);
+ LMS_LOG(AUDIO, DEBUG, "Estimated bitrate from duration/size: " << audioProperties->bitrate << " bps in " << audioFile.getPath());
+ }
+ assert(audioProperties->bitrate > 0);
+
+ audioProperties->channelCount = *bestStreamInfo->channelCount;
+ audioProperties->sampleRate = *bestStreamInfo->sampleRate;
+ audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample;
return audioProperties;
}
} // namespace
- AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
+ AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
: _audioFile{ std::make_unique(filePath) }
- , _audioProperties{ std::make_unique(computeAudioProperties(*_audioFile)) }
- , _tagReader{ std::make_unique(*_audioFile, enableExtraDebugLogs) }
- , _imageReader{ std::make_unique(*_audioFile) }
+ , _audioProperties{ computeAudioProperties(*_audioFile) }
{
+ if (parseOptions.readTags)
+ _tagReader = std::make_unique(*_audioFile, parseOptions.enableExtraDebugLogs);
+
+ if (parseOptions.readImages)
+ _imageReader = std::make_unique(*_audioFile);
}
AudioFileInfo::~AudioFileInfo() = default;
- const AudioProperties& AudioFileInfo::getAudioProperties() const
+ const AudioProperties* AudioFileInfo::getAudioProperties() const
{
- return *_audioProperties;
+ return _audioProperties.has_value() ? &_audioProperties.value() : nullptr;
}
- const IImageReader& AudioFileInfo::getImageReader() const
+ const IImageReader* AudioFileInfo::getImageReader() const
{
- return *_imageReader;
+ return _imageReader.get();
}
- const ITagReader& AudioFileInfo::getTagReader() const
+ const ITagReader* AudioFileInfo::getTagReader() const
{
- return *_tagReader;
+ return _tagReader.get();
}
} // namespace lms::audio::ffmpeg
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp
index e8c806a7..e0874668 100644
--- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp
@@ -20,9 +20,11 @@
#pragma once
#include
+#include
-#include "audio/AudioTypes.hpp"
+#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
+#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::ffmpeg
{
@@ -33,18 +35,19 @@ namespace lms::audio::ffmpeg
class AudioFileInfo final : public IAudioFileInfo
{
public:
- AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
- ~AudioFileInfo();
+ AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
+ ~AudioFileInfo() override;
+
AudioFileInfo(const AudioFileInfo&) = delete;
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
private:
- const AudioProperties& getAudioProperties() const override;
- const IImageReader& getImageReader() const override;
- const ITagReader& getTagReader() const override;
+ const AudioProperties* getAudioProperties() const override;
+ const IImageReader* getImageReader() const override;
+ const ITagReader* getTagReader() const override;
std::unique_ptr _audioFile;
- std::unique_ptr _audioProperties;
+ const std::optional _audioProperties;
std::unique_ptr _tagReader;
std::unique_ptr _imageReader;
};
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp
new file mode 100644
index 00000000..9af6d1f8
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "AudioFileInfoParser.hpp"
+
+#include
+
+#include "AudioFileInfo.hpp"
+#include "Utils.hpp"
+
+namespace lms::audio::ffmpeg
+{
+ std::unique_ptr AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
+ {
+ return std::make_unique(p, parseOptions);
+ }
+
+ std::span AudioFileInfoParser::getSupportedExtensions() const
+ {
+ return utils::getSupportedExtensions();
+ }
+} // namespace lms::audio::ffmpeg
\ No newline at end of file
diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp
new file mode 100644
index 00000000..0c3a1e98
--- /dev/null
+++ b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "audio/IAudioFileInfoParser.hpp"
+
+namespace lms::audio::ffmpeg
+{
+ class AudioFileInfoParser : public IAudioFileInfoParser
+ {
+ private:
+ std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
+ std::span getSupportedExtensions() const override;
+ };
+} // namespace lms::audio::ffmpeg
\ No newline at end of file
diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp
index ad935d1e..cedd6b8c 100644
--- a/src/libs/audio/impl/ffmpeg/ImageReader.cpp
+++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp
@@ -40,17 +40,16 @@ namespace lms::audio::ffmpeg
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
} };
- _audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
+ _audioFile.visitAttachedPictures([&](const PictureView& picture, const AudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
if (metaDataHasKeyword(metaData, "front"))
- image.type = Image::Type::FrontCover;
+ image.type = core::media::ImageType::FrontCover;
else if (metaDataHasKeyword(metaData, "back"))
- image.type = Image::Type::BackCover;
+ image.type = core::media::ImageType::BackCover;
visitor(image);
});
}
-
} // namespace lms::audio::ffmpeg
diff --git a/src/libs/audio/impl/ffmpeg/TagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp
index fa5d9e4f..dcf648aa 100644
--- a/src/libs/audio/impl/ffmpeg/TagReader.cpp
+++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp
@@ -144,8 +144,7 @@ namespace lms::audio::ffmpeg
} // namespace
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
- : _audioFile{ audioFile }
- , _metaDataMap{ audioFile.getMetaData() }
+ : _metaDataMap{ audioFile.extractMetaData() }
{
if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
diff --git a/src/libs/audio/impl/ffmpeg/TagReader.hpp b/src/libs/audio/impl/ffmpeg/TagReader.hpp
index 25e7bd7f..9864e0d3 100644
--- a/src/libs/audio/impl/ffmpeg/TagReader.hpp
+++ b/src/libs/audio/impl/ffmpeg/TagReader.hpp
@@ -39,7 +39,6 @@ namespace lms::audio::ffmpeg
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
- const AudioFile& _audioFile;
AudioFile::MetadataMap _metaDataMap;
};
} // namespace lms::audio::ffmpeg
diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp
index cd6ea9d5..649f8872 100644
--- a/src/libs/audio/impl/ffmpeg/Transcoder.cpp
+++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp
@@ -19,13 +19,14 @@
#include "Transcoder.hpp"
-#include
+#include
#include
#include "core/IChildProcessManager.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
+#include "core/media/MimeType.hpp"
#include "audio/Exception.hpp"
#include "audio/TranscodeTypes.hpp"
@@ -42,18 +43,29 @@ namespace lms::audio::ffmpeg
{
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
- static std::atomic globalId{};
- static std::filesystem::path ffmpegPath;
-
- void Transcoder::init()
+ namespace
{
- ffmpegPath = core::Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
- if (!std::filesystem::exists(ffmpegPath))
- throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
- }
+ class FFmpegPath
+ {
+ public:
+ FFmpegPath()
+ {
+ path = core::Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
+ if (!std::filesystem::exists(path))
+ throw Exception{ "File '" + path.string() + "' does not exist!" };
+ }
+
+ const std::filesystem::path& get() const { return path; }
+
+ private:
+ std::filesystem::path path;
+ };
+ } // namespace
+
+ std::atomic Transcoder::_nextDebugId;
Transcoder::Transcoder(const TranscodeParameters& parameters)
- : _debugId{ globalId++ }
+ : _debugId{ _nextDebugId++ }
, _inputParams{ parameters.inputParameters }
, _outputParams{ parameters.outputParameters }
{
@@ -64,8 +76,7 @@ namespace lms::audio::ffmpeg
void Transcoder::start()
{
- if (ffmpegPath.empty())
- init();
+ static FFmpegPath ffmpegPath;
try
{
@@ -76,15 +87,16 @@ namespace lms::audio::ffmpeg
}
catch (const std::filesystem::filesystem_error& e)
{
- // TODO store/raise e.code()
- throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
+ throw IOFileException{ _inputParams.filePath, "Failed to check if file exists", e.code() };
}
LOG(INFO, "Transcoding file " << _inputParams.filePath);
std::vector args;
- args.emplace_back(ffmpegPath.string());
+ args.emplace_back(ffmpegPath.get().string());
+
+ // TODO some codecs have restrictions, take them into account (channel count, sample rate, etc.)
// Make sure:
// - we do not produce anything in the stderr output
@@ -114,7 +126,7 @@ namespace lms::audio::ffmpeg
args.emplace_back("-1");
}
- // Skip video flows (including covers)
+ // Skip video flows (including covers!)
args.emplace_back("-vn");
// Output bitrates
@@ -124,63 +136,88 @@ namespace lms::audio::ffmpeg
args.emplace_back(std::to_string(*_outputParams.bitrate));
}
+ // -ac doc says: "For output streams it is set by default to the number of input audio channels".
+ // But it looks like specifying it is required in case the original layout is not compatible with the output codec selected
+ {
+ args.emplace_back("-ac");
+ args.emplace_back(std::to_string(_outputParams.channelCount ? *_outputParams.channelCount : _inputParams.audioProperties.channelCount));
+ }
+
+ if (_outputParams.sampleRate)
+ {
+ args.emplace_back("-ar");
+ args.emplace_back(std::to_string(*_outputParams.sampleRate));
+ }
+
+ if (_outputParams.bitsPerSample)
+ {
+ args.emplace_back("-sample_fmt");
+ args.emplace_back("s" + std::to_string(*_outputParams.bitsPerSample));
+ }
+
// Codecs and formats
if (_outputParams.format)
{
- switch (*_outputParams.format)
+ args.emplace_back("-f");
+
+ switch (_outputParams.format->container)
{
- case OutputFormat::MP3:
- args.emplace_back("-f");
+ case core::media::Container::FLAC:
+ args.emplace_back("flac");
+ break;
+ case core::media::Container::Ogg:
+ args.emplace_back("ogg");
+ break;
+ case core::media::Container::MPEG:
args.emplace_back("mp3");
break;
- case OutputFormat::OGG_OPUS:
- args.emplace_back("-acodec");
+ default:
+ throw Exception{ "Unsupported container type " + std::string{ core::media::containerToString(_outputParams.format->container).str() } };
+ }
+
+ args.emplace_back("-acodec");
+
+ switch (_outputParams.format->codec)
+ {
+ case core::media::Codec::MP3:
+ args.emplace_back("libmp3lame");
+ break;
+
+ case core::media::Codec::Opus:
args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("ogg");
break;
- case OutputFormat::MATROSKA_OPUS:
- args.emplace_back("-acodec");
- args.emplace_back("libopus");
- args.emplace_back("-f");
- args.emplace_back("matroska");
- break;
-
- case OutputFormat::OGG_VORBIS:
- args.emplace_back("-acodec");
+ case core::media::Codec::Vorbis:
args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("ogg");
break;
- case OutputFormat::WEBM_VORBIS:
- args.emplace_back("-acodec");
- args.emplace_back("libvorbis");
- args.emplace_back("-f");
- args.emplace_back("webm");
+ case core::media::Codec::FLAC:
+ args.emplace_back("flac");
break;
default:
- throw Exception{ "Unhandled format (" + std::to_string(static_cast(*_outputParams.format)) + ")" };
+ throw Exception{ "Unhandled codec type " + std::string{ core::media::getCodecDesc(_outputParams.format->codec).name.str() } };
}
}
args.emplace_back("pipe:1");
- LOG(DEBUG, "Dumping args (" << args.size() << ")");
- for (const std::string& arg : args)
- LOG(DEBUG, "Arg = '" << arg << "'");
+ if (core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG))
+ {
+ LOG(DEBUG, "Dumping args (" << args.size() << ")");
+ for (const std::string& arg : args)
+ LOG(DEBUG, "Arg = '" << arg << "'");
+ }
// Caution: stdin must have been closed before
try
{
- _childProcess = core::Service::get()->spawnChildProcess(ffmpegPath, args);
+ _childProcess = core::Service::get()->spawnChildProcess(ffmpegPath.get(), args);
}
catch (core::ChildProcessException& exception)
{
- throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
+ throw Exception{ "Cannot execute '" + ffmpegPath.get().string() + "': " + exception.what() };
}
}
@@ -202,25 +239,10 @@ namespace lms::audio::ffmpeg
std::string_view Transcoder::getOutputMimeType() const
{
- // TODO: use input mime type
if (_outputParams.format)
- {
- 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 core::media::getMimeType(_outputParams.format->container, _outputParams.format->codec).str();
- return "application/octet-stream"; // default, should not happen
+ return core::media::getMimeType(_inputParams.audioProperties.container, _inputParams.audioProperties.codec).str();
}
bool Transcoder::finished() const
diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.hpp b/src/libs/audio/impl/ffmpeg/Transcoder.hpp
index 715c6d4f..7204922c 100644
--- a/src/libs/audio/impl/ffmpeg/Transcoder.hpp
+++ b/src/libs/audio/impl/ffmpeg/Transcoder.hpp
@@ -19,6 +19,8 @@
#pragma once
+#include
+
#include "audio/ITranscoder.hpp"
namespace lms::core
@@ -47,7 +49,8 @@ namespace lms::audio::ffmpeg
static void init();
void start();
- const std::size_t _debugId{};
+ static std::atomic _nextDebugId;
+ const std::size_t _debugId;
const TranscodeInputParameters _inputParams;
const TranscodeOutputParameters _outputParams;
std::unique_ptr _childProcess;
diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp
index 6cc81dea..b0251481 100644
--- a/src/libs/audio/impl/taglib/AudioFileInfo.cpp
+++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp
@@ -43,8 +43,11 @@
#include
#endif
-#include "audio/AudioTypes.hpp"
+#include "core/ILogger.hpp"
+
+#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
+#include "audio/IAudioFileInfoParser.hpp"
#include "ImageReader.hpp"
#include "TagReader.hpp"
@@ -54,60 +57,78 @@ namespace lms::audio::taglib
{
namespace
{
- AudioProperties computeAudioProperties(const ::TagLib::File& file)
+ std::optional computeAudioProperties(const ::TagLib::File& file, const std::filesystem::path& filePath)
{
assert(file.audioProperties());
AudioProperties audioProperties;
{
- const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
+ const ::TagLib::AudioProperties* properties{ file.audioProperties() };
+ if (!properties)
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine audio properties in " << filePath);
+ return std::nullopt;
+ }
// Common properties
- audioProperties.bitrate = static_cast(properties.bitrate() * 1000);
+ audioProperties.bitrate = static_cast(properties->bitrate() * 1000);
if (audioProperties.bitrate == 0)
- throw AudioFileParsingException{ "Cannot determine bitrate" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << filePath);
+ return std::nullopt;
+ }
- audioProperties.channelCount = static_cast(properties.channels());
+ audioProperties.channelCount = static_cast(properties->channels());
if (audioProperties.channelCount == 0)
- throw AudioFileParsingException{ "Cannot determine channel count" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << filePath);
+ return std::nullopt;
+ }
- audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
+ audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
if (audioProperties.duration == decltype(audioProperties.duration)::zero())
- throw AudioFileParsingException{ "Cannot determine duration" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << filePath);
+ return std::nullopt;
+ }
- audioProperties.sampleRate = static_cast(properties.sampleRate());
+ audioProperties.sampleRate = static_cast(properties->sampleRate());
if (audioProperties.sampleRate == 0)
- throw AudioFileParsingException{ "Cannot determine sample rate" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << filePath);
+ return std::nullopt;
+ }
}
// Guess container from the file type
if (const auto* apeFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::APE;
- audioProperties.codec = CodecType::APE; // TODO version?
+ audioProperties.container = core::media::Container::APE;
+ audioProperties.codec = core::media::Codec::APE; // TODO version?
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
}
else if (const auto* asfFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::ASF;
+ audioProperties.container = core::media::Container::ASF;
switch (asfFile->audioProperties()->codec())
{
case ::TagLib::ASF::Properties::Codec::WMA1:
- audioProperties.codec = CodecType::WMA1;
+ audioProperties.codec = core::media::Codec::WMA1;
break;
case ::TagLib::ASF::Properties::Codec::WMA2:
- audioProperties.codec = CodecType::WMA2;
+ audioProperties.codec = core::media::Codec::WMA2;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
- audioProperties.codec = CodecType::WMA9Lossless;
+ audioProperties.codec = core::media::Codec::WMA9Lossless;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
- audioProperties.codec = CodecType::WMA9Pro;
+ audioProperties.codec = core::media::Codec::WMA9Pro;
break;
case ::TagLib::ASF::Properties::Codec::Unknown:
- throw AudioFileParsingException{ "Unhandled ASF codec" };
+ LMS_LOG(AUDIO, DEBUG, "Unhandled ASF codec in " << filePath);
+ return std::nullopt;
}
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
@@ -115,110 +136,116 @@ namespace lms::audio::taglib
#if LMS_TAGLIB_HAS_DSF
else if (const auto* dsfFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::DSF;
- audioProperties.codec = CodecType::DSD;
+ audioProperties.container = core::media::Container::DSF;
+ audioProperties.codec = core::media::Codec::DSD;
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_DSF
else if (const auto* flacFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::FLAC;
- audioProperties.codec = CodecType::FLAC;
+ audioProperties.container = core::media::Container::FLAC;
+ audioProperties.codec = core::media::Codec::FLAC;
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
}
else if (const auto* mp4File{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::MP4;
+ audioProperties.container = core::media::Container::MP4;
switch (mp4File->audioProperties()->codec())
{
case ::TagLib::MP4::Properties::Codec::AAC:
- audioProperties.codec = CodecType::AAC;
+ audioProperties.codec = core::media::Codec::AAC;
break;
case ::TagLib::MP4::Properties::Codec::ALAC:
- audioProperties.codec = CodecType::ALAC;
+ audioProperties.codec = core::media::Codec::ALAC;
break;
case ::TagLib::MP4::Properties::Codec::Unknown:
- throw AudioFileParsingException{ "Unhandled MP4 codec" };
+ LMS_LOG(AUDIO, DEBUG, "Unhandled MP4 codec in " << filePath);
+ return std::nullopt;
}
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
}
else if (const auto* mpcFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::MPC;
+ audioProperties.container = core::media::Container::MPC;
switch (mpcFile->audioProperties()->mpcVersion())
{
case 7:
- audioProperties.codec = CodecType::MPC7;
+ audioProperties.codec = core::media::Codec::MPC7;
break;
case 8:
- audioProperties.codec = CodecType::MPC8;
+ audioProperties.codec = core::media::Codec::MPC8;
break;
default:
- throw AudioFileParsingException{ "Unhandled MPC codec" };
+ LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath);
+ return std::nullopt;
}
}
else if (const auto* mpegFile{ dynamic_cast(&file) })
{
const auto& properties{ *mpegFile->audioProperties() };
- audioProperties.container = ContainerType::MPEG;
+ audioProperties.container = core::media::Container::MPEG;
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
&& mpegFile->audioProperties()->layer() == 3)
- audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
+ audioProperties.codec = core::media::Codec::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
#if LMS_TAGLIB_HAS_ADTS
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
- audioProperties.codec = CodecType::AAC;
+ audioProperties.codec = core::media::Codec::AAC;
#endif
else
- throw AudioFileParsingException{ "Unhandled MPEG codec" };
+ {
+ LMS_LOG(AUDIO, DEBUG, "Unhandled MPEG codec in " << filePath);
+ return std::nullopt;
+ }
}
else if (dynamic_cast(&file))
{
- audioProperties.container = ContainerType::Ogg;
- audioProperties.codec = CodecType::Opus;
+ audioProperties.container = core::media::Container::Ogg;
+ audioProperties.codec = core::media::Codec::Opus;
}
else if (dynamic_cast(&file))
{
- audioProperties.container = ContainerType::Ogg;
- audioProperties.codec = CodecType::Vorbis;
+ audioProperties.container = core::media::Container::Ogg;
+ audioProperties.codec = core::media::Codec::Vorbis;
}
else if (const auto* aiffFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::AIFF;
- audioProperties.codec = CodecType::PCM;
+ audioProperties.container = core::media::Container::AIFF;
+ audioProperties.codec = core::media::Codec::PCM;
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::WAV;
- audioProperties.codec = CodecType::PCM;
+ audioProperties.container = core::media::Container::WAV;
+ audioProperties.codec = core::media::Codec::PCM;
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_SHORTEN
else if (const auto* shortenFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::Shorten;
- audioProperties.codec = CodecType::Shorten;
+ audioProperties.container = core::media::Container::Shorten;
+ audioProperties.codec = core::media::Codec::Shorten;
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_SHORTEN
else if (const auto* trueAudioFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::TrueAudio;
- audioProperties.codec = CodecType::TrueAudio;
+ audioProperties.container = core::media::Container::TrueAudio;
+ audioProperties.codec = core::media::Codec::TrueAudio;
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavPackFile{ dynamic_cast(&file) })
{
- audioProperties.container = ContainerType::WavPack;
- audioProperties.codec = CodecType::WavPack;
+ audioProperties.container = core::media::Container::WavPack;
+ audioProperties.codec = core::media::Codec::WavPack;
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
}
else
{
- throw AudioFileParsingException{ "Unhandled file type" };
+ LMS_LOG(AUDIO, DEBUG, "Unhandled file type in " << filePath);
+ return std::nullopt;
}
if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0)
@@ -228,30 +255,32 @@ namespace lms::audio::taglib
}
} // namespace
- AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
+ AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
: _filePath{ filePath }
- , _file{ utils::parseFile(filePath, readStyle) }
- , _audioProperties{ std::make_unique(computeAudioProperties(*_file)) }
- , _tagReader{ std::make_unique(*_file, enableExtraDebugLogs) }
- , _imageReader{ std::make_unique(*_file) }
+ , _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
+ , _audioProperties{ computeAudioProperties(*_file, filePath) }
{
+ if (parseOptions.readTags)
+ _tagReader = std::make_unique(*_file, parseOptions.enableExtraDebugLogs);
+
+ if (parseOptions.readImages)
+ _imageReader = std::make_unique(*_file);
}
AudioFileInfo::~AudioFileInfo() = default;
- const AudioProperties& AudioFileInfo::getAudioProperties() const
+ const AudioProperties* AudioFileInfo::getAudioProperties() const
{
- return *_audioProperties;
+ return _audioProperties.has_value() ? &_audioProperties.value() : nullptr;
}
- const IImageReader& AudioFileInfo::getImageReader() const
+ const IImageReader* AudioFileInfo::getImageReader() const
{
- return *_imageReader;
+ return _imageReader.get();
}
- const ITagReader& AudioFileInfo::getTagReader() const
+ const ITagReader* AudioFileInfo::getTagReader() const
{
- return *_tagReader;
+ return _tagReader.get();
}
-
} // namespace lms::audio::taglib
diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp
index 4cd85452..00d734ac 100644
--- a/src/libs/audio/impl/taglib/AudioFileInfo.hpp
+++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp
@@ -19,8 +19,12 @@
#pragma once
-#include "audio/AudioTypes.hpp"
+#include
+#include
+
+#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
+#include "audio/IAudioFileInfoParser.hpp"
#include "audio/IImageReader.hpp"
#include "audio/ITagReader.hpp"
@@ -37,20 +41,20 @@ namespace lms::audio::taglib
class AudioFileInfo final : public IAudioFileInfo
{
public:
- AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
+ AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
~AudioFileInfo() override;
AudioFileInfo(const AudioFileInfo&) = delete;
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
private:
- const AudioProperties& getAudioProperties() const override;
- const IImageReader& getImageReader() const override;
- const ITagReader& getTagReader() const override;
+ const AudioProperties* getAudioProperties() const override;
+ const IImageReader* getImageReader() const override;
+ const ITagReader* getTagReader() const override;
const std::filesystem::path _filePath;
std::unique_ptr<::TagLib::File> _file;
- std::unique_ptr _audioProperties;
+ std::optional _audioProperties;
std::unique_ptr _tagReader;
std::unique_ptr _imageReader;
};
diff --git a/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp b/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp
new file mode 100644
index 00000000..bc87c66e
--- /dev/null
+++ b/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "AudioFileInfoParser.hpp"
+
+#include
+
+#include "AudioFileInfo.hpp"
+#include "Utils.hpp"
+
+namespace lms::audio::taglib
+{
+ std::unique_ptr AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
+ {
+ return std::make_unique(p, parseOptions);
+ }
+
+ std::span AudioFileInfoParser::getSupportedExtensions() const
+ {
+ return utils::getSupportedExtensions();
+ }
+} // namespace lms::audio::taglib
\ No newline at end of file
diff --git a/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp b/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp
new file mode 100644
index 00000000..c6e30cc4
--- /dev/null
+++ b/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "audio/IAudioFileInfoParser.hpp"
+
+namespace lms::audio::taglib
+{
+ class AudioFileInfoParser : public IAudioFileInfoParser
+ {
+ private:
+ std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
+ std::span getSupportedExtensions() const override;
+ };
+} // namespace lms::audio::taglib
\ No newline at end of file
diff --git a/src/libs/audio/impl/taglib/ImageReader.cpp b/src/libs/audio/impl/taglib/ImageReader.cpp
index adad0a24..35378549 100644
--- a/src/libs/audio/impl/taglib/ImageReader.cpp
+++ b/src/libs/audio/impl/taglib/ImageReader.cpp
@@ -44,157 +44,157 @@ namespace lms::audio::taglib
{
namespace
{
- Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
+ core::media::ImageType imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
{
switch (type)
{
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
- return Image::Type::Other;
+ return core::media::ImageType::Other;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
- return Image::Type::FileIcon;
+ return core::media::ImageType::FileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
- return Image::Type::OtherFileIcon;
+ return core::media::ImageType::OtherFileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
- return Image::Type::FrontCover;
+ return core::media::ImageType::FrontCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
- return Image::Type::BackCover;
+ return core::media::ImageType::BackCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
- return Image::Type::LeafletPage;
+ return core::media::ImageType::LeafletPage;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
- return Image::Type::Media;
+ return core::media::ImageType::Media;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
- return Image::Type::LeadArtist;
+ return core::media::ImageType::LeadArtist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
- return Image::Type::Artist;
+ return core::media::ImageType::Artist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
- return Image::Type::Conductor;
+ return core::media::ImageType::Conductor;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
- return Image::Type::Band;
+ return core::media::ImageType::Band;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
- return Image::Type::Composer;
+ return core::media::ImageType::Composer;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
- return Image::Type::Lyricist;
+ return core::media::ImageType::Lyricist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
- return Image::Type::RecordingLocation;
+ return core::media::ImageType::RecordingLocation;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
- return Image::Type::DuringRecording;
+ return core::media::ImageType::DuringRecording;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
- return Image::Type::DuringPerformance;
+ return core::media::ImageType::DuringPerformance;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
- return Image::Type::MovieScreenCapture;
+ return core::media::ImageType::MovieScreenCapture;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
- return Image::Type::ColouredFish;
+ return core::media::ImageType::ColouredFish;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
- return Image::Type::Illustration;
+ return core::media::ImageType::Illustration;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
- return Image::Type::BandLogo;
+ return core::media::ImageType::BandLogo;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
- return Image::Type::PublisherLogo;
+ return core::media::ImageType::PublisherLogo;
}
- return Image::Type::Unknown;
+ return core::media::ImageType::Unknown;
}
- Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
+ core::media::ImageType imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
{
switch (type)
{
case ::TagLib::ASF::Picture::Type::Other:
- return Image::Type::Other;
+ return core::media::ImageType::Other;
case ::TagLib::ASF::Picture::Type::FileIcon:
- return Image::Type::FileIcon;
+ return core::media::ImageType::FileIcon;
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
- return Image::Type::OtherFileIcon;
+ return core::media::ImageType::OtherFileIcon;
case ::TagLib::ASF::Picture::Type::FrontCover:
- return Image::Type::FrontCover;
+ return core::media::ImageType::FrontCover;
case ::TagLib::ASF::Picture::Type::BackCover:
- return Image::Type::BackCover;
+ return core::media::ImageType::BackCover;
case ::TagLib::ASF::Picture::Type::LeafletPage:
- return Image::Type::LeafletPage;
+ return core::media::ImageType::LeafletPage;
case ::TagLib::ASF::Picture::Type::Media:
- return Image::Type::Media;
+ return core::media::ImageType::Media;
case ::TagLib::ASF::Picture::Type::LeadArtist:
- return Image::Type::LeadArtist;
+ return core::media::ImageType::LeadArtist;
case ::TagLib::ASF::Picture::Type::Artist:
- return Image::Type::Artist;
+ return core::media::ImageType::Artist;
case ::TagLib::ASF::Picture::Type::Conductor:
- return Image::Type::Conductor;
+ return core::media::ImageType::Conductor;
case ::TagLib::ASF::Picture::Type::Band:
- return Image::Type::Band;
+ return core::media::ImageType::Band;
case ::TagLib::ASF::Picture::Type::Composer:
- return Image::Type::Composer;
+ return core::media::ImageType::Composer;
case ::TagLib::ASF::Picture::Type::Lyricist:
- return Image::Type::Lyricist;
+ return core::media::ImageType::Lyricist;
case ::TagLib::ASF::Picture::Type::RecordingLocation:
- return Image::Type::RecordingLocation;
+ return core::media::ImageType::RecordingLocation;
case ::TagLib::ASF::Picture::Type::DuringRecording:
- return Image::Type::DuringRecording;
+ return core::media::ImageType::DuringRecording;
case ::TagLib::ASF::Picture::Type::DuringPerformance:
- return Image::Type::DuringPerformance;
+ return core::media::ImageType::DuringPerformance;
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
- return Image::Type::MovieScreenCapture;
+ return core::media::ImageType::MovieScreenCapture;
case ::TagLib::ASF::Picture::Type::ColouredFish:
- return Image::Type::ColouredFish;
+ return core::media::ImageType::ColouredFish;
case ::TagLib::ASF::Picture::Type::Illustration:
- return Image::Type::Illustration;
+ return core::media::ImageType::Illustration;
case ::TagLib::ASF::Picture::Type::BandLogo:
- return Image::Type::BandLogo;
+ return core::media::ImageType::BandLogo;
case ::TagLib::ASF::Picture::Type::PublisherLogo:
- return Image::Type::PublisherLogo;
+ return core::media::ImageType::PublisherLogo;
}
- return Image::Type::Unknown;
+ return core::media::ImageType::Unknown;
}
- Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
+ core::media::ImageType imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
{
switch (type)
{
case ::TagLib::FLAC::Picture::Type::Other:
- return Image::Type::Other;
+ return core::media::ImageType::Other;
case ::TagLib::FLAC::Picture::Type::FileIcon:
- return Image::Type::FileIcon;
+ return core::media::ImageType::FileIcon;
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
- return Image::Type::OtherFileIcon;
+ return core::media::ImageType::OtherFileIcon;
case ::TagLib::FLAC::Picture::Type::FrontCover:
- return Image::Type::FrontCover;
+ return core::media::ImageType::FrontCover;
case ::TagLib::FLAC::Picture::Type::BackCover:
- return Image::Type::BackCover;
+ return core::media::ImageType::BackCover;
case ::TagLib::FLAC::Picture::Type::LeafletPage:
- return Image::Type::LeafletPage;
+ return core::media::ImageType::LeafletPage;
case ::TagLib::FLAC::Picture::Type::Media:
- return Image::Type::Media;
+ return core::media::ImageType::Media;
case ::TagLib::FLAC::Picture::Type::LeadArtist:
- return Image::Type::LeadArtist;
+ return core::media::ImageType::LeadArtist;
case ::TagLib::FLAC::Picture::Type::Artist:
- return Image::Type::Artist;
+ return core::media::ImageType::Artist;
case ::TagLib::FLAC::Picture::Type::Conductor:
- return Image::Type::Conductor;
+ return core::media::ImageType::Conductor;
case ::TagLib::FLAC::Picture::Type::Band:
- return Image::Type::Band;
+ return core::media::ImageType::Band;
case ::TagLib::FLAC::Picture::Type::Composer:
- return Image::Type::Composer;
+ return core::media::ImageType::Composer;
case ::TagLib::FLAC::Picture::Type::Lyricist:
- return Image::Type::Lyricist;
+ return core::media::ImageType::Lyricist;
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
- return Image::Type::RecordingLocation;
+ return core::media::ImageType::RecordingLocation;
case ::TagLib::FLAC::Picture::Type::DuringRecording:
- return Image::Type::DuringRecording;
+ return core::media::ImageType::DuringRecording;
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
- return Image::Type::DuringPerformance;
+ return core::media::ImageType::DuringPerformance;
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
- return Image::Type::MovieScreenCapture;
+ return core::media::ImageType::MovieScreenCapture;
case ::TagLib::FLAC::Picture::Type::ColouredFish:
- return Image::Type::ColouredFish;
+ return core::media::ImageType::ColouredFish;
case ::TagLib::FLAC::Picture::Type::Illustration:
- return Image::Type::Illustration;
+ return core::media::ImageType::Illustration;
case ::TagLib::FLAC::Picture::Type::BandLogo:
- return Image::Type::BandLogo;
+ return core::media::ImageType::BandLogo;
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
- return Image::Type::PublisherLogo;
+ return core::media::ImageType::PublisherLogo;
}
- return Image::Type::Unknown;
+ return core::media::ImageType::Unknown;
}
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
@@ -217,14 +217,14 @@ namespace lms::audio::taglib
}
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
- Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
+ core::media::ImageType imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
- return Image::Type::FrontCover;
+ return core::media::ImageType::FrontCover;
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
- return Image::Type::BackCover;
+ return core::media::ImageType::BackCover;
- return Image::Type::Unknown;
+ return core::media::ImageType::Unknown;
}
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
@@ -294,7 +294,7 @@ namespace lms::audio::taglib
image.data = pictureData;
// By convention, consider the first cover art as the front cover
- image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown;
+ image.type = firstCover ? core::media::ImageType::FrontCover : core::media::ImageType::Unknown;
firstCover = false;
visitor(image);
diff --git a/src/libs/audio/impl/taglib/Utils.cpp b/src/libs/audio/impl/taglib/Utils.cpp
index aebdaf05..594a8b10 100644
--- a/src/libs/audio/impl/taglib/Utils.cpp
+++ b/src/libs/audio/impl/taglib/Utils.cpp
@@ -47,7 +47,7 @@
#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
-#include "audio/IAudioFileInfo.hpp"
+#include "audio/Exception.hpp"
namespace lms::audio::taglib::utils
{
@@ -89,15 +89,15 @@ namespace lms::audio::taglib::utils
return std::span{ supportedExtensions };
}
- TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
+ TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
{
switch (readStyle)
{
- case ParserOptions::AudioPropertiesReadStyle::Fast:
+ case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast:
return TagLib::AudioProperties::ReadStyle::Fast;
- case ParserOptions::AudioPropertiesReadStyle::Average:
+ case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average:
return TagLib::AudioProperties::ReadStyle::Average;
- case ParserOptions::AudioPropertiesReadStyle::Accurate:
+ case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Accurate:
return TagLib::AudioProperties::ReadStyle::Accurate;
}
@@ -111,7 +111,7 @@ namespace lms::audio::taglib::utils
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
- throw IOException{ "fopen failed", ec };
+ throw IOFileException{ p, "fopen failed", ec };
}
int fd{ ::fileno(file) };
@@ -119,7 +119,7 @@ namespace lms::audio::taglib::utils
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
- throw IOException{ "fileno failed", ec };
+ throw IOFileException{ p, "fileno failed", ec };
}
return TagLib::FileStream{ fd, true };
@@ -236,7 +236,7 @@ namespace lms::audio::taglib::utils
return file;
}
- std::unique_ptr parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
+ std::unique_ptr parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
@@ -252,10 +252,7 @@ namespace lms::audio::taglib::utils
}
if (!file)
- throw AudioFileParsingException{ "Parsing failed" };
-
- if (!file->audioProperties())
- throw AudioFileParsingException{ "No audio properties" };
+ throw Exception{ "Parsing failed" };
return file;
}
diff --git a/src/libs/audio/impl/taglib/Utils.hpp b/src/libs/audio/impl/taglib/Utils.hpp
index f79e96bf..fa614508 100644
--- a/src/libs/audio/impl/taglib/Utils.hpp
+++ b/src/libs/audio/impl/taglib/Utils.hpp
@@ -25,10 +25,10 @@
#include
-#include "audio/IAudioFileInfo.hpp"
+#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::taglib::utils
{
std::span getSupportedExtensions();
- std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
+ std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle);
} // namespace lms::audio::taglib::utils
\ No newline at end of file
diff --git a/src/libs/audio/include/audio/AudioProperties.hpp b/src/libs/audio/include/audio/AudioProperties.hpp
new file mode 100644
index 00000000..07dadd0b
--- /dev/null
+++ b/src/libs/audio/include/audio/AudioProperties.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
+#include
+
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
+
+namespace lms::audio
+{
+ struct AudioProperties
+ {
+ core::media::Container container;
+ core::media::Codec codec;
+ std::chrono::milliseconds duration;
+ unsigned bitrate;
+ unsigned channelCount;
+ unsigned sampleRate;
+ std::optional bitsPerSample;
+ };
+} // namespace lms::audio
\ No newline at end of file
diff --git a/src/libs/audio/include/audio/Exception.hpp b/src/libs/audio/include/audio/Exception.hpp
index 13f13437..feaa9560 100644
--- a/src/libs/audio/include/audio/Exception.hpp
+++ b/src/libs/audio/include/audio/Exception.hpp
@@ -19,6 +19,9 @@
#pragma once
+#include
+#include
+
#include "core/Exception.hpp"
namespace lms::audio
@@ -28,4 +31,23 @@ namespace lms::audio
public:
using LmsException::LmsException;
};
+
+ class IOFileException : public Exception
+ {
+ public:
+ IOFileException(const std::filesystem::path& path, std::string_view message, std::error_code err)
+ : Exception{ "File '" + path.string() + "': " + std::string{ message } + ": " + err.message() }
+ , _path{ path }
+ , _err{ err }
+ {
+ }
+
+ const std::filesystem::path& getPath() const { return _path; }
+ std::error_code getErrorCode() const { return _err; }
+
+ private:
+ std::filesystem::path _path;
+ std::error_code _err;
+ };
+
} // namespace lms::audio
diff --git a/src/libs/audio/include/audio/IAudioFileInfo.hpp b/src/libs/audio/include/audio/IAudioFileInfo.hpp
index b417e78b..adb707e5 100644
--- a/src/libs/audio/include/audio/IAudioFileInfo.hpp
+++ b/src/libs/audio/include/audio/IAudioFileInfo.hpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 Emeric Poupon
+ * Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,14 +19,9 @@
#pragma once
-#include
-#include
-
-#include "audio/Exception.hpp"
-
namespace lms::audio
{
- class AudioProperties;
+ struct AudioProperties;
class IImageReader;
class ITagReader;
@@ -35,52 +30,8 @@ namespace lms::audio
public:
virtual ~IAudioFileInfo() = default;
- virtual const AudioProperties& getAudioProperties() const = 0;
- virtual const IImageReader& getImageReader() const = 0;
- virtual const ITagReader& getTagReader() const = 0;
+ virtual const AudioProperties* getAudioProperties() const = 0; // may be null even if requested, due to backend limitation
+ virtual const IImageReader* getImageReader() const = 0;
+ virtual const ITagReader* getTagReader() const = 0;
};
-
- class AudioFileParsingException : public Exception
- {
- public:
- using Exception::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;
- };
-
- struct ParserOptions
- {
- enum class Parser
- {
- TagLib,
- FFmpeg,
- };
-
- enum class AudioPropertiesReadStyle
- {
- Fast,
- Average,
- Accurate,
- };
-
- Parser parser{ Parser::TagLib };
- AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
- bool enableExtraDebugLogs{};
- };
- std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
-
- std::span getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
} // namespace lms::audio
diff --git a/src/libs/audio/include/audio/IAudioFileInfoParser.hpp b/src/libs/audio/include/audio/IAudioFileInfoParser.hpp
new file mode 100644
index 00000000..45350209
--- /dev/null
+++ b/src/libs/audio/include/audio/IAudioFileInfoParser.hpp
@@ -0,0 +1,61 @@
+/*
+ * 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::audio
+{
+ class IAudioFileInfo;
+
+ struct AudioFileInfoParseOptions
+ {
+ enum class AudioPropertiesReadStyle
+ {
+ Fast, // fastest
+ Average, // normal
+ Accurate, // full scan
+ };
+
+ AudioPropertiesReadStyle audioPropertiesReadStyle{ AudioPropertiesReadStyle::Average };
+ bool readTags{ true };
+ bool readImages{ true };
+
+ bool enableExtraDebugLogs{};
+ };
+
+ class IAudioFileInfoParser
+ {
+ public:
+ virtual ~IAudioFileInfoParser() = default;
+
+ virtual std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const = 0;
+ virtual std::span getSupportedExtensions() const = 0;
+ };
+
+ enum class AudioFileInfoParserBackend
+ {
+ TagLib,
+ FFmpeg,
+ };
+ static inline constexpr AudioFileInfoParserBackend defaultAudioFileInfoParserBackend{ AudioFileInfoParserBackend::TagLib };
+ std::unique_ptr createAudioFileInfoParser(AudioFileInfoParserBackend backend = defaultAudioFileInfoParserBackend);
+} // namespace lms::audio
diff --git a/src/libs/audio/include/audio/IImageReader.hpp b/src/libs/audio/include/audio/IImageReader.hpp
index 0063151c..8d301b45 100644
--- a/src/libs/audio/include/audio/IImageReader.hpp
+++ b/src/libs/audio/include/audio/IImageReader.hpp
@@ -23,69 +23,18 @@
#include
#include
-#include "core/LiteralString.hpp"
+#include "core/media/ImageType.hpp"
namespace lms::audio
{
struct Image
{
- // See TagLib types (based on ID3v2 APIC types)
- enum class Type
- {
- // No information
- Unknown,
- // A type not enumerated below
- Other,
- // 32x32 PNG image that should be used as the file icon
- FileIcon,
- // File icon of a different size or format
- OtherFileIcon,
- // Front cover image of the album
- FrontCover,
- // Back cover image of the album
- BackCover,
- // Inside leaflet page of the album
- LeafletPage,
- // Image from the album itself
- Media,
- // Picture of the lead artist or soloist
- LeadArtist,
- // Picture of the artist or performer
- Artist,
- // Picture of the conductor
- Conductor,
- // Picture of the band or orchestra
- Band,
- // Picture of the composer
- Composer,
- // Picture of the lyricist or text writer
- Lyricist,
- // Picture of the recording location or studio
- RecordingLocation,
- // Picture of the artists during recording
- DuringRecording,
- // Picture of the artists during performance
- DuringPerformance,
- // Picture from a movie or video related to the track
- MovieScreenCapture,
- // Picture of a large, coloured fish
- ColouredFish,
- // Illustration related to the track
- Illustration,
- // Logo of the band or performer
- BandLogo,
- // Logo of the publisher (record company)
- PublisherLogo
- };
-
- Type type{ Type::Unknown };
+ core::media::ImageType type{ core::media::ImageType::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span data;
};
- core::LiteralString imageTypeToString(Image::Type type);
-
class IImageReader
{
public:
diff --git a/src/libs/audio/include/audio/TranscodeTypes.hpp b/src/libs/audio/include/audio/TranscodeTypes.hpp
index 1aaf34cf..0dfcf4b3 100644
--- a/src/libs/audio/include/audio/TranscodeTypes.hpp
+++ b/src/libs/audio/include/audio/TranscodeTypes.hpp
@@ -23,28 +23,30 @@
#include
#include
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
+
+#include "audio/AudioProperties.hpp"
+
namespace lms::audio
{
- // TODO deprecate?
- enum class OutputFormat
- {
- MP3,
- OGG_OPUS,
- MATROSKA_OPUS,
- OGG_VORBIS,
- WEBM_VORBIS,
- };
-
struct TranscodeInputParameters
{
std::filesystem::path filePath;
- std::chrono::milliseconds duration{}; // Duration of the audio file
- std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
+ AudioProperties audioProperties; // properties of the audio file
+ std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
+ };
+
+ struct TranscodeOutputFormat
+ {
+ core::media::Container container;
+ core::media::Codec codec;
};
struct TranscodeOutputParameters
{
- std::optional format;
+ // Not setting a value here means to use the same value as the input
+ std::optional format;
std::optional bitrate;
std::optional bitsPerSample;
std::optional channelCount;
diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt
index d25b4264..d8ddf79d 100644
--- a/src/libs/core/CMakeLists.txt
+++ b/src/libs/core/CMakeLists.txt
@@ -13,6 +13,10 @@ configure_file(
add_library(lmscore STATIC
impl/http/Client.cpp
impl/http/SendQueue.cpp
+ impl/media/Codec.cpp
+ impl/media/Container.cpp
+ impl/media/ImageType.cpp
+ impl/media/MimeType.cpp
impl/ArchiveZipper.cpp
impl/ChildProcess.cpp
impl/ChildProcessManager.cpp
diff --git a/src/libs/core/impl/Logger.cpp b/src/libs/core/impl/Logger.cpp
index a5f20884..fbebb231 100644
--- a/src/libs/core/impl/Logger.cpp
+++ b/src/libs/core/impl/Logger.cpp
@@ -144,33 +144,33 @@ namespace lms::core::logging
{
case core::logging::Severity::DEBUG:
if (_logFileStream)
- addOutputStream(*_logFileStream, { core::logging::Severity::DEBUG });
+ addOutputStream(*_logFileStream, core::logging::Severity::DEBUG);
else
- addOutputStream(std::cout, { core::logging::Severity::DEBUG });
+ addOutputStream(std::cout, core::logging::Severity::DEBUG);
[[fallthrough]];
case core::logging::Severity::INFO:
if (_logFileStream)
- addOutputStream(*_logFileStream, { core::logging::Severity::INFO });
+ addOutputStream(*_logFileStream, core::logging::Severity::INFO);
else
- addOutputStream(std::cout, { core::logging::Severity::INFO });
+ addOutputStream(std::cout, core::logging::Severity::INFO);
[[fallthrough]];
case core::logging::Severity::WARNING:
if (_logFileStream)
- addOutputStream(*_logFileStream, { core::logging::Severity::WARNING });
+ addOutputStream(*_logFileStream, core::logging::Severity::WARNING);
else
- addOutputStream(std::cerr, { core::logging::Severity::WARNING });
+ addOutputStream(std::cerr, core::logging::Severity::WARNING);
[[fallthrough]];
case core::logging::Severity::ERROR:
if (_logFileStream)
- addOutputStream(*_logFileStream, { core::logging::Severity::ERROR });
+ addOutputStream(*_logFileStream, core::logging::Severity::ERROR);
else
- addOutputStream(std::cerr, { core::logging::Severity::ERROR });
+ addOutputStream(std::cerr, core::logging::Severity::ERROR);
[[fallthrough]];
case core::logging::Severity::FATAL:
if (_logFileStream)
- addOutputStream(*_logFileStream, { core::logging::Severity::FATAL });
+ addOutputStream(*_logFileStream, core::logging::Severity::FATAL);
else
- addOutputStream(std::cerr, { core::logging::Severity::FATAL });
+ addOutputStream(std::cerr, core::logging::Severity::FATAL);
break;
}
}
@@ -206,5 +206,4 @@ namespace lms::core::logging
std::unique_lock lock{ outputStream->mutex };
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
}
-
} // namespace lms::core::logging
\ No newline at end of file
diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp
index 50a9035c..99db1e12 100644
--- a/src/libs/core/impl/String.cpp
+++ b/src/libs/core/impl/String.cpp
@@ -31,7 +31,7 @@
namespace lms::core::stringUtils
{
- namespace details
+ namespace detail
{
constexpr std::pair jsEscapeChars[]{
{ '\\', "\\\\" },
@@ -203,7 +203,7 @@ namespace lms::core::stringUtils
return std::distance(std::cbegin(months), str) + 1;
}
- } // namespace details
+ } // namespace detail
template<>
std::optional readAs(std::string_view str)
@@ -240,32 +240,32 @@ namespace lms::core::stringUtils
std::vector splitString(std::string_view str, std::span separators)
{
- return details::splitString(str, separators);
+ return detail::splitString(str, separators);
}
std::vector splitString(std::string_view str, std::span separators)
{
- return details::splitString(str, separators);
+ return detail::splitString(str, separators);
}
std::string joinStrings(std::span strings, std::string_view delimiter)
{
- return details::joinStrings(strings, delimiter);
+ return detail::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span strings, std::string_view delimiter)
{
- return details::joinStrings(strings, delimiter);
+ return detail::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span strings, char delimiter)
{
- return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
+ return detail::joinStrings(strings, std::string_view{ &delimiter, 1 });
}
std::string joinStrings(std::span strings, char delimiter)
{
- return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
+ return detail::joinStrings(strings, std::string_view{ &delimiter, 1 });
}
std::string escapeAndJoinStrings(std::span strings, char delimiter, char escapeChar)
@@ -434,32 +434,32 @@ namespace lms::core::stringUtils
std::string jsEscape(std::string_view str)
{
- return details::escape(str, details::jsEscapeChars);
+ return detail::escape(str, detail::jsEscapeChars);
}
void writeJSEscapedString(std::ostream& os, std::string_view str)
{
- details::writeEscapedString(os, str, details::jsEscapeChars);
+ detail::writeEscapedString(os, str, detail::jsEscapeChars);
}
std::string jsonEscape(std::string_view str)
{
- return details::escape(str, details::jsonEscapeChars);
+ return detail::escape(str, detail::jsonEscapeChars);
}
void writeJsonEscapedString(std::ostream& os, std::string_view str)
{
- details::writeEscapedString(os, str, details::jsonEscapeChars);
+ detail::writeEscapedString(os, str, detail::jsonEscapeChars);
}
std::string xmlEscape(std::string_view str)
{
- return details::escape(str, details::xmlEscapeChars);
+ return detail::escape(str, detail::xmlEscapeChars);
}
void writeXmlEscapedString(std::ostream& os, std::string_view str)
{
- details::writeEscapedString(os, str, details::xmlEscapeChars);
+ detail::writeEscapedString(os, str, detail::xmlEscapeChars);
}
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
@@ -611,11 +611,11 @@ namespace lms::core::stringUtils
timeStr += ":00";
// Normalize zone
- const std::optional offset{ details::getRFC822ZoneOffset(zoneStr) };
+ const std::optional offset{ detail::getRFC822ZoneOffset(zoneStr) };
if (!offset)
return {};
- std::optional month{ details::getRFC822Month(monthStr) };
+ std::optional month{ detail::getRFC822Month(monthStr) };
if (!month)
return {};
diff --git a/src/libs/core/impl/media/Codec.cpp b/src/libs/core/impl/media/Codec.cpp
new file mode 100644
index 00000000..17ff0a5e
--- /dev/null
+++ b/src/libs/core/impl/media/Codec.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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/media/Codec.hpp"
+#include "core/Exception.hpp"
+
+#include
+#include
+
+namespace lms::core::media
+{
+ namespace
+ {
+ constexpr std::array codecDescs{
+ CodecDesc{ Codec::AAC, "AAC", "Advanced Audio Coding", false },
+ CodecDesc{ Codec::AC3, "AC3", "Dolby AC-3", false },
+ CodecDesc{ Codec::ALAC, "ALAC", "Apple Lossless Audio Codec", true },
+ CodecDesc{ Codec::APE, "APE", "Monkey's Audio", true },
+ CodecDesc{ Codec::DSD, "DSD", "Direct Stream Digital", true },
+ CodecDesc{ Codec::EAC3, "E-AC3", "Dolby Digital Plus", false },
+ CodecDesc{ Codec::FLAC, "FLAC", "Free Lossless Audio Codec", true },
+ CodecDesc{ Codec::MP3, "MP3", "MPEG-3", false },
+ CodecDesc{ Codec::MP4ALS, "MP4ALS", "MPEG-4 Audio Lossless Coding", true },
+ CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false },
+ CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false },
+ CodecDesc{ Codec::Opus, "Opus", "Opus", false },
+ CodecDesc{ Codec::PCM, "PCM", "Pulse-code modulation", false },
+ CodecDesc{ Codec::Shorten, "Shorten", "Shorten", true },
+ CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true },
+ CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false },
+ CodecDesc{ Codec::WavPack, "WavPack", "WavPack", true },
+ CodecDesc{ Codec::WMA1, "WMA1", "Windows Media Audio", false },
+ CodecDesc{ Codec::WMA2, "WMA2", "Windows Media Audio 2", false },
+ CodecDesc{ Codec::WMA9Pro, "WMA9Pro", "Windows Media Audio Professional", false },
+ CodecDesc{ Codec::WMA9Lossless, "WMA9Lossless", "Windows Media Audio Lossless", true },
+ };
+ } // namespace
+
+ void visitCodecs(const std::function& visitor)
+ {
+ std::for_each(std::cbegin(codecDescs), std::cend(codecDescs), visitor);
+ }
+
+ const CodecDesc& getCodecDesc(Codec type)
+ {
+ const auto it{ std::find_if(codecDescs.begin(), codecDescs.end(), [type](const auto& desc) { return desc.type == type; }) };
+ if (it == codecDescs.end()) [[unlikely]]
+ throw LmsException{ "Unknown codec type" };
+
+ return *it;
+ }
+} // namespace lms::core::media
diff --git a/src/libs/core/impl/media/Container.cpp b/src/libs/core/impl/media/Container.cpp
new file mode 100644
index 00000000..6bb0698e
--- /dev/null
+++ b/src/libs/core/impl/media/Container.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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/media/Container.hpp"
+
+namespace lms::core::media
+{
+ core::LiteralString containerToString(Container type)
+ {
+ switch (type)
+ {
+ case Container::AIFF:
+ return "AIFF";
+ case Container::APE:
+ return "APE";
+ case Container::ASF:
+ return "ASF";
+ case Container::DSF:
+ return "DSF";
+ case Container::FLAC:
+ return "FLAC";
+ case Container::MP4:
+ return "MP4";
+ case Container::MPC:
+ return "MPC";
+ case Container::MPEG:
+ return "MPEG";
+ case Container::Ogg:
+ return "Ogg";
+ case Container::Shorten:
+ return "Shorten";
+ case Container::TrueAudio:
+ return "TrueAudio";
+ case Container::WAV:
+ return "WAV";
+ case Container::WavPack:
+ return "WavPack";
+ }
+
+ return "Unknown";
+ }
+} // namespace lms::core::media
\ No newline at end of file
diff --git a/src/libs/audio/impl/ImageReader.cpp b/src/libs/core/impl/media/ImageType.cpp
similarity index 59%
rename from src/libs/audio/impl/ImageReader.cpp
rename to src/libs/core/impl/media/ImageType.cpp
index 7b314875..894f1150 100644
--- a/src/libs/audio/impl/ImageReader.cpp
+++ b/src/libs/core/impl/media/ImageType.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 Emeric Poupon
+ * Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,61 +17,60 @@
* along with LMS. If not, see .
*/
-#include "audio/IImageReader.hpp"
+#include "core/media/ImageType.hpp"
-namespace lms::audio
+namespace lms::core::media
{
- core::LiteralString imageTypeToString(Image::Type type)
+ core::LiteralString imageTypeToString(ImageType type)
{
switch (type)
{
- case Image::Type::Other:
+ case ImageType::Other:
return "Other";
- case Image::Type::FileIcon:
+ case ImageType::FileIcon:
return "FileIcon";
- case Image::Type::OtherFileIcon:
+ case ImageType::OtherFileIcon:
return "OtherFileIcon";
- case Image::Type::FrontCover:
+ case ImageType::FrontCover:
return "FrontCover";
- case Image::Type::BackCover:
+ case ImageType::BackCover:
return "BackCover";
- case Image::Type::LeafletPage:
+ case ImageType::LeafletPage:
return "LeafletPage";
- case Image::Type::Media:
+ case ImageType::Media:
return "Media";
- case Image::Type::LeadArtist:
+ case ImageType::LeadArtist:
return "LeadArtist";
- case Image::Type::Artist:
+ case ImageType::Artist:
return "Artist";
- case Image::Type::Conductor:
+ case ImageType::Conductor:
return "Conductor";
- case Image::Type::Band:
+ case ImageType::Band:
return "Band";
- case Image::Type::Composer:
+ case ImageType::Composer:
return "Composer";
- case Image::Type::Lyricist:
+ case ImageType::Lyricist:
return "Lyricist";
- case Image::Type::RecordingLocation:
+ case ImageType::RecordingLocation:
return "RecordingLocation";
- case Image::Type::DuringRecording:
+ case ImageType::DuringRecording:
return "DuringRecording";
- case Image::Type::DuringPerformance:
+ case ImageType::DuringPerformance:
return "DuringPerformance";
- case Image::Type::MovieScreenCapture:
+ case ImageType::MovieScreenCapture:
return "MovieScreenCapture";
- case Image::Type::ColouredFish:
+ case ImageType::ColouredFish:
return "ColouredFish";
- case Image::Type::Illustration:
+ case ImageType::Illustration:
return "Illustration";
- case Image::Type::BandLogo:
+ case ImageType::BandLogo:
return "BandLogo";
- case Image::Type::PublisherLogo:
+ case ImageType::PublisherLogo:
return "PublisherLogo";
- case Image::Type::Unknown:
+ case ImageType::Unknown:
break;
}
return "Unknown";
}
-
-} // namespace lms::audio
\ No newline at end of file
+} // namespace lms::core::media
\ No newline at end of file
diff --git a/src/libs/core/impl/media/MimeType.cpp b/src/libs/core/impl/media/MimeType.cpp
new file mode 100644
index 00000000..ef17cba3
--- /dev/null
+++ b/src/libs/core/impl/media/MimeType.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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/media/MimeType.hpp"
+
+namespace lms::core::media
+{
+ core::LiteralString getMimeType(Container container, Codec codec)
+ {
+ switch (container)
+ {
+ case Container::AIFF:
+ return "audio/x-aiff";
+ case Container::APE:
+ return "audio/x-monkeys-audio";
+ case Container::ASF:
+ return "audio/x-ms-wma";
+ case Container::DSF:
+ return "audio/x-dsd-dsf";
+ case Container::FLAC:
+ return "audio/flac";
+ case Container::MP4:
+ switch (codec)
+ {
+ case Codec::AAC:
+ return "audio/mp4; codecs=\"mp4a.40.2\"";
+ case Codec::ALAC:
+ return "audio/mp4; codecs=\"alac\"";
+ case Codec::MP4ALS:
+ return "audio/mp4; codecs=\"mp4als\"";
+ default:
+ return "audio/mp4";
+ }
+
+ case Container::MPC:
+ return "audio/x-musepack";
+ case Container::MPEG:
+ return "audio/mpeg";
+ case Container::Ogg:
+ switch (codec)
+ {
+ case Codec::Opus:
+ return "audio/opus";
+ case Codec::Vorbis:
+ return "audio/ogg; codecs=\"vorbis\"";
+ case Codec::FLAC:
+ return "audio/ogg; codecs=\"flac\"";
+ default:
+ return "audio/ogg";
+ }
+
+ case Container::Shorten:
+ return "audio/x-shn";
+ case Container::TrueAudio:
+ return "audio/x-tta";
+ case Container::WAV:
+ return "audio/wav";
+ case Container::WavPack:
+ return "audio/x-wavpack";
+ }
+
+ return "application/octet-stream";
+ }
+} // namespace lms::core::media
\ No newline at end of file
diff --git a/src/libs/core/include/core/EnumSet.hpp b/src/libs/core/include/core/EnumSet.hpp
index dc035610..c2ec5c73 100644
--- a/src/libs/core/include/core/EnumSet.hpp
+++ b/src/libs/core/include/core/EnumSet.hpp
@@ -19,6 +19,7 @@
#pragma once
+#include
#include
#include
#include
@@ -51,6 +52,11 @@ namespace lms::core
assign(begin, end);
}
+ constexpr std::size_t size() const
+ {
+ return std::popcount(_bitfield);
+ }
+
template
constexpr void assign(It begin, It end)
{
diff --git a/src/libs/core/include/core/Tuple.hpp b/src/libs/core/include/core/Tuple.hpp
index 128ad0e3..c64019c7 100644
--- a/src/libs/core/include/core/Tuple.hpp
+++ b/src/libs/core/include/core/Tuple.hpp
@@ -23,7 +23,7 @@
namespace lms::core
{
- namespace details
+ namespace detail
{
template
struct Seq
@@ -45,11 +45,11 @@ namespace lms::core
{
auto l = { (f(std::get(t)), 0)... };
}
- } // namespace details
+ } // namespace detail
template
void forEachTypeInTuple(std::tuple const& t, Func f)
{
- details::forEachTypeInTuple(t, f, details::GenSeq());
+ detail::forEachTypeInTuple(t, f, detail::GenSeq());
}
} // namespace lms::core
\ No newline at end of file
diff --git a/src/libs/core/include/core/UUID.hpp b/src/libs/core/include/core/UUID.hpp
index b57d4628..422bba75 100644
--- a/src/libs/core/include/core/UUID.hpp
+++ b/src/libs/core/include/core/UUID.hpp
@@ -19,6 +19,7 @@
#pragma once
+#include
#include
#include
#include
@@ -49,3 +50,15 @@ namespace lms::core::stringUtils
std::optional
readAs(std::string_view str);
}
+
+namespace std
+{
+ template<>
+ struct hash
+ {
+ size_t operator()(const lms::core::UUID& str) const
+ {
+ return hash{}(str.getAsString());
+ }
+ };
+} // namespace std
diff --git a/src/libs/audio/include/audio/AudioTypes.hpp b/src/libs/core/include/core/media/Codec.hpp
similarity index 61%
rename from src/libs/audio/include/audio/AudioTypes.hpp
rename to src/libs/core/include/core/media/Codec.hpp
index 84478949..06438922 100644
--- a/src/libs/audio/include/audio/AudioTypes.hpp
+++ b/src/libs/core/include/core/media/Codec.hpp
@@ -19,38 +19,20 @@
#pragma once
-#include
-#include
+#include
#include "core/LiteralString.hpp"
-namespace lms::audio
+namespace lms::core::media
{
- enum class ContainerType
- {
- AIFF,
- APE, // Monkey's Audio
- ASF, // Advanced Systems Format
- DSF,
- FLAC,
- MP4,
- MPC, // Musepack
- MPEG,
- Ogg,
- Shorten,
- TrueAudio,
- WAV,
- WavPack,
- };
-
- core::LiteralString containerTypeToString(ContainerType type);
-
- enum class CodecType
+ enum class Codec
{
AAC,
+ AC3,
ALAC, // Apple Lossless Audio Codec (ALAC)
APE, // Monkey's Audio
DSD, // DSD
+ EAC3,
FLAC, // Flac
MP3,
MP4ALS, // MPEG-4 Audio Lossless Coding
@@ -68,16 +50,13 @@ namespace lms::audio
WMA9Lossless,
};
- core::LiteralString codecTypeToString(CodecType type);
-
- struct AudioProperties
+ struct CodecDesc
{
- ContainerType container;
- CodecType codec;
- std::chrono::milliseconds duration;
- unsigned bitrate;
- unsigned channelCount;
- unsigned sampleRate;
- std::optional bitsPerSample;
+ Codec type;
+ core::LiteralString name;
+ core::LiteralString longName;
+ bool isLossless;
};
-} // namespace lms::audio
\ No newline at end of file
+ void visitCodecs(const std::function& visitor);
+ const CodecDesc& getCodecDesc(Codec type);
+} // namespace lms::core::media
diff --git a/src/libs/core/include/core/media/Container.hpp b/src/libs/core/include/core/media/Container.hpp
new file mode 100644
index 00000000..b53af373
--- /dev/null
+++ b/src/libs/core/include/core/media/Container.hpp
@@ -0,0 +1,44 @@
+/*
+ * 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 "core/LiteralString.hpp"
+
+namespace lms::core::media
+{
+ enum class Container
+ {
+ AIFF,
+ APE, // Monkey's Audio
+ ASF, // Advanced Systems Format
+ DSF,
+ FLAC,
+ MP4,
+ MPC, // Musepack
+ MPEG,
+ Ogg,
+ Shorten,
+ TrueAudio,
+ WAV,
+ WavPack,
+ };
+
+ core::LiteralString containerToString(Container type);
+} // namespace lms::core::media
\ No newline at end of file
diff --git a/src/libs/core/include/core/media/ImageType.hpp b/src/libs/core/include/core/media/ImageType.hpp
new file mode 100644
index 00000000..de710938
--- /dev/null
+++ b/src/libs/core/include/core/media/ImageType.hpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "core/LiteralString.hpp"
+
+namespace lms::core::media
+{
+ // Based on ID3v2 APIC types
+ enum class ImageType
+ {
+ // No information
+ Unknown,
+ // A type not enumerated below
+ Other,
+ // 32x32 PNG image that should be used as the file icon
+ FileIcon,
+ // File icon of a different size or format
+ OtherFileIcon,
+ // Front cover image of the album
+ FrontCover,
+ // Back cover image of the album
+ BackCover,
+ // Inside leaflet page of the album
+ LeafletPage,
+ // Image from the album itself
+ Media,
+ // Picture of the lead artist or soloist
+ LeadArtist,
+ // Picture of the artist or performer
+ Artist,
+ // Picture of the conductor
+ Conductor,
+ // Picture of the band or orchestra
+ Band,
+ // Picture of the composer
+ Composer,
+ // Picture of the lyricist or text writer
+ Lyricist,
+ // Picture of the recording location or studio
+ RecordingLocation,
+ // Picture of the artists during recording
+ DuringRecording,
+ // Picture of the artists during performance
+ DuringPerformance,
+ // Picture from a movie or video related to the track
+ MovieScreenCapture,
+ // Picture of a large, coloured fish
+ ColouredFish,
+ // Illustration related to the track
+ Illustration,
+ // Logo of the band or performer
+ BandLogo,
+ // Logo of the publisher (record company)
+ PublisherLogo
+ };
+
+ core::LiteralString imageTypeToString(ImageType type);
+} // namespace lms::core::media
\ No newline at end of file
diff --git a/src/libs/core/include/core/media/MimeType.hpp b/src/libs/core/include/core/media/MimeType.hpp
new file mode 100644
index 00000000..6f24f545
--- /dev/null
+++ b/src/libs/core/include/core/media/MimeType.hpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "core/LiteralString.hpp"
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
+
+namespace lms::core::media
+{
+ core::LiteralString getMimeType(Container container, Codec codec);
+}
\ No newline at end of file
diff --git a/src/libs/core/test/EnumSet.cpp b/src/libs/core/test/EnumSet.cpp
index cc37ce8e..6679ccae 100644
--- a/src/libs/core/test/EnumSet.cpp
+++ b/src/libs/core/test/EnumSet.cpp
@@ -31,9 +31,19 @@ namespace lms::core
Two,
};
+ {
+ constexpr EnumSet test;
+
+ static_assert(test.size() == 0);
+ static_assert(test.empty());
+ static_assert(!test.contains(Foo::One));
+ static_assert(!test.contains(Foo::Two));
+ }
+
{
constexpr EnumSet test{ Foo::One };
+ static_assert(test.size() == 1);
static_assert(!test.empty());
static_assert(test.contains(Foo::One));
static_assert(!test.contains(Foo::Two));
@@ -49,6 +59,8 @@ namespace lms::core
constexpr EnumSet test{ Foo::One, Foo::Two };
constexpr auto bitfield{ test.getBitfield() };
+ static_assert(test.size() == 2);
+
EnumSet test2;
EXPECT_FALSE(test2.contains(Foo::One));
EXPECT_FALSE(test2.contains(Foo::Two));
diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt
index 4e79b43a..2ba86032 100644
--- a/src/libs/database/CMakeLists.txt
+++ b/src/libs/database/CMakeLists.txt
@@ -1,4 +1,5 @@
add_library(lmsdatabase STATIC
+ impl/objects/detail/Types.cpp
impl/objects/Artist.cpp
impl/objects/ArtistInfo.cpp
impl/objects/Artwork.cpp
@@ -29,6 +30,7 @@ add_library(lmsdatabase STATIC
impl/objects/TrackEmbeddedImage.cpp
impl/objects/TrackEmbeddedImageLink.cpp
impl/objects/TrackLyrics.cpp
+ impl/objects/Types.cpp
impl/objects/UIState.cpp
impl/objects/User.cpp
impl/Db.cpp
@@ -39,7 +41,6 @@ add_library(lmsdatabase STATIC
impl/Session.cpp
impl/SqlQuery.cpp
impl/Transaction.cpp
- impl/Types.cpp
impl/Utils.cpp
)
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index cda0492a..639c8f08 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -24,6 +24,7 @@
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
+
#include "database/Session.hpp"
#include "database/objects/ScanSettings.hpp"
@@ -34,7 +35,7 @@ namespace lms::db
{
namespace
{
- static constexpr Version LMS_DATABASE_VERSION{ 100 };
+ static constexpr Version LMS_DATABASE_VERSION{ 102 };
}
VersionInfo::VersionInfo()
@@ -1581,6 +1582,105 @@ FROM track)");
constraint "fk_podcast_episode_podcast" foreign key ("podcast_id") references "podcast" ("id") on delete cascade deferrable initially deferred))");
}
+ void migrateFromV100(Session& session)
+ {
+ // Add container and codec to track table + made bits_per_sample optional
+
+ 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,
+ "absolute_file_path" text not null,
+ "file_size" bigint not null,
+ "file_last_write" text,
+ "file_added" text,
+ "duration" integer,
+ "container" integer not null,
+ "codec" integer not null,
+ "bitrate" integer not null,
+ "channel_count" integer not null,
+ "sample_rate" integer not null,
+ "bits_per_sample" integer,
+ "replay_gain" real,
+ "track_number" integer,
+ "name" text not null,
+ "date" text,
+ "original_date" text,
+ "mbid" text not null,
+ "recording_mbid" text not null,
+ "copyright" text not null,
+ "copyright_url" text not null,
+ "artist_display_name" text not null,
+ "comment" text not null,
+ "advisory" integer not null,
+ "medium_id" bigint,
+ "release_id" bigint,
+ "media_library_id" bigint,
+ "directory_id" bigint,
+ "preferred_artwork_id" bigint,
+ "preferred_media_artwork_id" bigint,
+ constraint "fk_track_medium" foreign key ("medium_id") references "medium" ("id") on delete cascade deferrable initially deferred,
+ 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))");
+
+ // Copy data from track to track_backup
+ utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
+ SELECT
+ id,
+ version,
+ scan_version,
+ absolute_file_path,
+ file_size,
+ file_last_write,
+ file_added,
+ duration,
+ 0 as container,
+ 0 as codec,
+ bitrate,
+ channel_count,
+ sample_rate,
+ CASE WHEN bits_per_sample = 0 THEN NULL ELSE bits_per_sample END as bits_per_sample,
+ replay_gain,
+ track_number,
+ name,
+ date,
+ original_date,
+ mbid,
+ recording_mbid,
+ copyright,
+ copyright_url,
+ artist_display_name,
+ comment,
+ advisory,
+ medium_id,
+ release_id,
+ media_library_id,
+ directory_id,
+ preferred_artwork_id,
+ preferred_media_artwork_id
+ FROM track)");
+
+ utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
+
+ // Just increment the scan version of the settings to make the next scan rescan all audio files
+ utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
+ }
+
+ void migrateFromV101(Session& session)
+ {
+ // Add audio properties for podcast episodes
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD container INTEGER NOT NULL DEFAULT(0)"); // 0 means unknown
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD codec INTEGER NOT NULL DEFAULT(0)"); // 0 means unknown
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bitrate INTEGER NOT NULL DEFAULT(0)");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD channel_count INTEGER NOT NULL DEFAULT(0)");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bits_per_sample INTEGER");
+ }
+
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1657,6 +1757,8 @@ FROM track)");
{ 97, migrateFromV97 },
{ 98, migrateFromV98 },
{ 99, migrateFromV99 },
+ { 100, migrateFromV100 },
+ { 101, migrateFromV101 },
};
bool migrationPerformed{};
diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp
index 2292f596..bd8da398 100644
--- a/src/libs/database/impl/Utils.hpp
+++ b/src/libs/database/impl/Utils.hpp
@@ -42,7 +42,7 @@ namespace lms::db::utils
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
- namespace details
+ namespace detail
{
template
void recordQueryPlanIfNeeded(const Query& query)
@@ -50,7 +50,7 @@ namespace lms::db::utils
if (IQueryPlanRecorder * recorder{ core::Service::get() })
static_cast(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString());
}
- } // namespace details
+ } // namespace detail
template
void applyRange(Query& query, std::optional range)
@@ -100,7 +100,7 @@ namespace lms::db::utils
template
void forEachQueryResult(const Query& query, UnaryFunc&& func)
{
- details::recordQueryPlanIfNeeded(query);
+ detail::recordQueryPlanIfNeeded(query);
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
@@ -110,7 +110,7 @@ namespace lms::db::utils
template
std::vector fetchQueryResults(const Query& query)
{
- details::recordQueryPlanIfNeeded(query);
+ detail::recordQueryPlanIfNeeded(query);
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
@@ -121,7 +121,7 @@ namespace lms::db::utils
template
std::vector::type> fetchQueryResults(const Query& query)
{
- details::recordQueryPlanIfNeeded(query);
+ detail::recordQueryPlanIfNeeded(query);
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
@@ -132,7 +132,7 @@ namespace lms::db::utils
template
auto fetchQuerySingleResult(const Query& query)
{
- details::recordQueryPlanIfNeeded(query);
+ detail::recordQueryPlanIfNeeded(query);
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
return query.resultValue();
diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp
index f74971d0..15cacc41 100644
--- a/src/libs/database/impl/objects/Artist.cpp
+++ b/src/libs/database/impl/objects/Artist.cpp
@@ -34,6 +34,7 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
+#include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
@@ -56,6 +57,7 @@ namespace lms::db
|| params.track.isValid()
|| params.release.isValid()
|| params.filters.clusters.size() == 1
+ || params.filters.codec.has_value()
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
@@ -67,6 +69,7 @@ namespace lms::db
|| params.sortMethod == ArtistSortMethod::AddedDesc
|| params.writtenAfter.isValid()
|| params.release.isValid()
+ || params.filters.codec.has_value()
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
@@ -79,6 +82,9 @@ namespace lms::db
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
+ if (params.filters.codec.has_value())
+ query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
+
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp
index 8fd3ac05..3c146aa5 100644
--- a/src/libs/database/impl/objects/Listen.cpp
+++ b/src/libs/database/impl/objects/Listen.cpp
@@ -28,6 +28,7 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
+#include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Listen)
@@ -49,11 +50,15 @@ namespace lms::db
assert(!params.artist.isValid()); // poor check
if (params.filters.mediaLibrary.isValid()
+ || params.filters.codec.has_value()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
+ if (params.filters.codec.has_value())
+ query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
+
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
diff --git a/src/libs/database/impl/objects/PodcastEpisode.cpp b/src/libs/database/impl/objects/PodcastEpisode.cpp
index a0d52d91..d831f8da 100644
--- a/src/libs/database/impl/objects/PodcastEpisode.cpp
+++ b/src/libs/database/impl/objects/PodcastEpisode.cpp
@@ -27,6 +27,7 @@
#include "database/objects/Podcast.hpp"
#include "Utils.hpp"
+#include "detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
@@ -98,6 +99,16 @@ namespace lms::db
utils::forEachQueryRangeResult(query, params.range, func);
}
+ std::optional PodcastEpisode::getContainer() const
+ {
+ return detail::getMediaContainerType(_container);
+ }
+
+ std::optional PodcastEpisode::getCodec() const
+ {
+ return detail::getMediaCodecType(_codec);
+ }
+
ObjectPtr PodcastEpisode::getArtwork() const
{
return _artwork;
@@ -108,6 +119,16 @@ namespace lms::db
return _artwork.id();
}
+ void PodcastEpisode::setContainer(core::media::Container container)
+ {
+ _container = detail::getDbContainer(container);
+ }
+
+ void PodcastEpisode::setCodec(core::media::Codec codec)
+ {
+ _codec = detail::getDbCodec(codec);
+ }
+
void PodcastEpisode::setArtwork(ObjectPtr artwork)
{
_artwork = getDboPtr(artwork);
diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp
index dd9cf913..4525289d 100644
--- a/src/libs/database/impl/objects/Release.cpp
+++ b/src/libs/database/impl/objects/Release.cpp
@@ -40,6 +40,7 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
+#include "detail/Types.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
@@ -74,6 +75,7 @@ namespace lms::db
|| params.artist.isValid()
|| params.filters.clusters.size() == 1
|| params.filters.mediaLibrary.isValid()
+ || params.filters.codec.has_value()
|| params.directory.isValid()
|| params.parentDirectory.isValid())
{
@@ -217,6 +219,9 @@ namespace lms::db
query.where(oss.str());
}
+ if (params.filters.codec.has_value())
+ query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value()));
+
if (params.releaseGroupMBID)
query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString());
@@ -679,6 +684,22 @@ namespace lms::db
return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0"));
}
+ std::vector Release::getCodecs() const
+ {
+ assert(session());
+
+ // Get the codec ordered by frequency
+ auto query{ session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") };
+
+ std::vector res;
+ utils::forEachQueryResult(query, [&](detail::Codec codec) {
+ if (const auto mediaCodecType{ detail::getMediaCodecType(codec) })
+ res.push_back(*mediaCodecType);
+ });
+
+ return res;
+ }
+
std::vector Release::getArtists(TrackArtistLinkType linkType) const
{
assert(session());
diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp
index d42dcf48..bd7acaac 100644
--- a/src/libs/database/impl/objects/Track.cpp
+++ b/src/libs/database/impl/objects/Track.cpp
@@ -23,6 +23,7 @@
#include
#include "core/ILogger.hpp"
+
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "database/objects/Artist.hpp"
@@ -41,6 +42,7 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
+#include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp"
@@ -195,6 +197,9 @@ namespace lms::db
query.where("t_e_i_l.track_embedded_image_id = ?").bind(params.embeddedImageId);
}
+ if (params.filters.codec.has_value())
+ query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
+
switch (params.sortMethod)
{
case TrackSortMethod::None:
@@ -523,6 +528,16 @@ namespace lms::db
_absoluteFilePath = filePath;
}
+ void Track::setContainer(core::media::Container container)
+ {
+ _container = detail::getDbContainer(container);
+ }
+
+ void Track::setCodec(core::media::Codec codec)
+ {
+ _codec = detail::getDbCodec(codec);
+ }
+
void Track::setName(std::string_view name)
{
_name = std::string{ name, 0, _maxNameLength };
@@ -603,6 +618,16 @@ namespace lms::db
_preferredMediaArtwork = getDboPtr(artwork);
}
+ std::optional Track::getContainer() const
+ {
+ return detail::getMediaContainerType(_container);
+ }
+
+ std::optional Track::getCodec() const
+ {
+ return detail::getMediaCodecType(_codec);
+ }
+
std::optional Track::getYear() const
{
return _date.getYear();
diff --git a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
index c6866f13..507ca4bb 100644
--- a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
+++ b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
@@ -28,6 +28,8 @@
#include "database/objects/TrackEmbeddedImageLink.hpp"
#include "Utils.hpp"
+#include "detail/Types.hpp"
+#include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
@@ -83,7 +85,7 @@ namespace lms::db
}
if (params.imageType.has_value())
- query.where("t_e_i_l.type = ?").bind(params.imageType.value());
+ query.where("t_e_i_l.type = ?").bind(detail::getDbImageType(params.imageType.value()));
}
switch (params.sortMethod)
diff --git a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
index e39b2965..4cd05a08 100644
--- a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
+++ b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
@@ -23,11 +23,11 @@
#include
#include "database/Session.hpp"
-#include "database/Types.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackEmbeddedImage.hpp"
#include "Utils.hpp"
+#include "detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
@@ -77,4 +77,14 @@ namespace lms::db
{
return _image;
}
+
+ core::media::ImageType TrackEmbeddedImageLink::getType() const
+ {
+ return detail::getMediaImageType(_type);
+ }
+
+ void TrackEmbeddedImageLink::setType(core::media::ImageType type)
+ {
+ _type = detail::getDbImageType(type);
+ }
} // namespace lms::db
diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp
index 5ef0393c..2f35e473 100644
--- a/src/libs/database/impl/objects/TrackList.cpp
+++ b/src/libs/database/impl/objects/TrackList.cpp
@@ -31,6 +31,7 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
+#include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
@@ -49,6 +50,7 @@ namespace lms::db
auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
if (!params.filters.clusters.empty()
+ || params.filters.codec.has_value()
|| params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
@@ -61,11 +63,15 @@ namespace lms::db
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (params.filters.mediaLibrary.isValid()
+ || params.filters.codec.has_value()
|| params.filters.label.isValid()
|| params.filters.releaseType.isValid())
{
query.join("track t ON t.id = t_l_e.track_id");
+ if (params.filters.codec.has_value())
+ query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
+
if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/objects/Types.cpp
similarity index 94%
rename from src/libs/database/impl/Types.cpp
rename to src/libs/database/impl/objects/Types.cpp
index f07a30fd..2ae8bede 100644
--- a/src/libs/database/impl/Types.cpp
+++ b/src/libs/database/impl/objects/Types.cpp
@@ -17,9 +17,9 @@
* along with LMS. If not, see .
*/
-#include "database/Types.hpp"
+#include "database/objects/Types.hpp"
-#include
+#include
namespace lms::db
{
@@ -54,7 +54,7 @@ namespace lms::db
return "unknown";
}
- static const std::set allowedAudioBitrates{
+ static const std::unordered_set allowedAudioBitrates{
64000,
96000,
128000,
@@ -72,5 +72,4 @@ namespace lms::db
{
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
}
-
} // namespace lms::db
diff --git a/src/libs/database/impl/objects/detail/Types.cpp b/src/libs/database/impl/objects/detail/Types.cpp
new file mode 100644
index 00000000..5fe01d0b
--- /dev/null
+++ b/src/libs/database/impl/objects/detail/Types.cpp
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "Types.hpp"
+
+namespace lms::db::detail
+{
+ std::optional getMediaContainerType(db::detail::Container container)
+ {
+ switch (container)
+ {
+ case Container::AIFF:
+ return core::media::Container::AIFF;
+ case Container::APE:
+ return core::media::Container::APE;
+ case Container::ASF:
+ return core::media::Container::ASF;
+ case Container::DSF:
+ return core::media::Container::DSF;
+ case Container::FLAC:
+ return core::media::Container::FLAC;
+ case Container::MP4:
+ return core::media::Container::MP4;
+ case Container::MPC:
+ return core::media::Container::MPC;
+ case Container::MPEG:
+ return core::media::Container::MPEG;
+ case Container::Ogg:
+ return core::media::Container::Ogg;
+ case Container::Shorten:
+ return core::media::Container::Shorten;
+ case Container::TrueAudio:
+ return core::media::Container::TrueAudio;
+ case Container::WAV:
+ return core::media::Container::WAV;
+ case Container::WavPack:
+ return core::media::Container::WavPack;
+
+ case Container::Unknown:
+ break;
+ }
+
+ return std::nullopt;
+ }
+
+ db::detail::Container getDbContainer(core::media::Container container)
+ {
+ switch (container)
+ {
+ case core::media::Container::AIFF:
+ return Container::AIFF;
+ case core::media::Container::APE:
+ return Container::APE;
+ case core::media::Container::ASF:
+ return Container::ASF;
+ case core::media::Container::DSF:
+ return Container::DSF;
+ case core::media::Container::FLAC:
+ return Container::FLAC;
+ case core::media::Container::MP4:
+ return Container::MP4;
+ case core::media::Container::MPC:
+ return Container::MPC;
+ case core::media::Container::MPEG:
+ return Container::MPEG;
+ case core::media::Container::Ogg:
+ return Container::Ogg;
+ case core::media::Container::Shorten:
+ return Container::Shorten;
+ case core::media::Container::TrueAudio:
+ return Container::TrueAudio;
+ case core::media::Container::WAV:
+ return Container::WAV;
+ case core::media::Container::WavPack:
+ return Container::WavPack;
+ }
+
+ return Container::Unknown;
+ }
+
+ std::optional getMediaCodecType(db::detail::Codec codec)
+ {
+ switch (codec)
+ {
+ case Codec::AAC:
+ return core::media::Codec::AAC;
+ case Codec::AC3:
+ return core::media::Codec::AC3;
+ case Codec::ALAC:
+ return core::media::Codec::ALAC;
+ case Codec::APE:
+ return core::media::Codec::APE;
+ case Codec::DSD:
+ return core::media::Codec::DSD;
+ case Codec::EAC3:
+ return core::media::Codec::EAC3;
+ case Codec::FLAC:
+ return core::media::Codec::FLAC;
+ case Codec::MP3:
+ return core::media::Codec::MP3;
+ case Codec::MP4ALS:
+ return core::media::Codec::MP4ALS;
+ case Codec::MPC7:
+ return core::media::Codec::MPC7;
+ case Codec::MPC8:
+ return core::media::Codec::MPC8;
+ case Codec::Opus:
+ return core::media::Codec::Opus;
+ case Codec::PCM:
+ return core::media::Codec::PCM;
+ case Codec::Shorten:
+ return core::media::Codec::Shorten;
+ case Codec::TrueAudio:
+ return core::media::Codec::TrueAudio;
+ case Codec::Vorbis:
+ return core::media::Codec::Vorbis;
+ case Codec::WavPack:
+ return core::media::Codec::WavPack;
+ case Codec::WMA1:
+ return core::media::Codec::WMA1;
+ case Codec::WMA2:
+ return core::media::Codec::WMA2;
+ case Codec::WMA9Pro:
+ return core::media::Codec::WMA9Pro;
+ case Codec::WMA9Lossless:
+ return core::media::Codec::WMA9Lossless;
+
+ case Codec::Unknown:
+ break;
+ }
+
+ return std::nullopt;
+ }
+
+ db::detail::Codec getDbCodec(core::media::Codec codec)
+ {
+ switch (codec)
+ {
+ case core::media::Codec::AAC:
+ return Codec::AAC;
+ case core::media::Codec::AC3:
+ return Codec::AC3;
+ case core::media::Codec::ALAC:
+ return Codec::ALAC;
+ case core::media::Codec::APE:
+ return Codec::APE;
+ case core::media::Codec::DSD:
+ return Codec::DSD;
+ case core::media::Codec::EAC3:
+ return Codec::EAC3;
+ case core::media::Codec::FLAC:
+ return Codec::FLAC;
+ case core::media::Codec::MP3:
+ return Codec::MP3;
+ case core::media::Codec::MP4ALS:
+ return Codec::MP4ALS;
+ case core::media::Codec::MPC7:
+ return Codec::MPC7;
+ case core::media::Codec::MPC8:
+ return Codec::MPC8;
+ case core::media::Codec::Opus:
+ return Codec::Opus;
+ case core::media::Codec::PCM:
+ return Codec::PCM;
+ case core::media::Codec::Shorten:
+ return Codec::Shorten;
+ case core::media::Codec::TrueAudio:
+ return Codec::TrueAudio;
+ case core::media::Codec::Vorbis:
+ return Codec::Vorbis;
+ case core::media::Codec::WavPack:
+ return Codec::WavPack;
+ case core::media::Codec::WMA1:
+ return Codec::WMA1;
+ case core::media::Codec::WMA2:
+ return Codec::WMA2;
+ case core::media::Codec::WMA9Pro:
+ return Codec::WMA9Pro;
+ case core::media::Codec::WMA9Lossless:
+ return Codec::WMA9Lossless;
+ }
+
+ return Codec::Unknown;
+ }
+
+ core::media::ImageType getMediaImageType(db::detail::ImageType type)
+ {
+ switch (type)
+ {
+ case db::detail::ImageType::Unknown:
+ return core::media::ImageType::Unknown;
+ case db::detail::ImageType::Other:
+ return core::media::ImageType::Other;
+ case db::detail::ImageType::FileIcon:
+ return core::media::ImageType::FileIcon;
+ case db::detail::ImageType::OtherFileIcon:
+ return core::media::ImageType::OtherFileIcon;
+ case db::detail::ImageType::FrontCover:
+ return core::media::ImageType::FrontCover;
+ case db::detail::ImageType::BackCover:
+ return core::media::ImageType::BackCover;
+ case db::detail::ImageType::LeafletPage:
+ return core::media::ImageType::LeafletPage;
+ case db::detail::ImageType::Media:
+ return core::media::ImageType::Media;
+ case db::detail::ImageType::LeadArtist:
+ return core::media::ImageType::LeadArtist;
+ case db::detail::ImageType::Artist:
+ return core::media::ImageType::Artist;
+ case db::detail::ImageType::Conductor:
+ return core::media::ImageType::Conductor;
+ case db::detail::ImageType::Band:
+ return core::media::ImageType::Band;
+ case db::detail::ImageType::Composer:
+ return core::media::ImageType::Composer;
+ case db::detail::ImageType::Lyricist:
+ return core::media::ImageType::Lyricist;
+ case db::detail::ImageType::RecordingLocation:
+ return core::media::ImageType::RecordingLocation;
+ case db::detail::ImageType::DuringRecording:
+ return core::media::ImageType::DuringRecording;
+ case db::detail::ImageType::DuringPerformance:
+ return core::media::ImageType::DuringPerformance;
+ case db::detail::ImageType::MovieScreenCapture:
+ return core::media::ImageType::MovieScreenCapture;
+ case db::detail::ImageType::ColouredFish:
+ return core::media::ImageType::ColouredFish;
+ case db::detail::ImageType::Illustration:
+ return core::media::ImageType::Illustration;
+ case db::detail::ImageType::BandLogo:
+ return core::media::ImageType::BandLogo;
+ case db::detail::ImageType::PublisherLogo:
+ return core::media::ImageType::PublisherLogo;
+ }
+
+ return core::media::ImageType::Unknown;
+ }
+
+ db::detail::ImageType getDbImageType(core::media::ImageType type)
+ {
+ switch (type)
+ {
+ case core::media::ImageType::Unknown:
+ return db::detail::ImageType::Unknown;
+ case core::media::ImageType::Other:
+ return db::detail::ImageType::Other;
+ case core::media::ImageType::FileIcon:
+ return db::detail::ImageType::FileIcon;
+ case core::media::ImageType::OtherFileIcon:
+ return db::detail::ImageType::OtherFileIcon;
+ case core::media::ImageType::FrontCover:
+ return db::detail::ImageType::FrontCover;
+ case core::media::ImageType::BackCover:
+ return db::detail::ImageType::BackCover;
+ case core::media::ImageType::LeafletPage:
+ return db::detail::ImageType::LeafletPage;
+ case core::media::ImageType::Media:
+ return db::detail::ImageType::Media;
+ case core::media::ImageType::LeadArtist:
+ return db::detail::ImageType::LeadArtist;
+ case core::media::ImageType::Artist:
+ return db::detail::ImageType::Artist;
+ case core::media::ImageType::Conductor:
+ return db::detail::ImageType::Conductor;
+ case core::media::ImageType::Band:
+ return db::detail::ImageType::Band;
+ case core::media::ImageType::Composer:
+ return db::detail::ImageType::Composer;
+ case core::media::ImageType::Lyricist:
+ return db::detail::ImageType::Lyricist;
+ case core::media::ImageType::RecordingLocation:
+ return db::detail::ImageType::RecordingLocation;
+ case core::media::ImageType::DuringRecording:
+ return db::detail::ImageType::DuringRecording;
+ case core::media::ImageType::DuringPerformance:
+ return db::detail::ImageType::DuringPerformance;
+ case core::media::ImageType::MovieScreenCapture:
+ return db::detail::ImageType::MovieScreenCapture;
+ case core::media::ImageType::ColouredFish:
+ return db::detail::ImageType::ColouredFish;
+ case core::media::ImageType::Illustration:
+ return db::detail::ImageType::Illustration;
+ case core::media::ImageType::BandLogo:
+ return db::detail::ImageType::BandLogo;
+ case core::media::ImageType::PublisherLogo:
+ return db::detail::ImageType::PublisherLogo;
+ }
+
+ return db::detail::ImageType::Unknown;
+ }
+} // namespace lms::db::detail
\ No newline at end of file
diff --git a/src/libs/database/impl/objects/detail/Types.hpp b/src/libs/database/impl/objects/detail/Types.hpp
new file mode 100644
index 00000000..ba7dc789
--- /dev/null
+++ b/src/libs/database/impl/objects/detail/Types.hpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
+#include "core/media/ImageType.hpp"
+
+#include "database/objects/detail/Types.hpp"
+
+namespace lms::db::detail
+{
+ std::optional getMediaContainerType(db::detail::Container container);
+ db::detail::Container getDbContainer(core::media::Container container);
+
+ std::optional getMediaCodecType(db::detail::Codec codec);
+ db::detail::Codec getDbCodec(core::media::Codec codec);
+
+ core::media::ImageType getMediaImageType(db::detail::ImageType type);
+ db::detail::ImageType getDbImageType(core::media::ImageType type);
+} // namespace lms::db::detail
\ No newline at end of file
diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp
index 37bfcfa4..46c1661d 100644
--- a/src/libs/database/include/database/Types.hpp
+++ b/src/libs/database/include/database/Types.hpp
@@ -21,14 +21,10 @@
#include
#include
-#include
-#include
#include
#include "core/Exception.hpp"
-#include "core/LiteralString.hpp"
-#include "core/TaggedType.hpp"
namespace lms::db
{
@@ -113,243 +109,4 @@ namespace lms::db
std::size_t getTotalFileCount() const { return trackCount + imageCount + trackLyricsCount + playListCount + artistInfoCount; }
};
-
- struct YearRange
- {
- int begin{};
- int end{};
- };
-
- struct FileInfo
- {
- Wt::WDateTime lastWrittenTime;
- std::size_t scanVersion{};
- };
-
- enum class ArtistSortMethod
- {
- None,
- Id,
- Name,
- SortName,
- Random,
- LastWrittenDesc,
- AddedDesc,
- StarredDateDesc,
- };
-
- enum class ClusterSortMethod
- {
- None,
- Name,
- };
-
- enum class DirectorySortMethod
- {
- None,
- Name,
- };
-
- using ImageHashType = core::TaggedType;
-
- enum class LabelSortMethod
- {
- None,
- Name,
- };
-
- enum class MediumSortMethod
- {
- None,
- PositionAsc,
- };
-
- enum class PodcastEpisodeSortMode
- {
- None,
- PubDateAsc,
- PubDateDesc,
- };
-
- enum class ReleaseSortMethod
- {
- None,
- Id,
- Name,
- SortName,
- ArtistNameThenName,
- DateAsc,
- DateDesc,
- OriginalDate,
- OriginalDateDesc,
- Random,
- LastWrittenDesc,
- AddedDesc,
- StarredDateDesc,
- };
-
- enum class ReleaseTypeSortMethod
- {
- None,
- Name,
- };
-
- enum class TrackArtistLinkSortMethod
- {
- None,
- OriginalDateDesc,
- };
-
- enum class TrackEmbeddedImageSortMethod
- {
- None,
- SizeDesc,
- TrackNumberThenSizeDesc,
- DiscNumberThenTrackNumberThenSizeDesc,
- TrackListIndexAscThenSizeDesc,
- };
-
- enum class TrackListSortMethod
- {
- None,
- Name,
- LastModifiedDesc,
- };
-
- enum class TrackSortMethod
- {
- None,
- Id,
- Random,
- LastWrittenDesc,
- AddedDesc,
- StarredDateDesc,
- AbsoluteFilePath,
- Name,
- DateDescAndRelease,
- OriginalDateDescAndRelease,
- Release, // order by disc/track number
- TrackList, // order by asc order in tracklist
- TrackNumber,
- };
-
- enum class TrackLyricsSortMethod
- {
- None,
- ExternalFirst,
- EmbeddedFirst,
- };
-
- enum class ImageType
- {
- Unknown = 0,
- Other = 1,
- FileIcon = 2,
- OtherFileIcon = 3,
- FrontCover = 4,
- BackCover = 5,
- LeafletPage = 6,
- Media = 7,
- LeadArtist = 8,
- Artist = 9,
- Conductor = 10,
- Band = 11,
- Composer = 12,
- Lyricist = 13,
- RecordingLocation = 14,
- DuringRecording = 15,
- DuringPerformance = 16,
- MovieScreenCapture = 17,
- ColouredFish = 18,
- Illustration = 19,
- BandLogo = 20,
- PublisherLogo = 21
- };
-
- enum class TrackArtistLinkType
- {
- Artist = 0, // regular track artist
- Arranger = 1,
- Composer = 2,
- Conductor = 3,
- Lyricist = 4,
- Mixer = 5,
- Performer = 6,
- Producer = 7,
- ReleaseArtist = 8,
- Remixer = 9,
- Writer = 10,
- };
-
- core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type);
-
- // User selectable transcoding output formats
- enum class TranscodingOutputFormat
- {
- MP3 = 1,
- OGG_OPUS = 2,
- OGG_VORBIS = 3,
- WEBM_VORBIS = 4,
- MATROSKA_OPUS = 5,
- };
-
- using Bitrate = std::uint32_t;
- // Do not remove values!
- void visitAllowedAudioBitrates(std::function);
- bool isAudioBitrateAllowed(Bitrate bitrate);
-
- using Rating = int;
-
- enum class ScrobblingBackend
- {
- Internal = 0,
- ListenBrainz = 1,
- };
-
- enum class FeedbackBackend
- {
- Internal = 0,
- ListenBrainz = 1,
- };
-
- enum class SyncState
- {
- PendingAdd = 0,
- Synchronized = 1,
- PendingRemove = 2,
- };
-
- enum class UserType
- {
- REGULAR = 0,
- ADMIN = 1,
- DEMO = 2,
- };
-
- enum class UITheme
- {
- Light = 0,
- Dark = 1,
- };
-
- enum class SubsonicArtistListMode
- {
- AllArtists = 0,
- ReleaseArtists = 1,
- TrackArtists = 2,
- };
-
- enum class TrackListType
- {
- PlayList = 0, // user controlled playlists
- Internal = 1, // internal usage (current playqueue, history, ...)
- };
-
- enum class Advisory
- {
- UnSet = 0,
- Unknown = 1,
- Clean = 2,
- Explicit = 3,
- };
} // namespace lms::db
diff --git a/src/libs/database/include/database/objects/Artist.hpp b/src/libs/database/include/database/objects/Artist.hpp
index 7cae8afd..21d9e96e 100644
--- a/src/libs/database/include/database/objects/Artist.hpp
+++ b/src/libs/database/include/database/objects/Artist.hpp
@@ -41,6 +41,7 @@
#include "database/objects/MediaLibraryId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
+#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
diff --git a/src/libs/database/include/database/objects/Cluster.hpp b/src/libs/database/include/database/objects/Cluster.hpp
index f30e343d..01a60648 100644
--- a/src/libs/database/include/database/objects/Cluster.hpp
+++ b/src/libs/database/include/database/objects/Cluster.hpp
@@ -33,6 +33,7 @@
#include "database/objects/ClusterId.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/TrackId.hpp"
+#include "database/objects/Types.hpp"
namespace lms::db
{
diff --git a/src/libs/database/include/database/objects/Directory.hpp b/src/libs/database/include/database/objects/Directory.hpp
index 3174f235..dd8ac918 100644
--- a/src/libs/database/include/database/objects/Directory.hpp
+++ b/src/libs/database/include/database/objects/Directory.hpp
@@ -29,6 +29,7 @@
#include
#include "core/EnumSet.hpp"
+
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
@@ -36,6 +37,7 @@
#include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp"
+#include "database/objects/Types.hpp"
namespace lms::db
{
diff --git a/src/libs/database/include/database/objects/Filters.hpp b/src/libs/database/include/database/objects/Filters.hpp
index 1d95a09f..a31d9de8 100644
--- a/src/libs/database/include/database/objects/Filters.hpp
+++ b/src/libs/database/include/database/objects/Filters.hpp
@@ -19,9 +19,12 @@
#pragma once
+#include
#include
#include
+#include "core/media/Codec.hpp"
+
#include "database/objects/ClusterId.hpp"
#include "database/objects/LabelId.hpp"
#include "database/objects/MediaLibraryId.hpp"
@@ -31,10 +34,11 @@ namespace lms::db
{
struct Filters
{
- MediaLibraryId mediaLibrary; // tracks that belongs to this library
- std::vector clusters; // tracks that belong to all these clusters
- LabelId label; // tracks which release has this label
- ReleaseTypeId releaseType; // tracks which release has this type
+ MediaLibraryId mediaLibrary; // tracks that belongs to this library
+ std::vector clusters; // tracks that belong to *all* these clusters
+ LabelId label; // tracks which release has this label
+ ReleaseTypeId releaseType; // tracks which release has this type
+ std::optional codec; // tracks that match this codec
Filters& setClusters(std::span _clusters)
{
@@ -56,5 +60,10 @@ namespace lms::db
releaseType = _releaseType;
return *this;
}
+ Filters& setCodec(std::optional _codec)
+ {
+ codec = _codec;
+ return *this;
+ }
};
} // namespace lms::db
\ No newline at end of file
diff --git a/src/libs/database/include/database/objects/Listen.hpp b/src/libs/database/include/database/objects/Listen.hpp
index b6882fa2..13d6f77a 100644
--- a/src/libs/database/include/database/objects/Listen.hpp
+++ b/src/libs/database/include/database/objects/Listen.hpp
@@ -31,6 +31,7 @@
#include "database/objects/ListenId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
+#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
diff --git a/src/libs/database/include/database/objects/Medium.hpp b/src/libs/database/include/database/objects/Medium.hpp
index 97e8a64e..55c3dff5 100644
--- a/src/libs/database/include/database/objects/Medium.hpp
+++ b/src/libs/database/include/database/objects/Medium.hpp
@@ -33,6 +33,7 @@
#include "database/objects/ArtworkId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp"
+#include "database/objects/Types.hpp"
namespace lms::db
{
diff --git a/src/libs/database/include/database/objects/PlayQueue.hpp b/src/libs/database/include/database/objects/PlayQueue.hpp
index 845db082..981b603f 100644
--- a/src/libs/database/include/database/objects/PlayQueue.hpp
+++ b/src/libs/database/include/database/objects/PlayQueue.hpp
@@ -59,7 +59,8 @@ namespace lms::db
// Get tracks, ordered by position
std::vector getTrackIds() const;
void visitTracks(const std::function& track)>& visitor) const;
- const Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; }
+ Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; }
+ bool isEmpty() const { return _tracks.empty(); }
// Modifiers
void setCurrentIndex(std::size_t index) { _currentIndex = index; }
diff --git a/src/libs/database/include/database/objects/PodcastEpisode.hpp b/src/libs/database/include/database/objects/PodcastEpisode.hpp
index 40389793..eb226ca2 100644
--- a/src/libs/database/include/database/objects/PodcastEpisode.hpp
+++ b/src/libs/database/include/database/objects/PodcastEpisode.hpp
@@ -29,11 +29,16 @@
#include
#include
+#include "core/media/Codec.hpp"
+#include "core/media/Container.hpp"
+
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtworkId.hpp"
#include "database/objects/PodcastEpisodeId.hpp"
#include "database/objects/PodcastId.hpp"
+#include "database/objects/Types.hpp"
+#include "database/objects/detail/Types.hpp"
namespace lms::db
{
@@ -90,6 +95,15 @@ namespace lms::db
ManualDownloadState getManualDownloadState() const { return _manualDownloadState; }
const std::filesystem::path& getAudioRelativeFilePath() const { return _audioRelativeFilePath; }
+ // Audio properties
+ std::chrono::milliseconds getDuration() const { return _duration; }
+ std::optional getContainer() const;
+ std::optional getCodec() const;
+ std::size_t getBitrate() const { return _bitrate; }
+ std::size_t getChannelCount() const { return _channelCount; }
+ std::size_t getSampleRate() const { return _sampleRate; }
+ std::optional