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); }