Merge branch 'develop' for release v3.73.0

This commit is contained in:
emeric
2025-12-08 22:57:11 +01:00
197 changed files with 6189 additions and 1736 deletions
+1 -1
View File
@@ -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/)
+5 -4
View File
@@ -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 \
+1 -1
View File
@@ -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_.
+2
View File
@@ -24,7 +24,9 @@
${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
${</if-has-mbid>}
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
</ul>
</div>
</div>
-2
View File
@@ -371,11 +371,9 @@
<message id="Lms.Settings.transcoding-mode.never">Never</message>
<message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.user-interface">User Interface</message>
</messages>
-2
View File
@@ -370,11 +370,9 @@
<message id="Lms.Settings.transcoding-mode.never">Nunca</message>
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate de transcodificación</message>
<message id="Lms.Settings.transcoding-output-format">Formato de transcodificación</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.user-interface">Interfaz de usuario</message>
<!--Wt-->
-2
View File
@@ -370,11 +370,9 @@
<message id="Lms.Settings.transcoding-mode.never">Jamais</message>
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.user-interface">Interface utilisateur</message>
<!--Wt-->
-2
View File
@@ -370,11 +370,9 @@
<message id="Lms.Settings.transcoding-mode.never">Mai</message>
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.user-interface">Interfaccia utente</message>
<!--Wt-->
-2
View File
@@ -396,11 +396,9 @@
<message id="Lms.Settings.transcoding-mode.never">Nigdy</message>
<message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message>
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.user-interface">Interfejs użytkownika</message>
<!--Wt-->
-2
View File
@@ -367,11 +367,9 @@
<message id="Lms.Settings.transcoding-mode.never">从不</message>
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
<message id="Lms.Settings.transcoding-output-format">转码格式</message>
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<!--Wt-->
+2
View File
@@ -65,7 +65,9 @@
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
</ul>
</div>
</div>
+4
View File
@@ -27,7 +27,9 @@
${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
${</if-has-mbid>}
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
<li>${release-info class="dropdown-item"}</li>
</ul>
</div>
@@ -109,7 +111,9 @@
<li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
<li>${track-info class="dropdown-item"}</li>
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
</ul>
+2
View File
@@ -13,7 +13,9 @@
<ul class="dropdown-menu">
<li>${play-shuffled class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
${<if-has-delete>}
<li>${delete class="dropdown-item"}</li>
${</if-has-delete>}
+2
View File
@@ -59,7 +59,9 @@
<li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
${<if-has-download>}
<li>${download class="dropdown-item"}</li>
${</if-has-download>}
<li>${track-info class="dropdown-item"}</li>
${<if-has-lyrics>}<li>${track-lyrics class="dropdown-item"}</li>${</if-has-lyrics>}
</ul>
+6 -3
View File
@@ -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;
+3 -3
View File
@@ -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
)
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <memory>
#include "audio/IAudioFileInfoParser.hpp"
#include "ffmpeg/AudioFileInfoParser.hpp"
#include "taglib/AudioFileInfoParser.hpp"
namespace lms::audio
{
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend)
{
switch (backend)
{
case AudioFileInfoParserBackend::TagLib:
return std::make_unique<taglib::AudioFileInfoParser>();
case AudioFileInfoParserBackend::FFmpeg:
return std::make_unique<ffmpeg::AudioFileInfoParser>();
}
return nullptr;
}
} // namespace lms::audio
-105
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <memory>
#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<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
{
switch (parserOptions.parser)
{
case ParserOptions::Parser::TagLib:
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
case ParserOptions::Parser::FFmpeg:
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
}
return {};
}
std::span<const std::filesystem::path> 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
+125 -69
View File
@@ -20,21 +20,22 @@
#include "AudioFile.hpp"
#include <array>
#include <cstdio>
#include <unordered_map>
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/log.h>
}
#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<ContainerType> avdemuxerToContainerType(std::string_view name)
std::optional<core::media::Container> 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<CodecType> avcodecToCodecType(AVCodecID codec)
std::optional<core::media::Codec> 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<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
return;
if (level > AV_LOG_WARNING)
return;
std::array<char, 256> 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<void(const Picture&, const MetadataMap&)> func) const
void AudioFile::visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const
{
static const std::unordered_map<int, std::string> 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<const std::byte*>(pkt.data), static_cast<std::size_t>(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;
}
+8 -7
View File
@@ -27,7 +27,8 @@
#include <string>
#include <vector>
#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<const std::byte> data; // valid as long as IAudioFile exists
@@ -44,17 +45,17 @@ namespace lms::audio::ffmpeg
struct ContainerInfo
{
std::optional<ContainerType> container;
std::optional<core::media::Container> container;
std::string containerName;
std::size_t bitrate{};
std::optional<std::size_t> bitrate;
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
std::optional<CodecType> codec;
std::optional<core::media::Codec> codec;
std::string codecName;
std::optional<size_t> 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<StreamInfo> getStreamInfo() const;
std::optional<StreamInfo> getBestStreamInfo() const;
std::optional<std::size_t> getBestStreamIndex() const;
bool hasAttachedPictures() const;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
void visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
+76 -28
View File
@@ -19,7 +19,12 @@
#include "AudioFileInfo.hpp"
#include "audio/AudioTypes.hpp"
#include <filesystem>
#include <system_error>
#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<AudioProperties> computeAudioProperties(const AudioFile& audioFile)
{
AudioProperties audioProperties;
std::optional<AudioProperties> 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::size_t>(std::chrono::duration_cast<std::chrono::duration<double>>(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<AudioFile>(filePath) }
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
, _audioProperties{ computeAudioProperties(*_audioFile) }
{
if (parseOptions.readTags)
_tagReader = std::make_unique<TagReader>(*_audioFile, parseOptions.enableExtraDebugLogs);
if (parseOptions.readImages)
_imageReader = std::make_unique<ImageReader>(*_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
+10 -7
View File
@@ -20,9 +20,11 @@
#pragma once
#include <filesystem>
#include <optional>
#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> _audioFile;
std::unique_ptr<AudioProperties> _audioProperties;
const std::optional<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "AudioFileInfoParser.hpp"
#include <memory>
#include "AudioFileInfo.hpp"
#include "Utils.hpp"
namespace lms::audio::ffmpeg
{
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
{
return std::make_unique<AudioFileInfo>(p, parseOptions);
}
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
{
return utils::getSupportedExtensions();
}
} // namespace lms::audio::ffmpeg
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::ffmpeg
{
class AudioFileInfoParser : public IAudioFileInfoParser
{
private:
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
};
} // namespace lms::audio::ffmpeg
+3 -4
View File
@@ -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
+1 -2
View File
@@ -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<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
-1
View File
@@ -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
+84 -62
View File
@@ -19,13 +19,14 @@
#include "Transcoder.hpp"
#include <atomic>
#include <filesystem>
#include <iomanip>
#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<size_t> globalId{};
static std::filesystem::path ffmpegPath;
void Transcoder::init()
namespace
{
ffmpegPath = core::Service<core::IConfig>::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<core::IConfig>::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<std::size_t> 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<std::string> 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<int>(*_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<core::logging::ILogger>::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<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
_childProcess = core::Service<core::IChildProcessManager>::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
+4 -1
View File
@@ -19,6 +19,8 @@
#pragma once
#include <atomic>
#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<std::size_t> _nextDebugId;
const std::size_t _debugId;
const TranscodeInputParameters _inputParams;
const TranscodeOutputParameters _outputParams;
std::unique_ptr<core::IChildProcess> _childProcess;
+91 -62
View File
@@ -43,8 +43,11 @@
#include <taglib/shortenfile.h>
#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<AudioProperties> 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<std::size_t>(properties.bitrate() * 1000);
audioProperties.bitrate = static_cast<std::size_t>(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<std::size_t>(properties.channels());
audioProperties.channelCount = static_cast<std::size_t>(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<std::size_t>(properties.sampleRate());
audioProperties.sampleRate = static_cast<std::size_t>(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<const ::TagLib::APE::File*>(&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<const ::TagLib::ASF::File*>(&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<const ::TagLib::DSF::File*>(&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<const ::TagLib::FLAC::File*>(&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<const ::TagLib::MP4::File*>(&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<const ::TagLib::MPC::File*>(&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<const ::TagLib::MPEG::File*>(&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<const ::TagLib::Ogg::Opus::File*>(&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<const ::TagLib::Ogg::Vorbis::File*>(&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<const ::TagLib::RIFF::AIFF::File*>(&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<const ::TagLib::RIFF::WAV::File*>(&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<const ::TagLib::Shorten::File*>(&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<const ::TagLib::TrueAudio::File*>(&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<const ::TagLib::WavPack::File*>(&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<AudioProperties>(computeAudioProperties(*_file)) }
, _tagReader{ std::make_unique<TagReader>(*_file, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_file) }
, _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
, _audioProperties{ computeAudioProperties(*_file, filePath) }
{
if (parseOptions.readTags)
_tagReader = std::make_unique<TagReader>(*_file, parseOptions.enableExtraDebugLogs);
if (parseOptions.readImages)
_imageReader = std::make_unique<ImageReader>(*_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
+10 -6
View File
@@ -19,8 +19,12 @@
#pragma once
#include "audio/AudioTypes.hpp"
#include <filesystem>
#include <optional>
#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> _audioProperties;
std::optional<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "AudioFileInfoParser.hpp"
#include <memory>
#include "AudioFileInfo.hpp"
#include "Utils.hpp"
namespace lms::audio::taglib
{
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
{
return std::make_unique<AudioFileInfo>(p, parseOptions);
}
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
{
return utils::getSupportedExtensions();
}
} // namespace lms::audio::taglib
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::taglib
{
class AudioFileInfoParser : public IAudioFileInfoParser
{
private:
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
};
} // namespace lms::audio::taglib
+74 -74
View File
@@ -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);
+9 -12
View File
@@ -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<const std::filesystem::path>{ 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<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)
{
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;
}
+2 -2
View File
@@ -25,10 +25,10 @@
#include <taglib/tfile.h>
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::taglib::utils
{
std::span<const std::filesystem::path> 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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <optional>
#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<unsigned> bitsPerSample;
};
} // namespace lms::audio
@@ -19,6 +19,9 @@
#pragma once
#include <filesystem>
#include <system_error>
#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
@@ -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 <filesystem>
#include <span>
#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<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
} // namespace lms::audio
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <span>
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<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const = 0;
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
};
enum class AudioFileInfoParserBackend
{
TagLib,
FFmpeg,
};
static inline constexpr AudioFileInfoParserBackend defaultAudioFileInfoParserBackend{ AudioFileInfoParserBackend::TagLib };
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend = defaultAudioFileInfoParserBackend);
} // namespace lms::audio
+2 -53
View File
@@ -23,69 +23,18 @@
#include <span>
#include <string>
#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<const std::byte> data;
};
core::LiteralString imageTypeToString(Image::Type type);
class IImageReader
{
public:
+15 -13
View File
@@ -23,28 +23,30 @@
#include <filesystem>
#include <optional>
#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<OutputFormat> format;
// Not setting a value here means to use the same value as the input
std::optional<TranscodeOutputFormat> format;
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
+4
View File
@@ -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
+10 -11
View File
@@ -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
+16 -16
View File
@@ -31,7 +31,7 @@
namespace lms::core::stringUtils
{
namespace details
namespace detail
{
constexpr std::pair<char, std::string_view> jsEscapeChars[]{
{ '\\', "\\\\" },
@@ -203,7 +203,7 @@ namespace lms::core::stringUtils
return std::distance(std::cbegin(months), str) + 1;
}
} // namespace details
} // namespace detail
template<>
std::optional<std::string> readAs(std::string_view str)
@@ -240,32 +240,32 @@ namespace lms::core::stringUtils
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string_view> separators)
{
return details::splitString(str, separators);
return detail::splitString(str, separators);
}
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string> separators)
{
return details::splitString(str, separators);
return detail::splitString(str, separators);
}
std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter)
{
return details::joinStrings(strings, delimiter);
return detail::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter)
{
return details::joinStrings(strings, delimiter);
return detail::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span<const std::string> 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<const std::string_view> 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<const std::string_view> 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<std::chrono::minutes> offset{ details::getRFC822ZoneOffset(zoneStr) };
const std::optional<std::chrono::minutes> offset{ detail::getRFC822ZoneOffset(zoneStr) };
if (!offset)
return {};
std::optional<unsigned> month{ details::getRFC822Month(monthStr) };
std::optional<unsigned> month{ detail::getRFC822Month(monthStr) };
if (!month)
return {};
+68
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "core/media/Codec.hpp"
#include "core/Exception.hpp"
#include <algorithm>
#include <array>
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<void(const CodecDesc&)>& 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
+58
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
} // namespace lms::core::media
+80
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
+6
View File
@@ -19,6 +19,7 @@
#pragma once
#include <bit>
#include <cassert>
#include <cstdint>
#include <initializer_list>
@@ -51,6 +52,11 @@ namespace lms::core
assign(begin, end);
}
constexpr std::size_t size() const
{
return std::popcount(_bitfield);
}
template<typename It>
constexpr void assign(It begin, It end)
{
+3 -3
View File
@@ -23,7 +23,7 @@
namespace lms::core
{
namespace details
namespace detail
{
template<int... Is>
struct Seq
@@ -45,11 +45,11 @@ namespace lms::core
{
auto l = { (f(std::get<Is>(t)), 0)... };
}
} // namespace details
} // namespace detail
template<typename... Ts, typename Func>
void forEachTypeInTuple(std::tuple<Ts...> const& t, Func f)
{
details::forEachTypeInTuple(t, f, details::GenSeq<sizeof...(Ts)>());
detail::forEachTypeInTuple(t, f, detail::GenSeq<sizeof...(Ts)>());
}
} // namespace lms::core
+13
View File
@@ -19,6 +19,7 @@
#pragma once
#include <functional>
#include <optional>
#include <string>
#include <string_view>
@@ -49,3 +50,15 @@ namespace lms::core::stringUtils
std::optional<UUID>
readAs(std::string_view str);
}
namespace std
{
template<>
struct hash<lms::core::UUID>
{
size_t operator()(const lms::core::UUID& str) const
{
return hash<std::string_view>{}(str.getAsString());
}
};
} // namespace std
@@ -19,38 +19,20 @@
#pragma once
#include <chrono>
#include <optional>
#include <functional>
#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<unsigned> bitsPerSample;
Codec type;
core::LiteralString name;
core::LiteralString longName;
bool isLossless;
};
} // namespace lms::audio
void visitCodecs(const std::function<void(const CodecDesc&)>& visitor);
const CodecDesc& getCodecDesc(Codec type);
} // namespace lms::core::media
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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);
}
+12
View File
@@ -31,9 +31,19 @@ namespace lms::core
Two,
};
{
constexpr EnumSet<Foo> test;
static_assert(test.size() == 0);
static_assert(test.empty());
static_assert(!test.contains(Foo::One));
static_assert(!test.contains(Foo::Two));
}
{
constexpr EnumSet<Foo> 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<Foo> test{ Foo::One, Foo::Two };
constexpr auto bitfield{ test.getBitfield() };
static_assert(test.size() == 2);
EnumSet<Foo> test2;
EXPECT_FALSE(test2.contains(Foo::One));
EXPECT_FALSE(test2.contains(Foo::Two));
+2 -1
View File
@@ -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
)
+103 -1
View File
@@ -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{};
+6 -6
View File
@@ -42,7 +42,7 @@ namespace lms::db::utils
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
namespace details
namespace detail
{
template<typename Query>
void recordQueryPlanIfNeeded(const Query& query)
@@ -50,7 +50,7 @@ namespace lms::db::utils
if (IQueryPlanRecorder * recorder{ core::Service<IQueryPlanRecorder>::get() })
static_cast<QueryPlanRecorder*>(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString());
}
} // namespace details
} // namespace detail
template<typename Query>
void applyRange(Query& query, std::optional<Range> range)
@@ -100,7 +100,7 @@ namespace lms::db::utils
template<typename Query, typename UnaryFunc>
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<typename T, typename Query>
std::vector<T> 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<typename Query>
std::vector<typename QueryResultType<Query>::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<typename Query>
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();
@@ -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);
@@ -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);
@@ -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<core::media::Container> PodcastEpisode::getContainer() const
{
return detail::getMediaContainerType(_container);
}
std::optional<core::media::Codec> PodcastEpisode::getCodec() const
{
return detail::getMediaCodecType(_codec);
}
ObjectPtr<Artwork> 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)
{
_artwork = getDboPtr(artwork);
@@ -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<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0"));
}
std::vector<core::media::Codec> Release::getCodecs() const
{
assert(session());
// Get the codec ordered by frequency
auto query{ session()->query<detail::Codec>("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") };
std::vector<core::media::Codec> res;
utils::forEachQueryResult(query, [&](detail::Codec codec) {
if (const auto mediaCodecType{ detail::getMediaCodecType(codec) })
res.push_back(*mediaCodecType);
});
return res;
}
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
{
assert(session());
+25
View File
@@ -23,6 +23,7 @@
#include <Wt/Dbo/WtSqlTraits.h>
#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<core::media::Container> Track::getContainer() const
{
return detail::getMediaContainerType(_container);
}
std::optional<core::media::Codec> Track::getCodec() const
{
return detail::getMediaCodecType(_codec);
}
std::optional<int> Track::getYear() const
{
return _date.getYear();
@@ -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)
@@ -23,11 +23,11 @@
#include <Wt/Dbo/WtSqlTraits.h>
#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
@@ -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<ResultType>("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);
@@ -17,9 +17,9 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "database/Types.hpp"
#include "database/objects/Types.hpp"
#include <set>
#include <unordered_set>
namespace lms::db
{
@@ -54,7 +54,7 @@ namespace lms::db
return "unknown";
}
static const std::set<Bitrate> allowedAudioBitrates{
static const std::unordered_set<Bitrate> allowedAudioBitrates{
64000,
96000,
128000,
@@ -72,5 +72,4 @@ namespace lms::db
{
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
}
} // namespace lms::db
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "Types.hpp"
namespace lms::db::detail
{
std::optional<core::media::Container> 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<core::media::Codec> 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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#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<core::media::Container> getMediaContainerType(db::detail::Container container);
db::detail::Container getDbContainer(core::media::Container container);
std::optional<core::media::Codec> 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
@@ -21,14 +21,10 @@
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <functional>
#include <Wt/WDate.h>
#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<class ImageHash, std::uint64_t>;
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<void(Bitrate)>);
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
@@ -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
@@ -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
{
@@ -29,6 +29,7 @@
#include <Wt/Dbo/Field.h>
#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
{
@@ -19,9 +19,12 @@
#pragma once
#include <optional>
#include <span>
#include <vector>
#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<ClusterId> 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<ClusterId> 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<core::media::Codec> codec; // tracks that match this codec
Filters& setClusters(std::span<const ClusterId> _clusters)
{
@@ -56,5 +60,10 @@ namespace lms::db
releaseType = _releaseType;
return *this;
}
Filters& setCodec(std::optional<core::media::Codec> _codec)
{
codec = _codec;
return *this;
}
};
} // namespace lms::db
@@ -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
@@ -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
{
@@ -59,7 +59,8 @@ namespace lms::db
// Get tracks, ordered by position
std::vector<TrackId> getTrackIds() const;
void visitTracks(const std::function<void(const ObjectPtr<Track>& 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; }
@@ -29,11 +29,16 @@
#include <Wt/Dbo/collection.h>
#include <Wt/WDateTime.h>
#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<core::media::Container> getContainer() const;
std::optional<core::media::Codec> 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<std::size_t> getBitsPerSample() const { return _bitsPerSample; }
std::string_view getTitle() const { return _title; }
std::string_view getLink() const { return _link; }
std::string_view getDescription() const { return _description; }
@@ -103,7 +117,6 @@ namespace lms::db
std::string_view getSubtitle() const { return _subtitle; }
std::string_view getSummary() const { return _summary; }
bool isExplicit() const { return _explicit; }
std::chrono::duration<int, std::milli> getDuration() const { return _duration; }
ObjectPtr<Podcast> getPodcast() const { return _podcast; }
PodcastId getPodcastId() const { return _podcast.id(); }
ObjectPtr<Artwork> getArtwork() const;
@@ -113,6 +126,15 @@ namespace lms::db
void setManualDownloadState(ManualDownloadState state) { _manualDownloadState = state; }
void setAudioRelativeFilePath(const std::filesystem::path& relativeFilePath) { _audioRelativeFilePath = relativeFilePath; }
// Audio properties
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setContainer(core::media::Container container);
void setCodec(core::media::Codec codec);
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; }
void setSampleRate(std::size_t sampleRate) { _sampleRate = sampleRate; }
void setBitsPerSample(std::optional<std::size_t> bitsPerSample) { _bitsPerSample = bitsPerSample; }
void setTitle(std::string_view title) { _title = title; }
void setLink(std::string_view link) { _link = link; }
void setDescription(std::string_view description) { _description = description; }
@@ -135,6 +157,14 @@ namespace lms::db
Wt::Dbo::field(a, _manualDownloadState, "manual_download_state");
Wt::Dbo::field(a, _audioRelativeFilePath, "audio_relative_file_path");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _container, "container");
Wt::Dbo::field(a, _codec, "codec");
Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _channelCount, "channel_count");
Wt::Dbo::field(a, _sampleRate, "sample_rate");
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
Wt::Dbo::field(a, _title, "title");
Wt::Dbo::field(a, _link, "link");
Wt::Dbo::field(a, _description, "description");
@@ -148,7 +178,6 @@ namespace lms::db
Wt::Dbo::field(a, _subtitle, "subtitle");
Wt::Dbo::field(a, _summary, "summary");
Wt::Dbo::field(a, _explicit, "explicit");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::belongsTo(a, _artwork, "artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::belongsTo(a, _podcast, "podcast", Wt::Dbo::OnDeleteCascade);
@@ -162,6 +191,15 @@ namespace lms::db
ManualDownloadState _manualDownloadState{ ManualDownloadState::None };
std::filesystem::path _audioRelativeFilePath; // relative to cache dir, only set if downloaded
// Audio properties
std::chrono::duration<int, std::milli> _duration{ 0 };
detail::Container _container{ detail::Container::Unknown };
detail::Codec _codec{ detail::Codec::Unknown };
int _bitrate{}; // in bps
int _channelCount{};
int _sampleRate{};
std::optional<int> _bitsPerSample;
std::string _url;
std::string _title;
std::string _link;
@@ -178,7 +216,6 @@ namespace lms::db
std::string _subtitle;
std::string _summary;
bool _explicit{};
std::chrono::duration<int, std::milli> _duration{ 0 };
Wt::Dbo::ptr<Artwork> _artwork;
Wt::Dbo::ptr<Podcast> _podcast;
@@ -28,6 +28,7 @@
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/RatedArtistId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -28,6 +28,7 @@
#include "database/Types.hpp"
#include "database/objects/RatedReleaseId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -28,6 +28,7 @@
#include "database/Types.hpp"
#include "database/objects/RatedTrackId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -31,6 +31,8 @@
#include "core/EnumSet.hpp"
#include "core/PartialDateTime.hpp"
#include "core/UUID.hpp"
#include "core/media/Codec.hpp"
#include "database/IdRange.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
@@ -43,6 +45,7 @@
#include "database/objects/MediaLibraryId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/ReleaseTypeId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -277,6 +280,7 @@ namespace lms::db
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
std::size_t getMeanBitrate() const;
std::vector<core::media::Codec> getCodecs() const;
// Accessors
std::string_view getName() const { return _name; }
@@ -26,6 +26,7 @@
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/StarredArtistId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -26,6 +26,7 @@
#include "database/Types.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/StarredReleaseId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -28,6 +28,7 @@
#include "database/Types.hpp"
#include "database/objects/StarredTrackId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -34,6 +34,9 @@
#include "core/EnumSet.hpp"
#include "core/PartialDateTime.hpp"
#include "core/UUID.hpp"
#include "core/media/Codec.hpp"
#include "core/media/Container.hpp"
#include "database/IdRange.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
@@ -48,7 +51,9 @@
#include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/TrackListId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
#include "database/objects/detail/Types.hpp"
namespace lms::db
{
@@ -216,19 +221,28 @@ namespace lms::db
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
static void updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
// Accessors
// Setters
void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
void setName(std::string_view name);
// File properties
void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setLastWriteTime(const Wt::WDateTime& time) { _fileLastWrite = time; }
void setAddedTime(const Wt::WDateTime& time) { _fileAdded = time; }
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; }
// Audio properties
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setContainer(core::media::Container container);
void setCodec(core::media::Codec codec);
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; }
void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; }
void setSampleRate(std::size_t sampleRate) { _sampleRate = sampleRate; }
void setBitsPerSample(std::optional<std::size_t> bitsPerSample) { _bitsPerSample = bitsPerSample; }
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
// Metadata
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
void setName(std::string_view name);
void setDate(const core::PartialDateTime& date) { _date = date; }
void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; }
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
@@ -236,7 +250,6 @@ namespace lms::db
void setCopyright(std::string_view copyright);
void setCopyrightURL(std::string_view copyrightURL);
void setAdvisory(Advisory advisory) { _advisory = advisory; }
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void setComment(std::string_view comment) { _comment = comment; }
void clearArtistLinks();
@@ -254,32 +267,41 @@ namespace lms::db
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
void setPreferredMediaArtwork(ObjectPtr<Artwork> artwork);
// Getters
std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::string getName() const { return _name; }
// File properties
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
long long getFileSize() const { return _fileSize; }
std::size_t getBitrate() const { return _bitrate; }
std::size_t getBitsPerSample() const { return _bitsPerSample; }
std::size_t getChannelCount() const { return _channelCount; }
std::chrono::milliseconds getDuration() const { return _duration; }
std::size_t getSampleRate() const { return _sampleRate; }
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
// Audio properties
std::chrono::milliseconds getDuration() const { return _duration; }
std::optional<core::media::Container> getContainer() const;
std::optional<core::media::Codec> 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<std::size_t> getBitsPerSample() const { return _bitsPerSample; }
std::optional<float> getReplayGain() const { return _replayGain; }
// Metadata
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::string getName() const { return _name; }
const core::PartialDateTime& getDate() const { return _date; }
std::optional<int> getYear() const;
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
std::optional<int> getOriginalYear() const;
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
Advisory getAdvisory() const { return _advisory; }
std::optional<float> getReplayGain() const { return _replayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::string_view getComment() const { return _comment; }
Advisory getAdvisory() const { return _advisory; }
// no artistLinkTypes means get all
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
@@ -304,27 +326,32 @@ namespace lms::db
void persist(Action& a)
{
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
Wt::Dbo::field(a, _channelCount, "channel_count");
Wt::Dbo::field(a, _sampleRate, "sample_rate");
Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
Wt::Dbo::field(a, _fileSize, "file_size");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _container, "container");
Wt::Dbo::field(a, _codec, "codec");
Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _channelCount, "channel_count");
Wt::Dbo::field(a, _sampleRate, "sample_rate");
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
Wt::Dbo::field(a, _replayGain, "replay_gain");
Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _trackMBID, "mbid");
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _advisory, "advisory");
Wt::Dbo::field(a, _replayGain, "replay_gain");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table?
Wt::Dbo::field(a, _advisory, "advisory");
Wt::Dbo::belongsTo(a, _medium, "medium", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
@@ -347,27 +374,36 @@ namespace lms::db
static constexpr std::size_t _maxCopyrightURLLength{ 512 };
int _scanVersion{};
std::optional<int> _trackNumber;
std::string _name;
int _bitrate{}; // in bps
int _bitsPerSample{};
int _channelCount{};
std::chrono::duration<int, std::milli> _duration{};
int _sampleRate{};
core::PartialDateTime _date;
core::PartialDateTime _originalDate;
// File information
std::filesystem::path _absoluteFilePath; // full path
long long _fileSize{};
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
// Audio properties
std::chrono::duration<int, std::milli> _duration{};
detail::Container _container{ detail::Container::Unknown };
detail::Codec _codec{ detail::Codec::Unknown };
int _bitrate{}; // in bps
int _channelCount{};
int _sampleRate{};
std::optional<int> _bitsPerSample;
std::optional<float> _replayGain;
// Metadata
std::optional<int> _trackNumber;
std::string _name;
core::PartialDateTime _date;
core::PartialDateTime _originalDate;
std::string _trackMBID;
std::string _recordingMBID;
std::string _copyright;
std::string _copyrightURL;
Advisory _advisory{ Advisory::UnSet };
std::optional<float> _replayGain;
std::string _artistDisplayName;
std::string _comment;
Advisory _advisory{ Advisory::UnSet };
Wt::Dbo::ptr<Medium> _medium;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
@@ -26,12 +26,14 @@
#include <Wt/Dbo/Field.h>
#include "core/EnumSet.hpp"
#include "database/IdType.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
@@ -26,6 +26,8 @@
#include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h>
#include "core/media/ImageType.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/MediumId.hpp"
@@ -33,6 +35,7 @@
#include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/TrackListId.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
@@ -52,7 +55,7 @@ namespace lms::db
ReleaseId release;
MediumId medium;
TrackListId trackList;
std::optional<ImageType> imageType;
std::optional<core::media::ImageType> imageType;
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
@@ -80,7 +83,7 @@ namespace lms::db
trackList = _trackList;
return *this;
}
FindParameters& setImageType(std::optional<ImageType> _imageType)
FindParameters& setImageType(std::optional<core::media::ImageType> _imageType)
{
imageType = _imageType;
return *this;
@@ -24,10 +24,13 @@
#include <Wt/Dbo/Field.h>
#include "core/media/ImageType.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackEmbeddedImageLinkId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/detail/Types.hpp"
namespace lms::db
{
@@ -49,12 +52,12 @@ namespace lms::db
ObjectPtr<Track> getTrack() const;
ObjectPtr<TrackEmbeddedImage> getImage() const;
std::size_t getIndex() const { return _index; }
ImageType getType() const { return _type; }
core::media::ImageType getType() const;
std::string_view getDescription() const { return _description; }
// setters
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
void setType(ImageType type) { _type = type; }
void setType(core::media::ImageType type);
void setDescription(std::string_view description) { _description = description; }
template<class Action>
@@ -75,7 +78,7 @@ namespace lms::db
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
int _index{}; // index within the track
ImageType _type{ ImageType::Unknown };
detail::ImageType _type{ detail::ImageType::Unknown };
std::string _description;
Wt::Dbo::ptr<Track> _track;
@@ -33,6 +33,7 @@
#include "database/objects/Filters.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/TrackListId.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -32,6 +32,7 @@
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
LMS_DECLARE_IDTYPE(TrackLyricsId)
@@ -0,0 +1,243 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WDate.h>
#include "core/LiteralString.hpp"
#include "core/TaggedType.hpp"
namespace lms::db
{
// For enums that have explicit values, never change enum values as they are stored in database.
// => only add new values or remove unused values, do not recycle values.
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<class ImageHash, std::uint64_t>;
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 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;
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
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
@@ -28,8 +28,10 @@
#include "core/EnumSet.hpp"
#include "core/UUID.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp"
namespace lms::db
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
namespace lms::db::detail
{
// For enums that have explicit values, never change enum values as they are stored in database.
// => only add new values or remove unused values, do not recycle values.
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 Container
{
Unknown = 0,
AIFF = 1,
APE = 2,
ASF = 3,
DSF = 4,
FLAC = 5,
MP4 = 6,
MPC = 7,
MPEG = 8,
Ogg = 9,
Shorten = 10,
TrueAudio = 11,
WAV = 12,
WavPack = 13,
};
enum class Codec
{
Unknown = 0,
AAC = 1,
AC3 = 2,
ALAC = 3,
APE = 4,
DSD = 5,
EAC3 = 6,
FLAC = 7,
MP3 = 8,
MP4ALS = 9,
MPC7 = 10,
MPC8 = 11,
Opus = 12,
PCM = 13,
Shorten = 14,
TrueAudio = 15,
Vorbis = 16,
WavPack = 17,
WMA1 = 18,
WMA2 = 19,
WMA9Pro = 20,
WMA9Lossless = 21,
};
} // namespace lms::db::detail
+28
View File
@@ -518,6 +518,34 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Artist_findByCodec)
{
ScopedArtist artist1{ session, "A" };
ScopedArtist artist2{ session, "B" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setCodec(core::media::Codec::MP3);
track2.get().modify()->setCodec(core::media::Codec::FLAC);
}
{
auto transaction{ session.createReadTransaction() };
Artist::FindParameters params;
params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC));
const auto artists{ Artist::find(session, params) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist2.getId());
}
}
TEST_F(DatabaseFixture, Artist_findByName)
{
ScopedArtist artist{ session, "AAA" };
+1
View File
@@ -20,6 +20,7 @@
#include "Common.hpp"
#include "core/String.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp"
#include "database/objects/AuthToken.hpp"
+68
View File
@@ -1214,6 +1214,74 @@ namespace lms::db::tests
checkExpectedBitrate(192); // 0 should not be taken into account
}
TEST_F(DatabaseFixture, Release_codec)
{
ScopedRelease release1{ session, "MyRelease1" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
ScopedTrack track4{ session };
{
auto transaction{ session.createReadTransaction() };
ASSERT_EQ(release1->getCodecs().size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setCodec(core::media::Codec::FLAC);
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setCodec(core::media::Codec::MP3);
track2.get().modify()->setRelease(release1.get());
track3.get().modify()->setCodec(core::media::Codec::MP3);
track3.get().modify()->setRelease(release1.get());
track4.get().modify()->setCodec(core::media::Codec::AAC);
track4.get().modify()->setRelease(release1.get());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release1->getCodecs(), (std::vector<core::media::Codec>{ core::media::Codec::MP3, core::media::Codec::FLAC, core::media::Codec::AAC }));
}
}
TEST_F(DatabaseFixture, Release_findBycodec)
{
ScopedRelease release1{ session, "MyRelease1" };
ScopedTrack track1{ session };
ScopedRelease release2{ session, "MyRelease2" };
ScopedTrack track2{ session };
{
auto transaction{ session.createReadTransaction() };
ASSERT_EQ(release1->getCodecs().size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setCodec(core::media::Codec::FLAC);
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setCodec(core::media::Codec::MP3);
track2.get().modify()->setRelease(release2.get());
}
{
auto transaction{ session.createReadTransaction() };
Release::FindParameters params;
params.setFilters(Filters{}.setCodec(core::media::Codec::MP3));
auto releases{ Release::findIds(session, params) };
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release2.getId());
}
}
TEST_F(DatabaseFixture, Release_trackCount)
{
ScopedRelease release1{ session, "MyRelease1" };

Some files were not shown because too many files have changed in this diff Show More