Added a fallback to ffmpeg parser in case of audio properties are not properly decoded, ref #781
This commit is contained in:
@@ -4,17 +4,19 @@ pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
|||||||
add_library(lmsaudio STATIC
|
add_library(lmsaudio STATIC
|
||||||
impl/ffmpeg/AudioFile.cpp
|
impl/ffmpeg/AudioFile.cpp
|
||||||
impl/ffmpeg/AudioFileInfo.cpp
|
impl/ffmpeg/AudioFileInfo.cpp
|
||||||
|
impl/ffmpeg/AudioFileInfoParser.cpp
|
||||||
impl/ffmpeg/ImageReader.cpp
|
impl/ffmpeg/ImageReader.cpp
|
||||||
impl/ffmpeg/TagReader.cpp
|
impl/ffmpeg/TagReader.cpp
|
||||||
impl/ffmpeg/Transcoder.cpp
|
impl/ffmpeg/Transcoder.cpp
|
||||||
impl/ffmpeg/Utils.cpp
|
impl/ffmpeg/Utils.cpp
|
||||||
impl/taglib/AudioFileInfo.cpp
|
impl/taglib/AudioFileInfo.cpp
|
||||||
|
impl/taglib/AudioFileInfoParser.cpp
|
||||||
impl/taglib/ImageReader.cpp
|
impl/taglib/ImageReader.cpp
|
||||||
impl/taglib/TagReader.cpp
|
impl/taglib/TagReader.cpp
|
||||||
impl/taglib/Utils.cpp
|
impl/taglib/Utils.cpp
|
||||||
impl/AudioTypes.cpp
|
impl/AudioTypes.cpp
|
||||||
impl/ImageReader.cpp
|
impl/ImageReader.cpp
|
||||||
impl/ParseAudioFileInfo.cpp
|
impl/AudioFileInfoParser.cpp
|
||||||
impl/TagReader.cpp
|
impl/TagReader.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
#include "ffmpeg/AudioFileInfoParser.hpp"
|
||||||
|
#include "taglib/AudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend)
|
||||||
|
{
|
||||||
|
switch (backend)
|
||||||
|
{
|
||||||
|
case AudioFileInfoParserBackend::TagLib:
|
||||||
|
return std::make_unique<taglib::AudioFileInfoParser>();
|
||||||
|
case AudioFileInfoParserBackend::FFmpeg:
|
||||||
|
return std::make_unique<ffmpeg::AudioFileInfoParser>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} // namespace lms::audio
|
||||||
@@ -62,12 +62,16 @@ namespace lms::audio
|
|||||||
{
|
{
|
||||||
case CodecType::AAC:
|
case CodecType::AAC:
|
||||||
return "AAC";
|
return "AAC";
|
||||||
|
case CodecType::AC3:
|
||||||
|
return "AC3";
|
||||||
case CodecType::ALAC:
|
case CodecType::ALAC:
|
||||||
return "ALAC";
|
return "ALAC";
|
||||||
case CodecType::APE:
|
case CodecType::APE:
|
||||||
return "APE";
|
return "APE";
|
||||||
case CodecType::DSD:
|
case CodecType::DSD:
|
||||||
return "DSD";
|
return "DSD";
|
||||||
|
case CodecType::EAC3:
|
||||||
|
return "E-AC3";
|
||||||
case CodecType::FLAC:
|
case CodecType::FLAC:
|
||||||
return "FLAC";
|
return "FLAC";
|
||||||
case CodecType::MP3:
|
case CodecType::MP3:
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
|
||||||
|
|
||||||
#include "ffmpeg/AudioFileInfo.hpp"
|
|
||||||
#include "ffmpeg/Utils.hpp"
|
|
||||||
#include "taglib/AudioFileInfo.hpp"
|
|
||||||
#include "taglib/Utils.hpp"
|
|
||||||
|
|
||||||
namespace lms::audio
|
|
||||||
{
|
|
||||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
|
|
||||||
{
|
|
||||||
switch (parserOptions.parser)
|
|
||||||
{
|
|
||||||
case ParserOptions::Parser::TagLib:
|
|
||||||
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
|
|
||||||
case ParserOptions::Parser::FFmpeg:
|
|
||||||
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
|
|
||||||
{
|
|
||||||
switch (parser)
|
|
||||||
{
|
|
||||||
case ParserOptions::Parser::TagLib:
|
|
||||||
return taglib::utils::getSupportedExtensions();
|
|
||||||
case ParserOptions::Parser::FFmpeg:
|
|
||||||
return ffmpeg::utils::getSupportedExtensions();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace lms::audio
|
|
||||||
@@ -20,21 +20,22 @@
|
|||||||
#include "AudioFile.hpp"
|
#include "AudioFile.hpp"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
#define __STDC_CONSTANT_MACROS
|
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libavutil/error.h>
|
#include <libavutil/error.h>
|
||||||
|
#include <libavutil/log.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::audio::ffmpeg
|
namespace lms::audio::ffmpeg
|
||||||
{
|
{
|
||||||
@@ -50,25 +51,23 @@ namespace lms::audio::ffmpeg
|
|||||||
return "Unknown error";
|
return "Unknown error";
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioFileException : public AudioFileParsingException
|
class AvException : public Exception
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioFileException(int avError)
|
AvException(int avError)
|
||||||
: AudioFileParsingException{ averror_to_string(avError) }
|
: Exception{ averror_to_string(avError) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
void extractMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||||
{
|
{
|
||||||
if (!dictionnary)
|
if (!dictionnary)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AVDictionaryEntry* tag = NULL;
|
const AVDictionaryEntry* tag{ NULL };
|
||||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
while ((tag = av_dict_iterate(dictionnary, tag)))
|
||||||
{
|
|
||||||
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
|
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
|
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
|
||||||
@@ -107,58 +106,112 @@ namespace lms::audio::ffmpeg
|
|||||||
{
|
{
|
||||||
switch (codec)
|
switch (codec)
|
||||||
{
|
{
|
||||||
case AV_CODEC_ID_MP3:
|
|
||||||
return CodecType::MP3;
|
|
||||||
case AV_CODEC_ID_AAC:
|
case AV_CODEC_ID_AAC:
|
||||||
return CodecType::AAC;
|
return CodecType::AAC;
|
||||||
case AV_CODEC_ID_VORBIS:
|
case AV_CODEC_ID_AC3:
|
||||||
return CodecType::Vorbis;
|
return CodecType::AC3;
|
||||||
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_ALAC:
|
case AV_CODEC_ID_ALAC:
|
||||||
return CodecType::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:
|
case AV_CODEC_ID_APE:
|
||||||
return CodecType::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:
|
||||||
case AV_CODEC_ID_DSD_LSBF_PLANAR:
|
case AV_CODEC_ID_DSD_LSBF_PLANAR:
|
||||||
case AV_CODEC_ID_DSD_MSBF:
|
case AV_CODEC_ID_DSD_MSBF:
|
||||||
case AV_CODEC_ID_DSD_MSBF_PLANAR:
|
case AV_CODEC_ID_DSD_MSBF_PLANAR:
|
||||||
return CodecType::DSD;
|
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:
|
default:
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core::LiteralString avLogLevelToStr(int level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case AV_LOG_TRACE:
|
||||||
|
return "trace";
|
||||||
|
case AV_LOG_DEBUG:
|
||||||
|
return "debug";
|
||||||
|
case AV_LOG_VERBOSE:
|
||||||
|
return "verbose";
|
||||||
|
case AV_LOG_INFO:
|
||||||
|
return "info";
|
||||||
|
case AV_LOG_WARNING:
|
||||||
|
return "warning";
|
||||||
|
case AV_LOG_ERROR:
|
||||||
|
return "error";
|
||||||
|
case AV_LOG_FATAL:
|
||||||
|
return "fatal";
|
||||||
|
case AV_LOG_PANIC:
|
||||||
|
return "panic";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void avLogCallback(void*, int level, const char* fmt, va_list vl)
|
||||||
|
{
|
||||||
|
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (level > AV_LOG_WARNING)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::array<char, 256> buffer{ 0 };
|
||||||
|
std::vsnprintf(buffer.data(), buffer.size(), fmt, vl);
|
||||||
|
|
||||||
|
LMS_LOG(AUDIO, DEBUG, "ffmpeg [" << avLogLevelToStr(level) << "] " << buffer.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
class AvInitializer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AvInitializer()
|
||||||
|
{
|
||||||
|
::av_log_set_callback(avLogCallback);
|
||||||
|
}
|
||||||
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AudioFile::AudioFile(const std::filesystem::path& p)
|
AudioFile::AudioFile(const std::filesystem::path& p)
|
||||||
: _p{ p }
|
: _p{ p }
|
||||||
{
|
{
|
||||||
|
static AvInitializer init;
|
||||||
|
|
||||||
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
|
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
|
||||||
if (error < 0)
|
if (error < 0)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
||||||
throw AudioFileException{ error };
|
throw AvException{ error };
|
||||||
}
|
}
|
||||||
|
|
||||||
error = avformat_find_stream_info(_context, nullptr);
|
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));
|
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
|
||||||
avformat_close_input(&_context);
|
avformat_close_input(&_context);
|
||||||
throw AudioFileException{ error };
|
throw AvException{ error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,11 +246,11 @@ namespace lms::audio::ffmpeg
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioFile::MetadataMap AudioFile::getMetaData() const
|
AudioFile::MetadataMap AudioFile::extractMetaData() const
|
||||||
{
|
{
|
||||||
MetadataMap res;
|
MetadataMap res;
|
||||||
|
|
||||||
getMetaDataFromDictionnary(_context->metadata, res);
|
extractMetaDataFromDictionnary(_context->metadata, res);
|
||||||
|
|
||||||
// HACK for OGG files
|
// HACK for OGG files
|
||||||
// If we did not find tags, search metadata in streams
|
// 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)
|
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())
|
if (!res.empty())
|
||||||
break;
|
break;
|
||||||
@@ -266,7 +319,7 @@ namespace lms::audio::ffmpeg
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
|
void AudioFile::visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const
|
||||||
{
|
{
|
||||||
static const std::unordered_map<int, std::string> codecMimeMap{
|
static const std::unordered_map<int, std::string> codecMimeMap{
|
||||||
{ AV_CODEC_ID_BMP, "image/bmp" },
|
{ AV_CODEC_ID_BMP, "image/bmp" },
|
||||||
@@ -286,14 +339,14 @@ namespace lms::audio::ffmpeg
|
|||||||
|
|
||||||
if (avstream->codecpar == nullptr)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MetadataMap metadata;
|
MetadataMap metadata;
|
||||||
getMetaDataFromDictionnary(avstream->metadata, metadata);
|
extractMetaDataFromDictionnary(avstream->metadata, metadata);
|
||||||
|
|
||||||
Picture picture;
|
PictureView picture;
|
||||||
|
|
||||||
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
||||||
if (itMime != codecMimeMap.end())
|
if (itMime != codecMimeMap.end())
|
||||||
@@ -303,10 +356,10 @@ namespace lms::audio::ffmpeg
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
picture.mimeType = "application/octet-stream";
|
picture.mimeType = "application/octet-stream";
|
||||||
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
|
LMS_LOG(AUDIO, WARNING, "AVCodecID" << avstream->codecpar->codec_id << " (" << ::avcodec_get_name(avstream->codecpar->codec_id) << ") not handled in mime type conversion");
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVPacket& pkt{ avstream->attached_pic };
|
const ::AVPacket& pkt{ avstream->attached_pic };
|
||||||
|
|
||||||
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
|
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
|
||||||
func(picture, metadata);
|
func(picture, metadata);
|
||||||
@@ -325,7 +378,7 @@ namespace lms::audio::ffmpeg
|
|||||||
|
|
||||||
if (!avstream->codecpar)
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ extern "C"
|
|||||||
|
|
||||||
namespace lms::audio::ffmpeg
|
namespace lms::audio::ffmpeg
|
||||||
{
|
{
|
||||||
struct Picture
|
struct PictureView
|
||||||
{
|
{
|
||||||
std::string mimeType;
|
std::string mimeType;
|
||||||
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||||
@@ -75,12 +75,12 @@ namespace lms::audio::ffmpeg
|
|||||||
|
|
||||||
const std::filesystem::path& getPath() const;
|
const std::filesystem::path& getPath() const;
|
||||||
ContainerInfo getContainerInfo() const;
|
ContainerInfo getContainerInfo() const;
|
||||||
MetadataMap getMetaData() const;
|
MetadataMap extractMetaData() const;
|
||||||
std::vector<StreamInfo> getStreamInfo() const;
|
std::vector<StreamInfo> getStreamInfo() const;
|
||||||
std::optional<StreamInfo> getBestStreamInfo() const;
|
std::optional<StreamInfo> getBestStreamInfo() const;
|
||||||
std::optional<std::size_t> getBestStreamIndex() const;
|
std::optional<std::size_t> getBestStreamIndex() const;
|
||||||
bool hasAttachedPictures() const;
|
bool hasAttachedPictures() const;
|
||||||
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
|
void visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "AudioFileInfo.hpp"
|
#include "AudioFileInfo.hpp"
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
|
||||||
@@ -30,65 +32,88 @@ namespace lms::audio::ffmpeg
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
AudioProperties computeAudioProperties(const AudioFile& audioFile)
|
std::optional<AudioProperties> computeAudioProperties(const AudioFile& audioFile)
|
||||||
{
|
{
|
||||||
AudioProperties audioProperties;
|
std::optional<AudioProperties> audioProperties;
|
||||||
|
|
||||||
const auto containerInfo{ audioFile.getContainerInfo() };
|
const auto containerInfo{ audioFile.getContainerInfo() };
|
||||||
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
|
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
|
||||||
if (!bestStreamInfo)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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.emplace();
|
||||||
audioProperties.duration = containerInfo.duration;
|
|
||||||
audioProperties.codec = *bestStreamInfo->codec;
|
audioProperties->container = *containerInfo.container;
|
||||||
audioProperties.bitrate = *bestStreamInfo->bitrate;
|
audioProperties->duration = containerInfo.duration;
|
||||||
audioProperties.channelCount = *bestStreamInfo->channelCount;
|
audioProperties->codec = *bestStreamInfo->codec;
|
||||||
audioProperties.sampleRate = *bestStreamInfo->sampleRate;
|
audioProperties->bitrate = *bestStreamInfo->bitrate;
|
||||||
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
|
audioProperties->channelCount = *bestStreamInfo->channelCount;
|
||||||
|
audioProperties->sampleRate = *bestStreamInfo->sampleRate;
|
||||||
|
audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample;
|
||||||
|
|
||||||
return audioProperties;
|
return audioProperties;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
|
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
|
||||||
: _audioFile{ std::make_unique<AudioFile>(filePath) }
|
: _audioFile{ std::make_unique<AudioFile>(filePath) }
|
||||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
|
, _audioProperties{ computeAudioProperties(*_audioFile) }
|
||||||
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
|
|
||||||
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
|
|
||||||
{
|
{
|
||||||
|
if (parseOptions.readTags)
|
||||||
|
_tagReader = std::make_unique<TagReader>(*_audioFile, parseOptions.enableExtraDebugLogs);
|
||||||
|
|
||||||
|
if (parseOptions.readImages)
|
||||||
|
_imageReader = std::make_unique<ImageReader>(*_audioFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioFileInfo::~AudioFileInfo() = default;
|
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
|
} // namespace lms::audio::ffmpeg
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
namespace lms::audio::ffmpeg
|
namespace lms::audio::ffmpeg
|
||||||
{
|
{
|
||||||
@@ -33,18 +35,19 @@ namespace lms::audio::ffmpeg
|
|||||||
class AudioFileInfo final : public IAudioFileInfo
|
class AudioFileInfo final : public IAudioFileInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
|
AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
|
||||||
~AudioFileInfo();
|
~AudioFileInfo() override;
|
||||||
|
|
||||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const AudioProperties& getAudioProperties() const override;
|
const AudioProperties* getAudioProperties() const override;
|
||||||
const IImageReader& getImageReader() const override;
|
const IImageReader* getImageReader() const override;
|
||||||
const ITagReader& getTagReader() const override;
|
const ITagReader* getTagReader() const override;
|
||||||
|
|
||||||
std::unique_ptr<AudioFile> _audioFile;
|
std::unique_ptr<AudioFile> _audioFile;
|
||||||
std::unique_ptr<AudioProperties> _audioProperties;
|
const std::optional<AudioProperties> _audioProperties;
|
||||||
std::unique_ptr<TagReader> _tagReader;
|
std::unique_ptr<TagReader> _tagReader;
|
||||||
std::unique_ptr<ImageReader> _imageReader;
|
std::unique_ptr<ImageReader> _imageReader;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "AudioFileInfo.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::ffmpeg
|
||||||
|
{
|
||||||
|
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
|
||||||
|
{
|
||||||
|
return std::make_unique<AudioFileInfo>(p, parseOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return utils::getSupportedExtensions();
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::ffmpeg
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::ffmpeg
|
||||||
|
{
|
||||||
|
class AudioFileInfoParser : public IAudioFileInfoParser
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
};
|
||||||
|
} // namespace lms::audio::ffmpeg
|
||||||
@@ -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); });
|
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 image;
|
||||||
image.data = picture.data;
|
image.data = picture.data;
|
||||||
image.mimeType = picture.mimeType;
|
image.mimeType = picture.mimeType;
|
||||||
@@ -52,5 +52,4 @@ namespace lms::audio::ffmpeg
|
|||||||
visitor(image);
|
visitor(image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::audio::ffmpeg
|
} // namespace lms::audio::ffmpeg
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ namespace lms::audio::ffmpeg
|
|||||||
|
|
||||||
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
|
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
|
||||||
: _audioFile{ audioFile }
|
: _audioFile{ audioFile }
|
||||||
, _metaDataMap{ audioFile.getMetaData() }
|
, _metaDataMap{ audioFile.extractMetaData() }
|
||||||
{
|
{
|
||||||
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,8 +43,11 @@
|
|||||||
#include <taglib/shortenfile.h>
|
#include <taglib/shortenfile.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
#include "ImageReader.hpp"
|
#include "ImageReader.hpp"
|
||||||
#include "TagReader.hpp"
|
#include "TagReader.hpp"
|
||||||
@@ -54,31 +57,48 @@ namespace lms::audio::taglib
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
AudioProperties computeAudioProperties(const ::TagLib::File& file)
|
std::optional<AudioProperties> computeAudioProperties(const ::TagLib::File& file, const std::filesystem::path& filePath)
|
||||||
{
|
{
|
||||||
assert(file.audioProperties());
|
assert(file.audioProperties());
|
||||||
|
|
||||||
AudioProperties 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
|
// Common properties
|
||||||
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
|
audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||||
if (audioProperties.bitrate == 0)
|
if (audioProperties.bitrate == 0)
|
||||||
throw AudioFileParsingException{ "Cannot determine bitrate" };
|
{
|
||||||
|
LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << filePath);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
|
audioProperties.channelCount = static_cast<std::size_t>(properties->channels());
|
||||||
if (audioProperties.channelCount == 0)
|
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())
|
if (audioProperties.duration == decltype(audioProperties.duration)::zero())
|
||||||
throw AudioFileParsingException{ "Cannot determine duration" };
|
{
|
||||||
|
LMS_LOG(AUDIO, DEBUG, "Cannot determine duration in " << filePath);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
|
audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
||||||
if (audioProperties.sampleRate == 0)
|
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
|
// Guess container from the file type
|
||||||
@@ -107,7 +127,8 @@ namespace lms::audio::taglib
|
|||||||
audioProperties.codec = CodecType::WMA9Pro;
|
audioProperties.codec = CodecType::WMA9Pro;
|
||||||
break;
|
break;
|
||||||
case ::TagLib::ASF::Properties::Codec::Unknown:
|
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();
|
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
|
||||||
@@ -138,7 +159,8 @@ namespace lms::audio::taglib
|
|||||||
audioProperties.codec = CodecType::ALAC;
|
audioProperties.codec = CodecType::ALAC;
|
||||||
break;
|
break;
|
||||||
case ::TagLib::MP4::Properties::Codec::Unknown:
|
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();
|
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
|
||||||
@@ -156,7 +178,8 @@ namespace lms::audio::taglib
|
|||||||
audioProperties.codec = CodecType::MPC8;
|
audioProperties.codec = CodecType::MPC8;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw AudioFileParsingException{ "Unhandled MPC codec" };
|
LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath);
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
|
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
|
||||||
@@ -172,7 +195,10 @@ namespace lms::audio::taglib
|
|||||||
audioProperties.codec = CodecType::AAC;
|
audioProperties.codec = CodecType::AAC;
|
||||||
#endif
|
#endif
|
||||||
else
|
else
|
||||||
throw AudioFileParsingException{ "Unhandled MPEG codec" };
|
{
|
||||||
|
LMS_LOG(AUDIO, DEBUG, "Unhandled MPEG codec in " << filePath);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
|
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
|
||||||
{
|
{
|
||||||
@@ -218,7 +244,8 @@ namespace lms::audio::taglib
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw AudioFileParsingException{ "Unhandled file type" };
|
LMS_LOG(AUDIO, DEBUG, "Unhandled file type in " << filePath);
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0)
|
if (audioProperties.bitsPerSample && *audioProperties.bitsPerSample == 0)
|
||||||
@@ -228,30 +255,32 @@ namespace lms::audio::taglib
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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 }
|
: _filePath{ filePath }
|
||||||
, _file{ utils::parseFile(filePath, readStyle) }
|
, _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
|
||||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_file)) }
|
, _audioProperties{ computeAudioProperties(*_file, filePath) }
|
||||||
, _tagReader{ std::make_unique<TagReader>(*_file, enableExtraDebugLogs) }
|
|
||||||
, _imageReader{ std::make_unique<ImageReader>(*_file) }
|
|
||||||
{
|
{
|
||||||
|
if (parseOptions.readTags)
|
||||||
|
_tagReader = std::make_unique<TagReader>(*_file, parseOptions.enableExtraDebugLogs);
|
||||||
|
|
||||||
|
if (parseOptions.readImages)
|
||||||
|
_imageReader = std::make_unique<ImageReader>(*_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioFileInfo::~AudioFileInfo() = default;
|
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
|
} // namespace lms::audio::taglib
|
||||||
|
|||||||
@@ -19,8 +19,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "audio/IImageReader.hpp"
|
#include "audio/IImageReader.hpp"
|
||||||
#include "audio/ITagReader.hpp"
|
#include "audio/ITagReader.hpp"
|
||||||
|
|
||||||
@@ -37,20 +41,20 @@ namespace lms::audio::taglib
|
|||||||
class AudioFileInfo final : public IAudioFileInfo
|
class AudioFileInfo final : public IAudioFileInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
|
AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
|
||||||
~AudioFileInfo() override;
|
~AudioFileInfo() override;
|
||||||
|
|
||||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const AudioProperties& getAudioProperties() const override;
|
const AudioProperties* getAudioProperties() const override;
|
||||||
const IImageReader& getImageReader() const override;
|
const IImageReader* getImageReader() const override;
|
||||||
const ITagReader& getTagReader() const override;
|
const ITagReader* getTagReader() const override;
|
||||||
|
|
||||||
const std::filesystem::path _filePath;
|
const std::filesystem::path _filePath;
|
||||||
std::unique_ptr<::TagLib::File> _file;
|
std::unique_ptr<::TagLib::File> _file;
|
||||||
std::unique_ptr<AudioProperties> _audioProperties;
|
std::optional<AudioProperties> _audioProperties;
|
||||||
std::unique_ptr<TagReader> _tagReader;
|
std::unique_ptr<TagReader> _tagReader;
|
||||||
std::unique_ptr<ImageReader> _imageReader;
|
std::unique_ptr<ImageReader> _imageReader;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "AudioFileInfo.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::taglib
|
||||||
|
{
|
||||||
|
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
|
||||||
|
{
|
||||||
|
return std::make_unique<AudioFileInfo>(p, parseOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
|
||||||
|
{
|
||||||
|
return utils::getSupportedExtensions();
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::taglib
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::taglib
|
||||||
|
{
|
||||||
|
class AudioFileInfoParser : public IAudioFileInfoParser
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
|
||||||
|
std::span<const std::filesystem::path> getSupportedExtensions() const override;
|
||||||
|
};
|
||||||
|
} // namespace lms::audio::taglib
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::audio::taglib::utils
|
namespace lms::audio::taglib::utils
|
||||||
{
|
{
|
||||||
@@ -89,15 +89,15 @@ namespace lms::audio::taglib::utils
|
|||||||
return std::span<const std::filesystem::path>{ supportedExtensions };
|
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||||
}
|
}
|
||||||
|
|
||||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
|
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
|
||||||
{
|
{
|
||||||
switch (readStyle)
|
switch (readStyle)
|
||||||
{
|
{
|
||||||
case ParserOptions::AudioPropertiesReadStyle::Fast:
|
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast:
|
||||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||||
case ParserOptions::AudioPropertiesReadStyle::Average:
|
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Average:
|
||||||
return TagLib::AudioProperties::ReadStyle::Average;
|
return TagLib::AudioProperties::ReadStyle::Average;
|
||||||
case ParserOptions::AudioPropertiesReadStyle::Accurate:
|
case AudioFileInfoParseOptions::AudioPropertiesReadStyle::Accurate:
|
||||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ namespace lms::audio::taglib::utils
|
|||||||
{
|
{
|
||||||
const std::error_code ec{ errno, std::generic_category() };
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
||||||
throw IOException{ "fopen failed", ec };
|
throw IOFileException{ "fopen failed", ec };
|
||||||
}
|
}
|
||||||
|
|
||||||
int fd{ ::fileno(file) };
|
int fd{ ::fileno(file) };
|
||||||
@@ -119,7 +119,7 @@ namespace lms::audio::taglib::utils
|
|||||||
{
|
{
|
||||||
const std::error_code ec{ errno, std::generic_category() };
|
const std::error_code ec{ errno, std::generic_category() };
|
||||||
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
|
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 };
|
return TagLib::FileStream{ fd, true };
|
||||||
@@ -236,7 +236,7 @@ namespace lms::audio::taglib::utils
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
|
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||||
|
|
||||||
@@ -252,10 +252,7 @@ namespace lms::audio::taglib::utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!file)
|
if (!file)
|
||||||
throw AudioFileParsingException{ "Parsing failed" };
|
throw Exception{ "Parsing failed" };
|
||||||
|
|
||||||
if (!file->audioProperties())
|
|
||||||
throw AudioFileParsingException{ "No audio properties" };
|
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@
|
|||||||
|
|
||||||
#include <taglib/tfile.h>
|
#include <taglib/tfile.h>
|
||||||
|
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
namespace lms::audio::taglib::utils
|
namespace lms::audio::taglib::utils
|
||||||
{
|
{
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||||
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
|
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle);
|
||||||
} // namespace lms::audio::taglib::utils
|
} // namespace lms::audio::taglib::utils
|
||||||
@@ -48,9 +48,11 @@ namespace lms::audio
|
|||||||
enum class CodecType
|
enum class CodecType
|
||||||
{
|
{
|
||||||
AAC,
|
AAC,
|
||||||
|
AC3,
|
||||||
ALAC, // Apple Lossless Audio Codec (ALAC)
|
ALAC, // Apple Lossless Audio Codec (ALAC)
|
||||||
APE, // Monkey's Audio
|
APE, // Monkey's Audio
|
||||||
DSD, // DSD
|
DSD, // DSD
|
||||||
|
EAC3,
|
||||||
FLAC, // Flac
|
FLAC, // Flac
|
||||||
MP3,
|
MP3,
|
||||||
MP4ALS, // MPEG-4 Audio Lossless Coding
|
MP4ALS, // MPEG-4 Audio Lossless Coding
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
#include "core/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::audio
|
namespace lms::audio
|
||||||
@@ -28,4 +30,20 @@ namespace lms::audio
|
|||||||
public:
|
public:
|
||||||
using LmsException::LmsException;
|
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
|
} // namespace lms::audio
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -19,11 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <span>
|
|
||||||
|
|
||||||
#include "audio/Exception.hpp"
|
|
||||||
|
|
||||||
namespace lms::audio
|
namespace lms::audio
|
||||||
{
|
{
|
||||||
class AudioProperties;
|
class AudioProperties;
|
||||||
@@ -35,52 +30,8 @@ namespace lms::audio
|
|||||||
public:
|
public:
|
||||||
virtual ~IAudioFileInfo() = default;
|
virtual ~IAudioFileInfo() = default;
|
||||||
|
|
||||||
virtual const AudioProperties& getAudioProperties() 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 IImageReader* getImageReader() const = 0;
|
||||||
virtual const ITagReader& getTagReader() const = 0;
|
virtual const ITagReader* getTagReader() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AudioFileParsingException : public Exception
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using Exception::Exception;
|
|
||||||
};
|
|
||||||
|
|
||||||
class IOException : public Exception
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
IOException(std::string_view message, std::error_code err)
|
|
||||||
: Exception{ std::string{ message } + ": " + err.message() }
|
|
||||||
, _err{ err }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::error_code getErrorCode() const { return _err; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::error_code _err;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ParserOptions
|
|
||||||
{
|
|
||||||
enum class Parser
|
|
||||||
{
|
|
||||||
TagLib,
|
|
||||||
FFmpeg,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class AudioPropertiesReadStyle
|
|
||||||
{
|
|
||||||
Fast,
|
|
||||||
Average,
|
|
||||||
Accurate,
|
|
||||||
};
|
|
||||||
|
|
||||||
Parser parser{ Parser::TagLib };
|
|
||||||
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
|
|
||||||
bool enableExtraDebugLogs{};
|
|
||||||
};
|
|
||||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
|
|
||||||
|
|
||||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
|
|
||||||
} // namespace lms::audio
|
} // namespace lms::audio
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
class IAudioFileInfo;
|
||||||
|
|
||||||
|
struct AudioFileInfoParseOptions
|
||||||
|
{
|
||||||
|
enum class AudioPropertiesReadStyle
|
||||||
|
{
|
||||||
|
Fast, // fastest
|
||||||
|
Average, // normal
|
||||||
|
Accurate, // full scan
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioPropertiesReadStyle audioPropertiesReadStyle{ AudioPropertiesReadStyle::Average };
|
||||||
|
bool readTags{ true };
|
||||||
|
bool readImages{ true };
|
||||||
|
|
||||||
|
bool enableExtraDebugLogs{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class IAudioFileInfoParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IAudioFileInfoParser() = default;
|
||||||
|
|
||||||
|
virtual std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const = 0;
|
||||||
|
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AudioFileInfoParserBackend
|
||||||
|
{
|
||||||
|
TagLib,
|
||||||
|
FFmpeg,
|
||||||
|
};
|
||||||
|
static inline constexpr AudioFileInfoParserBackend defaultAudioFileInfoParserBackend{ AudioFileInfoParserBackend::TagLib };
|
||||||
|
std::unique_ptr<IAudioFileInfoParser> createAudioFileInfoParser(AudioFileInfoParserBackend backend = defaultAudioFileInfoParserBackend);
|
||||||
|
} // namespace lms::audio
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "audio/IImageReader.hpp"
|
#include "audio/IImageReader.hpp"
|
||||||
#include "database/IDb.hpp"
|
#include "database/IDb.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
@@ -53,6 +55,7 @@ namespace lms::artwork
|
|||||||
const std::filesystem::path& defaultArtistImageSvgPath)
|
const std::filesystem::path& defaultArtistImageSvgPath)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||||
|
, _audioFileInfoParser{ audio::createAudioFileInfoParser() }
|
||||||
{
|
{
|
||||||
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||||
|
|
||||||
@@ -108,11 +111,14 @@ namespace lms::artwork
|
|||||||
{
|
{
|
||||||
std::size_t currentIndex{};
|
std::size_t currentIndex{};
|
||||||
|
|
||||||
audio::ParserOptions options;
|
audio::AudioFileInfoParseOptions options;
|
||||||
options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images
|
options.audioPropertiesReadStyle = audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle::Fast; // only for images
|
||||||
|
options.readTags = false;
|
||||||
|
options.readImages = true;
|
||||||
|
|
||||||
auto audioFile{ audio::parseAudioFile(p) };
|
const auto audioFileInfo{ _audioFileInfoParser->parse(p, options) };
|
||||||
audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) {
|
assert(audioFileInfo->getImageReader());
|
||||||
|
audioFileInfo->getImageReader()->visitImages([&](const audio::Image& parsedImage) {
|
||||||
if (currentIndex++ != index)
|
if (currentIndex++ != index)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
|
|
||||||
#include "ImageCache.hpp"
|
#include "ImageCache.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
class IAudioFileInfoParser;
|
||||||
|
}
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
@@ -65,6 +70,7 @@ namespace lms::artwork
|
|||||||
ImageCache _cache;
|
ImageCache _cache;
|
||||||
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
||||||
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
||||||
|
std::unique_ptr<audio::IAudioFileInfoParser> _audioFileInfoParser;
|
||||||
|
|
||||||
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
|
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
|
||||||
unsigned _jpegQuality;
|
unsigned _jpegQuality;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ add_library(lmsscanner STATIC
|
|||||||
impl/helpers/ArtistHelpers.cpp
|
impl/helpers/ArtistHelpers.cpp
|
||||||
impl/scanners/artistinfo/ArtistInfoParser.cpp
|
impl/scanners/artistinfo/ArtistInfoParser.cpp
|
||||||
impl/scanners/artistinfo/ArtistInfoFileScanner.cpp
|
impl/scanners/artistinfo/ArtistInfoFileScanner.cpp
|
||||||
|
impl/scanners/audiofile/AudioFileInfoParserSet.cpp
|
||||||
impl/scanners/audiofile/AudioFileScanOperation.cpp
|
impl/scanners/audiofile/AudioFileScanOperation.cpp
|
||||||
impl/scanners/audiofile/AudioFileScanner.cpp
|
impl/scanners/audiofile/AudioFileScanner.cpp
|
||||||
impl/scanners/audiofile/TrackMetadataParser.cpp
|
impl/scanners/audiofile/TrackMetadataParser.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<core::IConfig>::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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
struct AudioFileInfoParserSet
|
||||||
|
{
|
||||||
|
std::unique_ptr<audio::IAudioFileInfoParser> taglibParser;
|
||||||
|
std::unique_ptr<audio::IAudioFileInfoParser> ffmpegParser;
|
||||||
|
std::vector<std::filesystem::path> supportedExtensions;
|
||||||
|
|
||||||
|
audio::AudioFileInfoParseOptions::AudioPropertiesReadStyle audioPropertiesReadStyle; // a bit hacky to have this here
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioFileInfoParserSet createAudioFileInfoParserSet();
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -25,7 +25,9 @@
|
|||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
#include "core/XxHash3.hpp"
|
#include "core/XxHash3.hpp"
|
||||||
|
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
#include "helpers/ArtistHelpers.hpp"
|
#include "helpers/ArtistHelpers.hpp"
|
||||||
#include "scanners/IFileScanOperation.hpp"
|
#include "scanners/IFileScanOperation.hpp"
|
||||||
#include "scanners/Utils.hpp"
|
#include "scanners/Utils.hpp"
|
||||||
|
#include "scanners/audiofile/AudioFileInfoParserSet.hpp"
|
||||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
@@ -488,10 +491,10 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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 }
|
: FileScanOperationBase{ std::move(fileToScan), db, settings }
|
||||||
|
, _audioFileInfoParserSet{ audioFileInfoParserSet }
|
||||||
, _metadataParser{ metadataParser }
|
, _metadataParser{ metadataParser }
|
||||||
, _parserOptions{ parserOptions }
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,18 +504,42 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
try
|
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.emplace();
|
||||||
|
|
||||||
_file->audioProperties = audioFileInfo->getAudioProperties();
|
// Fallback on ffmpeg in case no audio properties are found by taglib
|
||||||
_file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader());
|
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<NoAudioTrackFoundError>(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
|
// We fill missing artist mbids with mbids found on other artist roles
|
||||||
fillMissingMbids(_file->track);
|
fillMissingMbids(_file->track);
|
||||||
|
|
||||||
std::size_t index{};
|
std::size_t index{};
|
||||||
audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) {
|
audioFileInfo->getImageReader()->visitImages([&](const audio::Image& image) {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||||
@@ -539,7 +566,7 @@ namespace lms::scanner
|
|||||||
index++;
|
index++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (const audio::IOException& e)
|
catch (const audio::IOFileException& e)
|
||||||
{
|
{
|
||||||
addError<IOScanError>(getFilePath(), e.getErrorCode());
|
addError<IOScanError>(getFilePath(), e.getErrorCode());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
|
||||||
#include "audio/IImageReader.hpp"
|
#include "audio/IImageReader.hpp"
|
||||||
#include "image/Types.hpp"
|
#include "image/Types.hpp"
|
||||||
|
|
||||||
@@ -39,6 +38,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
class AudioFileInfoParserSet;
|
||||||
class TrackMetadataParser;
|
class TrackMetadataParser;
|
||||||
|
|
||||||
struct ImageInfo
|
struct ImageInfo
|
||||||
@@ -55,7 +55,7 @@ namespace lms::scanner
|
|||||||
class AudioFileScanOperation : public FileScanOperationBase
|
class AudioFileScanOperation : public FileScanOperationBase
|
||||||
{
|
{
|
||||||
public:
|
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() override;
|
||||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||||
@@ -65,8 +65,8 @@ namespace lms::scanner
|
|||||||
void scan() override;
|
void scan() override;
|
||||||
OperationResult processResult() override;
|
OperationResult processResult() override;
|
||||||
|
|
||||||
|
const AudioFileInfoParserSet& _audioFileInfoParserSet;
|
||||||
const TrackMetadataParser& _metadataParser;
|
const TrackMetadataParser& _metadataParser;
|
||||||
const audio::ParserOptions& _parserOptions;
|
|
||||||
|
|
||||||
struct AudioFileInfo
|
struct AudioFileInfo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,11 +19,6 @@
|
|||||||
|
|
||||||
#include "AudioFileScanner.hpp"
|
#include "AudioFileScanner.hpp"
|
||||||
|
|
||||||
#include "core/IConfig.hpp"
|
|
||||||
#include "core/Service.hpp"
|
|
||||||
|
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
|
||||||
|
|
||||||
#include "database/IDb.hpp"
|
#include "database/IDb.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
@@ -31,6 +26,7 @@
|
|||||||
|
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "scanners/Utils.hpp"
|
#include "scanners/Utils.hpp"
|
||||||
|
#include "scanners/audiofile/AudioFileInfoParserSet.hpp"
|
||||||
#include "scanners/audiofile/AudioFileScanOperation.hpp"
|
#include "scanners/audiofile/AudioFileScanOperation.hpp"
|
||||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||||
|
|
||||||
@@ -38,20 +34,6 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
|
|
||||||
{
|
|
||||||
std::string_view readStyle{ core::Service<core::IConfig>::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 createTrackMetadataParserParameters(const ScannerSettings& settings)
|
||||||
{
|
{
|
||||||
TrackMetadataParser::Parameters params;
|
TrackMetadataParser::Parameters params;
|
||||||
@@ -62,23 +44,13 @@ namespace lms::scanner
|
|||||||
|
|
||||||
return params;
|
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
|
} // namespace
|
||||||
|
|
||||||
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
|
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
, _settings{ settings }
|
, _settings{ settings }
|
||||||
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
|
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
|
||||||
, _parserOptions{ createAudioFileParserOptions() }
|
, _audioFileInfoParserSet{ createAudioFileInfoParserSet() }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +68,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
|
||||||
{
|
{
|
||||||
return audio::getSupportedExtensions(_parserOptions.parser);
|
return _audioFileInfoParserSet.supportedExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioFileScanner::needsScan(const FileToScan& file) const
|
bool AudioFileScanner::needsScan(const FileToScan& file) const
|
||||||
@@ -112,6 +84,6 @@ namespace lms::scanner
|
|||||||
|
|
||||||
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
|
||||||
{
|
{
|
||||||
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
|
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _audioFileInfoParserSet, _trackMetadataParser);
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -19,23 +19,14 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
|
||||||
|
|
||||||
#include "scanners/IFileScanner.hpp"
|
#include "scanners/IFileScanner.hpp"
|
||||||
|
#include "scanners/audiofile/AudioFileInfoParserSet.hpp"
|
||||||
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
#include "scanners/audiofile/TrackMetadataParser.hpp"
|
||||||
|
|
||||||
namespace lms
|
namespace lms::db
|
||||||
{
|
{
|
||||||
namespace db
|
class IDb;
|
||||||
{
|
}
|
||||||
class IDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace metadata
|
|
||||||
{
|
|
||||||
class IAudioFileParser;
|
|
||||||
}
|
|
||||||
} // namespace lms
|
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -59,6 +50,6 @@ namespace lms::scanner
|
|||||||
db::IDb& _db;
|
db::IDb& _db;
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
const TrackMetadataParser _trackMetadataParser;
|
const TrackMetadataParser _trackMetadataParser;
|
||||||
const audio::ParserOptions _parserOptions;
|
const AudioFileInfoParserSet _audioFileInfoParserSet;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/Exception.hpp"
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/objects/PodcastEpisode.hpp"
|
#include "database/objects/PodcastEpisode.hpp"
|
||||||
@@ -118,13 +119,21 @@ namespace lms::api::subsonic
|
|||||||
// TODO: put this information in db during scan
|
// TODO: put this information in db during scan
|
||||||
try
|
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)
|
catch (const audio::Exception& e)
|
||||||
{
|
{
|
||||||
// TODO 404?
|
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/objects/Artist.hpp"
|
#include "database/objects/Artist.hpp"
|
||||||
@@ -145,10 +147,18 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
try
|
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->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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
|
|
||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/objects/Artist.hpp"
|
#include "database/objects/Artist.hpp"
|
||||||
@@ -131,10 +133,18 @@ namespace lms::ui::TrackListHelpers
|
|||||||
|
|
||||||
try
|
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->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)
|
catch (const audio::Exception& e)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "audio/AudioTypes.hpp"
|
#include "audio/AudioTypes.hpp"
|
||||||
#include "audio/Exception.hpp"
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioFileInfo.hpp"
|
#include "audio/IAudioFileInfo.hpp"
|
||||||
|
#include "audio/IAudioFileInfoParser.hpp"
|
||||||
#include "audio/IImageReader.hpp"
|
#include "audio/IImageReader.hpp"
|
||||||
#include "audio/ITagReader.hpp"
|
#include "audio/ITagReader.hpp"
|
||||||
|
|
||||||
@@ -133,11 +134,21 @@ namespace lms::audio
|
|||||||
|
|
||||||
void displayInfo(const IAudioFileInfo& fileInfo)
|
void displayInfo(const IAudioFileInfo& fileInfo)
|
||||||
{
|
{
|
||||||
std::cout << "Audio properties:\n"
|
if (fileInfo.getAudioProperties())
|
||||||
<< fileInfo.getAudioProperties() << std::endl;
|
{
|
||||||
|
std::cout << "Audio properties:\n"
|
||||||
|
<< *fileInfo.getAudioProperties() << "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Failed to parse audio properties!\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
displayImages(fileInfo.getImageReader());
|
if (fileInfo.getImageReader())
|
||||||
displayTags(fileInfo.getTagReader());
|
displayImages(*fileInfo.getImageReader());
|
||||||
|
|
||||||
|
if (fileInfo.getTagReader())
|
||||||
|
displayTags(*fileInfo.getTagReader());
|
||||||
|
|
||||||
std::cout << "\n";
|
std::cout << "\n";
|
||||||
}
|
}
|
||||||
@@ -200,15 +211,16 @@ int main(int argc, char* argv[])
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio::ParserOptions parserOptions;
|
audio::AudioFileInfoParserBackend parserBackend{ audio::defaultAudioFileInfoParserBackend };
|
||||||
parserOptions.enableExtraDebugLogs = true;
|
|
||||||
if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as<std::string>(), "taglib"))
|
if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as<std::string>(), "taglib"))
|
||||||
parserOptions.parser = audio::ParserOptions::Parser::TagLib;
|
parserBackend = audio::AudioFileInfoParserBackend::TagLib;
|
||||||
else if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as<std::string>(), "ffmpeg"))
|
else if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as<std::string>(), "ffmpeg"))
|
||||||
parserOptions.parser = audio::ParserOptions::Parser::FFmpeg;
|
parserBackend = audio::AudioFileInfoParserBackend::FFmpeg;
|
||||||
else
|
else
|
||||||
throw program_options::validation_error{ program_options::validation_error::invalid_option_value, "parser" };
|
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<std::vector<std::string>>() };
|
const auto& inputFiles{ vm["file"].as<std::vector<std::string>>() };
|
||||||
// log to stdout
|
// log to stdout
|
||||||
core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::DEBUG) };
|
core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::DEBUG) };
|
||||||
@@ -219,8 +231,11 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
audio::AudioFileInfoParseOptions parseOptions;
|
||||||
|
parseOptions.enableExtraDebugLogs = true;
|
||||||
|
|
||||||
std::cout << "Parsing file " << file << ":\n";
|
std::cout << "Parsing file " << file << ":\n";
|
||||||
const auto audioFileInfo{ audio::parseAudioFile(file, parserOptions) };
|
const auto audioFileInfo{ parser->parse(file, parseOptions) };
|
||||||
|
|
||||||
displayInfo(*audioFileInfo);
|
displayInfo(*audioFileInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user