From d69827c2a287551adf8430b28b0b78590b0e6c8f Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 20 Nov 2025 23:15:07 +0100 Subject: [PATCH 01/22] Added a fallback to ffmpeg parser in case of audio properties are not properly decoded, ref #781 --- src/libs/audio/CMakeLists.txt | 4 +- src/libs/audio/impl/AudioFileInfoParser.cpp | 41 +++++ src/libs/audio/impl/AudioTypes.cpp | 4 + src/libs/audio/impl/ParseAudioFileInfo.cpp | 57 ------- src/libs/audio/impl/ffmpeg/AudioFile.cpp | 149 ++++++++++++------ src/libs/audio/impl/ffmpeg/AudioFile.hpp | 6 +- src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp | 75 ++++++--- src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp | 15 +- .../audio/impl/ffmpeg/AudioFileInfoParser.cpp | 38 +++++ .../audio/impl/ffmpeg/AudioFileInfoParser.hpp | 32 ++++ src/libs/audio/impl/ffmpeg/ImageReader.cpp | 3 +- src/libs/audio/impl/ffmpeg/TagReader.cpp | 2 +- src/libs/audio/impl/taglib/AudioFileInfo.cpp | 83 ++++++---- src/libs/audio/impl/taglib/AudioFileInfo.hpp | 14 +- .../audio/impl/taglib/AudioFileInfoParser.cpp | 38 +++++ .../audio/impl/taglib/AudioFileInfoParser.hpp | 32 ++++ src/libs/audio/impl/taglib/Utils.cpp | 21 ++- src/libs/audio/impl/taglib/Utils.hpp | 4 +- src/libs/audio/include/audio/AudioTypes.hpp | 2 + src/libs/audio/include/audio/Exception.hpp | 18 +++ .../audio/include/audio/IAudioFileInfo.hpp | 57 +------ .../include/audio/IAudioFileInfoParser.hpp | 61 +++++++ .../services/artwork/impl/ArtworkService.cpp | 14 +- .../services/artwork/impl/ArtworkService.hpp | 6 + src/libs/services/scanner/CMakeLists.txt | 1 + .../audiofile/AudioFileInfoParserSet.cpp | 60 +++++++ .../audiofile/AudioFileInfoParserSet.hpp | 39 +++++ .../audiofile/AudioFileScanOperation.cpp | 41 ++++- .../audiofile/AudioFileScanOperation.hpp | 6 +- .../scanners/audiofile/AudioFileScanner.cpp | 36 +---- .../scanners/audiofile/AudioFileScanner.hpp | 19 +-- .../impl/endpoints/MediaRetrieval.cpp | 15 +- src/lms/ui/explore/ReleaseView.cpp | 14 +- src/lms/ui/explore/TrackListHelpers.cpp | 14 +- src/tools/audioinfo/LmsAudioInfo.cpp | 33 ++-- 35 files changed, 736 insertions(+), 318 deletions(-) create mode 100644 src/libs/audio/impl/AudioFileInfoParser.cpp delete mode 100644 src/libs/audio/impl/ParseAudioFileInfo.cpp create mode 100644 src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp create mode 100644 src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp create mode 100644 src/libs/audio/impl/taglib/AudioFileInfoParser.cpp create mode 100644 src/libs/audio/impl/taglib/AudioFileInfoParser.hpp create mode 100644 src/libs/audio/include/audio/IAudioFileInfoParser.hpp create mode 100644 src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.cpp create mode 100644 src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.hpp diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt index 48f071bb..bd8d1d4f 100644 --- a/src/libs/audio/CMakeLists.txt +++ b/src/libs/audio/CMakeLists.txt @@ -4,17 +4,19 @@ pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) add_library(lmsaudio STATIC impl/ffmpeg/AudioFile.cpp impl/ffmpeg/AudioFileInfo.cpp + impl/ffmpeg/AudioFileInfoParser.cpp impl/ffmpeg/ImageReader.cpp impl/ffmpeg/TagReader.cpp impl/ffmpeg/Transcoder.cpp impl/ffmpeg/Utils.cpp impl/taglib/AudioFileInfo.cpp + impl/taglib/AudioFileInfoParser.cpp impl/taglib/ImageReader.cpp impl/taglib/TagReader.cpp impl/taglib/Utils.cpp impl/AudioTypes.cpp impl/ImageReader.cpp - impl/ParseAudioFileInfo.cpp + impl/AudioFileInfoParser.cpp impl/TagReader.cpp ) diff --git a/src/libs/audio/impl/AudioFileInfoParser.cpp b/src/libs/audio/impl/AudioFileInfoParser.cpp new file mode 100644 index 00000000..069399d7 --- /dev/null +++ b/src/libs/audio/impl/AudioFileInfoParser.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include "audio/IAudioFileInfoParser.hpp" + +#include "ffmpeg/AudioFileInfoParser.hpp" +#include "taglib/AudioFileInfoParser.hpp" + +namespace lms::audio +{ + std::unique_ptr createAudioFileInfoParser(AudioFileInfoParserBackend backend) + { + switch (backend) + { + case AudioFileInfoParserBackend::TagLib: + return std::make_unique(); + case AudioFileInfoParserBackend::FFmpeg: + return std::make_unique(); + } + + return nullptr; + } +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/audio/impl/AudioTypes.cpp index d0a5930b..ad360bcb 100644 --- a/src/libs/audio/impl/AudioTypes.cpp +++ b/src/libs/audio/impl/AudioTypes.cpp @@ -62,12 +62,16 @@ namespace lms::audio { case CodecType::AAC: return "AAC"; + case CodecType::AC3: + return "AC3"; case CodecType::ALAC: return "ALAC"; case CodecType::APE: return "APE"; case CodecType::DSD: return "DSD"; + case CodecType::EAC3: + return "E-AC3"; case CodecType::FLAC: return "FLAC"; case CodecType::MP3: diff --git a/src/libs/audio/impl/ParseAudioFileInfo.cpp b/src/libs/audio/impl/ParseAudioFileInfo.cpp deleted file mode 100644 index 321d46a7..00000000 --- a/src/libs/audio/impl/ParseAudioFileInfo.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2015 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include - -#include "audio/IAudioFileInfo.hpp" - -#include "ffmpeg/AudioFileInfo.hpp" -#include "ffmpeg/Utils.hpp" -#include "taglib/AudioFileInfo.hpp" -#include "taglib/Utils.hpp" - -namespace lms::audio -{ - std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions) - { - switch (parserOptions.parser) - { - case ParserOptions::Parser::TagLib: - return std::make_unique(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs); - case ParserOptions::Parser::FFmpeg: - return std::make_unique(p, parserOptions.enableExtraDebugLogs); - } - - return {}; - } - - std::span getSupportedExtensions(ParserOptions::Parser parser) - { - switch (parser) - { - case ParserOptions::Parser::TagLib: - return taglib::utils::getSupportedExtensions(); - case ParserOptions::Parser::FFmpeg: - return ffmpeg::utils::getSupportedExtensions(); - } - - return {}; - } - -} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp index ad1861a7..7f679b99 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -20,21 +20,22 @@ #include "AudioFile.hpp" #include +#include #include extern "C" { -#define __STDC_CONSTANT_MACROS #include #include #include +#include } #include "core/ILogger.hpp" #include "core/String.hpp" #include "audio/AudioTypes.hpp" -#include "audio/IAudioFileInfo.hpp" +#include "audio/Exception.hpp" namespace lms::audio::ffmpeg { @@ -50,25 +51,23 @@ namespace lms::audio::ffmpeg return "Unknown error"; } - class AudioFileException : public AudioFileParsingException + class AvException : public Exception { public: - AudioFileException(int avError) - : AudioFileParsingException{ averror_to_string(avError) } + AvException(int avError) + : Exception{ averror_to_string(avError) } { } }; - void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) + void extractMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) { if (!dictionnary) return; - AVDictionaryEntry* tag = NULL; - while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) - { + const AVDictionaryEntry* tag{ NULL }; + while ((tag = av_dict_iterate(dictionnary, tag))) res[core::stringUtils::stringToUpper(tag->key)] = tag->value; - } } std::optional avdemuxerToContainerType(std::string_view name) @@ -107,58 +106,112 @@ namespace lms::audio::ffmpeg { 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; + case AV_CODEC_ID_AC3: + return CodecType::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; 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; 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; + case AV_CODEC_ID_EAC3: + return CodecType::EAC3; + case AV_CODEC_ID_FLAC: + return CodecType::FLAC; + case AV_CODEC_ID_MP3: + return CodecType::MP3; + case AV_CODEC_ID_MP4ALS: + return CodecType::MP4ALS; + case AV_CODEC_ID_MUSEPACK7: + return CodecType::MPC7; + case AV_CODEC_ID_MUSEPACK8: + return CodecType::MPC8; + case AV_CODEC_ID_OPUS: + return CodecType::Opus; + case AV_CODEC_ID_SHORTEN: + return CodecType::Shorten; + case AV_CODEC_ID_VORBIS: + return CodecType::Vorbis; + case AV_CODEC_ID_WAVPACK: + return CodecType::WavPack; + case AV_CODEC_ID_WMALOSSLESS: + return CodecType::WMA9Lossless; + case AV_CODEC_ID_WMAPRO: + return CodecType::WMA9Pro; + case AV_CODEC_ID_WMAV1: + return CodecType::WMA1; + case AV_CODEC_ID_WMAV2: + return CodecType::WMA2; default: return std::nullopt; } } + + core::LiteralString avLogLevelToStr(int level) + { + switch (level) + { + case AV_LOG_TRACE: + return "trace"; + case AV_LOG_DEBUG: + return "debug"; + case AV_LOG_VERBOSE: + return "verbose"; + case AV_LOG_INFO: + return "info"; + case AV_LOG_WARNING: + return "warning"; + case AV_LOG_ERROR: + return "error"; + case AV_LOG_FATAL: + return "fatal"; + case AV_LOG_PANIC: + return "panic"; + default: + return "unknown"; + } + } + + void avLogCallback(void*, int level, const char* fmt, va_list vl) + { + if (!core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) + return; + + if (level > AV_LOG_WARNING) + return; + + std::array buffer{ 0 }; + std::vsnprintf(buffer.data(), buffer.size(), fmt, vl); + + LMS_LOG(AUDIO, DEBUG, "ffmpeg [" << avLogLevelToStr(level) << "] " << buffer.data()); + } + + class AvInitializer + { + public: + AvInitializer() + { + ::av_log_set_callback(avLogCallback); + } + }; } // namespace AudioFile::AudioFile(const std::filesystem::path& p) : _p{ p } { + 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 +219,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 }; } } @@ -193,11 +246,11 @@ namespace lms::audio::ffmpeg 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 +258,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 +319,7 @@ namespace lms::audio::ffmpeg return false; } - void AudioFile::visitAttachedPictures(std::function func) const + void AudioFile::visitAttachedPictures(std::function func) const { static const std::unordered_map codecMimeMap{ { AV_CODEC_ID_BMP, "image/bmp" }, @@ -286,14 +339,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 +356,10 @@ namespace lms::audio::ffmpeg else { picture.mimeType = "application/octet-stream"; - LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"); + LMS_LOG(AUDIO, WARNING, "AVCodecID" << avstream->codecpar->codec_id << " (" << ::avcodec_get_name(avstream->codecpar->codec_id) << ") not handled in mime type conversion"); } - const AVPacket& pkt{ avstream->attached_pic }; + const ::AVPacket& pkt{ avstream->attached_pic }; picture.data = std::span{ reinterpret_cast(pkt.data), static_cast(pkt.size) }; func(picture, metadata); @@ -325,7 +378,7 @@ namespace lms::audio::ffmpeg if (!avstream->codecpar) { - LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set"); + LMS_LOG(AUDIO, WARNING, "Skipping stream " << streamIndex << " since no codecpar is set"); return res; } diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp index 2af7fb1b..64c4303d 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -36,7 +36,7 @@ extern "C" namespace lms::audio::ffmpeg { - struct Picture + struct PictureView { std::string mimeType; std::span data; // valid as long as IAudioFile exists @@ -75,12 +75,12 @@ namespace lms::audio::ffmpeg const std::filesystem::path& getPath() const; ContainerInfo getContainerInfo() const; - MetadataMap getMetaData() const; + MetadataMap extractMetaData() const; std::vector getStreamInfo() const; std::optional getBestStreamInfo() const; std::optional getBestStreamIndex() const; bool hasAttachedPictures() const; - void visitAttachedPictures(std::function func) const; + void visitAttachedPictures(std::function func) const; private: std::optional getStreamInfo(std::size_t streamIndex) const; diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp index fe0793f9..f1ebbcc6 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp @@ -19,6 +19,8 @@ #include "AudioFileInfo.hpp" +#include "core/ILogger.hpp" + #include "audio/AudioTypes.hpp" #include "audio/IAudioFileInfo.hpp" @@ -30,65 +32,88 @@ namespace lms::audio::ffmpeg { namespace { - AudioProperties computeAudioProperties(const AudioFile& audioFile) + std::optional computeAudioProperties(const AudioFile& audioFile) { - AudioProperties audioProperties; + std::optional audioProperties; const auto containerInfo{ audioFile.getContainerInfo() }; const auto bestStreamInfo{ audioFile.getBestStreamInfo() }; if (!bestStreamInfo) - throw AudioFileParsingException{ "Cannot find best audio stream" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot find best audio stream in " << audioFile.getPath()); + return audioProperties; + } if (!containerInfo.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 + "'" }; + { + LMS_LOG(AUDIO, DEBUG, "Unhandled codec '" << bestStreamInfo->codecName << "' in " << audioFile.getPath()); + return audioProperties; + } if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0) - throw AudioFileParsingException{ "Cannot determine bitrate" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate 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; + audioProperties->bitrate = *bestStreamInfo->bitrate; + audioProperties->channelCount = *bestStreamInfo->channelCount; + audioProperties->sampleRate = *bestStreamInfo->sampleRate; + audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample; return audioProperties; } } // namespace - AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs) + AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions) : _audioFile{ std::make_unique(filePath) } - , _audioProperties{ std::make_unique(computeAudioProperties(*_audioFile)) } - , _tagReader{ std::make_unique(*_audioFile, enableExtraDebugLogs) } - , _imageReader{ std::make_unique(*_audioFile) } + , _audioProperties{ computeAudioProperties(*_audioFile) } { + if (parseOptions.readTags) + _tagReader = std::make_unique(*_audioFile, parseOptions.enableExtraDebugLogs); + + if (parseOptions.readImages) + _imageReader = std::make_unique(*_audioFile); } AudioFileInfo::~AudioFileInfo() = default; - const AudioProperties& AudioFileInfo::getAudioProperties() const + const AudioProperties* AudioFileInfo::getAudioProperties() const { - return *_audioProperties; + return _audioProperties.has_value() ? &_audioProperties.value() : nullptr; } - const IImageReader& AudioFileInfo::getImageReader() const + const IImageReader* AudioFileInfo::getImageReader() const { - return *_imageReader; + return _imageReader.get(); } - const ITagReader& AudioFileInfo::getTagReader() const + const ITagReader* AudioFileInfo::getTagReader() const { - return *_tagReader; + return _tagReader.get(); } } // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp index e8c806a7..8d89a766 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp @@ -20,9 +20,11 @@ #pragma once #include +#include #include "audio/AudioTypes.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" namespace lms::audio::ffmpeg { @@ -33,18 +35,19 @@ namespace lms::audio::ffmpeg class AudioFileInfo final : public IAudioFileInfo { public: - AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs); - ~AudioFileInfo(); + AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions); + ~AudioFileInfo() override; + AudioFileInfo(const AudioFileInfo&) = delete; AudioFileInfo& operator=(const AudioFileInfo&) = delete; private: - const AudioProperties& getAudioProperties() const override; - const IImageReader& getImageReader() const override; - const ITagReader& getTagReader() const override; + const AudioProperties* getAudioProperties() const override; + const IImageReader* getImageReader() const override; + const ITagReader* getTagReader() const override; std::unique_ptr _audioFile; - std::unique_ptr _audioProperties; + const std::optional _audioProperties; std::unique_ptr _tagReader; std::unique_ptr _imageReader; }; diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp new file mode 100644 index 00000000..9af6d1f8 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "AudioFileInfoParser.hpp" + +#include + +#include "AudioFileInfo.hpp" +#include "Utils.hpp" + +namespace lms::audio::ffmpeg +{ + std::unique_ptr AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const + { + return std::make_unique(p, parseOptions); + } + + std::span AudioFileInfoParser::getSupportedExtensions() const + { + return utils::getSupportedExtensions(); + } +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp new file mode 100644 index 00000000..0c3a1e98 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfoParser.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "audio/IAudioFileInfoParser.hpp" + +namespace lms::audio::ffmpeg +{ + class AudioFileInfoParser : public IAudioFileInfoParser + { + private: + std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override; + std::span getSupportedExtensions() const override; + }; +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp index ad935d1e..8014ea93 100644 --- a/src/libs/audio/impl/ffmpeg/ImageReader.cpp +++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp @@ -40,7 +40,7 @@ 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; @@ -52,5 +52,4 @@ namespace lms::audio::ffmpeg visitor(image); }); } - } // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/impl/ffmpeg/TagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp index fa5d9e4f..8c3eccda 100644 --- a/src/libs/audio/impl/ffmpeg/TagReader.cpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp @@ -145,7 +145,7 @@ namespace lms::audio::ffmpeg TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs) : _audioFile{ audioFile } - , _metaDataMap{ audioFile.getMetaData() } + , _metaDataMap{ audioFile.extractMetaData() } { if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp index 6cc81dea..02b41ed5 100644 --- a/src/libs/audio/impl/taglib/AudioFileInfo.cpp +++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp @@ -43,8 +43,11 @@ #include #endif +#include "core/ILogger.hpp" + #include "audio/AudioTypes.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "ImageReader.hpp" #include "TagReader.hpp" @@ -54,31 +57,48 @@ namespace lms::audio::taglib { namespace { - AudioProperties computeAudioProperties(const ::TagLib::File& file) + std::optional computeAudioProperties(const ::TagLib::File& file, const std::filesystem::path& filePath) { assert(file.audioProperties()); AudioProperties audioProperties; { - const ::TagLib::AudioProperties& properties{ *file.audioProperties() }; + const ::TagLib::AudioProperties* properties{ file.audioProperties() }; + if (!properties) + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine audio properties in " << filePath); + return std::nullopt; + } // Common properties - audioProperties.bitrate = static_cast(properties.bitrate() * 1000); + audioProperties.bitrate = static_cast(properties->bitrate() * 1000); if (audioProperties.bitrate == 0) - throw AudioFileParsingException{ "Cannot determine bitrate" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << filePath); + return std::nullopt; + } - audioProperties.channelCount = static_cast(properties.channels()); + audioProperties.channelCount = static_cast(properties->channels()); if (audioProperties.channelCount == 0) - throw AudioFileParsingException{ "Cannot determine channel count" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << filePath); + return std::nullopt; + } - audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() }; + audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; if (audioProperties.duration == decltype(audioProperties.duration)::zero()) - throw AudioFileParsingException{ "Cannot determine duration" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << filePath); + return std::nullopt; + } - audioProperties.sampleRate = static_cast(properties.sampleRate()); + audioProperties.sampleRate = static_cast(properties->sampleRate()); if (audioProperties.sampleRate == 0) - throw AudioFileParsingException{ "Cannot determine sample rate" }; + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << filePath); + return std::nullopt; + } } // Guess container from the file type @@ -107,7 +127,8 @@ namespace lms::audio::taglib audioProperties.codec = CodecType::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(); @@ -138,7 +159,8 @@ namespace lms::audio::taglib audioProperties.codec = CodecType::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(); @@ -156,7 +178,8 @@ namespace lms::audio::taglib audioProperties.codec = CodecType::MPC8; break; default: - throw AudioFileParsingException{ "Unhandled MPC codec" }; + LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath); + return std::nullopt; } } else if (const auto* mpegFile{ dynamic_cast(&file) }) @@ -172,7 +195,10 @@ namespace lms::audio::taglib audioProperties.codec = CodecType::AAC; #endif else - throw AudioFileParsingException{ "Unhandled MPEG codec" }; + { + LMS_LOG(AUDIO, DEBUG, "Unhandled MPEG codec in " << filePath); + return std::nullopt; + } } else if (dynamic_cast(&file)) { @@ -218,7 +244,8 @@ namespace lms::audio::taglib } else { - throw AudioFileParsingException{ "Unhandled file type" }; + LMS_LOG(AUDIO, DEBUG, "Unhandled file type in " << filePath); + return std::nullopt; } if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0) @@ -228,30 +255,32 @@ namespace lms::audio::taglib } } // namespace - AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs) + AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions) : _filePath{ filePath } - , _file{ utils::parseFile(filePath, readStyle) } - , _audioProperties{ std::make_unique(computeAudioProperties(*_file)) } - , _tagReader{ std::make_unique(*_file, enableExtraDebugLogs) } - , _imageReader{ std::make_unique(*_file) } + , _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) } + , _audioProperties{ computeAudioProperties(*_file, filePath) } { + if (parseOptions.readTags) + _tagReader = std::make_unique(*_file, parseOptions.enableExtraDebugLogs); + + if (parseOptions.readImages) + _imageReader = std::make_unique(*_file); } AudioFileInfo::~AudioFileInfo() = default; - const AudioProperties& AudioFileInfo::getAudioProperties() const + const AudioProperties* AudioFileInfo::getAudioProperties() const { - return *_audioProperties; + return _audioProperties.has_value() ? &_audioProperties.value() : nullptr; } - const IImageReader& AudioFileInfo::getImageReader() const + const IImageReader* AudioFileInfo::getImageReader() const { - return *_imageReader; + return _imageReader.get(); } - const ITagReader& AudioFileInfo::getTagReader() const + const ITagReader* AudioFileInfo::getTagReader() const { - return *_tagReader; + return _tagReader.get(); } - } // namespace lms::audio::taglib diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp index 4cd85452..4dca6f08 100644 --- a/src/libs/audio/impl/taglib/AudioFileInfo.hpp +++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp @@ -19,8 +19,12 @@ #pragma once +#include +#include + #include "audio/AudioTypes.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "audio/IImageReader.hpp" #include "audio/ITagReader.hpp" @@ -37,20 +41,20 @@ namespace lms::audio::taglib class AudioFileInfo final : public IAudioFileInfo { public: - AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs); + AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions); ~AudioFileInfo() override; AudioFileInfo(const AudioFileInfo&) = delete; AudioFileInfo& operator=(const AudioFileInfo&) = delete; private: - const AudioProperties& getAudioProperties() const override; - const IImageReader& getImageReader() const override; - const ITagReader& getTagReader() const override; + const AudioProperties* getAudioProperties() const override; + const IImageReader* getImageReader() const override; + const ITagReader* getTagReader() const override; const std::filesystem::path _filePath; std::unique_ptr<::TagLib::File> _file; - std::unique_ptr _audioProperties; + std::optional _audioProperties; std::unique_ptr _tagReader; std::unique_ptr _imageReader; }; diff --git a/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp b/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp new file mode 100644 index 00000000..bc87c66e --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfoParser.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "AudioFileInfoParser.hpp" + +#include + +#include "AudioFileInfo.hpp" +#include "Utils.hpp" + +namespace lms::audio::taglib +{ + std::unique_ptr AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const + { + return std::make_unique(p, parseOptions); + } + + std::span AudioFileInfoParser::getSupportedExtensions() const + { + return utils::getSupportedExtensions(); + } +} // namespace lms::audio::taglib \ No newline at end of file diff --git a/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp b/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp new file mode 100644 index 00000000..c6e30cc4 --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfoParser.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "audio/IAudioFileInfoParser.hpp" + +namespace lms::audio::taglib +{ + class AudioFileInfoParser : public IAudioFileInfoParser + { + private: + std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override; + std::span getSupportedExtensions() const override; + }; +} // namespace lms::audio::taglib \ No newline at end of file diff --git a/src/libs/audio/impl/taglib/Utils.cpp b/src/libs/audio/impl/taglib/Utils.cpp index aebdaf05..121a2a6a 100644 --- a/src/libs/audio/impl/taglib/Utils.cpp +++ b/src/libs/audio/impl/taglib/Utils.cpp @@ -47,7 +47,7 @@ #include "core/ITraceLogger.hpp" #include "core/String.hpp" -#include "audio/IAudioFileInfo.hpp" +#include "audio/Exception.hpp" namespace lms::audio::taglib::utils { @@ -89,15 +89,15 @@ namespace lms::audio::taglib::utils return std::span{ supportedExtensions }; } - TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle) + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle) { switch (readStyle) { - case ParserOptions::AudioPropertiesReadStyle::Fast: + case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; - case ParserOptions::AudioPropertiesReadStyle::Average: + case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; - case ParserOptions::AudioPropertiesReadStyle::Accurate: + case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; } @@ -111,7 +111,7 @@ namespace lms::audio::taglib::utils { const std::error_code ec{ errno, std::generic_category() }; LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message()); - throw IOException{ "fopen failed", ec }; + throw IOFileException{ "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{ "fileno failed", ec }; } return TagLib::FileStream{ fd, true }; @@ -236,7 +236,7 @@ namespace lms::audio::taglib::utils return file; } - std::unique_ptr parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle) + std::unique_ptr parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle) { LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile"); @@ -252,10 +252,7 @@ namespace lms::audio::taglib::utils } if (!file) - throw AudioFileParsingException{ "Parsing failed" }; - - if (!file->audioProperties()) - throw AudioFileParsingException{ "No audio properties" }; + throw Exception{ "Parsing failed" }; return file; } diff --git a/src/libs/audio/impl/taglib/Utils.hpp b/src/libs/audio/impl/taglib/Utils.hpp index f79e96bf..fa614508 100644 --- a/src/libs/audio/impl/taglib/Utils.hpp +++ b/src/libs/audio/impl/taglib/Utils.hpp @@ -25,10 +25,10 @@ #include -#include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" namespace lms::audio::taglib::utils { std::span getSupportedExtensions(); - std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle); + std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle); } // namespace lms::audio::taglib::utils \ No newline at end of file diff --git a/src/libs/audio/include/audio/AudioTypes.hpp b/src/libs/audio/include/audio/AudioTypes.hpp index 84478949..952f9c14 100644 --- a/src/libs/audio/include/audio/AudioTypes.hpp +++ b/src/libs/audio/include/audio/AudioTypes.hpp @@ -48,9 +48,11 @@ namespace lms::audio enum class CodecType { AAC, + AC3, ALAC, // Apple Lossless Audio Codec (ALAC) APE, // Monkey's Audio DSD, // DSD + EAC3, FLAC, // Flac MP3, MP4ALS, // MPEG-4 Audio Lossless Coding diff --git a/src/libs/audio/include/audio/Exception.hpp b/src/libs/audio/include/audio/Exception.hpp index 13f13437..6c3c9e52 100644 --- a/src/libs/audio/include/audio/Exception.hpp +++ b/src/libs/audio/include/audio/Exception.hpp @@ -19,6 +19,8 @@ #pragma once +#include + #include "core/Exception.hpp" namespace lms::audio @@ -28,4 +30,20 @@ namespace lms::audio public: using LmsException::LmsException; }; + + class IOFileException : public Exception + { + public: + IOFileException(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; + }; + } // namespace lms::audio diff --git a/src/libs/audio/include/audio/IAudioFileInfo.hpp b/src/libs/audio/include/audio/IAudioFileInfo.hpp index b417e78b..68639161 100644 --- a/src/libs/audio/include/audio/IAudioFileInfo.hpp +++ b/src/libs/audio/include/audio/IAudioFileInfo.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * @@ -19,11 +19,6 @@ #pragma once -#include -#include - -#include "audio/Exception.hpp" - namespace lms::audio { class AudioProperties; @@ -35,52 +30,8 @@ namespace lms::audio public: virtual ~IAudioFileInfo() = default; - virtual const AudioProperties& getAudioProperties() const = 0; - virtual const IImageReader& getImageReader() const = 0; - virtual const ITagReader& getTagReader() const = 0; + virtual const AudioProperties* getAudioProperties() const = 0; // may be null even if requested, due to backend limitation + virtual const IImageReader* getImageReader() const = 0; + virtual const ITagReader* getTagReader() const = 0; }; - - class AudioFileParsingException : public Exception - { - public: - using Exception::Exception; - }; - - class IOException : public Exception - { - public: - IOException(std::string_view message, std::error_code err) - : Exception{ std::string{ message } + ": " + err.message() } - , _err{ err } - { - } - - std::error_code getErrorCode() const { return _err; } - - private: - std::error_code _err; - }; - - struct ParserOptions - { - enum class Parser - { - TagLib, - FFmpeg, - }; - - enum class AudioPropertiesReadStyle - { - Fast, - Average, - Accurate, - }; - - Parser parser{ Parser::TagLib }; - AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average }; - bool enableExtraDebugLogs{}; - }; - std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{}); - - std::span getSupportedExtensions(ParserOptions::ParserOptions::Parser parser); } // namespace lms::audio diff --git a/src/libs/audio/include/audio/IAudioFileInfoParser.hpp b/src/libs/audio/include/audio/IAudioFileInfoParser.hpp new file mode 100644 index 00000000..45350209 --- /dev/null +++ b/src/libs/audio/include/audio/IAudioFileInfoParser.hpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +namespace lms::audio +{ + class IAudioFileInfo; + + struct AudioFileInfoParseOptions + { + enum class AudioPropertiesReadStyle + { + Fast, // fastest + Average, // normal + Accurate, // full scan + }; + + AudioPropertiesReadStyle audioPropertiesReadStyle{ AudioPropertiesReadStyle::Average }; + bool readTags{ true }; + bool readImages{ true }; + + bool enableExtraDebugLogs{}; + }; + + class IAudioFileInfoParser + { + public: + virtual ~IAudioFileInfoParser() = default; + + virtual std::unique_ptr parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const = 0; + virtual std::span getSupportedExtensions() const = 0; + }; + + enum class AudioFileInfoParserBackend + { + TagLib, + FFmpeg, + }; + static inline constexpr AudioFileInfoParserBackend defaultAudioFileInfoParserBackend{ AudioFileInfoParserBackend::TagLib }; + std::unique_ptr createAudioFileInfoParser(AudioFileInfoParserBackend backend = defaultAudioFileInfoParserBackend); +} // namespace lms::audio diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 7395ed20..8dda3d27 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -24,7 +24,9 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" +#include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "audio/IImageReader.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" @@ -53,6 +55,7 @@ namespace lms::artwork const std::filesystem::path& defaultArtistImageSvgPath) : _db{ db } , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } + , _audioFileInfoParser{ audio::createAudioFileInfoParser() } { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); @@ -108,11 +111,14 @@ namespace lms::artwork { std::size_t currentIndex{}; - audio::ParserOptions options; - options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images + audio::AudioFileInfoParseOptions options; + options.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only for images + options.readTags = false; + options.readImages = true; - auto audioFile{ audio::parseAudioFile(p) }; - audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) { + const auto audioFileInfo{ _audioFileInfoParser->parse(p, options) }; + assert(audioFileInfo->getImageReader()); + audioFileInfo->getImageReader()->visitImages([&](const audio::Image& parsedImage) { if (currentIndex++ != index) return; diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index 53bd1455..e698cc35 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -28,6 +28,11 @@ #include "ImageCache.hpp" +namespace lms::audio +{ + class IAudioFileInfoParser; +} + namespace lms::db { class Session; @@ -65,6 +70,7 @@ namespace lms::artwork ImageCache _cache; std::shared_ptr _defaultReleaseCover; std::shared_ptr _defaultArtistImage; + std::unique_ptr _audioFileInfoParser; static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize unsigned _jpegQuality; diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 055371cc..69ef7c3e 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(lmsscanner STATIC impl/helpers/ArtistHelpers.cpp impl/scanners/artistinfo/ArtistInfoParser.cpp impl/scanners/artistinfo/ArtistInfoFileScanner.cpp + impl/scanners/audiofile/AudioFileInfoParserSet.cpp impl/scanners/audiofile/AudioFileScanOperation.cpp impl/scanners/audiofile/AudioFileScanner.cpp impl/scanners/audiofile/TrackMetadataParser.cpp diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.cpp new file mode 100644 index 00000000..738a04dc --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "audio/IAudioFileInfoParser.hpp" +#include "core/Exception.hpp" +#include "core/IConfig.hpp" +#include "core/Service.hpp" + +#include "scanners/audiofile/AudioFileInfoParserSet.hpp" + +namespace lms::scanner +{ + namespace + { + audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle getParserReadStyle() + { + std::string_view readStyle{ core::Service::get()->getString("scanner-parser-read-style", "average") }; + + if (readStyle == "fast") + return audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; + if (readStyle == "average") + return audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; + if (readStyle == "accurate") + return audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Accurate; + + throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; + } + } // namespace + + AudioFileInfoParserSet createAudioFileInfoParserSet() + { + AudioFileInfoParserSet parserSet; + + parserSet.taglibParser = audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::TagLib); + parserSet.ffmpegParser = audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg); + + const auto extensions{ parserSet.taglibParser->getSupportedExtensions() }; + parserSet.supportedExtensions.assign(std::cbegin(extensions), std::cend(extensions)); + + parserSet.audioPropertiesReadStyle = getParserReadStyle(); + + return parserSet; + } +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.hpp new file mode 100644 index 00000000..f097d548 --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileInfoParserSet.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "audio/IAudioFileInfoParser.hpp" + +namespace lms::scanner +{ + struct AudioFileInfoParserSet + { + std::unique_ptr taglibParser; + std::unique_ptr ffmpegParser; + std::vector supportedExtensions; + + audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle audioPropertiesReadStyle; // a bit hacky to have this here + }; + + AudioFileInfoParserSet createAudioFileInfoParserSet(); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 814b9242..502d14ab 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -25,7 +25,9 @@ #include "core/Path.hpp" #include "core/XxHash3.hpp" +#include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "image/Exception.hpp" #include "image/Image.hpp" @@ -52,6 +54,7 @@ #include "helpers/ArtistHelpers.hpp" #include "scanners/IFileScanOperation.hpp" #include "scanners/Utils.hpp" +#include "scanners/audiofile/AudioFileInfoParserSet.hpp" #include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms::scanner @@ -488,10 +491,10 @@ namespace lms::scanner } } // namespace - AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions) + AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const AudioFileInfoParserSet& audioFileInfoParserSet, const TrackMetadataParser& metadataParser) : FileScanOperationBase{ std::move(fileToScan), db, settings } + , _audioFileInfoParserSet{ audioFileInfoParserSet } , _metadataParser{ metadataParser } - , _parserOptions{ parserOptions } { } @@ -501,18 +504,42 @@ namespace lms::scanner { try { - auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) }; + audio::AudioFileInfoParseOptions options; + options.audioPropertiesReadStyle = _audioFileInfoParserSet.audioPropertiesReadStyle; + options.readImages = true; + options.readTags = true; + + const auto audioFileInfo{ _audioFileInfoParserSet.taglibParser->parse(getFilePath(), options) }; _file.emplace(); - _file->audioProperties = audioFileInfo->getAudioProperties(); - _file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader()); + // Fallback on ffmpeg in case no audio properties are found by taglib + if (!audioFileInfo->getAudioProperties()) + { + LMS_LOG(DBUPDATER, DEBUG, "Cannot parse audio properties in " << getFilePath() << " using TagLib, switching to ffmpeg"); + + options.readTags = false; + options.readImages = false; + const auto ffmpegAudioFileInfo{ _audioFileInfoParserSet.ffmpegParser->parse(getFilePath(), options) }; + if (!ffmpegAudioFileInfo->getAudioProperties()) + { + addError(getFilePath()); + return; + } + _file->audioProperties = *ffmpegAudioFileInfo->getAudioProperties(); + } + else + { + _file->audioProperties = *audioFileInfo->getAudioProperties(); + } + + _file->track = _metadataParser.parseTrackMetaData(*audioFileInfo->getTagReader()); // We fill missing artist mbids with mbids found on other artist roles fillMissingMbids(_file->track); std::size_t index{}; - audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) { + audioFileInfo->getImageReader()->visitImages([&](const audio::Image& image) { try { image::ImageProperties properties{ image::probeImage(image.data) }; @@ -539,7 +566,7 @@ namespace lms::scanner index++; }); } - catch (const audio::IOException& e) + catch (const audio::IOFileException& e) { addError(getFilePath(), e.getErrorCode()); } diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp index 7d5e00ff..0cb62b57 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp @@ -22,7 +22,6 @@ #include #include "audio/AudioTypes.hpp" -#include "audio/IAudioFileInfo.hpp" #include "audio/IImageReader.hpp" #include "image/Types.hpp" @@ -39,6 +38,7 @@ namespace lms::db namespace lms::scanner { + class AudioFileInfoParserSet; class TrackMetadataParser; struct ImageInfo @@ -55,7 +55,7 @@ namespace lms::scanner class AudioFileScanOperation : public FileScanOperationBase { public: - AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions); + AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const AudioFileInfoParserSet& audioFileInfoParserSet, const TrackMetadataParser& metadataParser); ~AudioFileScanOperation() override; AudioFileScanOperation(const AudioFileScanOperation&) = delete; AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete; @@ -65,8 +65,8 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; + const AudioFileInfoParserSet& _audioFileInfoParserSet; const TrackMetadataParser& _metadataParser; - const audio::ParserOptions& _parserOptions; struct AudioFileInfo { diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp index f38d65af..4d796301 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp @@ -19,11 +19,6 @@ #include "AudioFileScanner.hpp" -#include "core/IConfig.hpp" -#include "core/Service.hpp" - -#include "audio/IAudioFileInfo.hpp" - #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" @@ -31,6 +26,7 @@ #include "ScannerSettings.hpp" #include "scanners/Utils.hpp" +#include "scanners/audiofile/AudioFileInfoParserSet.hpp" #include "scanners/audiofile/AudioFileScanOperation.hpp" #include "scanners/audiofile/TrackMetadataParser.hpp" @@ -38,20 +34,6 @@ namespace lms::scanner { namespace { - audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle() - { - std::string_view readStyle{ core::Service::get()->getString("scanner-parser-read-style", "average") }; - - if (readStyle == "fast") - return audio::ParserOptions::AudioPropertiesReadStyle::Fast; - if (readStyle == "average") - return audio::ParserOptions::AudioPropertiesReadStyle::Average; - if (readStyle == "accurate") - return audio::ParserOptions::AudioPropertiesReadStyle::Accurate; - - throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; - } - TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings) { TrackMetadataParser::Parameters params; @@ -62,23 +44,13 @@ namespace lms::scanner return params; } - - audio::ParserOptions createAudioFileParserOptions() - { - audio::ParserOptions options; - options.readStyle = getParserReadStyle(); - options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib - - return options; - } - } // namespace AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings) : _db{ db } , _settings{ settings } , _trackMetadataParser{ createTrackMetadataParserParameters(settings) } - , _parserOptions{ createAudioFileParserOptions() } + , _audioFileInfoParserSet{ createAudioFileInfoParserSet() } { } @@ -96,7 +68,7 @@ namespace lms::scanner std::span AudioFileScanner::getSupportedExtensions() const { - return audio::getSupportedExtensions(_parserOptions.parser); + return _audioFileInfoParserSet.supportedExtensions; } bool AudioFileScanner::needsScan(const FileToScan& file) const @@ -112,6 +84,6 @@ namespace lms::scanner std::unique_ptr AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions); + return std::make_unique(std::move(fileToScan), _db, _settings, _audioFileInfoParserSet, _trackMetadataParser); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp index 5a8b5c32..a7e3527b 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp @@ -19,23 +19,14 @@ #pragma once -#include "audio/IAudioFileInfo.hpp" - #include "scanners/IFileScanner.hpp" +#include "scanners/audiofile/AudioFileInfoParserSet.hpp" #include "scanners/audiofile/TrackMetadataParser.hpp" -namespace lms +namespace lms::db { - namespace db - { - class IDb; - } - - namespace metadata - { - class IAudioFileParser; - } -} // namespace lms + class IDb; +} namespace lms::scanner { @@ -59,6 +50,6 @@ namespace lms::scanner db::IDb& _db; const ScannerSettings& _settings; const TrackMetadataParser _trackMetadataParser; - const audio::ParserOptions _parserOptions; + const AudioFileInfoParserSet _audioFileInfoParserSet; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 8254c229..13c84402 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -30,6 +30,7 @@ #include "audio/AudioTypes.hpp" #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/objects/PodcastEpisode.hpp" @@ -118,13 +119,21 @@ namespace lms::api::subsonic // TODO: put this information in db during scan try { - const auto audioFile{ audio::parseAudioFile(trackPath) }; + const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; - return isCodecCompatibleWithOutputFormat(audioFile->getAudioProperties().codec, outputFormat); + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed + parseOptions.readImages = false; + parseOptions.readTags = false; + const auto audioFile{ parser->parse(trackPath, parseOptions) }; + + if (!audioFile->getAudioProperties()) + throw RequestedDataNotFoundError{}; + + return isCodecCompatibleWithOutputFormat(audioFile->getAudioProperties()->codec, outputFormat); } catch (const audio::Exception& e) { - // TODO 404? throw RequestedDataNotFoundError{}; } } diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 63c451d6..cf001f96 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -30,7 +30,9 @@ #include "core/String.hpp" #include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -145,10 +147,18 @@ namespace lms::ui { try { - if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) + const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; + + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed + parseOptions.readImages = false; + parseOptions.readTags = false; + const auto audioFile{ parser->parse(track->getAbsoluteFilePath(), parseOptions) }; + + if (audioFile->getAudioProperties()) { releaseInfo->setCondition("if-has-codec", true); - releaseInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); + releaseInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties()->codec).c_str(), Wt::TextFormat::Plain); break; } } diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 8721745b..3591fe46 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -26,7 +26,9 @@ #include "core/Service.hpp" #include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -131,10 +133,18 @@ namespace lms::ui::TrackListHelpers try { - if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) + const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; + + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed + parseOptions.readImages = false; + parseOptions.readTags = false; + const auto audioFile{ parser->parse(track->getAbsoluteFilePath(), parseOptions) }; + + if (audioFile->getAudioProperties()) { trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); + trackInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties()->codec).c_str(), Wt::TextFormat::Plain); } } catch (const audio::Exception& e) diff --git a/src/tools/audioinfo/LmsAudioInfo.cpp b/src/tools/audioinfo/LmsAudioInfo.cpp index 2a05ad15..d7df8362 100644 --- a/src/tools/audioinfo/LmsAudioInfo.cpp +++ b/src/tools/audioinfo/LmsAudioInfo.cpp @@ -31,6 +31,7 @@ #include "audio/AudioTypes.hpp" #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "audio/IImageReader.hpp" #include "audio/ITagReader.hpp" @@ -133,11 +134,21 @@ namespace lms::audio void displayInfo(const IAudioFileInfo& fileInfo) { - std::cout << "Audio properties:\n" - << fileInfo.getAudioProperties() << std::endl; + if (fileInfo.getAudioProperties()) + { + std::cout << "Audio properties:\n" + << *fileInfo.getAudioProperties() << "\n"; + } + else + { + std::cout << "Failed to parse audio properties!\n\n"; + } - displayImages(fileInfo.getImageReader()); - displayTags(fileInfo.getTagReader()); + if (fileInfo.getImageReader()) + displayImages(*fileInfo.getImageReader()); + + if (fileInfo.getTagReader()) + displayTags(*fileInfo.getTagReader()); std::cout << "\n"; } @@ -200,15 +211,16 @@ int main(int argc, char* argv[]) return EXIT_FAILURE; } - audio::ParserOptions parserOptions; - parserOptions.enableExtraDebugLogs = true; + audio::AudioFileInfoParserBackend parserBackend{ audio::defaultAudioFileInfoParserBackend }; if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "taglib")) - parserOptions.parser = audio::ParserOptions::Parser::TagLib; + parserBackend = audio::AudioFileInfoParserBackend::TagLib; else if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "ffmpeg")) - parserOptions.parser = audio::ParserOptions::Parser::FFmpeg; + parserBackend = audio::AudioFileInfoParserBackend::FFmpeg; else throw program_options::validation_error{ program_options::validation_error::invalid_option_value, "parser" }; + const auto parser{ audio::createAudioFileInfoParser(parserBackend) }; + const auto& inputFiles{ vm["file"].as>() }; // log to stdout core::Service logger{ core::logging::createLogger(core::logging::Severity::DEBUG) }; @@ -219,8 +231,11 @@ int main(int argc, char* argv[]) try { + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.enableExtraDebugLogs = true; + std::cout << "Parsing file " << file << ":\n"; - const auto audioFileInfo{ audio::parseAudioFile(file, parserOptions) }; + const auto audioFileInfo{ parser->parse(file, parseOptions) }; displayInfo(*audioFileInfo); } From 41f59c205441de95662b4f7fc5ead8cce32194e8 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 21 Nov 2025 21:08:57 +0100 Subject: [PATCH 02/22] Added more unit tests to check behavior when artist and mbid counts are not equal, ref #782 --- .../scanner/test/TrackMetadataParser.cpp | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/libs/services/scanner/test/TrackMetadataParser.cpp b/src/libs/services/scanner/test/TrackMetadataParser.cpp index ddfb87ab..6f94bf4b 100644 --- a/src/libs/services/scanner/test/TrackMetadataParser.cpp +++ b/src/libs/services/scanner/test/TrackMetadataParser.cpp @@ -720,6 +720,57 @@ namespace lms::scanner::tests EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } + TEST(TrackMetadataParser, heterogeneousArtistMbids) + { + { + const TestTagReader testTags{ + { + { audio::TagType::Artists, { "MyArtist" } }, + { audio::TagType::MusicBrainzArtistID, { "970f720b-c955-4ae7-870e-3a3716577cbf", "58eea0c6-1327-4354-8438-13e581894f35" } }, + + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtists, { "MyArtist" } }, + { audio::TagType::MusicBrainzReleaseArtistID, { "970f720b-c955-4ae7-870e-3a3716577cbf", "58eea0c6-1327-4354-8438-13e581894f35" } }, + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "MyArtist"); + EXPECT_EQ(track.artists[0].mbid, std::nullopt); + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + ASSERT_EQ(track.medium->release->artists.size(), 1); + EXPECT_EQ(track.medium->release->artists[0].name, "MyArtist"); + EXPECT_EQ(track.medium->release->artists[0].mbid, std::nullopt); + } + + { + const TestTagReader testTags{ + { + { audio::TagType::Artist, { "MyArtist" } }, + { audio::TagType::MusicBrainzArtistID, { "970f720b-c955-4ae7-870e-3a3716577cbf", "58eea0c6-1327-4354-8438-13e581894f35" } }, + + { audio::TagType::Album, { "MyAlbum" } }, + { audio::TagType::AlbumArtist, { "MyArtist" } }, + { audio::TagType::MusicBrainzReleaseArtistID, { "970f720b-c955-4ae7-870e-3a3716577cbf", "58eea0c6-1327-4354-8438-13e581894f35" } }, + } + }; + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.artists.size(), 1); + EXPECT_EQ(track.artists[0].name, "MyArtist"); + EXPECT_EQ(track.artists[0].mbid, std::nullopt); + + ASSERT_TRUE(track.medium.has_value()); + ASSERT_TRUE(track.medium->release.has_value()); + ASSERT_EQ(track.medium->release->artists.size(), 1); + EXPECT_EQ(track.medium->release->artists[0].name, "MyArtist"); + EXPECT_EQ(track.medium->release->artists[0].mbid, std::nullopt); + } + } + TEST(TrackMetadataParser, release_sortNameFallback) { const TestTagReader testTags{ From 32177e058355f9ad504ffb2136f1d9a79c9cbd6e Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 26 Nov 2025 21:31:21 +0100 Subject: [PATCH 03/22] FFmpeg parser: better fallback for streams that do not expose bitrate --- src/libs/audio/impl/ffmpeg/AudioFile.cpp | 8 +++-- src/libs/audio/impl/ffmpeg/AudioFile.hpp | 2 +- src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp | 37 ++++++++++++++++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp index 7f679b99..9e5eed5c 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -32,6 +32,7 @@ extern "C" } #include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" #include "core/String.hpp" #include "audio/AudioTypes.hpp" @@ -189,7 +190,7 @@ namespace lms::audio::ffmpeg std::array buffer{ 0 }; std::vsnprintf(buffer.data(), buffer.size(), fmt, vl); - LMS_LOG(AUDIO, DEBUG, "ffmpeg [" << avLogLevelToStr(level) << "] " << buffer.data()); + LMS_LOG(AUDIO, DEBUG, "FFmpeg [" << avLogLevelToStr(level) << "] " << buffer.data()); } class AvInitializer @@ -205,6 +206,8 @@ namespace lms::audio::ffmpeg 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) }; @@ -240,7 +243,8 @@ 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; diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp index 64c4303d..fde8a031 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -47,7 +47,7 @@ namespace lms::audio::ffmpeg std::optional container; std::string containerName; - std::size_t bitrate{}; + std::optional bitrate; std::chrono::milliseconds duration{}; }; diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp index f1ebbcc6..62fe54e4 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp @@ -19,6 +19,9 @@ #include "AudioFileInfo.hpp" +#include +#include + #include "core/ILogger.hpp" #include "audio/AudioTypes.hpp" @@ -44,6 +47,12 @@ namespace lms::audio::ffmpeg return audioProperties; } + if (containerInfo.duration.count() == 0) + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << audioFile.getPath()); + return audioProperties; + } + if (!containerInfo.container) { LMS_LOG(AUDIO, DEBUG, "Unhandled container '" << containerInfo.containerName << "' in " << audioFile.getPath()); @@ -56,12 +65,6 @@ namespace lms::audio::ffmpeg return audioProperties; } - if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0) - { - LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << audioFile.getPath()); - return audioProperties; - } - if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0) { LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << audioFile.getPath()); @@ -79,7 +82,27 @@ namespace lms::audio::ffmpeg audioProperties->container = *containerInfo.container; audioProperties->duration = containerInfo.duration; audioProperties->codec = *bestStreamInfo->codec; - audioProperties->bitrate = *bestStreamInfo->bitrate; + + if (bestStreamInfo->bitrate) + audioProperties->bitrate = *bestStreamInfo->bitrate; + else if (containerInfo.bitrate) + audioProperties->bitrate = *containerInfo.bitrate; + else // Fallback on a bitrate based on duration/size + { + std::error_code ec; + const auto fileSize{ std::filesystem::file_size(audioFile.getPath(), ec) }; + if (ec) + { + LMS_LOG(AUDIO, DEBUG, "Cannot determine file size for " << audioFile.getPath() << ": " << ec.message()); + audioProperties.reset(); + return audioProperties; + } + + audioProperties->bitrate = static_cast(std::chrono::duration_cast>(containerInfo.duration).count() / fileSize); + LMS_LOG(AUDIO, DEBUG, "Estimated bitrate from duration/size: " << audioProperties->bitrate << " bps in " << audioFile.getPath()); + } + assert(audioProperties->bitrate > 0); + audioProperties->channelCount = *bestStreamInfo->channelCount; audioProperties->sampleRate = *bestStreamInfo->sampleRate; audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample; From 37d603d72c98db2cf55bef06082397b9a7b47be8 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 26 Nov 2025 21:48:57 +0100 Subject: [PATCH 04/22] Store container and codec info in database --- src/libs/database/CMakeLists.txt | 2 +- src/libs/database/impl/Migration.cpp | 91 ++++- .../database/impl/{ => objects}/Types.cpp | 6 +- src/libs/database/include/database/Types.hpp | 243 -------------- .../include/database/objects/Artist.hpp | 1 + .../include/database/objects/Cluster.hpp | 1 + .../include/database/objects/Directory.hpp | 2 + .../include/database/objects/Listen.hpp | 1 + .../include/database/objects/Medium.hpp | 1 + .../database/objects/PodcastEpisode.hpp | 1 + .../include/database/objects/RatedArtist.hpp | 1 + .../include/database/objects/RatedRelease.hpp | 1 + .../include/database/objects/RatedTrack.hpp | 1 + .../include/database/objects/Release.hpp | 2 + .../database/objects/StarredArtist.hpp | 1 + .../database/objects/StarredRelease.hpp | 1 + .../include/database/objects/StarredTrack.hpp | 1 + .../include/database/objects/Track.hpp | 111 +++--- .../database/objects/TrackArtistLink.hpp | 2 + .../database/objects/TrackEmbeddedImage.hpp | 1 + .../objects/TrackEmbeddedImageLink.hpp | 2 +- .../include/database/objects/TrackList.hpp | 1 + .../include/database/objects/TrackLyrics.hpp | 1 + .../include/database/objects/Types.hpp | 315 ++++++++++++++++++ .../include/database/objects/User.hpp | 2 + .../auth/include/services/auth/Types.hpp | 3 +- .../services/feedback/IFeedbackService.hpp | 1 + .../services/recommendation/impl/IEngine.hpp | 3 +- .../recommendation/IRecommendationService.hpp | 3 +- .../audiofile/AudioFileScanOperation.cpp | 98 +++++- .../impl/listenbrainz/ListensSynchronizer.hpp | 2 +- .../scrobbling/IScrobblingService.hpp | 1 + src/libs/subsonic/impl/responses/Artist.hpp | 2 +- src/libs/subsonic/impl/responses/Song.cpp | 2 +- src/lms/ui/LmsApplication.hpp | 2 +- src/lms/ui/MediaPlayer.hpp | 2 +- src/lms/ui/explore/ArtistCollector.hpp | 5 +- src/lms/ui/explore/ArtistListHelpers.hpp | 5 +- src/lms/ui/explore/TrackListHelpers.hpp | 3 +- 39 files changed, 618 insertions(+), 306 deletions(-) rename src/libs/database/impl/{ => objects}/Types.cpp (94%) create mode 100644 src/libs/database/include/database/objects/Types.hpp diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 4e79b43a..c896269e 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -29,6 +29,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 +40,6 @@ add_library(lmsdatabase STATIC impl/Session.cpp impl/SqlQuery.cpp impl/Transaction.cpp - impl/Types.cpp impl/Utils.cpp ) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index cda0492a..f8eca7ac 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -34,7 +34,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 100 }; + static constexpr Version LMS_DATABASE_VERSION{ 101 }; } VersionInfo::VersionInfo() @@ -1581,6 +1581,94 @@ 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"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1657,6 +1745,7 @@ FROM track)"); { 97, migrateFromV97 }, { 98, migrateFromV98 }, { 99, migrateFromV99 }, + { 100, migrateFromV100 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/objects/Types.cpp similarity index 94% rename from src/libs/database/impl/Types.cpp rename to src/libs/database/impl/objects/Types.cpp index f07a30fd..df159c91 100644 --- a/src/libs/database/impl/Types.cpp +++ b/src/libs/database/impl/objects/Types.cpp @@ -17,9 +17,9 @@ * along with LMS. If not, see . */ -#include "database/Types.hpp" +#include "database/objects/Types.hpp" -#include +#include namespace lms::db { @@ -54,7 +54,7 @@ namespace lms::db return "unknown"; } - static const std::set allowedAudioBitrates{ + static const std::unordered_set allowedAudioBitrates{ 64000, 96000, 128000, diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 37bfcfa4..46c1661d 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -21,14 +21,10 @@ #include #include -#include -#include #include #include "core/Exception.hpp" -#include "core/LiteralString.hpp" -#include "core/TaggedType.hpp" namespace lms::db { @@ -113,243 +109,4 @@ namespace lms::db std::size_t getTotalFileCount() const { return trackCount + imageCount + trackLyricsCount + playListCount + artistInfoCount; } }; - - struct YearRange - { - int begin{}; - int end{}; - }; - - struct FileInfo - { - Wt::WDateTime lastWrittenTime; - std::size_t scanVersion{}; - }; - - enum class ArtistSortMethod - { - None, - Id, - Name, - SortName, - Random, - LastWrittenDesc, - AddedDesc, - StarredDateDesc, - }; - - enum class ClusterSortMethod - { - None, - Name, - }; - - enum class DirectorySortMethod - { - None, - Name, - }; - - using ImageHashType = core::TaggedType; - - enum class LabelSortMethod - { - None, - Name, - }; - - enum class MediumSortMethod - { - None, - PositionAsc, - }; - - enum class PodcastEpisodeSortMode - { - None, - PubDateAsc, - PubDateDesc, - }; - - enum class ReleaseSortMethod - { - None, - Id, - Name, - SortName, - ArtistNameThenName, - DateAsc, - DateDesc, - OriginalDate, - OriginalDateDesc, - Random, - LastWrittenDesc, - AddedDesc, - StarredDateDesc, - }; - - enum class ReleaseTypeSortMethod - { - None, - Name, - }; - - enum class TrackArtistLinkSortMethod - { - None, - OriginalDateDesc, - }; - - enum class TrackEmbeddedImageSortMethod - { - None, - SizeDesc, - TrackNumberThenSizeDesc, - DiscNumberThenTrackNumberThenSizeDesc, - TrackListIndexAscThenSizeDesc, - }; - - enum class TrackListSortMethod - { - None, - Name, - LastModifiedDesc, - }; - - enum class TrackSortMethod - { - None, - Id, - Random, - LastWrittenDesc, - AddedDesc, - StarredDateDesc, - AbsoluteFilePath, - Name, - DateDescAndRelease, - OriginalDateDescAndRelease, - Release, // order by disc/track number - TrackList, // order by asc order in tracklist - TrackNumber, - }; - - enum class TrackLyricsSortMethod - { - None, - ExternalFirst, - EmbeddedFirst, - }; - - enum class ImageType - { - Unknown = 0, - Other = 1, - FileIcon = 2, - OtherFileIcon = 3, - FrontCover = 4, - BackCover = 5, - LeafletPage = 6, - Media = 7, - LeadArtist = 8, - Artist = 9, - Conductor = 10, - Band = 11, - Composer = 12, - Lyricist = 13, - RecordingLocation = 14, - DuringRecording = 15, - DuringPerformance = 16, - MovieScreenCapture = 17, - ColouredFish = 18, - Illustration = 19, - BandLogo = 20, - PublisherLogo = 21 - }; - - enum class TrackArtistLinkType - { - Artist = 0, // regular track artist - Arranger = 1, - Composer = 2, - Conductor = 3, - Lyricist = 4, - Mixer = 5, - Performer = 6, - Producer = 7, - ReleaseArtist = 8, - Remixer = 9, - Writer = 10, - }; - - core::LiteralString trackArtistLinkTypeToString(TrackArtistLinkType type); - - // User selectable transcoding output formats - enum class TranscodingOutputFormat - { - MP3 = 1, - OGG_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, - MATROSKA_OPUS = 5, - }; - - using Bitrate = std::uint32_t; - // Do not remove values! - void visitAllowedAudioBitrates(std::function); - bool isAudioBitrateAllowed(Bitrate bitrate); - - using Rating = int; - - enum class ScrobblingBackend - { - Internal = 0, - ListenBrainz = 1, - }; - - enum class FeedbackBackend - { - Internal = 0, - ListenBrainz = 1, - }; - - enum class SyncState - { - PendingAdd = 0, - Synchronized = 1, - PendingRemove = 2, - }; - - enum class UserType - { - REGULAR = 0, - ADMIN = 1, - DEMO = 2, - }; - - enum class UITheme - { - Light = 0, - Dark = 1, - }; - - enum class SubsonicArtistListMode - { - AllArtists = 0, - ReleaseArtists = 1, - TrackArtists = 2, - }; - - enum class TrackListType - { - PlayList = 0, // user controlled playlists - Internal = 1, // internal usage (current playqueue, history, ...) - }; - - enum class Advisory - { - UnSet = 0, - Unknown = 1, - Clean = 2, - Explicit = 3, - }; } // namespace lms::db diff --git a/src/libs/database/include/database/objects/Artist.hpp b/src/libs/database/include/database/objects/Artist.hpp index 7cae8afd..21d9e96e 100644 --- a/src/libs/database/include/database/objects/Artist.hpp +++ b/src/libs/database/include/database/objects/Artist.hpp @@ -41,6 +41,7 @@ #include "database/objects/MediaLibraryId.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" namespace lms::db diff --git a/src/libs/database/include/database/objects/Cluster.hpp b/src/libs/database/include/database/objects/Cluster.hpp index f30e343d..01a60648 100644 --- a/src/libs/database/include/database/objects/Cluster.hpp +++ b/src/libs/database/include/database/objects/Cluster.hpp @@ -33,6 +33,7 @@ #include "database/objects/ClusterId.hpp" #include "database/objects/Release.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/Directory.hpp b/src/libs/database/include/database/objects/Directory.hpp index 3174f235..dd8ac918 100644 --- a/src/libs/database/include/database/objects/Directory.hpp +++ b/src/libs/database/include/database/objects/Directory.hpp @@ -29,6 +29,7 @@ #include #include "core/EnumSet.hpp" + #include "database/Object.hpp" #include "database/Types.hpp" #include "database/objects/ArtistId.hpp" @@ -36,6 +37,7 @@ #include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediumId.hpp" #include "database/objects/ReleaseId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/Listen.hpp b/src/libs/database/include/database/objects/Listen.hpp index b6882fa2..13d6f77a 100644 --- a/src/libs/database/include/database/objects/Listen.hpp +++ b/src/libs/database/include/database/objects/Listen.hpp @@ -31,6 +31,7 @@ #include "database/objects/ListenId.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" namespace lms::db diff --git a/src/libs/database/include/database/objects/Medium.hpp b/src/libs/database/include/database/objects/Medium.hpp index 97e8a64e..55c3dff5 100644 --- a/src/libs/database/include/database/objects/Medium.hpp +++ b/src/libs/database/include/database/objects/Medium.hpp @@ -33,6 +33,7 @@ #include "database/objects/ArtworkId.hpp" #include "database/objects/MediumId.hpp" #include "database/objects/ReleaseId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/PodcastEpisode.hpp b/src/libs/database/include/database/objects/PodcastEpisode.hpp index 40389793..8db9c541 100644 --- a/src/libs/database/include/database/objects/PodcastEpisode.hpp +++ b/src/libs/database/include/database/objects/PodcastEpisode.hpp @@ -34,6 +34,7 @@ #include "database/objects/ArtworkId.hpp" #include "database/objects/PodcastEpisodeId.hpp" #include "database/objects/PodcastId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/RatedArtist.hpp b/src/libs/database/include/database/objects/RatedArtist.hpp index 0a336d08..f007c493 100644 --- a/src/libs/database/include/database/objects/RatedArtist.hpp +++ b/src/libs/database/include/database/objects/RatedArtist.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/RatedRelease.hpp b/src/libs/database/include/database/objects/RatedRelease.hpp index 4096f380..3b0cf162 100644 --- a/src/libs/database/include/database/objects/RatedRelease.hpp +++ b/src/libs/database/include/database/objects/RatedRelease.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/RatedTrack.hpp b/src/libs/database/include/database/objects/RatedTrack.hpp index de944e5b..878b935f 100644 --- a/src/libs/database/include/database/objects/RatedTrack.hpp +++ b/src/libs/database/include/database/objects/RatedTrack.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index 7fe7c4c2..18999963 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -31,6 +31,7 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" + #include "database/IdRange.hpp" #include "database/Object.hpp" #include "database/Types.hpp" @@ -43,6 +44,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 diff --git a/src/libs/database/include/database/objects/StarredArtist.hpp b/src/libs/database/include/database/objects/StarredArtist.hpp index 3bca8939..3b91942a 100644 --- a/src/libs/database/include/database/objects/StarredArtist.hpp +++ b/src/libs/database/include/database/objects/StarredArtist.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/StarredRelease.hpp b/src/libs/database/include/database/objects/StarredRelease.hpp index 6f172d38..3f81c8de 100644 --- a/src/libs/database/include/database/objects/StarredRelease.hpp +++ b/src/libs/database/include/database/objects/StarredRelease.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/StarredTrack.hpp b/src/libs/database/include/database/objects/StarredTrack.hpp index 352b1ba7..9a6b1c58 100644 --- a/src/libs/database/include/database/objects/StarredTrack.hpp +++ b/src/libs/database/include/database/objects/StarredTrack.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index 1e410d7d..88f7e95f 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -34,6 +34,7 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" + #include "database/IdRange.hpp" #include "database/Object.hpp" #include "database/Types.hpp" @@ -48,6 +49,7 @@ #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" namespace lms::db @@ -216,19 +218,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 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(ContainerType container) { _container = container; } + void setCodec(CodecType codec) { _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 bitsPerSample) { _bitsPerSample = bitsPerSample; } + void setReplayGain(std::optional replayGain) { _replayGain = replayGain; } + + // Metadata + void setTrackNumber(std::optional 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& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } @@ -236,7 +247,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 replayGain) { _replayGain = replayGain; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setComment(std::string_view comment) { _comment = comment; } void clearArtistLinks(); @@ -254,32 +264,41 @@ namespace lms::db void setPreferredArtwork(ObjectPtr artwork); void setPreferredMediaArtwork(ObjectPtr artwork); + // Getters std::size_t getScanVersion() const { return _scanVersion; } - std::optional 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; } + ContainerType getContainer() const { return _container; } + CodecType getCodec() const { return _codec; } + std::size_t getBitrate() const { return _bitrate; } + std::size_t getChannelCount() const { return _channelCount; } + std::size_t getSampleRate() const { return _sampleRate; } + std::optional getBitsPerSample() const { return _bitsPerSample; } + std::optional getReplayGain() const { return _replayGain; } + + // Metadata + std::optional getTrackNumber() const { return _trackNumber; } + std::string getName() const { return _name; } const core::PartialDateTime& getDate() const { return _date; } std::optional getYear() const; const core::PartialDateTime& getOriginalDate() const { return _originalDate; } std::optional getOriginalYear() const; const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } - const Wt::WDateTime& getAddedTime() const { return _fileAdded; } bool hasLyrics() const; std::optional getTrackMBID() const { return core::UUID::fromString(_trackMBID); } std::optional getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } std::optional getCopyright() const; std::optional getCopyrightURL() const; - Advisory getAdvisory() const { return _advisory; } - std::optional 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> getArtists(core::EnumSet artistLinkTypes) const; // no type means all @@ -304,27 +323,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, _advisory, "advisory"); Wt::Dbo::belongsTo(a, _medium, "medium", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); @@ -347,27 +371,36 @@ namespace lms::db static constexpr std::size_t _maxCopyrightURLLength{ 512 }; int _scanVersion{}; - std::optional _trackNumber; - std::string _name; - int _bitrate{}; // in bps - int _bitsPerSample{}; - int _channelCount{}; - std::chrono::duration _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 _duration{}; + ContainerType _container{ ContainerType::Unknown }; + CodecType _codec{ CodecType::Unknown }; + int _bitrate{}; // in bps + int _channelCount{}; + int _sampleRate{}; + std::optional _bitsPerSample; + std::optional _replayGain; + + // Metadata + std::optional _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 _replayGain; std::string _artistDisplayName; std::string _comment; + Advisory _advisory{ Advisory::UnSet }; + Wt::Dbo::ptr _medium; Wt::Dbo::ptr _release; Wt::Dbo::ptr _mediaLibrary; diff --git a/src/libs/database/include/database/objects/TrackArtistLink.hpp b/src/libs/database/include/database/objects/TrackArtistLink.hpp index f4e8406d..e10ec355 100644 --- a/src/libs/database/include/database/objects/TrackArtistLink.hpp +++ b/src/libs/database/include/database/objects/TrackArtistLink.hpp @@ -26,12 +26,14 @@ #include #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) diff --git a/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp index 48d472cd..4237ec84 100644 --- a/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp @@ -33,6 +33,7 @@ #include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackId.hpp" #include "database/objects/TrackListId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp b/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp index ea670888..8334616c 100644 --- a/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp +++ b/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp @@ -25,9 +25,9 @@ #include #include "database/Object.hpp" -#include "database/Types.hpp" #include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackEmbeddedImageLinkId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { diff --git a/src/libs/database/include/database/objects/TrackList.hpp b/src/libs/database/include/database/objects/TrackList.hpp index 37647203..81b712da 100644 --- a/src/libs/database/include/database/objects/TrackList.hpp +++ b/src/libs/database/include/database/objects/TrackList.hpp @@ -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 diff --git a/src/libs/database/include/database/objects/TrackLyrics.hpp b/src/libs/database/include/database/objects/TrackLyrics.hpp index dd7c5c9c..ee7e35bf 100644 --- a/src/libs/database/include/database/objects/TrackLyrics.hpp +++ b/src/libs/database/include/database/objects/TrackLyrics.hpp @@ -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) diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp new file mode 100644 index 00000000..966083be --- /dev/null +++ b/src/libs/database/include/database/objects/Types.hpp @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "core/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; + + 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; + void visitAllowedAudioBitrates(std::function); + bool isAudioBitrateAllowed(Bitrate bitrate); + + using Rating = int; + + enum class ScrobblingBackend + { + Internal = 0, + ListenBrainz = 1, + }; + + enum class FeedbackBackend + { + Internal = 0, + ListenBrainz = 1, + }; + + enum class SyncState + { + PendingAdd = 0, + Synchronized = 1, + PendingRemove = 2, + }; + + enum class UserType + { + REGULAR = 0, + ADMIN = 1, + DEMO = 2, + }; + + enum class UITheme + { + Light = 0, + Dark = 1, + }; + + enum class SubsonicArtistListMode + { + AllArtists = 0, + ReleaseArtists = 1, + TrackArtists = 2, + }; + + enum class TrackListType + { + PlayList = 0, // user controlled playlists + Internal = 1, // internal usage (current playqueue, history, ...) + }; + + enum class Advisory + { + UnSet = 0, + Unknown = 1, + Clean = 2, + Explicit = 3, + }; + + enum class ContainerType + { + 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 CodecType + { + 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 diff --git a/src/libs/database/include/database/objects/User.hpp b/src/libs/database/include/database/objects/User.hpp index ba731886..88dd21c0 100644 --- a/src/libs/database/include/database/objects/User.hpp +++ b/src/libs/database/include/database/objects/User.hpp @@ -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 diff --git a/src/libs/services/auth/include/services/auth/Types.hpp b/src/libs/services/auth/include/services/auth/Types.hpp index 4122b2d2..d1a4027e 100644 --- a/src/libs/services/auth/include/services/auth/Types.hpp +++ b/src/libs/services/auth/include/services/auth/Types.hpp @@ -22,7 +22,8 @@ #include #include "core/Exception.hpp" -#include "database/Types.hpp" + +#include "database/objects/Types.hpp" namespace lms::auth { diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index e1353b4a..f54858e2 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -30,6 +30,7 @@ #include "database/objects/Filters.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" namespace lms::db diff --git a/src/libs/services/recommendation/impl/IEngine.hpp b/src/libs/services/recommendation/impl/IEngine.hpp index a072fba4..bdf48f7b 100644 --- a/src/libs/services/recommendation/impl/IEngine.hpp +++ b/src/libs/services/recommendation/impl/IEngine.hpp @@ -22,8 +22,9 @@ #include #include "core/EnumSet.hpp" -#include "database/Types.hpp" + #include "database/objects/TrackListId.hpp" +#include "database/objects/Types.hpp" #include "services/recommendation/Types.hpp" namespace lms::db diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp index fabb9a0d..b0520482 100644 --- a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -23,10 +23,11 @@ #include #include "core/EnumSet.hpp" -#include "database/Types.hpp" + #include "database/objects/ArtistId.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackListId.hpp" +#include "database/objects/Types.hpp" #include "services/recommendation/Types.hpp" namespace lms::db diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 502d14ab..b189e793 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -296,7 +296,7 @@ namespace lms::scanner return dbLyrics; } - db::ImageType convertImageType(audio::Image::Type type) + db::ImageType audioImageTypeToDbImageType(audio::Image::Type type) { switch (type) { @@ -349,6 +349,92 @@ namespace lms::scanner return db::ImageType::Unknown; } + db::ContainerType audioContainerToDbContainer(audio::ContainerType type) + { + switch (type) + { + case audio::ContainerType::AIFF: + return db::ContainerType::AIFF; + case audio::ContainerType::APE: + return db::ContainerType::APE; + case audio::ContainerType::ASF: + return db::ContainerType::ASF; + case audio::ContainerType::DSF: + return db::ContainerType::DSF; + case audio::ContainerType::FLAC: + return db::ContainerType::FLAC; + case audio::ContainerType::MP4: + return db::ContainerType::MP4; + case audio::ContainerType::MPC: + return db::ContainerType::MPC; + case audio::ContainerType::MPEG: + return db::ContainerType::MPEG; + case audio::ContainerType::Shorten: + return db::ContainerType::Shorten; + case audio::ContainerType::Ogg: + return db::ContainerType::Ogg; + case audio::ContainerType::TrueAudio: + return db::ContainerType::TrueAudio; + case audio::ContainerType::WAV: + return db::ContainerType::WAV; + case audio::ContainerType::WavPack: + return db::ContainerType::WavPack; + } + + return db::ContainerType::Unknown; + } + + db::CodecType audioCodecToDbCodec(audio::CodecType type) + { + switch (type) + { + case audio::CodecType::AAC: + return db::CodecType::AAC; + case audio::CodecType::AC3: + return db::CodecType::AC3; + case audio::CodecType::ALAC: + return db::CodecType::ALAC; + case audio::CodecType::APE: + return db::CodecType::APE; + case audio::CodecType::DSD: + return db::CodecType::DSD; + case audio::CodecType::EAC3: + return db::CodecType::EAC3; + case audio::CodecType::FLAC: + return db::CodecType::FLAC; + case audio::CodecType::MP3: + return db::CodecType::MP3; + case audio::CodecType::MP4ALS: + return db::CodecType::MP4ALS; + case audio::CodecType::MPC7: + return db::CodecType::MPC7; + case audio::CodecType::MPC8: + return db::CodecType::MPC8; + case audio::CodecType::Opus: + return db::CodecType::Opus; + case audio::CodecType::PCM: + return db::CodecType::PCM; + case audio::CodecType::Shorten: + return db::CodecType::Shorten; + case audio::CodecType::TrueAudio: + return db::CodecType::TrueAudio; + case audio::CodecType::Vorbis: + return db::CodecType::Vorbis; + case audio::CodecType::WavPack: + return db::CodecType::WavPack; + case audio::CodecType::WMA1: + return db::CodecType::WMA1; + case audio::CodecType::WMA2: + return db::CodecType::WMA2; + case audio::CodecType::WMA9Pro: + return db::CodecType::WMA9Pro; + case audio::CodecType::WMA9Lossless: + return db::CodecType::WMA9Lossless; + } + + return db::CodecType::Unknown; + } + db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo) { db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) }; @@ -372,7 +458,7 @@ namespace lms::scanner const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) }; db::TrackEmbeddedImageLink::pointer imageLink{ session.create(dbTrack, image) }; imageLink.modify()->setIndex(imageInfo.index); - imageLink.modify()->setType(convertImageType(imageInfo.type)); + imageLink.modify()->setType(audioImageTypeToDbImageType(imageInfo.type)); imageLink.modify()->setDescription(imageInfo.description); return imageLink; @@ -695,11 +781,13 @@ namespace lms::scanner track.modify()->setScanVersion(getScannerSettings().audioScanVersion); // Audio properties - track.modify()->setBitrate(_file->audioProperties.bitrate); - track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0); - track.modify()->setChannelCount(_file->audioProperties.channelCount); track.modify()->setDuration(_file->audioProperties.duration); + track.modify()->setContainer(audioContainerToDbContainer(_file->audioProperties.container)); + track.modify()->setCodec(audioCodecToDbCodec(_file->audioProperties.codec)); + track.modify()->setBitrate(_file->audioProperties.bitrate); + track.modify()->setChannelCount(_file->audioProperties.channelCount); track.modify()->setSampleRate(_file->audioProperties.sampleRate); + track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample); track.modify()->setFileSize(getFileSize()); track.modify()->setLastWriteTime(getLastWriteTime()); diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index 25523e3b..3bd41c2f 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -26,7 +26,7 @@ #include #include -#include "database/Types.hpp" +#include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" #include "services/scrobbling/Listen.hpp" diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index 3d0b2e09..ff5c016f 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -31,6 +31,7 @@ #include "database/objects/Filters.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" #include "services/scrobbling/Listen.hpp" namespace lms::db diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp index 3420d38b..89547fc5 100644 --- a/src/libs/subsonic/impl/responses/Artist.hpp +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -23,7 +23,7 @@ #include #include "database/Object.hpp" -#include "database/Types.hpp" +#include "database/objects/Types.hpp" #include "SubsonicResponse.hpp" diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 87a0d753..2a23f26b 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -183,7 +183,7 @@ namespace lms::api::subsonic return trackResponse; trackResponse.setAttribute("comment", track->getComment()); - trackResponse.setAttribute("bitDepth", track->getBitsPerSample()); + trackResponse.setAttribute("bitDepth", track->getBitsPerSample() ? *track->getBitsPerSample() : 0); trackResponse.setAttribute("samplingRate", track->getSampleRate()); trackResponse.setAttribute("channelCount", track->getChannelCount()); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 75dfee52..a7776097 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -26,7 +26,7 @@ #include #include "database/Object.hpp" -#include "database/Types.hpp" +#include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" #include "services/scanner/ScannerEvents.hpp" diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index ea5472ac..b335d065 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -27,8 +27,8 @@ #include #include -#include "database/Types.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" namespace lms::ui { diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index 7d09cdb1..6d96602b 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -21,9 +21,10 @@ #include -#include "DatabaseCollectorBase.hpp" -#include "database/Types.hpp" #include "database/objects/ArtistId.hpp" +#include "database/objects/Types.hpp" + +#include "DatabaseCollectorBase.hpp" namespace lms::db { diff --git a/src/lms/ui/explore/ArtistListHelpers.hpp b/src/lms/ui/explore/ArtistListHelpers.hpp index 55a7fd0c..edde480a 100644 --- a/src/lms/ui/explore/ArtistListHelpers.hpp +++ b/src/lms/ui/explore/ArtistListHelpers.hpp @@ -23,9 +23,10 @@ #include -#include "common/ValueStringModel.hpp" #include "database/Object.hpp" -#include "database/Types.hpp" +#include "database/objects/Types.hpp" + +#include "common/ValueStringModel.hpp" namespace lms::db { diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp index 3dbae791..ef4c0c36 100644 --- a/src/lms/ui/explore/TrackListHelpers.hpp +++ b/src/lms/ui/explore/TrackListHelpers.hpp @@ -27,10 +27,11 @@ #include #include "core/EnumSet.hpp" + #include "database/Object.hpp" -#include "database/Types.hpp" #include "database/objects/ArtistId.hpp" #include "database/objects/TrackId.hpp" +#include "database/objects/Types.hpp" namespace lms::db { From c70935a5a224c33e9b01a34b6c90218cdbb4268d Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 26 Nov 2025 22:20:37 +0100 Subject: [PATCH 05/22] UI: use codec info in database instead of rescanning files --- src/libs/database/impl/objects/Release.cpp | 8 ++ src/libs/database/impl/objects/Types.cpp | 91 +++++++++++++++++++ .../include/database/objects/Release.hpp | 1 + .../include/database/objects/Types.hpp | 4 + src/libs/database/test/Release.cpp | 34 +++++++ .../impl/endpoints/MediaRetrieval.cpp | 1 + src/lms/ui/explore/ReleaseView.cpp | 35 +++---- src/lms/ui/explore/TrackListHelpers.cpp | 25 +---- 8 files changed, 153 insertions(+), 46 deletions(-) diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index dd9cf913..fab11b60 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -679,6 +679,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0")); } + std::vector Release::getCodecs() const + { + assert(session()); + + // Get the codec ordered by frequency + return utils::fetchQueryResults(session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC")); + } + std::vector Release::getArtists(TrackArtistLinkType linkType) const { assert(session()); diff --git a/src/libs/database/impl/objects/Types.cpp b/src/libs/database/impl/objects/Types.cpp index df159c91..32388860 100644 --- a/src/libs/database/impl/objects/Types.cpp +++ b/src/libs/database/impl/objects/Types.cpp @@ -73,4 +73,95 @@ namespace lms::db return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); } + core::LiteralString containerTypeToString(ContainerType type) + { + switch (type) + { + case ContainerType::Unknown: + break; + + 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 "Unknown"; + } + + core::LiteralString codecTypeToString(CodecType type) + { + switch (type) + { + case CodecType::Unknown: + break; + + case CodecType::AAC: + return "AAC"; + case CodecType::AC3: + return "AC3"; + case CodecType::ALAC: + return "ALAC"; + case CodecType::APE: + return "APE"; + case CodecType::DSD: + return "DSD"; + case CodecType::EAC3: + return "EAC3"; + 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 "Unknown"; + } } // namespace lms::db diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index 18999963..f8b8a3d8 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -279,6 +279,7 @@ namespace lms::db std::optional getCopyright() const; std::optional getCopyrightURL() const; std::size_t getMeanBitrate() const; + std::vector getCodecs() const; // Accessors std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp index 966083be..6cdbf15a 100644 --- a/src/libs/database/include/database/objects/Types.hpp +++ b/src/libs/database/include/database/objects/Types.hpp @@ -286,6 +286,8 @@ namespace lms::db WavPack = 13, }; + core::LiteralString containerTypeToString(ContainerType type); + enum class CodecType { Unknown = 0, @@ -312,4 +314,6 @@ namespace lms::db WMA9Pro = 20, WMA9Lossless = 21, }; + + core::LiteralString codecTypeToString(CodecType type); } // namespace lms::db diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 07e18749..54f1419c 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1214,6 +1214,40 @@ 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(CodecType::FLAC); + track1.get().modify()->setRelease(release1.get()); + + track2.get().modify()->setCodec(CodecType::MP3); + track2.get().modify()->setRelease(release1.get()); + + track3.get().modify()->setCodec(CodecType::MP3); + track3.get().modify()->setRelease(release1.get()); + + track4.get().modify()->setCodec(CodecType::AAC); + track4.get().modify()->setRelease(release1.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(release1->getCodecs(), (std::vector{ CodecType::MP3, CodecType::FLAC, CodecType::AAC })); + } + } + TEST_F(DatabaseFixture, Release_trackCount) { ScopedRelease release1{ session, "MyRelease1" }; diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 13c84402..91ff9f36 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -117,6 +117,7 @@ namespace lms::api::subsonic bool isOutputFormatCompatible(const std::filesystem::path& trackPath, audio::OutputFormat outputFormat) { // TODO: put this information in db during scan + // It is in base only for tracks, not yet for podcasts try { const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index cf001f96..3f6e0bb2 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -19,20 +19,17 @@ #include "ReleaseView.hpp" -#include #include +#include #include +#include #include #include #include #include "core/String.hpp" -#include "audio/AudioTypes.hpp" -#include "audio/Exception.hpp" -#include "audio/IAudioFileInfo.hpp" -#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -142,28 +139,20 @@ namespace lms::ui releaseInfo->bindString("release-labels", core::stringUtils::joinStrings(labels, " ยท ")); } - // TODO: save in DB and aggregate all this - for (const db::Track::pointer& track : db::Track::find(LmsApp->getDbSession(), db::Track::FindParameters{}.setRelease(releaseId).setRange(db::Range{ 0, 1 })).results) + // Codecs { - try + std::string codecStr; + for (db::CodecType codec : release->getCodecs()) { - const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; - - audio::AudioFileInfoParseOptions parseOptions; - parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed - parseOptions.readImages = false; - parseOptions.readTags = false; - const auto audioFile{ parser->parse(track->getAbsoluteFilePath(), parseOptions) }; - - if (audioFile->getAudioProperties()) - { - releaseInfo->setCondition("if-has-codec", true); - releaseInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties()->codec).c_str(), Wt::TextFormat::Plain); - break; - } + if (!codecStr.empty()) + codecStr += " ยท "; + codecStr += db::codecTypeToString(codec).str(); } - catch (const audio::Exception& e) + + if (!codecStr.empty()) { + releaseInfo->setCondition("if-has-codec", true); + releaseInfo->bindString("codec", codecStr, Wt::TextFormat::Plain); } } diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 3591fe46..6bead9d7 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -25,10 +25,6 @@ #include "core/Service.hpp" -#include "audio/AudioTypes.hpp" -#include "audio/Exception.hpp" -#include "audio/IAudioFileInfo.hpp" -#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -131,25 +127,8 @@ namespace lms::ui::TrackListHelpers } } - try - { - const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; - - audio::AudioFileInfoParseOptions parseOptions; - parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed - parseOptions.readImages = false; - parseOptions.readTags = false; - const auto audioFile{ parser->parse(track->getAbsoluteFilePath(), parseOptions) }; - - if (audioFile->getAudioProperties()) - { - trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", audio::codecTypeToString(audioFile->getAudioProperties()->codec).c_str(), Wt::TextFormat::Plain); - } - } - catch (const audio::Exception& e) - { - } + trackInfo->setCondition("if-has-codec", true); + trackInfo->bindString("codec", db::codecTypeToString(track->getCodec()).c_str(), Wt::TextFormat::Plain); trackInfo->bindString("duration", utils::durationToString(track->getDuration())); if (track->getBitrate()) From c3cd30f2481b9d830d026ce67a963c69697a05fc Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 29 Nov 2025 15:37:04 +0100 Subject: [PATCH 06/22] Store audio properties in database also for podcasts --- src/libs/database/impl/Migration.cpp | 15 +- .../database/objects/PodcastEpisode.hpp | 38 ++++- src/libs/database/test/Migration.cpp | 1 + src/libs/services/podcast/CMakeLists.txt | 1 + .../services/podcast/impl/PodcastService.cpp | 1 + .../impl/steps/CheckForMissingFilesStep.cpp | 14 +- .../steps/DownloadEpisodeArtworksStep.cpp | 2 +- .../impl/steps/DownloadEpisodesStep.cpp | 144 ++++++++++++++++-- .../impl/steps/DownloadEpisodesStep.hpp | 8 + .../steps/DownloadPodcastArtworksStep.cpp | 2 +- 10 files changed, 202 insertions(+), 24 deletions(-) diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index f8eca7ac..639c8f08 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -24,6 +24,7 @@ #include "core/Exception.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" + #include "database/Session.hpp" #include "database/objects/ScanSettings.hpp" @@ -34,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 101 }; + static constexpr Version LMS_DATABASE_VERSION{ 102 }; } VersionInfo::VersionInfo() @@ -1669,6 +1670,17 @@ FROM track)"); 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)" }; @@ -1746,6 +1758,7 @@ FROM track)"); { 98, migrateFromV98 }, { 99, migrateFromV99 }, { 100, migrateFromV100 }, + { 101, migrateFromV101 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/include/database/objects/PodcastEpisode.hpp b/src/libs/database/include/database/objects/PodcastEpisode.hpp index 8db9c541..0f1dc25c 100644 --- a/src/libs/database/include/database/objects/PodcastEpisode.hpp +++ b/src/libs/database/include/database/objects/PodcastEpisode.hpp @@ -91,6 +91,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; } + ContainerType getContainer() const { return _container; } + CodecType getCodec() const { return _codec; } + std::size_t getBitrate() const { return _bitrate; } + std::size_t getChannelCount() const { return _channelCount; } + std::size_t getSampleRate() const { return _sampleRate; } + std::optional 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; } @@ -104,7 +113,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 getDuration() const { return _duration; } ObjectPtr getPodcast() const { return _podcast; } PodcastId getPodcastId() const { return _podcast.id(); } ObjectPtr getArtwork() const; @@ -114,6 +122,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(ContainerType container) { _container = container; } + void setCodec(CodecType codec) { _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 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; } @@ -136,6 +153,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"); @@ -149,7 +174,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); @@ -163,6 +187,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 _duration{ 0 }; + ContainerType _container{ ContainerType::Unknown }; + CodecType _codec{ CodecType::Unknown }; + int _bitrate{}; // in bps + int _channelCount{}; + int _sampleRate{}; + std::optional _bitsPerSample; + std::string _url; std::string _title; std::string _link; @@ -179,7 +212,6 @@ namespace lms::db std::string _subtitle; std::string _summary; bool _explicit{}; - std::chrono::duration _duration{ 0 }; Wt::Dbo::ptr _artwork; Wt::Dbo::ptr _podcast; diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 09de4d17..2d62e054 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.cpp @@ -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" diff --git a/src/libs/services/podcast/CMakeLists.txt b/src/libs/services/podcast/CMakeLists.txt index 54b4a77f..1921939b 100644 --- a/src/libs/services/podcast/CMakeLists.txt +++ b/src/libs/services/podcast/CMakeLists.txt @@ -24,6 +24,7 @@ target_include_directories(lmspodcast PRIVATE target_link_libraries(lmspodcast PRIVATE lmscore + lmsaudio lmsimage pugixml::pugixml ) diff --git a/src/libs/services/podcast/impl/PodcastService.cpp b/src/libs/services/podcast/impl/PodcastService.cpp index bb36b245..18f8f747 100644 --- a/src/libs/services/podcast/impl/PodcastService.cpp +++ b/src/libs/services/podcast/impl/PodcastService.cpp @@ -26,6 +26,7 @@ #include "core/ITraceLogger.hpp" #include "core/Service.hpp" #include "core/http/IClient.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Podcast.hpp" diff --git a/src/libs/services/podcast/impl/steps/CheckForMissingFilesStep.cpp b/src/libs/services/podcast/impl/steps/CheckForMissingFilesStep.cpp index 9ff46aa4..03ca5991 100644 --- a/src/libs/services/podcast/impl/steps/CheckForMissingFilesStep.cpp +++ b/src/libs/services/podcast/impl/steps/CheckForMissingFilesStep.cpp @@ -51,13 +51,7 @@ namespace lms::podcast assert(std::holds_alternative(artwork->getUnderlyingId())); // these artworks can only be an image const std::filesystem::path filePath{ artwork->getAbsoluteFilePath() }; - if (!fileExists(filePath.string())) - { - LMS_LOG(PODCAST, DEBUG, "Artwork file is missing: " << filePath); - return false; - } - - return true; + return fileExists(filePath.string()); } } // namespace @@ -86,7 +80,10 @@ namespace lms::podcast if (const db::Artwork::pointer artwork{ podcast->getArtwork() }) { if (!checkArtworkFile(artwork)) + { + LMS_LOG(PODCAST, DEBUG, "Artwork file " << artwork->getAbsoluteFilePath() << " is missing for podcast '" << podcast->getTitle() << "'"); missingImages.push_back(artwork->getImageId()); + } } }); @@ -94,7 +91,10 @@ namespace lms::podcast if (const db::Artwork::pointer artwork{ episode->getArtwork() }) { if (!checkArtworkFile(artwork)) + { + LMS_LOG(PODCAST, DEBUG, "Artwork file " << artwork->getAbsoluteFilePath() << " is missing for podcast episode '" << episode->getTitle() << "'"); missingImages.push_back(artwork->getImageId()); + } } }); } diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp b/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp index 0d95573e..1cc6bd6a 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp @@ -112,7 +112,7 @@ namespace lms::podcast const std::string url{ episode->getImageUrl() }; const std::filesystem::path finalFilePath{ getCachePath() / utils::generateRandomFileName() }; - LMS_LOG(PODCAST, DEBUG, "Downloading episode artwork for episode '" << episode->getTitle() << "' from '" << url << "' in file '" << finalFilePath << "'"); + LMS_LOG(PODCAST, DEBUG, "Downloading episode artwork for episode '" << episode->getTitle() << "' from '" << url << "' in file " << finalFilePath); core::http::ClientGETRequestParameters params; params.relativeUrl = episode->getImageUrl(); diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp index 26cb2cf7..25b2ad7f 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp @@ -26,6 +26,10 @@ #include "core/Service.hpp" #include "core/http/IClient.hpp" +#include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Podcast.hpp" @@ -38,7 +42,93 @@ namespace lms::podcast { namespace { - void updateEpisode(db::Session& session, db::PodcastEpisodeId episodeId, const std::filesystem::path& relativeFilePath) + db::ContainerType audioContainerToDbContainer(audio::ContainerType type) + { + switch (type) + { + case audio::ContainerType::AIFF: + return db::ContainerType::AIFF; + case audio::ContainerType::APE: + return db::ContainerType::APE; + case audio::ContainerType::ASF: + return db::ContainerType::ASF; + case audio::ContainerType::DSF: + return db::ContainerType::DSF; + case audio::ContainerType::FLAC: + return db::ContainerType::FLAC; + case audio::ContainerType::MP4: + return db::ContainerType::MP4; + case audio::ContainerType::MPC: + return db::ContainerType::MPC; + case audio::ContainerType::MPEG: + return db::ContainerType::MPEG; + case audio::ContainerType::Shorten: + return db::ContainerType::Shorten; + case audio::ContainerType::Ogg: + return db::ContainerType::Ogg; + case audio::ContainerType::TrueAudio: + return db::ContainerType::TrueAudio; + case audio::ContainerType::WAV: + return db::ContainerType::WAV; + case audio::ContainerType::WavPack: + return db::ContainerType::WavPack; + } + + return db::ContainerType::Unknown; + } + + db::CodecType audioCodecToDbCodec(audio::CodecType type) + { + switch (type) + { + case audio::CodecType::AAC: + return db::CodecType::AAC; + case audio::CodecType::AC3: + return db::CodecType::AC3; + case audio::CodecType::ALAC: + return db::CodecType::ALAC; + case audio::CodecType::APE: + return db::CodecType::APE; + case audio::CodecType::DSD: + return db::CodecType::DSD; + case audio::CodecType::EAC3: + return db::CodecType::EAC3; + case audio::CodecType::FLAC: + return db::CodecType::FLAC; + case audio::CodecType::MP3: + return db::CodecType::MP3; + case audio::CodecType::MP4ALS: + return db::CodecType::MP4ALS; + case audio::CodecType::MPC7: + return db::CodecType::MPC7; + case audio::CodecType::MPC8: + return db::CodecType::MPC8; + case audio::CodecType::Opus: + return db::CodecType::Opus; + case audio::CodecType::PCM: + return db::CodecType::PCM; + case audio::CodecType::Shorten: + return db::CodecType::Shorten; + case audio::CodecType::TrueAudio: + return db::CodecType::TrueAudio; + case audio::CodecType::Vorbis: + return db::CodecType::Vorbis; + case audio::CodecType::WavPack: + return db::CodecType::WavPack; + case audio::CodecType::WMA1: + return db::CodecType::WMA1; + case audio::CodecType::WMA2: + return db::CodecType::WMA2; + case audio::CodecType::WMA9Pro: + return db::CodecType::WMA9Pro; + case audio::CodecType::WMA9Lossless: + return db::CodecType::WMA9Lossless; + } + + return db::CodecType::Unknown; + } + + void updateEpisode(db::Session& session, db::PodcastEpisodeId episodeId, const std::filesystem::path& relativeFilePath, const audio::AudioProperties& audioProperties) { auto transaction{ session.createWriteTransaction() }; @@ -47,6 +137,14 @@ namespace lms::podcast return; // may have been deleted by admin dbEpisode.modify()->setAudioRelativeFilePath(relativeFilePath); + + dbEpisode.modify()->setDuration(audioProperties.duration); + dbEpisode.modify()->setContainer(audioContainerToDbContainer(audioProperties.container)); + dbEpisode.modify()->setCodec(audioCodecToDbCodec(audioProperties.codec)); + dbEpisode.modify()->setBitrate(audioProperties.bitrate); + dbEpisode.modify()->setChannelCount(audioProperties.channelCount); + dbEpisode.modify()->setSampleRate(audioProperties.sampleRate); + dbEpisode.modify()->setBitsPerSample(audioProperties.bitsPerSample); } } // namespace @@ -54,10 +152,12 @@ namespace lms::podcast : RefreshStep{ context, std::move(callback) } , _autoDownloadEpisodes{ core::Service::get()->getBool("podcast-auto-download-episodes", true) } , _autoDownloadEpisodesMaxAge{ core::Service::get()->getULong("podcast-auto-download-episodes-max-age-days", 30) } - + , _audioFileInfoParser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) } { } + DownloadEpisodesStep::~DownloadEpisodesStep() = default; + core::LiteralString DownloadEpisodesStep::getName() const { return "Download episodes"; @@ -171,18 +271,40 @@ namespace lms::podcast assert(msg.body().empty()); getExecutor().post([=, this] { LMS_LOG(PODCAST, DEBUG, "Download episode from '" << url << "' complete"); - LMS_LOG(PODCAST, DEBUG, "Renaming temp file " << tmpFilePath << " to " << finalFilePath); - std::error_code ec; - std::filesystem::rename(tmpFilePath, finalFilePath, ec); - if (ec) - LMS_LOG(PODCAST, ERROR, "Failed to rename temp file " << tmpFilePath << " to " << finalFilePath << ": " << ec.message()); - else - updateEpisode(getDb().getTLSSession(), episodeId, randomName); + try + { + audio::AudioFileInfoParseOptions options; + options.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; + options.readImages = false; + options.readTags = false; - // TODO: now the file is complete, should we attempt to read it and get the real information like duration and size? + const auto audioFileInfo{ _audioFileInfoParser->parse(tmpFilePath, options) }; + const auto* audioProperties{ audioFileInfo->getAudioProperties() }; + if (audioProperties) + { + std::error_code ec; + std::filesystem::rename(tmpFilePath, finalFilePath, ec); + if (ec) + { + LMS_LOG(PODCAST, ERROR, "Failed to rename temp file " << tmpFilePath << " to " << finalFilePath << ": " << ec.message()); + } + else + { + updateEpisode(getDb().getTLSSession(), episodeId, randomName, *audioProperties); + LMS_LOG(PODCAST, INFO, "Downloaded episode '" << episode->getTitle() << "'"); + } + } + else + { + LMS_LOG(PODCAST, WARNING, "Failed to get audio properties from downloaded episode from '" << url << "'"); + } + } + catch (const audio::Exception& e) + { + LMS_LOG(PODCAST, WARNING, "Failed to parse downloaded episode from '" << url << "': " << e.what()); + } - LMS_LOG(PODCAST, INFO, "Downloaded episode '" << episode->getTitle() << "'"); processNext(); }); }; diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.hpp b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.hpp index 35d23265..b430cb7f 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.hpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.hpp @@ -25,12 +25,18 @@ #include "RefreshStep.hpp" +namespace lms::audio +{ + class IAudioFileInfoParser; +} + namespace lms::podcast { class DownloadEpisodesStep : public RefreshStep { public: DownloadEpisodesStep(RefreshContext& context, OnDoneCallback callback); + ~DownloadEpisodesStep() override; private: core::LiteralString getName() const override; @@ -44,6 +50,8 @@ namespace lms::podcast const bool _autoDownloadEpisodes; const std::chrono::days _autoDownloadEpisodesMaxAge; + const std::unique_ptr _audioFileInfoParser; + std::deque _episodesToDownload; }; diff --git a/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp b/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp index c27e1912..2db0e696 100644 --- a/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp @@ -111,7 +111,7 @@ namespace lms::podcast const std::string url{ podcast->getImageUrl() }; const std::filesystem::path finalFilePath{ getCachePath() / utils::generateRandomFileName() }; - LMS_LOG(PODCAST, DEBUG, "Downloading podcast artwork '" << podcast->getTitle() << "' from '" << url << "' in file '" << finalFilePath << "'"); + LMS_LOG(PODCAST, DEBUG, "Downloading podcast artwork '" << podcast->getTitle() << "' from '" << url << "' in file " << finalFilePath); core::http::ClientGETRequestParameters params; params.relativeUrl = podcast->getImageUrl(); From d834f34683ac3e2ee10edb9228aeaf2cfed07190 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Nov 2025 18:48:04 +0100 Subject: [PATCH 07/22] Factorized some container/codec handling code + removed the no longer needed transcode output formats mka and webm --- Dockerfile-release | 7 +- approot/messages.xml | 2 - approot/messages_es.xml | 2 - approot/messages_fr.xml | 2 - approot/messages_it.xml | 2 - approot/messages_pl.xml | 2 - approot/messages_zh.xml | 2 - src/libs/audio/CMakeLists.txt | 2 - src/libs/audio/impl/ffmpeg/AudioFile.cpp | 69 ++-- src/libs/audio/impl/ffmpeg/AudioFile.hpp | 7 +- src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp | 2 +- src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp | 2 +- src/libs/audio/impl/ffmpeg/ImageReader.cpp | 4 +- src/libs/audio/impl/ffmpeg/Transcoder.cpp | 128 ++++---- src/libs/audio/impl/ffmpeg/Transcoder.hpp | 5 +- src/libs/audio/impl/taglib/AudioFileInfo.cpp | 70 ++-- src/libs/audio/impl/taglib/AudioFileInfo.hpp | 2 +- src/libs/audio/impl/taglib/ImageReader.cpp | 148 ++++----- src/libs/audio/impl/taglib/Utils.cpp | 4 +- .../audio/include/audio/AudioProperties.hpp | 40 +++ src/libs/audio/include/audio/Exception.hpp | 8 +- src/libs/audio/include/audio/IImageReader.hpp | 55 +--- .../audio/include/audio/TranscodeTypes.hpp | 28 +- src/libs/core/CMakeLists.txt | 4 + .../impl/media/CodecType.cpp} | 43 +-- src/libs/core/impl/media/ContainerType.cpp | 58 ++++ .../impl/media/ImageType.cpp} | 55 ++-- src/libs/core/impl/media/MimeType.cpp | 80 +++++ .../include/core/media/CodecType.hpp} | 37 +-- .../core/include/core/media/ContainerType.hpp | 44 +++ .../core/include/core/media/ImageType.hpp | 76 +++++ src/libs/core/include/core/media/MimeType.hpp | 29 ++ src/libs/database/CMakeLists.txt | 1 + .../database/impl/objects/PodcastEpisode.cpp | 21 ++ src/libs/database/impl/objects/Release.cpp | 13 +- src/libs/database/impl/objects/Track.cpp | 22 ++ .../impl/objects/TrackEmbeddedImage.cpp | 4 +- .../impl/objects/TrackEmbeddedImageLink.cpp | 12 +- src/libs/database/impl/objects/Types.cpp | 92 ------ .../database/impl/objects/detail/Types.cpp | 307 ++++++++++++++++++ .../database/impl/objects/detail/Types.hpp | 40 +++ .../database/objects/PodcastEpisode.hpp | 16 +- .../include/database/objects/Release.hpp | 3 +- .../include/database/objects/Track.hpp | 15 +- .../database/objects/TrackEmbeddedImage.hpp | 6 +- .../objects/TrackEmbeddedImageLink.hpp | 9 +- .../include/database/objects/Types.hpp | 80 +---- .../include/database/objects/detail/Types.hpp | 97 ++++++ src/libs/database/test/Release.cpp | 10 +- src/libs/database/test/TrackEmbeddedImage.cpp | 50 ++- .../impl/steps/DownloadEpisodesStep.cpp | 92 +----- .../audiofile/AudioFileScanOperation.cpp | 154 +-------- .../audiofile/AudioFileScanOperation.hpp | 7 +- .../steps/ScanStepAssociateMediumImages.cpp | 2 +- .../steps/ScanStepAssociateReleaseImages.cpp | 6 +- .../steps/ScanStepAssociateTrackImages.cpp | 4 +- .../transcoding/impl/TranscodeService.cpp | 10 +- .../impl/endpoints/MediaRetrieval.cpp | 181 ++++++----- src/libs/subsonic/impl/responses/Song.cpp | 36 +- src/lms/ui/MediaPlayer.cpp | 2 - src/lms/ui/SettingsView.cpp | 2 - src/lms/ui/explore/ReleaseView.cpp | 4 +- src/lms/ui/explore/TrackListHelpers.cpp | 7 +- .../ui/resource/AudioTranscodingResource.cpp | 83 ++++- src/tools/audioinfo/LmsAudioInfo.cpp | 20 +- 65 files changed, 1424 insertions(+), 1003 deletions(-) create mode 100644 src/libs/audio/include/audio/AudioProperties.hpp rename src/libs/{audio/impl/AudioTypes.cpp => core/impl/media/CodecType.cpp} (66%) create mode 100644 src/libs/core/impl/media/ContainerType.cpp rename src/libs/{audio/impl/ImageReader.cpp => core/impl/media/ImageType.cpp} (59%) create mode 100644 src/libs/core/impl/media/MimeType.cpp rename src/libs/{audio/include/audio/AudioTypes.hpp => core/include/core/media/CodecType.hpp} (65%) create mode 100644 src/libs/core/include/core/media/ContainerType.hpp create mode 100644 src/libs/core/include/core/media/ImageType.hpp create mode 100644 src/libs/core/include/core/media/MimeType.hpp create mode 100644 src/libs/database/impl/objects/detail/Types.cpp create mode 100644 src/libs/database/impl/objects/detail/Types.hpp create mode 100644 src/libs/database/include/database/objects/detail/Types.hpp diff --git a/Dockerfile-release b/Dockerfile-release index 1bf40d82..0a162378 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -67,11 +67,12 @@ RUN \ --enable-libopenjpeg \ --enable-libopus \ --enable-libvorbis \ + --enable-zlib \ --disable-everything \ - --enable-decoder=aac*,ac3*,alac,als,dsd*,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png \ + --enable-decoder=aac*,ac3*,alac,als,ape,asf,dsd*,flac,libopus,pcm*,libvorbis,mp3*,mpc7,mpc8,shorten,tta,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-demuxer=aac,aiff,ape,asf,dsf,flac,m4a,mp3,mov,mpc,mpc8,ogg,shn,tta,wav,wv \ + --enable-muxer=ogg,mp3 \ --enable-protocol=file,pipe \ --enable-filter=aresample \ --enable-lto \ diff --git a/approot/messages.xml b/approot/messages.xml index 9a4fd79f..00d92c46 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -371,11 +371,9 @@ Never Transcoding output bitrate Transcoding output format -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis User Interface diff --git a/approot/messages_es.xml b/approot/messages_es.xml index b60112a2..5800e983 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -370,11 +370,9 @@ Nunca Bitrate de transcodificaciรณn Formato de transcodificaciรณn -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis Interfaz de usuario diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index ed0a2b30..4ca0b8a5 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -370,11 +370,9 @@ Jamais Bitrate du transcodage Format du transcodage -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis Interface utilisateur diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 13e74ee0..a1856a6b 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -370,11 +370,9 @@ Mai Bitrate transcodifica Formato di transcodifica -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis Interfaccia utente diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 2687adc6..6e8ff867 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -396,11 +396,9 @@ Nigdy Przepล‚ywnoล›ฤ‡ transkodowania Format wyjล›ciowy transkodowania -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis Interfejs uลผytkownika diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index e5f4cda8..37f11a77 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -367,11 +367,9 @@ ไปŽไธ ่ฝฌ็ ๆฏ”็‰น็އ ่ฝฌ็ ๆ ผๅผ -Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis -WebM/Vorbis diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt index bd8d1d4f..0979b975 100644 --- a/src/libs/audio/CMakeLists.txt +++ b/src/libs/audio/CMakeLists.txt @@ -14,8 +14,6 @@ add_library(lmsaudio STATIC impl/taglib/ImageReader.cpp impl/taglib/TagReader.cpp impl/taglib/Utils.cpp - impl/AudioTypes.cpp - impl/ImageReader.cpp impl/AudioFileInfoParser.cpp impl/TagReader.cpp ) diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp index 9e5eed5c..2afd4617 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -35,7 +35,6 @@ extern "C" #include "core/ITraceLogger.hpp" #include "core/String.hpp" -#include "audio/AudioTypes.hpp" #include "audio/Exception.hpp" namespace lms::audio::ffmpeg @@ -71,83 +70,83 @@ namespace lms::audio::ffmpeg res[core::stringUtils::stringToUpper(tag->key)] = tag->value; } - std::optional avdemuxerToContainerType(std::string_view name) + std::optional avdemuxerToContainerType(std::string_view name) { if (name == "aiff") - return ContainerType::AIFF; + return core::media::ContainerType::AIFF; if (name == "ape") - return ContainerType::APE; + return core::media::ContainerType::APE; if (name.starts_with("asf")) - return ContainerType::ASF; + return core::media::ContainerType::ASF; if (name == "dsf") - return ContainerType::DSF; + return core::media::ContainerType::DSF; if (name == "flac") - return ContainerType::FLAC; + return core::media::ContainerType::FLAC; if (name.find("mp4") != std::string_view::npos) - return ContainerType::MP4; + return core::media::ContainerType::MP4; if (name.starts_with("mpc")) - return ContainerType::MPC; + return core::media::ContainerType::MPC; if (name == "mp3") - return ContainerType::MPEG; + return core::media::ContainerType::MPEG; if (name == "ogg") - return ContainerType::Ogg; + return core::media::ContainerType::Ogg; if (name == "shn") - return ContainerType::Shorten; + return core::media::ContainerType::Shorten; if (name == "tta") - return ContainerType::TrueAudio; + return core::media::ContainerType::TrueAudio; if (name == "wav") - return ContainerType::WAV; + return core::media::ContainerType::WAV; if (name == "wv") - return ContainerType::WavPack; + return core::media::ContainerType::WavPack; return std::nullopt; } - std::optional avcodecToCodecType(AVCodecID codec) + std::optional avcodecToCodecType(AVCodecID codec) { switch (codec) { case AV_CODEC_ID_AAC: - return CodecType::AAC; + return core::media::CodecType::AAC; case AV_CODEC_ID_AC3: - return CodecType::AC3; + return core::media::CodecType::AC3; case AV_CODEC_ID_ALAC: - return CodecType::ALAC; + return core::media::CodecType::ALAC; case AV_CODEC_ID_APE: - return CodecType::APE; + return core::media::CodecType::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::CodecType::DSD; case AV_CODEC_ID_EAC3: - return CodecType::EAC3; + return core::media::CodecType::EAC3; case AV_CODEC_ID_FLAC: - return CodecType::FLAC; + return core::media::CodecType::FLAC; case AV_CODEC_ID_MP3: - return CodecType::MP3; + return core::media::CodecType::MP3; case AV_CODEC_ID_MP4ALS: - return CodecType::MP4ALS; + return core::media::CodecType::MP4ALS; case AV_CODEC_ID_MUSEPACK7: - return CodecType::MPC7; + return core::media::CodecType::MPC7; case AV_CODEC_ID_MUSEPACK8: - return CodecType::MPC8; + return core::media::CodecType::MPC8; case AV_CODEC_ID_OPUS: - return CodecType::Opus; + return core::media::CodecType::Opus; case AV_CODEC_ID_SHORTEN: - return CodecType::Shorten; + return core::media::CodecType::Shorten; case AV_CODEC_ID_VORBIS: - return CodecType::Vorbis; + return core::media::CodecType::Vorbis; case AV_CODEC_ID_WAVPACK: - return CodecType::WavPack; + return core::media::CodecType::WavPack; case AV_CODEC_ID_WMALOSSLESS: - return CodecType::WMA9Lossless; + return core::media::CodecType::WMA9Lossless; case AV_CODEC_ID_WMAPRO: - return CodecType::WMA9Pro; + return core::media::CodecType::WMA9Pro; case AV_CODEC_ID_WMAV1: - return CodecType::WMA1; + return core::media::CodecType::WMA1; case AV_CODEC_ID_WMAV2: - return CodecType::WMA2; + return core::media::CodecType::WMA2; default: return std::nullopt; diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp index fde8a031..61aa973c 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -27,7 +27,8 @@ #include #include -#include "audio/AudioTypes.hpp" +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" extern "C" { @@ -44,7 +45,7 @@ namespace lms::audio::ffmpeg struct ContainerInfo { - std::optional container; + std::optional container; std::string containerName; std::optional bitrate; @@ -54,7 +55,7 @@ namespace lms::audio::ffmpeg struct StreamInfo { size_t index{}; - std::optional codec; + std::optional codec; std::string codecName; std::optional bitrate; diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp index 62fe54e4..e987412e 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp @@ -24,7 +24,7 @@ #include "core/ILogger.hpp" -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/IAudioFileInfo.hpp" #include "AudioFile.hpp" diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp index 8d89a766..e0874668 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp @@ -22,7 +22,7 @@ #include #include -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp index 8014ea93..cedd6b8c 100644 --- a/src/libs/audio/impl/ffmpeg/ImageReader.cpp +++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp @@ -45,9 +45,9 @@ namespace lms::audio::ffmpeg 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); }); diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp index cd6ea9d5..72261de6 100644 --- a/src/libs/audio/impl/ffmpeg/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -19,13 +19,14 @@ #include "Transcoder.hpp" -#include +#include #include #include "core/IChildProcessManager.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Service.hpp" +#include "core/media/MimeType.hpp" #include "audio/Exception.hpp" #include "audio/TranscodeTypes.hpp" @@ -42,18 +43,29 @@ namespace lms::audio::ffmpeg { #define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message) - static std::atomic globalId{}; - static std::filesystem::path ffmpegPath; - - void Transcoder::init() + namespace { - ffmpegPath = core::Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); - if (!std::filesystem::exists(ffmpegPath)) - throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; - } + class FFmpegPath + { + public: + FFmpegPath() + { + path = core::Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); + if (!std::filesystem::exists(path)) + throw Exception{ "File '" + path.string() + "' does not exist!" }; + } + + const std::filesystem::path& get() const { return path; } + + private: + std::filesystem::path path; + }; + } // namespace + + std::atomic Transcoder::_nextDebugId; Transcoder::Transcoder(const TranscodeParameters& parameters) - : _debugId{ globalId++ } + : _debugId{ _nextDebugId++ } , _inputParams{ parameters.inputParameters } , _outputParams{ parameters.outputParameters } { @@ -64,8 +76,7 @@ namespace lms::audio::ffmpeg void Transcoder::start() { - if (ffmpegPath.empty()) - init(); + static FFmpegPath ffmpegPath; try { @@ -76,15 +87,16 @@ namespace lms::audio::ffmpeg } catch (const std::filesystem::filesystem_error& e) { - // TODO store/raise e.code() - throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() }; + throw IOFileException{ _inputParams.filePath, "Failed to test file existence", e.code() }; } LOG(INFO, "Transcoding file " << _inputParams.filePath); std::vector args; - args.emplace_back(ffmpegPath.string()); + args.emplace_back(ffmpegPath.get().string()); + + // TODO some codecs have restrictions, take them into account (channel count, sample rate, etc.) // Make sure: // - we do not produce anything in the stderr output @@ -114,7 +126,7 @@ namespace lms::audio::ffmpeg args.emplace_back("-1"); } - // Skip video flows (including covers) + // Skip video flows (including covers!) args.emplace_back("-vn"); // Output bitrates @@ -124,63 +136,68 @@ namespace lms::audio::ffmpeg args.emplace_back(std::to_string(*_outputParams.bitrate)); } - // 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::ContainerType::FLAC: + args.emplace_back("flac"); + break; + case core::media::ContainerType::Ogg: + args.emplace_back("ogg"); + break; + case core::media::ContainerType::MPEG: args.emplace_back("mp3"); break; - case OutputFormat::OGG_OPUS: - args.emplace_back("-acodec"); + default: + throw Exception{ "Unsupported container type " + std::string{ core::media::containerTypeToString(_outputParams.format->container).str() } }; + } + + args.emplace_back("-acodec"); + + switch (_outputParams.format->codec) + { + case core::media::CodecType::MP3: + args.emplace_back("libmp3lame"); + break; + + case core::media::CodecType::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::CodecType::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::CodecType::FLAC: + args.emplace_back("flac"); break; default: - throw Exception{ "Unhandled format (" + std::to_string(static_cast(*_outputParams.format)) + ")" }; + throw Exception{ "Unhandled codec type " + std::string{ codecTypeToString(_outputParams.format->codec).str() } }; } } args.emplace_back("pipe:1"); - LOG(DEBUG, "Dumping args (" << args.size() << ")"); - for (const std::string& arg : args) - LOG(DEBUG, "Arg = '" << arg << "'"); + if (core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) + { + LOG(DEBUG, "Dumping args (" << args.size() << ")"); + for (const std::string& arg : args) + LOG(DEBUG, "Arg = '" << arg << "'"); + } // Caution: stdin must have been closed before try { - _childProcess = core::Service::get()->spawnChildProcess(ffmpegPath, args); + _childProcess = core::Service::get()->spawnChildProcess(ffmpegPath.get(), args); } catch (core::ChildProcessException& exception) { - throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() }; + throw Exception{ "Cannot execute '" + ffmpegPath.get().string() + "': " + exception.what() }; } } @@ -202,25 +219,10 @@ namespace lms::audio::ffmpeg std::string_view Transcoder::getOutputMimeType() const { - // TODO: use input mime type if (_outputParams.format) - { - switch (*_outputParams.format) - { - case OutputFormat::MP3: - return "audio/mpeg"; - case OutputFormat::OGG_OPUS: - return "audio/opus"; - case OutputFormat::MATROSKA_OPUS: - return "audio/x-matroska"; - case OutputFormat::OGG_VORBIS: - return "audio/ogg"; - case OutputFormat::WEBM_VORBIS: - return "audio/webm"; - } - } + return core::media::getMimeType(_outputParams.format->container, _outputParams.format->codec).str(); - return "application/octet-stream"; // default, should not happen + return core::media::getMimeType(_inputParams.audioProperties.container, _inputParams.audioProperties.codec).str(); } bool Transcoder::finished() const diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.hpp b/src/libs/audio/impl/ffmpeg/Transcoder.hpp index 715c6d4f..7204922c 100644 --- a/src/libs/audio/impl/ffmpeg/Transcoder.hpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.hpp @@ -19,6 +19,8 @@ #pragma once +#include + #include "audio/ITranscoder.hpp" namespace lms::core @@ -47,7 +49,8 @@ namespace lms::audio::ffmpeg static void init(); void start(); - const std::size_t _debugId{}; + static std::atomic _nextDebugId; + const std::size_t _debugId; const TranscodeInputParameters _inputParams; const TranscodeOutputParameters _outputParams; std::unique_ptr _childProcess; diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp index 02b41ed5..1a749dd7 100644 --- a/src/libs/audio/impl/taglib/AudioFileInfo.cpp +++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp @@ -45,7 +45,7 @@ #include "core/ILogger.hpp" -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" @@ -104,27 +104,27 @@ namespace lms::audio::taglib // Guess container from the file type if (const auto* apeFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::APE; - audioProperties.codec = CodecType::APE; // TODO version? + audioProperties.container = core::media::ContainerType::APE; + audioProperties.codec = core::media::CodecType::APE; // TODO version? audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample(); } else if (const auto* asfFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::ASF; + audioProperties.container = core::media::ContainerType::ASF; switch (asfFile->audioProperties()->codec()) { case ::TagLib::ASF::Properties::Codec::WMA1: - audioProperties.codec = CodecType::WMA1; + audioProperties.codec = core::media::CodecType::WMA1; break; case ::TagLib::ASF::Properties::Codec::WMA2: - audioProperties.codec = CodecType::WMA2; + audioProperties.codec = core::media::CodecType::WMA2; break; case ::TagLib::ASF::Properties::Codec::WMA9Lossless: - audioProperties.codec = CodecType::WMA9Lossless; + audioProperties.codec = core::media::CodecType::WMA9Lossless; break; case ::TagLib::ASF::Properties::Codec::WMA9Pro: - audioProperties.codec = CodecType::WMA9Pro; + audioProperties.codec = core::media::CodecType::WMA9Pro; break; case ::TagLib::ASF::Properties::Codec::Unknown: LMS_LOG(AUDIO, DEBUG, "Unhandled ASF codec in " << filePath); @@ -136,27 +136,27 @@ namespace lms::audio::taglib #if LMS_TAGLIB_HAS_DSF else if (const auto* dsfFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::DSF; - audioProperties.codec = CodecType::DSD; + audioProperties.container = core::media::ContainerType::DSF; + audioProperties.codec = core::media::CodecType::DSD; audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample(); } #endif // LMS_TAGLIB_HAS_DSF else if (const auto* flacFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::FLAC; - audioProperties.codec = CodecType::FLAC; + audioProperties.container = core::media::ContainerType::FLAC; + audioProperties.codec = core::media::CodecType::FLAC; audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample(); } else if (const auto* mp4File{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::MP4; + audioProperties.container = core::media::ContainerType::MP4; switch (mp4File->audioProperties()->codec()) { case ::TagLib::MP4::Properties::Codec::AAC: - audioProperties.codec = CodecType::AAC; + audioProperties.codec = core::media::CodecType::AAC; break; case ::TagLib::MP4::Properties::Codec::ALAC: - audioProperties.codec = CodecType::ALAC; + audioProperties.codec = core::media::CodecType::ALAC; break; case ::TagLib::MP4::Properties::Codec::Unknown: LMS_LOG(AUDIO, DEBUG, "Unhandled MP4 codec in " << filePath); @@ -167,15 +167,15 @@ namespace lms::audio::taglib } else if (const auto* mpcFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::MPC; + audioProperties.container = core::media::ContainerType::MPC; switch (mpcFile->audioProperties()->mpcVersion()) { case 7: - audioProperties.codec = CodecType::MPC7; + audioProperties.codec = core::media::CodecType::MPC7; break; case 8: - audioProperties.codec = CodecType::MPC8; + audioProperties.codec = core::media::CodecType::MPC8; break; default: LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath); @@ -186,13 +186,13 @@ namespace lms::audio::taglib { const auto& properties{ *mpegFile->audioProperties() }; - audioProperties.container = ContainerType::MPEG; + audioProperties.container = core::media::ContainerType::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::CodecType::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::CodecType::AAC; #endif else { @@ -202,44 +202,44 @@ namespace lms::audio::taglib } else if (dynamic_cast(&file)) { - audioProperties.container = ContainerType::Ogg; - audioProperties.codec = CodecType::Opus; + audioProperties.container = core::media::ContainerType::Ogg; + audioProperties.codec = core::media::CodecType::Opus; } else if (dynamic_cast(&file)) { - audioProperties.container = ContainerType::Ogg; - audioProperties.codec = CodecType::Vorbis; + audioProperties.container = core::media::ContainerType::Ogg; + audioProperties.codec = core::media::CodecType::Vorbis; } else if (const auto* aiffFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::AIFF; - audioProperties.codec = CodecType::PCM; + audioProperties.container = core::media::ContainerType::AIFF; + audioProperties.codec = core::media::CodecType::PCM; audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample(); } else if (const auto* wavFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::WAV; - audioProperties.codec = CodecType::PCM; + audioProperties.container = core::media::ContainerType::WAV; + audioProperties.codec = core::media::CodecType::PCM; audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample(); } #if LMS_TAGLIB_HAS_SHORTEN else if (const auto* shortenFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::Shorten; - audioProperties.codec = CodecType::Shorten; + audioProperties.container = core::media::ContainerType::Shorten; + audioProperties.codec = core::media::CodecType::Shorten; audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample(); } #endif // LMS_TAGLIB_HAS_SHORTEN else if (const auto* trueAudioFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::TrueAudio; - audioProperties.codec = CodecType::TrueAudio; + audioProperties.container = core::media::ContainerType::TrueAudio; + audioProperties.codec = core::media::CodecType::TrueAudio; audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample(); } else if (const auto* wavPackFile{ dynamic_cast(&file) }) { - audioProperties.container = ContainerType::WavPack; - audioProperties.codec = CodecType::WavPack; + audioProperties.container = core::media::ContainerType::WavPack; + audioProperties.codec = core::media::CodecType::WavPack; audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample(); } else diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp index 4dca6f08..00d734ac 100644 --- a/src/libs/audio/impl/taglib/AudioFileInfo.hpp +++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp @@ -22,7 +22,7 @@ #include #include -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" #include "audio/IImageReader.hpp" diff --git a/src/libs/audio/impl/taglib/ImageReader.cpp b/src/libs/audio/impl/taglib/ImageReader.cpp index adad0a24..35378549 100644 --- a/src/libs/audio/impl/taglib/ImageReader.cpp +++ b/src/libs/audio/impl/taglib/ImageReader.cpp @@ -44,157 +44,157 @@ namespace lms::audio::taglib { namespace { - Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type) + core::media::ImageType imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type) { switch (type) { case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other: - return Image::Type::Other; + return core::media::ImageType::Other; case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: - return Image::Type::FileIcon; + return core::media::ImageType::FileIcon; case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: - return Image::Type::OtherFileIcon; + return core::media::ImageType::OtherFileIcon; case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: - return Image::Type::FrontCover; + return core::media::ImageType::FrontCover; case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: - return Image::Type::BackCover; + return core::media::ImageType::BackCover; case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: - return Image::Type::LeafletPage; + return core::media::ImageType::LeafletPage; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media: - return Image::Type::Media; + return core::media::ImageType::Media; case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: - return Image::Type::LeadArtist; + return core::media::ImageType::LeadArtist; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist: - return Image::Type::Artist; + return core::media::ImageType::Artist; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: - return Image::Type::Conductor; + return core::media::ImageType::Conductor; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band: - return Image::Type::Band; + return core::media::ImageType::Band; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer: - return Image::Type::Composer; + return core::media::ImageType::Composer; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: - return Image::Type::Lyricist; + return core::media::ImageType::Lyricist; case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: - return Image::Type::RecordingLocation; + return core::media::ImageType::RecordingLocation; case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: - return Image::Type::DuringRecording; + return core::media::ImageType::DuringRecording; case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: - return Image::Type::DuringPerformance; + return core::media::ImageType::DuringPerformance; case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: - return Image::Type::MovieScreenCapture; + return core::media::ImageType::MovieScreenCapture; case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: - return Image::Type::ColouredFish; + return core::media::ImageType::ColouredFish; case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: - return Image::Type::Illustration; + return core::media::ImageType::Illustration; case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: - return Image::Type::BandLogo; + return core::media::ImageType::BandLogo; case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: - return Image::Type::PublisherLogo; + return core::media::ImageType::PublisherLogo; } - return Image::Type::Unknown; + return core::media::ImageType::Unknown; } - Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type) + core::media::ImageType imageTypeFromfromASF(TagLib::ASF::Picture::Type type) { switch (type) { case ::TagLib::ASF::Picture::Type::Other: - return Image::Type::Other; + return core::media::ImageType::Other; case ::TagLib::ASF::Picture::Type::FileIcon: - return Image::Type::FileIcon; + return core::media::ImageType::FileIcon; case ::TagLib::ASF::Picture::Type::OtherFileIcon: - return Image::Type::OtherFileIcon; + return core::media::ImageType::OtherFileIcon; case ::TagLib::ASF::Picture::Type::FrontCover: - return Image::Type::FrontCover; + return core::media::ImageType::FrontCover; case ::TagLib::ASF::Picture::Type::BackCover: - return Image::Type::BackCover; + return core::media::ImageType::BackCover; case ::TagLib::ASF::Picture::Type::LeafletPage: - return Image::Type::LeafletPage; + return core::media::ImageType::LeafletPage; case ::TagLib::ASF::Picture::Type::Media: - return Image::Type::Media; + return core::media::ImageType::Media; case ::TagLib::ASF::Picture::Type::LeadArtist: - return Image::Type::LeadArtist; + return core::media::ImageType::LeadArtist; case ::TagLib::ASF::Picture::Type::Artist: - return Image::Type::Artist; + return core::media::ImageType::Artist; case ::TagLib::ASF::Picture::Type::Conductor: - return Image::Type::Conductor; + return core::media::ImageType::Conductor; case ::TagLib::ASF::Picture::Type::Band: - return Image::Type::Band; + return core::media::ImageType::Band; case ::TagLib::ASF::Picture::Type::Composer: - return Image::Type::Composer; + return core::media::ImageType::Composer; case ::TagLib::ASF::Picture::Type::Lyricist: - return Image::Type::Lyricist; + return core::media::ImageType::Lyricist; case ::TagLib::ASF::Picture::Type::RecordingLocation: - return Image::Type::RecordingLocation; + return core::media::ImageType::RecordingLocation; case ::TagLib::ASF::Picture::Type::DuringRecording: - return Image::Type::DuringRecording; + return core::media::ImageType::DuringRecording; case ::TagLib::ASF::Picture::Type::DuringPerformance: - return Image::Type::DuringPerformance; + return core::media::ImageType::DuringPerformance; case ::TagLib::ASF::Picture::Type::MovieScreenCapture: - return Image::Type::MovieScreenCapture; + return core::media::ImageType::MovieScreenCapture; case ::TagLib::ASF::Picture::Type::ColouredFish: - return Image::Type::ColouredFish; + return core::media::ImageType::ColouredFish; case ::TagLib::ASF::Picture::Type::Illustration: - return Image::Type::Illustration; + return core::media::ImageType::Illustration; case ::TagLib::ASF::Picture::Type::BandLogo: - return Image::Type::BandLogo; + return core::media::ImageType::BandLogo; case ::TagLib::ASF::Picture::Type::PublisherLogo: - return Image::Type::PublisherLogo; + return core::media::ImageType::PublisherLogo; } - return Image::Type::Unknown; + return core::media::ImageType::Unknown; } - Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type) + core::media::ImageType imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type) { switch (type) { case ::TagLib::FLAC::Picture::Type::Other: - return Image::Type::Other; + return core::media::ImageType::Other; case ::TagLib::FLAC::Picture::Type::FileIcon: - return Image::Type::FileIcon; + return core::media::ImageType::FileIcon; case ::TagLib::FLAC::Picture::Type::OtherFileIcon: - return Image::Type::OtherFileIcon; + return core::media::ImageType::OtherFileIcon; case ::TagLib::FLAC::Picture::Type::FrontCover: - return Image::Type::FrontCover; + return core::media::ImageType::FrontCover; case ::TagLib::FLAC::Picture::Type::BackCover: - return Image::Type::BackCover; + return core::media::ImageType::BackCover; case ::TagLib::FLAC::Picture::Type::LeafletPage: - return Image::Type::LeafletPage; + return core::media::ImageType::LeafletPage; case ::TagLib::FLAC::Picture::Type::Media: - return Image::Type::Media; + return core::media::ImageType::Media; case ::TagLib::FLAC::Picture::Type::LeadArtist: - return Image::Type::LeadArtist; + return core::media::ImageType::LeadArtist; case ::TagLib::FLAC::Picture::Type::Artist: - return Image::Type::Artist; + return core::media::ImageType::Artist; case ::TagLib::FLAC::Picture::Type::Conductor: - return Image::Type::Conductor; + return core::media::ImageType::Conductor; case ::TagLib::FLAC::Picture::Type::Band: - return Image::Type::Band; + return core::media::ImageType::Band; case ::TagLib::FLAC::Picture::Type::Composer: - return Image::Type::Composer; + return core::media::ImageType::Composer; case ::TagLib::FLAC::Picture::Type::Lyricist: - return Image::Type::Lyricist; + return core::media::ImageType::Lyricist; case ::TagLib::FLAC::Picture::Type::RecordingLocation: - return Image::Type::RecordingLocation; + return core::media::ImageType::RecordingLocation; case ::TagLib::FLAC::Picture::Type::DuringRecording: - return Image::Type::DuringRecording; + return core::media::ImageType::DuringRecording; case ::TagLib::FLAC::Picture::Type::DuringPerformance: - return Image::Type::DuringPerformance; + return core::media::ImageType::DuringPerformance; case ::TagLib::FLAC::Picture::Type::MovieScreenCapture: - return Image::Type::MovieScreenCapture; + return core::media::ImageType::MovieScreenCapture; case ::TagLib::FLAC::Picture::Type::ColouredFish: - return Image::Type::ColouredFish; + return core::media::ImageType::ColouredFish; case ::TagLib::FLAC::Picture::Type::Illustration: - return Image::Type::Illustration; + return core::media::ImageType::Illustration; case ::TagLib::FLAC::Picture::Type::BandLogo: - return Image::Type::BandLogo; + return core::media::ImageType::BandLogo; case ::TagLib::FLAC::Picture::Type::PublisherLogo: - return Image::Type::PublisherLogo; + return core::media::ImageType::PublisherLogo; } - return Image::Type::Unknown; + return core::media::ImageType::Unknown; } const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format) @@ -217,14 +217,14 @@ namespace lms::audio::taglib } #if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES - Image::Type imageTypeFromAPEPictureType(std::string_view pictureType) + core::media::ImageType imageTypeFromAPEPictureType(std::string_view pictureType) { if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front")) - return Image::Type::FrontCover; + return core::media::ImageType::FrontCover; if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) - return Image::Type::BackCover; + return core::media::ImageType::BackCover; - return Image::Type::Unknown; + return core::media::ImageType::Unknown; } #endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES @@ -294,7 +294,7 @@ namespace lms::audio::taglib image.data = pictureData; // By convention, consider the first cover art as the front cover - image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown; + image.type = firstCover ? core::media::ImageType::FrontCover : core::media::ImageType::Unknown; firstCover = false; visitor(image); diff --git a/src/libs/audio/impl/taglib/Utils.cpp b/src/libs/audio/impl/taglib/Utils.cpp index 121a2a6a..594a8b10 100644 --- a/src/libs/audio/impl/taglib/Utils.cpp +++ b/src/libs/audio/impl/taglib/Utils.cpp @@ -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 IOFileException{ "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 IOFileException{ "fileno failed", ec }; + throw IOFileException{ p, "fileno failed", ec }; } return TagLib::FileStream{ fd, true }; diff --git a/src/libs/audio/include/audio/AudioProperties.hpp b/src/libs/audio/include/audio/AudioProperties.hpp new file mode 100644 index 00000000..2b70ee7f --- /dev/null +++ b/src/libs/audio/include/audio/AudioProperties.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" + +namespace lms::audio +{ + struct AudioProperties + { + core::media::ContainerType container; + core::media::CodecType codec; + std::chrono::milliseconds duration; + unsigned bitrate; + unsigned channelCount; + unsigned sampleRate; + std::optional bitsPerSample; + }; +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/include/audio/Exception.hpp b/src/libs/audio/include/audio/Exception.hpp index 6c3c9e52..feaa9560 100644 --- a/src/libs/audio/include/audio/Exception.hpp +++ b/src/libs/audio/include/audio/Exception.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include "core/Exception.hpp" @@ -34,15 +35,18 @@ namespace lms::audio class IOFileException : public Exception { public: - IOFileException(std::string_view message, std::error_code err) - : Exception{ std::string{ message } + ": " + err.message() } + 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; }; diff --git a/src/libs/audio/include/audio/IImageReader.hpp b/src/libs/audio/include/audio/IImageReader.hpp index 0063151c..8d301b45 100644 --- a/src/libs/audio/include/audio/IImageReader.hpp +++ b/src/libs/audio/include/audio/IImageReader.hpp @@ -23,69 +23,18 @@ #include #include -#include "core/LiteralString.hpp" +#include "core/media/ImageType.hpp" namespace lms::audio { struct Image { - // See TagLib types (based on ID3v2 APIC types) - enum class Type - { - // No information - Unknown, - // A type not enumerated below - Other, - // 32x32 PNG image that should be used as the file icon - FileIcon, - // File icon of a different size or format - OtherFileIcon, - // Front cover image of the album - FrontCover, - // Back cover image of the album - BackCover, - // Inside leaflet page of the album - LeafletPage, - // Image from the album itself - Media, - // Picture of the lead artist or soloist - LeadArtist, - // Picture of the artist or performer - Artist, - // Picture of the conductor - Conductor, - // Picture of the band or orchestra - Band, - // Picture of the composer - Composer, - // Picture of the lyricist or text writer - Lyricist, - // Picture of the recording location or studio - RecordingLocation, - // Picture of the artists during recording - DuringRecording, - // Picture of the artists during performance - DuringPerformance, - // Picture from a movie or video related to the track - MovieScreenCapture, - // Picture of a large, coloured fish - ColouredFish, - // Illustration related to the track - Illustration, - // Logo of the band or performer - BandLogo, - // Logo of the publisher (record company) - PublisherLogo - }; - - Type type{ Type::Unknown }; + core::media::ImageType type{ core::media::ImageType::Unknown }; std::string mimeType{ "application/octet-stream" }; std::string description; std::span data; }; - core::LiteralString imageTypeToString(Image::Type type); - class IImageReader { public: diff --git a/src/libs/audio/include/audio/TranscodeTypes.hpp b/src/libs/audio/include/audio/TranscodeTypes.hpp index 1aaf34cf..a2324646 100644 --- a/src/libs/audio/include/audio/TranscodeTypes.hpp +++ b/src/libs/audio/include/audio/TranscodeTypes.hpp @@ -23,28 +23,30 @@ #include #include +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.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::ContainerType container; + core::media::CodecType codec; }; struct TranscodeOutputParameters { - std::optional format; + // Not setting a value here means to use the same value as the input + std::optional format; std::optional bitrate; std::optional bitsPerSample; std::optional channelCount; diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index d25b4264..0ddfff3a 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -13,6 +13,10 @@ configure_file( add_library(lmscore STATIC impl/http/Client.cpp impl/http/SendQueue.cpp + impl/media/CodecType.cpp + impl/media/ContainerType.cpp + impl/media/ImageType.cpp + impl/media/MimeType.cpp impl/ArchiveZipper.cpp impl/ChildProcess.cpp impl/ChildProcessManager.cpp diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/core/impl/media/CodecType.cpp similarity index 66% rename from src/libs/audio/impl/AudioTypes.cpp rename to src/libs/core/impl/media/CodecType.cpp index ad360bcb..b5b778f0 100644 --- a/src/libs/audio/impl/AudioTypes.cpp +++ b/src/libs/core/impl/media/CodecType.cpp @@ -17,45 +17,10 @@ * along with LMS. If not, see . */ -#include "audio/AudioTypes.hpp" +#include "core/media/CodecType.hpp" -namespace lms::audio +namespace lms::core::media { - 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) @@ -104,6 +69,6 @@ namespace lms::audio return "WMA9Lossless"; } - return ""; + return "Unknown"; } -} // namespace lms::audio \ No newline at end of file +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/impl/media/ContainerType.cpp b/src/libs/core/impl/media/ContainerType.cpp new file mode 100644 index 00000000..cf1fe447 --- /dev/null +++ b/src/libs/core/impl/media/ContainerType.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "core/media/ContainerType.hpp" + +namespace lms::core::media +{ + 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 "Unknown"; + } +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/audio/impl/ImageReader.cpp b/src/libs/core/impl/media/ImageType.cpp similarity index 59% rename from src/libs/audio/impl/ImageReader.cpp rename to src/libs/core/impl/media/ImageType.cpp index 7b314875..894f1150 100644 --- a/src/libs/audio/impl/ImageReader.cpp +++ b/src/libs/core/impl/media/ImageType.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * @@ -17,61 +17,60 @@ * along with LMS. If not, see . */ -#include "audio/IImageReader.hpp" +#include "core/media/ImageType.hpp" -namespace lms::audio +namespace lms::core::media { - core::LiteralString imageTypeToString(Image::Type type) + core::LiteralString imageTypeToString(ImageType type) { switch (type) { - case Image::Type::Other: + case ImageType::Other: return "Other"; - case Image::Type::FileIcon: + case ImageType::FileIcon: return "FileIcon"; - case Image::Type::OtherFileIcon: + case ImageType::OtherFileIcon: return "OtherFileIcon"; - case Image::Type::FrontCover: + case ImageType::FrontCover: return "FrontCover"; - case Image::Type::BackCover: + case ImageType::BackCover: return "BackCover"; - case Image::Type::LeafletPage: + case ImageType::LeafletPage: return "LeafletPage"; - case Image::Type::Media: + case ImageType::Media: return "Media"; - case Image::Type::LeadArtist: + case ImageType::LeadArtist: return "LeadArtist"; - case Image::Type::Artist: + case ImageType::Artist: return "Artist"; - case Image::Type::Conductor: + case ImageType::Conductor: return "Conductor"; - case Image::Type::Band: + case ImageType::Band: return "Band"; - case Image::Type::Composer: + case ImageType::Composer: return "Composer"; - case Image::Type::Lyricist: + case ImageType::Lyricist: return "Lyricist"; - case Image::Type::RecordingLocation: + case ImageType::RecordingLocation: return "RecordingLocation"; - case Image::Type::DuringRecording: + case ImageType::DuringRecording: return "DuringRecording"; - case Image::Type::DuringPerformance: + case ImageType::DuringPerformance: return "DuringPerformance"; - case Image::Type::MovieScreenCapture: + case ImageType::MovieScreenCapture: return "MovieScreenCapture"; - case Image::Type::ColouredFish: + case ImageType::ColouredFish: return "ColouredFish"; - case Image::Type::Illustration: + case ImageType::Illustration: return "Illustration"; - case Image::Type::BandLogo: + case ImageType::BandLogo: return "BandLogo"; - case Image::Type::PublisherLogo: + case ImageType::PublisherLogo: return "PublisherLogo"; - case Image::Type::Unknown: + case ImageType::Unknown: break; } return "Unknown"; } - -} // namespace lms::audio \ No newline at end of file +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/impl/media/MimeType.cpp b/src/libs/core/impl/media/MimeType.cpp new file mode 100644 index 00000000..aa8d1d1f --- /dev/null +++ b/src/libs/core/impl/media/MimeType.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "core/media/MimeType.hpp" + +namespace lms::core::media +{ + core::LiteralString getMimeType(ContainerType container, CodecType codec) + { + switch (container) + { + case ContainerType::AIFF: + return "audio/x-aiff"; + case ContainerType::APE: + return "audio/x-monkeys-audio"; + case ContainerType::ASF: + return "audio/x-ms-wma"; + case ContainerType::DSF: + return "audio/x-dsd-dsf"; + case ContainerType::FLAC: + return "audio/flac"; + case ContainerType::MP4: + switch (codec) + { + case CodecType::AAC: + return "audio/mp4; codecs=\"mp4a.40.2\""; + case CodecType::ALAC: + return "audio/mp4; codecs=\"alac\""; + case CodecType::MP4ALS: + return "audio/mp4; codecs=\"mp4als\""; + default: + return "audio/mp4"; + } + + case ContainerType::MPC: + return "audio/x-musepack"; + case ContainerType::MPEG: + return "audio/mpeg"; + case ContainerType::Ogg: + switch (codec) + { + case CodecType::Opus: + return "audio/opus"; + case CodecType::Vorbis: + return "audio/ogg; codecs=\"vorbis\""; + case CodecType::FLAC: + return "audio/ogg; codecs=\"flac\""; + default: + return "audio/ogg"; + } + + case ContainerType::Shorten: + return "audio/x-shn"; + case ContainerType::TrueAudio: + return "audio/x-tta"; + case ContainerType::WAV: + return "audio/wav"; + case ContainerType::WavPack: + return "audio/x-wavpack"; + } + + return "application/octet-stream"; + } +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/audio/include/audio/AudioTypes.hpp b/src/libs/core/include/core/media/CodecType.hpp similarity index 65% rename from src/libs/audio/include/audio/AudioTypes.hpp rename to src/libs/core/include/core/media/CodecType.hpp index 952f9c14..dac67e6d 100644 --- a/src/libs/audio/include/audio/AudioTypes.hpp +++ b/src/libs/core/include/core/media/CodecType.hpp @@ -19,32 +19,10 @@ #pragma once -#include -#include - #include "core/LiteralString.hpp" -namespace lms::audio +namespace lms::core::media { - enum class ContainerType - { - AIFF, - APE, // Monkey's Audio - ASF, // Advanced Systems Format - DSF, - FLAC, - MP4, - MPC, // Musepack - MPEG, - Ogg, - Shorten, - TrueAudio, - WAV, - WavPack, - }; - - core::LiteralString containerTypeToString(ContainerType type); - enum class CodecType { AAC, @@ -71,15 +49,4 @@ namespace lms::audio }; core::LiteralString codecTypeToString(CodecType type); - - struct AudioProperties - { - ContainerType container; - CodecType codec; - std::chrono::milliseconds duration; - unsigned bitrate; - unsigned channelCount; - unsigned sampleRate; - std::optional bitsPerSample; - }; -} // namespace lms::audio \ No newline at end of file +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/include/core/media/ContainerType.hpp b/src/libs/core/include/core/media/ContainerType.hpp new file mode 100644 index 00000000..709d8203 --- /dev/null +++ b/src/libs/core/include/core/media/ContainerType.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "core/LiteralString.hpp" + +namespace lms::core::media +{ + enum class 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); +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/include/core/media/ImageType.hpp b/src/libs/core/include/core/media/ImageType.hpp new file mode 100644 index 00000000..de710938 --- /dev/null +++ b/src/libs/core/include/core/media/ImageType.hpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "core/LiteralString.hpp" + +namespace lms::core::media +{ + // Based on ID3v2 APIC types + enum class ImageType + { + // No information + Unknown, + // A type not enumerated below + Other, + // 32x32 PNG image that should be used as the file icon + FileIcon, + // File icon of a different size or format + OtherFileIcon, + // Front cover image of the album + FrontCover, + // Back cover image of the album + BackCover, + // Inside leaflet page of the album + LeafletPage, + // Image from the album itself + Media, + // Picture of the lead artist or soloist + LeadArtist, + // Picture of the artist or performer + Artist, + // Picture of the conductor + Conductor, + // Picture of the band or orchestra + Band, + // Picture of the composer + Composer, + // Picture of the lyricist or text writer + Lyricist, + // Picture of the recording location or studio + RecordingLocation, + // Picture of the artists during recording + DuringRecording, + // Picture of the artists during performance + DuringPerformance, + // Picture from a movie or video related to the track + MovieScreenCapture, + // Picture of a large, coloured fish + ColouredFish, + // Illustration related to the track + Illustration, + // Logo of the band or performer + BandLogo, + // Logo of the publisher (record company) + PublisherLogo + }; + + core::LiteralString imageTypeToString(ImageType type); +} // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/include/core/media/MimeType.hpp b/src/libs/core/include/core/media/MimeType.hpp new file mode 100644 index 00000000..319d70de --- /dev/null +++ b/src/libs/core/include/core/media/MimeType.hpp @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "core/LiteralString.hpp" +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" + +namespace lms::core::media +{ + core::LiteralString getMimeType(ContainerType container, CodecType codec); +} \ No newline at end of file diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index c896269e..2ba86032 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(lmsdatabase STATIC + impl/objects/detail/Types.cpp impl/objects/Artist.cpp impl/objects/ArtistInfo.cpp impl/objects/Artwork.cpp diff --git a/src/libs/database/impl/objects/PodcastEpisode.cpp b/src/libs/database/impl/objects/PodcastEpisode.cpp index a0d52d91..a8aab5f2 100644 --- a/src/libs/database/impl/objects/PodcastEpisode.cpp +++ b/src/libs/database/impl/objects/PodcastEpisode.cpp @@ -27,6 +27,7 @@ #include "database/objects/Podcast.hpp" #include "Utils.hpp" +#include "detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/PathTraits.hpp" @@ -98,6 +99,16 @@ namespace lms::db utils::forEachQueryRangeResult(query, params.range, func); } + std::optional PodcastEpisode::getContainer() const + { + return detail::getMediaContainerType(_container); + } + + std::optional PodcastEpisode::getCodec() const + { + return detail::getMediaCodecType(_codec); + } + ObjectPtr PodcastEpisode::getArtwork() const { return _artwork; @@ -108,6 +119,16 @@ namespace lms::db return _artwork.id(); } + void PodcastEpisode::setContainer(core::media::ContainerType container) + { + _container = detail::getDbContainerType(container); + } + + void PodcastEpisode::setCodec(core::media::CodecType codec) + { + _codec = detail::getDbCodecType(codec); + } + void PodcastEpisode::setArtwork(ObjectPtr artwork) { _artwork = getDboPtr(artwork); diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index fab11b60..6f572a2c 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -40,6 +40,7 @@ #include "SqlQuery.hpp" #include "Utils.hpp" +#include "detail/Types.hpp" #include "traits/EnumSetTraits.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/PartialDateTimeTraits.hpp" @@ -679,12 +680,20 @@ namespace lms::db return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0")); } - std::vector Release::getCodecs() const + std::vector Release::getCodecs() const { assert(session()); // Get the codec ordered by frequency - return utils::fetchQueryResults(session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC")); + auto query{ session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") }; + + std::vector res; + utils::forEachQueryResult(query, [&](detail::CodecType codec) { + if (const auto mediaCodecType{ detail::getMediaCodecType(codec) }) + res.push_back(*mediaCodecType); + }); + + return res; } std::vector Release::getArtists(TrackArtistLinkType linkType) const diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index d42dcf48..5ad089b2 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -23,6 +23,7 @@ #include #include "core/ILogger.hpp" + #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -41,6 +42,7 @@ #include "SqlQuery.hpp" #include "Utils.hpp" +#include "objects/detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/PartialDateTimeTraits.hpp" #include "traits/PathTraits.hpp" @@ -523,6 +525,16 @@ namespace lms::db _absoluteFilePath = filePath; } + void Track::setContainer(core::media::ContainerType container) + { + _container = detail::getDbContainerType(container); + } + + void Track::setCodec(core::media::CodecType codec) + { + _codec = detail::getDbCodecType(codec); + } + void Track::setName(std::string_view name) { _name = std::string{ name, 0, _maxNameLength }; @@ -603,6 +615,16 @@ namespace lms::db _preferredMediaArtwork = getDboPtr(artwork); } + std::optional Track::getContainer() const + { + return detail::getMediaContainerType(_container); + } + + std::optional Track::getCodec() const + { + return detail::getMediaCodecType(_codec); + } + std::optional Track::getYear() const { return _date.getYear(); diff --git a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp index c6866f13..507ca4bb 100644 --- a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp +++ b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp @@ -28,6 +28,8 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "Utils.hpp" +#include "detail/Types.hpp" +#include "objects/detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/ImageHashTypeTraits.hpp" @@ -83,7 +85,7 @@ namespace lms::db } if (params.imageType.has_value()) - query.where("t_e_i_l.type = ?").bind(params.imageType.value()); + query.where("t_e_i_l.type = ?").bind(detail::getDbImageType(params.imageType.value())); } switch (params.sortMethod) diff --git a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp index e39b2965..4cd05a08 100644 --- a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp +++ b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp @@ -23,11 +23,11 @@ #include #include "database/Session.hpp" -#include "database/Types.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "Utils.hpp" +#include "detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/ImageHashTypeTraits.hpp" @@ -77,4 +77,14 @@ namespace lms::db { return _image; } + + core::media::ImageType TrackEmbeddedImageLink::getType() const + { + return detail::getMediaImageType(_type); + } + + void TrackEmbeddedImageLink::setType(core::media::ImageType type) + { + _type = detail::getDbImageType(type); + } } // namespace lms::db diff --git a/src/libs/database/impl/objects/Types.cpp b/src/libs/database/impl/objects/Types.cpp index 32388860..2ae8bede 100644 --- a/src/libs/database/impl/objects/Types.cpp +++ b/src/libs/database/impl/objects/Types.cpp @@ -72,96 +72,4 @@ namespace lms::db { return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); } - - core::LiteralString containerTypeToString(ContainerType type) - { - switch (type) - { - case ContainerType::Unknown: - break; - - 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 "Unknown"; - } - - core::LiteralString codecTypeToString(CodecType type) - { - switch (type) - { - case CodecType::Unknown: - break; - - case CodecType::AAC: - return "AAC"; - case CodecType::AC3: - return "AC3"; - case CodecType::ALAC: - return "ALAC"; - case CodecType::APE: - return "APE"; - case CodecType::DSD: - return "DSD"; - case CodecType::EAC3: - return "EAC3"; - 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 "Unknown"; - } } // namespace lms::db diff --git a/src/libs/database/impl/objects/detail/Types.cpp b/src/libs/database/impl/objects/detail/Types.cpp new file mode 100644 index 00000000..aa2573ed --- /dev/null +++ b/src/libs/database/impl/objects/detail/Types.cpp @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Types.hpp" + +namespace lms::db::detail +{ + std::optional getMediaContainerType(db::detail::ContainerType container) + { + switch (container) + { + case ContainerType::AIFF: + return core::media::ContainerType::AIFF; + case ContainerType::APE: + return core::media::ContainerType::APE; + case ContainerType::ASF: + return core::media::ContainerType::ASF; + case ContainerType::DSF: + return core::media::ContainerType::DSF; + case ContainerType::FLAC: + return core::media::ContainerType::FLAC; + case ContainerType::MP4: + return core::media::ContainerType::MP4; + case ContainerType::MPC: + return core::media::ContainerType::MPC; + case ContainerType::MPEG: + return core::media::ContainerType::MPEG; + case ContainerType::Ogg: + return core::media::ContainerType::Ogg; + case ContainerType::Shorten: + return core::media::ContainerType::Shorten; + case ContainerType::TrueAudio: + return core::media::ContainerType::TrueAudio; + case ContainerType::WAV: + return core::media::ContainerType::WAV; + case ContainerType::WavPack: + return core::media::ContainerType::WavPack; + + case ContainerType::Unknown: + break; + } + + return std::nullopt; + } + + db::detail::ContainerType getDbContainerType(core::media::ContainerType container) + { + switch (container) + { + case core::media::ContainerType::AIFF: + return ContainerType::AIFF; + case core::media::ContainerType::APE: + return ContainerType::APE; + case core::media::ContainerType::ASF: + return ContainerType::ASF; + case core::media::ContainerType::DSF: + return ContainerType::DSF; + case core::media::ContainerType::FLAC: + return ContainerType::FLAC; + case core::media::ContainerType::MP4: + return ContainerType::MP4; + case core::media::ContainerType::MPC: + return ContainerType::MPC; + case core::media::ContainerType::MPEG: + return ContainerType::MPEG; + case core::media::ContainerType::Ogg: + return ContainerType::Ogg; + case core::media::ContainerType::Shorten: + return ContainerType::Shorten; + case core::media::ContainerType::TrueAudio: + return ContainerType::TrueAudio; + case core::media::ContainerType::WAV: + return ContainerType::WAV; + case core::media::ContainerType::WavPack: + return ContainerType::WavPack; + } + + return ContainerType::Unknown; + } + + std::optional getMediaCodecType(db::detail::CodecType codec) + { + switch (codec) + { + case CodecType::AAC: + return core::media::CodecType::AAC; + case CodecType::AC3: + return core::media::CodecType::AC3; + case CodecType::ALAC: + return core::media::CodecType::ALAC; + case CodecType::APE: + return core::media::CodecType::APE; + case CodecType::DSD: + return core::media::CodecType::DSD; + case CodecType::EAC3: + return core::media::CodecType::EAC3; + case CodecType::FLAC: + return core::media::CodecType::FLAC; + case CodecType::MP3: + return core::media::CodecType::MP3; + case CodecType::MP4ALS: + return core::media::CodecType::MP4ALS; + case CodecType::MPC7: + return core::media::CodecType::MPC7; + case CodecType::MPC8: + return core::media::CodecType::MPC8; + case CodecType::Opus: + return core::media::CodecType::Opus; + case CodecType::PCM: + return core::media::CodecType::PCM; + case CodecType::Shorten: + return core::media::CodecType::Shorten; + case CodecType::TrueAudio: + return core::media::CodecType::TrueAudio; + case CodecType::Vorbis: + return core::media::CodecType::Vorbis; + case CodecType::WavPack: + return core::media::CodecType::WavPack; + case CodecType::WMA1: + return core::media::CodecType::WMA1; + case CodecType::WMA2: + return core::media::CodecType::WMA2; + case CodecType::WMA9Pro: + return core::media::CodecType::WMA9Pro; + case CodecType::WMA9Lossless: + return core::media::CodecType::WMA9Lossless; + + case CodecType::Unknown: + break; + } + + return std::nullopt; + } + + db::detail::CodecType getDbCodecType(core::media::CodecType codec) + { + switch (codec) + { + case core::media::CodecType::AAC: + return CodecType::AAC; + case core::media::CodecType::AC3: + return CodecType::AC3; + case core::media::CodecType::ALAC: + return CodecType::ALAC; + case core::media::CodecType::APE: + return CodecType::APE; + case core::media::CodecType::DSD: + return CodecType::DSD; + case core::media::CodecType::EAC3: + return CodecType::EAC3; + case core::media::CodecType::FLAC: + return CodecType::FLAC; + case core::media::CodecType::MP3: + return CodecType::MP3; + case core::media::CodecType::MP4ALS: + return CodecType::MP4ALS; + case core::media::CodecType::MPC7: + return CodecType::MPC7; + case core::media::CodecType::MPC8: + return CodecType::MPC8; + case core::media::CodecType::Opus: + return CodecType::Opus; + case core::media::CodecType::PCM: + return CodecType::PCM; + case core::media::CodecType::Shorten: + return CodecType::Shorten; + case core::media::CodecType::TrueAudio: + return CodecType::TrueAudio; + case core::media::CodecType::Vorbis: + return CodecType::Vorbis; + case core::media::CodecType::WavPack: + return CodecType::WavPack; + case core::media::CodecType::WMA1: + return CodecType::WMA1; + case core::media::CodecType::WMA2: + return CodecType::WMA2; + case core::media::CodecType::WMA9Pro: + return CodecType::WMA9Pro; + case core::media::CodecType::WMA9Lossless: + return CodecType::WMA9Lossless; + } + + return CodecType::Unknown; + } + + core::media::ImageType getMediaImageType(db::detail::ImageType type) + { + switch (type) + { + case db::detail::ImageType::Unknown: + return core::media::ImageType::Unknown; + case db::detail::ImageType::Other: + return core::media::ImageType::Other; + case db::detail::ImageType::FileIcon: + return core::media::ImageType::FileIcon; + case db::detail::ImageType::OtherFileIcon: + return core::media::ImageType::OtherFileIcon; + case db::detail::ImageType::FrontCover: + return core::media::ImageType::FrontCover; + case db::detail::ImageType::BackCover: + return core::media::ImageType::BackCover; + case db::detail::ImageType::LeafletPage: + return core::media::ImageType::LeafletPage; + case db::detail::ImageType::Media: + return core::media::ImageType::Media; + case db::detail::ImageType::LeadArtist: + return core::media::ImageType::LeadArtist; + case db::detail::ImageType::Artist: + return core::media::ImageType::Artist; + case db::detail::ImageType::Conductor: + return core::media::ImageType::Conductor; + case db::detail::ImageType::Band: + return core::media::ImageType::Band; + case db::detail::ImageType::Composer: + return core::media::ImageType::Composer; + case db::detail::ImageType::Lyricist: + return core::media::ImageType::Lyricist; + case db::detail::ImageType::RecordingLocation: + return core::media::ImageType::RecordingLocation; + case db::detail::ImageType::DuringRecording: + return core::media::ImageType::DuringRecording; + case db::detail::ImageType::DuringPerformance: + return core::media::ImageType::DuringPerformance; + case db::detail::ImageType::MovieScreenCapture: + return core::media::ImageType::MovieScreenCapture; + case db::detail::ImageType::ColouredFish: + return core::media::ImageType::ColouredFish; + case db::detail::ImageType::Illustration: + return core::media::ImageType::Illustration; + case db::detail::ImageType::BandLogo: + return core::media::ImageType::BandLogo; + case db::detail::ImageType::PublisherLogo: + return core::media::ImageType::PublisherLogo; + } + + return core::media::ImageType::Unknown; + } + + db::detail::ImageType getDbImageType(core::media::ImageType type) + { + switch (type) + { + case core::media::ImageType::Unknown: + return db::detail::ImageType::Unknown; + case core::media::ImageType::Other: + return db::detail::ImageType::Other; + case core::media::ImageType::FileIcon: + return db::detail::ImageType::FileIcon; + case core::media::ImageType::OtherFileIcon: + return db::detail::ImageType::OtherFileIcon; + case core::media::ImageType::FrontCover: + return db::detail::ImageType::FrontCover; + case core::media::ImageType::BackCover: + return db::detail::ImageType::BackCover; + case core::media::ImageType::LeafletPage: + return db::detail::ImageType::LeafletPage; + case core::media::ImageType::Media: + return db::detail::ImageType::Media; + case core::media::ImageType::LeadArtist: + return db::detail::ImageType::LeadArtist; + case core::media::ImageType::Artist: + return db::detail::ImageType::Artist; + case core::media::ImageType::Conductor: + return db::detail::ImageType::Conductor; + case core::media::ImageType::Band: + return db::detail::ImageType::Band; + case core::media::ImageType::Composer: + return db::detail::ImageType::Composer; + case core::media::ImageType::Lyricist: + return db::detail::ImageType::Lyricist; + case core::media::ImageType::RecordingLocation: + return db::detail::ImageType::RecordingLocation; + case core::media::ImageType::DuringRecording: + return db::detail::ImageType::DuringRecording; + case core::media::ImageType::DuringPerformance: + return db::detail::ImageType::DuringPerformance; + case core::media::ImageType::MovieScreenCapture: + return db::detail::ImageType::MovieScreenCapture; + case core::media::ImageType::ColouredFish: + return db::detail::ImageType::ColouredFish; + case core::media::ImageType::Illustration: + return db::detail::ImageType::Illustration; + case core::media::ImageType::BandLogo: + return db::detail::ImageType::BandLogo; + case core::media::ImageType::PublisherLogo: + return db::detail::ImageType::PublisherLogo; + } + + return db::detail::ImageType::Unknown; + } +} // namespace lms::db::detail \ No newline at end of file diff --git a/src/libs/database/impl/objects/detail/Types.hpp b/src/libs/database/impl/objects/detail/Types.hpp new file mode 100644 index 00000000..eed0b5e4 --- /dev/null +++ b/src/libs/database/impl/objects/detail/Types.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" +#include "core/media/ImageType.hpp" + +#include "database/objects/detail/Types.hpp" + +namespace lms::db::detail +{ + std::optional getMediaContainerType(db::detail::ContainerType container); + db::detail::ContainerType getDbContainerType(core::media::ContainerType container); + + std::optional getMediaCodecType(db::detail::CodecType codec); + db::detail::CodecType getDbCodecType(core::media::CodecType codec); + + core::media::ImageType getMediaImageType(db::detail::ImageType type); + db::detail::ImageType getDbImageType(core::media::ImageType type); +} // namespace lms::db::detail \ No newline at end of file diff --git a/src/libs/database/include/database/objects/PodcastEpisode.hpp b/src/libs/database/include/database/objects/PodcastEpisode.hpp index 0f1dc25c..7911375c 100644 --- a/src/libs/database/include/database/objects/PodcastEpisode.hpp +++ b/src/libs/database/include/database/objects/PodcastEpisode.hpp @@ -29,12 +29,16 @@ #include #include +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.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 { @@ -93,8 +97,8 @@ namespace lms::db // Audio properties std::chrono::milliseconds getDuration() const { return _duration; } - ContainerType getContainer() const { return _container; } - CodecType getCodec() const { return _codec; } + std::optional getContainer() const; + std::optional getCodec() const; std::size_t getBitrate() const { return _bitrate; } std::size_t getChannelCount() const { return _channelCount; } std::size_t getSampleRate() const { return _sampleRate; } @@ -124,8 +128,8 @@ namespace lms::db // Audio properties void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setContainer(ContainerType container) { _container = container; } - void setCodec(CodecType codec) { _codec = codec; } + void setContainer(core::media::ContainerType container); + void setCodec(core::media::CodecType 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; } @@ -189,8 +193,8 @@ namespace lms::db // Audio properties std::chrono::duration _duration{ 0 }; - ContainerType _container{ ContainerType::Unknown }; - CodecType _codec{ CodecType::Unknown }; + detail::ContainerType _container{ detail::ContainerType::Unknown }; + detail::CodecType _codec{ detail::CodecType::Unknown }; int _bitrate{}; // in bps int _channelCount{}; int _sampleRate{}; diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index f8b8a3d8..b2a078e4 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -31,6 +31,7 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" +#include "core/media/CodecType.hpp" #include "database/IdRange.hpp" #include "database/Object.hpp" @@ -279,7 +280,7 @@ namespace lms::db std::optional getCopyright() const; std::optional getCopyrightURL() const; std::size_t getMeanBitrate() const; - std::vector getCodecs() const; + std::vector getCodecs() const; // Accessors std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index 88f7e95f..b1d2d38a 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -34,6 +34,8 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" #include "database/IdRange.hpp" #include "database/Object.hpp" @@ -51,6 +53,7 @@ #include "database/objects/TrackListId.hpp" #include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" +#include "database/objects/detail/Types.hpp" namespace lms::db { @@ -229,8 +232,8 @@ namespace lms::db // Audio properties void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setContainer(ContainerType container) { _container = container; } - void setCodec(CodecType codec) { _codec = codec; } + void setContainer(core::media::ContainerType container); + void setCodec(core::media::CodecType 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; } @@ -275,8 +278,8 @@ namespace lms::db // Audio properties std::chrono::milliseconds getDuration() const { return _duration; } - ContainerType getContainer() const { return _container; } - CodecType getCodec() const { return _codec; } + std::optional getContainer() const; + std::optional getCodec() const; std::size_t getBitrate() const { return _bitrate; } std::size_t getChannelCount() const { return _channelCount; } std::size_t getSampleRate() const { return _sampleRate; } @@ -380,8 +383,8 @@ namespace lms::db // Audio properties std::chrono::duration _duration{}; - ContainerType _container{ ContainerType::Unknown }; - CodecType _codec{ CodecType::Unknown }; + detail::ContainerType _container{ detail::ContainerType::Unknown }; + detail::CodecType _codec{ detail::CodecType::Unknown }; int _bitrate{}; // in bps int _channelCount{}; int _sampleRate{}; diff --git a/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp index 4237ec84..2c4bf49f 100644 --- a/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp @@ -26,6 +26,8 @@ #include #include +#include "core/media/ImageType.hpp" + #include "database/Object.hpp" #include "database/Types.hpp" #include "database/objects/MediumId.hpp" @@ -53,7 +55,7 @@ namespace lms::db ReleaseId release; MediumId medium; TrackListId trackList; - std::optional imageType; + std::optional imageType; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; FindParameters& setRange(std::optional _range) @@ -81,7 +83,7 @@ namespace lms::db trackList = _trackList; return *this; } - FindParameters& setImageType(std::optional _imageType) + FindParameters& setImageType(std::optional _imageType) { imageType = _imageType; return *this; diff --git a/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp b/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp index 8334616c..c99f3e0c 100644 --- a/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp +++ b/src/libs/database/include/database/objects/TrackEmbeddedImageLink.hpp @@ -24,10 +24,13 @@ #include +#include "core/media/ImageType.hpp" + #include "database/Object.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 getTrack() const; ObjectPtr 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(index); } - void setType(ImageType type) { _type = type; } + void setType(core::media::ImageType type); void setDescription(std::string_view description) { _description = description; } template @@ -75,7 +78,7 @@ namespace lms::db static pointer create(Session& session, ObjectPtr track, ObjectPtr image); int _index{}; // index within the track - ImageType _type{ ImageType::Unknown }; + detail::ImageType _type{ detail::ImageType::Unknown }; std::string _description; Wt::Dbo::ptr _track; diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp index 6cdbf15a..47953b83 100644 --- a/src/libs/database/include/database/objects/Types.hpp +++ b/src/libs/database/include/database/objects/Types.hpp @@ -155,32 +155,6 @@ namespace lms::db 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 @@ -204,8 +178,8 @@ namespace lms::db MP3 = 1, OGG_OPUS = 2, OGG_VORBIS = 3, - WEBM_VORBIS = 4, - MATROSKA_OPUS = 5, + // WEBM_VORBIS = 4, + // MATROSKA_OPUS = 5, }; using Bitrate = std::uint32_t; @@ -266,54 +240,4 @@ namespace lms::db Clean = 2, Explicit = 3, }; - - enum class ContainerType - { - 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, - }; - - core::LiteralString containerTypeToString(ContainerType type); - - enum class CodecType - { - 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, - }; - - core::LiteralString codecTypeToString(CodecType type); } // namespace lms::db diff --git a/src/libs/database/include/database/objects/detail/Types.hpp b/src/libs/database/include/database/objects/detail/Types.hpp new file mode 100644 index 00000000..854a3c87 --- /dev/null +++ b/src/libs/database/include/database/objects/detail/Types.hpp @@ -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 . + */ + +#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 ContainerType + { + 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 CodecType + { + 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 diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 54f1419c..4b2757a0 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1229,22 +1229,22 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setCodec(CodecType::FLAC); + track1.get().modify()->setCodec(core::media::CodecType::FLAC); track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setCodec(CodecType::MP3); + track2.get().modify()->setCodec(core::media::CodecType::MP3); track2.get().modify()->setRelease(release1.get()); - track3.get().modify()->setCodec(CodecType::MP3); + track3.get().modify()->setCodec(core::media::CodecType::MP3); track3.get().modify()->setRelease(release1.get()); - track4.get().modify()->setCodec(CodecType::AAC); + track4.get().modify()->setCodec(core::media::CodecType::AAC); track4.get().modify()->setRelease(release1.get()); } { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(release1->getCodecs(), (std::vector{ CodecType::MP3, CodecType::FLAC, CodecType::AAC })); + EXPECT_EQ(release1->getCodecs(), (std::vector{ core::media::CodecType::MP3, core::media::CodecType::FLAC, core::media::CodecType::AAC })); } } diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp index 161e215c..61bcda1a 100644 --- a/src/libs/database/test/TrackEmbeddedImage.cpp +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -130,7 +130,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - link.get().modify()->setType(ImageType::FrontCover); + link.get().modify()->setType(core::media::ImageType::FrontCover); } { @@ -173,6 +173,30 @@ namespace lms::db::tests TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); EXPECT_TRUE(visited); } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setTrack(track.getId()); + params.setImageType(core::media::ImageType::FrontCover); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_TRUE(visited); + } + + { + auto transaction{ session.createReadTransaction() }; + + TrackEmbeddedImage::FindParameters params; + params.setTrack(track.getId()); + params.setImageType(core::media::ImageType::BackCover); + + bool visited{}; + TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); + EXPECT_FALSE(visited); + } } TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts) @@ -200,18 +224,18 @@ namespace lms::db::tests track1.get().modify()->setMedium(medium1.get()); track1.get().modify()->setTrackNumber(2); - link1.get().modify()->setType(ImageType::FrontCover); + link1.get().modify()->setType(core::media::ImageType::FrontCover); image1.get().modify()->setSize(750); - link2.get().modify()->setType(ImageType::Media); + link2.get().modify()->setType(core::media::ImageType::Media); image2.get().modify()->setSize(1000); - link3.get().modify()->setType(ImageType::Media); + link3.get().modify()->setType(core::media::ImageType::Media); image3.get().modify()->setSize(2000); track2.get().modify()->setRelease(release.get()); track2.get().modify()->setMedium(medium2.get()); track2.get().modify()->setTrackNumber(1); - link4.get().modify()->setType(ImageType::Media); + link4.get().modify()->setType(core::media::ImageType::Media); image4.get().modify()->setSize(1500); } @@ -220,7 +244,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setRelease(release.getId()); - params.setImageType(ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(TrackEmbeddedImageSortMethod::SizeDesc); std::vector visitedIds; @@ -236,7 +260,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setMedium(medium1.getId()); - params.setImageType(ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); std::vector visitedIds; @@ -251,7 +275,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setRelease(release.getId()); - params.setImageType(ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); std::vector visitedIds; @@ -267,7 +291,7 @@ namespace lms::db::tests TrackEmbeddedImage::FindParameters params; params.setRelease(release.getId()); - params.setImageType(ImageType::BackCover); + params.setImageType(core::media::ImageType::BackCover); params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); std::vector visitedIds; @@ -383,7 +407,7 @@ namespace lms::db::tests const TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; ASSERT_NE(link, TrackEmbeddedImageLink::pointer{}); EXPECT_EQ(link->getIndex(), 0); - EXPECT_EQ(link->getType(), ImageType::Unknown); + EXPECT_EQ(link->getType(), core::media::ImageType::Unknown); EXPECT_EQ(link->getDescription(), ""); EXPECT_EQ(link->getTrack(), track.get()); EXPECT_EQ(link->getImage(), image.get()); @@ -395,7 +419,7 @@ namespace lms::db::tests TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; ASSERT_NE(link, TrackEmbeddedImage::pointer{}); link.modify()->setIndex(2); - link.modify()->setType(ImageType::FrontCover); + link.modify()->setType(core::media::ImageType::FrontCover); link.modify()->setDescription("MyDesc"); } @@ -405,7 +429,7 @@ namespace lms::db::tests const TrackEmbeddedImageLink::pointer img{ TrackEmbeddedImageLink::find(session, imageLink.getId()) }; ASSERT_NE(img, TrackEmbeddedImage::pointer{}); EXPECT_EQ(img->getIndex(), 2); - EXPECT_EQ(img->getType(), ImageType::FrontCover); + EXPECT_EQ(img->getType(), core::media::ImageType::FrontCover); EXPECT_EQ(img->getDescription(), "MyDesc"); } @@ -415,7 +439,7 @@ namespace lms::db::tests bool visited{}; TrackEmbeddedImageLink::find(session, image->getId(), [&](const TrackEmbeddedImageLink::pointer& link) { EXPECT_EQ(link->getIndex(), 2); - EXPECT_EQ(link->getType(), ImageType::FrontCover); + EXPECT_EQ(link->getType(), core::media::ImageType::FrontCover); EXPECT_EQ(link->getDescription(), "MyDesc"); EXPECT_EQ(link->getTrack(), track.get()); diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp index 25b2ad7f..a3704984 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp @@ -26,7 +26,7 @@ #include "core/Service.hpp" #include "core/http/IClient.hpp" -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" @@ -42,92 +42,6 @@ namespace lms::podcast { namespace { - db::ContainerType audioContainerToDbContainer(audio::ContainerType type) - { - switch (type) - { - case audio::ContainerType::AIFF: - return db::ContainerType::AIFF; - case audio::ContainerType::APE: - return db::ContainerType::APE; - case audio::ContainerType::ASF: - return db::ContainerType::ASF; - case audio::ContainerType::DSF: - return db::ContainerType::DSF; - case audio::ContainerType::FLAC: - return db::ContainerType::FLAC; - case audio::ContainerType::MP4: - return db::ContainerType::MP4; - case audio::ContainerType::MPC: - return db::ContainerType::MPC; - case audio::ContainerType::MPEG: - return db::ContainerType::MPEG; - case audio::ContainerType::Shorten: - return db::ContainerType::Shorten; - case audio::ContainerType::Ogg: - return db::ContainerType::Ogg; - case audio::ContainerType::TrueAudio: - return db::ContainerType::TrueAudio; - case audio::ContainerType::WAV: - return db::ContainerType::WAV; - case audio::ContainerType::WavPack: - return db::ContainerType::WavPack; - } - - return db::ContainerType::Unknown; - } - - db::CodecType audioCodecToDbCodec(audio::CodecType type) - { - switch (type) - { - case audio::CodecType::AAC: - return db::CodecType::AAC; - case audio::CodecType::AC3: - return db::CodecType::AC3; - case audio::CodecType::ALAC: - return db::CodecType::ALAC; - case audio::CodecType::APE: - return db::CodecType::APE; - case audio::CodecType::DSD: - return db::CodecType::DSD; - case audio::CodecType::EAC3: - return db::CodecType::EAC3; - case audio::CodecType::FLAC: - return db::CodecType::FLAC; - case audio::CodecType::MP3: - return db::CodecType::MP3; - case audio::CodecType::MP4ALS: - return db::CodecType::MP4ALS; - case audio::CodecType::MPC7: - return db::CodecType::MPC7; - case audio::CodecType::MPC8: - return db::CodecType::MPC8; - case audio::CodecType::Opus: - return db::CodecType::Opus; - case audio::CodecType::PCM: - return db::CodecType::PCM; - case audio::CodecType::Shorten: - return db::CodecType::Shorten; - case audio::CodecType::TrueAudio: - return db::CodecType::TrueAudio; - case audio::CodecType::Vorbis: - return db::CodecType::Vorbis; - case audio::CodecType::WavPack: - return db::CodecType::WavPack; - case audio::CodecType::WMA1: - return db::CodecType::WMA1; - case audio::CodecType::WMA2: - return db::CodecType::WMA2; - case audio::CodecType::WMA9Pro: - return db::CodecType::WMA9Pro; - case audio::CodecType::WMA9Lossless: - return db::CodecType::WMA9Lossless; - } - - return db::CodecType::Unknown; - } - void updateEpisode(db::Session& session, db::PodcastEpisodeId episodeId, const std::filesystem::path& relativeFilePath, const audio::AudioProperties& audioProperties) { auto transaction{ session.createWriteTransaction() }; @@ -139,8 +53,8 @@ namespace lms::podcast dbEpisode.modify()->setAudioRelativeFilePath(relativeFilePath); dbEpisode.modify()->setDuration(audioProperties.duration); - dbEpisode.modify()->setContainer(audioContainerToDbContainer(audioProperties.container)); - dbEpisode.modify()->setCodec(audioCodecToDbCodec(audioProperties.codec)); + dbEpisode.modify()->setContainer(audioProperties.container); + dbEpisode.modify()->setCodec(audioProperties.codec); dbEpisode.modify()->setBitrate(audioProperties.bitrate); dbEpisode.modify()->setChannelCount(audioProperties.channelCount); dbEpisode.modify()->setSampleRate(audioProperties.sampleRate); diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index b189e793..51e94eb1 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -28,12 +28,9 @@ #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" -#include "image/Exception.hpp" -#include "image/Image.hpp" - +#include "audio/IImageReader.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" -#include "database/Types.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Cluster.hpp" @@ -47,6 +44,8 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackFeatures.hpp" #include "database/objects/TrackLyrics.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" #include "services/scanner/ScanErrors.hpp" @@ -296,145 +295,6 @@ namespace lms::scanner return dbLyrics; } - db::ImageType audioImageTypeToDbImageType(audio::Image::Type type) - { - switch (type) - { - case audio::Image::Type::Unknown: - return db::ImageType::Unknown; - case audio::Image::Type::Other: - return db::ImageType::Other; - case audio::Image::Type::FileIcon: - return db::ImageType::FileIcon; - case audio::Image::Type::OtherFileIcon: - return db::ImageType::OtherFileIcon; - case audio::Image::Type::FrontCover: - return db::ImageType::FrontCover; - case audio::Image::Type::BackCover: - return db::ImageType::BackCover; - case audio::Image::Type::LeafletPage: - return db::ImageType::LeafletPage; - case audio::Image::Type::Media: - return db::ImageType::Media; - case audio::Image::Type::LeadArtist: - return db::ImageType::LeadArtist; - case audio::Image::Type::Artist: - return db::ImageType::Artist; - case audio::Image::Type::Conductor: - return db::ImageType::Conductor; - case audio::Image::Type::Band: - return db::ImageType::Band; - case audio::Image::Type::Composer: - return db::ImageType::Composer; - case audio::Image::Type::Lyricist: - return db::ImageType::Lyricist; - case audio::Image::Type::RecordingLocation: - return db::ImageType::RecordingLocation; - case audio::Image::Type::DuringRecording: - return db::ImageType::DuringRecording; - case audio::Image::Type::DuringPerformance: - return db::ImageType::DuringPerformance; - case audio::Image::Type::MovieScreenCapture: - return db::ImageType::MovieScreenCapture; - case audio::Image::Type::ColouredFish: - return db::ImageType::ColouredFish; - case audio::Image::Type::Illustration: - return db::ImageType::Illustration; - case audio::Image::Type::BandLogo: - return db::ImageType::BandLogo; - case audio::Image::Type::PublisherLogo: - return db::ImageType::PublisherLogo; - } - - return db::ImageType::Unknown; - } - - db::ContainerType audioContainerToDbContainer(audio::ContainerType type) - { - switch (type) - { - case audio::ContainerType::AIFF: - return db::ContainerType::AIFF; - case audio::ContainerType::APE: - return db::ContainerType::APE; - case audio::ContainerType::ASF: - return db::ContainerType::ASF; - case audio::ContainerType::DSF: - return db::ContainerType::DSF; - case audio::ContainerType::FLAC: - return db::ContainerType::FLAC; - case audio::ContainerType::MP4: - return db::ContainerType::MP4; - case audio::ContainerType::MPC: - return db::ContainerType::MPC; - case audio::ContainerType::MPEG: - return db::ContainerType::MPEG; - case audio::ContainerType::Shorten: - return db::ContainerType::Shorten; - case audio::ContainerType::Ogg: - return db::ContainerType::Ogg; - case audio::ContainerType::TrueAudio: - return db::ContainerType::TrueAudio; - case audio::ContainerType::WAV: - return db::ContainerType::WAV; - case audio::ContainerType::WavPack: - return db::ContainerType::WavPack; - } - - return db::ContainerType::Unknown; - } - - db::CodecType audioCodecToDbCodec(audio::CodecType type) - { - switch (type) - { - case audio::CodecType::AAC: - return db::CodecType::AAC; - case audio::CodecType::AC3: - return db::CodecType::AC3; - case audio::CodecType::ALAC: - return db::CodecType::ALAC; - case audio::CodecType::APE: - return db::CodecType::APE; - case audio::CodecType::DSD: - return db::CodecType::DSD; - case audio::CodecType::EAC3: - return db::CodecType::EAC3; - case audio::CodecType::FLAC: - return db::CodecType::FLAC; - case audio::CodecType::MP3: - return db::CodecType::MP3; - case audio::CodecType::MP4ALS: - return db::CodecType::MP4ALS; - case audio::CodecType::MPC7: - return db::CodecType::MPC7; - case audio::CodecType::MPC8: - return db::CodecType::MPC8; - case audio::CodecType::Opus: - return db::CodecType::Opus; - case audio::CodecType::PCM: - return db::CodecType::PCM; - case audio::CodecType::Shorten: - return db::CodecType::Shorten; - case audio::CodecType::TrueAudio: - return db::CodecType::TrueAudio; - case audio::CodecType::Vorbis: - return db::CodecType::Vorbis; - case audio::CodecType::WavPack: - return db::CodecType::WavPack; - case audio::CodecType::WMA1: - return db::CodecType::WMA1; - case audio::CodecType::WMA2: - return db::CodecType::WMA2; - case audio::CodecType::WMA9Pro: - return db::CodecType::WMA9Pro; - case audio::CodecType::WMA9Lossless: - return db::CodecType::WMA9Lossless; - } - - return db::CodecType::Unknown; - } - db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo) { db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) }; @@ -458,7 +318,7 @@ namespace lms::scanner const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) }; db::TrackEmbeddedImageLink::pointer imageLink{ session.create(dbTrack, image) }; imageLink.modify()->setIndex(imageInfo.index); - imageLink.modify()->setType(audioImageTypeToDbImageType(imageInfo.type)); + imageLink.modify()->setType(imageInfo.type); imageLink.modify()->setDescription(imageInfo.description); return imageLink; @@ -654,7 +514,7 @@ namespace lms::scanner } catch (const audio::IOFileException& e) { - addError(getFilePath(), e.getErrorCode()); + addError(e.getPath(), e.getErrorCode()); } catch (const audio::Exception& e) { @@ -782,8 +642,8 @@ namespace lms::scanner // Audio properties track.modify()->setDuration(_file->audioProperties.duration); - track.modify()->setContainer(audioContainerToDbContainer(_file->audioProperties.container)); - track.modify()->setCodec(audioCodecToDbCodec(_file->audioProperties.codec)); + track.modify()->setContainer(_file->audioProperties.container); + track.modify()->setCodec(_file->audioProperties.codec); track.modify()->setBitrate(_file->audioProperties.bitrate); track.modify()->setChannelCount(_file->audioProperties.channelCount); track.modify()->setSampleRate(_file->audioProperties.sampleRate); diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp index 0cb62b57..d78f1bb0 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp @@ -21,8 +21,9 @@ #include -#include "audio/AudioTypes.hpp" -#include "audio/IImageReader.hpp" +#include "core/media/ImageType.hpp" + +#include "audio/AudioProperties.hpp" #include "image/Types.hpp" #include "scanners/FileScanOperationBase.hpp" @@ -44,7 +45,7 @@ namespace lms::scanner struct ImageInfo { std::size_t index; - audio::Image::Type type{ audio::Image::Type::Unknown }; + core::media::ImageType type{ core::media::ImageType::Unknown }; std::uint64_t hash{}; std::size_t size{}; image::ImageProperties properties; diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp index 331bf201..895e0a9f 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp @@ -125,7 +125,7 @@ namespace lms::scanner db::TrackEmbeddedImage::FindParameters params; params.setMedium(medium->getId()); - params.setImageType(db::ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& foundImage) { diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index b32b1ab5..5a91a074 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -140,7 +140,7 @@ namespace lms::scanner { db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); - params.setImageType(db::ImageType::FrontCover); + params.setImageType(core::media::ImageType::FrontCover); params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { if (!artwork) @@ -155,7 +155,7 @@ namespace lms::scanner { db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); - params.setImageType(db::ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { if (!artwork) @@ -167,7 +167,7 @@ namespace lms::scanner { db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); - params.setImageType(db::ImageType::Other); + params.setImageType(core::media::ImageType::Other); params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { if (!artwork) diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp index e24f97a9..592b0b56 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateTrackImages.cpp @@ -64,7 +64,7 @@ namespace lms::scanner { db::TrackEmbeddedImage::FindParameters params; params.setTrack(track->getId()); - params.setImageType(db::ImageType::FrontCover); + params.setImageType(core::media::ImageType::FrontCover); params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { @@ -94,7 +94,7 @@ namespace lms::scanner { db::TrackEmbeddedImage::FindParameters params; params.setTrack(track->getId()); - params.setImageType(db::ImageType::Media); + params.setImageType(core::media::ImageType::Media); params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { diff --git a/src/libs/services/transcoding/impl/TranscodeService.cpp b/src/libs/services/transcoding/impl/TranscodeService.cpp index f9b503b5..5108e8df 100644 --- a/src/libs/services/transcoding/impl/TranscodeService.cpp +++ b/src/libs/services/transcoding/impl/TranscodeService.cpp @@ -19,11 +19,7 @@ #include "TranscodeService.hpp" -#include "audio/ITranscoder.hpp" #include "core/ILogger.hpp" -#include "core/UUID.hpp" -#include "database/IDb.hpp" -#include "database/Session.hpp" #include "TranscodeResourceHandler.hpp" @@ -61,10 +57,10 @@ namespace lms::transcoding if (estimateContentLength) { - if (parameters.inputParameters.offset < parameters.inputParameters.duration) - estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset); + if (parameters.inputParameters.offset < parameters.inputParameters.audioProperties.duration) + estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.audioProperties.duration - parameters.inputParameters.offset); else - LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.duration << ": not estimating content length"); + LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.audioProperties.duration << ": not estimating content length"); } return std::make_unique(parameters, estimatedContentLength); diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 91ff9f36..f31c9dc7 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -26,8 +26,9 @@ #include "core/ILogger.hpp" #include "core/IResourceHandler.hpp" #include "core/String.hpp" +#include "core/media/MimeType.hpp" -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" @@ -37,6 +38,7 @@ #include "database/objects/PodcastEpisodeId.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Types.hpp" #include "database/objects/User.hpp" #include "services/artwork/IArtworkService.hpp" @@ -54,84 +56,73 @@ namespace lms::api::subsonic { namespace { - std::optional subsonicStreamFormatToTranscodingOutputFormat(std::string_view format) + struct OutputFormat { - for (const auto& [str, avFormat] : std::initializer_list>{ - { "mp3", audio::OutputFormat::MP3 }, - { "opus", audio::OutputFormat::OGG_OPUS }, - { "vorbis", audio::OutputFormat::OGG_VORBIS }, - }) - { - if (core::stringUtils::stringCaseInsensitiveEqual(str, format)) - return avFormat; - } - return std::nullopt; + std::string name; + core::media::ContainerType container; + core::media::CodecType codec; + }; + constexpr std::array outputFormats{ + OutputFormat{ "mp3", core::media::ContainerType::MPEG, core::media::CodecType::MP3 }, + OutputFormat{ "opus", core::media::ContainerType::Ogg, core::media::CodecType::Opus }, + OutputFormat{ "vorbis", core::media::ContainerType::Ogg, core::media::CodecType::Vorbis }, + }; + + std::optional getOutputFormatByName(std::string_view format) + { + std::optional res; + + const auto itFormat{ std::find_if(outputFormats.begin(), outputFormats.end(), [&format, &res](const OutputFormat& outputFormat) { + if (core::stringUtils::stringCaseInsensitiveEqual(format, outputFormat.name)) + { + res = outputFormat; + return true; + } + return false; + }) }; + if (itFormat != outputFormats.end()) + res = *itFormat; + + return res; } - audio::OutputFormat userTranscodeFormatToTranscodingFormat(db::TranscodingOutputFormat format) + std::optional userTranscodeFormatToOutputFormat(db::TranscodingOutputFormat format) { + std::optional res; + switch (format) { case db::TranscodingOutputFormat::MP3: - return audio::OutputFormat::MP3; + res = getOutputFormatByName("mp3"); + break; case db::TranscodingOutputFormat::OGG_OPUS: - return audio::OutputFormat::OGG_OPUS; - case db::TranscodingOutputFormat::MATROSKA_OPUS: - return audio::OutputFormat::MATROSKA_OPUS; + res = getOutputFormatByName("opus"); + break; case db::TranscodingOutputFormat::OGG_VORBIS: - return audio::OutputFormat::OGG_VORBIS; - case db::TranscodingOutputFormat::WEBM_VORBIS: - return audio::OutputFormat::WEBM_VORBIS; + res = getOutputFormatByName("vorbis"); + break; } - return audio::OutputFormat::OGG_OPUS; + return res; } - bool isCodecCompatibleWithOutputFormat(audio::CodecType codec, audio::OutputFormat outputFormat) + audio::AudioProperties getAudioProperties(const std::filesystem::path& trackPath) { - switch (outputFormat) - { - case audio::OutputFormat::MP3: - return codec == audio::CodecType::MP3; - - case audio::OutputFormat::OGG_OPUS: - case audio::OutputFormat::MATROSKA_OPUS: - return codec == audio::CodecType::Opus; - - case audio::OutputFormat::OGG_VORBIS: - case audio::OutputFormat::WEBM_VORBIS: - return codec == audio::CodecType::Vorbis; - } - - return true; - } - - struct StreamParameters - { - std::filesystem::path filePath; - std::string fileMimeType; // set if known - std::optional transcodeParameters; - bool estimateContentLength{}; - }; - - bool isOutputFormatCompatible(const std::filesystem::path& trackPath, audio::OutputFormat outputFormat) - { - // TODO: put this information in db during scan - // It is in base only for tracks, not yet for podcasts try { const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; audio::AudioFileInfoParseOptions parseOptions; - parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only coded needed + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; parseOptions.readImages = false; parseOptions.readTags = false; const auto audioFile{ parser->parse(trackPath, parseOptions) }; - if (!audioFile->getAudioProperties()) + const audio::AudioProperties* properties{ audioFile->getAudioProperties() }; + if (!properties) throw RequestedDataNotFoundError{}; - return isCodecCompatibleWithOutputFormat(audioFile->getAudioProperties()->codec, outputFormat); + return *properties; } catch (const audio::Exception& e) { @@ -143,9 +134,7 @@ namespace lms::api::subsonic struct AudioFileInfo { std::filesystem::path path; - std::chrono::milliseconds duration{}; - std::size_t bitrate{}; - std::string mimeType; // set if known + audio::AudioProperties audioProperties; }; AudioFileInfo getAudioFileInfo(db::Session& session, AudioFileId audioFileId) @@ -161,8 +150,20 @@ namespace lms::api::subsonic throw RequestedDataNotFoundError{}; res.path = track->getAbsoluteFilePath(); - res.duration = track->getDuration(); - res.bitrate = track->getBitrate(); + if (track->getContainer() && track->getCodec()) + { + res.audioProperties.container = *track->getContainer(); + res.audioProperties.codec = *track->getCodec(); + res.audioProperties.duration = track->getDuration(); + res.audioProperties.bitrate = track->getBitrate(); + res.audioProperties.channelCount = track->getChannelCount(); + res.audioProperties.sampleRate = track->getSampleRate(); + res.audioProperties.bitsPerSample = track->getBitsPerSample(); + } + else + { + res.audioProperties = getAudioProperties(res.path); + } } else if (const db::PodcastEpisodeId * episodeId{ std::get_if(&audioFileId) }) { @@ -173,14 +174,33 @@ namespace lms::api::subsonic std::filesystem::path podcastCachePath{ core::Service::get()->getCachePath() }; res.path = podcastCachePath / episode->getAudioRelativeFilePath(); - res.duration = episode->getDuration(); - res.bitrate = episode->getEnclosureLength() / std::chrono::duration_cast(episode->getDuration()).count() * 8; - res.mimeType = episode->getEnclosureContentType(); + if (episode->getContainer() && episode->getCodec()) + { + res.audioProperties.container = *episode->getContainer(); + res.audioProperties.codec = *episode->getCodec(); + res.audioProperties.duration = episode->getDuration(); + res.audioProperties.bitrate = episode->getBitrate(); + res.audioProperties.channelCount = episode->getChannelCount(); + res.audioProperties.sampleRate = episode->getSampleRate(); + res.audioProperties.bitsPerSample = episode->getBitsPerSample(); + } + else + { + res.audioProperties = getAudioProperties(res.path); + } } return res; } + struct StreamParameters + { + std::filesystem::path filePath; + audio::AudioProperties audioProperties; + std::optional transcodeParameters; + bool estimateContentLength{}; + }; + StreamParameters getStreamParameters(RequestContext& context) { // Mandatory params @@ -201,32 +221,31 @@ namespace lms::api::subsonic StreamParameters parameters; parameters.filePath = audioFileInfo.path; - parameters.fileMimeType = audioFileInfo.mimeType; parameters.estimateContentLength = estimateContentLength; if (format == "raw") // raw => no transcoding return parameters; // TODO: what if offset is not 0? - std::optional requestedFormat{ subsonicStreamFormatToTranscodingOutputFormat(format) }; + std::optional requestedFormat{ getOutputFormatByName(format) }; if (!requestedFormat) { if (context.getUser()->getSubsonicEnableTranscodingByDefault()) - requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); + requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); } - if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.bitrate <= maxBitRate)) + if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate)) { LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate is compatible with parameters => no transcoding"); return parameters; // no transcoding needed } - // scan the file to check if its format is compatible with the actual requested format + // Check if the input file is compatible with the actual requested format // same codec => apply max bitrate // otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate) std::size_t bitrate{}; - if (requestedFormat && isOutputFormatCompatible(audioFileInfo.path, *requestedFormat)) + if (requestedFormat && requestedFormat->container == audioFileInfo.audioProperties.container && requestedFormat->codec == audioFileInfo.audioProperties.codec) { - if (maxBitRate == 0 || audioFileInfo.bitrate <= maxBitRate) + if (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate) { LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate and format are compatible with parameters => no transcoding"); return parameters; // no transcoding needed @@ -235,22 +254,24 @@ namespace lms::api::subsonic } // Need to transcode here - if (!requestedFormat) - requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); - if (!bitrate) - bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate(); - if (maxBitRate) - bitrate = std::min(bitrate, maxBitRate); - audio::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; + if (!requestedFormat) // no format provided => use user's default + requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); + if (!bitrate) // no bitrate provided => use user's default + bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate(); + if (maxBitRate) // but still honor provided maxBitRate, if any + bitrate = std::min(bitrate, maxBitRate); + transcodeParameters.inputParameters.filePath = audioFileInfo.path; - transcodeParameters.inputParameters.duration = audioFileInfo.duration; + transcodeParameters.inputParameters.audioProperties = audioFileInfo.audioProperties; transcodeParameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; - ; transcodeParameters.outputParameters.bitrate = bitrate; - transcodeParameters.outputParameters.format = *requestedFormat; + transcodeParameters.outputParameters.format.emplace(); + transcodeParameters.outputParameters.format->container = requestedFormat->container; + transcodeParameters.outputParameters.format->codec = requestedFormat->codec; + transcodeParameters.outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) return parameters; @@ -371,7 +392,7 @@ namespace lms::api::subsonic if (streamParameters.transcodeParameters) resourceHandler = core::Service::get()->createTranscodeResourceHandler(*streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = core::createFileResourceHandler(streamParameters.filePath, streamParameters.fileMimeType); + resourceHandler = core::createFileResourceHandler(streamParameters.filePath, core::media::getMimeType(streamParameters.audioProperties.container, streamParameters.audioProperties.codec).str()); } else { diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 2a23f26b..b7a44fda 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -51,31 +51,25 @@ namespace lms::api::subsonic { - using namespace db; - namespace { - std::string_view formatToSuffix(TranscodingOutputFormat format) + std::string_view formatToSuffix(db::TranscodingOutputFormat format) { switch (format) { - case TranscodingOutputFormat::MP3: + case db::TranscodingOutputFormat::MP3: return "mp3"; - case TranscodingOutputFormat::OGG_OPUS: + case db::TranscodingOutputFormat::OGG_OPUS: return "opus"; - case TranscodingOutputFormat::MATROSKA_OPUS: - return "mka"; - case TranscodingOutputFormat::OGG_VORBIS: + case db::TranscodingOutputFormat::OGG_VORBIS: return "ogg"; - case TranscodingOutputFormat::WEBM_VORBIS: - return "webm"; } return ""; } } // namespace - Response::Node createSongNode(RequestContext& context, const Track::pointer& track, bool id3) + Response::Node createSongNode(RequestContext& context, const db::Track::pointer& track, bool id3) { LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong"); @@ -136,7 +130,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("coverArt", idToString(coverArtId)); } - const std::vector& artists{ track->getArtists({ TrackArtistLinkType::Artist }) }; + const std::vector& artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) }; if (!artists.empty()) { if (!track->getArtistDisplayName().empty()) @@ -148,7 +142,7 @@ namespace lms::api::subsonic trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); } - const Release::pointer release{ track->getRelease() }; + const db::Release::pointer release{ track->getRelease() }; if (release) { trackResponse.setAttribute("album", release->getName()); @@ -167,13 +161,13 @@ namespace lms::api::subsonic trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); // Report the first GENRE for this track - std::vector genres; + std::vector genres; { - Cluster::FindParameters params; + db::Cluster::FindParameters params; params.setTrack(track->getId()); params.setClusterTypeName("GENRE"); - genres = Cluster::find(context.getDbSession(), params).results; + genres = db::Cluster::find(context.getDbSession(), params).results; if (!genres.empty()) trackResponse.setAttribute("genre", genres.front()->getName()); } @@ -204,13 +198,13 @@ namespace lms::api::subsonic trackResponse.createEmptyArrayChild("artists"); trackResponse.createEmptyArrayChild("contributors"); - TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist) { + db::TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const db::TrackArtistLink::pointer& link, const db::Artist::pointer& artist) { switch (link->getType()) { - case TrackArtistLinkType::Artist: + case db::TrackArtistLinkType::Artist: trackResponse.addArrayChild("artists", createArtistNode(artist)); break; - case TrackArtistLinkType::ReleaseArtist: + case db::TrackArtistLinkType::ReleaseArtist: trackResponse.addArrayChild("albumartists", createArtistNode(artist)); break; default: @@ -226,11 +220,11 @@ namespace lms::api::subsonic auto addClusters{ [&](Response::Node::Key field, std::string_view clusterTypeName) { trackResponse.createEmptyArrayValue(field); - Cluster::FindParameters params; + db::Cluster::FindParameters params; params.setTrack(track->getId()); params.setClusterTypeName(clusterTypeName); - for (const auto& cluster : Cluster::find(context.getDbSession(), params).results) + for (const auto& cluster : db::Cluster::find(context.getDbSession(), params).results) trackResponse.addArrayValue(field, cluster->getName()); } }; diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 6fa7e23f..6155327d 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -101,9 +101,7 @@ namespace lms::ui { case MediaPlayer::Format::MP3: case MediaPlayer::Format::OGG_OPUS: - case MediaPlayer::Format::MATROSKA_OPUS: case MediaPlayer::Format::OGG_VORBIS: - case MediaPlayer::Format::WEBM_VORBIS: return format; } diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 94a3078a..8707d4a7 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -478,9 +478,7 @@ namespace lms::ui _transcodingOutputFormatModel = std::make_shared>(); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS); - _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), db::TranscodingOutputFormat::MATROSKA_OPUS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS); - _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), db::TranscodingOutputFormat::WEBM_VORBIS); _replayGainModeModel = std::make_shared(); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 3f6e0bb2..56ffb17f 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -142,11 +142,11 @@ namespace lms::ui // Codecs { std::string codecStr; - for (db::CodecType codec : release->getCodecs()) + for (core::media::CodecType codec : release->getCodecs()) { if (!codecStr.empty()) codecStr += " ยท "; - codecStr += db::codecTypeToString(codec).str(); + codecStr += core::media::codecTypeToString(codec).str(); } if (!codecStr.empty()) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 6bead9d7..c83d3be9 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -127,8 +127,11 @@ namespace lms::ui::TrackListHelpers } } - trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", db::codecTypeToString(track->getCodec()).c_str(), Wt::TextFormat::Plain); + if (const auto codec{ track->getCodec() }) + { + trackInfo->setCondition("if-has-codec", true); + trackInfo->bindString("codec", core::media::codecTypeToString(*codec).c_str(), Wt::TextFormat::Plain); + } trackInfo->bindString("duration", utils::durationToString(track->getDuration())); if (track->getBitrate()) diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index a5977b8c..dd8f434d 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -27,6 +27,11 @@ #include "core/IResourceHandler.hpp" #include "core/Service.hpp" #include "core/String.hpp" + +#include "audio/AudioProperties.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" #include "database/Session.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" @@ -51,14 +56,8 @@ namespace lms::core::stringUtils switch (static_cast(*encodedFormat)) { case db::TranscodingOutputFormat::MP3: - [[fallthrough]]; case db::TranscodingOutputFormat::OGG_OPUS: - [[fallthrough]]; - case db::TranscodingOutputFormat::MATROSKA_OPUS: - [[fallthrough]]; case db::TranscodingOutputFormat::OGG_VORBIS: - [[fallthrough]]; - case db::TranscodingOutputFormat::WEBM_VORBIS: return format; } @@ -72,20 +71,64 @@ namespace lms::ui { namespace { - std::optional AudioFormatToTranscodingFormat(db::TranscodingOutputFormat format) + std::optional getAudioProperties(const std::filesystem::path& trackPath) + { + std::optional res; + + try + { + const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; + + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; + parseOptions.readImages = false; + parseOptions.readTags = false; + const auto audioFile{ parser->parse(trackPath, parseOptions) }; + + if (const audio::AudioProperties * properties{ audioFile->getAudioProperties() }) + res = *properties; + } + catch (const audio::Exception& e) + { + LMS_LOG(UI, DEBUG, "Cannot parse audio properties in " << trackPath << ": " << e.what()); + } + + return res; + } + + std::optional getAudioProperties(const db::Track::pointer& track) + { + std::optional res; + + if (track->getContainer() && track->getCodec()) + { + res.emplace(); + res->container = *track->getContainer(); + res->codec = *track->getCodec(); + res->duration = track->getDuration(); + res->bitrate = track->getBitrate(); + res->channelCount = track->getChannelCount(); + res->sampleRate = track->getSampleRate(); + res->bitsPerSample = track->getBitsPerSample(); + } + else + { + res = getAudioProperties(track->getAbsoluteFilePath()); + } + + return res; + } + + std::optional AudioFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return audio::OutputFormat::MP3; + return audio::TranscodeOutputFormat{ core::media::ContainerType::MPEG, core::media::CodecType::MP3 }; case db::TranscodingOutputFormat::OGG_OPUS: - return audio::OutputFormat::OGG_OPUS; - case db::TranscodingOutputFormat::MATROSKA_OPUS: - return audio::OutputFormat::MATROSKA_OPUS; + return audio::TranscodeOutputFormat{ core::media::ContainerType::Ogg, core::media::CodecType::Opus }; case db::TranscodingOutputFormat::OGG_VORBIS: - return audio::OutputFormat::OGG_VORBIS; - case db::TranscodingOutputFormat::WEBM_VORBIS: - return audio::OutputFormat::WEBM_VORBIS; + return audio::TranscodeOutputFormat{ core::media::ContainerType::Ogg, core::media::CodecType::Vorbis }; } TRANSCODE_LOG(ERROR, "Cannot convert from db audio format to transcoding format"); @@ -128,8 +171,8 @@ namespace lms::ui return std::nullopt; } - const std::optional avFormat{ AudioFormatToTranscodingFormat(*format) }; - if (!avFormat) + const std::optional outputFormat{ AudioFormatToTranscodingFormat(*format) }; + if (!outputFormat) return std::nullopt; // optional parameter @@ -145,12 +188,16 @@ namespace lms::ui return std::nullopt; parameters.inputParameters.filePath = track->getAbsoluteFilePath(); - parameters.inputParameters.duration = track->getDuration(); + const auto audioProperties{ getAudioProperties(track) }; + if (!audioProperties) + return std::nullopt; + + parameters.inputParameters.audioProperties = *audioProperties; } parameters.inputParameters.offset = std::chrono::seconds{ offset }; parameters.outputParameters.stripMetadata = true; - parameters.outputParameters.format = *avFormat; + parameters.outputParameters.format = *outputFormat; parameters.outputParameters.bitrate = *bitrate; return parameters; diff --git a/src/tools/audioinfo/LmsAudioInfo.cpp b/src/tools/audioinfo/LmsAudioInfo.cpp index d7df8362..2044890f 100644 --- a/src/tools/audioinfo/LmsAudioInfo.cpp +++ b/src/tools/audioinfo/LmsAudioInfo.cpp @@ -27,8 +27,9 @@ #include "core/ILogger.hpp" #include "core/String.hpp" +#include "core/media/MimeType.hpp" -#include "audio/AudioTypes.hpp" +#include "audio/AudioProperties.hpp" #include "audio/Exception.hpp" #include "audio/IAudioFileInfo.hpp" #include "audio/IAudioFileInfoParser.hpp" @@ -39,14 +40,15 @@ namespace lms::audio { std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) { - os << "\tDuration: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast>(audioProperties.duration) << std::endl; - os << "\tContainer: " << containerTypeToString(audioProperties.container) << std::endl; - os << "\tCodec: " << codecTypeToString(audioProperties.codec) << std::endl; - os << "\tBitrate: " << audioProperties.bitrate << " bps" << std::endl; + os << "\tDuration: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast>(audioProperties.duration) << '\n'; + os << "\tContainer: " << core::media::containerTypeToString(audioProperties.container) << '\n'; + os << "\tCodec: " << core::media::codecTypeToString(audioProperties.codec) << '\n'; + os << "\tMimeType: " << core::media::getMimeType(audioProperties.container, audioProperties.codec) << '\n'; + os << "\tBitrate: " << audioProperties.bitrate << " bps" << '\n'; if (audioProperties.bitsPerSample) - os << "\tBitsPerSample: " << *audioProperties.bitsPerSample << std::endl; - os << "\tChannelCount: " << audioProperties.channelCount << std::endl; - os << "\tSampleRate: " << audioProperties.sampleRate << std::endl; + os << "\tBitsPerSample: " << *audioProperties.bitsPerSample << '\n'; + os << "\tChannelCount: " << audioProperties.channelCount << '\n'; + os << "\tSampleRate: " << audioProperties.sampleRate << '\n'; return os; } @@ -115,7 +117,7 @@ namespace lms::audio std::ostream& operator<<(std::ostream& os, const Image& image) { - os << "\ttype = " << imageTypeToString(image.type) << std::endl; + os << "\ttype = " << core::media::imageTypeToString(image.type) << std::endl; if (!image.description.empty()) os << "\tdesc: " << image.description << std::endl; os << "\tmimeType: " << image.mimeType << std::endl; From 4ff309175767bf261cb7e063eac4d58c97e709d7 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Dec 2025 19:42:15 +0100 Subject: [PATCH 08/22] Bumped version to 3.73.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 29844248..bc0ce574 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) -project(lms VERSION 3.72.1) +project(lms VERSION 3.73.0) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/) From 80db76c3e3fec87e66e0927b545d4a6d24062c66 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Dec 2025 22:17:58 +0100 Subject: [PATCH 09/22] OpenSubsonic API: added support for the indexBasedQueue extension --- .../include/database/objects/PlayQueue.hpp | 3 +- src/libs/subsonic/impl/SubsonicResource.cpp | 2 + .../subsonic/impl/endpoints/Bookmarks.cpp | 161 +++++++++++------- .../subsonic/impl/endpoints/Bookmarks.hpp | 2 + src/libs/subsonic/impl/endpoints/System.cpp | 6 + src/libs/subsonic/impl/responses/Bookmark.cpp | 2 +- 6 files changed, 115 insertions(+), 61 deletions(-) diff --git a/src/libs/database/include/database/objects/PlayQueue.hpp b/src/libs/database/include/database/objects/PlayQueue.hpp index 845db082..981b603f 100644 --- a/src/libs/database/include/database/objects/PlayQueue.hpp +++ b/src/libs/database/include/database/objects/PlayQueue.hpp @@ -59,7 +59,8 @@ namespace lms::db // Get tracks, ordered by position std::vector getTrackIds() const; void visitTracks(const std::function& track)>& visitor) const; - const Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; } + Wt::WDateTime getLastModifiedDateTime() const { return _lastModifiedDateTime; } + bool isEmpty() const { return _tracks.empty(); } // Modifiers void setCurrentIndex(std::size_t index) { _currentIndex = index; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index ac1b4316..32a30b84 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -230,6 +230,8 @@ namespace lms::api::subsonic { "/deleteBookmark", { handleDeleteBookmark } }, { "/getPlayQueue", { handleGetPlayQueue } }, { "/savePlayQueue", { handleSavePlayQueue } }, + { "/getPlayQueueByIndex", { handleGetPlayQueueByIndex } }, + { "/savePlayQueueByIndex", { handleSavePlayQueueByIndex } }, // Media library scanning { "/getScanStatus", { Scan::handleGetScanStatus } }, diff --git a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp index 41ea02cc..3f9758ec 100644 --- a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp +++ b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp @@ -30,24 +30,20 @@ #include "responses/Bookmark.hpp" #include "responses/Song.hpp" -#include "core/String.hpp" - namespace lms::api::subsonic { - using namespace db; - Response handleGetBookmarks(RequestContext& context) { auto transaction{ context.getDbSession().createReadTransaction() }; - const auto bookmarkIds{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId()) }; + const auto bookmarkIds{ db::TrackBookmark::find(context.getDbSession(), context.getUser()->getId()) }; Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node& bookmarksNode{ response.createNode("bookmarks") }; - for (const TrackBookmarkId bookmarkId : bookmarkIds.results) + for (const db::TrackBookmarkId bookmarkId : bookmarkIds.results) { - const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.getDbSession(), bookmarkId) }; + const db::TrackBookmark::pointer bookmark{ db::TrackBookmark::find(context.getDbSession(), bookmarkId) }; Response::Node bookmarkNode{ createBookmarkNode(bookmark) }; bookmarkNode.addChild("entry", createSongNode(context, bookmark->getTrack(), context.getUser())); bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode)); @@ -59,20 +55,20 @@ namespace lms::api::subsonic Response handleCreateBookmark(RequestContext& context) { // Mandatory params - TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; + db::TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; unsigned long position{ getMandatoryParameterAs(context.getParameters(), "position") }; const std::optional comment{ getParameterAs(context.getParameters(), "comment") }; auto transaction{ context.getDbSession().createWriteTransaction() }; - const Track::pointer track{ Track::find(context.getDbSession(), trackId) }; + const db::Track::pointer track{ db::Track::find(context.getDbSession(), trackId) }; if (!track) throw RequestedDataNotFoundError{}; // Replace any existing bookmark - auto bookmark{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; + auto bookmark{ db::TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; if (!bookmark) - bookmark = context.getDbSession().create(context.getUser(), track); + bookmark = context.getDbSession().create(context.getUser(), track); bookmark.modify()->setOffset(std::chrono::milliseconds{ position }); if (comment) @@ -84,11 +80,11 @@ namespace lms::api::subsonic Response handleDeleteBookmark(RequestContext& context) { // Mandatory params - TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; + db::TrackId trackId{ getMandatoryParameterAs(context.getParameters(), "id") }; auto transaction{ context.getDbSession().createWriteTransaction() }; - auto bookmark{ TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; + auto bookmark{ db::TrackBookmark::find(context.getDbSession(), context.getUser()->getId(), trackId) }; if (!bookmark) throw RequestedDataNotFoundError{}; @@ -97,27 +93,58 @@ namespace lms::api::subsonic return Response::createOkResponse(context.getServerProtocolVersion()); } - // Use a dedicated internal playlist - Response handleGetPlayQueue(RequestContext& context) + static db::PlayQueue::pointer getOrCreatePlayQueue(RequestContext& context) + { + constexpr std::string_view name; + + db::PlayQueue::pointer playQueue; + { + auto transaction{ context.getDbSession().createReadTransaction() }; + + playQueue = db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), name); + } + + if (!playQueue) + { + auto transaction{ context.getDbSession().createWriteTransaction() }; + + playQueue = db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), name); + if (!playQueue) + { + playQueue = context.getDbSession().create(context.getUser(), name); + playQueue.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); + } + } + + return playQueue; + } + + // PlayQueue makes use of a dedicated internal playlist + static Response handleGetPlayQueueCommon(RequestContext& context, bool byIndex) { Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; - auto transaction{ context.getDbSession().createReadTransaction() }; - const db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), "subsonic") }; - if (playQueue) - { - Response::Node& playQueueNode{ response.createNode("playQueue") }; - if (auto currentTrack{ playQueue->getTrackAtCurrentIndex() }) - { - // optional fields - playQueueNode.setAttribute("current", idToString(currentTrack->getId())); - playQueueNode.setAttribute("position", playQueue->getCurrentPositionInTrack().count()); - } + const db::PlayQueue::pointer playQueue{ getOrCreatePlayQueue(context) }; + assert(playQueue); - // mandatory fields - playQueueNode.setAttribute("username", context.getUser()->getLoginName()); - playQueueNode.setAttribute("changed", core::stringUtils::toISO8601String(playQueue->getLastModifiedDateTime())); - playQueueNode.setAttribute("changedBy", "unknown"); // we don't store the client name (could be several same clients on several devices...) + auto transaction{ context.getDbSession().createReadTransaction() }; + + Response::Node& playQueueNode{ response.createNode(byIndex ? core::LiteralString{ "playQueueByIndex" } : core::LiteralString{ "playQueue" }) }; + + // mandatory fields + playQueueNode.setAttribute("username", context.getUser()->getLoginName()); + playQueueNode.setAttribute("changed", core::stringUtils::toISO8601String(playQueue->getLastModifiedDateTime())); + playQueueNode.setAttribute("changedBy", ""); // we don't store the client name (could be several same clients on several devices...) + + // optional fields + if (!playQueue->isEmpty()) + { + if (byIndex) + playQueueNode.setAttribute("index", playQueue->getCurrentIndex()); + else if (const db::Track::pointer currentTrack{ playQueue->getTrackAtCurrentIndex() }) + playQueueNode.setAttribute("current", idToString(currentTrack->getId())); + + playQueueNode.setAttribute("position", std::chrono::duration_cast(playQueue->getCurrentPositionInTrack()).count()); playQueue->visitTracks([&](const db::Track::pointer& track) { playQueueNode.addArrayChild("entry", createSongNode(context, track, true /* id3 */)); @@ -127,50 +154,66 @@ namespace lms::api::subsonic return response; } - Response handleSavePlayQueue(RequestContext& context) + Response handleGetPlayQueue(RequestContext& context) { - // optional params - std::vector trackIds{ getMultiParametersAs(context.getParameters(), "id") }; - const std::optional currentTrackId{ getParameterAs(context.getParameters(), "current") }; + return handleGetPlayQueueCommon(context, false); + } + + Response handleGetPlayQueueByIndex(RequestContext& context) + { + return handleGetPlayQueueCommon(context, true); + } + + static Response handleSavePlayQueueCommon(RequestContext& context, bool byIndex) + { + std::vector trackIds{ getMultiParametersAs(context.getParameters(), "id") }; + const std::optional currentTrackId{ byIndex ? std::nullopt : getParameterAs(context.getParameters(), "current") }; + const std::optional currentIndex{ byIndex ? getParameterAs(context.getParameters(), "currentIndex") : std::nullopt }; const std::chrono::milliseconds currentPositionInTrack{ getParameterAs(context.getParameters(), "current").value_or(0) }; - std::vector tracks; - tracks.reserve(trackIds.size()); - - // no id means we clear the play queue (see https://github.com/opensubsonic/open-subsonic-api/pull/106) - if (!trackIds.empty()) - { - auto transaction{ context.getDbSession().createReadTransaction() }; - for (db::TrackId trackId : trackIds) - { - if (db::Track::pointer track{ db::Track::find(context.getDbSession(), trackId) }) - tracks.push_back(track); - } - } + db::PlayQueue::pointer playQueue{ getOrCreatePlayQueue(context) }; + assert(playQueue); { auto transaction{ context.getDbSession().createWriteTransaction() }; - db::PlayQueue::pointer playQueue{ db::PlayQueue::find(context.getDbSession(), context.getUser()->getId(), "subsonic") }; - if (!playQueue) - playQueue = context.getDbSession().create(context.getUser(), "subsonic"); - + // no id means we clear the play queue (see https://github.com/opensubsonic/open-subsonic-api/pull/106) playQueue.modify()->clear(); - std::size_t index{}; - for (std::size_t i{}; i < tracks.size(); ++i) - { - db::Track::pointer& track{ tracks[i] }; - playQueue.modify()->addTrack(track); - if (track->getId() == currentTrackId) - index = i; + std::size_t currentPlayQueueIndex{}; + std::size_t currentTrackIndex{}; + for (db::TrackId trackId : trackIds) + { + if (const db::Track::pointer track{ db::Track::find(context.getDbSession(), trackId) }) + { + playQueue.modify()->addTrack(track); + + if ((currentTrackId && *currentTrackId == track->getId()) + || (currentIndex && *currentIndex == currentTrackIndex)) + { + playQueue.modify()->setCurrentIndex(currentPlayQueueIndex); + } + + currentPlayQueueIndex++; + } + + currentTrackIndex++; } - playQueue.modify()->setCurrentIndex(index); playQueue.modify()->setCurrentPositionInTrack(currentPositionInTrack); playQueue.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); } return Response::createOkResponse(context.getServerProtocolVersion()); } + + Response handleSavePlayQueue(RequestContext& context) + { + return handleSavePlayQueueCommon(context, false); + } + + Response handleSavePlayQueueByIndex(RequestContext& context) + { + return handleSavePlayQueueCommon(context, true); + } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/Bookmarks.hpp b/src/libs/subsonic/impl/endpoints/Bookmarks.hpp index 223582ed..4e7f0247 100644 --- a/src/libs/subsonic/impl/endpoints/Bookmarks.hpp +++ b/src/libs/subsonic/impl/endpoints/Bookmarks.hpp @@ -29,4 +29,6 @@ namespace lms::api::subsonic Response handleDeleteBookmark(RequestContext& context); Response handleGetPlayQueue(RequestContext& context); Response handleSavePlayQueue(RequestContext& context); + Response handleGetPlayQueueByIndex(RequestContext& context); + Response handleSavePlayQueueByIndex(RequestContext& context); } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/endpoints/System.cpp b/src/libs/subsonic/impl/endpoints/System.cpp index 1b195949..7b47c521 100644 --- a/src/libs/subsonic/impl/endpoints/System.cpp +++ b/src/libs/subsonic/impl/endpoints/System.cpp @@ -53,6 +53,12 @@ namespace lms::api::subsonic apiKeyAuthentication.addArrayValue("versions", 1); } + { + Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") }; + apiKeyAuthentication.setAttribute("name", "indexBasedQueue"); + apiKeyAuthentication.addArrayValue("versions", 1); + } + return response; }; } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/responses/Bookmark.cpp b/src/libs/subsonic/impl/responses/Bookmark.cpp index 07069412..34fa86e8 100644 --- a/src/libs/subsonic/impl/responses/Bookmark.cpp +++ b/src/libs/subsonic/impl/responses/Bookmark.cpp @@ -24,7 +24,7 @@ namespace lms::api::subsonic { - static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" }; + constexpr std::string_view reportedDummyDate{ "2000-01-01T00:00:00" }; Response::Node createBookmarkNode(const db::ObjectPtr& trackBookmark) { From e5a6de3e076a870150709b9be8d9ae052727b7a1 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Dec 2025 22:27:48 +0100 Subject: [PATCH 10/22] Added missing GPL headers --- .../include/services/recommendation/Types.hpp | 19 +++++++++++++++++ src/libs/subsonic/impl/endpoints/System.cpp | 19 +++++++++++++++++ .../impl/endpoints/UserManagement.cpp | 21 +++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/libs/services/recommendation/include/services/recommendation/Types.hpp b/src/libs/services/recommendation/include/services/recommendation/Types.hpp index 5389640e..b593ce80 100644 --- a/src/libs/services/recommendation/include/services/recommendation/Types.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/Types.hpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + #pragma once #include diff --git a/src/libs/subsonic/impl/endpoints/System.cpp b/src/libs/subsonic/impl/endpoints/System.cpp index 7b47c521..81468271 100644 --- a/src/libs/subsonic/impl/endpoints/System.cpp +++ b/src/libs/subsonic/impl/endpoints/System.cpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + #include "endpoints/System.hpp" namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/endpoints/UserManagement.cpp b/src/libs/subsonic/impl/endpoints/UserManagement.cpp index 2781db15..02b32e75 100644 --- a/src/libs/subsonic/impl/endpoints/UserManagement.cpp +++ b/src/libs/subsonic/impl/endpoints/UserManagement.cpp @@ -1,9 +1,26 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + #include "UserManagement.hpp" -#include "core/Service.hpp" #include "database/Session.hpp" #include "database/objects/User.hpp" -#include "services/auth/IPasswordService.hpp" #include "ParameterParsing.hpp" #include "responses/User.hpp" From d843cf5c06d6cf230c2ed93fb978c57f11502535 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Dec 2025 22:57:15 +0100 Subject: [PATCH 11/22] Fixed various warnings --- src/libs/audio/impl/ffmpeg/TagReader.cpp | 3 +-- src/libs/audio/impl/ffmpeg/TagReader.hpp | 1 - src/libs/audio/include/audio/IAudioFileInfo.hpp | 2 +- .../audiofile/AudioFileScanOperation.hpp | 2 +- .../transcoding/impl/TranscodeService.cpp | 8 +++----- .../transcoding/impl/TranscodeService.hpp | 5 +---- .../services/transcoding/ITranscodeService.hpp | 17 ++++------------- .../subsonic/impl/endpoints/MediaRetrieval.hpp | 2 +- src/libs/subsonic/impl/responses/Album.hpp | 2 +- src/libs/subsonic/impl/responses/AlbumInfo.hpp | 2 +- src/libs/subsonic/impl/responses/Artist.hpp | 2 +- src/libs/subsonic/impl/responses/Genre.hpp | 2 +- src/libs/subsonic/impl/responses/Lyrics.hpp | 2 +- src/libs/subsonic/impl/responses/Playlist.hpp | 2 +- src/libs/subsonic/impl/responses/Podcast.hpp | 2 +- src/libs/subsonic/impl/responses/Song.hpp | 2 +- src/libs/subsonic/impl/responses/User.hpp | 2 +- src/lms/main.cpp | 2 +- 18 files changed, 22 insertions(+), 38 deletions(-) diff --git a/src/libs/audio/impl/ffmpeg/TagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp index 8c3eccda..dcf648aa 100644 --- a/src/libs/audio/impl/ffmpeg/TagReader.cpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp @@ -144,8 +144,7 @@ namespace lms::audio::ffmpeg } // namespace TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs) - : _audioFile{ audioFile } - , _metaDataMap{ audioFile.extractMetaData() } + : _metaDataMap{ audioFile.extractMetaData() } { if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { diff --git a/src/libs/audio/impl/ffmpeg/TagReader.hpp b/src/libs/audio/impl/ffmpeg/TagReader.hpp index 25e7bd7f..9864e0d3 100644 --- a/src/libs/audio/impl/ffmpeg/TagReader.hpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.hpp @@ -39,7 +39,6 @@ namespace lms::audio::ffmpeg void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - const AudioFile& _audioFile; AudioFile::MetadataMap _metaDataMap; }; } // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/include/audio/IAudioFileInfo.hpp b/src/libs/audio/include/audio/IAudioFileInfo.hpp index 68639161..adb707e5 100644 --- a/src/libs/audio/include/audio/IAudioFileInfo.hpp +++ b/src/libs/audio/include/audio/IAudioFileInfo.hpp @@ -21,7 +21,7 @@ namespace lms::audio { - class AudioProperties; + struct AudioProperties; class IImageReader; class ITagReader; diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp index d78f1bb0..37e03242 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp @@ -39,7 +39,7 @@ namespace lms::db namespace lms::scanner { - class AudioFileInfoParserSet; + struct AudioFileInfoParserSet; class TrackMetadataParser; struct ImageInfo diff --git a/src/libs/services/transcoding/impl/TranscodeService.cpp b/src/libs/services/transcoding/impl/TranscodeService.cpp index 5108e8df..3306f22f 100644 --- a/src/libs/services/transcoding/impl/TranscodeService.cpp +++ b/src/libs/services/transcoding/impl/TranscodeService.cpp @@ -34,14 +34,12 @@ namespace lms::transcoding } } // namespace - std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) + std::unique_ptr createTranscodeService() { - return std::make_unique(db, childProcessManager); + return std::make_unique(); } - TranscodeService::TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) - : _db{ db } - , _childProcessManager(childProcessManager) + TranscodeService::TranscodeService() { LMS_LOG(TRANSCODING, INFO, "Service started!"); } diff --git a/src/libs/services/transcoding/impl/TranscodeService.hpp b/src/libs/services/transcoding/impl/TranscodeService.hpp index 82b86f88..9626bdfc 100644 --- a/src/libs/services/transcoding/impl/TranscodeService.hpp +++ b/src/libs/services/transcoding/impl/TranscodeService.hpp @@ -26,7 +26,7 @@ namespace lms::transcoding class TranscodeService : public ITranscodeService { public: - explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); + explicit TranscodeService(); ~TranscodeService() override; TranscodeService(const TranscodeService&) = delete; @@ -34,8 +34,5 @@ namespace lms::transcoding private: std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) override; - - db::IDb& _db; - core::IChildProcessManager& _childProcessManager; }; } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp index a4def09c..b7a14df2 100644 --- a/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp +++ b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp @@ -23,19 +23,10 @@ #include "audio/TranscodeTypes.hpp" -namespace lms +namespace lms::core { - namespace core - { - class IChildProcessManager; - class IResourceHandler; - } // namespace core - - namespace db - { - class IDb; - } -} // namespace lms + class IResourceHandler; +} // namespace lms::core namespace lms::transcoding { @@ -47,5 +38,5 @@ namespace lms::transcoding virtual std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength = false) = 0; }; - std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); + std::unique_ptr createTranscodeService(); } // namespace lms::transcoding diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp index 45da25a9..4291466c 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.hpp @@ -26,7 +26,7 @@ namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response handleGetLyrics(RequestContext& context); Response handleGetLyricsBySongId(RequestContext& context); diff --git a/src/libs/subsonic/impl/responses/Album.hpp b/src/libs/subsonic/impl/responses/Album.hpp index 7af99b53..99688f4f 100644 --- a/src/libs/subsonic/impl/responses/Album.hpp +++ b/src/libs/subsonic/impl/responses/Album.hpp @@ -33,7 +33,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr& release, bool id3, const db::ObjectPtr& directory = {}); } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/AlbumInfo.hpp b/src/libs/subsonic/impl/responses/AlbumInfo.hpp index fac1c759..77c90a63 100644 --- a/src/libs/subsonic/impl/responses/AlbumInfo.hpp +++ b/src/libs/subsonic/impl/responses/AlbumInfo.hpp @@ -30,7 +30,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createAlbumInfoNode(RequestContext& context, const db::ObjectPtr& release); } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp index 89547fc5..1ae4ec86 100644 --- a/src/libs/subsonic/impl/responses/Artist.hpp +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -36,7 +36,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; namespace utils { diff --git a/src/libs/subsonic/impl/responses/Genre.hpp b/src/libs/subsonic/impl/responses/Genre.hpp index cc269676..cd34c9dd 100644 --- a/src/libs/subsonic/impl/responses/Genre.hpp +++ b/src/libs/subsonic/impl/responses/Genre.hpp @@ -30,7 +30,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createGenreNode(RequestContext& context, const db::ObjectPtr& cluster); } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/responses/Lyrics.hpp b/src/libs/subsonic/impl/responses/Lyrics.hpp index 78c2f1b4..0a7fdb67 100644 --- a/src/libs/subsonic/impl/responses/Lyrics.hpp +++ b/src/libs/subsonic/impl/responses/Lyrics.hpp @@ -30,7 +30,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createLyricsNode(RequestContext& context, const db::ObjectPtr& lyrics); Response::Node createStructuredLyricsNode(RequestContext& context, const db::ObjectPtr& lyrics); diff --git a/src/libs/subsonic/impl/responses/Playlist.hpp b/src/libs/subsonic/impl/responses/Playlist.hpp index 6c6c8e00..d4aaca07 100644 --- a/src/libs/subsonic/impl/responses/Playlist.hpp +++ b/src/libs/subsonic/impl/responses/Playlist.hpp @@ -30,7 +30,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createPlaylistNode(RequestContext& context, const db::ObjectPtr& tracklist); } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Podcast.hpp b/src/libs/subsonic/impl/responses/Podcast.hpp index 51eda2c9..06cab0f6 100644 --- a/src/libs/subsonic/impl/responses/Podcast.hpp +++ b/src/libs/subsonic/impl/responses/Podcast.hpp @@ -31,7 +31,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createPodcastEpisodeNode(const db::ObjectPtr& episode); Response::Node createPodcastNode(RequestContext& context, const db::ObjectPtr& podcast, bool includeEpisodes); diff --git a/src/libs/subsonic/impl/responses/Song.hpp b/src/libs/subsonic/impl/responses/Song.hpp index 87761172..ac22ba1a 100644 --- a/src/libs/subsonic/impl/responses/Song.hpp +++ b/src/libs/subsonic/impl/responses/Song.hpp @@ -32,7 +32,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createSongNode(RequestContext& context, const db::ObjectPtr& track, bool id3); } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/User.hpp b/src/libs/subsonic/impl/responses/User.hpp index 6eb758a9..68bf29e6 100644 --- a/src/libs/subsonic/impl/responses/User.hpp +++ b/src/libs/subsonic/impl/responses/User.hpp @@ -30,7 +30,7 @@ namespace lms::db namespace lms::api::subsonic { - struct RequestContext; + class RequestContext; Response::Node createUserNode(RequestContext& context, const db::ObjectPtr& user); } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 14c149d2..7b6f40c8 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -428,7 +428,7 @@ namespace lms core::Service recommendationService{ recommendation::createRecommendationService(*database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(*database, *recommendationService) }; core::Service scannerService{ scanner::createScannerService(*database, cachePath) }; - core::Service transcodingService{ transcoding::createTranscodeService(*database, *childProcessManagerService) }; + core::Service transcodingService{ transcoding::createTranscodeService() }; core::Service podcastService{ podcast::createPodcastService(ioContext, *database, cachePath / "podcasts") }; scannerService->getEvents().scanComplete.connect([&] { From 954b873f2b11d2d5ab11ee44e9ebd988eddf0c4f Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 2 Dec 2025 23:05:09 +0100 Subject: [PATCH 12/22] Fixed various warnings --- src/libs/core/impl/Logger.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/core/impl/Logger.cpp b/src/libs/core/impl/Logger.cpp index a5f20884..fbebb231 100644 --- a/src/libs/core/impl/Logger.cpp +++ b/src/libs/core/impl/Logger.cpp @@ -144,33 +144,33 @@ namespace lms::core::logging { case core::logging::Severity::DEBUG: if (_logFileStream) - addOutputStream(*_logFileStream, { core::logging::Severity::DEBUG }); + addOutputStream(*_logFileStream, core::logging::Severity::DEBUG); else - addOutputStream(std::cout, { core::logging::Severity::DEBUG }); + addOutputStream(std::cout, core::logging::Severity::DEBUG); [[fallthrough]]; case core::logging::Severity::INFO: if (_logFileStream) - addOutputStream(*_logFileStream, { core::logging::Severity::INFO }); + addOutputStream(*_logFileStream, core::logging::Severity::INFO); else - addOutputStream(std::cout, { core::logging::Severity::INFO }); + addOutputStream(std::cout, core::logging::Severity::INFO); [[fallthrough]]; case core::logging::Severity::WARNING: if (_logFileStream) - addOutputStream(*_logFileStream, { core::logging::Severity::WARNING }); + addOutputStream(*_logFileStream, core::logging::Severity::WARNING); else - addOutputStream(std::cerr, { core::logging::Severity::WARNING }); + addOutputStream(std::cerr, core::logging::Severity::WARNING); [[fallthrough]]; case core::logging::Severity::ERROR: if (_logFileStream) - addOutputStream(*_logFileStream, { core::logging::Severity::ERROR }); + addOutputStream(*_logFileStream, core::logging::Severity::ERROR); else - addOutputStream(std::cerr, { core::logging::Severity::ERROR }); + addOutputStream(std::cerr, core::logging::Severity::ERROR); [[fallthrough]]; case core::logging::Severity::FATAL: if (_logFileStream) - addOutputStream(*_logFileStream, { core::logging::Severity::FATAL }); + addOutputStream(*_logFileStream, core::logging::Severity::FATAL); else - addOutputStream(std::cerr, { core::logging::Severity::FATAL }); + addOutputStream(std::cerr, core::logging::Severity::FATAL); break; } } @@ -206,5 +206,4 @@ namespace lms::core::logging std::unique_lock lock{ outputStream->mutex }; outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl; } - } // namespace lms::core::logging \ No newline at end of file From 7feecb2027a585e5a3f75c1a5bbc892a315ec3e0 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Dec 2025 20:53:11 +0100 Subject: [PATCH 13/22] Implement the new OpenSubsonic API transcoding extension fixes #787 --- Dockerfile-release | 4 +- src/libs/audio/impl/ffmpeg/Transcoder.cpp | 19 + src/libs/core/impl/media/CodecType.cpp | 36 +- src/libs/core/include/core/UUID.hpp | 13 + .../core/include/core/media/CodecType.hpp | 4 +- src/libs/subsonic/CMakeLists.txt | 6 + src/libs/subsonic/impl/SubsonicResource.cpp | 11 +- src/libs/subsonic/impl/SubsonicResponse.hpp | 17 +- .../impl/endpoints/MediaAnnotation.cpp | 2 +- .../impl/endpoints/MediaRetrieval.cpp | 127 +--- src/libs/subsonic/impl/endpoints/Podcast.cpp | 2 +- src/libs/subsonic/impl/endpoints/System.cpp | 49 +- .../subsonic/impl/endpoints/Transcoding.cpp | 181 +++++ .../subsonic/impl/endpoints/Transcoding.hpp | 33 + .../endpoints/transcoding/AudioFileId.hpp | 30 + .../endpoints/transcoding/AudioFileInfo.cpp | 120 ++++ .../endpoints/transcoding/AudioFileInfo.hpp | 42 ++ .../transcoding/TranscodeDecision.cpp | 595 ++++++++++++++++ .../transcoding/TranscodeDecision.hpp | 76 +++ .../transcoding/TranscodeDecisionTracker.cpp | 92 +++ .../transcoding/TranscodeDecisionTracker.hpp | 53 ++ .../subsonic/impl/payloads/ClientInfo.cpp | 324 +++++++++ .../subsonic/impl/payloads/ClientInfo.hpp | 89 +++ .../subsonic/impl/payloads/StreamDetails.cpp | 44 ++ .../subsonic/impl/payloads/StreamDetails.hpp | 44 ++ src/libs/subsonic/test/CMakeLists.txt | 6 +- src/libs/subsonic/test/ClientInfo.cpp | 319 +++++++++ src/libs/subsonic/test/Subsonic.cpp | 33 + ...cResponseTest.cpp => SubsonicResponse.cpp} | 6 - src/libs/subsonic/test/TranscodeDecision.cpp | 641 ++++++++++++++++++ 30 files changed, 2855 insertions(+), 163 deletions(-) create mode 100644 src/libs/subsonic/impl/endpoints/Transcoding.cpp create mode 100644 src/libs/subsonic/impl/endpoints/Transcoding.hpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/AudioFileId.hpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.cpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.hpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.cpp create mode 100644 src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.hpp create mode 100644 src/libs/subsonic/impl/payloads/ClientInfo.cpp create mode 100644 src/libs/subsonic/impl/payloads/ClientInfo.hpp create mode 100644 src/libs/subsonic/impl/payloads/StreamDetails.cpp create mode 100644 src/libs/subsonic/impl/payloads/StreamDetails.hpp create mode 100644 src/libs/subsonic/test/ClientInfo.cpp create mode 100644 src/libs/subsonic/test/Subsonic.cpp rename src/libs/subsonic/test/{SubsonicResponseTest.cpp => SubsonicResponse.cpp} (97%) create mode 100644 src/libs/subsonic/test/TranscodeDecision.cpp diff --git a/Dockerfile-release b/Dockerfile-release index 0a162378..585c76db 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -70,9 +70,9 @@ RUN \ --enable-zlib \ --disable-everything \ --enable-decoder=aac*,ac3*,alac,als,ape,asf,dsd*,flac,libopus,pcm*,libvorbis,mp3*,mpc7,mpc8,shorten,tta,wavpack,wma*,libopenjpg,png \ - --enable-encoder=libmp3lame,libopus,libvorbis \ + --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=ogg,mp3 \ + --enable-muxer=flac,mp3,ogg \ --enable-protocol=file,pipe \ --enable-filter=aresample \ --enable-lto \ diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp index 72261de6..f146adca 100644 --- a/src/libs/audio/impl/ffmpeg/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -136,6 +136,25 @@ namespace lms::audio::ffmpeg args.emplace_back(std::to_string(*_outputParams.bitrate)); } + if (_outputParams.channelCount) + { + args.emplace_back("-ac"); + args.emplace_back(std::to_string(*_outputParams.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) { args.emplace_back("-f"); diff --git a/src/libs/core/impl/media/CodecType.cpp b/src/libs/core/impl/media/CodecType.cpp index b5b778f0..2a0bc587 100644 --- a/src/libs/core/impl/media/CodecType.cpp +++ b/src/libs/core/impl/media/CodecType.cpp @@ -71,4 +71,38 @@ namespace lms::core::media return "Unknown"; } -} // namespace lms::core::media \ No newline at end of file + + bool isCodecLossless(CodecType type) + { + switch (type) + { + case CodecType::ALAC: + case CodecType::APE: + case CodecType::DSD: + + case CodecType::FLAC: + case CodecType::MP4ALS: + case CodecType::PCM: + case CodecType::Shorten: + case CodecType::TrueAudio: + case CodecType::WavPack: + case CodecType::WMA9Lossless: + return true; + + case CodecType::AAC: + case CodecType::AC3: + case CodecType::EAC3: + case CodecType::MP3: + case CodecType::MPC7: + case CodecType::MPC8: + case CodecType::Opus: + case CodecType::Vorbis: + case CodecType::WMA1: + case CodecType::WMA2: + case CodecType::WMA9Pro: + return false; + } + + return false; + } +} // namespace lms::core::media diff --git a/src/libs/core/include/core/UUID.hpp b/src/libs/core/include/core/UUID.hpp index b57d4628..422bba75 100644 --- a/src/libs/core/include/core/UUID.hpp +++ b/src/libs/core/include/core/UUID.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -49,3 +50,15 @@ namespace lms::core::stringUtils std::optional readAs(std::string_view str); } + +namespace std +{ + template<> + struct hash + { + size_t operator()(const lms::core::UUID& str) const + { + return hash{}(str.getAsString()); + } + }; +} // namespace std diff --git a/src/libs/core/include/core/media/CodecType.hpp b/src/libs/core/include/core/media/CodecType.hpp index dac67e6d..3311a253 100644 --- a/src/libs/core/include/core/media/CodecType.hpp +++ b/src/libs/core/include/core/media/CodecType.hpp @@ -49,4 +49,6 @@ namespace lms::core::media }; core::LiteralString codecTypeToString(CodecType type); -} // namespace lms::core::media \ No newline at end of file + + bool isCodecLossless(CodecType type); +} // namespace lms::core::media diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 802f5611..84ee7eab 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,8 @@ add_library(lmssubsonic STATIC + impl/endpoints/transcoding/AudioFileInfo.cpp + impl/endpoints/transcoding/TranscodeDecision.cpp + impl/endpoints/transcoding/TranscodeDecisionTracker.cpp impl/endpoints/AlbumSongLists.cpp impl/endpoints/Bookmarks.cpp impl/endpoints/Browsing.cpp @@ -10,7 +13,10 @@ add_library(lmssubsonic STATIC impl/endpoints/Podcast.cpp impl/endpoints/Searching.cpp impl/endpoints/System.cpp + impl/endpoints/Transcoding.cpp impl/endpoints/UserManagement.cpp + impl/payloads/ClientInfo.cpp + impl/payloads/StreamDetails.cpp impl/responses/Album.cpp impl/responses/AlbumInfo.cpp impl/responses/Artist.cpp diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 32a30b84..0210f6b4 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -49,6 +49,7 @@ #include "endpoints/Podcast.hpp" #include "endpoints/Searching.hpp" #include "endpoints/System.hpp" +#include "endpoints/Transcoding.hpp" #include "endpoints/UserManagement.hpp" namespace lms::api::subsonic @@ -181,6 +182,9 @@ namespace lms::api::subsonic { "/getLyricsBySongId", { handleGetLyricsBySongId } }, { "/getAvatar", { handleNotImplemented } }, + // Transcoding extensions + { "/getTranscodeDecision", { handleGetTranscodeDecision } }, + // Media annotation { "/star", { handleStarRequest } }, { "/unstar", { handleUnstarRequest } }, @@ -244,6 +248,9 @@ namespace lms::api::subsonic { "/download", handleDownload }, { "/stream", handleStream }, { "/getCoverArt", handleGetCoverArt }, + + // Transcoding extension + { "/getTranscodeStream", handleGetTranscodeStream }, }; struct TLSMonotonicMemoryResourceCleaner @@ -352,9 +359,7 @@ namespace lms::api::subsonic } catch (const Error& e) { - LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'" - << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" - << ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"); + LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"); Response resp{ Response::createFailedResponse(protocolVersion, e) }; resp.write(response.out(), format); response.setMimeType(std::string{ ResponseFormatToMimeType(format) }); diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 44f19387..94d76898 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -219,13 +219,24 @@ namespace lms::api::subsonic class BadParameterGenericError : public GenericError { public: - BadParameterGenericError(const std::string& parameterName) - : _parameterName{ parameterName } {} + BadParameterGenericError(std::string_view parameterName, std::string_view details = {}) + : _parameterName{ parameterName } + , _details{ details } + { + } + + std::string_view getParameterName() const { return _parameterName; } private: - std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; } + std::string getMessage() const override + { + std::string res{ "Parameter '" + _parameterName + "': " }; + res += _details.empty() ? "bad value" : _details; + return res; + } const std::string _parameterName; + const std::string _details; }; class ParameterValueTooHighGenericError : public GenericError diff --git a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp index ae033bae..6df094dc 100644 --- a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp @@ -101,7 +101,7 @@ namespace lms::api::subsonic const int rating = getMandatoryParameterAs(parameters, "rating"); // The rating between 1 and 5 (inclusive), or 0 to remove the rating if (rating < 0 || rating > 5) - throw BadParameterGenericError{ "rating must be 0 or in range 1-5" }; + throw BadParameterGenericError{ "rating", "must be 0 or in range 1-5" }; if (rating > 0) res.rating = rating; diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index f31c9dc7..9ed5fee3 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -28,13 +28,7 @@ #include "core/String.hpp" #include "core/media/MimeType.hpp" -#include "audio/AudioProperties.hpp" -#include "audio/Exception.hpp" -#include "audio/IAudioFileInfo.hpp" -#include "audio/IAudioFileInfoParser.hpp" - #include "database/Session.hpp" -#include "database/objects/PodcastEpisode.hpp" #include "database/objects/PodcastEpisodeId.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackLyrics.hpp" @@ -42,7 +36,6 @@ #include "database/objects/User.hpp" #include "services/artwork/IArtworkService.hpp" -#include "services/podcast/IPodcastService.hpp" #include "services/transcoding/ITranscodeService.hpp" #include "CoverArtId.hpp" @@ -51,6 +44,7 @@ #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" #include "responses/Lyrics.hpp" +#include "transcoding/AudioFileInfo.hpp" namespace lms::api::subsonic { @@ -106,93 +100,6 @@ namespace lms::api::subsonic return res; } - audio::AudioProperties getAudioProperties(const std::filesystem::path& trackPath) - { - try - { - const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; - - audio::AudioFileInfoParseOptions parseOptions; - parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; - parseOptions.readImages = false; - parseOptions.readTags = false; - const auto audioFile{ parser->parse(trackPath, parseOptions) }; - - const audio::AudioProperties* properties{ audioFile->getAudioProperties() }; - if (!properties) - throw RequestedDataNotFoundError{}; - - return *properties; - } - catch (const audio::Exception& e) - { - throw RequestedDataNotFoundError{}; - } - } - - using AudioFileId = std::variant; - struct AudioFileInfo - { - std::filesystem::path path; - audio::AudioProperties audioProperties; - }; - - AudioFileInfo getAudioFileInfo(db::Session& session, AudioFileId audioFileId) - { - AudioFileInfo res; - - auto transaction{ session.createReadTransaction() }; - - if (const db::TrackId * trackId{ std::get_if(&audioFileId) }) - { - const db::Track::pointer track{ db::Track::find(session, *trackId) }; - if (!track) - throw RequestedDataNotFoundError{}; - - res.path = track->getAbsoluteFilePath(); - if (track->getContainer() && track->getCodec()) - { - res.audioProperties.container = *track->getContainer(); - res.audioProperties.codec = *track->getCodec(); - res.audioProperties.duration = track->getDuration(); - res.audioProperties.bitrate = track->getBitrate(); - res.audioProperties.channelCount = track->getChannelCount(); - res.audioProperties.sampleRate = track->getSampleRate(); - res.audioProperties.bitsPerSample = track->getBitsPerSample(); - } - else - { - res.audioProperties = getAudioProperties(res.path); - } - } - else if (const db::PodcastEpisodeId * episodeId{ std::get_if(&audioFileId) }) - { - const db::PodcastEpisode::pointer episode{ db::PodcastEpisode::find(session, *episodeId) }; - if (!episode) - throw RequestedDataNotFoundError{}; - - std::filesystem::path podcastCachePath{ core::Service::get()->getCachePath() }; - - res.path = podcastCachePath / episode->getAudioRelativeFilePath(); - if (episode->getContainer() && episode->getCodec()) - { - res.audioProperties.container = *episode->getContainer(); - res.audioProperties.codec = *episode->getCodec(); - res.audioProperties.duration = episode->getDuration(); - res.audioProperties.bitrate = episode->getBitrate(); - res.audioProperties.channelCount = episode->getChannelCount(); - res.audioProperties.sampleRate = episode->getSampleRate(); - res.audioProperties.bitsPerSample = episode->getBitsPerSample(); - } - else - { - res.audioProperties = getAudioProperties(res.path); - } - } - - return res; - } - struct StreamParameters { std::filesystem::path filePath; @@ -383,31 +290,23 @@ namespace lms::api::subsonic { std::shared_ptr resourceHandler; - try + Wt::Http::ResponseContinuation* continuation = request.continuation(); + if (!continuation) { - Wt::Http::ResponseContinuation* continuation = request.continuation(); - if (!continuation) - { - StreamParameters streamParameters{ getStreamParameters(context) }; - if (streamParameters.transcodeParameters) - resourceHandler = core::Service::get()->createTranscodeResourceHandler(*streamParameters.transcodeParameters, streamParameters.estimateContentLength); - else - resourceHandler = core::createFileResourceHandler(streamParameters.filePath, core::media::getMimeType(streamParameters.audioProperties.container, streamParameters.audioProperties.codec).str()); - } + StreamParameters streamParameters{ getStreamParameters(context) }; + if (streamParameters.transcodeParameters) + resourceHandler = core::Service::get()->createTranscodeResourceHandler(*streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - { - resourceHandler = Wt::cpp17::any_cast>(continuation->data()); - } - - continuation = resourceHandler->processRequest(request, response); - if (continuation) - continuation->setData(resourceHandler); + resourceHandler = core::createFileResourceHandler(streamParameters.filePath, core::media::getMimeType(streamParameters.audioProperties.container, streamParameters.audioProperties.codec).str()); } - catch (const audio::Exception& e) + else { - response.setStatus(404); // report not found if something wrong happened - LMS_LOG(API_SUBSONIC, ERROR, "Caught Av exception: " << e.what()); + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } + + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); } void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) diff --git a/src/libs/subsonic/impl/endpoints/Podcast.cpp b/src/libs/subsonic/impl/endpoints/Podcast.cpp index c4d129ff..389462c1 100644 --- a/src/libs/subsonic/impl/endpoints/Podcast.cpp +++ b/src/libs/subsonic/impl/endpoints/Podcast.cpp @@ -97,7 +97,7 @@ namespace lms::api::subsonic const std::string url{ getMandatoryParameterAs(context.getParameters(), "url") }; if (url.empty() || !(url.starts_with("http://") || url.starts_with("https://"))) - throw BadParameterGenericError{ "Invalid url" }; + throw BadParameterGenericError{ "url", "must start by http:// or https://" }; // no effect if podcast already exists core::Service::get()->addPodcast(url); diff --git a/src/libs/subsonic/impl/endpoints/System.cpp b/src/libs/subsonic/impl/endpoints/System.cpp index 81468271..9cc6eeee 100644 --- a/src/libs/subsonic/impl/endpoints/System.cpp +++ b/src/libs/subsonic/impl/endpoints/System.cpp @@ -17,6 +17,8 @@ * along with LMS. If not, see . */ +#include + #include "endpoints/System.hpp" namespace lms::api::subsonic @@ -42,40 +44,27 @@ namespace lms::api::subsonic { Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + struct Extension { - Response::Node& transcodeOffsetNode{ response.createArrayNode("openSubsonicExtensions") }; - transcodeOffsetNode.setAttribute("name", "transcodeOffset"); - transcodeOffsetNode.addArrayValue("versions", 1); - } + core::LiteralString name; + int version; + }; - { - Response::Node& formPostNode{ response.createArrayNode("openSubsonicExtensions") }; - formPostNode.setAttribute("name", "formPost"); - formPostNode.addArrayValue("versions", 1); - } + constexpr std::array extensions{ + Extension{ "apiKeyAuthentication", 1 }, + Extension{ "getPodcastEpisode", 1 }, + Extension{ "formPost", 1 }, + Extension{ "indexBasedQueue", 1 }, + Extension{ "songLyrics", 1 }, + Extension{ "transcodeOffset", 1 }, + Extension{ "transcoding", 1 }, + }; + for (const Extension& extension : extensions) { - Response::Node& songLyricsNode{ response.createArrayNode("openSubsonicExtensions") }; - songLyricsNode.setAttribute("name", "songLyrics"); - songLyricsNode.addArrayValue("versions", 1); - } - - { - Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") }; - apiKeyAuthentication.setAttribute("name", "apiKeyAuthentication"); - apiKeyAuthentication.addArrayValue("versions", 1); - } - - { - Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") }; - apiKeyAuthentication.setAttribute("name", "getPodcastEpisode"); - apiKeyAuthentication.addArrayValue("versions", 1); - } - - { - Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") }; - apiKeyAuthentication.setAttribute("name", "indexBasedQueue"); - apiKeyAuthentication.addArrayValue("versions", 1); + Response::Node& extensionNode{ response.createArrayNode("openSubsonicExtensions") }; + extensionNode.setAttribute("name", extension.name.str()); + extensionNode.addArrayValue("versions", extension.version); } return response; diff --git a/src/libs/subsonic/impl/endpoints/Transcoding.cpp b/src/libs/subsonic/impl/endpoints/Transcoding.cpp new file mode 100644 index 00000000..743a2116 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/Transcoding.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Transcoding.hpp" + +#include +#include + +#include "core/IResourceHandler.hpp" +#include "core/Service.hpp" +#include "core/UUID.hpp" +#include "core/Utils.hpp" + +#include "database/objects/PodcastEpisodeId.hpp" +#include "services/transcoding/ITranscodeService.hpp" + +#include "ParameterParsing.hpp" +#include "RequestContext.hpp" +#include "SubsonicId.hpp" +#include "SubsonicResponse.hpp" +#include "payloads/ClientInfo.hpp" +#include "payloads/StreamDetails.hpp" +#include "transcoding/AudioFileInfo.hpp" +#include "transcoding/TranscodeDecision.hpp" +#include "transcoding/TranscodeDecisionTracker.hpp" + +namespace lms::api::subsonic +{ + namespace + { + StreamDetails createStreamDetailsFromAudioProperties(const audio::AudioProperties& audioProperties) + { + StreamDetails res; + res.protocol = "http"; + res.container = core::media::containerTypeToString(audioProperties.container).str(); + res.codec = core::media::codecTypeToString(audioProperties.codec).str(); + res.audioChannels = audioProperties.channelCount; + res.audioBitrate = audioProperties.bitrate; + res.audioProfile = ""; // TODO + res.audioSamplerate = audioProperties.sampleRate; + res.audioBitdepth = audioProperties.bitsPerSample; + + return res; + } + + AudioFileId getMandatoryMediaIdParameter(RequestContext& context) + { + const std::string mediaType{ getMandatoryParameterAs(context.getParameters(), "mediaType") }; + + AudioFileId audioFileId; + if (mediaType == "song") + audioFileId = getMandatoryParameterAs(context.getParameters(), "mediaId"); + else if (mediaType == "podcast") + audioFileId = getMandatoryParameterAs(context.getParameters(), "mediaId"); + else + throw BadParameterGenericError{ "id", "must be 'song' or 'podcast'" }; + + return audioFileId; + } + + } // namespace + + Response handleGetTranscodeDecision(RequestContext& context) + { + // Parameters + const AudioFileId audioFileId{ getMandatoryMediaIdParameter(context) }; + + const ClientInfo clientInfo{ parseClientInfoFromJson(context.getBody()) }; + const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioFileId) }; + + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response::Node& transcodeNode{ response.createNode("transcodeDecision") }; + + { + const StreamDetails sourceStream{ createStreamDetailsFromAudioProperties(audioFileInfo.audioProperties) }; + transcodeNode.addChild("sourceStream", createStreamDetails(sourceStream)); + } + + const details::TranscodeDecisionResult transcodeDecision{ details::computeTranscodeDecision(clientInfo, audioFileInfo.audioProperties) }; + + std::visit(core::utils::overloads{ + [&](const details::DirectPlayResult&) { + transcodeNode.setAttribute("canDirectPlay", true); + transcodeNode.setAttribute("canTranscode", false); + }, + [&](const details::TranscodeResult& transcodeRes) { + transcodeNode.setAttribute("canDirectPlay", false); + transcodeNode.setAttribute("canTranscode", true); + + for (details::TranscodeReason reason : transcodeRes.reasons) + transcodeNode.addArrayValue("transcodeReason", transcodeReasonToString(reason).str()); + + const core::UUID uuid{ getTranscodeDecisionTracker().add(audioFileId, transcodeRes.targetStreamInfo) }; + transcodeNode.addChild("transcodeStream", createStreamDetails(transcodeRes.targetStreamInfo)); + transcodeNode.setAttribute("transcodeParams", uuid.getAsString()); + }, + [&](const details::FailureResult& failureRes) { + transcodeNode.setAttribute("canDirectPlay", false); + transcodeNode.setAttribute("canTranscode", false); + transcodeNode.setAttribute("errorReason", failureRes.reason); + } }, + transcodeDecision); + + return response; + } + + audio::TranscodeParameters getTranscodingParameters(RequestContext& context) + { + // Parameters + const AudioFileId audioFileId{ getMandatoryMediaIdParameter(context) }; + const core::UUID uuid{ getMandatoryParameterAs(context.getParameters(), "transcodeParams") }; + const std::chrono::seconds offset{ getParameterAs(context.getParameters(), "offset").value_or(0) }; + + const std::shared_ptr entry{ getTranscodeDecisionTracker().get(uuid) }; + if (!entry || entry->audioFileId != audioFileId) + throw RequestedDataNotFoundError{}; + + const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioFileId) }; + + audio::TranscodeParameters params; + params.inputParameters.filePath = audioFileInfo.path; + params.inputParameters.audioProperties = audioFileInfo.audioProperties; + params.inputParameters.offset = offset; + + if (entry->targetStreamInfo.audioChannels) + params.outputParameters.channelCount = *entry->targetStreamInfo.audioChannels; + if (entry->targetStreamInfo.audioSamplerate) + params.outputParameters.sampleRate = *entry->targetStreamInfo.audioSamplerate; + if (entry->targetStreamInfo.audioBitrate) + params.outputParameters.bitrate = *entry->targetStreamInfo.audioBitrate; + if (entry->targetStreamInfo.audioBitdepth) + params.outputParameters.bitsPerSample = *entry->targetStreamInfo.audioBitdepth; + + params.outputParameters.stripMetadata = false; + + const audio::TranscodeOutputFormat* transcodeOutputFormat{ details::selectTranscodeOutputFormat(entry->targetStreamInfo.container, entry->targetStreamInfo.codec) }; + if (!transcodeOutputFormat) + throw InternalErrorGenericError{ "Unsupported output format" }; + + params.outputParameters.format = *transcodeOutputFormat; + + return params; + } + + void handleGetTranscodeStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) + { + std::shared_ptr resourceHandler; + + Wt::Http::ResponseContinuation* continuation = request.continuation(); + if (!continuation) + { + const audio::TranscodeParameters params{ getTranscodingParameters(context) }; + resourceHandler = core::Service::get()->createTranscodeResourceHandler(params, false /* estimate content length */); + } + else + { + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); + } + assert(resourceHandler); // handles errors internally + + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); + } +} // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/endpoints/Transcoding.hpp b/src/libs/subsonic/impl/endpoints/Transcoding.hpp new file mode 100644 index 00000000..67f52250 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/Transcoding.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic +{ + struct RequestContext; + + Response handleGetTranscodeDecision(RequestContext& context); + void handleGetTranscodeStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); +} // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/endpoints/transcoding/AudioFileId.hpp b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileId.hpp new file mode 100644 index 00000000..fe42c677 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileId.hpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "database/objects/PodcastEpisodeId.hpp" +#include "database/objects/TrackId.hpp" + +namespace lms::api::subsonic +{ + using AudioFileId = std::variant; +} diff --git a/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.cpp b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.cpp new file mode 100644 index 00000000..3a71404e --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "AudioFileInfo.hpp" + +#include "audio/AudioProperties.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IAudioFileInfoParser.hpp" + +#include "database/Session.hpp" +#include "database/objects/PodcastEpisode.hpp" +#include "database/objects/Track.hpp" + +#include "services/podcast/IPodcastService.hpp" + +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic +{ + namespace + { + audio::AudioProperties getAudioProperties(const std::filesystem::path& trackPath) + { + try + { + const auto parser{ audio::createAudioFileInfoParser(audio::AudioFileInfoParserBackend::FFmpeg) }; + + audio::AudioFileInfoParseOptions parseOptions; + parseOptions.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average; + parseOptions.readImages = false; + parseOptions.readTags = false; + const auto audioFile{ parser->parse(trackPath, parseOptions) }; + + const audio::AudioProperties* properties{ audioFile->getAudioProperties() }; + if (!properties) + throw RequestedDataNotFoundError{}; + + return *properties; + } + catch (const audio::Exception& e) + { + throw RequestedDataNotFoundError{}; + } + } + + } // namespace + + AudioFileInfo getAudioFileInfo(db::Session& session, AudioFileId audioFileId) + { + AudioFileInfo res; + + auto transaction{ session.createReadTransaction() }; + + if (const db::TrackId * trackId{ std::get_if(&audioFileId) }) + { + const db::Track::pointer track{ db::Track::find(session, *trackId) }; + if (!track) + throw RequestedDataNotFoundError{}; + + res.path = track->getAbsoluteFilePath(); + if (track->getContainer() && track->getCodec()) + { + res.audioProperties.container = *track->getContainer(); + res.audioProperties.codec = *track->getCodec(); + res.audioProperties.duration = track->getDuration(); + res.audioProperties.bitrate = track->getBitrate(); + res.audioProperties.channelCount = track->getChannelCount(); + res.audioProperties.sampleRate = track->getSampleRate(); + res.audioProperties.bitsPerSample = track->getBitsPerSample(); + } + else + { + res.audioProperties = getAudioProperties(res.path); + } + } + else if (const db::PodcastEpisodeId * episodeId{ std::get_if(&audioFileId) }) + { + const db::PodcastEpisode::pointer episode{ db::PodcastEpisode::find(session, *episodeId) }; + if (!episode) + throw RequestedDataNotFoundError{}; + + std::filesystem::path podcastCachePath{ core::Service::get()->getCachePath() }; + + res.path = podcastCachePath / episode->getAudioRelativeFilePath(); + if (episode->getContainer() && episode->getCodec()) + { + res.audioProperties.container = *episode->getContainer(); + res.audioProperties.codec = *episode->getCodec(); + res.audioProperties.duration = episode->getDuration(); + res.audioProperties.bitrate = episode->getBitrate(); + res.audioProperties.channelCount = episode->getChannelCount(); + res.audioProperties.sampleRate = episode->getSampleRate(); + res.audioProperties.bitsPerSample = episode->getBitsPerSample(); + } + else + { + res.audioProperties = getAudioProperties(res.path); + } + } + + return res; + } +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.hpp b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.hpp new file mode 100644 index 00000000..f3025316 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/AudioFileInfo.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "audio/AudioProperties.hpp" + +#include "AudioFileId.hpp" + +namespace lms::db +{ + class Session; +} + +namespace lms::api::subsonic +{ + struct AudioFileInfo + { + std::filesystem::path path; + audio::AudioProperties audioProperties; + }; + + AudioFileInfo getAudioFileInfo(db::Session& session, AudioFileId audioFileId); // throw RequestedDataNotFoundError on failure +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp new file mode 100644 index 00000000..4de90dce --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "TranscodeDecision.hpp" + +#include + +#include "core/ILogger.hpp" +#include "core/String.hpp" + +#include "audio/TranscodeTypes.hpp" + +#include "SubsonicResponse.hpp" +#include "payloads/ClientInfo.hpp" + +namespace lms::api::subsonic::details +{ + namespace + { + constexpr std::array supportedTranscodeOutputFormats{ + audio::TranscodeOutputFormat{ .container = core::media::ContainerType::MPEG, .codec = core::media::CodecType::MP3 }, + audio::TranscodeOutputFormat{ .container = core::media::ContainerType::Ogg, .codec = core::media::CodecType::Vorbis }, + audio::TranscodeOutputFormat{ .container = core::media::ContainerType::Ogg, .codec = core::media::CodecType::Opus }, + audio::TranscodeOutputFormat{ .container = core::media::ContainerType::FLAC, .codec = core::media::CodecType::FLAC }, + }; + + bool isMatchingContainerName(core::media::ContainerType container, std::string_view containerStr) + { + using namespace std::literals; // for "..."sv + + constexpr std::array aiffNames{ "aif"sv, "aiff"sv }; + constexpr std::array apeNames{ "ape"sv }; + constexpr std::array asfNames{ "asf"sv, "wma"sv }; + constexpr std::array dsfNames{ "dsf"sv }; + constexpr std::array mpcNames{ "mpc"sv, "mpp"sv, "mp"sv }; + constexpr std::array mpegNames{ "mp3"sv, "mp2"sv, "mpeg"sv }; + constexpr std::array oggNames{ "ogg"sv, "oga"sv }; + constexpr std::array flacNames{ "flac"sv }; + constexpr std::array mp4Names{ "aac"sv, "adts"sv, "m4a"sv, "mp4"sv, "m4b"sv, "m4p"sv }; + constexpr std::array shortenNames{ "shn"sv }; + constexpr std::array trueAudioNames{ "tta"sv }; + constexpr std::array wavNames{ "wav"sv }; + constexpr std::array wavPackNames{ "wv"sv }; + + std::span containerNames; + + switch (container) + { + case core::media::ContainerType::AIFF: + containerNames = aiffNames; + break; + case core::media::ContainerType::APE: + containerNames = apeNames; + break; + case core::media::ContainerType::ASF: + containerNames = asfNames; + break; + case core::media::ContainerType::DSF: + containerNames = dsfNames; + break; + case core::media::ContainerType::MPC: + containerNames = mpcNames; + break; + case core::media::ContainerType::MPEG: + containerNames = mpegNames; + break; + case core::media::ContainerType::Ogg: + containerNames = oggNames; + break; + case core::media::ContainerType::FLAC: + containerNames = flacNames; + break; + case core::media::ContainerType::MP4: + containerNames = mp4Names; + break; + case core::media::ContainerType::Shorten: + containerNames = shortenNames; + break; + case core::media::ContainerType::TrueAudio: + containerNames = trueAudioNames; + break; + case core::media::ContainerType::WAV: + containerNames = wavNames; + break; + case core::media::ContainerType::WavPack: + containerNames = wavPackNames; + break; + } + + return std::any_of(std::cbegin(containerNames), std::cend(containerNames), [&](std::string_view containerName) { return core::stringUtils::stringCaseInsensitiveEqual(containerName, containerStr); }); + } + + bool isMatchingCodecName(core::media::CodecType codec, std::string_view codecStr) + { + using namespace std::literals; // for "..."sv + + constexpr std::array aacCodecNames{ "aac"sv, "adts"sv }; + constexpr std::array ac3CodecNames{ "ac3"sv, "ac-3"sv }; + constexpr std::array alacCodecNames{ "alac"sv }; + constexpr std::array apeCodecNames{ "ape"sv }; + constexpr std::array dsdCodecNames{ "dsd"sv }; + constexpr std::array flacCodecNames{ "flac"sv }; + constexpr std::array eac3CodecNames{ "eac3"sv, "e-ac3"sv, "e-ac-3"sv, "eac-3"sv }; + constexpr std::array mp3CodecNames{ "mp3"sv }; + constexpr std::array mp4alsCodecNames{ "mp4als"sv, "als"sv }; + constexpr std::array mpc7CodecNames{ "mpc7"sv, "musepack7"sv }; + constexpr std::array mpc8CodecNames{ "mpc8"sv, "musepack8"sv }; + constexpr std::array opusCodecNames{ "opus"sv }; + constexpr std::array pcmCodecNames{ "pcm"sv }; + constexpr std::array shortenCodecNames{ "shn"sv, "shorten"sv }; + constexpr std::array trueAudioCodecNames{ "tta"sv }; + constexpr std::array vorbisCodecNames{ "vorbis"sv }; + constexpr std::array wavPackCodecNames{ "wv"sv }; + constexpr std::array wma1CodecNames{ "wma1"sv, "wmav1"sv }; + constexpr std::array wma2CodecNames{ "wma2"sv, "wmav2"sv }; + constexpr std::array wma9LosslessCodecNames{ "wmalossless"sv, "wma9lossless"sv }; + constexpr std::array wma9ProCodecNames{ "wmapro"sv, "wma9pro"sv }; + + std::span codecNames; + switch (codec) + { + case core::media::CodecType::AAC: + codecNames = aacCodecNames; + break; + case core::media::CodecType::AC3: + codecNames = ac3CodecNames; + break; + case core::media::CodecType::ALAC: + codecNames = alacCodecNames; + break; + case core::media::CodecType::APE: + codecNames = apeCodecNames; + break; + case core::media::CodecType::DSD: + codecNames = dsdCodecNames; + break; + case core::media::CodecType::EAC3: + codecNames = eac3CodecNames; + break; + case core::media::CodecType::FLAC: + codecNames = flacCodecNames; + break; + case core::media::CodecType::MP3: + codecNames = mp3CodecNames; + break; + case core::media::CodecType::MP4ALS: + codecNames = mp4alsCodecNames; + break; + case core::media::CodecType::MPC7: + codecNames = mpc7CodecNames; + break; + case core::media::CodecType::MPC8: + codecNames = mpc8CodecNames; + break; + case core::media::CodecType::Opus: + codecNames = opusCodecNames; + break; + case core::media::CodecType::PCM: + codecNames = pcmCodecNames; + break; + case core::media::CodecType::Shorten: + codecNames = shortenCodecNames; + break; + case core::media::CodecType::TrueAudio: + codecNames = trueAudioCodecNames; + break; + case core::media::CodecType::Vorbis: + codecNames = vorbisCodecNames; + break; + case core::media::CodecType::WavPack: + codecNames = wavPackCodecNames; + break; + case core::media::CodecType::WMA1: + codecNames = wma1CodecNames; + break; + case core::media::CodecType::WMA2: + codecNames = wma2CodecNames; + break; + case core::media::CodecType::WMA9Lossless: + codecNames = wma9LosslessCodecNames; + break; + case core::media::CodecType::WMA9Pro: + codecNames = wma9ProCodecNames; + break; + } + + return std::any_of(std::cbegin(codecNames), std::cend(codecNames), [&](std::string_view codecName) { return core::stringUtils::stringCaseInsensitiveEqual(codecName, codecStr); }); + } + + struct AdjustResult + { + enum class Type + { + None, + Adjusted, + CannotAdjust, + }; + Type type{ Type::None }; + std::optional newValue; + }; + + AdjustResult adjustUsingEqualsLimitation(std::span values, unsigned originalValue) + { + if (values.size() == 1) + { + const auto value{ core::stringUtils::readAs(values.front()) }; + assert(value); + if (originalValue == *value) + return AdjustResult{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + } + else + { + // Get the closest allowed value *below* originalValue (we don't want to upscale) + // Not sure if this worth doing this? + + std::optional closestValue; + for (std::string_view valueStr : values) + { + const auto value{ core::stringUtils::readAs(valueStr) }; + assert(value); + if (*value == originalValue) + return AdjustResult{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + if (*value < originalValue && (!closestValue || *value > *closestValue)) + closestValue = *value; + } + if (closestValue) + return AdjustResult{ .type = AdjustResult::Type::Adjusted, .newValue = *closestValue }; + } + + // Don't really know what to do here + return AdjustResult{ .type = AdjustResult::Type::CannotAdjust, .newValue = std::nullopt }; + } + + AdjustResult adjustUsingNotEqualsLimitation(std::span values, unsigned originalValue) + { + if (std::none_of(std::cbegin(values), std::cend(values), [&](std::string_view valueStr) { + const auto value{ core::stringUtils::readAs(valueStr) }; + assert(value); + return *value == originalValue; + })) + { + return AdjustResult{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + } + + // don't really know what to do here + return AdjustResult{ .type = AdjustResult::Type::CannotAdjust, .newValue = std::nullopt }; + } + + AdjustResult adjustUsingLessThanEqualLimitation(std::span values, unsigned originalValue) + { + // Take only the first value into account + const auto value{ core::stringUtils::readAs(values.front()) }; + assert(value); + if (originalValue <= *value) + return AdjustResult{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + + return AdjustResult{ .type = AdjustResult::Type::Adjusted, .newValue = *value }; + } + + AdjustResult adjustUsingGreaterThanEqualLimitation(std::span values, unsigned originalValue) + { + // Take only the first value into account + const auto value{ core::stringUtils::readAs(values.front()) }; + assert(value); + + if (originalValue >= *value) + return AdjustResult{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + + // We don't want to use a higher value than the original one (we don't want to upscale) + return AdjustResult{ .type = AdjustResult::Type::CannotAdjust, .newValue = *value }; + } + + AdjustResult adjustUsingLimitation(Limitation::ComparisonOperator comparisonOp, std::span values, unsigned originalValue) + { + assert(values.size() >= 1); + + switch (comparisonOp) + { + case Limitation::ComparisonOperator::Equals: + return adjustUsingEqualsLimitation(values, originalValue); + + case Limitation::ComparisonOperator::NotEquals: + return adjustUsingNotEqualsLimitation(values, originalValue); + + case Limitation::ComparisonOperator::LessThanEqual: + return adjustUsingLessThanEqualLimitation(values, originalValue); + + case Limitation::ComparisonOperator::GreaterThanEqual: + return adjustUsingGreaterThanEqualLimitation(values, originalValue); + } + + throw InternalErrorGenericError{ "Unhandled limitation comparison operator" }; + } + + bool isStreamCompatibleWithLimitation(const audio::AudioProperties& source, const Limitation& limitation) + { + if (!limitation.required) + return true; + + // TODO handle strings + std::optional valueToCheck{}; + switch (limitation.name) + { + case Limitation::Type::AudioBitrate: + valueToCheck = static_cast(source.bitrate); + break; + case Limitation::Type::AudioChannels: + valueToCheck = static_cast(source.channelCount); + break; + break; + case Limitation::Type::AudioSamplerate: + valueToCheck = static_cast(source.sampleRate); + break; + case Limitation::Type::AudioProfile: + // TODO; + break; + case Limitation::Type::AudioBitdepth: + if (source.bitsPerSample) + valueToCheck = static_cast(*source.bitsPerSample); + break; + } + + if (!valueToCheck) + return false; + + const AdjustResult adjustResult{ adjustUsingLimitation(limitation.comparison, limitation.values, *valueToCheck) }; + return adjustResult.type == AdjustResult::Type::None; + } + + const CodecProfile* getAudioCodecProfile(std::span codecProfiles, core::media::CodecType codec) + { + for (const CodecProfile& profile : codecProfiles) + { + if (profile.type != "AudioCodec") + continue; + + if (isMatchingCodecName(codec, profile.name)) + return &profile; + } + return nullptr; + } + + std::optional needsTranscode(const DirectPlayProfile& profile, std::span codecProfiles, const audio::AudioProperties& source) + { + if (!profile.containers.empty() && std::none_of(std::cbegin(profile.containers), std::cend(profile.containers), [&](const std::string& container) { return isMatchingContainerName(source.container, container); })) + return TranscodeReason::ContainerNotSupported; + + if (!profile.audioCodecs.empty() && std::none_of(std::cbegin(profile.audioCodecs), std::cend(profile.audioCodecs), [&](const std::string& audioCodec) { return isMatchingCodecName(source.codec, audioCodec); })) + return TranscodeReason::AudioCodecNotSupported; + + if (!profile.protocols.empty() && std::find(std::cbegin(profile.protocols), std::cend(profile.protocols), "http") == std::cend(profile.protocols)) + return TranscodeReason::ProtocolNotSupported; + + if (profile.maxAudioChannels && source.channelCount > *profile.maxAudioChannels) + return TranscodeReason::AudioChannelsNotSupported; + + // check potential codec profiles limitations + if (const CodecProfile * codecProfile{ getAudioCodecProfile(codecProfiles, source.codec) }) + { + for (const Limitation& limitation : codecProfile->limitations) + { + if (!isStreamCompatibleWithLimitation(source, limitation)) + { + switch (limitation.name) + { + case Limitation::Type::AudioBitrate: + return TranscodeReason::AudioBitrateNotSupported; + case Limitation::Type::AudioChannels: + return TranscodeReason::AudioChannelsNotSupported; + case Limitation::Type::AudioSamplerate: + return TranscodeReason::AudioSampleRateNotSupported; + case Limitation::Type::AudioProfile: + return TranscodeReason::AudioCodecNotSupported; + case Limitation::Type::AudioBitdepth: + return TranscodeReason::AudioBitdepthNotSupported; + } + } + } + } + + return std::nullopt; + } + + AdjustResult applyLimitation(const audio::AudioProperties& source, const Limitation& limitation, StreamDetails& transcodedStream) + { + switch (limitation.name) + { + case Limitation::Type::AudioChannels: + { + // transcodedStream.audioChannels may already be set by the transcoding profile maxAudioChannels + const AdjustResult adjustResult{ adjustUsingLimitation(limitation.comparison, limitation.values, transcodedStream.audioChannels ? *transcodedStream.audioChannels : source.channelCount) }; + if (adjustResult.type == AdjustResult::Type::Adjusted) + transcodedStream.audioChannels = *adjustResult.newValue; + return adjustResult; + } + + case Limitation::Type::AudioBitrate: + { + const AdjustResult adjustResult{ adjustUsingLimitation(limitation.comparison, limitation.values, transcodedStream.audioBitrate ? *transcodedStream.audioBitrate : source.bitrate) }; + if (adjustResult.type == AdjustResult::Type::Adjusted) + transcodedStream.audioBitrate = *adjustResult.newValue; + return adjustResult; + } + + case Limitation::Type::AudioProfile: + { + // TODO + AdjustResult res{ .type = AdjustResult::Type::None, .newValue = std::nullopt }; + return res; + } + + case Limitation::Type::AudioSamplerate: + { + const AdjustResult adjustResult{ adjustUsingLimitation(limitation.comparison, limitation.values, source.sampleRate) }; + if (adjustResult.type == AdjustResult::Type::Adjusted) + transcodedStream.audioSamplerate = *adjustResult.newValue; + return adjustResult; + } + + case Limitation::Type::AudioBitdepth: + { + if (source.bitsPerSample) + { + const AdjustResult adjustResult{ adjustUsingLimitation(limitation.comparison, limitation.values, *source.bitsPerSample) }; + if (adjustResult.type == AdjustResult::Type::Adjusted) + transcodedStream.audioBitdepth = *adjustResult.newValue; + + return adjustResult; + } + } + } + + return AdjustResult{ .type = AdjustResult::Type::CannotAdjust, .newValue = std::nullopt }; + } + + std::optional computeTranscodedStream(std::optional maxAudioBitrate, const TranscodingProfile& profile, std::span codecProfiles, const audio::AudioProperties& source) + { + if (profile.protocol != "http") + return std::nullopt; + + const audio::TranscodeOutputFormat* transcodeFormat{ selectTranscodeOutputFormat(profile.container, profile.audioCodec) }; + if (!transcodeFormat) + return std::nullopt; + + StreamDetails transcodedStream; + transcodedStream.protocol = "http"; + transcodedStream.container = profile.container; // put back what was requested instead of our internal names + transcodedStream.codec = profile.audioCodec; // put back what was requested instead of our internal names + + if (core::media::isCodecLossless(source.codec)) + { + if (!core::media::isCodecLossless(transcodeFormat->codec)) + { + // If coming from lossless source, maximize the bitrate if going to a non lossless source + // otherwise, pick a good enough value as we don't want to keep the original bitrate which does not make sense for lossy codecs + if (maxAudioBitrate) + transcodedStream.audioBitrate = maxAudioBitrate; + else + transcodedStream.audioBitrate = 256'000; // TODO, only if no bitrate limitation found? take channel count into account? + } + else + { + // If going to a lossless codec, make sure we can respect the original bitrate + // technically, we could have a chance to respect the bitrate if we apply limitations, but that's not easy to have a strong garantee + if (maxAudioBitrate && source.bitrate > *maxAudioBitrate) + return std::nullopt; + } + } + else + { + // source is lossy + + if (core::media::isCodecLossless(transcodeFormat->codec)) + return std::nullopt; // not compatible with lossless codecs + + // let's pick the same bitrate as the lossy source + transcodedStream.audioBitrate = source.bitrate; + } + + if (maxAudioBitrate && source.bitrate > *maxAudioBitrate) + transcodedStream.audioBitrate = *maxAudioBitrate; + + if (profile.maxAudioChannels && source.channelCount > *profile.maxAudioChannels) + transcodedStream.audioChannels = *profile.maxAudioChannels; + + if (const CodecProfile * codecProfile{ getAudioCodecProfile(codecProfiles, transcodeFormat->codec) }) + { + for (const Limitation& limitation : codecProfile->limitations) + { + const AdjustResult result{ applyLimitation(source, limitation, transcodedStream) }; + if (limitation.name == Limitation::Type::AudioBitrate && core::media::isCodecLossless(transcodeFormat->codec) && result.type == AdjustResult::Type::Adjusted) + return std::nullopt; // not compatible with lossless codecs + + if (result.type == AdjustResult::Type::CannotAdjust) + return std::nullopt; + } + } + + return transcodedStream; + } + + bool canDirectPlay(const ClientInfo& clientInfo, const audio::AudioProperties& source, std::vector& transcodeReasons) + { + // Check global constraints + if (clientInfo.maxAudioBitrate && *clientInfo.maxAudioBitrate < source.bitrate) + { + transcodeReasons.push_back(TranscodeReason::AudioBitrateNotSupported); + return false; + } + + // Check direct play profiles + for (const DirectPlayProfile& profile : clientInfo.directPlayProfiles) + { + const std::optional transcodeReason{ needsTranscode(profile, clientInfo.codecProfiles, source) }; + if (!transcodeReason) + return true; + + transcodeReasons.push_back(*transcodeReason); + } + + return false; + } + } // namespace + + core::LiteralString transcodeReasonToString(TranscodeReason reason) + { + switch (reason) + { + case TranscodeReason::AudioCodecNotSupported: + return "audio codec not supported"; + case TranscodeReason::AudioBitrateNotSupported: + return "audio bitrate not supported"; + case TranscodeReason::AudioChannelsNotSupported: + return "audio channels not supported"; + case TranscodeReason::AudioSampleRateNotSupported: + return "audio samplerate not supported"; + case TranscodeReason::AudioBitdepthNotSupported: + return "audio bitdepth not supported"; + case TranscodeReason::ContainerNotSupported: + return "container not supported"; + case TranscodeReason::ProtocolNotSupported: + return "protocol not supported"; + } + + return "unknown"; + } + + const audio::TranscodeOutputFormat* selectTranscodeOutputFormat(std::string_view containerName, std::string_view codecName) + { + // Find a supported output format + const auto it{ std::find_if(std::cbegin(supportedTranscodeOutputFormats), std::cend(supportedTranscodeOutputFormats), [&](const audio::TranscodeOutputFormat& format) { + return isMatchingCodecName(format.codec, codecName) && isMatchingContainerName(format.container, containerName); + }) }; + if (it == std::cend(supportedTranscodeOutputFormats)) + return nullptr; + + return &(*it); + } + + TranscodeDecisionResult computeTranscodeDecision(const ClientInfo& clientInfo, const audio::AudioProperties& source) + { + std::vector transcodeReasons; + + if (canDirectPlay(clientInfo, source, transcodeReasons)) + return DirectPlayResult{}; + + LMS_LOG(API_SUBSONIC, DEBUG, "Direct play not possible: no compatible direct play profile found"); + + // Check transcoding profiles, we have to select the first one we can handle, order is important + for (const TranscodingProfile& profile : clientInfo.transcodingProfiles) + { + std::optional targetStream{ computeTranscodedStream(clientInfo.maxTranscodingAudioBitrate, profile, clientInfo.codecProfiles, source) }; + if (targetStream) + return TranscodeResult{ .reasons = transcodeReasons, .targetStreamInfo = *targetStream }; + } + + return FailureResult{ "No compatible direct play or transcoding profile found" }; + } +} // namespace lms::api::subsonic::details diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp new file mode 100644 index 00000000..705fc119 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "core/LiteralString.hpp" + +#include "audio/AudioProperties.hpp" +#include "audio/TranscodeTypes.hpp" + +#include "payloads/StreamDetails.hpp" + +namespace lms::api::subsonic +{ + struct ClientInfo; + + namespace details + { + enum class TranscodeReason + { + AudioCodecNotSupported, + AudioBitrateNotSupported, + AudioChannelsNotSupported, + AudioSampleRateNotSupported, + AudioBitdepthNotSupported, + ContainerNotSupported, + ProtocolNotSupported + }; + + core::LiteralString transcodeReasonToString(TranscodeReason reason); + + struct DirectPlayResult + { + bool operator==(const DirectPlayResult&) const = default; + }; + + struct TranscodeResult + { + std::vector reasons; + StreamDetails targetStreamInfo; + + bool operator==(const TranscodeResult&) const = default; + }; + + struct FailureResult + { + std::string reason; + + bool operator==(const FailureResult&) const = default; + }; + + using TranscodeDecisionResult = std::variant; + TranscodeDecisionResult computeTranscodeDecision(const ClientInfo& clientInfo, const audio::AudioProperties& source); + + const audio::TranscodeOutputFormat* selectTranscodeOutputFormat(std::string_view containerName, std::string_view codecName); + } // namespace details +} // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.cpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.cpp new file mode 100644 index 00000000..f0a4ca58 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "TranscodeDecisionTracker.hpp" + +#include +#include + +#include "core/Random.hpp" + +namespace lms::api::subsonic +{ + namespace + { + class TranscodeDecisionTracker : public ITranscodeDecisionTracker + { + public: + core::UUID add(AudioFileId audioFileId, const StreamDetails& targetStreamInfo) override + { + const core::UUID uuid{ core::UUID::generate() }; + const Clock::time_point now{ Clock::now() }; + + auto entry{ std::make_shared(now, audioFileId, targetStreamInfo) }; + + { + std::scoped_lock lock{ mutex }; + + purgeOutdatedEntries(now); + + entries.emplace(uuid, entry); + } + + return uuid; + } + + std::shared_ptr get(const core::UUID& uuid) override + { + const Clock::time_point now{ Clock::now() }; + std::shared_ptr res; + + std::scoped_lock lock{ mutex }; + + auto it{ entries.find(uuid) }; + if (it != entries.end()) + { + if (now > it->second->addedTimePoint + maxEntryDuration) + entries.erase(it); + else + res = it->second; + } + + return res; + } + + private: + void purgeOutdatedEntries(Clock::time_point now) + { + std::erase_if(entries, [&](const auto& entry) { return now > entry.second->addedTimePoint + maxEntryDuration; }); + while (entries.size() > maxEntryCount) + entries.erase(core::random::pickRandom(entries)); // TODO kill oldest one? + } + + std::mutex mutex; + std::unordered_map> entries; + + static constexpr std::size_t maxEntryCount{ 1'000 }; + static constexpr std::chrono::hours maxEntryDuration{ 12 }; + }; + } // namespace + + ITranscodeDecisionTracker& getTranscodeDecisionTracker() + { + static TranscodeDecisionTracker tracker; + return tracker; + } +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.hpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.hpp new file mode 100644 index 00000000..5da0a4c8 --- /dev/null +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecisionTracker.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "core/UUID.hpp" + +#include "payloads/StreamDetails.hpp" + +#include "AudioFileId.hpp" + +namespace lms::api::subsonic +{ + // Keeps track of decisions using UUIDs + class ITranscodeDecisionTracker + { + public: + virtual ~ITranscodeDecisionTracker() = default; + + using Clock = std::chrono::steady_clock; + + struct Entry + { + Clock::time_point addedTimePoint; + AudioFileId audioFileId; + StreamDetails targetStreamInfo; + }; + + virtual core::UUID add(AudioFileId audioFileId, const StreamDetails& targetStreamInfo) = 0; + virtual std::shared_ptr get(const core::UUID& uuid) = 0; + }; + + ITranscodeDecisionTracker& getTranscodeDecisionTracker(); +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/payloads/ClientInfo.cpp b/src/libs/subsonic/impl/payloads/ClientInfo.cpp new file mode 100644 index 00000000..01c65a34 --- /dev/null +++ b/src/libs/subsonic/impl/payloads/ClientInfo.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ClientInfo.hpp" + +#include + +#include +#include +#include + +#include "core/String.hpp" + +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic +{ + namespace + { + template + std::optional parseValue(const Wt::Json::Object& object, const std::string& entry) + { + try + { + std::optional res; + + const Wt::Json::Value& value{ object.get(entry) }; + if (value.isNull()) + return res; + + if constexpr (std::is_same_v) + { + if (value.type() != Wt::Json::Type::Bool) + throw BadParameterGenericError{ entry, "field must be a boolean" }; + } + else if constexpr (std::is_same_v) + { + if (value.type() != Wt::Json::Type::String) + throw BadParameterGenericError{ entry, "field must be a string" }; + } + else if constexpr (std::is_integral_v) + { + if (value.type() != Wt::Json::Type::Number) + throw BadParameterGenericError{ entry, "field must be a number" }; + } + else + { + static_assert(false, "Unhandled type"); + } + + return static_cast(value); + } + catch (const Wt::WException& e) + { + throw BadParameterGenericError{ entry, "failed to read value" }; + } + } + + template + std::vector parseMandatoryArrayValues(const Wt::Json::Object& object, const std::string& entry) + { + std::vector res; + + try + { + const Wt::Json::Value& value{ object.get(entry) }; + if (value.isNull()) + throw BadParameterGenericError{ entry, "missing field" }; + + if (value.type() != Wt::Json::Type::Array) + throw BadParameterGenericError{ entry, "field must be an array" }; + + for (const Wt::Json::Value& item : static_cast(value)) + { + if constexpr (std::is_same_v) + { + if (item.type() != Wt::Json::Type::Bool) + throw BadParameterGenericError{ entry, "array item must be a boolean" }; + } + else if constexpr (std::is_same_v) + { + if (item.type() != Wt::Json::Type::String) + throw BadParameterGenericError{ entry, "array item must be a string" }; + } + else if constexpr (std::is_integral_v) + { + if (item.type() != Wt::Json::Type::Number) + throw BadParameterGenericError{ entry, "array item must be a number" }; + } + else + { + static_assert(false, "Unhandled type"); + } + + res.emplace_back(static_cast(item)); + } + } + catch (const Wt::WException& e) + { + throw BadParameterGenericError{ entry, "failed to read value" }; + } + + return res; + } + + template + T parseMandatoryValue(const Wt::Json::Object& object, const std::string& entry) + { + std::optional res{ parseValue(object, entry) }; + if (!res) + throw BadParameterGenericError{ entry, "field is mandatory" }; + + return *res; + } + + Limitation::Type parseLimitationType(std::string_view str) + { + if (str == "audioChannels") + return Limitation::Type::AudioChannels; + if (str == "audioBitrate") + return Limitation::Type::AudioBitrate; + if (str == "audioProfile") + return Limitation::Type::AudioProfile; + if (str == "audioSamplerate") + return Limitation::Type::AudioSamplerate; + if (str == "audioBitdepth") + return Limitation::Type::AudioBitdepth; + + throw BadParameterGenericError{ "ClientInfo::CodecProfile::Limitation::name", "unexpected value '" + std::string{ str } + "'" }; + } + + Limitation::ComparisonOperator parseComparisonOperator(std::string_view str) + { + // Equals, NotEquals, LessThanEqual, GreaterThanEqual + if (str == "Equals") + return Limitation::ComparisonOperator::Equals; + if (str == "NotEquals") + return Limitation::ComparisonOperator::NotEquals; + if (str == "LessThanEqual") + return Limitation::ComparisonOperator::LessThanEqual; + if (str == "GreaterThanEqual") + return Limitation::ComparisonOperator::GreaterThanEqual; + + throw BadParameterGenericError{ "ClientInfo::CodecProfile::Limitation::comparison", "unexpected value '" + std::string{ str } + "'" }; + } + + void checkLimitationValidity(const Limitation& limitation) + { + if (limitation.values.empty()) + throw BadParameterGenericError{ "ClientInfo::CodecProfile::Limitation::values", "must have at least one value" }; + + // only numeric values are allowed for some limitation types + switch (limitation.name) + { + case Limitation::Type::AudioChannels: + case Limitation::Type::AudioBitrate: + case Limitation::Type::AudioSamplerate: + case Limitation::Type::AudioBitdepth: + { + // must be a numeric value + std::string_view value{ limitation.values.front() }; + if (!core::stringUtils::readAs(value)) + throw BadParameterGenericError{ "ClientInfo::CodecProfile::Limitation::values", "'" + std::string{ value } + "' is not a valid number" }; + } + break; + + case Limitation::Type::AudioProfile: + // any value is allowed + break; + } + } + } // namespace + + ClientInfo parseClientInfoFromJson(std::istream& is) + { + ClientInfo res; + + const std::string msgBody{ std::istreambuf_iterator{ is }, std::istreambuf_iterator{} }; + + try + { + Wt::Json::Object root; + Wt::Json::parse(msgBody, root); + + res.name = parseMandatoryValue(root, "name"); + res.platform = parseMandatoryValue(root, "platform"); + { + const auto maxAudioBitrate = parseValue(root, "maxAudioBitrate"); + if (maxAudioBitrate && *maxAudioBitrate > 0) + res.maxAudioBitrate = *maxAudioBitrate; + } + + { + const auto maxTranscodingAudioBitrate = parseValue(root, "maxTranscodingAudioBitrate"); + if (maxTranscodingAudioBitrate && *maxTranscodingAudioBitrate > 0) + res.maxTranscodingAudioBitrate = *maxTranscodingAudioBitrate; + } + + if (const Wt::Json::Value & directPlayProfiles{ root.get("directPlayProfiles") }; directPlayProfiles.type() != Wt::Json::Type::Null) + { + for (const Wt::Json::Object& profile : static_cast(directPlayProfiles)) + { + DirectPlayProfile directPlayProfile; + + auto checkValues{ [](std::span values) { + return std::none_of(std::cbegin(values), std::cend(values), [](const std::string& value) { return value.empty() || value == "*"; }); + } }; + + // containers is stored in an array of string + directPlayProfile.containers = parseMandatoryArrayValues(profile, "containers"); + if (!checkValues(directPlayProfile.containers)) + throw BadParameterGenericError{ "ClientInfo::DirectPlayProfile::containers", "Invalid value" }; + + directPlayProfile.audioCodecs = parseMandatoryArrayValues(profile, "audioCodecs"); + if (!checkValues(directPlayProfile.audioCodecs)) + throw BadParameterGenericError{ "ClientInfo::DirectPlayProfile::containers", "Invalid value" }; + + directPlayProfile.protocols = parseMandatoryArrayValues(profile, "protocols"); + if (!checkValues(directPlayProfile.protocols)) + throw BadParameterGenericError{ "ClientInfo::DirectPlayProfile::protocols", "Invalid value" }; + + { + const auto maxAudioChannels = parseValue(profile, "maxAudioChannels"); + if (maxAudioChannels && *maxAudioChannels > 0) + directPlayProfile.maxAudioChannels = *maxAudioChannels; + } + + res.directPlayProfiles.push_back(directPlayProfile); + } + } + + if (const Wt::Json::Value & transcodingProfiles{ root.get("transcodingProfiles") }; transcodingProfiles.type() != Wt::Json::Type::Null) + { + for (const Wt::Json::Object& profile : static_cast(transcodingProfiles)) + { + TranscodingProfile transcodingProfile; + transcodingProfile.container = parseMandatoryValue(profile, "container"); + if (transcodingProfile.container.empty()) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::container", "cannot be empty" }; + + if (transcodingProfile.container.find('*') != std::string::npos) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::container", "cannot have *" }; + if (transcodingProfile.container.find(',') != std::string::npos) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::container", "cannot have ," }; + + transcodingProfile.audioCodec = parseMandatoryValue(profile, "audioCodec"); + if (transcodingProfile.audioCodec.empty()) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::audioCodec", "cannot be empty" }; + if (transcodingProfile.audioCodec.find('*') != std::string::npos) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::audioCodec", "cannot have *" }; + if (transcodingProfile.audioCodec.find(',') != std::string::npos) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::audioCodec", "cannot have ," }; + + transcodingProfile.protocol = parseMandatoryValue(profile, "protocol"); + if (transcodingProfile.protocol.empty()) + throw BadParameterGenericError{ "ClientInfo::TranscodingProfile::protocol", "cannot be empty" }; + + { + const auto maxAudioChannels{ parseValue(profile, "maxAudioChannels") }; + if (maxAudioChannels && *maxAudioChannels > 0) + transcodingProfile.maxAudioChannels = *maxAudioChannels; + } + + res.transcodingProfiles.push_back(transcodingProfile); + } + } + + // codecProfiles + if (const Wt::Json::Value & codecProfiles{ root.get("codecProfiles") }; codecProfiles.type() != Wt::Json::Type::Null) + { + for (const Wt::Json::Object& profile : static_cast(codecProfiles)) + { + CodecProfile codecProfile; + codecProfile.type = parseMandatoryValue(profile, "type"); + if (codecProfile.type != "AudioCodec" && codecProfile.type != "VideoCodec") + throw BadParameterGenericError{ "ClientInfo::CodecProfile::type", "unexpected value" }; + + codecProfile.name = parseMandatoryValue(profile, "name"); + if (codecProfile.name.empty()) + throw BadParameterGenericError{ "ClientInfo::CodecProfile::name", "name must not be empty" }; + + if (const Wt::Json::Value & limitations{ profile.get("limitations") }; limitations.type() != Wt::Json::Type::Null) + { + for (const Wt::Json::Object& limitation : static_cast(limitations)) + { + Limitation lim; + lim.name = parseLimitationType(parseMandatoryValue(limitation, "name")); + lim.comparison = parseComparisonOperator(parseMandatoryValue(limitation, "comparison")); + lim.values = parseMandatoryArrayValues(limitation, "values"); + lim.required = parseMandatoryValue(limitation, "required"); + + checkLimitationValidity(lim); + codecProfile.limitations.push_back(lim); + } + } + + res.codecProfiles.push_back(codecProfile); + } + } + } + catch (const Wt::WException& error) + { + throw BadParameterGenericError{ "ClientInfo", error.what() }; + } + + return res; + } +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/payloads/ClientInfo.hpp b/src/libs/subsonic/impl/payloads/ClientInfo.hpp new file mode 100644 index 00000000..f16ae891 --- /dev/null +++ b/src/libs/subsonic/impl/payloads/ClientInfo.hpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace lms::api::subsonic +{ + struct DirectPlayProfile + { + std::vector containers; // Supported containers (e.g., mp3, flac). + std::vector audioCodecs; // Supported audio codecs. + std::vector protocols; // The streaming protocols. Can be http or hls. + std::optional maxAudioChannels; // The maximum number of audio channels supported. + }; + + struct TranscodingProfile + { + std::string container; // The container format (e.g., mp3, flac). + std::string audioCodec; // The target audio codec for transcoding. + std::string protocol; // The streaming protocol. Can be http or hls. + std::optional maxAudioChannels; // The maximum number of audio channels for the transcoded stream. + }; + + struct Limitation + { + enum class Type + { + AudioChannels, + AudioBitrate, + AudioProfile, + AudioSamplerate, + AudioBitdepth, + }; + + enum class ComparisonOperator : unsigned char + { + Equals, + NotEquals, + LessThanEqual, + GreaterThanEqual, + }; + + Type name; // The name of the limitation. Can be audioChannels, audioBitrate, audioProfile, audioSamplerate, or audioBitdepth. + ComparisonOperator comparison; // The comparison operator. Can be Equals, NotEquals, LessThanEqual, GreaterThanEqual + std::vector values; // The values to compare against. For LessThanEqual and GreaterThanEqual, only the first value will be used. + bool required; // Whether this limitation must be met. + }; + + struct CodecProfile + { + std::string type; // The type of codec profile. Currently only AudioCodec is supported + std::string name; // The name of the codec (e.g., mp3, flac). + std::vector limitations; // A list of limitations for this codec. + }; + + struct ClientInfo + { + std::string name; // The name of the client device + std::string platform; // The platform of the client (e.g., Android, iOS). + std::optional maxAudioBitrate; // The maximum audio bitrate the client can handle. + std::optional maxTranscodingAudioBitrate; // The maximum audio bitrate for transcoded content. + std::vector directPlayProfiles; // A list of profiles for direct playback. + std::vector transcodingProfiles; // A list of profiles for transcoding. The server should evaluate these in the order they are listed, as a priority list. + std::vector codecProfiles; // A list of codec-specific profiles. + }; + + [[nodiscard]] ClientInfo parseClientInfoFromJson(std::istream& is); +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/payloads/StreamDetails.cpp b/src/libs/subsonic/impl/payloads/StreamDetails.cpp new file mode 100644 index 00000000..df7cc044 --- /dev/null +++ b/src/libs/subsonic/impl/payloads/StreamDetails.cpp @@ -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 . + */ + +#include "StreamDetails.hpp" + +namespace lms::api::subsonic +{ + Response::Node createStreamDetails(const StreamDetails& streamDetails) + { + Response::Node streamDetailsNode{}; + + streamDetailsNode.setAttribute("protocol", "http"); + streamDetailsNode.setAttribute("container", streamDetails.container); + streamDetailsNode.setAttribute("codec", streamDetails.codec); + if (streamDetails.audioChannels) + streamDetailsNode.setAttribute("audioChannels", *streamDetails.audioChannels); + if (streamDetails.audioBitrate) + streamDetailsNode.setAttribute("audioBitrate", *streamDetails.audioBitrate); + if (!streamDetails.audioProfile.empty()) + streamDetailsNode.setAttribute("audioProfile", streamDetails.audioProfile); + if (streamDetails.audioSamplerate) + streamDetailsNode.setAttribute("audioSamplerate", *streamDetails.audioSamplerate); + if (streamDetails.audioBitdepth) + streamDetailsNode.setAttribute("audioBitdepth", *streamDetails.audioBitdepth); + + return streamDetailsNode; + } +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/payloads/StreamDetails.hpp b/src/libs/subsonic/impl/payloads/StreamDetails.hpp new file mode 100644 index 00000000..f2d96add --- /dev/null +++ b/src/libs/subsonic/impl/payloads/StreamDetails.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic +{ + struct StreamDetails + { + std::string protocol; // The streaming protocol. Can be http or hls. + std::string container; // The container format (e.g., mp3, flac). + std::string codec; // The audio codec (e.g., mp3, aac, flac). + std::optional audioChannels; // The number of audio channels. + std::optional audioBitrate; // The audio bitrate in kbps. + std::string audioProfile; // The audio profile (e.g., LC, HE-AAC). + std::optional audioSamplerate; // The audio sample rate in Hz. + std::optional audioBitdepth; // The audio bit depth in bits. + + auto operator<=>(const StreamDetails&) const = default; + }; + + Response::Node createStreamDetails(const StreamDetails& streamDetails); +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/test/CMakeLists.txt b/src/libs/subsonic/test/CMakeLists.txt index 6e79cf78..39678b48 100644 --- a/src/libs/subsonic/test/CMakeLists.txt +++ b/src/libs/subsonic/test/CMakeLists.txt @@ -1,7 +1,10 @@ include(GoogleTest) add_executable(test-subsonic - SubsonicResponseTest.cpp + ClientInfo.cpp + Subsonic.cpp + SubsonicResponse.cpp + TranscodeDecision.cpp ) target_include_directories(test-subsonic PRIVATE @@ -10,6 +13,7 @@ target_include_directories(test-subsonic PRIVATE target_link_libraries(test-subsonic PRIVATE lmscore + lmsaudio lmssubsonic GTest::GTest ) diff --git a/src/libs/subsonic/test/ClientInfo.cpp b/src/libs/subsonic/test/ClientInfo.cpp new file mode 100644 index 00000000..6d23df81 --- /dev/null +++ b/src/libs/subsonic/test/ClientInfo.cpp @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "SubsonicResponse.hpp" +#include "payloads/ClientInfo.hpp" + +namespace lms::api::subsonic +{ + TEST(ClientInfo, basic) + { + // Example as in https://opensubsonic.netlify.app/docs/payloads/clientinfo/ + std::istringstream iss{ R"({ + "name": "Play:1", + "platform": "Sonos", + "maxAudioBitrate": 512000, + "maxTranscodingAudioBitrate": 256000, + "directPlayProfiles": [ + { + "containers": [ "mp3" ], + "audioCodecs": [ "mp3" ], + "protocols": [ "http" ], + "maxAudioChannels": 2 + }, + { + "containers": [ "flac" ], + "audioCodecs": [ "flac" ], + "protocols": [], + "maxAudioChannels": 2 + } + , + { + "containers": [ "mp4" ], + "audioCodecs": [ "flac", "aac", "alac" ], + "protocols": [], + "maxAudioChannels": 2 + } + ], + "transcodingProfiles": [ + { + "container": "mp3", + "audioCodec": "mp3", + "protocol": "http", + "maxAudioChannels": 2 + }, + { + "container": "flac", + "audioCodec": "flac", + "protocol": "http", + "maxAudioChannels": 2 + } + ], + "codecProfiles": [ + { + "type": "AudioCodec", + "name": "mp3", + "limitations": [ + { "name": "audioBitrate", "comparison": "LessThanEqual", "values": [ "320000" ], "required": true } + ] + }, + { + "type": "AudioCodec", + "name": "flac", + "limitations": [ + { "name": "audioSamplerate", "comparison": "LessThanEqual", "values": [ "192000" ], "required": false }, + { "name": "audioChannels", "comparison": "Equals", "values": ["1", "2" ], "required": false } + ] + } + ] +} +)" }; + try + { + const ClientInfo clientInfo{ parseClientInfoFromJson(iss) }; + + EXPECT_EQ(clientInfo.name, "Play:1"); + EXPECT_EQ(clientInfo.platform, "Sonos"); + EXPECT_TRUE(clientInfo.maxAudioBitrate.has_value()); + EXPECT_EQ(clientInfo.maxAudioBitrate.value(), 512000); + EXPECT_TRUE(clientInfo.maxTranscodingAudioBitrate.has_value()); + EXPECT_EQ(clientInfo.maxTranscodingAudioBitrate.value(), 256000); + + ASSERT_EQ(clientInfo.directPlayProfiles.size(), 3); + ASSERT_EQ(clientInfo.directPlayProfiles[0].containers.size(), 1); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[0], "mp3"); + ASSERT_EQ(clientInfo.directPlayProfiles[0].audioCodecs.size(), 1); + EXPECT_EQ(clientInfo.directPlayProfiles[0].audioCodecs[0], "mp3"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].protocols, std::vector{ "http" }); + EXPECT_EQ(clientInfo.directPlayProfiles[0].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.directPlayProfiles[1].containers.size(), 1); + EXPECT_EQ(clientInfo.directPlayProfiles[1].containers[0], "flac"); + EXPECT_EQ(clientInfo.directPlayProfiles[1].audioCodecs.size(), 1); + EXPECT_EQ(clientInfo.directPlayProfiles[1].audioCodecs[0], "flac"); + EXPECT_TRUE(clientInfo.directPlayProfiles[1].protocols.empty()); + EXPECT_EQ(clientInfo.directPlayProfiles[1].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.directPlayProfiles[2].containers.size(), 1); + EXPECT_EQ(clientInfo.directPlayProfiles[2].containers[0], "mp4"); + EXPECT_EQ(clientInfo.directPlayProfiles[2].audioCodecs.size(), 3); + EXPECT_EQ(clientInfo.directPlayProfiles[2].audioCodecs[0], "flac"); + EXPECT_EQ(clientInfo.directPlayProfiles[2].audioCodecs[1], "aac"); + EXPECT_EQ(clientInfo.directPlayProfiles[2].audioCodecs[2], "alac"); + EXPECT_TRUE(clientInfo.directPlayProfiles[2].protocols.empty()); + EXPECT_EQ(clientInfo.directPlayProfiles[2].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.transcodingProfiles.size(), 2); + EXPECT_EQ(clientInfo.transcodingProfiles[0].container, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].audioCodec, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].maxAudioChannels, 2); + EXPECT_EQ(clientInfo.transcodingProfiles[1].container, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].audioCodec, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.codecProfiles.size(), 2); + EXPECT_EQ(clientInfo.codecProfiles[0].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[0].name, "mp3"); + ASSERT_EQ(clientInfo.codecProfiles[0].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].name, Limitation::Type::AudioBitrate); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values[0], "320000"); + EXPECT_TRUE(clientInfo.codecProfiles[0].limitations[0].required); + EXPECT_EQ(clientInfo.codecProfiles[1].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[1].name, "flac"); + ASSERT_EQ(clientInfo.codecProfiles[1].limitations.size(), 2); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values[0], "192000"); + EXPECT_FALSE(clientInfo.codecProfiles[1].limitations[0].required); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[1].name, Limitation::Type::AudioChannels); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[1].comparison, Limitation::ComparisonOperator::Equals); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[1].values.size(), 2); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[1].values[0], "1"); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[1].values[1], "2"); + EXPECT_FALSE(clientInfo.codecProfiles[1].limitations[1].required); + } + catch (const Error& e) + { + GTEST_FAIL() << e.getMessage(); + } + } + + TEST(ClientInfo, multi) + { + std::istringstream iss{ R"({"name":"LocalDevice","platform":"Android","maxAudioBitrate":320000,"maxTranscodingAudioBitrate":320000,"directPlayProfiles":[{"containers":["mp4","mka","m4a","mp3","mp2","wav","flac","ogg","alac","opus","vorbis"],"audioCodecs":[],"protocols":[],"maxAudioChannels":32}],"transcodingProfiles":[{"container":"flac","audioCodec":"flac","protocol":"http","maxAudioChannels":0},{"container":"ogg","audioCodec":"opus","protocol":"http","maxAudioChannels":6},{"container":"mp3","audioCodec":"mp3","protocol":"http","maxAudioChannels":2}],"codecProfiles":[{"type":"AudioCodec","name":"vorbis","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","values":["48000"],"required":true}]},{"type":"AudioCodec","name":"opus","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","values":["48000"],"required":true}]}]})" }; + + try + { + const ClientInfo clientInfo{ parseClientInfoFromJson(iss) }; + + EXPECT_EQ(clientInfo.name, "LocalDevice"); + EXPECT_EQ(clientInfo.platform, "Android"); + EXPECT_EQ(clientInfo.maxAudioBitrate, 320000); + EXPECT_EQ(clientInfo.maxTranscodingAudioBitrate, 320000); + ASSERT_EQ(clientInfo.directPlayProfiles.size(), 1); + ASSERT_EQ(clientInfo.directPlayProfiles[0].containers.size(), 11); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[0], "mp4"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[1], "mka"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[2], "m4a"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[3], "mp3"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[4], "mp2"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[5], "wav"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[6], "flac"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[7], "ogg"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[8], "alac"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[9], "opus"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[10], "vorbis"); + ASSERT_EQ(clientInfo.directPlayProfiles[0].audioCodecs.size(), 0); + EXPECT_TRUE(clientInfo.directPlayProfiles[0].protocols.empty()); + EXPECT_EQ(clientInfo.directPlayProfiles[0].maxAudioChannels, 32); + ASSERT_EQ(clientInfo.transcodingProfiles.size(), 3); + EXPECT_EQ(clientInfo.transcodingProfiles[0].container, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].audioCodec, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].maxAudioChannels, std::nullopt); + EXPECT_EQ(clientInfo.transcodingProfiles[1].container, "ogg"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].audioCodec, "opus"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].maxAudioChannels, 6); + EXPECT_EQ(clientInfo.transcodingProfiles[2].container, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].audioCodec, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.codecProfiles.size(), 2); + EXPECT_EQ(clientInfo.codecProfiles[0].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[0].name, "vorbis"); + ASSERT_EQ(clientInfo.codecProfiles[0].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values[0], "48000"); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].required, true); + EXPECT_EQ(clientInfo.codecProfiles[1].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[1].name, "opus"); + ASSERT_EQ(clientInfo.codecProfiles[1].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values[0], "48000"); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].required, true); + } + catch (const Error& e) + { + GTEST_FAIL() << e.getMessage(); + } + } + + TEST(ClientInfo, multi2) + { + std::istringstream iss{ R"({"name":"Upnp/192.168.1.1/Foo","platform":"UPnP","maxAudioBitrate":0,"maxTranscodingAudioBitrate":0,"directPlayProfiles":[{"containers":["opus","ogg","oga","aac","webma","webm","wav","flac","mka"],"audioCodecs":[],"protocols":[],"maxAudioChannels":0},{"containers":["mp3"],"audioCodecs":["mp3"],"protocols":[],"maxAudioChannels":0},{"containers":["m4a","mp4"],"audioCodecs":["aac"],"protocols":[],"maxAudioChannels":0}],"transcodingProfiles":[{"container":"flac","audioCodec":"flac","protocol":"http","maxAudioChannels":6},{"container":"mp4","audioCodec":"aac","protocol":"http","maxAudioChannels":6},{"container":"aac","audioCodec":"aac","protocol":"http","maxAudioChannels":6},{"container":"mp3","audioCodec":"mp3","protocol":"http","maxAudioChannels":2}],"codecProfiles":[{"type":"AudioCodec","name":"flac","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","values":["48000"],"required":true}]},{"type":"AudioCodec","name":"vorbis","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","values":["48000"],"required":true}]},{"type":"AudioCodec","name":"opus","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","values":["48000"],"required":true}]}]})" }; + try + { + const ClientInfo clientInfo{ parseClientInfoFromJson(iss) }; + + EXPECT_EQ(clientInfo.name, "Upnp/192.168.1.1/Foo"); + EXPECT_EQ(clientInfo.platform, "UPnP"); + EXPECT_EQ(clientInfo.maxAudioBitrate, std::nullopt); + EXPECT_EQ(clientInfo.maxTranscodingAudioBitrate, std::nullopt); + ASSERT_EQ(clientInfo.directPlayProfiles.size(), 3); + ASSERT_EQ(clientInfo.directPlayProfiles[0].containers.size(), 9); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[0], "opus"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[1], "ogg"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[2], "oga"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[3], "aac"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[4], "webma"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[5], "webm"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[6], "wav"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[7], "flac"); + EXPECT_EQ(clientInfo.directPlayProfiles[0].containers[8], "mka"); + ASSERT_EQ(clientInfo.directPlayProfiles[0].audioCodecs.size(), 0); + EXPECT_TRUE(clientInfo.directPlayProfiles[0].protocols.empty()); + EXPECT_EQ(clientInfo.directPlayProfiles[0].maxAudioChannels, std::nullopt); + ASSERT_EQ(clientInfo.transcodingProfiles.size(), 4); + EXPECT_EQ(clientInfo.transcodingProfiles[0].container, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].audioCodec, "flac"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[0].maxAudioChannels, 6); + EXPECT_EQ(clientInfo.transcodingProfiles[1].container, "mp4"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].audioCodec, "aac"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[1].maxAudioChannels, 6); + EXPECT_EQ(clientInfo.transcodingProfiles[2].container, "aac"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].audioCodec, "aac"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[2].maxAudioChannels, 6); + EXPECT_EQ(clientInfo.transcodingProfiles[3].container, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[3].audioCodec, "mp3"); + EXPECT_EQ(clientInfo.transcodingProfiles[3].protocol, "http"); + EXPECT_EQ(clientInfo.transcodingProfiles[3].maxAudioChannels, 2); + ASSERT_EQ(clientInfo.codecProfiles.size(), 3); + EXPECT_EQ(clientInfo.codecProfiles[0].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[0].name, "flac"); + ASSERT_EQ(clientInfo.codecProfiles[0].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].values[0], "48000"); + EXPECT_EQ(clientInfo.codecProfiles[0].limitations[0].required, true); + EXPECT_EQ(clientInfo.codecProfiles[1].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[1].name, "vorbis"); + ASSERT_EQ(clientInfo.codecProfiles[1].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].values[0], "48000"); + EXPECT_EQ(clientInfo.codecProfiles[1].limitations[0].required, true); + EXPECT_EQ(clientInfo.codecProfiles[2].type, "AudioCodec"); + EXPECT_EQ(clientInfo.codecProfiles[2].name, "opus"); + ASSERT_EQ(clientInfo.codecProfiles[2].limitations.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[2].limitations[0].name, Limitation::Type::AudioSamplerate); + EXPECT_EQ(clientInfo.codecProfiles[2].limitations[0].comparison, Limitation::ComparisonOperator::LessThanEqual); + EXPECT_EQ(clientInfo.codecProfiles[2].limitations[0].values.size(), 1); + EXPECT_EQ(clientInfo.codecProfiles[2].limitations[0].values[0], "48000"); + EXPECT_EQ(clientInfo.codecProfiles[2].limitations[0].required, true); + } + catch (const Error& e) + { + GTEST_FAIL() << e.getMessage(); + } + } + + TEST(ClientInfo, badfield) + { + std::istringstream iss{ R"({"name":"LocalDevice","platform":"Android","maxAudioBitrate":"320000","maxTranscodingAudioBitrate":320000,"directPlayProfiles":[{"container":"mp4,mka,m4a,mp3,mp2,wav,flac,ogg,alac,opus,vorbis","audioCodec":"*","protocol":"*","maxAudioChannels":32}],"transcodingProfiles":[{"container":"flac","audioCodec":"flac","protocol":"http","maxAudioChannels":0},{"container":"ogg","audioCodec":"opus","protocol":"http","maxAudioChannels":6},{"container":"mp3","audioCodec":"mp3","protocol":"http","maxAudioChannels":2}],"codecProfiles":[{"type":"AudioCodec","name":"vorbis","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","value":"48000","required":true}]},{"type":"AudioCodec","name":"opus","limitations":[{"name":"audioSamplerate","comparison":"LessThanEqual","value":"48000","required":true}]}]})" }; + + try + { + const ClientInfo clientInfo{ parseClientInfoFromJson(iss) }; + GTEST_FAIL() << "Expected error"; + } + catch (const BadParameterGenericError& e) + { + EXPECT_EQ(e.getParameterName(), "maxAudioBitrate"); + } + catch (const Error& e) + { + GTEST_FAIL() << e.getMessage(); + } + } + +} // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/test/Subsonic.cpp b/src/libs/subsonic/test/Subsonic.cpp new file mode 100644 index 00000000..b00b3a0b --- /dev/null +++ b/src/libs/subsonic/test/Subsonic.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include "core/ILogger.hpp" +#include "core/Service.hpp" + +int main(int argc, char** argv) +{ + using namespace lms; + core::Service logger{ core::logging::createLogger(core::logging::Severity::ERROR) }; + + ::testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/src/libs/subsonic/test/SubsonicResponseTest.cpp b/src/libs/subsonic/test/SubsonicResponse.cpp similarity index 97% rename from src/libs/subsonic/test/SubsonicResponseTest.cpp rename to src/libs/subsonic/test/SubsonicResponse.cpp index c3b4cbb7..a21523ab 100644 --- a/src/libs/subsonic/test/SubsonicResponseTest.cpp +++ b/src/libs/subsonic/test/SubsonicResponse.cpp @@ -114,9 +114,3 @@ namespace lms::api::subsonic::tests } } // namespace lms::api::subsonic::tests - -int main(int argc, char** argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/src/libs/subsonic/test/TranscodeDecision.cpp b/src/libs/subsonic/test/TranscodeDecision.cpp new file mode 100644 index 00000000..45bd8b02 --- /dev/null +++ b/src/libs/subsonic/test/TranscodeDecision.cpp @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include + +#include "core/Utils.hpp" +#include "core/media/CodecType.hpp" +#include "core/media/ContainerType.hpp" + +#include "endpoints/transcoding/TranscodeDecision.hpp" +#include "payloads/ClientInfo.hpp" + +namespace lms::api::subsonic +{ + namespace details + { + std::ostream& operator<<(std::ostream& os, const details::TranscodeDecisionResult& result) + { + std::visit(core::utils::overloads{ + [&](const details::DirectPlayResult&) { os << "direct play"; }, + [&](const details::FailureResult& res) { os << "failure: " << res.reason; }, + [&](const details::TranscodeResult& res) { + os << "transcode: reasons = {"; + + bool firstReason{ true }; + for (TranscodeReason reason : res.reasons) + { + if (!firstReason) + os << ", "; + os << transcodeReasonToString(reason); + firstReason = false; + } + os << "}, target stream = {"; + os << "protocol = " << res.targetStreamInfo.protocol << ", container = " << res.targetStreamInfo.container << ", codec = " << res.targetStreamInfo.codec; + if (res.targetStreamInfo.audioChannels) + os << ", audioChannels = " << *res.targetStreamInfo.audioChannels; + if (res.targetStreamInfo.audioBitrate) + os << ", audioBitrate = " << *res.targetStreamInfo.audioBitrate; + if (!res.targetStreamInfo.audioProfile.empty()) + os << ", audioProfile = " << res.targetStreamInfo.audioProfile; + if (res.targetStreamInfo.audioSamplerate) + os << ", audioSamplerate = " << *res.targetStreamInfo.audioSamplerate; + if (res.targetStreamInfo.audioBitdepth) + os << ", audioBitdepth = " << *res.targetStreamInfo.audioBitdepth; + os << "}"; + } }, + result); + + return os; + } // namespace + }; // namespace details + + namespace + { + struct TestCase + { + ClientInfo clientInfo; + audio::AudioProperties source; + + details::TranscodeDecisionResult expected; + }; + + void processTests(std::span testCases) + { + for (std::size_t testCaseIndex{ 0 }; testCaseIndex < std::size(testCases); ++testCaseIndex) + { + const auto& testCase{ testCases[testCaseIndex] }; + const details::TranscodeDecisionResult decision{ details::computeTranscodeDecision(testCase.clientInfo, testCase.source) }; + + EXPECT_EQ(testCase.expected, decision) << "testCaseIndex: " << testCaseIndex; + } + } + } // namespace + + TEST(TranscodeDecision, directPlay) + { + const TestCase testCases[]{ + // Direct play + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 512'000, + .maxTranscodingAudioBitrate = 256'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "256000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::DirectPlayResult{} }, + }, + + // Needs transcode due to codec limitation + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 512'000, + .maxTranscodingAudioBitrate = 96'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "96000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to global limitation on the direct play bitrate + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 96'000, + .maxTranscodingAudioBitrate = 96'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "256000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to codec limitation, but global limitation is even more restrictive + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 96'000, + .maxTranscodingAudioBitrate = 96'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "128000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to max audio sample rate not handle by codec limitation + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 2, + .sampleRate = 96'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to max nb channels not handle by profile + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 } } }, + .transcodingProfiles = { { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 5, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to max nb channels not handle by codec. TODO take channel reduction into account for bitrate + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 5, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because codec not handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, + }, + .source = { + .container = core::media::ContainerType::Ogg, + .codec = core::media::CodecType::Opus, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because codec not handled (lossless source => using max bitrate) + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, + }, + .source = { + .container = core::media::ContainerType::FLAC, + .codec = core::media::CodecType::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 320000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because codec not handled (lossless source => using a default good bitrate) + { + .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = {} }, + .source = { + .container = core::media::ContainerType::FLAC, + .codec = core::media::CodecType::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // check protocol * and codec * are properly handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::DirectPlayResult{} }, + }, + + // check container * is properly handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::DirectPlayResult{} }, + }, + + // want flac but bitrate too high + { + .clientInfo = { + .name = "LocalDevice", + .platform = "Android", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 }, + } }, + .transcodingProfiles = { { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } } }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::ContainerType::FLAC, + .codec = core::media::CodecType::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 1'000'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // want flac but source sample rate is too high + { + .clientInfo = { + .name = "SONOS", + .platform = "UPnP", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { { + { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + } }, + .codecProfiles = { + { + { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + }, + }, + }, + .source = { + .container = core::media::ContainerType::FLAC, + .codec = core::media::CodecType::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 950'000, + .channelCount = 2, + .sampleRate = 96'000, + .bitsPerSample = 24, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + }, + + // want flac but source sample rate is too high, no max bitrate + { + .clientInfo = { + .name = "SONOS", + .platform = "UPnP", + .maxAudioBitrate = std::nullopt, + .maxTranscodingAudioBitrate = std::nullopt, + .directPlayProfiles = { { + { .containers = { "opus", "ogg", "oga", "aac", "webma", "webm", "wav", "flac", "mka" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "mp4", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + } }, + .codecProfiles = { + { + { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + }, + }, + }, + .source = { + .container = core::media::ContainerType::FLAC, + .codec = core::media::CodecType::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 950'000, + .channelCount = 2, + .sampleRate = 96'000, + .bitsPerSample = 24, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + }, + + // wants a lossy codec not handled -> transcode to lossy + { + .clientInfo = { + .name = "SONOS", + .platform = "UPnP", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { { + { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + } }, + .codecProfiles = { + { + { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + }, + }, + }, + .source = { + .container = core::media::ContainerType::Ogg, + .codec = core::media::CodecType::Vorbis, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // wants a lossless codec not handled -> transcode to lossless + { + .clientInfo = { + .name = "SONOS", + .platform = "UPnP", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { { + { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + } }, + .codecProfiles = { + { + { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, + }, + }, + }, + .source = { + .container = core::media::ContainerType::DSF, + .codec = core::media::CodecType::DSD, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 950'000, + .channelCount = 2, + .sampleRate = 96'000, + .bitsPerSample = 24, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + }, + + // no protocol specified + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 512'000, + .maxTranscodingAudioBitrate = 96'000, + .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = 2 }, + } }, + .transcodingProfiles = { + { { .container = "mp3", .audioCodec = "mp3", .protocol = { "http" }, .maxAudioChannels = 2 } }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "96000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::ContainerType::MPEG, + .codec = core::media::CodecType::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } +} // namespace lms::api::subsonic From 2d90281b11ce058ad41f1d8707d58ee22632433d Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Dec 2025 22:02:16 +0100 Subject: [PATCH 14/22] Added an option ui-allow-downloads in lms.conf to enable/disable downloads, fixes #776 --- approot/artist.xml | 2 + approot/playqueue.xml | 2 + approot/release.xml | 4 ++ approot/tracklist.xml | 2 + approot/tracks.xml | 2 + conf/lms.conf | 9 +++-- src/lms/ui/LmsApplication.cpp | 50 ++++++++++++++----------- src/lms/ui/LmsApplication.hpp | 3 ++ src/lms/ui/PlayQueue.cpp | 10 +++-- src/lms/ui/explore/ArtistView.cpp | 9 ++++- src/lms/ui/explore/ReleaseView.cpp | 16 ++++++-- src/lms/ui/explore/TrackListHelpers.cpp | 8 +++- src/lms/ui/explore/TrackListView.cpp | 8 +++- 13 files changed, 88 insertions(+), 37 deletions(-) diff --git a/approot/artist.xml b/approot/artist.xml index 13d24a6a..78195b00 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -24,7 +24,9 @@ ${}
  • ${tr:Lms.Explore.musicbrainz-artist}
  • ${
    } + ${}
  • ${download class="dropdown-item"}
  • + ${
    } diff --git a/approot/playqueue.xml b/approot/playqueue.xml index c372f8df..2f6f2453 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -65,7 +65,9 @@ diff --git a/approot/release.xml b/approot/release.xml index 0d856885..d19cd070 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -27,7 +27,9 @@ ${}
  • ${tr:Lms.Explore.musicbrainz-release}
  • ${
    } + ${}
  • ${download class="dropdown-item"}
  • + ${
    }
  • ${release-info class="dropdown-item"}
  • @@ -109,7 +111,9 @@
  • ${play-next class="dropdown-item"}
  • ${play-last class="dropdown-item"}
  • ${star class="dropdown-item"}
  • + ${}
  • ${download class="dropdown-item"}
  • + ${
    }
  • ${track-info class="dropdown-item"}
  • ${}
  • ${track-lyrics class="dropdown-item"}
  • ${
    } diff --git a/approot/tracklist.xml b/approot/tracklist.xml index 5a2ec74b..9f4fc8ca 100644 --- a/approot/tracklist.xml +++ b/approot/tracklist.xml @@ -13,7 +13,9 @@ diff --git a/conf/lms.conf b/conf/lms.conf index 269ae960..3a22444d 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -113,9 +113,6 @@ artist-image-file-names = ("artist"); # Note: files named after the disc itself are always searched before the names in this list. medium-image-file-names = ("discsubtitle"); -# Playqueue max entry count -playqueue-max-entry-count = 1000; - # Internal tracing for profiling purposes. Enable only if necessary, as it incurs some runtime overhead! # Possible values are "disabled", "overview" or "detailed". # If enabled, data has to be dumped in the tracing view located in the admin menu @@ -140,3 +137,9 @@ podcast-auto-download-episodes = true; # Max age in days for auto-downloaded episodes before deletion podcast-auto-download-episodes-max-age-days = 30; + +# Playqueue max entry count +ui-playqueue-max-entry-count = 1000; + +# Allow downloads +ui-allow-downloads = true; \ No newline at end of file diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 271b043a..216f0a36 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -26,9 +26,11 @@ #include #include +#include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/Service.hpp" + #include "database/IDb.hpp" #include "database/IQueryPlanRecorder.hpp" #include "database/Session.hpp" @@ -198,6 +200,31 @@ namespace lms::ui return static_cast(Wt::WApplication::instance()); } + LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend) + : Wt::WApplication{ env } + , _db{ db } + , _appManager{ appManager } + , _authBackend{ authBackend } + , _areDownloadsEnabled(core::Service::get()->getBool("ui-allow-downloads", true)) + { + try + { + init(); + } + catch (LmsApplicationException& e) + { + LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what()); + handleException(e); + } + catch (std::exception& e) + { + LMS_LOG(UI, ERROR, "Caught exception: " << e.what()); + throw core::LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html + } + } + + LmsApplication::~LmsApplication() = default; + db::IDb& LmsApplication::getDb() { return _db; @@ -240,30 +267,11 @@ namespace lms::ui return _user->userLoginName; } - LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend) - : Wt::WApplication{ env } - , _db{ db } - , _appManager{ appManager } - , _authBackend{ authBackend } + bool LmsApplication::areDownloadsEnabled() const { - try - { - init(); - } - catch (LmsApplicationException& e) - { - LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what()); - handleException(e); - } - catch (std::exception& e) - { - LMS_LOG(UI, ERROR, "Caught exception: " << e.what()); - throw core::LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html - } + return _areDownloadsEnabled; } - LmsApplication::~LmsApplication() = default; - void LmsApplication::init() { LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationInit"); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index a7776097..c54646c4 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -70,6 +70,8 @@ namespace lms::ui db::UserType getUserType() const; // user must be logged in prior this call std::string_view getUserLoginName() const; // user must be logged in prior this call + bool areDownloadsEnabled() const; + // Proxified scanner events scanner::Events& getScannerEvents() { return _scannerEvents; } @@ -109,6 +111,7 @@ namespace lms::ui Wt::Signal<> _preQuit; LmsApplicationManager& _appManager; const AuthenticationBackend _authBackend; + const bool _areDownloadsEnabled; scanner::Events _scannerEvents; struct UserAuthInfo { diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index b5328b9a..3bdfe3e5 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -114,7 +114,7 @@ namespace lms::ui PlayQueue::PlayQueue() : Template{ Wt::WString::tr("Lms.PlayQueue.template") } - , _capacity{ core::Service::get()->getULong("playqueue-max-entry-count", 1000) } + , _capacity{ core::Service::get()->getULong("ui-playqueue-max-entry-count", 1000) } { initTrackLists(); @@ -578,8 +578,12 @@ namespace lms::ui } }); - entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(trackId) }); + if (LmsApp->areDownloadsEnabled()) + { + entry->setCondition("if-has-download", true); + entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(trackId) }); + } } void PlayQueue::enqueueRadioTracksIfNeeded() diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 35ed3989..58c0590a 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -168,8 +168,13 @@ namespace lms::ui .connect([this] { _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { _artistId }); }); - bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(_artistId) }); + + if (LmsApp->areDownloadsEnabled()) + { + setCondition("if-has-download", true); + bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(_artistId) }); + } { auto isStarred{ [this] { return core::Service::get()->isStarred(LmsApp->getUserId(), _artistId); } }; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 56ffb17f..ad55f306 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -359,8 +359,12 @@ namespace lms::ui _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { _releaseId }); }); - bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(_releaseId) }); + if (LmsApp->areDownloadsEnabled()) + { + setCondition("if-has-download", true); + bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(_releaseId) }); + } bindNew("release-info", Wt::WString::tr("Lms.Explore.release-info")) ->clicked() @@ -527,8 +531,12 @@ namespace lms::ui starBtn->clicked().connect([=] { toggle(); }); } - entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(trackId) }); + if (LmsApp->areDownloadsEnabled()) + { + entry->setCondition("if-has-download", true); + entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(trackId) }); + } entry->bindNew("track-info", Wt::WString::tr("Lms.Explore.track-info")) ->clicked() diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index c83d3be9..a7c6c07a 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -299,8 +299,12 @@ namespace lms::ui::TrackListHelpers starBtn->clicked().connect([=] { toggle(); }); } - entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(trackId) }); + if (LmsApp->areDownloadsEnabled()) + { + entry->setCondition("if-has-download", true); + entry->bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(trackId) }); + } entry->bindNew("track-info", Wt::WString::tr("Lms.Explore.track-info")) ->clicked() diff --git a/src/lms/ui/explore/TrackListView.cpp b/src/lms/ui/explore/TrackListView.cpp index 2bd658d2..6dc4717e 100644 --- a/src/lms/ui/explore/TrackListView.cpp +++ b/src/lms/ui/explore/TrackListView.cpp @@ -135,8 +135,12 @@ namespace lms::ui _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, *trackListId); }); - bindNew("download", Wt::WString::tr("Lms.Explore.download")) - ->setLink(Wt::WLink{ std::make_unique(*trackListId) }); + if (LmsApp->areDownloadsEnabled()) + { + setCondition("if-has-download", true); + bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink{ std::make_unique(*trackListId) }); + } if (trackList->getUserId() == LmsApp->getUserId()) { From bbde689a1632dbbd17f9d0b02e88b69d8f93e588 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 6 Dec 2025 22:24:30 +0100 Subject: [PATCH 15/22] Added size() method --- src/libs/core/include/core/EnumSet.hpp | 6 ++++++ src/libs/core/test/EnumSet.cpp | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/libs/core/include/core/EnumSet.hpp b/src/libs/core/include/core/EnumSet.hpp index dc035610..c2ec5c73 100644 --- a/src/libs/core/include/core/EnumSet.hpp +++ b/src/libs/core/include/core/EnumSet.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -51,6 +52,11 @@ namespace lms::core assign(begin, end); } + constexpr std::size_t size() const + { + return std::popcount(_bitfield); + } + template constexpr void assign(It begin, It end) { diff --git a/src/libs/core/test/EnumSet.cpp b/src/libs/core/test/EnumSet.cpp index cc37ce8e..6679ccae 100644 --- a/src/libs/core/test/EnumSet.cpp +++ b/src/libs/core/test/EnumSet.cpp @@ -31,9 +31,19 @@ namespace lms::core Two, }; + { + constexpr EnumSet test; + + static_assert(test.size() == 0); + static_assert(test.empty()); + static_assert(!test.contains(Foo::One)); + static_assert(!test.contains(Foo::Two)); + } + { constexpr EnumSet test{ Foo::One }; + static_assert(test.size() == 1); static_assert(!test.empty()); static_assert(test.contains(Foo::One)); static_assert(!test.contains(Foo::Two)); @@ -49,6 +59,8 @@ namespace lms::core constexpr EnumSet test{ Foo::One, Foo::Two }; constexpr auto bitfield{ test.getBitfield() }; + static_assert(test.size() == 2); + EnumSet test2; EXPECT_FALSE(test2.contains(Foo::One)); EXPECT_FALSE(test2.contains(Foo::Two)); From 22ed4247451a62716ad69e51a84fe135c8ba9c6a Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 6 Dec 2025 23:30:00 +0100 Subject: [PATCH 16/22] Added a way to filter by codec --- src/libs/database/impl/objects/Artist.cpp | 6 ++++ src/libs/database/impl/objects/Listen.cpp | 5 +++ src/libs/database/impl/objects/Release.cpp | 4 +++ src/libs/database/impl/objects/Track.cpp | 3 ++ src/libs/database/impl/objects/TrackList.cpp | 6 ++++ .../include/database/objects/Filters.hpp | 17 +++++++--- .../include/database/objects/Track.hpp | 2 +- src/libs/database/test/Artist.cpp | 28 +++++++++++++++ src/libs/database/test/Release.cpp | 34 +++++++++++++++++++ src/libs/database/test/Track.cpp | 25 +++++++------- 10 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp index f74971d0..04547009 100644 --- a/src/libs/database/impl/objects/Artist.cpp +++ b/src/libs/database/impl/objects/Artist.cpp @@ -34,6 +34,7 @@ #include "SqlQuery.hpp" #include "Utils.hpp" +#include "objects/detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/StringViewTraits.hpp" @@ -56,6 +57,7 @@ namespace lms::db || params.track.isValid() || params.release.isValid() || params.filters.clusters.size() == 1 + || params.filters.codec.has_value() || params.filters.mediaLibrary.isValid() || params.filters.label.isValid() || params.filters.releaseType.isValid()) @@ -67,6 +69,7 @@ namespace lms::db || params.sortMethod == ArtistSortMethod::AddedDesc || params.writtenAfter.isValid() || params.release.isValid() + || params.filters.codec.has_value() || params.filters.mediaLibrary.isValid() || params.filters.label.isValid() || params.filters.releaseType.isValid()) @@ -79,6 +82,9 @@ namespace lms::db if (params.release.isValid()) query.where("t.release_id = ?").bind(params.release); + if (params.filters.codec.has_value()) + query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp index 8fd3ac05..8ec09058 100644 --- a/src/libs/database/impl/objects/Listen.cpp +++ b/src/libs/database/impl/objects/Listen.cpp @@ -28,6 +28,7 @@ #include "SqlQuery.hpp" #include "Utils.hpp" +#include "objects/detail/Types.hpp" #include "traits/IdTypeTraits.hpp" DBO_INSTANTIATE_TEMPLATES(lms::db::Listen) @@ -49,11 +50,15 @@ namespace lms::db assert(!params.artist.isValid()); // poor check if (params.filters.mediaLibrary.isValid() + || params.filters.codec.has_value() || params.filters.label.isValid() || params.filters.releaseType.isValid()) { query.join("track t ON t.id = t_a_l.track_id"); + if (params.filters.codec.has_value()) + query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index 6f572a2c..d0442de6 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -75,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()) { @@ -218,6 +219,9 @@ namespace lms::db query.where(oss.str()); } + if (params.filters.codec.has_value()) + query.where("t.codec = ?").bind(detail::getDbCodecType(params.filters.codec.value())); + if (params.releaseGroupMBID) query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString()); diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index 5ad089b2..b089e19e 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -197,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::getDbCodecType(*params.filters.codec)); + switch (params.sortMethod) { case TrackSortMethod::None: diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp index 5ef0393c..55c4b06e 100644 --- a/src/libs/database/impl/objects/TrackList.cpp +++ b/src/libs/database/impl/objects/TrackList.cpp @@ -31,6 +31,7 @@ #include "SqlQuery.hpp" #include "Utils.hpp" +#include "objects/detail/Types.hpp" #include "traits/IdTypeTraits.hpp" #include "traits/StringViewTraits.hpp" @@ -49,6 +50,7 @@ namespace lms::db auto query{ session.getDboSession()->query("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") }; if (!params.filters.clusters.empty() + || params.filters.codec.has_value() || params.filters.mediaLibrary.isValid() || params.filters.label.isValid() || params.filters.releaseType.isValid()) @@ -61,11 +63,15 @@ namespace lms::db query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); if (params.filters.mediaLibrary.isValid() + || params.filters.codec.has_value() || params.filters.label.isValid() || params.filters.releaseType.isValid()) { query.join("track t ON t.id = t_l_e.track_id"); + if (params.filters.codec.has_value()) + query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/include/database/objects/Filters.hpp b/src/libs/database/include/database/objects/Filters.hpp index 1d95a09f..ead8f741 100644 --- a/src/libs/database/include/database/objects/Filters.hpp +++ b/src/libs/database/include/database/objects/Filters.hpp @@ -19,9 +19,12 @@ #pragma once +#include #include #include +#include "core/media/CodecType.hpp" + #include "database/objects/ClusterId.hpp" #include "database/objects/LabelId.hpp" #include "database/objects/MediaLibraryId.hpp" @@ -31,10 +34,11 @@ namespace lms::db { struct Filters { - MediaLibraryId mediaLibrary; // tracks that belongs to this library - std::vector clusters; // tracks that belong to all these clusters - LabelId label; // tracks which release has this label - ReleaseTypeId releaseType; // tracks which release has this type + MediaLibraryId mediaLibrary; // tracks that belongs to this library + std::vector clusters; // tracks that belong to *all* these clusters + LabelId label; // tracks which release has this label + ReleaseTypeId releaseType; // tracks which release has this type + std::optional codec; // tracks that match this codec Filters& setClusters(std::span _clusters) { @@ -56,5 +60,10 @@ namespace lms::db releaseType = _releaseType; return *this; } + Filters& setCodec(std::optional _codec) + { + codec = _codec; + return *this; + } }; } // namespace lms::db \ No newline at end of file diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index b1d2d38a..3cdfc664 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -350,7 +350,7 @@ namespace lms::db Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyrightURL, "copyright_url"); 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); diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 0e443db8..5ae331ed 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -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::CodecType::MP3); + track2.get().modify()->setCodec(core::media::CodecType::FLAC); + } + + { + auto transaction{ session.createReadTransaction() }; + + Artist::FindParameters params; + params.setFilters(Filters{}.setCodec(core::media::CodecType::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" }; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 4b2757a0..a7419ba9 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1248,6 +1248,40 @@ namespace lms::db::tests } } + 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::CodecType::FLAC); + track1.get().modify()->setRelease(release1.get()); + + track2.get().modify()->setCodec(core::media::CodecType::MP3); + track2.get().modify()->setRelease(release2.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + Release::FindParameters params; + params.setFilters(Filters{}.setCodec(core::media::CodecType::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" }; diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 9b83cf63..79c54bc9 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -238,27 +238,26 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Track_findAbsoluteFilePath) + TEST_F(DatabaseFixture, Track_findByCodec) { - ScopedTrack track{ session }; - const std::filesystem::path absoluteFilePath{ "/path/to/track.mp3" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + { auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setAbsoluteFilePath(absoluteFilePath); + track1.get().modify()->setCodec(core::media::CodecType::MP3); + track2.get().modify()->setCodec(core::media::CodecType::FLAC); } { auto transaction{ session.createReadTransaction() }; - TrackId lastRetrievedTrackId; - std::vector> visitedTracks; - Track::findAbsoluteFilePath(session, lastRetrievedTrackId, 10, [&](TrackId trackId, const std::filesystem::path& filePath) { - visitedTracks.emplace_back(trackId, filePath); - }); - ASSERT_EQ(visitedTracks.size(), 1); - EXPECT_EQ(visitedTracks[0].first, track.getId()); - EXPECT_EQ(visitedTracks[0].second, absoluteFilePath); - EXPECT_EQ(lastRetrievedTrackId, track.getId()); + Track::FindParameters params; + params.setFilters(Filters{}.setCodec(core::media::CodecType::FLAC)); + + const auto tracks{ Track::find(session, params) }; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0]->getId(), track2.getId()); } } From 66e4f9ef92669f8a4f87dd540279d21aaec9b1dc Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 6 Dec 2025 23:44:45 +0100 Subject: [PATCH 17/22] Minor cleanup --- src/libs/core/impl/String.cpp | 32 ++++++------ src/libs/core/include/core/Tuple.hpp | 6 +-- src/libs/database/impl/Utils.hpp | 12 ++--- .../impl/internal/InternalBackend.cpp | 16 +++--- .../impl/listenbrainz/ListenBrainzBackend.cpp | 12 ++--- .../subsonic/impl/endpoints/Transcoding.cpp | 12 ++--- .../transcoding/TranscodeDecision.cpp | 4 +- .../transcoding/TranscodeDecision.hpp | 4 +- src/libs/subsonic/test/TranscodeDecision.cpp | 52 +++++++++---------- src/lms/ui/State.cpp | 5 +- src/lms/ui/State.hpp | 12 ++--- src/lms/ui/resource/DownloadResource.cpp | 16 +++--- 12 files changed, 91 insertions(+), 92 deletions(-) diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 50a9035c..99db1e12 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -31,7 +31,7 @@ namespace lms::core::stringUtils { - namespace details + namespace detail { constexpr std::pair jsEscapeChars[]{ { '\\', "\\\\" }, @@ -203,7 +203,7 @@ namespace lms::core::stringUtils return std::distance(std::cbegin(months), str) + 1; } - } // namespace details + } // namespace detail template<> std::optional readAs(std::string_view str) @@ -240,32 +240,32 @@ namespace lms::core::stringUtils std::vector splitString(std::string_view str, std::span separators) { - return details::splitString(str, separators); + return detail::splitString(str, separators); } std::vector splitString(std::string_view str, std::span separators) { - return details::splitString(str, separators); + return detail::splitString(str, separators); } std::string joinStrings(std::span strings, std::string_view delimiter) { - return details::joinStrings(strings, delimiter); + return detail::joinStrings(strings, delimiter); } std::string joinStrings(std::span strings, std::string_view delimiter) { - return details::joinStrings(strings, delimiter); + return detail::joinStrings(strings, delimiter); } std::string joinStrings(std::span strings, char delimiter) { - return details::joinStrings(strings, std::string_view{ &delimiter, 1 }); + return detail::joinStrings(strings, std::string_view{ &delimiter, 1 }); } std::string joinStrings(std::span strings, char delimiter) { - return details::joinStrings(strings, std::string_view{ &delimiter, 1 }); + return detail::joinStrings(strings, std::string_view{ &delimiter, 1 }); } std::string escapeAndJoinStrings(std::span strings, char delimiter, char escapeChar) @@ -434,32 +434,32 @@ namespace lms::core::stringUtils std::string jsEscape(std::string_view str) { - return details::escape(str, details::jsEscapeChars); + return detail::escape(str, detail::jsEscapeChars); } void writeJSEscapedString(std::ostream& os, std::string_view str) { - details::writeEscapedString(os, str, details::jsEscapeChars); + detail::writeEscapedString(os, str, detail::jsEscapeChars); } std::string jsonEscape(std::string_view str) { - return details::escape(str, details::jsonEscapeChars); + return detail::escape(str, detail::jsonEscapeChars); } void writeJsonEscapedString(std::ostream& os, std::string_view str) { - details::writeEscapedString(os, str, details::jsonEscapeChars); + detail::writeEscapedString(os, str, detail::jsonEscapeChars); } std::string xmlEscape(std::string_view str) { - return details::escape(str, details::xmlEscapeChars); + return detail::escape(str, detail::xmlEscapeChars); } void writeXmlEscapedString(std::ostream& os, std::string_view str) { - details::writeEscapedString(os, str, details::xmlEscapeChars); + detail::writeEscapedString(os, str, detail::xmlEscapeChars); } std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) @@ -611,11 +611,11 @@ namespace lms::core::stringUtils timeStr += ":00"; // Normalize zone - const std::optional offset{ details::getRFC822ZoneOffset(zoneStr) }; + const std::optional offset{ detail::getRFC822ZoneOffset(zoneStr) }; if (!offset) return {}; - std::optional month{ details::getRFC822Month(monthStr) }; + std::optional month{ detail::getRFC822Month(monthStr) }; if (!month) return {}; diff --git a/src/libs/core/include/core/Tuple.hpp b/src/libs/core/include/core/Tuple.hpp index 128ad0e3..c64019c7 100644 --- a/src/libs/core/include/core/Tuple.hpp +++ b/src/libs/core/include/core/Tuple.hpp @@ -23,7 +23,7 @@ namespace lms::core { - namespace details + namespace detail { template struct Seq @@ -45,11 +45,11 @@ namespace lms::core { auto l = { (f(std::get(t)), 0)... }; } - } // namespace details + } // namespace detail template void forEachTypeInTuple(std::tuple const& t, Func f) { - details::forEachTypeInTuple(t, f, details::GenSeq()); + detail::forEachTypeInTuple(t, f, detail::GenSeq()); } } // namespace lms::core \ No newline at end of file diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 2292f596..bd8da398 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -42,7 +42,7 @@ namespace lms::db::utils Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); - namespace details + namespace detail { template void recordQueryPlanIfNeeded(const Query& query) @@ -50,7 +50,7 @@ namespace lms::db::utils if (IQueryPlanRecorder * recorder{ core::Service::get() }) static_cast(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString()); } - } // namespace details + } // namespace detail template void applyRange(Query& query, std::optional range) @@ -100,7 +100,7 @@ namespace lms::db::utils template void forEachQueryResult(const Query& query, UnaryFunc&& func) { - details::recordQueryPlanIfNeeded(query); + detail::recordQueryPlanIfNeeded(query); LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString()); @@ -110,7 +110,7 @@ namespace lms::db::utils template std::vector fetchQueryResults(const Query& query) { - details::recordQueryPlanIfNeeded(query); + detail::recordQueryPlanIfNeeded(query); LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); @@ -121,7 +121,7 @@ namespace lms::db::utils template std::vector::type> fetchQueryResults(const Query& query) { - details::recordQueryPlanIfNeeded(query); + detail::recordQueryPlanIfNeeded(query); LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString()); @@ -132,7 +132,7 @@ namespace lms::db::utils template auto fetchQuerySingleResult(const Query& query) { - details::recordQueryPlanIfNeeded(query); + detail::recordQueryPlanIfNeeded(query); LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString()); return query.resultValue(); diff --git a/src/libs/services/feedback/impl/internal/InternalBackend.cpp b/src/libs/services/feedback/impl/internal/InternalBackend.cpp index 0c90b540..526efa47 100644 --- a/src/libs/services/feedback/impl/internal/InternalBackend.cpp +++ b/src/libs/services/feedback/impl/internal/InternalBackend.cpp @@ -27,7 +27,7 @@ namespace lms::feedback { - namespace details + namespace detail { template void onStarred(db::Session& session, typename StarredObjType::IdType id) @@ -46,7 +46,7 @@ namespace lms::feedback if (auto starredObj{ StarredObjType::find(session, id) }) starredObj.remove(); } - } // namespace details + } // namespace detail InternalBackend::InternalBackend(db::IDb& db) : _db{ db } @@ -55,31 +55,31 @@ namespace lms::feedback void InternalBackend::onStarred(db::StarredArtistId artistId) { - details::onStarred(_db.getTLSSession(), artistId); + detail::onStarred(_db.getTLSSession(), artistId); } void InternalBackend::onUnstarred(db::StarredArtistId artistId) { - details::onUnstarred(_db.getTLSSession(), artistId); + detail::onUnstarred(_db.getTLSSession(), artistId); } void InternalBackend::onStarred(db::StarredReleaseId releaseId) { - details::onStarred(_db.getTLSSession(), releaseId); + detail::onStarred(_db.getTLSSession(), releaseId); } void InternalBackend::onUnstarred(db::StarredReleaseId releaseId) { - details::onUnstarred(_db.getTLSSession(), releaseId); + detail::onUnstarred(_db.getTLSSession(), releaseId); } void InternalBackend::onStarred(db::StarredTrackId trackId) { - details::onStarred(_db.getTLSSession(), trackId); + detail::onStarred(_db.getTLSSession(), trackId); } void InternalBackend::onUnstarred(db::StarredTrackId trackId) { - details::onUnstarred(_db.getTLSSession(), trackId); + detail::onUnstarred(_db.getTLSSession(), trackId); } } // namespace lms::feedback diff --git a/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp index 59a7fc71..dbd936d8 100644 --- a/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp +++ b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp @@ -32,7 +32,7 @@ namespace lms::feedback::listenBrainz { - namespace details + namespace detail { template void onStarred(db::Session& session, typename StarredObjType::IdType id) @@ -54,7 +54,7 @@ namespace lms::feedback::listenBrainz if (auto starredObj{ StarredObjType::find(session, id) }) starredObj.remove(); } - } // namespace details + } // namespace detail ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, db::IDb& db) : _ioContext{ ioContext } @@ -73,22 +73,22 @@ namespace lms::feedback::listenBrainz void ListenBrainzBackend::onStarred(db::StarredArtistId starredArtistId) { - details::onStarred(_db.getTLSSession(), starredArtistId); + detail::onStarred(_db.getTLSSession(), starredArtistId); } void ListenBrainzBackend::onUnstarred(db::StarredArtistId starredArtistId) { - details::onUnstarred(_db.getTLSSession(), starredArtistId); + detail::onUnstarred(_db.getTLSSession(), starredArtistId); } void ListenBrainzBackend::onStarred(db::StarredReleaseId starredReleaseId) { - details::onStarred(_db.getTLSSession(), starredReleaseId); + detail::onStarred(_db.getTLSSession(), starredReleaseId); } void ListenBrainzBackend::onUnstarred(db::StarredReleaseId starredReleaseId) { - details::onUnstarred(_db.getTLSSession(), starredReleaseId); + detail::onUnstarred(_db.getTLSSession(), starredReleaseId); } void ListenBrainzBackend::onStarred(db::StarredTrackId starredTrackId) diff --git a/src/libs/subsonic/impl/endpoints/Transcoding.cpp b/src/libs/subsonic/impl/endpoints/Transcoding.cpp index 743a2116..aa96abc1 100644 --- a/src/libs/subsonic/impl/endpoints/Transcoding.cpp +++ b/src/libs/subsonic/impl/endpoints/Transcoding.cpp @@ -92,25 +92,25 @@ namespace lms::api::subsonic transcodeNode.addChild("sourceStream", createStreamDetails(sourceStream)); } - const details::TranscodeDecisionResult transcodeDecision{ details::computeTranscodeDecision(clientInfo, audioFileInfo.audioProperties) }; + const detail::TranscodeDecisionResult transcodeDecision{ detail::computeTranscodeDecision(clientInfo, audioFileInfo.audioProperties) }; std::visit(core::utils::overloads{ - [&](const details::DirectPlayResult&) { + [&](const detail::DirectPlayResult&) { transcodeNode.setAttribute("canDirectPlay", true); transcodeNode.setAttribute("canTranscode", false); }, - [&](const details::TranscodeResult& transcodeRes) { + [&](const detail::TranscodeResult& transcodeRes) { transcodeNode.setAttribute("canDirectPlay", false); transcodeNode.setAttribute("canTranscode", true); - for (details::TranscodeReason reason : transcodeRes.reasons) + for (detail::TranscodeReason reason : transcodeRes.reasons) transcodeNode.addArrayValue("transcodeReason", transcodeReasonToString(reason).str()); const core::UUID uuid{ getTranscodeDecisionTracker().add(audioFileId, transcodeRes.targetStreamInfo) }; transcodeNode.addChild("transcodeStream", createStreamDetails(transcodeRes.targetStreamInfo)); transcodeNode.setAttribute("transcodeParams", uuid.getAsString()); }, - [&](const details::FailureResult& failureRes) { + [&](const detail::FailureResult& failureRes) { transcodeNode.setAttribute("canDirectPlay", false); transcodeNode.setAttribute("canTranscode", false); transcodeNode.setAttribute("errorReason", failureRes.reason); @@ -149,7 +149,7 @@ namespace lms::api::subsonic params.outputParameters.stripMetadata = false; - const audio::TranscodeOutputFormat* transcodeOutputFormat{ details::selectTranscodeOutputFormat(entry->targetStreamInfo.container, entry->targetStreamInfo.codec) }; + const audio::TranscodeOutputFormat* transcodeOutputFormat{ detail::selectTranscodeOutputFormat(entry->targetStreamInfo.container, entry->targetStreamInfo.codec) }; if (!transcodeOutputFormat) throw InternalErrorGenericError{ "Unsupported output format" }; diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp index 4de90dce..d7910ac8 100644 --- a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp @@ -29,7 +29,7 @@ #include "SubsonicResponse.hpp" #include "payloads/ClientInfo.hpp" -namespace lms::api::subsonic::details +namespace lms::api::subsonic::detail { namespace { @@ -592,4 +592,4 @@ namespace lms::api::subsonic::details return FailureResult{ "No compatible direct play or transcoding profile found" }; } -} // namespace lms::api::subsonic::details +} // namespace lms::api::subsonic::detail diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp index 705fc119..1a77061c 100644 --- a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.hpp @@ -33,7 +33,7 @@ namespace lms::api::subsonic { struct ClientInfo; - namespace details + namespace detail { enum class TranscodeReason { @@ -72,5 +72,5 @@ namespace lms::api::subsonic TranscodeDecisionResult computeTranscodeDecision(const ClientInfo& clientInfo, const audio::AudioProperties& source); const audio::TranscodeOutputFormat* selectTranscodeOutputFormat(std::string_view containerName, std::string_view codecName); - } // namespace details + } // namespace detail } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/test/TranscodeDecision.cpp b/src/libs/subsonic/test/TranscodeDecision.cpp index 45bd8b02..3438eaba 100644 --- a/src/libs/subsonic/test/TranscodeDecision.cpp +++ b/src/libs/subsonic/test/TranscodeDecision.cpp @@ -30,14 +30,14 @@ namespace lms::api::subsonic { - namespace details + namespace detail { - std::ostream& operator<<(std::ostream& os, const details::TranscodeDecisionResult& result) + std::ostream& operator<<(std::ostream& os, const detail::TranscodeDecisionResult& result) { std::visit(core::utils::overloads{ - [&](const details::DirectPlayResult&) { os << "direct play"; }, - [&](const details::FailureResult& res) { os << "failure: " << res.reason; }, - [&](const details::TranscodeResult& res) { + [&](const detail::DirectPlayResult&) { os << "direct play"; }, + [&](const detail::FailureResult& res) { os << "failure: " << res.reason; }, + [&](const detail::TranscodeResult& res) { os << "transcode: reasons = {"; bool firstReason{ true }; @@ -66,7 +66,7 @@ namespace lms::api::subsonic return os; } // namespace - }; // namespace details + }; // namespace detail namespace { @@ -75,7 +75,7 @@ namespace lms::api::subsonic ClientInfo clientInfo; audio::AudioProperties source; - details::TranscodeDecisionResult expected; + detail::TranscodeDecisionResult expected; }; void processTests(std::span testCases) @@ -83,7 +83,7 @@ namespace lms::api::subsonic for (std::size_t testCaseIndex{ 0 }; testCaseIndex < std::size(testCases); ++testCaseIndex) { const auto& testCase{ testCases[testCaseIndex] }; - const details::TranscodeDecisionResult decision{ details::computeTranscodeDecision(testCase.clientInfo, testCase.source) }; + const detail::TranscodeDecisionResult decision{ detail::computeTranscodeDecision(testCase.clientInfo, testCase.source) }; EXPECT_EQ(testCase.expected, decision) << "testCaseIndex: " << testCaseIndex; } @@ -120,7 +120,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::DirectPlayResult{} }, + .expected = { detail::DirectPlayResult{} }, }, // Needs transcode due to codec limitation @@ -150,7 +150,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // Needs transcode due to global limitation on the direct play bitrate @@ -180,7 +180,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // Needs transcode due to codec limitation, but global limitation is even more restrictive @@ -210,7 +210,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // Needs transcode due to max audio sample rate not handle by codec limitation @@ -240,7 +240,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, // Needs transcode due to max nb channels not handle by profile @@ -264,7 +264,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // Needs transcode due to max nb channels not handle by codec. TODO take channel reduction into account for bitrate @@ -292,7 +292,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // needs transcode because codec not handled @@ -320,7 +320,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // needs transcode because codec not handled (lossless source => using max bitrate) @@ -348,7 +348,7 @@ namespace lms::api::subsonic .bitsPerSample = 16, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 320000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 320000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // needs transcode because codec not handled (lossless source => using a default good bitrate) @@ -371,7 +371,7 @@ namespace lms::api::subsonic .bitsPerSample = 16, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // check protocol * and codec * are properly handled @@ -397,7 +397,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::DirectPlayResult{} }, + .expected = { detail::DirectPlayResult{} }, }, // check container * is properly handled @@ -423,7 +423,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::DirectPlayResult{} }, + .expected = { detail::DirectPlayResult{} }, }, // want flac but bitrate too high @@ -449,7 +449,7 @@ namespace lms::api::subsonic .bitsPerSample = 16, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // want flac but source sample rate is too high @@ -487,7 +487,7 @@ namespace lms::api::subsonic .bitsPerSample = 24, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, // want flac but source sample rate is too high, no max bitrate @@ -526,7 +526,7 @@ namespace lms::api::subsonic .bitsPerSample = 24, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioSampleRateNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, // wants a lossy codec not handled -> transcode to lossy @@ -564,7 +564,7 @@ namespace lms::api::subsonic .bitsPerSample = 16, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, // wants a lossless codec not handled -> transcode to lossless @@ -602,7 +602,7 @@ namespace lms::api::subsonic .bitsPerSample = 24, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported, details::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, // no protocol specified @@ -632,7 +632,7 @@ namespace lms::api::subsonic .bitsPerSample = std::nullopt, }, - .expected = { details::TranscodeResult{ .reasons = { details::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, }; diff --git a/src/lms/ui/State.cpp b/src/lms/ui/State.cpp index 52f6c7e1..6d7d657c 100644 --- a/src/lms/ui/State.cpp +++ b/src/lms/ui/State.cpp @@ -25,7 +25,7 @@ #include "LmsApplication.hpp" -namespace lms::ui::state::details +namespace lms::ui::state::detail { void writeValue(std::string_view item, std::string_view value) { @@ -81,5 +81,4 @@ namespace lms::ui::state::details state.remove(); } } - -} // namespace lms::ui::state::details +} // namespace lms::ui::state::detail diff --git a/src/lms/ui/State.hpp b/src/lms/ui/State.hpp index 603233ab..8f51338c 100644 --- a/src/lms/ui/State.hpp +++ b/src/lms/ui/State.hpp @@ -27,12 +27,12 @@ namespace lms::ui::state { - namespace details + namespace detail { std::optional readValue(std::string_view item); void writeValue(std::string_view item, std::string_view value); void eraseValue(std::string_view item); - } // namespace details + } // namespace detail template void writeValue(std::string_view item, std::optional value) @@ -40,18 +40,18 @@ namespace lms::ui::state if (value.has_value()) { if constexpr (std::is_enum_v) - details::writeValue(item, std::to_string(static_cast>(*value))); + detail::writeValue(item, std::to_string(static_cast>(*value))); else - details::writeValue(item, std::to_string(*value)); + detail::writeValue(item, std::to_string(*value)); } else - details::eraseValue(item); + detail::eraseValue(item); } template std::optional readValue(std::string_view item) { - if (std::optional res{ details::readValue(item) }) + if (std::optional res{ detail::readValue(item) }) return core::stringUtils::readAs(*res); return std::nullopt; diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 9997d9e4..8762fe76 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -115,7 +115,7 @@ namespace lms::ui } } // namespace - namespace details + namespace detail { std::string getTrackPathName(const db::Track::pointer& track) { @@ -165,7 +165,7 @@ namespace lms::ui return zip::createArchiveZipper(files); } - } // namespace details + } // namespace detail DownloadArtistResource::DownloadArtistResource(db::ArtistId artistId) : _artistId{ artistId } @@ -182,7 +182,7 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createReadTransaction() }; const auto trackResults{ db::Track::find(LmsApp->getDbSession(), db::Track::FindParameters{}.setArtist(_artistId).setSortMethod(db::TrackSortMethod::DateDescAndRelease)) }; - return details::createZipper(trackResults.results); + return detail::createZipper(trackResults.results); } DownloadReleaseResource::DownloadReleaseResource(db::ReleaseId releaseId) @@ -200,7 +200,7 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto tracks{ db::Track::find(LmsApp->getDbSession(), db::Track::FindParameters{}.setRelease(_releaseId).setSortMethod(db::TrackSortMethod::Release)) }; - return details::createZipper(tracks.results); + return detail::createZipper(tracks.results); } DownloadTrackResource::DownloadTrackResource(db::TrackId trackId) @@ -210,7 +210,7 @@ namespace lms::ui db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) }; if (track) - suggestFileName(details::getTrackPathName(track) + ".zip"); + suggestFileName(detail::getTrackPathName(track) + ".zip"); } std::unique_ptr DownloadTrackResource::createZipper() @@ -224,7 +224,7 @@ namespace lms::ui return {}; } - return details::createZipper({ track }); + return detail::createZipper({ track }); } DownloadTrackListResource::DownloadTrackListResource(db::TrackListId trackListId) @@ -234,7 +234,7 @@ namespace lms::ui const db::TrackList::pointer trackList{ db::TrackList::find(LmsApp->getDbSession(), trackListId) }; if (trackList) - suggestFileName(details::getTrackListPathName(trackList) + ".zip"); + suggestFileName(detail::getTrackListPathName(trackList) + ".zip"); } std::unique_ptr DownloadTrackListResource::createZipper() @@ -244,6 +244,6 @@ namespace lms::ui db::Track::FindParameters params; params.setTrackList(_trackListId); const auto tracks{ db::Track::find(LmsApp->getDbSession(), params) }; - return details::createZipper(tracks.results); + return detail::createZipper(tracks.results); } } // namespace lms::ui From 6d27f18f19131638e82318da374e50925a6be908 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Dec 2025 22:40:45 +0100 Subject: [PATCH 18/22] Minor cleanup on audio codecs --- src/libs/audio/impl/ffmpeg/AudioFile.cpp | 68 ++--- src/libs/audio/impl/ffmpeg/AudioFile.hpp | 8 +- src/libs/audio/impl/ffmpeg/Transcoder.cpp | 18 +- src/libs/audio/impl/taglib/AudioFileInfo.cpp | 68 ++--- .../audio/include/audio/AudioProperties.hpp | 8 +- .../audio/include/audio/TranscodeTypes.hpp | 8 +- src/libs/core/CMakeLists.txt | 4 +- src/libs/core/impl/media/Codec.cpp | 68 +++++ src/libs/core/impl/media/CodecType.cpp | 108 ------- .../{ContainerType.cpp => Container.cpp} | 30 +- src/libs/core/impl/media/MimeType.cpp | 40 +-- .../core/media/{CodecType.hpp => Codec.hpp} | 16 +- .../{ContainerType.hpp => Container.hpp} | 4 +- src/libs/core/include/core/media/MimeType.hpp | 6 +- src/libs/database/impl/objects/Artist.cpp | 2 +- src/libs/database/impl/objects/Listen.cpp | 2 +- .../database/impl/objects/PodcastEpisode.cpp | 12 +- src/libs/database/impl/objects/Release.cpp | 10 +- src/libs/database/impl/objects/Track.cpp | 14 +- src/libs/database/impl/objects/TrackList.cpp | 2 +- .../database/impl/objects/detail/Types.cpp | 288 +++++++++--------- .../database/impl/objects/detail/Types.hpp | 12 +- .../include/database/objects/Filters.hpp | 14 +- .../database/objects/PodcastEpisode.hpp | 16 +- .../include/database/objects/Release.hpp | 4 +- .../include/database/objects/Track.hpp | 16 +- .../include/database/objects/detail/Types.hpp | 4 +- src/libs/database/test/Artist.cpp | 6 +- src/libs/database/test/Release.cpp | 16 +- src/libs/database/test/Track.cpp | 6 +- .../impl/endpoints/MediaRetrieval.cpp | 10 +- .../subsonic/impl/endpoints/Transcoding.cpp | 4 +- .../transcoding/TranscodeDecision.cpp | 90 +++--- src/libs/subsonic/test/TranscodeDecision.cpp | 76 ++--- src/lms/ui/explore/ReleaseView.cpp | 4 +- src/lms/ui/explore/TrackListHelpers.cpp | 2 +- .../ui/resource/AudioTranscodingResource.cpp | 6 +- src/tools/audioinfo/LmsAudioInfo.cpp | 4 +- 38 files changed, 521 insertions(+), 553 deletions(-) create mode 100644 src/libs/core/impl/media/Codec.cpp delete mode 100644 src/libs/core/impl/media/CodecType.cpp rename src/libs/core/impl/media/{ContainerType.cpp => Container.cpp} (68%) rename src/libs/core/include/core/media/{CodecType.hpp => Codec.hpp} (80%) rename src/libs/core/include/core/media/{ContainerType.hpp => Container.hpp} (91%) diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp index 2afd4617..1583eb73 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -70,83 +70,83 @@ namespace lms::audio::ffmpeg res[core::stringUtils::stringToUpper(tag->key)] = tag->value; } - std::optional avdemuxerToContainerType(std::string_view name) + std::optional avdemuxerToContainerType(std::string_view name) { if (name == "aiff") - return core::media::ContainerType::AIFF; + return core::media::Container::AIFF; if (name == "ape") - return core::media::ContainerType::APE; + return core::media::Container::APE; if (name.starts_with("asf")) - return core::media::ContainerType::ASF; + return core::media::Container::ASF; if (name == "dsf") - return core::media::ContainerType::DSF; + return core::media::Container::DSF; if (name == "flac") - return core::media::ContainerType::FLAC; + return core::media::Container::FLAC; if (name.find("mp4") != std::string_view::npos) - return core::media::ContainerType::MP4; + return core::media::Container::MP4; if (name.starts_with("mpc")) - return core::media::ContainerType::MPC; + return core::media::Container::MPC; if (name == "mp3") - return core::media::ContainerType::MPEG; + return core::media::Container::MPEG; if (name == "ogg") - return core::media::ContainerType::Ogg; + return core::media::Container::Ogg; if (name == "shn") - return core::media::ContainerType::Shorten; + return core::media::Container::Shorten; if (name == "tta") - return core::media::ContainerType::TrueAudio; + return core::media::Container::TrueAudio; if (name == "wav") - return core::media::ContainerType::WAV; + return core::media::Container::WAV; if (name == "wv") - return core::media::ContainerType::WavPack; + return core::media::Container::WavPack; return std::nullopt; } - std::optional avcodecToCodecType(AVCodecID codec) + std::optional avcodecToCodecType(AVCodecID codec) { switch (codec) { case AV_CODEC_ID_AAC: - return core::media::CodecType::AAC; + return core::media::Codec::AAC; case AV_CODEC_ID_AC3: - return core::media::CodecType::AC3; + return core::media::Codec::AC3; case AV_CODEC_ID_ALAC: - return core::media::CodecType::ALAC; + return core::media::Codec::ALAC; case AV_CODEC_ID_APE: - return core::media::CodecType::APE; + 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 core::media::CodecType::DSD; + return core::media::Codec::DSD; case AV_CODEC_ID_EAC3: - return core::media::CodecType::EAC3; + return core::media::Codec::EAC3; case AV_CODEC_ID_FLAC: - return core::media::CodecType::FLAC; + return core::media::Codec::FLAC; case AV_CODEC_ID_MP3: - return core::media::CodecType::MP3; + return core::media::Codec::MP3; case AV_CODEC_ID_MP4ALS: - return core::media::CodecType::MP4ALS; + return core::media::Codec::MP4ALS; case AV_CODEC_ID_MUSEPACK7: - return core::media::CodecType::MPC7; + return core::media::Codec::MPC7; case AV_CODEC_ID_MUSEPACK8: - return core::media::CodecType::MPC8; + return core::media::Codec::MPC8; case AV_CODEC_ID_OPUS: - return core::media::CodecType::Opus; + return core::media::Codec::Opus; case AV_CODEC_ID_SHORTEN: - return core::media::CodecType::Shorten; + return core::media::Codec::Shorten; case AV_CODEC_ID_VORBIS: - return core::media::CodecType::Vorbis; + return core::media::Codec::Vorbis; case AV_CODEC_ID_WAVPACK: - return core::media::CodecType::WavPack; + return core::media::Codec::WavPack; case AV_CODEC_ID_WMALOSSLESS: - return core::media::CodecType::WMA9Lossless; + return core::media::Codec::WMA9Lossless; case AV_CODEC_ID_WMAPRO: - return core::media::CodecType::WMA9Pro; + return core::media::Codec::WMA9Pro; case AV_CODEC_ID_WMAV1: - return core::media::CodecType::WMA1; + return core::media::Codec::WMA1; case AV_CODEC_ID_WMAV2: - return core::media::CodecType::WMA2; + return core::media::Codec::WMA2; default: return std::nullopt; diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp index 61aa973c..fdf0ac65 100644 --- a/src/libs/audio/impl/ffmpeg/AudioFile.hpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -27,8 +27,8 @@ #include #include -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" extern "C" { @@ -45,7 +45,7 @@ namespace lms::audio::ffmpeg struct ContainerInfo { - std::optional container; + std::optional container; std::string containerName; std::optional bitrate; @@ -55,7 +55,7 @@ namespace lms::audio::ffmpeg struct StreamInfo { size_t index{}; - std::optional codec; + std::optional codec; std::string codecName; std::optional bitrate; diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp index f146adca..026e861b 100644 --- a/src/libs/audio/impl/ffmpeg/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -161,42 +161,42 @@ namespace lms::audio::ffmpeg switch (_outputParams.format->container) { - case core::media::ContainerType::FLAC: + case core::media::Container::FLAC: args.emplace_back("flac"); break; - case core::media::ContainerType::Ogg: + case core::media::Container::Ogg: args.emplace_back("ogg"); break; - case core::media::ContainerType::MPEG: + case core::media::Container::MPEG: args.emplace_back("mp3"); break; default: - throw Exception{ "Unsupported container type " + std::string{ core::media::containerTypeToString(_outputParams.format->container).str() } }; + 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::CodecType::MP3: + case core::media::Codec::MP3: args.emplace_back("libmp3lame"); break; - case core::media::CodecType::Opus: + case core::media::Codec::Opus: args.emplace_back("libopus"); break; - case core::media::CodecType::Vorbis: + case core::media::Codec::Vorbis: args.emplace_back("libvorbis"); break; - case core::media::CodecType::FLAC: + case core::media::Codec::FLAC: args.emplace_back("flac"); break; default: - throw Exception{ "Unhandled codec type " + std::string{ codecTypeToString(_outputParams.format->codec).str() } }; + throw Exception{ "Unhandled codec type " + std::string{ core::media::getCodecDesc(_outputParams.format->codec).name.str() } }; } } diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp index 1a749dd7..b0251481 100644 --- a/src/libs/audio/impl/taglib/AudioFileInfo.cpp +++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp @@ -104,27 +104,27 @@ namespace lms::audio::taglib // Guess container from the file type if (const auto* apeFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::APE; - audioProperties.codec = core::media::CodecType::APE; // TODO version? + audioProperties.container = core::media::Container::APE; + audioProperties.codec = core::media::Codec::APE; // TODO version? audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample(); } else if (const auto* asfFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::ASF; + audioProperties.container = core::media::Container::ASF; switch (asfFile->audioProperties()->codec()) { case ::TagLib::ASF::Properties::Codec::WMA1: - audioProperties.codec = core::media::CodecType::WMA1; + audioProperties.codec = core::media::Codec::WMA1; break; case ::TagLib::ASF::Properties::Codec::WMA2: - audioProperties.codec = core::media::CodecType::WMA2; + audioProperties.codec = core::media::Codec::WMA2; break; case ::TagLib::ASF::Properties::Codec::WMA9Lossless: - audioProperties.codec = core::media::CodecType::WMA9Lossless; + audioProperties.codec = core::media::Codec::WMA9Lossless; break; case ::TagLib::ASF::Properties::Codec::WMA9Pro: - audioProperties.codec = core::media::CodecType::WMA9Pro; + audioProperties.codec = core::media::Codec::WMA9Pro; break; case ::TagLib::ASF::Properties::Codec::Unknown: LMS_LOG(AUDIO, DEBUG, "Unhandled ASF codec in " << filePath); @@ -136,27 +136,27 @@ namespace lms::audio::taglib #if LMS_TAGLIB_HAS_DSF else if (const auto* dsfFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::DSF; - audioProperties.codec = core::media::CodecType::DSD; + audioProperties.container = core::media::Container::DSF; + audioProperties.codec = core::media::Codec::DSD; audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample(); } #endif // LMS_TAGLIB_HAS_DSF else if (const auto* flacFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::FLAC; - audioProperties.codec = core::media::CodecType::FLAC; + audioProperties.container = core::media::Container::FLAC; + audioProperties.codec = core::media::Codec::FLAC; audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample(); } else if (const auto* mp4File{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::MP4; + audioProperties.container = core::media::Container::MP4; switch (mp4File->audioProperties()->codec()) { case ::TagLib::MP4::Properties::Codec::AAC: - audioProperties.codec = core::media::CodecType::AAC; + audioProperties.codec = core::media::Codec::AAC; break; case ::TagLib::MP4::Properties::Codec::ALAC: - audioProperties.codec = core::media::CodecType::ALAC; + audioProperties.codec = core::media::Codec::ALAC; break; case ::TagLib::MP4::Properties::Codec::Unknown: LMS_LOG(AUDIO, DEBUG, "Unhandled MP4 codec in " << filePath); @@ -167,15 +167,15 @@ namespace lms::audio::taglib } else if (const auto* mpcFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::MPC; + audioProperties.container = core::media::Container::MPC; switch (mpcFile->audioProperties()->mpcVersion()) { case 7: - audioProperties.codec = core::media::CodecType::MPC7; + audioProperties.codec = core::media::Codec::MPC7; break; case 8: - audioProperties.codec = core::media::CodecType::MPC8; + audioProperties.codec = core::media::Codec::MPC8; break; default: LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath); @@ -186,13 +186,13 @@ namespace lms::audio::taglib { const auto& properties{ *mpegFile->audioProperties() }; - audioProperties.container = core::media::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 = core::media::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 = core::media::CodecType::AAC; + audioProperties.codec = core::media::Codec::AAC; #endif else { @@ -202,44 +202,44 @@ namespace lms::audio::taglib } else if (dynamic_cast(&file)) { - audioProperties.container = core::media::ContainerType::Ogg; - audioProperties.codec = core::media::CodecType::Opus; + audioProperties.container = core::media::Container::Ogg; + audioProperties.codec = core::media::Codec::Opus; } else if (dynamic_cast(&file)) { - audioProperties.container = core::media::ContainerType::Ogg; - audioProperties.codec = core::media::CodecType::Vorbis; + audioProperties.container = core::media::Container::Ogg; + audioProperties.codec = core::media::Codec::Vorbis; } else if (const auto* aiffFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::AIFF; - audioProperties.codec = core::media::CodecType::PCM; + audioProperties.container = core::media::Container::AIFF; + audioProperties.codec = core::media::Codec::PCM; audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample(); } else if (const auto* wavFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::WAV; - audioProperties.codec = core::media::CodecType::PCM; + audioProperties.container = core::media::Container::WAV; + audioProperties.codec = core::media::Codec::PCM; audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample(); } #if LMS_TAGLIB_HAS_SHORTEN else if (const auto* shortenFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::Shorten; - audioProperties.codec = core::media::CodecType::Shorten; + audioProperties.container = core::media::Container::Shorten; + audioProperties.codec = core::media::Codec::Shorten; audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample(); } #endif // LMS_TAGLIB_HAS_SHORTEN else if (const auto* trueAudioFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::TrueAudio; - audioProperties.codec = core::media::CodecType::TrueAudio; + audioProperties.container = core::media::Container::TrueAudio; + audioProperties.codec = core::media::Codec::TrueAudio; audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample(); } else if (const auto* wavPackFile{ dynamic_cast(&file) }) { - audioProperties.container = core::media::ContainerType::WavPack; - audioProperties.codec = core::media::CodecType::WavPack; + audioProperties.container = core::media::Container::WavPack; + audioProperties.codec = core::media::Codec::WavPack; audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample(); } else diff --git a/src/libs/audio/include/audio/AudioProperties.hpp b/src/libs/audio/include/audio/AudioProperties.hpp index 2b70ee7f..07dadd0b 100644 --- a/src/libs/audio/include/audio/AudioProperties.hpp +++ b/src/libs/audio/include/audio/AudioProperties.hpp @@ -22,15 +22,15 @@ #include #include -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" namespace lms::audio { struct AudioProperties { - core::media::ContainerType container; - core::media::CodecType codec; + core::media::Container container; + core::media::Codec codec; std::chrono::milliseconds duration; unsigned bitrate; unsigned channelCount; diff --git a/src/libs/audio/include/audio/TranscodeTypes.hpp b/src/libs/audio/include/audio/TranscodeTypes.hpp index a2324646..0dfcf4b3 100644 --- a/src/libs/audio/include/audio/TranscodeTypes.hpp +++ b/src/libs/audio/include/audio/TranscodeTypes.hpp @@ -23,8 +23,8 @@ #include #include -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" #include "audio/AudioProperties.hpp" @@ -39,8 +39,8 @@ namespace lms::audio struct TranscodeOutputFormat { - core::media::ContainerType container; - core::media::CodecType codec; + core::media::Container container; + core::media::Codec codec; }; struct TranscodeOutputParameters diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index 0ddfff3a..d8ddf79d 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -13,8 +13,8 @@ configure_file( add_library(lmscore STATIC impl/http/Client.cpp impl/http/SendQueue.cpp - impl/media/CodecType.cpp - impl/media/ContainerType.cpp + impl/media/Codec.cpp + impl/media/Container.cpp impl/media/ImageType.cpp impl/media/MimeType.cpp impl/ArchiveZipper.cpp diff --git a/src/libs/core/impl/media/Codec.cpp b/src/libs/core/impl/media/Codec.cpp new file mode 100644 index 00000000..17ff0a5e --- /dev/null +++ b/src/libs/core/impl/media/Codec.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "core/media/Codec.hpp" +#include "core/Exception.hpp" + +#include +#include + +namespace lms::core::media +{ + namespace + { + constexpr std::array codecDescs{ + CodecDesc{ Codec::AAC, "AAC", "Advanced Audio Coding", false }, + CodecDesc{ Codec::AC3, "AC3", "Dolby AC-3", false }, + CodecDesc{ Codec::ALAC, "ALAC", "Apple Lossless Audio Codec", true }, + CodecDesc{ Codec::APE, "APE", "Monkey's Audio", true }, + CodecDesc{ Codec::DSD, "DSD", "Direct Stream Digital", true }, + CodecDesc{ Codec::EAC3, "E-AC3", "Dolby Digital Plus", false }, + CodecDesc{ Codec::FLAC, "FLAC", "Free Lossless Audio Codec", true }, + CodecDesc{ Codec::MP3, "MP3", "MPEG-3", false }, + CodecDesc{ Codec::MP4ALS, "MP4ALS", "MPEG-4 Audio Lossless Coding", true }, + CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false }, + CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false }, + CodecDesc{ Codec::Opus, "Opus", "Opus", false }, + CodecDesc{ Codec::PCM, "PCM", "Pulse-code modulation", false }, + CodecDesc{ Codec::Shorten, "Shorten", "Shorten", true }, + CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true }, + CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false }, + CodecDesc{ Codec::WavPack, "WavPack", "WavPack", true }, + CodecDesc{ Codec::WMA1, "WMA1", "Windows Media Audio", false }, + CodecDesc{ Codec::WMA2, "WMA2", "Windows Media Audio 2", false }, + CodecDesc{ Codec::WMA9Pro, "WMA9Pro", "Windows Media Audio Professional", false }, + CodecDesc{ Codec::WMA9Lossless, "WMA9Lossless", "Windows Media Audio Lossless", true }, + }; + } // namespace + + void visitCodecs(const std::function& visitor) + { + std::for_each(std::cbegin(codecDescs), std::cend(codecDescs), visitor); + } + + const CodecDesc& getCodecDesc(Codec type) + { + const auto it{ std::find_if(codecDescs.begin(), codecDescs.end(), [type](const auto& desc) { return desc.type == type; }) }; + if (it == codecDescs.end()) [[unlikely]] + throw LmsException{ "Unknown codec type" }; + + return *it; + } +} // namespace lms::core::media diff --git a/src/libs/core/impl/media/CodecType.cpp b/src/libs/core/impl/media/CodecType.cpp deleted file mode 100644 index 2a0bc587..00000000 --- a/src/libs/core/impl/media/CodecType.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2025 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "core/media/CodecType.hpp" - -namespace lms::core::media -{ - core::LiteralString codecTypeToString(CodecType type) - { - switch (type) - { - case CodecType::AAC: - return "AAC"; - case CodecType::AC3: - return "AC3"; - case CodecType::ALAC: - return "ALAC"; - case CodecType::APE: - return "APE"; - case CodecType::DSD: - return "DSD"; - case CodecType::EAC3: - return "E-AC3"; - 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 "Unknown"; - } - - bool isCodecLossless(CodecType type) - { - switch (type) - { - case CodecType::ALAC: - case CodecType::APE: - case CodecType::DSD: - - case CodecType::FLAC: - case CodecType::MP4ALS: - case CodecType::PCM: - case CodecType::Shorten: - case CodecType::TrueAudio: - case CodecType::WavPack: - case CodecType::WMA9Lossless: - return true; - - case CodecType::AAC: - case CodecType::AC3: - case CodecType::EAC3: - case CodecType::MP3: - case CodecType::MPC7: - case CodecType::MPC8: - case CodecType::Opus: - case CodecType::Vorbis: - case CodecType::WMA1: - case CodecType::WMA2: - case CodecType::WMA9Pro: - return false; - } - - return false; - } -} // namespace lms::core::media diff --git a/src/libs/core/impl/media/ContainerType.cpp b/src/libs/core/impl/media/Container.cpp similarity index 68% rename from src/libs/core/impl/media/ContainerType.cpp rename to src/libs/core/impl/media/Container.cpp index cf1fe447..6bb0698e 100644 --- a/src/libs/core/impl/media/ContainerType.cpp +++ b/src/libs/core/impl/media/Container.cpp @@ -17,39 +17,39 @@ * along with LMS. If not, see . */ -#include "core/media/ContainerType.hpp" +#include "core/media/Container.hpp" namespace lms::core::media { - core::LiteralString containerTypeToString(ContainerType type) + core::LiteralString containerToString(Container type) { switch (type) { - case ContainerType::AIFF: + case Container::AIFF: return "AIFF"; - case ContainerType::APE: + case Container::APE: return "APE"; - case ContainerType::ASF: + case Container::ASF: return "ASF"; - case ContainerType::DSF: + case Container::DSF: return "DSF"; - case ContainerType::FLAC: + case Container::FLAC: return "FLAC"; - case ContainerType::MP4: + case Container::MP4: return "MP4"; - case ContainerType::MPC: + case Container::MPC: return "MPC"; - case ContainerType::MPEG: + case Container::MPEG: return "MPEG"; - case ContainerType::Ogg: + case Container::Ogg: return "Ogg"; - case ContainerType::Shorten: + case Container::Shorten: return "Shorten"; - case ContainerType::TrueAudio: + case Container::TrueAudio: return "TrueAudio"; - case ContainerType::WAV: + case Container::WAV: return "WAV"; - case ContainerType::WavPack: + case Container::WavPack: return "WavPack"; } diff --git a/src/libs/core/impl/media/MimeType.cpp b/src/libs/core/impl/media/MimeType.cpp index aa8d1d1f..ef17cba3 100644 --- a/src/libs/core/impl/media/MimeType.cpp +++ b/src/libs/core/impl/media/MimeType.cpp @@ -21,57 +21,57 @@ namespace lms::core::media { - core::LiteralString getMimeType(ContainerType container, CodecType codec) + core::LiteralString getMimeType(Container container, Codec codec) { switch (container) { - case ContainerType::AIFF: + case Container::AIFF: return "audio/x-aiff"; - case ContainerType::APE: + case Container::APE: return "audio/x-monkeys-audio"; - case ContainerType::ASF: + case Container::ASF: return "audio/x-ms-wma"; - case ContainerType::DSF: + case Container::DSF: return "audio/x-dsd-dsf"; - case ContainerType::FLAC: + case Container::FLAC: return "audio/flac"; - case ContainerType::MP4: + case Container::MP4: switch (codec) { - case CodecType::AAC: + case Codec::AAC: return "audio/mp4; codecs=\"mp4a.40.2\""; - case CodecType::ALAC: + case Codec::ALAC: return "audio/mp4; codecs=\"alac\""; - case CodecType::MP4ALS: + case Codec::MP4ALS: return "audio/mp4; codecs=\"mp4als\""; default: return "audio/mp4"; } - case ContainerType::MPC: + case Container::MPC: return "audio/x-musepack"; - case ContainerType::MPEG: + case Container::MPEG: return "audio/mpeg"; - case ContainerType::Ogg: + case Container::Ogg: switch (codec) { - case CodecType::Opus: + case Codec::Opus: return "audio/opus"; - case CodecType::Vorbis: + case Codec::Vorbis: return "audio/ogg; codecs=\"vorbis\""; - case CodecType::FLAC: + case Codec::FLAC: return "audio/ogg; codecs=\"flac\""; default: return "audio/ogg"; } - case ContainerType::Shorten: + case Container::Shorten: return "audio/x-shn"; - case ContainerType::TrueAudio: + case Container::TrueAudio: return "audio/x-tta"; - case ContainerType::WAV: + case Container::WAV: return "audio/wav"; - case ContainerType::WavPack: + case Container::WavPack: return "audio/x-wavpack"; } diff --git a/src/libs/core/include/core/media/CodecType.hpp b/src/libs/core/include/core/media/Codec.hpp similarity index 80% rename from src/libs/core/include/core/media/CodecType.hpp rename to src/libs/core/include/core/media/Codec.hpp index 3311a253..06438922 100644 --- a/src/libs/core/include/core/media/CodecType.hpp +++ b/src/libs/core/include/core/media/Codec.hpp @@ -19,11 +19,13 @@ #pragma once +#include + #include "core/LiteralString.hpp" namespace lms::core::media { - enum class CodecType + enum class Codec { AAC, AC3, @@ -48,7 +50,13 @@ namespace lms::core::media WMA9Lossless, }; - core::LiteralString codecTypeToString(CodecType type); - - bool isCodecLossless(CodecType type); + struct CodecDesc + { + Codec type; + core::LiteralString name; + core::LiteralString longName; + bool isLossless; + }; + void visitCodecs(const std::function& visitor); + const CodecDesc& getCodecDesc(Codec type); } // namespace lms::core::media diff --git a/src/libs/core/include/core/media/ContainerType.hpp b/src/libs/core/include/core/media/Container.hpp similarity index 91% rename from src/libs/core/include/core/media/ContainerType.hpp rename to src/libs/core/include/core/media/Container.hpp index 709d8203..b53af373 100644 --- a/src/libs/core/include/core/media/ContainerType.hpp +++ b/src/libs/core/include/core/media/Container.hpp @@ -23,7 +23,7 @@ namespace lms::core::media { - enum class ContainerType + enum class Container { AIFF, APE, // Monkey's Audio @@ -40,5 +40,5 @@ namespace lms::core::media WavPack, }; - core::LiteralString containerTypeToString(ContainerType type); + core::LiteralString containerToString(Container type); } // namespace lms::core::media \ No newline at end of file diff --git a/src/libs/core/include/core/media/MimeType.hpp b/src/libs/core/include/core/media/MimeType.hpp index 319d70de..6f24f545 100644 --- a/src/libs/core/include/core/media/MimeType.hpp +++ b/src/libs/core/include/core/media/MimeType.hpp @@ -20,10 +20,10 @@ #pragma once #include "core/LiteralString.hpp" -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" namespace lms::core::media { - core::LiteralString getMimeType(ContainerType container, CodecType codec); + core::LiteralString getMimeType(Container container, Codec codec); } \ No newline at end of file diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp index 04547009..15cacc41 100644 --- a/src/libs/database/impl/objects/Artist.cpp +++ b/src/libs/database/impl/objects/Artist.cpp @@ -83,7 +83,7 @@ namespace lms::db query.where("t.release_id = ?").bind(params.release); if (params.filters.codec.has_value()) - query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp index 8ec09058..3c146aa5 100644 --- a/src/libs/database/impl/objects/Listen.cpp +++ b/src/libs/database/impl/objects/Listen.cpp @@ -57,7 +57,7 @@ namespace lms::db query.join("track t ON t.id = t_a_l.track_id"); if (params.filters.codec.has_value()) - query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/impl/objects/PodcastEpisode.cpp b/src/libs/database/impl/objects/PodcastEpisode.cpp index a8aab5f2..d831f8da 100644 --- a/src/libs/database/impl/objects/PodcastEpisode.cpp +++ b/src/libs/database/impl/objects/PodcastEpisode.cpp @@ -99,12 +99,12 @@ namespace lms::db utils::forEachQueryRangeResult(query, params.range, func); } - std::optional PodcastEpisode::getContainer() const + std::optional PodcastEpisode::getContainer() const { return detail::getMediaContainerType(_container); } - std::optional PodcastEpisode::getCodec() const + std::optional PodcastEpisode::getCodec() const { return detail::getMediaCodecType(_codec); } @@ -119,14 +119,14 @@ namespace lms::db return _artwork.id(); } - void PodcastEpisode::setContainer(core::media::ContainerType container) + void PodcastEpisode::setContainer(core::media::Container container) { - _container = detail::getDbContainerType(container); + _container = detail::getDbContainer(container); } - void PodcastEpisode::setCodec(core::media::CodecType codec) + void PodcastEpisode::setCodec(core::media::Codec codec) { - _codec = detail::getDbCodecType(codec); + _codec = detail::getDbCodec(codec); } void PodcastEpisode::setArtwork(ObjectPtr artwork) diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index d0442de6..4525289d 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -220,7 +220,7 @@ namespace lms::db } if (params.filters.codec.has_value()) - query.where("t.codec = ?").bind(detail::getDbCodecType(params.filters.codec.value())); + query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value())); if (params.releaseGroupMBID) query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString()); @@ -684,15 +684,15 @@ namespace lms::db return utils::fetchQuerySingleResult(session()->query("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t").where("release_id = ?").bind(getId()).where("bitrate > 0")); } - std::vector Release::getCodecs() const + std::vector Release::getCodecs() const { assert(session()); // Get the codec ordered by frequency - auto query{ session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") }; + auto query{ session()->query("SELECT t.codec FROM track t").where("release_id = ?").bind(getId()).groupBy("t.codec").orderBy("COUNT(t.id) DESC") }; - std::vector res; - utils::forEachQueryResult(query, [&](detail::CodecType codec) { + std::vector res; + utils::forEachQueryResult(query, [&](detail::Codec codec) { if (const auto mediaCodecType{ detail::getMediaCodecType(codec) }) res.push_back(*mediaCodecType); }); diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index b089e19e..bd7acaac 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -198,7 +198,7 @@ namespace lms::db } if (params.filters.codec.has_value()) - query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); switch (params.sortMethod) { @@ -528,14 +528,14 @@ namespace lms::db _absoluteFilePath = filePath; } - void Track::setContainer(core::media::ContainerType container) + void Track::setContainer(core::media::Container container) { - _container = detail::getDbContainerType(container); + _container = detail::getDbContainer(container); } - void Track::setCodec(core::media::CodecType codec) + void Track::setCodec(core::media::Codec codec) { - _codec = detail::getDbCodecType(codec); + _codec = detail::getDbCodec(codec); } void Track::setName(std::string_view name) @@ -618,12 +618,12 @@ namespace lms::db _preferredMediaArtwork = getDboPtr(artwork); } - std::optional Track::getContainer() const + std::optional Track::getContainer() const { return detail::getMediaContainerType(_container); } - std::optional Track::getCodec() const + std::optional Track::getCodec() const { return detail::getMediaCodecType(_codec); } diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp index 55c4b06e..2f35e473 100644 --- a/src/libs/database/impl/objects/TrackList.cpp +++ b/src/libs/database/impl/objects/TrackList.cpp @@ -70,7 +70,7 @@ namespace lms::db query.join("track t ON t.id = t_l_e.track_id"); if (params.filters.codec.has_value()) - query.where("t.codec = ?").bind(detail::getDbCodecType(*params.filters.codec)); + query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); if (params.filters.mediaLibrary.isValid()) query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); diff --git a/src/libs/database/impl/objects/detail/Types.cpp b/src/libs/database/impl/objects/detail/Types.cpp index aa2573ed..5fe01d0b 100644 --- a/src/libs/database/impl/objects/detail/Types.cpp +++ b/src/libs/database/impl/objects/detail/Types.cpp @@ -21,182 +21,182 @@ namespace lms::db::detail { - std::optional getMediaContainerType(db::detail::ContainerType container) + std::optional getMediaContainerType(db::detail::Container container) { switch (container) { - case ContainerType::AIFF: - return core::media::ContainerType::AIFF; - case ContainerType::APE: - return core::media::ContainerType::APE; - case ContainerType::ASF: - return core::media::ContainerType::ASF; - case ContainerType::DSF: - return core::media::ContainerType::DSF; - case ContainerType::FLAC: - return core::media::ContainerType::FLAC; - case ContainerType::MP4: - return core::media::ContainerType::MP4; - case ContainerType::MPC: - return core::media::ContainerType::MPC; - case ContainerType::MPEG: - return core::media::ContainerType::MPEG; - case ContainerType::Ogg: - return core::media::ContainerType::Ogg; - case ContainerType::Shorten: - return core::media::ContainerType::Shorten; - case ContainerType::TrueAudio: - return core::media::ContainerType::TrueAudio; - case ContainerType::WAV: - return core::media::ContainerType::WAV; - case ContainerType::WavPack: - return core::media::ContainerType::WavPack; + 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 ContainerType::Unknown: + case Container::Unknown: break; } return std::nullopt; } - db::detail::ContainerType getDbContainerType(core::media::ContainerType container) + db::detail::Container getDbContainer(core::media::Container container) { switch (container) { - case core::media::ContainerType::AIFF: - return ContainerType::AIFF; - case core::media::ContainerType::APE: - return ContainerType::APE; - case core::media::ContainerType::ASF: - return ContainerType::ASF; - case core::media::ContainerType::DSF: - return ContainerType::DSF; - case core::media::ContainerType::FLAC: - return ContainerType::FLAC; - case core::media::ContainerType::MP4: - return ContainerType::MP4; - case core::media::ContainerType::MPC: - return ContainerType::MPC; - case core::media::ContainerType::MPEG: - return ContainerType::MPEG; - case core::media::ContainerType::Ogg: - return ContainerType::Ogg; - case core::media::ContainerType::Shorten: - return ContainerType::Shorten; - case core::media::ContainerType::TrueAudio: - return ContainerType::TrueAudio; - case core::media::ContainerType::WAV: - return ContainerType::WAV; - case core::media::ContainerType::WavPack: - return ContainerType::WavPack; + 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 ContainerType::Unknown; + return Container::Unknown; } - std::optional getMediaCodecType(db::detail::CodecType codec) + std::optional getMediaCodecType(db::detail::Codec codec) { switch (codec) { - case CodecType::AAC: - return core::media::CodecType::AAC; - case CodecType::AC3: - return core::media::CodecType::AC3; - case CodecType::ALAC: - return core::media::CodecType::ALAC; - case CodecType::APE: - return core::media::CodecType::APE; - case CodecType::DSD: - return core::media::CodecType::DSD; - case CodecType::EAC3: - return core::media::CodecType::EAC3; - case CodecType::FLAC: - return core::media::CodecType::FLAC; - case CodecType::MP3: - return core::media::CodecType::MP3; - case CodecType::MP4ALS: - return core::media::CodecType::MP4ALS; - case CodecType::MPC7: - return core::media::CodecType::MPC7; - case CodecType::MPC8: - return core::media::CodecType::MPC8; - case CodecType::Opus: - return core::media::CodecType::Opus; - case CodecType::PCM: - return core::media::CodecType::PCM; - case CodecType::Shorten: - return core::media::CodecType::Shorten; - case CodecType::TrueAudio: - return core::media::CodecType::TrueAudio; - case CodecType::Vorbis: - return core::media::CodecType::Vorbis; - case CodecType::WavPack: - return core::media::CodecType::WavPack; - case CodecType::WMA1: - return core::media::CodecType::WMA1; - case CodecType::WMA2: - return core::media::CodecType::WMA2; - case CodecType::WMA9Pro: - return core::media::CodecType::WMA9Pro; - case CodecType::WMA9Lossless: - return core::media::CodecType::WMA9Lossless; + 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 CodecType::Unknown: + case Codec::Unknown: break; } return std::nullopt; } - db::detail::CodecType getDbCodecType(core::media::CodecType codec) + db::detail::Codec getDbCodec(core::media::Codec codec) { switch (codec) { - case core::media::CodecType::AAC: - return CodecType::AAC; - case core::media::CodecType::AC3: - return CodecType::AC3; - case core::media::CodecType::ALAC: - return CodecType::ALAC; - case core::media::CodecType::APE: - return CodecType::APE; - case core::media::CodecType::DSD: - return CodecType::DSD; - case core::media::CodecType::EAC3: - return CodecType::EAC3; - case core::media::CodecType::FLAC: - return CodecType::FLAC; - case core::media::CodecType::MP3: - return CodecType::MP3; - case core::media::CodecType::MP4ALS: - return CodecType::MP4ALS; - case core::media::CodecType::MPC7: - return CodecType::MPC7; - case core::media::CodecType::MPC8: - return CodecType::MPC8; - case core::media::CodecType::Opus: - return CodecType::Opus; - case core::media::CodecType::PCM: - return CodecType::PCM; - case core::media::CodecType::Shorten: - return CodecType::Shorten; - case core::media::CodecType::TrueAudio: - return CodecType::TrueAudio; - case core::media::CodecType::Vorbis: - return CodecType::Vorbis; - case core::media::CodecType::WavPack: - return CodecType::WavPack; - case core::media::CodecType::WMA1: - return CodecType::WMA1; - case core::media::CodecType::WMA2: - return CodecType::WMA2; - case core::media::CodecType::WMA9Pro: - return CodecType::WMA9Pro; - case core::media::CodecType::WMA9Lossless: - return CodecType::WMA9Lossless; + 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 CodecType::Unknown; + return Codec::Unknown; } core::media::ImageType getMediaImageType(db::detail::ImageType type) diff --git a/src/libs/database/impl/objects/detail/Types.hpp b/src/libs/database/impl/objects/detail/Types.hpp index eed0b5e4..ba7dc789 100644 --- a/src/libs/database/impl/objects/detail/Types.hpp +++ b/src/libs/database/impl/objects/detail/Types.hpp @@ -21,19 +21,19 @@ #include -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" #include "core/media/ImageType.hpp" #include "database/objects/detail/Types.hpp" namespace lms::db::detail { - std::optional getMediaContainerType(db::detail::ContainerType container); - db::detail::ContainerType getDbContainerType(core::media::ContainerType container); + std::optional getMediaContainerType(db::detail::Container container); + db::detail::Container getDbContainer(core::media::Container container); - std::optional getMediaCodecType(db::detail::CodecType codec); - db::detail::CodecType getDbCodecType(core::media::CodecType codec); + std::optional getMediaCodecType(db::detail::Codec codec); + db::detail::Codec getDbCodec(core::media::Codec codec); core::media::ImageType getMediaImageType(db::detail::ImageType type); db::detail::ImageType getDbImageType(core::media::ImageType type); diff --git a/src/libs/database/include/database/objects/Filters.hpp b/src/libs/database/include/database/objects/Filters.hpp index ead8f741..a31d9de8 100644 --- a/src/libs/database/include/database/objects/Filters.hpp +++ b/src/libs/database/include/database/objects/Filters.hpp @@ -23,7 +23,7 @@ #include #include -#include "core/media/CodecType.hpp" +#include "core/media/Codec.hpp" #include "database/objects/ClusterId.hpp" #include "database/objects/LabelId.hpp" @@ -34,11 +34,11 @@ namespace lms::db { struct Filters { - MediaLibraryId mediaLibrary; // tracks that belongs to this library - std::vector clusters; // tracks that belong to *all* these clusters - LabelId label; // tracks which release has this label - ReleaseTypeId releaseType; // tracks which release has this type - std::optional codec; // tracks that match this codec + MediaLibraryId mediaLibrary; // tracks that belongs to this library + std::vector clusters; // tracks that belong to *all* these clusters + LabelId label; // tracks which release has this label + ReleaseTypeId releaseType; // tracks which release has this type + std::optional codec; // tracks that match this codec Filters& setClusters(std::span _clusters) { @@ -60,7 +60,7 @@ namespace lms::db releaseType = _releaseType; return *this; } - Filters& setCodec(std::optional _codec) + Filters& setCodec(std::optional _codec) { codec = _codec; return *this; diff --git a/src/libs/database/include/database/objects/PodcastEpisode.hpp b/src/libs/database/include/database/objects/PodcastEpisode.hpp index 7911375c..eb226ca2 100644 --- a/src/libs/database/include/database/objects/PodcastEpisode.hpp +++ b/src/libs/database/include/database/objects/PodcastEpisode.hpp @@ -29,8 +29,8 @@ #include #include -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" #include "database/Object.hpp" #include "database/Types.hpp" @@ -97,8 +97,8 @@ namespace lms::db // Audio properties std::chrono::milliseconds getDuration() const { return _duration; } - std::optional getContainer() const; - std::optional getCodec() const; + std::optional getContainer() const; + std::optional getCodec() const; std::size_t getBitrate() const { return _bitrate; } std::size_t getChannelCount() const { return _channelCount; } std::size_t getSampleRate() const { return _sampleRate; } @@ -128,8 +128,8 @@ namespace lms::db // Audio properties void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setContainer(core::media::ContainerType container); - void setCodec(core::media::CodecType codec); + 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; } @@ -193,8 +193,8 @@ namespace lms::db // Audio properties std::chrono::duration _duration{ 0 }; - detail::ContainerType _container{ detail::ContainerType::Unknown }; - detail::CodecType _codec{ detail::CodecType::Unknown }; + detail::Container _container{ detail::Container::Unknown }; + detail::Codec _codec{ detail::Codec::Unknown }; int _bitrate{}; // in bps int _channelCount{}; int _sampleRate{}; diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index b2a078e4..360b20d7 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -31,7 +31,7 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" -#include "core/media/CodecType.hpp" +#include "core/media/Codec.hpp" #include "database/IdRange.hpp" #include "database/Object.hpp" @@ -280,7 +280,7 @@ namespace lms::db std::optional getCopyright() const; std::optional getCopyrightURL() const; std::size_t getMeanBitrate() const; - std::vector getCodecs() const; + std::vector getCodecs() const; // Accessors std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index 3cdfc664..e5fc2efa 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -34,8 +34,8 @@ #include "core/EnumSet.hpp" #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" #include "database/IdRange.hpp" #include "database/Object.hpp" @@ -232,8 +232,8 @@ namespace lms::db // Audio properties void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setContainer(core::media::ContainerType container); - void setCodec(core::media::CodecType codec); + 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; } @@ -278,8 +278,8 @@ namespace lms::db // Audio properties std::chrono::milliseconds getDuration() const { return _duration; } - std::optional getContainer() const; - std::optional getCodec() const; + std::optional getContainer() const; + std::optional getCodec() const; std::size_t getBitrate() const { return _bitrate; } std::size_t getChannelCount() const { return _channelCount; } std::size_t getSampleRate() const { return _sampleRate; } @@ -383,8 +383,8 @@ namespace lms::db // Audio properties std::chrono::duration _duration{}; - detail::ContainerType _container{ detail::ContainerType::Unknown }; - detail::CodecType _codec{ detail::CodecType::Unknown }; + detail::Container _container{ detail::Container::Unknown }; + detail::Codec _codec{ detail::Codec::Unknown }; int _bitrate{}; // in bps int _channelCount{}; int _sampleRate{}; diff --git a/src/libs/database/include/database/objects/detail/Types.hpp b/src/libs/database/include/database/objects/detail/Types.hpp index 854a3c87..1001a2b8 100644 --- a/src/libs/database/include/database/objects/detail/Types.hpp +++ b/src/libs/database/include/database/objects/detail/Types.hpp @@ -49,7 +49,7 @@ namespace lms::db::detail PublisherLogo = 21 }; - enum class ContainerType + enum class Container { Unknown = 0, @@ -68,7 +68,7 @@ namespace lms::db::detail WavPack = 13, }; - enum class CodecType + enum class Codec { Unknown = 0, diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 5ae331ed..3986e74a 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -530,15 +530,15 @@ namespace lms::db::tests TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - track1.get().modify()->setCodec(core::media::CodecType::MP3); - track2.get().modify()->setCodec(core::media::CodecType::FLAC); + 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::CodecType::FLAC)); + params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC)); const auto artists{ Artist::find(session, params) }; ASSERT_EQ(artists.results.size(), 1); diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index a7419ba9..940f7b73 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -1229,22 +1229,22 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setCodec(core::media::CodecType::FLAC); + track1.get().modify()->setCodec(core::media::Codec::FLAC); track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setCodec(core::media::CodecType::MP3); + track2.get().modify()->setCodec(core::media::Codec::MP3); track2.get().modify()->setRelease(release1.get()); - track3.get().modify()->setCodec(core::media::CodecType::MP3); + track3.get().modify()->setCodec(core::media::Codec::MP3); track3.get().modify()->setRelease(release1.get()); - track4.get().modify()->setCodec(core::media::CodecType::AAC); + 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::CodecType::MP3, core::media::CodecType::FLAC, core::media::CodecType::AAC })); + EXPECT_EQ(release1->getCodecs(), (std::vector{ core::media::Codec::MP3, core::media::Codec::FLAC, core::media::Codec::AAC })); } } @@ -1263,10 +1263,10 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setCodec(core::media::CodecType::FLAC); + track1.get().modify()->setCodec(core::media::Codec::FLAC); track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setCodec(core::media::CodecType::MP3); + track2.get().modify()->setCodec(core::media::Codec::MP3); track2.get().modify()->setRelease(release2.get()); } @@ -1274,7 +1274,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; Release::FindParameters params; - params.setFilters(Filters{}.setCodec(core::media::CodecType::MP3)); + params.setFilters(Filters{}.setCodec(core::media::Codec::MP3)); auto releases{ Release::findIds(session, params) }; ASSERT_EQ(releases.results.size(), 1); diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 79c54bc9..2cea765d 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -245,15 +245,15 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setCodec(core::media::CodecType::MP3); - track2.get().modify()->setCodec(core::media::CodecType::FLAC); + track1.get().modify()->setCodec(core::media::Codec::MP3); + track2.get().modify()->setCodec(core::media::Codec::FLAC); } { auto transaction{ session.createReadTransaction() }; Track::FindParameters params; - params.setFilters(Filters{}.setCodec(core::media::CodecType::FLAC)); + params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC)); const auto tracks{ Track::find(session, params) }; ASSERT_EQ(tracks.results.size(), 1); diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 9ed5fee3..9f2db823 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -53,13 +53,13 @@ namespace lms::api::subsonic struct OutputFormat { std::string name; - core::media::ContainerType container; - core::media::CodecType codec; + core::media::Container container; + core::media::Codec codec; }; constexpr std::array outputFormats{ - OutputFormat{ "mp3", core::media::ContainerType::MPEG, core::media::CodecType::MP3 }, - OutputFormat{ "opus", core::media::ContainerType::Ogg, core::media::CodecType::Opus }, - OutputFormat{ "vorbis", core::media::ContainerType::Ogg, core::media::CodecType::Vorbis }, + OutputFormat{ "mp3", core::media::Container::MPEG, core::media::Codec::MP3 }, + OutputFormat{ "opus", core::media::Container::Ogg, core::media::Codec::Opus }, + OutputFormat{ "vorbis", core::media::Container::Ogg, core::media::Codec::Vorbis }, }; std::optional getOutputFormatByName(std::string_view format) diff --git a/src/libs/subsonic/impl/endpoints/Transcoding.cpp b/src/libs/subsonic/impl/endpoints/Transcoding.cpp index aa96abc1..b9112e5b 100644 --- a/src/libs/subsonic/impl/endpoints/Transcoding.cpp +++ b/src/libs/subsonic/impl/endpoints/Transcoding.cpp @@ -48,8 +48,8 @@ namespace lms::api::subsonic { StreamDetails res; res.protocol = "http"; - res.container = core::media::containerTypeToString(audioProperties.container).str(); - res.codec = core::media::codecTypeToString(audioProperties.codec).str(); + res.container = core::media::containerToString(audioProperties.container).str(); + res.codec = core::media::getCodecDesc(audioProperties.codec).name.str(); res.audioChannels = audioProperties.channelCount; res.audioBitrate = audioProperties.bitrate; res.audioProfile = ""; // TODO diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp index d7910ac8..a5b877bb 100644 --- a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp @@ -34,13 +34,13 @@ namespace lms::api::subsonic::detail namespace { constexpr std::array supportedTranscodeOutputFormats{ - audio::TranscodeOutputFormat{ .container = core::media::ContainerType::MPEG, .codec = core::media::CodecType::MP3 }, - audio::TranscodeOutputFormat{ .container = core::media::ContainerType::Ogg, .codec = core::media::CodecType::Vorbis }, - audio::TranscodeOutputFormat{ .container = core::media::ContainerType::Ogg, .codec = core::media::CodecType::Opus }, - audio::TranscodeOutputFormat{ .container = core::media::ContainerType::FLAC, .codec = core::media::CodecType::FLAC }, + audio::TranscodeOutputFormat{ .container = core::media::Container::MPEG, .codec = core::media::Codec::MP3 }, + audio::TranscodeOutputFormat{ .container = core::media::Container::Ogg, .codec = core::media::Codec::Vorbis }, + audio::TranscodeOutputFormat{ .container = core::media::Container::Ogg, .codec = core::media::Codec::Opus }, + audio::TranscodeOutputFormat{ .container = core::media::Container::FLAC, .codec = core::media::Codec::FLAC }, }; - bool isMatchingContainerName(core::media::ContainerType container, std::string_view containerStr) + bool isMatchingContainerName(core::media::Container container, std::string_view containerStr) { using namespace std::literals; // for "..."sv @@ -62,43 +62,43 @@ namespace lms::api::subsonic::detail switch (container) { - case core::media::ContainerType::AIFF: + case core::media::Container::AIFF: containerNames = aiffNames; break; - case core::media::ContainerType::APE: + case core::media::Container::APE: containerNames = apeNames; break; - case core::media::ContainerType::ASF: + case core::media::Container::ASF: containerNames = asfNames; break; - case core::media::ContainerType::DSF: + case core::media::Container::DSF: containerNames = dsfNames; break; - case core::media::ContainerType::MPC: + case core::media::Container::MPC: containerNames = mpcNames; break; - case core::media::ContainerType::MPEG: + case core::media::Container::MPEG: containerNames = mpegNames; break; - case core::media::ContainerType::Ogg: + case core::media::Container::Ogg: containerNames = oggNames; break; - case core::media::ContainerType::FLAC: + case core::media::Container::FLAC: containerNames = flacNames; break; - case core::media::ContainerType::MP4: + case core::media::Container::MP4: containerNames = mp4Names; break; - case core::media::ContainerType::Shorten: + case core::media::Container::Shorten: containerNames = shortenNames; break; - case core::media::ContainerType::TrueAudio: + case core::media::Container::TrueAudio: containerNames = trueAudioNames; break; - case core::media::ContainerType::WAV: + case core::media::Container::WAV: containerNames = wavNames; break; - case core::media::ContainerType::WavPack: + case core::media::Container::WavPack: containerNames = wavPackNames; break; } @@ -106,7 +106,7 @@ namespace lms::api::subsonic::detail return std::any_of(std::cbegin(containerNames), std::cend(containerNames), [&](std::string_view containerName) { return core::stringUtils::stringCaseInsensitiveEqual(containerName, containerStr); }); } - bool isMatchingCodecName(core::media::CodecType codec, std::string_view codecStr) + bool isMatchingCodecName(core::media::Codec codec, std::string_view codecStr) { using namespace std::literals; // for "..."sv @@ -135,67 +135,67 @@ namespace lms::api::subsonic::detail std::span codecNames; switch (codec) { - case core::media::CodecType::AAC: + case core::media::Codec::AAC: codecNames = aacCodecNames; break; - case core::media::CodecType::AC3: + case core::media::Codec::AC3: codecNames = ac3CodecNames; break; - case core::media::CodecType::ALAC: + case core::media::Codec::ALAC: codecNames = alacCodecNames; break; - case core::media::CodecType::APE: + case core::media::Codec::APE: codecNames = apeCodecNames; break; - case core::media::CodecType::DSD: + case core::media::Codec::DSD: codecNames = dsdCodecNames; break; - case core::media::CodecType::EAC3: + case core::media::Codec::EAC3: codecNames = eac3CodecNames; break; - case core::media::CodecType::FLAC: + case core::media::Codec::FLAC: codecNames = flacCodecNames; break; - case core::media::CodecType::MP3: + case core::media::Codec::MP3: codecNames = mp3CodecNames; break; - case core::media::CodecType::MP4ALS: + case core::media::Codec::MP4ALS: codecNames = mp4alsCodecNames; break; - case core::media::CodecType::MPC7: + case core::media::Codec::MPC7: codecNames = mpc7CodecNames; break; - case core::media::CodecType::MPC8: + case core::media::Codec::MPC8: codecNames = mpc8CodecNames; break; - case core::media::CodecType::Opus: + case core::media::Codec::Opus: codecNames = opusCodecNames; break; - case core::media::CodecType::PCM: + case core::media::Codec::PCM: codecNames = pcmCodecNames; break; - case core::media::CodecType::Shorten: + case core::media::Codec::Shorten: codecNames = shortenCodecNames; break; - case core::media::CodecType::TrueAudio: + case core::media::Codec::TrueAudio: codecNames = trueAudioCodecNames; break; - case core::media::CodecType::Vorbis: + case core::media::Codec::Vorbis: codecNames = vorbisCodecNames; break; - case core::media::CodecType::WavPack: + case core::media::Codec::WavPack: codecNames = wavPackCodecNames; break; - case core::media::CodecType::WMA1: + case core::media::Codec::WMA1: codecNames = wma1CodecNames; break; - case core::media::CodecType::WMA2: + case core::media::Codec::WMA2: codecNames = wma2CodecNames; break; - case core::media::CodecType::WMA9Lossless: + case core::media::Codec::WMA9Lossless: codecNames = wma9LosslessCodecNames; break; - case core::media::CodecType::WMA9Pro: + case core::media::Codec::WMA9Pro: codecNames = wma9ProCodecNames; break; } @@ -343,7 +343,7 @@ namespace lms::api::subsonic::detail return adjustResult.type == AdjustResult::Type::None; } - const CodecProfile* getAudioCodecProfile(std::span codecProfiles, core::media::CodecType codec) + const CodecProfile* getAudioCodecProfile(std::span codecProfiles, core::media::Codec codec) { for (const CodecProfile& profile : codecProfiles) { @@ -463,9 +463,9 @@ namespace lms::api::subsonic::detail transcodedStream.container = profile.container; // put back what was requested instead of our internal names transcodedStream.codec = profile.audioCodec; // put back what was requested instead of our internal names - if (core::media::isCodecLossless(source.codec)) + if (core::media::getCodecDesc(source.codec).isLossless) { - if (!core::media::isCodecLossless(transcodeFormat->codec)) + if (!core::media::getCodecDesc(transcodeFormat->codec).isLossless) { // If coming from lossless source, maximize the bitrate if going to a non lossless source // otherwise, pick a good enough value as we don't want to keep the original bitrate which does not make sense for lossy codecs @@ -486,7 +486,7 @@ namespace lms::api::subsonic::detail { // source is lossy - if (core::media::isCodecLossless(transcodeFormat->codec)) + if (core::media::getCodecDesc(transcodeFormat->codec).isLossless) return std::nullopt; // not compatible with lossless codecs // let's pick the same bitrate as the lossy source @@ -504,7 +504,7 @@ namespace lms::api::subsonic::detail for (const Limitation& limitation : codecProfile->limitations) { const AdjustResult result{ applyLimitation(source, limitation, transcodedStream) }; - if (limitation.name == Limitation::Type::AudioBitrate && core::media::isCodecLossless(transcodeFormat->codec) && result.type == AdjustResult::Type::Adjusted) + if (limitation.name == Limitation::Type::AudioBitrate && core::media::getCodecDesc(transcodeFormat->codec).isLossless && result.type == AdjustResult::Type::Adjusted) return std::nullopt; // not compatible with lossless codecs if (result.type == AdjustResult::Type::CannotAdjust) diff --git a/src/libs/subsonic/test/TranscodeDecision.cpp b/src/libs/subsonic/test/TranscodeDecision.cpp index 3438eaba..e9db7dcf 100644 --- a/src/libs/subsonic/test/TranscodeDecision.cpp +++ b/src/libs/subsonic/test/TranscodeDecision.cpp @@ -22,8 +22,8 @@ #include #include "core/Utils.hpp" -#include "core/media/CodecType.hpp" -#include "core/media/ContainerType.hpp" +#include "core/media/Codec.hpp" +#include "core/media/Container.hpp" #include "endpoints/transcoding/TranscodeDecision.hpp" #include "payloads/ClientInfo.hpp" @@ -111,8 +111,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -141,8 +141,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -171,8 +171,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -201,8 +201,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 192'000, .channelCount = 2, @@ -231,8 +231,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 192'000, .channelCount = 2, @@ -255,8 +255,8 @@ namespace lms::api::subsonic .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 192'000, .channelCount = 5, @@ -283,8 +283,8 @@ namespace lms::api::subsonic .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 192'000, .channelCount = 5, @@ -311,8 +311,8 @@ namespace lms::api::subsonic .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, }, .source = { - .container = core::media::ContainerType::Ogg, - .codec = core::media::CodecType::Opus, + .container = core::media::Container::Ogg, + .codec = core::media::Codec::Opus, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -339,8 +339,8 @@ namespace lms::api::subsonic .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, }, .source = { - .container = core::media::ContainerType::FLAC, - .codec = core::media::CodecType::FLAC, + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, .duration = std::chrono::seconds{ 60 }, .bitrate = 750'000, .channelCount = 2, @@ -362,8 +362,8 @@ namespace lms::api::subsonic }, .codecProfiles = {} }, .source = { - .container = core::media::ContainerType::FLAC, - .codec = core::media::CodecType::FLAC, + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, .duration = std::chrono::seconds{ 60 }, .bitrate = 750'000, .channelCount = 2, @@ -388,8 +388,8 @@ namespace lms::api::subsonic .codecProfiles = {}, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -414,8 +414,8 @@ namespace lms::api::subsonic .codecProfiles = {}, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -440,8 +440,8 @@ namespace lms::api::subsonic .codecProfiles = {}, }, .source = { - .container = core::media::ContainerType::FLAC, - .codec = core::media::CodecType::FLAC, + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, .duration = std::chrono::seconds{ 60 }, .bitrate = 1'000'000, .channelCount = 2, @@ -478,8 +478,8 @@ namespace lms::api::subsonic }, }, .source = { - .container = core::media::ContainerType::FLAC, - .codec = core::media::CodecType::FLAC, + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, .duration = std::chrono::seconds{ 60 }, .bitrate = 950'000, .channelCount = 2, @@ -517,8 +517,8 @@ namespace lms::api::subsonic }, }, .source = { - .container = core::media::ContainerType::FLAC, - .codec = core::media::CodecType::FLAC, + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, .duration = std::chrono::seconds{ 60 }, .bitrate = 950'000, .channelCount = 2, @@ -555,8 +555,8 @@ namespace lms::api::subsonic }, }, .source = { - .container = core::media::ContainerType::Ogg, - .codec = core::media::CodecType::Vorbis, + .container = core::media::Container::Ogg, + .codec = core::media::Codec::Vorbis, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, @@ -593,8 +593,8 @@ namespace lms::api::subsonic }, }, .source = { - .container = core::media::ContainerType::DSF, - .codec = core::media::CodecType::DSD, + .container = core::media::Container::DSF, + .codec = core::media::Codec::DSD, .duration = std::chrono::seconds{ 60 }, .bitrate = 950'000, .channelCount = 2, @@ -623,8 +623,8 @@ namespace lms::api::subsonic } } }, }, .source = { - .container = core::media::ContainerType::MPEG, - .codec = core::media::CodecType::MP3, + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, .duration = std::chrono::seconds{ 60 }, .bitrate = 128'000, .channelCount = 2, diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index ad55f306..00f9f839 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -142,11 +142,11 @@ namespace lms::ui // Codecs { std::string codecStr; - for (core::media::CodecType codec : release->getCodecs()) + for (core::media::Codec codec : release->getCodecs()) { if (!codecStr.empty()) codecStr += " ยท "; - codecStr += core::media::codecTypeToString(codec).str(); + codecStr += core::media::getCodecDesc(codec).name.str(); } if (!codecStr.empty()) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index a7c6c07a..f52d0dc2 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -130,7 +130,7 @@ namespace lms::ui::TrackListHelpers if (const auto codec{ track->getCodec() }) { trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", core::media::codecTypeToString(*codec).c_str(), Wt::TextFormat::Plain); + trackInfo->bindString("codec", core::media::getCodecDesc(*codec).name.c_str(), Wt::TextFormat::Plain); } trackInfo->bindString("duration", utils::durationToString(track->getDuration())); diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index dd8f434d..fcad2d20 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -124,11 +124,11 @@ namespace lms::ui switch (format) { case db::TranscodingOutputFormat::MP3: - return audio::TranscodeOutputFormat{ core::media::ContainerType::MPEG, core::media::CodecType::MP3 }; + return audio::TranscodeOutputFormat{ core::media::Container::MPEG, core::media::Codec::MP3 }; case db::TranscodingOutputFormat::OGG_OPUS: - return audio::TranscodeOutputFormat{ core::media::ContainerType::Ogg, core::media::CodecType::Opus }; + return audio::TranscodeOutputFormat{ core::media::Container::Ogg, core::media::Codec::Opus }; case db::TranscodingOutputFormat::OGG_VORBIS: - return audio::TranscodeOutputFormat{ core::media::ContainerType::Ogg, core::media::CodecType::Vorbis }; + return audio::TranscodeOutputFormat{ core::media::Container::Ogg, core::media::Codec::Vorbis }; } TRANSCODE_LOG(ERROR, "Cannot convert from db audio format to transcoding format"); diff --git a/src/tools/audioinfo/LmsAudioInfo.cpp b/src/tools/audioinfo/LmsAudioInfo.cpp index 2044890f..f4c6a967 100644 --- a/src/tools/audioinfo/LmsAudioInfo.cpp +++ b/src/tools/audioinfo/LmsAudioInfo.cpp @@ -41,8 +41,8 @@ namespace lms::audio std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) { os << "\tDuration: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast>(audioProperties.duration) << '\n'; - os << "\tContainer: " << core::media::containerTypeToString(audioProperties.container) << '\n'; - os << "\tCodec: " << core::media::codecTypeToString(audioProperties.codec) << '\n'; + os << "\tContainer: " << core::media::containerToString(audioProperties.container) << '\n'; + os << "\tCodec: " << core::media::getCodecDesc(audioProperties.codec).name << '\n'; os << "\tMimeType: " << core::media::getMimeType(audioProperties.container, audioProperties.codec) << '\n'; os << "\tBitrate: " << audioProperties.bitrate << " bps" << '\n'; if (audioProperties.bitsPerSample) From 40b4096ab05e1aa9b74ab2d385b6c686250880c7 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Dec 2025 23:46:47 +0100 Subject: [PATCH 19/22] UI: can now filter by codec --- src/lms/ui/explore/Filters.cpp | 135 +++++++++++++++++++++------------ src/lms/ui/explore/Filters.hpp | 4 + 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 7d90ef87..696c7795 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -26,6 +26,9 @@ #include #include +#include "core/Utils.hpp" + +#include "core/media/Codec.hpp" #include "database/Session.hpp" #include "database/objects/Cluster.hpp" #include "database/objects/LabelId.hpp" @@ -55,7 +58,11 @@ namespace lms::ui { }; - using TypeVariant = std::variant; + struct CodecTag + { + }; + + using TypeVariant = std::variant; using TypeModel = ValueStringModel; std::unique_ptr createTypeModel() @@ -72,11 +79,12 @@ namespace lms::ui typeModel->add(Wt::WString::tr("Lms.Explore.media-library"), MediaLibraryTag{}); typeModel->add(Wt::WString::tr("Lms.Explore.label"), LabelTag{}); typeModel->add(Wt::WString::tr("Lms.Explore.release-type"), ReleaseTypeTag{}); + typeModel->add(Wt::WString::tr("Lms.Explore.codec"), CodecTag{}); return typeModel; } - using ValueVariant = std::variant; + using ValueVariant = std::variant; using ValueModel = ValueStringModel; std::unique_ptr createValueModel(TypeVariant type) @@ -87,35 +95,37 @@ namespace lms::ui auto transaction{ session.createReadTransaction() }; - if (std::holds_alternative(type)) - { - db::MediaLibrary::find(session, [&](const db::MediaLibrary::pointer& library) { - valueModel->add(Wt::WString::fromUTF8(std::string{ library->getName() }), library->getId()); - }); - } - else if (std::holds_alternative(type)) - { - db::Label::find(session, db::LabelSortMethod::Name, [&](const db::Label::pointer& label) { - valueModel->add(Wt::WString::fromUTF8(std::string{ label->getName() }), label->getId()); - }); - } - else if (std::holds_alternative(type)) - { - db::ReleaseType::find(session, db::ReleaseTypeSortMethod::Name, [&](const db::ReleaseType::pointer& releaseType) { - valueModel->add(Wt::WString::fromUTF8(std::string{ releaseType->getName() }), releaseType->getId()); - }); - } - else if (const db::ClusterTypeId * clusterTypeId{ std::get_if(&type) }) - { - db::Cluster::FindParameters params; - params.setClusterType(*clusterTypeId); - params.setSortMethod(db::ClusterSortMethod::Name); - - db::Cluster::find(session, params, [&](const db::Cluster::pointer& cluster) { - valueModel->add(Wt::WString::fromUTF8(std::string{ cluster->getName() }), cluster->getId()); - }); - } + std::visit(core::utils::overloads{ + [&](const MediaLibraryTag&) { + db::MediaLibrary::find(session, [&](const db::MediaLibrary::pointer& library) { + valueModel->add(Wt::WString::fromUTF8(std::string{ library->getName() }), library->getId()); + }); + }, + [&](const LabelTag&) { + db::Label::find(session, db::LabelSortMethod::Name, [&](const db::Label::pointer& label) { + valueModel->add(Wt::WString::fromUTF8(std::string{ label->getName() }), label->getId()); + }); + }, + [&](const ReleaseTypeTag&) { + db::ReleaseType::find(session, db::ReleaseTypeSortMethod::Name, [&](const db::ReleaseType::pointer& releaseType) { + valueModel->add(Wt::WString::fromUTF8(std::string{ releaseType->getName() }), releaseType->getId()); + }); + }, + [&](const CodecTag&) { + core::media::visitCodecs([&](const core::media::CodecDesc& desc) { + valueModel->add(Wt::WString::fromUTF8(desc.longName.c_str()), desc.type); + }); + }, + [&](db::ClusterTypeId clusterTypeId) { + db::Cluster::FindParameters params; + params.setClusterType(clusterTypeId); + params.setSortMethod(db::ClusterSortMethod::Name); + db::Cluster::find(session, params, [&](const db::Cluster::pointer& cluster) { + valueModel->add(Wt::WString::fromUTF8(std::string{ cluster->getName() }), cluster->getId()); + }); + } }, + type); return valueModel; } } // namespace @@ -138,25 +148,28 @@ namespace lms::ui const auto valueModel{ std::static_pointer_cast(valueCombo->model()) }; const ValueVariant value{ valueModel->getValue(valueCombo->currentIndex()) }; - if (const db::MediaLibraryId * mediaLibraryId{ std::get_if(&value) }) - { - set(*mediaLibraryId); - state::writeValue("filters_media_library_id", mediaLibraryId->getValue()); - } - else if (const db::LabelId * labelId{ std::get_if(&value) }) - { - set(*labelId); - state::writeValue("filters_label_id", labelId->getValue()); - } - else if (const db::ReleaseTypeId * releaseTypeId{ std::get_if(&value) }) - { - set(*releaseTypeId); - state::writeValue("filters_release_type_id", releaseTypeId->getValue()); - } - else if (const db::ClusterId * clusterId{ std::get_if(&value) }) - { - add(*clusterId); - } + std::visit(core::utils::overloads{ + [&](db::MediaLibraryId mediaLibraryId) { + set(mediaLibraryId); + state::writeValue("filters_media_library_id", mediaLibraryId.getValue()); + }, + [&](db::LabelId labelId) { + set(labelId); + state::writeValue("filters_label_id", labelId.getValue()); + }, + [&](db::ReleaseTypeId releaseTypeId) { + set(releaseTypeId); + state::writeValue("filters_release_type_id", releaseTypeId.getValue()); + }, + [&](db::ClusterId clusterId) { + add(clusterId); + }, + [&](core::media::Codec codec) { + set(codec); + state::writeValue("filters_codec", codec); + }, + }, + value); // TODO LmsApp->getModalManager().dispose(dialogPtr); @@ -197,6 +210,8 @@ namespace lms::ui set(db::LabelId{ *labelId }); if (const std::optional releaseTypeId{ state::readValue("filters_release_type_id") }) set(db::ReleaseTypeId{ *releaseTypeId }); + if (const std::optional codec{ state::readValue("filters_codec") }) + set(*codec); } void Filters::add(db::ClusterId clusterId) @@ -324,6 +339,28 @@ namespace lms::ui emitFilterAddedNotification(); } + void Filters::set(core::media::Codec codec) + { + if (_codecFilter) + { + _filters->removeWidget(_codecFilter); + _codecFilter = nullptr; + _dbFilters.codec.reset(); + } + + _dbFilters.codec = codec; + _codecFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(core::media::getCodecDesc(codec).longName.c_str()), Wt::WString::tr("Lms.Explore.codec"), "bg-success", true)); + _codecFilter->clicked().connect(_codecFilter, [this] { + _filters->removeWidget(_codecFilter); + _dbFilters.codec.reset(); + _codecFilter = nullptr; + _sigUpdated.emit(); + state::writeValue("filters_codec", std::nullopt); + }); + + emitFilterAddedNotification(); + } + void Filters::emitFilterAddedNotification() { LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds{ 2 }); diff --git a/src/lms/ui/explore/Filters.hpp b/src/lms/ui/explore/Filters.hpp index c63b7398..99cdcf35 100644 --- a/src/lms/ui/explore/Filters.hpp +++ b/src/lms/ui/explore/Filters.hpp @@ -23,6 +23,8 @@ #include #include +#include "core/media/Codec.hpp" + #include "database/objects/Filters.hpp" namespace lms::ui @@ -43,12 +45,14 @@ namespace lms::ui void set(db::LabelId labelId); void set(db::MediaLibraryId mediaLibraryId); void set(db::ReleaseTypeId releaseTypeId); + void set(core::media::Codec codec); void emitFilterAddedNotification(); Wt::WContainerWidget* _filters{}; Wt::WInteractWidget* _mediaLibraryFilter{}; Wt::WInteractWidget* _labelFilter{}; Wt::WInteractWidget* _releaseTypeFilter{}; + Wt::WInteractWidget* _codecFilter{}; Wt::Signal<> _sigUpdated; From d51ff0a03f61df9506fc4ebb5f1f65dee9356d72 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 8 Dec 2025 21:00:44 +0100 Subject: [PATCH 20/22] Do not apply global filters on artist an release views --- README.md | 2 +- src/lms/ui/explore/ArtistView.cpp | 9 +-------- src/lms/ui/explore/ArtistView.hpp | 1 - src/lms/ui/explore/ReleaseView.cpp | 9 +-------- src/lms/ui/explore/ReleaseView.hpp | 1 - 5 files changed, 3 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d4344ff7..8534b2f0 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The folder must follow a structure defined by Kodi, as detailed [here](https://k __Note__: If no name is provided in the `artist.nfo` file, the name of the containing folder is used. ### Filtering -It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings. +It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, `codec`, and by music library. More tags, including custom ones, can be added in the database administration settings. __Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_. diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 58c0590a..297f58eb 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -80,11 +80,6 @@ namespace lms::ui refreshView(); }); - filters.updated().connect([this] { - _needForceRefresh = true; - refreshView(); - }); - refreshView(); } @@ -96,13 +91,12 @@ namespace lms::ui const auto artistId{ extractArtistIdFromInternalPath() }; // consider everything is up to date is the same artist is being rendered - if (!_needForceRefresh && artistId && *artistId == _artistId) + if (artistId && *artistId == _artistId) return; clear(); _artistId = {}; _trackContainer = nullptr; - _needForceRefresh = false; if (!artistId) throw ArtistNotFoundException{}; @@ -236,7 +230,6 @@ namespace lms::ui _releaseContainers.clear(); db::Release::FindParameters params; - params.setFilters(_filters.getDbFilters()); params.setArtist(_artistId, { db::TrackArtistLinkType::ReleaseArtist }, {}); params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod()); diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index a05f3de3..521cd03e 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -76,6 +76,5 @@ namespace lms::ui ReleaseContainer _appearsOnReleaseContainer{}; InfiniteScrollingContainer* _trackContainer{}; db::ArtistId _artistId{}; - bool _needForceRefresh{}; }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 00f9f839..f2ea10dc 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -255,11 +255,6 @@ namespace lms::ui refreshView(); }); - _filters.updated().connect([this] { - _needForceRefresh = true; - refreshView(); - }); - refreshView(); } @@ -271,12 +266,11 @@ namespace lms::ui const auto releaseId{ extractReleaseIdFromInternalPath() }; // consider everything is up to date is the same release is being rendered - if (!_needForceRefresh && releaseId && *releaseId == _releaseId) + if (releaseId && *releaseId == _releaseId) return; clear(); _releaseId = {}; - _needForceRefresh = false; if (!releaseId) throw ReleaseNotFoundException{}; @@ -451,7 +445,6 @@ namespace lms::ui db::Track::FindParameters params; params.setMedium(medium->getId()); params.setSortMethod(db::TrackSortMethod::TrackNumber); - params.setFilters(_filters.getDbFilters()); // TODO: do we really want to hide all tracks when a release does not match the current label filter? db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) { const db::TrackId trackId{ track->getId() }; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index 768fdbe6..76de048c 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -57,6 +57,5 @@ namespace lms::ui Filters& _filters; PlayQueueController& _playQueueController; db::ReleaseId _releaseId; - bool _needForceRefresh{}; }; } // namespace lms::ui From d3f260bfe630864a22c25c858af98a9d588fc545 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 8 Dec 2025 21:49:14 +0100 Subject: [PATCH 21/22] Minor cleanup --- src/lms/ui/MediaPlayer.hpp | 1 - src/lms/ui/resource/AudioTranscodingResource.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index b335d065..7620b6fe 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -129,5 +129,4 @@ namespace lms::ui Wt::WAnchor* _artist{}; Wt::WPushButton* _playQueue{}; }; - } // namespace lms::ui diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index fcad2d20..d30c960b 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -119,7 +119,7 @@ namespace lms::ui return res; } - std::optional AudioFormatToTranscodingFormat(db::TranscodingOutputFormat format) + std::optional audioFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { @@ -171,7 +171,7 @@ namespace lms::ui return std::nullopt; } - const std::optional outputFormat{ AudioFormatToTranscodingFormat(*format) }; + const std::optional outputFormat{ audioFormatToTranscodingFormat(*format) }; if (!outputFormat) return std::nullopt; From 1dce97bdddec93ce17f7f4c42bc088f99dc97357 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 8 Dec 2025 21:51:21 +0100 Subject: [PATCH 22/22] Transcoding: force setting the channel count to workaround when the input channel layout is not compatible with the codec selected, fixes #781 --- src/libs/audio/impl/ffmpeg/Transcoder.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/audio/impl/ffmpeg/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp index 026e861b..649f8872 100644 --- a/src/libs/audio/impl/ffmpeg/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -87,7 +87,7 @@ namespace lms::audio::ffmpeg } catch (const std::filesystem::filesystem_error& e) { - throw IOFileException{ _inputParams.filePath, "Failed to test file existence", e.code() }; + throw IOFileException{ _inputParams.filePath, "Failed to check if file exists", e.code() }; } LOG(INFO, "Transcoding file " << _inputParams.filePath); @@ -136,10 +136,11 @@ namespace lms::audio::ffmpeg args.emplace_back(std::to_string(*_outputParams.bitrate)); } - if (_outputParams.channelCount) + // -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)); + args.emplace_back(std::to_string(_outputParams.channelCount ? *_outputParams.channelCount : _inputParams.audioProperties.channelCount)); } if (_outputParams.sampleRate)