Scan for audio properties, metadata, and embedded images in one single pass. Removed now useless lmsmetadata library + reworked code accordingly

This commit is contained in:
emeric
2025-11-02 16:23:10 +01:00
parent 8e554b258c
commit b25e3b9b1e
112 changed files with 4005 additions and 3379 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ __Note__: If no name is provided in the `artist.nfo` file, the name of the conta
### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_.
__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_.
### Multiple artists
_LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
+1 -2
View File
@@ -1,8 +1,7 @@
add_subdirectory(av)
add_subdirectory(audio)
add_subdirectory(core)
add_subdirectory(database)
add_subdirectory(image)
add_subdirectory(metadata)
add_subdirectory(services)
add_subdirectory(som)
add_subdirectory(subsonic)
+40
View File
@@ -0,0 +1,40 @@
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
add_library(lmsaudio STATIC
impl/ffmpeg/AudioFile.cpp
impl/ffmpeg/AudioFileInfo.cpp
impl/ffmpeg/ImageReader.cpp
impl/ffmpeg/TagReader.cpp
impl/ffmpeg/Transcoder.cpp
impl/ffmpeg/Utils.cpp
impl/taglib/AudioFileInfo.cpp
impl/taglib/ImageReader.cpp
impl/taglib/TagReader.cpp
impl/taglib/Utils.cpp
impl/AudioTypes.cpp
impl/ImageReader.cpp
impl/ParseAudioFileInfo.cpp
impl/TagReader.cpp
)
target_include_directories(lmsaudio INTERFACE
include
)
target_include_directories(lmsaudio PRIVATE
include
${AVCODEC_INCLUDE_DIR}
${AVFORMAT_INCLUDE_DIR}
${AVUTIL_INCLUDE_DIR}
)
target_link_libraries(lmsaudio PUBLIC
lmscore
std::filesystem
)
target_link_libraries(lmsaudio PRIVATE
PkgConfig::LIBAV
PkgConfig::Taglib
)
+109
View File
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audio/AudioTypes.hpp"
namespace lms::audio
{
core::LiteralString containerTypeToString(ContainerType type)
{
switch (type)
{
case ContainerType::AIFF:
return "AIFF";
case ContainerType::APE:
return "APE";
case ContainerType::ASF:
return "ASF";
case ContainerType::DSF:
return "DSF";
case ContainerType::FLAC:
return "FLAC";
case ContainerType::MP4:
return "MP4";
case ContainerType::MPC:
return "MPC";
case ContainerType::MPEG:
return "MPEG";
case ContainerType::Ogg:
return "Ogg";
case ContainerType::Shorten:
return "Shorten";
case ContainerType::TrueAudio:
return "TrueAudio";
case ContainerType::WAV:
return "WAV";
case ContainerType::WavPack:
return "WavPack";
}
return "";
}
core::LiteralString codecTypeToString(CodecType type)
{
switch (type)
{
case CodecType::AAC:
return "AAC";
case CodecType::AC3:
return "AC3";
case CodecType::ALAC:
return "ALAC";
case CodecType::APE:
return "APE";
case CodecType::EAC3:
return "EAC3";
case CodecType::DSD:
return "DSD";
case CodecType::FLAC:
return "FLAC";
case CodecType::MP3:
return "MP3";
case CodecType::MP4ALS:
return "MP4ALS";
case CodecType::MPC7:
return "MPC7";
case CodecType::MPC8:
return "MPC8";
case CodecType::Opus:
return "Opus";
case CodecType::PCM:
return "PCM";
case CodecType::Shorten:
return "Shorten";
case CodecType::TrueAudio:
return "TrueAudio";
case CodecType::Vorbis:
return "Vorbis";
case CodecType::WavPack:
return "WavPack";
case CodecType::WMA1:
return "WMA1";
case CodecType::WMA2:
return "WMA2";
case CodecType::WMA9Pro:
return "WMA9Pro";
case CodecType::WMA9Lossless:
return "WMA9Lossless";
}
return "";
}
} // namespace lms::audio
+77
View File
@@ -0,0 +1,77 @@
/*
* 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 "audio/IImageReader.hpp"
namespace lms::audio
{
core::LiteralString imageTypeToString(Image::Type type)
{
switch (type)
{
case Image::Type::Other:
return "Other";
case Image::Type::FileIcon:
return "FileIcon";
case Image::Type::OtherFileIcon:
return "OtherFileIcon";
case Image::Type::FrontCover:
return "FrontCover";
case Image::Type::BackCover:
return "BackCover";
case Image::Type::LeafletPage:
return "LeafletPage";
case Image::Type::Media:
return "Media";
case Image::Type::LeadArtist:
return "LeadArtist";
case Image::Type::Artist:
return "Artist";
case Image::Type::Conductor:
return "Conductor";
case Image::Type::Band:
return "Band";
case Image::Type::Composer:
return "Composer";
case Image::Type::Lyricist:
return "Lyricist";
case Image::Type::RecordingLocation:
return "RecordingLocation";
case Image::Type::DuringRecording:
return "DuringRecording";
case Image::Type::DuringPerformance:
return "DuringPerformance";
case Image::Type::MovieScreenCapture:
return "MovieScreenCapture";
case Image::Type::ColouredFish:
return "ColouredFish";
case Image::Type::Illustration:
return "Illustration";
case Image::Type::BandLogo:
return "BandLogo";
case Image::Type::PublisherLogo:
return "PublisherLogo";
case Image::Type::Unknown:
break;
}
return "Unknown";
}
} // namespace lms::audio
@@ -0,0 +1,57 @@
/*
* 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
+277
View File
@@ -0,0 +1,277 @@
/*
* 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/ITagReader.hpp"
namespace lms::audio
{
core::LiteralString tagTypeToString(TagType type)
{
switch (type)
{
case TagType::AcoustID:
return "AcoustID";
case TagType::AcoustIDFingerprint:
return "AcoustIDFingerprint";
case TagType::Advisory:
return "Advisory";
case TagType::Album:
return "Album";
case TagType::AlbumArtist:
return "AlbumArtist";
case TagType::AlbumArtists:
return "AlbumArtists";
case TagType::AlbumArtistSortOrder:
return "AlbumArtistSortOrder";
case TagType::AlbumArtistsSortOrder:
return "AlbumArtistsSortOrder";
case TagType::AlbumComment:
return "AlbumComment";
case TagType::AlbumSortOrder:
return "AlbumSortOrder";
case TagType::Arranger:
return "Arranger";
case TagType::Artist:
return "Artist";
case TagType::ArtistSortOrder:
return "ArtistSortOrder";
case TagType::Artists:
return "Artists";
case TagType::ArtistsSortOrder:
return "ArtistsSortOrder";
case TagType::ASIN:
return "ASIN";
case TagType::Barcode:
return "Barcode";
case TagType::BPM:
return "BPM";
case TagType::CatalogNumber:
return "CatalogNumber";
case TagType::Comment:
return "Comment";
case TagType::Compilation:
return "Compilation";
case TagType::Composer:
return "Composer";
case TagType::ComposerSortOrder:
return "ComposerSortOrder";
case TagType::Composers:
return "Composers";
case TagType::ComposersSortOrder:
return "ComposersSortOrder";
case TagType::Conductor:
return "Conductor";
case TagType::ConductorSortOrder:
return "ConductorSortOrder";
case TagType::Conductors:
return "Conductors";
case TagType::ConductorsSortOrder:
return "ConductorsSortOrder";
case TagType::Copyright:
return "Copyright";
case TagType::CopyrightURL:
return "CopyrightURL";
case TagType::Date:
return "Date";
case TagType::Director:
return "Director";
case TagType::DiscNumber:
return "DiscNumber";
case TagType::DiscSubtitle:
return "DiscSubtitle";
case TagType::EncodedBy:
return "EncodedBy";
case TagType::EncoderSettings:
return "EncoderSettings";
case TagType::EncodingTime:
return "EncodingTime";
case TagType::Engineer:
return "Engineer";
case TagType::GaplessPlayback:
return "GaplessPlayback";
case TagType::Genre:
return "Genre";
case TagType::Grouping:
return "Grouping";
case TagType::InitialKey:
return "InitialKey";
case TagType::ISRC:
return "ISRC";
case TagType::Language:
return "Language";
case TagType::Lyricist:
return "Lyricist";
case TagType::LyricistSortOrder:
return "LyricistSortOrder";
case TagType::Lyricists:
return "Lyricists";
case TagType::LyricistsSortOrder:
return "LyricistsSortOrder";
case TagType::Media:
return "Media";
case TagType::MixDJ:
return "MixDJ";
case TagType::Mixer:
return "Mixer";
case TagType::MixerSortOrder:
return "MixerSortOrder";
case TagType::Mixers:
return "Mixers";
case TagType::MixersSortOrder:
return "MixersSortOrder";
case TagType::Movement:
return "Movement";
case TagType::MovementCount:
return "MovementCount";
case TagType::MovementNumber:
return "MovementNumber";
case TagType::Mood:
return "Mood";
case TagType::MusicBrainzArtistID:
return "MusicBrainzArtistID";
case TagType::MusicBrainzArrangerID:
return "MusicBrainzArrangerID";
case TagType::MusicBrainzComposerID:
return "MusicBrainzComposerID";
case TagType::MusicBrainzConductorID:
return "MusicBrainzConductorID";
case TagType::MusicBrainzDirectorID:
return "MusicBrainzDirectorID";
case TagType::MusicBrainzDiscID:
return "MusicBrainzDiscID";
case TagType::MusicBrainzLyricistID:
return "MusicBrainzLyricistID";
case TagType::MusicBrainzProducerID:
return "MusicBrainzProducerID";
case TagType::MusicBrainzOriginalArtistID:
return "MusicBrainzOriginalArtistID";
case TagType::MusicBrainzRecordingID:
return "MusicBrainzRecordingID";
case TagType::MusicBrainzRemixerID:
return "MusicBrainzRemixerID";
case TagType::MusicBrainzWorkID:
return "MusicBrainzWorkID";
case TagType::Remixer:
return "Remixer";
case TagType::Script:
return "Script";
case TagType::ShowName:
return "ShowName";
case TagType::ShowNameSortOrder:
return "ShowNameSortOrder";
case TagType::ShowWorkAndMovement:
return "ShowWorkAndMovement";
case TagType::Subtitle:
return "Subtitle";
case TagType::TrackNumber:
return "TrackNumber";
case TagType::TrackTitle:
return "TrackTitle";
case TagType::TrackTitleSortOrder:
return "TrackTitleSortOrder";
case TagType::WorkTitle:
return "WorkTitle";
case TagType::Writer:
return "Writer";
case TagType::License:
return "License";
case TagType::MusicBrainzOriginalReleaseID:
return "MusicBrainzOriginalReleaseID";
case TagType::MusicBrainzMixerID:
return "MusicBrainzMixerID";
case TagType::MusicBrainzReleaseArtistID:
return "MusicBrainzReleaseArtistID";
case TagType::MusicBrainzReleaseGroupID:
return "MusicBrainzReleaseGroupID";
case TagType::MusicBrainzReleaseID:
return "MusicBrainzReleaseID";
case TagType::MusicBrainzTrackID:
return "MusicBrainzTrackID";
case TagType::MusicIPFingerprint:
return "MusicIPFingerprint";
case TagType::MusicIPPUID:
return "MusicIPPUID";
case TagType::OriginalAlbum:
return "OriginalAlbum";
case TagType::OriginalArtist:
return "OriginalArtist";
case TagType::OriginalFilename:
return "OriginalFilename";
case TagType::OriginalReleaseDate:
return "OriginalReleaseDate";
case TagType::OriginalReleaseYear:
return "OriginalReleaseYear";
case TagType::Podcast:
return "Podcast";
case TagType::PodcastURL:
return "PodcastURL";
case TagType::Producer:
return "Producer";
case TagType::ProducerSortOrder:
return "ProducerSortOrder";
case TagType::Producers:
return "Producers";
case TagType::ProducersSortOrder:
return "ProducersSortOrder";
case TagType::Rating:
return "Rating";
case TagType::RecordLabel:
return "RecordLabel";
case TagType::ReleaseCountry:
return "ReleaseCountry";
case TagType::ReleaseDate:
return "ReleaseDate";
case TagType::ReleaseStatus:
return "ReleaseStatus";
case TagType::ReleaseType:
return "ReleaseType";
case TagType::RemixerSortOrder:
return "RemixerSortOrder";
case TagType::Remixers:
return "Remixers";
case TagType::RemixersSortOrder:
return "RemixersSortOrder";
case TagType::ReplayGainAlbumGain:
return "ReplayGainAlbumGain";
case TagType::ReplayGainAlbumPeak:
return "ReplayGainAlbumPeak";
case TagType::ReplayGainAlbumRange:
return "ReplayGainAlbumRange";
case TagType::ReplayGainReferenceLoudness:
return "ReplayGainReferenceLoudness";
case TagType::ReplayGainTrackGain:
return "ReplayGainTrackGain";
case TagType::ReplayGainTrackPeak:
return "ReplayGainTrackPeak";
case TagType::ReplayGainTrackRange:
return "ReplayGainTrackRange";
case TagType::TotalDiscs:
return "TotalDiscs";
case TagType::TotalTracks:
return "TotalTracks";
case TagType::Website:
return "Website";
case TagType::Count:
break;
}
return "";
}
} // namespace lms::audio
@@ -19,6 +19,9 @@
#include "AudioFile.hpp"
#include <array>
#include <unordered_map>
extern "C"
{
#define __STDC_CONSTANT_MACROS
@@ -27,15 +30,13 @@ extern "C"
#include <libavutil/error.h>
}
#include <array>
#include <unordered_map>
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "av/Exception.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
namespace lms::av
namespace lms::audio::ffmpeg
{
namespace
{
@@ -49,11 +50,11 @@ namespace lms::av
return "Unknown error";
}
class AudioFileException : public Exception
class AudioFileException : public AudioFileParsingException
{
public:
AudioFileException(int avError)
: Exception{ "AudioFileException: " + averror_to_string(avError) }
: AudioFileParsingException{ averror_to_string(avError) }
{
}
};
@@ -70,76 +71,104 @@ namespace lms::av
}
}
DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
{
if (name == "aiff")
return ContainerType::AIFF;
if (name == "ape")
return ContainerType::APE;
if (name.starts_with("asf"))
return ContainerType::ASF;
if (name == "dsf")
return ContainerType::DSF;
if (name == "flac")
return ContainerType::FLAC;
if (name.find("mp4") != std::string_view::npos)
return ContainerType::MP4;
if (name.starts_with("mpc"))
return ContainerType::MPC;
if (name == "mp3")
return ContainerType::MPEG;
if (name == "ogg")
return ContainerType::Ogg;
if (name == "shn")
return ContainerType::Shorten;
if (name == "tta")
return ContainerType::TrueAudio;
if (name == "wav")
return ContainerType::WAV;
if (name == "wv")
return ContainerType::WavPack;
return std::nullopt;
}
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_MP3:
return DecodingCodec::MP3;
return CodecType::MP3;
case AV_CODEC_ID_AAC:
return DecodingCodec::AAC;
return CodecType::AAC;
case AV_CODEC_ID_AC3:
return DecodingCodec::AC3;
return CodecType::AC3;
case AV_CODEC_ID_VORBIS:
return DecodingCodec::VORBIS;
return CodecType::Vorbis;
case AV_CODEC_ID_WMAV1:
return DecodingCodec::WMAV1;
return CodecType::WMA1;
case AV_CODEC_ID_WMAV2:
return DecodingCodec::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 DecodingCodec::FLAC;
return CodecType::FLAC;
case AV_CODEC_ID_ALAC:
return DecodingCodec::ALAC;
return CodecType::ALAC;
case AV_CODEC_ID_WAVPACK:
return DecodingCodec::WAVPACK;
return CodecType::WavPack;
case AV_CODEC_ID_MUSEPACK7:
return DecodingCodec::MUSEPACK7;
return CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8:
return DecodingCodec::MUSEPACK8;
return CodecType::MPC8;
case AV_CODEC_ID_APE:
return DecodingCodec::APE;
return CodecType::APE;
case AV_CODEC_ID_EAC3:
return DecodingCodec::EAC3;
return CodecType::EAC3;
case AV_CODEC_ID_MP4ALS:
return DecodingCodec::MP4ALS;
return CodecType::MP4ALS;
case AV_CODEC_ID_OPUS:
return DecodingCodec::OPUS;
return CodecType::Opus;
case AV_CODEC_ID_SHORTEN:
return DecodingCodec::SHORTEN;
return CodecType::Shorten;
case AV_CODEC_ID_DSD_LSBF:
return DecodingCodec::DSD_LSBF;
case AV_CODEC_ID_DSD_LSBF_PLANAR:
return DecodingCodec::DSD_LSBF_PLANAR;
case AV_CODEC_ID_DSD_MSBF:
return DecodingCodec::DSD_MSBF;
case AV_CODEC_ID_DSD_MSBF_PLANAR:
return DecodingCodec::DSD_MSBF_PLANAR;
return CodecType::DSD;
default:
return DecodingCodec::UNKNOWN;
return std::nullopt;
}
}
} // namespace
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
{
return std::make_unique<AudioFile>(p);
}
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
throw AudioFileException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AV, 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);
throw AudioFileException{ error };
}
@@ -158,9 +187,12 @@ namespace lms::av
ContainerInfo AudioFile::getContainerInfo() const
{
ContainerInfo info;
info.container = avdemuxerToContainerType(_context->iformat->name);
info.containerName = _context->iformat->name;
info.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
info.name = _context->iformat->name;
return info;
}
@@ -258,7 +290,7 @@ namespace lms::av
if (avstream->codecpar == nullptr)
{
LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
continue;
}
@@ -275,7 +307,7 @@ namespace lms::av
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
@@ -297,7 +329,7 @@ namespace lms::av
if (!avstream->codecpar)
{
LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
@@ -305,19 +337,29 @@ namespace lms::av
return res;
res.emplace();
res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
if (avstream->codecpar->bit_rate)
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
if (avstream->codecpar->bits_per_coded_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
else if (avstream->codecpar->bits_per_raw_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_raw_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
if (avstream->codecpar->channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
if (avstream->codecpar->ch_layout.nb_channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
assert(!res->codecName.empty()); // doc says it is never NULL
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
if (avstream->codecpar->sample_rate)
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
return res;
}
} // namespace lms::av
} // namespace lms::audio::ffmpeg
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include "audio/AudioTypes.hpp"
extern "C"
{
struct AVFormatContext;
}
namespace lms::audio::ffmpeg
{
struct Picture
{
std::string mimeType;
std::span<const std::byte> data; // valid as long as IAudioFile exists
};
struct ContainerInfo
{
std::optional<ContainerType> container;
std::string containerName;
std::size_t bitrate{};
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
std::optional<CodecType> codec;
std::string codecName;
std::optional<size_t> bitrate;
std::optional<std::size_t> bitsPerSample;
std::optional<std::size_t> channelCount;
std::optional<std::size_t> sampleRate;
};
class AudioFile
{
public:
AudioFile(const std::filesystem::path& p);
~AudioFile();
AudioFile(const AudioFile&) = delete;
AudioFile& operator=(const AudioFile&) = delete;
using MetadataMap = std::unordered_map<std::string, std::string>;
const std::filesystem::path& getPath() const;
ContainerInfo getContainerInfo() const;
MetadataMap getMetaData() const;
std::vector<StreamInfo> getStreamInfo() const;
std::optional<StreamInfo> getBestStreamInfo() const;
std::optional<std::size_t> getBestStreamIndex() const;
bool hasAttachedPictures() const;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context{};
};
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,82 @@
/*
* 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 "AudioFileInfo.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "AudioFile.hpp"
#include "ImageReader.hpp"
#include "TagReader.hpp"
namespace lms::audio::ffmpeg
{
namespace
{
AudioProperties computeAudioProperties(const AudioFile& audioFile)
{
AudioProperties audioProperties;
const auto containerInfo{ audioFile.getContainerInfo() };
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
if (!bestStreamInfo)
throw AudioFileParsingException{ audioFile.getPath(), "Cannot find best audio stream" };
if (!containerInfo.container)
throw AudioFileParsingException{ audioFile.getPath(), "Unhandled container type '" + containerInfo.containerName + "'" };
audioProperties.container = *containerInfo.container;
audioProperties.duration = containerInfo.duration;
audioProperties.codec = bestStreamInfo->codec;
audioProperties.bitrate = bestStreamInfo->bitrate;
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
audioProperties.channelCount = bestStreamInfo->channelCount;
audioProperties.sampleRate = bestStreamInfo->sampleRate;
return audioProperties;
}
} // namespace
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
: _audioFile{ std::make_unique<AudioFile>(filePath) }
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
{
}
AudioFileInfo::~AudioFileInfo() = default;
const AudioProperties& AudioFileInfo::getAudioProperties() const
{
return *_audioProperties;
}
const IImageReader& AudioFileInfo::getImageReader() const
{
return *_imageReader;
}
const ITagReader& AudioFileInfo::getTagReader() const
{
return *_tagReader;
}
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2020 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 "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::ffmpeg
{
class AudioFile;
class TagReader;
class ImageReader;
class AudioFileInfo final : public IAudioFileInfo
{
public:
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
~AudioFileInfo();
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;
std::unique_ptr<AudioFile> _audioFile;
std::unique_ptr<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
} // namespace lms::audio::ffmpeg
@@ -17,35 +17,30 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AvFormatImageReader.hpp"
#include "ImageReader.hpp"
#include "av/Exception.hpp"
#include "av/IAudioFile.hpp"
#include "metadata/Exception.hpp"
#include <algorithm>
namespace lms::metadata::avformat
#include "core/String.hpp"
#include "AudioFile.hpp"
namespace lms::audio::ffmpeg
{
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
ImageReader::ImageReader(const AudioFile& audioFile)
: _audioFile{ audioFile }
{
try
{
_audioFile = av::parseAudioFile(p);
}
catch (av::Exception& e)
{
throw AudioFileParsingException{ e.what() };
}
}
AvFormatImageReader::~AvFormatImageReader() = default;
ImageReader::~ImageReader() = default;
void AvFormatImageReader::visitImages(ImageVisitor visitor) const
void ImageReader::visitImages(const ImageVisitor& visitor) const
{
auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) {
auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) {
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
} };
_audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) {
_audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
@@ -58,4 +53,4 @@ namespace lms::metadata::avformat
});
}
} // namespace lms::metadata::avformat
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IImageReader.hpp"
namespace lms::audio::ffmpeg
{
class AudioFile;
class ImageReader : public IImageReader
{
public:
ImageReader(const AudioFile& audioFile);
~ImageReader() override;
ImageReader(const ImageReader&) = delete;
ImageReader& operator=(const ImageReader&) = delete;
private:
void visitImages(const ImageVisitor& visitor) const override;
const AudioFile& _audioFile;
};
} // namespace lms::audio::ffmpeg
@@ -17,15 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AvFormatTagReader.hpp"
#include "TagReader.hpp"
#include "av/Exception.hpp"
#include "av/IAudioFile.hpp"
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "metadata/Exception.hpp"
namespace lms::metadata::avformat
namespace lms::audio::ffmpeg
{
namespace
{
@@ -146,41 +143,20 @@ namespace lms::metadata::avformat
};
} // namespace
AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug)
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
: _audioFile{ audioFile }
, _metaDataMap{ audioFile.getMetaData() }
{
try
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
const auto audioFile{ av::parseAudioFile(p) };
_audioProperties.duration = audioFile->getContainerInfo().duration;
const auto bestAudioStream{ audioFile->getBestStreamInfo() };
if (bestAudioStream)
{
_audioProperties.bitrate = bestAudioStream->bitrate;
_audioProperties.bitsPerSample = bestAudioStream->bitsPerSample;
_audioProperties.channelCount = bestAudioStream->channelCount;
_audioProperties.sampleRate = bestAudioStream->sampleRate;
}
_containerInfo = audioFile->getContainerInfo();
_metaDataMap = audioFile->getMetaData();
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
for (const auto& [key, value] : _metaDataMap)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
}
}
catch (av::Exception& e)
{
throw AudioFileParsingException{ e.what() };
for (const auto& [key, value] : _metaDataMap)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
}
}
AvFormatTagReader::~AvFormatTagReader() = default;
TagReader::~TagReader() = default;
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ avFormatTagMapping.find(tag) };
if (itTagNames == std::cend(avFormatTagMapping))
@@ -200,7 +176,7 @@ namespace lms::metadata::avformat
}
}
void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
{
auto itValues{ _metaDataMap.find(std::string{ key }) };
if (itValues == std::cend(_metaDataMap))
@@ -209,14 +185,14 @@ namespace lms::metadata::avformat
visitor(itValues->second);
}
void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
});
}
void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
// MPEG files: need to visit LYRICS-language entries
for (const auto& [tag, value] : _metaDataMap)
@@ -234,4 +210,4 @@ namespace lms::metadata::avformat
visitor("", value);
});
}
} // namespace lms::metadata::avformat
} // namespace lms::audio::ffmpeg
@@ -19,31 +19,27 @@
#pragma once
#include <filesystem>
#include "audio/ITagReader.hpp"
#include "av/IAudioFile.hpp"
#include "AudioFile.hpp"
#include "ITagReader.hpp"
namespace lms::metadata::avformat
namespace lms::audio::ffmpeg
{
class AvFormatTagReader : public ITagReader
class TagReader : public ITagReader
{
public:
AvFormatTagReader(const std::filesystem::path& path, bool debug);
~AvFormatTagReader() override;
AvFormatTagReader(const AvFormatTagReader&) = delete;
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
~TagReader() override;
TagReader(const TagReader&) = delete;
TagReader& operator=(const TagReader&) = delete;
private:
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
AudioProperties _audioProperties;
av::IAudioFile::MetadataMap _metaDataMap;
av::ContainerInfo _containerInfo;
const AudioFile& _audioFile;
AudioFile::MetadataMap _metaDataMap;
};
} // namespace lms::metadata::avformat
} // namespace lms::audio::ffmpeg
@@ -27,17 +27,21 @@
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "av/Exception.hpp"
#include "audio/Exception.hpp"
#include "audio/TranscodeTypes.hpp"
namespace lms::av
namespace lms::audio
{
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters)
{
return std::make_unique<ffmpeg::Transcoder>(parameters);
}
} // namespace lms::audio
namespace lms::audio::ffmpeg
{
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
{
return std::make_unique<Transcoder>(inputParameters, outputParameters);
}
static std::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath;
@@ -48,10 +52,10 @@ namespace lms::av
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
}
Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams)
Transcoder::Transcoder(const TranscodeParameters& parameters)
: _debugId{ globalId++ }
, _inputParams{ inputParams }
, _outputParams{ outputParams }
, _inputParams{ parameters.inputParameters }
, _outputParams{ parameters.outputParameters }
{
start();
}
@@ -65,18 +69,18 @@ namespace lms::av
try
{
if (!std::filesystem::exists(_inputParams.file))
throw Exception{ "File " + _inputParams.file.string() + " does not exist!" };
if (!std::filesystem::is_regular_file(_inputParams.file))
throw Exception{ "File " + _inputParams.file.string() + " is not regular!" };
if (!std::filesystem::exists(_inputParams.filePath))
throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
if (!std::filesystem::is_regular_file(_inputParams.filePath))
throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
}
catch (const std::filesystem::filesystem_error& e)
{
// TODO store/raise e.code()
throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() };
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
}
LOG(INFO, "Transcoding file " << _inputParams.file);
LOG(INFO, "Transcoding file " << _inputParams.filePath);
std::vector<std::string> args;
@@ -101,14 +105,7 @@ namespace lms::av
// Input file
args.emplace_back("-i");
args.emplace_back(_inputParams.file.string());
// Stream mapping, if set
if (_inputParams.streamIndex)
{
args.emplace_back("-map");
args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex));
}
args.emplace_back(_inputParams.filePath.string());
if (_outputParams.stripMetadata)
{
@@ -121,47 +118,53 @@ namespace lms::av
args.emplace_back("-vn");
// Output bitrates
args.emplace_back("-b:a");
args.emplace_back(std::to_string(_outputParams.bitrate));
if (_outputParams.bitrate)
{
args.emplace_back("-b:a");
args.emplace_back(std::to_string(*_outputParams.bitrate));
}
// Codecs and formats
switch (_outputParams.format)
if (_outputParams.format)
{
case OutputFormat::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
switch (*_outputParams.format)
{
case OutputFormat::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("matroska");
break;
case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("matroska");
break;
case OutputFormat::OGG_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::OGG_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::WEBM_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("webm");
break;
case OutputFormat::WEBM_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("webm");
break;
default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParams.format)) + ")" };
default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
}
}
args.emplace_back("pipe:1");
@@ -199,18 +202,22 @@ namespace lms::av
std::string_view Transcoder::getOutputMimeType() const
{
switch (_outputParams.format)
// TODO: use input mime type
if (_outputParams.format)
{
case OutputFormat::MP3:
return "audio/mpeg";
case OutputFormat::OGG_OPUS:
return "audio/opus";
case OutputFormat::MATROSKA_OPUS:
return "audio/x-matroska";
case OutputFormat::OGG_VORBIS:
return "audio/ogg";
case OutputFormat::WEBM_VORBIS:
return "audio/webm";
switch (*_outputParams.format)
{
case OutputFormat::MP3:
return "audio/mpeg";
case OutputFormat::OGG_OPUS:
return "audio/opus";
case OutputFormat::MATROSKA_OPUS:
return "audio/x-matroska";
case OutputFormat::OGG_VORBIS:
return "audio/ogg";
case OutputFormat::WEBM_VORBIS:
return "audio/webm";
}
}
return "application/octet-stream"; // default, should not happen
@@ -222,5 +229,4 @@ namespace lms::av
return _childProcess->finished();
}
} // namespace lms::av
} // namespace lms::audio::ffmpeg
@@ -19,19 +19,19 @@
#pragma once
#include "av/ITranscoder.hpp"
#include "audio/ITranscoder.hpp"
namespace lms::core
{
class IChildProcess;
}
namespace lms::av
namespace lms::audio::ffmpeg
{
class Transcoder : public ITranscoder
{
public:
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
Transcoder(const TranscodeParameters& parameters);
~Transcoder() override;
Transcoder(const Transcoder&) = delete;
Transcoder& operator=(const Transcoder&) = delete;
@@ -41,15 +41,15 @@ namespace lms::av
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
std::string_view getOutputMimeType() const override;
const OutputParameters& getOutputParameters() const override { return _outputParams; }
const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; }
bool finished() const override;
static void init();
void start();
const std::size_t _debugId{};
const InputParameters _inputParams;
const OutputParameters _outputParams;
const TranscodeInputParameters _inputParams;
const TranscodeOutputParameters _outputParams;
std::unique_ptr<core::IChildProcess> _childProcess;
};
} // namespace lms::av
} // namespace lms::audio::ffmpeg
@@ -19,11 +19,11 @@
#include "Utils.hpp"
namespace lms::metadata::avformat::utils
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions()
{
// TODO: use av capability to retrieve supported formats
// TODO: list demuxers to retrieve supported formats
static const std::array<std::filesystem::path, 18> fileExtensions{
".aac",
".alac",
@@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils
};
return fileExtensions;
}
} // namespace lms::metadata::avformat::utils
} // namespace lms::audio::ffmpeg::utils
@@ -22,7 +22,7 @@
#include <filesystem>
#include <span>
namespace lms::metadata::avformat::utils
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
} // namespace lms::metadata::avformat::utils
} // namespace lms::audio::ffmpeg::utils
@@ -0,0 +1,227 @@
/*
* 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 "AudioFileInfo.hpp"
#include <cassert>
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/trueaudiofile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#endif
#if LMS_TAGLIB_HAS_SHORTEN
#include <taglib/shortenfile.h>
#endif
#include "audio/IAudioFileInfo.hpp"
#include "Utils.hpp"
namespace lms::audio::taglib
{
namespace
{
AudioProperties computeAudioProperties(const ::TagLib::File& file)
{
assert(file.audioProperties());
const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
AudioProperties audioProperties;
// Common properties
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
// Guess container from the file type
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
{
audioProperties.container = ContainerType::APE;
audioProperties.codec = CodecType::APE; // TODO version?
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
}
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&file) })
{
audioProperties.container = ContainerType::ASF;
switch (asfFile->audioProperties()->codec())
{
case ::TagLib::ASF::Properties::Codec::WMA1:
audioProperties.codec = CodecType::WMA1;
break;
case ::TagLib::ASF::Properties::Codec::WMA2:
audioProperties.codec = CodecType::WMA2;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
audioProperties.codec = CodecType::WMA9Lossless;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
audioProperties.codec = CodecType::WMA9Pro;
break;
case ::TagLib::ASF::Properties::Codec::Unknown:
audioProperties.codec = std::nullopt;
break;
}
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_DSF
else if (const auto* dsfFile{ dynamic_cast<const ::TagLib::DSF::File*>(&file) })
{
audioProperties.container = ContainerType::DSF;
audioProperties.codec = CodecType::DSD;
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_DSF
else if (const auto* flacFile{ dynamic_cast<const ::TagLib::FLAC::File*>(&file) })
{
audioProperties.container = ContainerType::FLAC;
audioProperties.codec = CodecType::FLAC;
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
}
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
{
audioProperties.container = ContainerType::MP4;
switch (mp4File->audioProperties()->codec())
{
case ::TagLib::MP4::Properties::Codec::AAC:
audioProperties.codec = CodecType::AAC;
break;
case ::TagLib::MP4::Properties::Codec::ALAC:
audioProperties.codec = CodecType::ALAC;
break;
case ::TagLib::MP4::Properties::Codec::Unknown:
audioProperties.codec = std::nullopt;
break;
}
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
}
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
{
audioProperties.container = ContainerType::MPC;
switch (mpcFile->audioProperties()->mpcVersion())
{
case 7:
audioProperties.codec = CodecType::MPC7;
break;
case 8:
audioProperties.codec = CodecType::MPC8;
break;
}
}
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
{
const auto& properties{ *mpegFile->audioProperties() };
audioProperties.container = ContainerType::MPEG;
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
&& mpegFile->audioProperties()->layer() == 3)
audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
audioProperties.codec = CodecType::AAC;
}
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Opus;
}
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Vorbis;
}
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
{
audioProperties.container = ContainerType::AIFF;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
{
audioProperties.container = ContainerType::WAV;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_SHORTEN
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
{
audioProperties.container = ContainerType::Shorten;
audioProperties.codec = CodecType::Shorten;
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_SHORTEN
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
{
audioProperties.container = ContainerType::TrueAudio;
audioProperties.codec = CodecType::TrueAudio;
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
{
audioProperties.container = ContainerType::WavPack;
audioProperties.codec = CodecType::WavPack;
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
}
return audioProperties;
}
} // namespace
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
: _filePath{ filePath }
, _file{ utils::parseFile(filePath, readStyle) }
, _audioProperties{ computeAudioProperties(*_file) }
, _tagReader{ *_file, enableExtraDebugLogs }
, _imageReader{ *_file }
{
}
const AudioProperties& AudioFileInfo::getAudioProperties() const
{
return _audioProperties;
}
const IImageReader& AudioFileInfo::getImageReader() const
{
return _imageReader;
}
const ITagReader& AudioFileInfo::getTagReader() const
{
return _tagReader;
}
} // namespace lms::audio::taglib
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 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 <taglib/tfile.h>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "audio/ITagReader.hpp"
#include "ImageReader.hpp"
#include "TagReader.hpp"
namespace lms::audio::taglib
{
class AudioFileInfo final : public IAudioFileInfo
{
public:
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
private:
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;
const AudioProperties _audioProperties;
const TagReader _tagReader;
const ImageReader _imageReader;
};
} // namespace lms::audio::taglib
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagLibImageReader.hpp"
#include "ImageReader.hpp"
#include "TagLibDefs.hpp"
@@ -33,16 +33,14 @@
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/tfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#include "core/ILogger.hpp"
#include "metadata/Exception.hpp"
#include "core/String.hpp"
#include "taglib/Utils.hpp"
namespace lms::metadata::taglib
namespace lms::audio::taglib
{
namespace
{
@@ -50,47 +48,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
case TagLib::ID3v2::AttachedPictureFrame::Type::Other:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other;
case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::ID3v2::AttachedPictureFrame::Type::Media:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor;
case TagLib::ID3v2::AttachedPictureFrame::Type::Band:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band;
case TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer;
case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration;
case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -101,47 +99,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
case TagLib::ASF::Picture::Type::Other:
case ::TagLib::ASF::Picture::Type::Other:
return Image::Type::Other;
case TagLib::ASF::Picture::Type::FileIcon:
case ::TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::ASF::Picture::Type::OtherFileIcon:
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::ASF::Picture::Type::FrontCover:
case ::TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::ASF::Picture::Type::BackCover:
case ::TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover;
case TagLib::ASF::Picture::Type::LeafletPage:
case ::TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::ASF::Picture::Type::Media:
case ::TagLib::ASF::Picture::Type::Media:
return Image::Type::Media;
case TagLib::ASF::Picture::Type::LeadArtist:
case ::TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::ASF::Picture::Type::Artist:
case ::TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist;
case TagLib::ASF::Picture::Type::Conductor:
case ::TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor;
case TagLib::ASF::Picture::Type::Band:
case ::TagLib::ASF::Picture::Type::Band:
return Image::Type::Band;
case TagLib::ASF::Picture::Type::Composer:
case ::TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer;
case TagLib::ASF::Picture::Type::Lyricist:
case ::TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::ASF::Picture::Type::RecordingLocation:
case ::TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::ASF::Picture::Type::DuringRecording:
case ::TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::ASF::Picture::Type::DuringPerformance:
case ::TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::ASF::Picture::Type::MovieScreenCapture:
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::ASF::Picture::Type::ColouredFish:
case ::TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::ASF::Picture::Type::Illustration:
case ::TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration;
case TagLib::ASF::Picture::Type::BandLogo:
case ::TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::ASF::Picture::Type::PublisherLogo:
case ::TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -152,47 +150,47 @@ namespace lms::metadata::taglib
{
switch (type)
{
case TagLib::FLAC::Picture::Type::Other:
case ::TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other;
case TagLib::FLAC::Picture::Type::FileIcon:
case ::TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::FLAC::Picture::Type::OtherFileIcon:
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::FLAC::Picture::Type::FrontCover:
case ::TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::FLAC::Picture::Type::BackCover:
case ::TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover;
case TagLib::FLAC::Picture::Type::LeafletPage:
case ::TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::FLAC::Picture::Type::Media:
case ::TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media;
case TagLib::FLAC::Picture::Type::LeadArtist:
case ::TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::FLAC::Picture::Type::Artist:
case ::TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist;
case TagLib::FLAC::Picture::Type::Conductor:
case ::TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor;
case TagLib::FLAC::Picture::Type::Band:
case ::TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band;
case TagLib::FLAC::Picture::Type::Composer:
case ::TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer;
case TagLib::FLAC::Picture::Type::Lyricist:
case ::TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::FLAC::Picture::Type::RecordingLocation:
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::FLAC::Picture::Type::DuringRecording:
case ::TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::FLAC::Picture::Type::DuringPerformance:
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::FLAC::Picture::Type::MovieScreenCapture:
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::FLAC::Picture::Type::ColouredFish:
case ::TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::FLAC::Picture::Type::Illustration:
case ::TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration;
case TagLib::FLAC::Picture::Type::BandLogo:
case ::TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::FLAC::Picture::Type::PublisherLogo:
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
@@ -203,44 +201,44 @@ namespace lms::metadata::taglib
{
switch (format)
{
case TagLib::MP4::CoverArt::Format::BMP:
case ::TagLib::MP4::CoverArt::Format::BMP:
return "image/bmp";
case TagLib::MP4::CoverArt::Format::GIF:
case ::TagLib::MP4::CoverArt::Format::GIF:
return "image/gif";
case TagLib::MP4::CoverArt::Format::JPEG:
case ::TagLib::MP4::CoverArt::Format::JPEG:
return "image/jpeg";
case TagLib::MP4::CoverArt::Format::PNG:
case ::TagLib::MP4::CoverArt::Format::PNG:
return "image/png";
case TagLib::MP4::CoverArt::Format::Unknown:
case ::TagLib::MP4::CoverArt::Format::Unknown:
return "application/octet-stream";
}
return "application/octet-stream";
}
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover;
else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover;
return Image::Type::Unknown;
}
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor)
void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor)
{
const auto& frameListMap{ id3v2Tags.frameListMap() };
for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
{
const auto* attachedPictureFrame{ dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(frame) };
const auto* attachedPictureFrame{ dynamic_cast<const ::TagLib::ID3v2::AttachedPictureFrame*>(frame) };
if (!attachedPictureFrame)
continue;
TagLib::ByteVector picture{ attachedPictureFrame->picture() };
::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
@@ -252,15 +250,15 @@ namespace lms::metadata::taglib
}
}
void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor)
void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor)
{
for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
{
TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
::TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
if (!asfPicture.isValid())
continue;
TagLib::ByteVector picture{ asfPicture.picture() };
::TagLib::ByteVector picture{ asfPicture.picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
@@ -273,22 +271,22 @@ namespace lms::metadata::taglib
}
}
void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor)
void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor)
{
const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
if (!coverItem.isValid())
return;
#if TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList)
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
return;
#endif
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
bool firstCover{ true };
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
{
TagLib::ByteVector picture{ coverArt.data() };
::TagLib::ByteVector picture{ coverArt.data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
@@ -303,11 +301,11 @@ namespace lms::metadata::taglib
}
}
void visitFLACImages(const TagLib::List<TagLib::FLAC::Picture*> pictureList, TagLibImageReader::ImageVisitor visitor)
void visitFLACImages(const ::TagLib::List<TagLib::FLAC::Picture*>& pictureList, const ImageReader::ImageVisitor& visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
TagLib::ByteVector picture{ flacPicture->data() };
::TagLib::ByteVector picture{ flacPicture->data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
@@ -320,14 +318,14 @@ namespace lms::metadata::taglib
}
}
void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor)
void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor)
{
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const TagLib::VariantMap& pictureProperty : pictureProperties)
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const ::TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
{
Image image;
TagLib::ByteVector picture;
::TagLib::ByteVector picture;
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
@@ -344,81 +342,77 @@ namespace lms::metadata::taglib
if (!image.data.empty())
visitor(image);
}
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
}
} // namespace
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
: _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) }
ImageReader::ImageReader(::TagLib::File& file)
: _file{ file }
{
if (!_file)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw AudioFileParsingException{};
}
}
void TagLibImageReader::visitImages(ImageVisitor visitor) const
ImageReader::~ImageReader() = default;
void ImageReader::visitImages(const ImageVisitor& visitor) const
{
// MP3
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
else if (const TagLib::MP4::File * mp4File{ dynamic_cast<const TagLib::MP4::File*>(&_file) })
{
visitMP4Images(*mp4File, std::move(visitor));
visitMP4Images(*mp4File, visitor);
}
// WMA
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
else if (const TagLib::ASF::File * asfFile{ dynamic_cast<const TagLib::ASF::File*>(&_file) })
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, std::move(visitor));
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, visitor);
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
{
if (flacFile->hasID3v2Tag()) // usage discouraged
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
else
visitFLACImages(flacFile->pictureList(), std::move(visitor));
visitFLACImages(flacFile->pictureList(), visitor);
}
// Ogg vorbis
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.get()) })
else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<const TagLib::Ogg::Vorbis::File*>(&_file) })
{
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
}
// Ogg Opus
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.get()) })
else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(&_file) })
{
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
visitFLACImages(opusFile->tag()->pictureList(), visitor);
}
// Aiff
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
visitID3V2Images(*aiffFile->tag(), visitor);
}
// Wav
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(&_file) })
{
if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
visitAPEImages(*mpcFile->APETag(), visitor);
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
if (wavPackFile->hasAPETag())
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
visitAPEImages(*wavPackFile->APETag(), visitor);
}
}
} // namespace lms::metadata::taglib
} // namespace lms::audio::taglib
@@ -19,27 +19,26 @@
#pragma once
#include "IImageReader.hpp"
#include "audio/IImageReader.hpp"
namespace lms::av
namespace TagLib
{
class IAudioFile;
class File;
}
namespace lms::metadata::avformat
namespace lms::audio::taglib
{
class AvFormatImageReader : public IImageReader
class ImageReader : public IImageReader
{
public:
AvFormatImageReader(const std::filesystem::path& p);
~AvFormatImageReader() override;
AvFormatImageReader(const AvFormatImageReader&) = delete;
AvFormatImageReader& operator=(const AvFormatImageReader&) = delete;
ImageReader(TagLib::File& _file);
~ImageReader() override;
ImageReader(const ImageReader&) = delete;
ImageReader& operator=(const ImageReader&) = delete;
private:
void visitImages(ImageVisitor visitor) const override;
void visitImages(const ImageVisitor& visitor) const override;
std::unique_ptr<av::IAudioFile> _audioFile;
TagLib::File& _file;
};
} // namespace lms::metadata::avformat
} // namespace lms::audio::taglib
@@ -22,15 +22,20 @@
#include <taglib/taglib.h>
#if (TAGLIB_MAJOR_VERSION >= 2)
#define TAGLIB_HAS_DSF 1
#define LMS_TAGLIB_HAS_DSF 1
#endif
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
#define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1
#endif
// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
#define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
#define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
#endif
// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1))
#define LMS_TAGLIB_HAS_SHORTEN 1
#endif
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagLibTagReader.hpp"
#include "TagReader.hpp"
#include <unordered_map>
@@ -25,7 +25,6 @@
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/apeproperties.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
@@ -38,33 +37,25 @@
#include <taglib/speexfile.h>
#include <taglib/synchronizedlyricsframe.h>
#include <taglib/tag.h>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h>
#include <taglib/trueaudiofile.h>
#include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#include <taglib/dsffile.h>
#endif
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "Utils.hpp"
namespace lms::metadata::taglib
namespace lms::audio::taglib
{
namespace
{
class TagParsingFailedException : public Exception
{
public:
using Exception::Exception;
};
// Mapping to internal taglib names and/or common alternative custom names
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
@@ -181,7 +172,7 @@ namespace lms::metadata::taglib
{ TagType::Writer, { "WRITER" } },
};
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
{
for (auto&& [tag, values] : src)
{
@@ -190,49 +181,36 @@ namespace lms::metadata::taglib
}
}
void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file)
void dedupTagValues(TagLib::PropertyMap& propertyMap)
{
for (auto& [key, values] : propertyMap)
{
if (values.size() <= 1)
continue;
TagLib::StringList newList;
for (const TagLib::String& value : values)
::TagLib::StringList newList;
for (const ::TagLib::String& value : values)
{
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; }))
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; }))
newList.append(value);
}
if (values != newList)
{
LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
values = newList;
}
}
}
} // namespace
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
: _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) }
TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
: _file{ file }
{
if (!_file)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw AudioFileParsingException{ "Parsing failed" };
}
_propertyMap = _file.properties();
if (!_file->audioProperties())
{
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
throw AudioFileNoAudioPropertiesException{};
}
computeAudioProperties();
_propertyMap = _file->properties();
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
enableExtraDebugLogs &= core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG);
if (enableExtraDebugLogs)
{
for (const auto& [key, values] : _propertyMap)
{
@@ -245,7 +223,7 @@ namespace lms::metadata::taglib
}
// Some tags may not be known by TagLib
auto getAPETags = [&](const TagLib::APE::Tag* apeTag) {
auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) {
if (!apeTag)
return;
@@ -254,7 +232,7 @@ namespace lms::metadata::taglib
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
dedupTagValues(_propertyMap, p);
dedupTagValues(_propertyMap);
const auto& frameListMap{ id3v2Tags.frameListMap() };
@@ -264,26 +242,30 @@ namespace lms::metadata::taglib
// consider each frame hold a different set of lyrics
// Synchronized lyrics frames
for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
{
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
std::string lyrics;
for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
{
std::chrono::milliseconds timestamp{};
switch (lyricsFrame->timestampFormat())
{
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
timestamp = std::chrono::milliseconds{ synchedText.time };
break;
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 };
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
{
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
if (properties && properties->sampleRate())
timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
}
break;
case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
break;
}
@@ -298,9 +280,9 @@ namespace lms::metadata::taglib
}
// Unsynchronized lyrics frames
for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
{
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
@@ -310,9 +292,9 @@ namespace lms::metadata::taglib
};
// WMA
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
if (const ::TagLib::ASF::File * asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&_file) })
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
{
for (const auto& [name, attributeList] : tag->attributeListMap())
{
@@ -320,19 +302,19 @@ namespace lms::metadata::taglib
continue;
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
if (debug)
if (enableExtraDebugLogs)
{
for (const auto& attribute : attributeList)
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "<Non unicode>" }) << "'");
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "<Non unicode>" }) << "'");
}
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
continue;
TagLib::StringList strAttributes;
for (const TagLib::ASF::Attribute& attribute : attributeList)
::TagLib::StringList strAttributes;
for (const ::TagLib::ASF::Attribute& attribute : attributeList)
{
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
strAttributes.append(attribute.toString());
}
@@ -356,23 +338,23 @@ namespace lms::metadata::taglib
}
}
// MP3
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
processID3v2Tags(*mp3File->ID3v2Tag());
processID3v2Tags(*mp3File->ID3v2Tag(false));
getAPETags(mp3File->APETag());
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&_file) })
{
// Taglib does not expose rtng in properties
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
{
#if TAGLIB_HAS_MP4_ITEM_TYPE
if (rtngItem.type() == TagLib::MP4::Item::Type::Byte)
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
#endif
_propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) };
_propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
}
if (!_propertyMap.contains("ORIGINALDATE"))
@@ -386,7 +368,7 @@ namespace lms::metadata::taglib
auto itOrigDateTag{ tags.find(origDateString) };
if (itOrigDateTag != std::cend(tags))
{
const TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
if (!dates.isEmpty())
{
_propertyMap["ORIGINALDATE"] = dates.front();
@@ -397,68 +379,36 @@ namespace lms::metadata::taglib
}
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
else if (::TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
{
if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag());
}
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<const TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
processID3v2Tags(*aiffFile->tag());
}
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<const TagLib::RIFF::WAV::File*>(&_file) })
{
if (wavFile->hasID3v2Tag())
processID3v2Tags(*wavFile->ID3v2Tag());
}
}
TagLibTagReader::~TagLibTagReader() = default;
TagReader::~TagReader() = default;
void TagLibTagReader::computeAudioProperties()
{
const TagLib::AudioProperties* properties{ _file->audioProperties() };
// Common properties
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
_audioProperties.channelCount = static_cast<std::size_t>(_file->audioProperties()->channels());
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
if (const auto* apeProperties{ dynamic_cast<const TagLib::APE::Properties*>(properties) })
_audioProperties.bitsPerSample = apeProperties->bitsPerSample();
if (const auto* asfProperties{ dynamic_cast<const TagLib::ASF::Properties*>(properties) })
_audioProperties.bitsPerSample = asfProperties->bitsPerSample();
else if (const auto* flacProperties{ dynamic_cast<const TagLib::FLAC::Properties*>(properties) })
_audioProperties.bitsPerSample = flacProperties->bitsPerSample();
else if (const auto* mp4Properties{ dynamic_cast<const TagLib::MP4::Properties*>(properties) })
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
else if (const auto* wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
else if (const auto* aiffProperties{ dynamic_cast<const TagLib::RIFF::AIFF::Properties*>(properties) })
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
#if TAGLIB_HAS_DSF
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSDIFF::Properties*>(properties) })
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
#endif
}
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ tagLibTagMapping.find(tag) };
if (itTagNames == std::cend(tagLibTagMapping))
@@ -478,19 +428,19 @@ namespace lms::metadata::taglib
}
}
void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
{
TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 };
::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 };
auto itValues{ _propertyMap.find(key) };
if (itValues == std::cend(_propertyMap))
return;
for (const TagLib::String& value : itValues->second)
for (const ::TagLib::String& value : itValues->second)
visitor(value.to8Bit(true));
}
void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
@@ -505,7 +455,7 @@ namespace lms::metadata::taglib
assert(rolePos != std::string::npos);
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
for (const TagLib::String& value : values)
for (const ::TagLib::String& value : values)
{
const std::string name{ value.to8Bit(true) };
visitor(role, name);
@@ -514,7 +464,7 @@ namespace lms::metadata::taglib
}
}
void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
if (!_id3v2Lyrics.empty())
{
@@ -529,4 +479,4 @@ namespace lms::metadata::taglib
});
}
}
} // namespace lms::metadata::taglib
} // namespace lms::audio::taglib
@@ -19,38 +19,37 @@
#pragma once
#include <filesystem>
#include <map>
#include <span>
#include <string>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h>
#include "ITagReader.hpp"
#include "audio/ITagReader.hpp"
namespace lms::metadata::taglib
namespace TagLib
{
class TagLibTagReader : public ITagReader
class File;
}
namespace lms::audio::taglib
{
class TagReader : public ITagReader
{
public:
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
~TagLibTagReader() override;
TagLibTagReader(const TagLibTagReader&) = delete;
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
TagReader(::TagLib::File& file, bool enableExtraDebugLogs);
~TagReader() override;
TagReader(const TagReader&) = delete;
TagReader& operator=(const TagReader&) = delete;
private:
void computeAudioProperties();
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
std::unique_ptr<TagLib::File> _file;
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
::TagLib::File& _file;
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::metadata::taglib
} // namespace lms::audio::taglib
@@ -39,15 +39,17 @@
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#endif
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
namespace lms::metadata::taglib::utils
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions()
{
@@ -56,23 +58,23 @@ namespace lms::metadata::taglib::utils
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
".wav",
#if TAGLIB_HAS_DSF
".dsf", ".dff", ".dsdiff"
#if LMS_TAGLIB_HAS_DSF
".dsf"
#endif
};
return std::span<const std::filesystem::path>{ supportedExtensions };
}
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
{
switch (readStyle)
{
case ParserReadStyle::Fast:
case ParserOptions::AudioPropertiesReadStyle::Fast:
return TagLib::AudioProperties::ReadStyle::Fast;
case ParserReadStyle::Average:
case ParserOptions::AudioPropertiesReadStyle::Average:
return TagLib::AudioProperties::ReadStyle::Average;
case ParserReadStyle::Accurate:
case ParserOptions::AudioPropertiesReadStyle::Accurate:
return TagLib::AudioProperties::ReadStyle::Accurate;
}
@@ -100,8 +102,9 @@ namespace lms::metadata::taglib::utils
return TagLib::FileStream{ fd, true };
}
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (extension.empty())
@@ -149,11 +152,9 @@ namespace lms::metadata::taglib::utils
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "WAV")
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
else if (ext == "DSF")
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "DFF" || ext == "DSDIFF")
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
@@ -162,8 +163,9 @@ namespace lms::metadata::taglib::utils
return file;
}
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (TagLib::MPEG::File::isSupported(stream))
@@ -200,11 +202,9 @@ namespace lms::metadata::taglib::utils
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::RIFF::WAV::File::isSupported(stream))
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if TAGLIB_HAS_DSF
#if LMS_TAGLIB_HAS_DSF
else if (TagLib::DSF::File::isSupported(stream))
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::DSDIFF::File::isSupported(stream))
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
@@ -213,20 +213,33 @@ namespace lms::metadata::taglib::utils
return file;
}
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties)
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
TagLib::FileStream fileStream{ createFileStream(p) };
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) };
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) };
if (!file)
{
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value());
file = parseFileByContent(&fileStream, tagLibReadStyle);
if (!file)
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content");
}
if (!file)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw AudioFileParsingException{ p, "Parsing failed" };
}
if (!file->audioProperties())
{
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
throw AudioFileNoAudioPropertiesException{ p };
}
return file;
}
} // namespace lms::metadata::taglib::utils
} // namespace lms::audio::taglib::utils
@@ -19,19 +19,16 @@
#pragma once
#include "IImageReader.hpp"
#include <filesystem>
#include <memory>
#include <span>
#include <taglib/tfile.h>
namespace lms::metadata::taglib
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::taglib::utils
{
class TagLibImageReader : public IImageReader
{
public:
TagLibImageReader(const std::filesystem::path& p);
void visitImages(ImageVisitor visitor) const override;
std::unique_ptr<TagLib::File> _file;
};
} // namespace lms::metadata::taglib
std::span<const std::filesystem::path> getSupportedExtensions();
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
} // namespace lms::audio::taglib::utils
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <optional>
#include "core/LiteralString.hpp"
namespace lms::audio
{
enum class ContainerType
{
AIFF,
APE, // Monkey's Audio
ASF, // Advanced Systems Format
DSF,
FLAC,
MP4,
MPC, // Musepack
MPEG,
Ogg,
Shorten,
TrueAudio,
WAV,
WavPack,
};
core::LiteralString containerTypeToString(ContainerType type);
enum class CodecType
{
AAC,
AC3,
ALAC, // Apple Lossless Audio Codec (ALAC)
APE, // Monkey's Audio
EAC3, // Enhanced AC-3
DSD, // DSD
FLAC, // Flac
MP3,
MP4ALS, // MPEG-4 Audio Lossless Coding
MPC7, // Musepack
MPC8, // Musepack
Opus, // Opus
PCM,
Shorten, // Shorten (shn)
TrueAudio,
Vorbis,
WavPack, // WavPack
WMA1,
WMA2,
WMA9Pro,
WMA9Lossless,
};
core::LiteralString codecTypeToString(CodecType type);
struct AudioProperties
{
std::optional<ContainerType> container;
std::optional<CodecType> codec;
std::chrono::milliseconds duration{};
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
std::optional<unsigned> sampleRate;
};
} // namespace lms::audio
@@ -21,11 +21,11 @@
#include "core/Exception.hpp"
namespace lms::av
namespace lms::audio
{
class Exception : public core::LmsException
{
public:
using LmsException::LmsException;
};
} // namespace lms::av
} // namespace lms::audio
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <span>
#include "audio/Exception.hpp"
namespace lms::audio
{
class AudioProperties;
class IImageReader;
class ITagReader;
class IAudioFileInfo
{
public:
virtual ~IAudioFileInfo() = default;
virtual const AudioProperties& getAudioProperties() const = 0;
virtual const IImageReader& getImageReader() const = 0;
virtual const ITagReader& getTagReader() const = 0;
};
class AudioFileParsingException : public Exception
{
public:
AudioFileParsingException(const std::filesystem::path& path, std::string_view error = "")
: Exception{ error }, _path{ path } {}
const std::filesystem::path& getPath() const { return _path; }
private:
std::filesystem::path _path;
};
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
{
public:
using AudioFileParsingException::AudioFileParsingException;
};
class IOException : public Exception
{
public:
IOException(std::string_view message, std::error_code err)
: Exception{ std::string{ message } + ": " + err.message() }
, _err{ err }
{
}
std::error_code getErrorCode() const { return _err; }
private:
std::error_code _err;
};
struct ParserOptions
{
enum class Parser
{
TagLib,
FFmpeg,
};
enum class AudioPropertiesReadStyle
{
Fast,
Average,
Accurate,
};
Parser parser{ Parser::TagLib };
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
bool enableExtraDebugLogs{};
};
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
} // namespace lms::audio
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <span>
#include <string>
#include "core/LiteralString.hpp"
namespace lms::audio
{
struct Image
{
// See TagLib types (based on ID3v2 APIC types)
enum class Type
{
// No information
Unknown,
// A type not enumerated below
Other,
// 32x32 PNG image that should be used as the file icon
FileIcon,
// File icon of a different size or format
OtherFileIcon,
// Front cover image of the album
FrontCover,
// Back cover image of the album
BackCover,
// Inside leaflet page of the album
LeafletPage,
// Image from the album itself
Media,
// Picture of the lead artist or soloist
LeadArtist,
// Picture of the artist or performer
Artist,
// Picture of the conductor
Conductor,
// Picture of the band or orchestra
Band,
// Picture of the composer
Composer,
// Picture of the lyricist or text writer
Lyricist,
// Picture of the recording location or studio
RecordingLocation,
// Picture of the artists during recording
DuringRecording,
// Picture of the artists during performance
DuringPerformance,
// Picture from a movie or video related to the track
MovieScreenCapture,
// Picture of a large, coloured fish
ColouredFish,
// Illustration related to the track
Illustration,
// Logo of the band or performer
BandLogo,
// Logo of the publisher (record company)
PublisherLogo
};
Type type{ Type::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span<const std::byte> data;
};
core::LiteralString imageTypeToString(Image::Type type);
class IImageReader
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(const ImageVisitor& visitor) const = 0;
};
} // namespace lms::audio
@@ -20,10 +20,11 @@
#pragma once
#include <functional>
#include <string_view>
#include "metadata/Types.hpp"
#include "core/LiteralString.hpp"
namespace lms::metadata
namespace lms::audio
{
// prefer using picard internal names
// see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
@@ -152,8 +153,12 @@ namespace lms::metadata
Website,
WorkTitle,
Writer,
Count, // Special value used to count the number of tags
};
core::LiteralString tagTypeToString(TagType type);
class ITagReader
{
public:
@@ -168,7 +173,5 @@ namespace lms::metadata
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
};
} // namespace lms::metadata
} // namespace lms::audio
@@ -20,37 +20,14 @@
#pragma once
#include <cstddef>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
namespace lms::av
#include "TranscodeTypes.hpp"
namespace lms::audio
{
struct InputParameters
{
std::filesystem::path file; // Path to the input file
std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from
std::optional<std::size_t> streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set)
};
enum class OutputFormat
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
struct OutputParameters
{
OutputFormat format;
std::size_t bitrate{ 128'000 };
bool stripMetadata{ true };
};
class ITranscoder
{
public:
@@ -62,10 +39,10 @@ namespace lms::av
virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0;
virtual std::string_view getOutputMimeType() const = 0;
virtual const OutputParameters& getOutputParameters() const = 0;
virtual const TranscodeOutputParameters& getOutputParameters() const = 0;
virtual bool finished() const = 0;
};
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
} // namespace lms::av
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters);
} // namespace lms::audio
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
// #include "AudioTypes.hpp"
namespace lms::audio
{
// TODO deprecated
enum class OutputFormat
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
struct TranscodeInputParameters
{
std::filesystem::path filePath;
std::chrono::milliseconds duration{}; // Duration of the audio file
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
};
struct TranscodeOutputParameters
{
std::optional<OutputFormat> format;
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
std::optional<unsigned> sampleRate;
bool stripMetadata{ true };
};
struct TranscodeParameters
{
TranscodeInputParameters inputParameters;
TranscodeOutputParameters outputParameters;
};
} // namespace lms::audio
-26
View File
@@ -1,26 +0,0 @@
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
add_library(lmsav STATIC
impl/AudioFile.cpp
impl/Transcoder.cpp
)
target_include_directories(lmsav INTERFACE
include
)
target_include_directories(lmsav PRIVATE
include
${AVCODEC_INCLUDE_DIR}
${AVFORMAT_INCLUDE_DIR}
${AVUTIL_INCLUDE_DIR}
)
target_link_libraries(lmsav PUBLIC
lmscore
std::filesystem
)
target_link_libraries(lmsav PRIVATE
PkgConfig::LIBAV
)
-51
View File
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2020 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 "av/IAudioFile.hpp"
struct AVFormatContext;
namespace lms::av
{
class AudioFile final : public IAudioFile
{
public:
AudioFile(const std::filesystem::path& p);
~AudioFile() override;
AudioFile(const AudioFile&) = delete;
AudioFile& operator=(const AudioFile&) = delete;
const std::filesystem::path& getPath() const override;
ContainerInfo getContainerInfo() const override;
MetadataMap getMetaData() const override;
std::vector<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const override;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context{};
};
} // namespace lms::av
-104
View File
@@ -1,104 +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/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace lms::av
{
// List should be sync with the codecs shipped in the lms's docker version
enum class DecodingCodec
{
UNKNOWN,
MP3,
AAC,
AC3,
VORBIS,
WMAV1,
WMAV2,
FLAC, // Flac
ALAC, // Apple Lossless Audio Codec (ALAC)
WAVPACK, // WavPack
MUSEPACK7, // Musepack
MUSEPACK8,
APE, // Monkey's Audio
EAC3, // Enhanced AC-3
MP4ALS, // MPEG-4 Audio Lossless Coding
OPUS, // Opus
SHORTEN, // Shorten (shn)
DSD_LSBF, // DSD (Direct Stream Digital), least significant bit first
DSD_LSBF_PLANAR, // DSD (Direct Stream Digital), least significant bit first, planar
DSD_MSBF, // DSD (Direct Stream Digital), most significant bit first
DSD_MSBF_PLANAR, // DSD (Direct Stream Digital), most significant bit first, planar
// TODO add PCM codecs
};
struct Picture
{
std::string mimeType;
std::span<const std::byte> data; // valid as long as IAudioFile exists
};
struct ContainerInfo
{
std::size_t bitrate{};
std::string name;
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
std::size_t bitrate{};
std::size_t bitsPerSample{};
std::size_t channelCount{};
std::size_t sampleRate{};
DecodingCodec codec;
std::string codecName;
};
class IAudioFile
{
public:
virtual ~IAudioFile() = default;
// Keys are forced to be in upper case
using MetadataMap = std::unordered_map<std::string, std::string>;
virtual const std::filesystem::path& getPath() const = 0;
virtual ContainerInfo getContainerInfo() const = 0;
virtual MetadataMap getMetaData() const = 0;
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap& metadata)> func) const = 0;
};
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
} // namespace lms::av
+2 -2
View File
@@ -37,10 +37,10 @@ namespace lms::core::logging
{
case Module::API_SUBSONIC:
return "API_SUBSONIC";
case Module::AUDIO:
return "AUDIO";
case Module::AUTH:
return "AUTH";
case Module::AV:
return "AV";
case Module::CHILDPROCESS:
return "CHILDPROC";
case Module::COVER:
+1 -1
View File
@@ -40,8 +40,8 @@ namespace lms::core::logging
enum class Module
{
API_SUBSONIC,
AUDIO,
AUTH,
AV,
CHILDPROCESS,
COVER,
DB,
-43
View File
@@ -1,43 +0,0 @@
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
if (BUILD_BENCHMARKS)
add_subdirectory(bench)
endif()
add_library(lmsmetadata STATIC
impl/ArtistInfo.cpp
impl/AudioFileParser.cpp
impl/avformat/AvFormatImageReader.cpp
impl/avformat/AvFormatTagReader.cpp
impl/avformat/Utils.cpp
impl/Lyrics.cpp
impl/PlayList.cpp
impl/taglib/TagLibImageReader.cpp
impl/taglib/TagLibTagReader.cpp
impl/taglib/Utils.cpp
impl/Utils.cpp
)
target_include_directories(lmsmetadata INTERFACE
include
)
target_include_directories(lmsmetadata PRIVATE
include
impl
)
target_link_libraries(lmsmetadata PRIVATE
lmsav
PkgConfig::Taglib
pugixml::pugixml
)
target_link_libraries(lmsmetadata PUBLIC
lmscore
std::filesystem
)
-15
View File
@@ -1,15 +0,0 @@
add_executable(bench-metadata
LyricsBench.cpp
Metadata.cpp
)
target_include_directories(bench-metadata PRIVATE
../impl
../test
)
target_link_libraries(bench-metadata PRIVATE
lmsmetadata
benchmark
)
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include "metadata/IAudioFileParser.hpp"
namespace lms::metadata
{
class IImageReader;
class ITagReader;
class AudioFileParser : public IAudioFileParser
{
public:
AudioFileParser(const AudioFileParserParameters& params = {});
~AudioFileParser() override = default;
AudioFileParser(const AudioFileParser&) = delete;
AudioFileParser& operator=(const AudioFileParser&) = delete;
protected:
std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) const override;
std::unique_ptr<Track> parseMetaData(const ITagReader& reader) const;
static void parseImages(const IImageReader& reader, ImageVisitor visitor);
private:
void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
void processTags(const ITagReader& reader, Track& track) const;
std::optional<Medium> getMedium(const ITagReader& tagReader) const;
std::optional<Release> getRelease(const ITagReader& tagReader) const;
const AudioFileParserParameters _params;
};
} // namespace lms::metadata
-39
View File
@@ -1,39 +0,0 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <memory>
#include <span>
#include <taglib/audioproperties.h>
#include <taglib/tfile.h>
#include "core/TaggedType.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle);
using ReadAudioProperties = core::TaggedBool<struct ReadAudioPropertiesTag>;
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties);
} // namespace lms::metadata::taglib::utils
@@ -1,61 +0,0 @@
/*
* Copyright (C) 2024 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 <system_error>
#include "core/Exception.hpp"
namespace lms::metadata
{
class Exception : public core::LmsException
{
public:
using LmsException::LmsException;
};
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;
};
class AudioFileParsingException : public Exception
{
public:
using Exception::Exception;
};
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
{
public:
using AudioFileParsingException::AudioFileParsingException;
};
} // namespace lms::metadata
@@ -1,46 +0,0 @@
/*
* Copyright (C) 2018 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 <functional>
#include <memory>
#include <span>
#include "metadata/Exception.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata
{
class IAudioFileParser
{
public:
virtual ~IAudioFileParser() = default;
virtual std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) const = 0;
using ImageVisitor = std::function<void(const Image&)>;
virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const = 0;
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
};
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params);
} // namespace lms::metadata
-933
View File
@@ -1,933 +0,0 @@
/*
* Copyright (C) 2024 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 <vector>
#include <gtest/gtest.h>
#include <Wt/WTime.h>
#include "AudioFileParser.hpp"
#include "TestTagReader.hpp"
namespace lms::metadata::tests
{
class TestAudioFileParser : public AudioFileParser
{
public:
using AudioFileParser::AudioFileParser;
using AudioFileParser::parseMetaData;
};
TEST(AudioFileParser, generalTest)
{
AudioFileParserParameters params;
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
TestAudioFileParser parser{ params };
std::unique_ptr<ITagReader> testTags{ createDefaultPopulatedTestTagReader() };
const std::unique_ptr<Track> track{ parser.parseMetaData(*testTags) };
// Audio properties
{
const AudioProperties& audioProperties{ testTags->getAudioProperties() };
EXPECT_EQ(track->audioProperties.bitrate, audioProperties.bitrate);
EXPECT_EQ(track->audioProperties.bitsPerSample, audioProperties.bitsPerSample);
EXPECT_EQ(track->audioProperties.channelCount, audioProperties.channelCount);
EXPECT_EQ(track->audioProperties.duration, audioProperties.duration);
EXPECT_EQ(track->audioProperties.sampleRate, audioProperties.sampleRate);
}
EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
ASSERT_TRUE(track->advisory.has_value());
EXPECT_EQ(track->advisory.value(), Track::Advisory::Clean);
EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2");
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "MyArtist1");
EXPECT_EQ(track->artists[0].sortName, "MyArtists1SortName");
EXPECT_EQ(track->artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae"));
EXPECT_EQ(track->artists[1].name, "MyArtist2");
EXPECT_EQ(track->artists[1].sortName, "MyArtists2SortName");
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
ASSERT_EQ(track->comments.size(), 2);
EXPECT_EQ(track->comments[0], "Comment1");
EXPECT_EQ(track->comments[1], "Comment2");
ASSERT_EQ(track->composerArtists.size(), 2);
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
EXPECT_EQ(track->composerArtists[1].name, "MyComposer2");
EXPECT_EQ(track->composerArtists[1].sortName, "MyComposerSortOrder2");
ASSERT_EQ(track->conductorArtists.size(), 2);
EXPECT_EQ(track->conductorArtists[0].name, "MyConductor1");
EXPECT_EQ(track->conductorArtists[1].name, "MyConductor2");
EXPECT_EQ(track->copyright, "MyCopyright");
EXPECT_EQ(track->copyrightURL, "MyCopyrightURL");
ASSERT_TRUE(track->date.isValid());
EXPECT_EQ(track->date.getYear(), 2020);
EXPECT_EQ(track->date.getMonth(), 3);
EXPECT_EQ(track->date.getDay(), 4);
ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
ASSERT_EQ(track->groupings.size(), 2);
EXPECT_EQ(track->groupings[0], "Grouping1");
EXPECT_EQ(track->groupings[1], "Grouping2");
ASSERT_EQ(track->languages.size(), 2);
EXPECT_EQ(track->languages[0], "Language1");
EXPECT_EQ(track->languages[1], "Language2");
ASSERT_EQ(track->lyricistArtists.size(), 2);
EXPECT_EQ(track->lyricistArtists[0].name, "MyLyricist1");
EXPECT_EQ(track->lyricistArtists[1].name, "MyLyricist2");
ASSERT_EQ(track->lyrics.size(), 1);
EXPECT_EQ(track->lyrics.front().language, "eng");
ASSERT_EQ(track->lyrics.front().synchronizedLines.size(), 2);
ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 }));
EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line");
ASSERT_TRUE(track->lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 }));
EXPECT_EQ(track->lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line");
ASSERT_TRUE(track->mbid.has_value());
EXPECT_EQ(track->mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a"));
ASSERT_EQ(track->mixerArtists.size(), 2);
EXPECT_EQ(track->mixerArtists[0].name, "MyMixer1");
EXPECT_EQ(track->mixerArtists[1].name, "MyMixer2");
ASSERT_EQ(track->moods.size(), 2);
EXPECT_EQ(track->moods[0], "Mood1");
EXPECT_EQ(track->moods[1], "Mood2");
ASSERT_TRUE(track->originalDate.isValid());
EXPECT_EQ(track->originalDate.getYear(), 2019);
EXPECT_EQ(track->originalDate.getMonth(), 2);
EXPECT_EQ(track->originalDate.getDay(), 3);
ASSERT_TRUE(track->originalYear.has_value());
EXPECT_EQ(track->originalYear.value(), 2019);
ASSERT_TRUE(track->performerArtists.contains("Rolea"));
ASSERT_EQ(track->performerArtists["Rolea"].size(), 2);
EXPECT_EQ(track->performerArtists["Rolea"][0].name, "MyPerformer1ForRoleA");
EXPECT_EQ(track->performerArtists["Rolea"][1].name, "MyPerformer2ForRoleA");
ASSERT_EQ(track->performerArtists["Roleb"].size(), 2);
EXPECT_EQ(track->performerArtists["Roleb"][0].name, "MyPerformer1ForRoleB");
EXPECT_EQ(track->performerArtists["Roleb"][1].name, "MyPerformer2ForRoleB");
ASSERT_TRUE(track->position.has_value());
EXPECT_EQ(track->position.value(), 7);
ASSERT_EQ(track->producerArtists.size(), 2);
EXPECT_EQ(track->producerArtists[0].name, "MyProducer1");
EXPECT_EQ(track->producerArtists[1].name, "MyProducer2");
ASSERT_TRUE(track->recordingMBID.has_value());
EXPECT_EQ(track->recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
ASSERT_TRUE(track->replayGain.has_value());
EXPECT_FLOAT_EQ(track->replayGain.value(), -0.33);
ASSERT_EQ(track->remixerArtists.size(), 2);
EXPECT_EQ(track->remixerArtists[0].name, "MyRemixer1");
EXPECT_EQ(track->remixerArtists[1].name, "MyRemixer2");
EXPECT_EQ(track->title, "MyTitle");
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"].size(), 2);
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][0], "MyTagValue1ForTagA");
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][1], "MyTagValue2ForTagA");
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2);
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB");
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB");
// Medium
ASSERT_TRUE(track->medium.has_value());
EXPECT_EQ(track->medium->media, "CD");
EXPECT_EQ(track->medium->name, "MySubtitle");
ASSERT_TRUE(track->medium->position.has_value());
EXPECT_EQ(track->medium->position.value(), 2);
ASSERT_TRUE(track->medium->replayGain.has_value());
EXPECT_FLOAT_EQ(track->medium->replayGain.value(), -0.5);
ASSERT_TRUE(track->medium->trackCount.has_value());
EXPECT_EQ(track->medium->trackCount.value(), 12);
// Release
ASSERT_TRUE(track->medium->release.has_value());
const Release& release{ track->medium->release.value() };
EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
ASSERT_EQ(release.artists.size(), 2);
EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1");
EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName");
EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName");
EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
EXPECT_TRUE(release.isCompilation);
EXPECT_EQ(release.barcode, "MyBarcode");
ASSERT_EQ(release.labels.size(), 2);
EXPECT_EQ(release.labels[0], "Label1");
EXPECT_EQ(release.labels[1], "Label2");
ASSERT_TRUE(release.mbid.has_value());
EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
EXPECT_EQ(release.mediumCount, 3);
EXPECT_EQ(release.name, "MyAlbum");
EXPECT_EQ(release.sortName, "MyAlbumSortName");
EXPECT_EQ(release.comment, "MyAlbumComment");
ASSERT_EQ(release.countries.size(), 2);
EXPECT_EQ(release.countries[0], "MyCountry1");
EXPECT_EQ(release.countries[1], "MyCountry2");
{
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
}
}
TEST(AudioFileParser, trim)
{
const TestTagReader testTags{
{
{ TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->genres.size(), 3);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
EXPECT_EQ(track->genres[2], "Genre3");
}
TEST(AudioFileParser, customDelimiters)
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
{ TagType::Genre, { "Genre1 ; Genre2" } },
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
}
};
AudioFileParserParameters params;
params.defaultTagDelimiters = { " ; ", "/" };
params.artistTagDelimiters = { " / ", " feat. " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 3);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[2].name, "Artist3");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit
ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
ASSERT_EQ(track->languages.size(), 3);
EXPECT_EQ(track->languages[0], "Lang1");
EXPECT_EQ(track->languages[1], "Lang2");
EXPECT_EQ(track->languages[2], "Lang3");
// Medium
ASSERT_TRUE(track->medium.has_value());
// Release
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->name, "MyAlbum");
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "AlbumArtist1");
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
}
TEST(AudioFileParser, customArtistDelimiters_whitelist)
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { " AC/DC " } },
{ TagType::Artist, { "AC/DC " } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "AC/DC");
EXPECT_EQ(track->artistDisplayName, "AC/DC");
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->name, "MyAlbum");
ASSERT_EQ(track->medium->release->artists.size(), 1);
EXPECT_EQ(track->medium->release->artists[0].name, "AC/DC");
EXPECT_EQ(track->medium->release->artistDisplayName, "AC/DC");
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_artists)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "AC/DC and MyArtist" } },
{ TagType::Artists, { "AC/DC", "MyArtist" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { " AC/DC " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "AC/DC");
EXPECT_EQ(track->artists[1].name, "MyArtist");
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_first)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "AC/DC;MyArtist" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/", ";" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "AC/DC");
EXPECT_EQ(track->artists[1].name, "MyArtist");
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_middle)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/", ";" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 3);
EXPECT_EQ(track->artists[0].name, "MyArtist1");
EXPECT_EQ(track->artists[1].name, "AC/DC");
EXPECT_EQ(track->artists[2].name, "MyArtist2");
EXPECT_EQ(track->artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi_separators_last)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " AC/DC; MyArtist" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { ";", "/" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "AC/DC");
EXPECT_EQ(track->artists[1].name, "MyArtist");
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_longest_first)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " AC/DC; MyArtist" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { ";", "/" };
params.artistsToNotSplit = { "AC", "DC", "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "AC/DC");
EXPECT_EQ(track->artists[1].name, "MyArtist");
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_begin)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " AC/DC; MyArtist" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "AC/DC; MyArtist");
EXPECT_EQ(track->artistDisplayName, "AC/DC; MyArtist");
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_middle)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "MyArtist1; AC/DC ; MyArtist2");
EXPECT_EQ(track->artistDisplayName, "MyArtist1; AC/DC ; MyArtist2");
}
TEST(AudioFileParser, customArtistDelimiters_whitelist_partial_end)
{
const TestTagReader testTags{
{
{ TagType::Artist, { " MyArtist; AC/DC " } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "MyArtist; AC/DC");
EXPECT_EQ(track->artistDisplayName, "MyArtist; AC/DC");
}
TEST(AudioFileParser, customDelimiters_foundInArtist)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1; Artist2" } },
{ TagType::Artists, { "Artist1", "Artist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist
}
TEST(AudioFileParser, customDelimiters_foundInArtists)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 feat. Artist2" } },
{ TagType::Artists, { "Artist1; Artist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2");
}
TEST(AudioFileParser, customDelimiters_notUsed)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 & Artist2" } },
{ TagType::Artists, { "Artist1", "Artist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
}
TEST(AudioFileParser, customDelimiters_onlyInArtist)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 & Artist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(AudioFileParser, customDelimitersUsedForArtists)
{
const TestTagReader testTags{
{
{ TagType::Artists, { "Artist1 & Artist2" } },
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(AudioFileParser, noArtistInArtist)
{
const TestTagReader testTags{
{
// nothing in Artist!
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 0);
EXPECT_EQ(track->artistDisplayName, "");
}
TEST(AudioFileParser, singleArtistInArtists)
{
const TestTagReader testTags{
{
// nothing in Artist!
{ TagType::Artists, { "Artist1" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artistDisplayName, "Artist1");
}
TEST(AudioFileParser, multipleArtistsInArtist)
{
const TestTagReader testTags{
{
// nothing in Artists!
{ TagType::Artist, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(AudioFileParser, multipleArtistsInArtists)
{
const TestTagReader testTags{
{
// nothing in Artist!
{ TagType::Artists, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(AudioFileParser, multipleArtistsInArtistsWithEndDelimiter)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 & (CV. Artist2)" } },
{ TagType::Artists, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
}
TEST(AudioFileParser, singleArtistInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist1" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 1);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
}
TEST(AudioFileParser, multipleArtistsInAlbumArtist)
{
const TestTagReader testTags{
{
// nothing in AlbumArtists!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(AudioFileParser, multipleArtistsInAlbumArtists_displayName)
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "Artist1 & Artist2" } },
{ TagType::AlbumArtists, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2");
}
TEST(AudioFileParser, multipleArtistsInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 & Artist2" } },
{ TagType::Artists, { "Artist1", "Artist2" } },
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
}
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 / Artist2" } },
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
AudioFileParserParameters params;
params.artistTagDelimiters = { " / " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
}
TEST(AudioFileParser, release_sortNameFallback)
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
// No AlbumSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
}
TEST(AudioFileParser, artist_sortNameFallback)
{
{
const TestTagReader testTags{
{
{ TagType::Artist, { "MyArtist" } },
{ TagType::ArtistSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ TagType::Artist, { "MyArtist" } },
{ TagType::ArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ TagType::Artist, { "MyArtist" } },
{ TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } },
{ TagType::ArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].sortName, "MyArtistSortName");
}
}
TEST(AudioFileParser, albumartist_sortNameFallback)
{
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "MyArtist" } },
{ TagType::AlbumArtistSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
const auto& artists{ track->medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "MyArtist" } },
{ TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
const auto& artists{ track->medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "MyArtist" } },
{ TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } },
{ TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
const auto& artists{ track->medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
}
TEST(AudioFileParser, advisory)
{
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
const TestTagReader testTags{
{
{ TagType::Advisory, { value } },
}
};
AudioFileParser parser;
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
if (track->advisory.has_value())
{
EXPECT_EQ(track->advisory.value(), expectedValue);
}
};
doTest("0", Track::Advisory::Unknown);
doTest("1", Track::Advisory::Explicit);
doTest("4", Track::Advisory::Explicit);
doTest("2", Track::Advisory::Clean);
doTest("", std::nullopt);
doTest("3", std::nullopt);
}
TEST(AudioFileParser, encodingTime)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
{
{ TagType::EncodingTime, { value } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'";
};
doTest("", core::PartialDateTime{});
doTest("foo", core::PartialDateTime{});
doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 });
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
}
TEST(AudioFileParser, date)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
{
{ TagType::Date, { value } },
}
};
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
};
doTest("", core::PartialDateTime{});
doTest("foo", core::PartialDateTime{});
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020-01", core::PartialDateTime{ 2020, 1 });
doTest("2020", core::PartialDateTime{ 2020 });
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020/01", core::PartialDateTime{ 2020, 1 });
doTest("2020", core::PartialDateTime{ 2020 });
}
} // namespace lms::metadata::tests
-24
View File
@@ -1,24 +0,0 @@
include(GoogleTest)
add_executable(test-metadata
ArtistInfo.cpp
Lyrics.cpp
Metadata.cpp
AudioFileParser.cpp
PlayList.cpp
Utils.cpp
)
target_include_directories(test-metadata PRIVATE
../impl
)
target_link_libraries(test-metadata PRIVATE
lmsmetadata
GTest::GTest
)
if (NOT CMAKE_CROSSCOMPILING)
gtest_discover_tests(test-metadata)
endif()
+1 -1
View File
@@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE
)
target_link_libraries(lmsartwork PRIVATE
lmsaudio
lmsimage
lmsmetadata
)
target_link_libraries(lmsartwork PUBLIC
@@ -23,6 +23,9 @@
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
@@ -37,7 +40,6 @@
#include "image/Exception.hpp"
#include "image/IEncodedImage.hpp"
#include "image/Image.hpp"
#include "metadata/IAudioFileParser.hpp"
namespace lms::artwork
{
@@ -50,7 +52,6 @@ namespace lms::artwork
const std::filesystem::path& defaultReleaseCoverSvgPath,
const std::filesystem::path& defaultArtistImageSvgPath)
: _db{ db }
, _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) }
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
{
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
@@ -107,7 +108,11 @@ namespace lms::artwork
{
std::size_t currentIndex{};
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
audio::ParserOptions options;
options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images
auto audioFile{ audio::parseAudioFile(p) };
audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) {
if (currentIndex++ != index)
return;
@@ -130,7 +135,7 @@ namespace lms::artwork
}
});
}
catch (const metadata::Exception& e)
catch (const audio::Exception& e)
{
LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
}
@@ -33,11 +33,6 @@ namespace lms::db
class Session;
}
namespace lms::metadata
{
class IAudioFileParser;
}
namespace lms::artwork
{
class ArtworkService : public IArtworkService
@@ -67,7 +62,6 @@ namespace lms::artwork
db::IDb& _db;
std::unique_ptr<metadata::IAudioFileParser> _audioFileParser;
ImageCache _cache;
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
+21 -7
View File
@@ -1,12 +1,17 @@
add_library(lmsscanner STATIC
impl/helpers/ArtistHelpers.cpp
impl/scanners/ArtistInfoFileScanner.cpp
impl/scanners/AudioFileScanOperation.cpp
impl/scanners/artistinfo/ArtistInfoParser.cpp
impl/scanners/artistinfo/ArtistInfoFileScanner.cpp
impl/scanners/audiofile/AudioFileScanOperation.cpp
impl/scanners/audiofile/AudioFileScanner.cpp
impl/scanners/audiofile/TrackMetadataParser.cpp
impl/scanners/audiofile/Utils.cpp
impl/scanners/lyrics/LyricsFileScanner.cpp
impl/scanners/lyrics/LyricsParser.cpp
impl/scanners/playlist/PlayListFileScanner.cpp
impl/scanners/playlist/PlayListParser.cpp
impl/scanners/FileScanOperationBase.cpp
impl/scanners/AudioFileScanner.cpp
impl/scanners/ImageFileScanner.cpp
impl/scanners/LyricsFileScanner.cpp
impl/scanners/PlayListFileScanner.cpp
impl/scanners/Utils.cpp
impl/steps/JobQueue.cpp
impl/steps/ScanErrorLogger.cpp
@@ -43,13 +48,22 @@ target_include_directories(lmsscanner PRIVATE
target_link_libraries(lmsscanner PRIVATE
lmscore
lmsdatabase
lmsaudio
lmsimage
lmsmetadata
lmsrecommendation
pugixml::pugixml
)
target_link_libraries(lmsscanner PUBLIC
lmsdatabase
std::filesystem
Wt::Wt
)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
if (BUILD_BENCHMARKS)
add_subdirectory(bench)
endif()
@@ -0,0 +1,17 @@
add_executable(bench-scanner
Lyrics.cpp
Scanner.cpp
TrackMetadataParser.cpp
)
target_include_directories(bench-scanner PRIVATE
../impl
../test
)
target_link_libraries(bench-scanner PRIVATE
lmsscanner
lmsaudio
benchmark
)
@@ -22,9 +22,9 @@
#include <benchmark/benchmark.h>
#include "metadata/Lyrics.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
namespace lms::metadata::benchmarks
namespace lms::scanner::benchmarks
{
static void BM_Lyrics(benchmark::State& state)
{
@@ -89,4 +89,4 @@ namespace lms::metadata::benchmarks
BENCHMARK(BM_Lyrics);
} // namespace lms::metadata::benchmarks
} // namespace lms::scanner::benchmarks
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <benchmark/benchmark.h>
BENCHMARK_MAIN();
@@ -19,30 +19,22 @@
#include <benchmark/benchmark.h>
#include "AudioFileParser.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
#include "TestTagReader.hpp"
#include "core/String.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata::benchmarks
namespace lms::scanner::benchmarks
{
class TestAudioFileParser : public AudioFileParser
{
public:
using AudioFileParser::AudioFileParser;
using AudioFileParser::parseMetaData;
};
static void BM_Metadata_parse(benchmark::State& state)
{
AudioFileParserParameters params;
TrackMetadataParser::Parameters params;
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
std::unique_ptr<ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
const TestAudioFileParser parser{ params };
std::unique_ptr<audio::ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
const TrackMetadataParser parser{ params };
for (auto _ : state)
{
std::unique_ptr<Track> track{ parser.parseMetaData(*testTags) };
benchmark::DoNotOptimize(parser.parseTrackMetaData(*testTags));
}
}
@@ -50,16 +42,15 @@ namespace lms::metadata::benchmarks
{
const tests::TestTagReader testTags{
{
{ TagType::Artist, { "AC/DC; MyArtist" } },
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
}
};
const AudioFileParserParameters params;
const TestAudioFileParser parser{ params };
const TrackMetadataParser parser;
for (auto _ : state)
{
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
}
}
@@ -67,11 +58,11 @@ namespace lms::metadata::benchmarks
{
const tests::TestTagReader testTags{
{
{ TagType::Artist, { "AC/DC; MyArtist" } },
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
}
};
AudioFileParserParameters params;
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" };
// The list itself is not important, the idea is to have some volume
params.artistsToNotSplit = { "AC/DC",
@@ -113,10 +104,10 @@ namespace lms::metadata::benchmarks
"White/Light",
"Yamantaka // Sonic Titan" };
const TestAudioFileParser parser{ params };
const TrackMetadataParser parser{ params };
for (auto _ : state)
{
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
}
}
@@ -124,18 +115,18 @@ namespace lms::metadata::benchmarks
{
const tests::TestTagReader testTags{
{
{ TagType::Artist, { "AC/DC; MyArtist" } },
{ audio::TagType::Artist, { "AC/DC; MyArtist" } },
}
};
AudioFileParserParameters params;
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" };
const TestAudioFileParser parser{ params };
const TrackMetadataParser parser{ params };
for (auto _ : state)
{
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags));
}
}
@@ -144,6 +135,4 @@ namespace lms::metadata::benchmarks
BENCHMARK(BM_Metadata_parseArtists_WithWhitelist);
BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist);
} // namespace lms::metadata::benchmarks
BENCHMARK_MAIN();
} // namespace lms::scanner::benchmarks
@@ -27,15 +27,16 @@
#include "core/IJobScheduler.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/ScanSettings.hpp"
#include "scanners/ArtistInfoFileScanner.hpp"
#include "scanners/AudioFileScanner.hpp"
#include "scanners/ImageFileScanner.hpp"
#include "scanners/LyricsFileScanner.hpp"
#include "scanners/PlayListFileScanner.hpp"
#include "scanners/artistinfo/ArtistInfoFileScanner.hpp"
#include "scanners/audiofile/AudioFileScanner.hpp"
#include "scanners/lyrics/LyricsFileScanner.hpp"
#include "scanners/playlist/PlayListFileScanner.hpp"
#include "steps/ScanStepArtistReconciliation.hpp"
#include "steps/ScanStepAssociateArtistImages.hpp"
@@ -21,13 +21,14 @@
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "metadata/Types.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner::helpers
{
namespace
{
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
db::Artist::pointer createArtist(db::Session& session, const Artist& artistInfo)
{
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
@@ -44,7 +45,7 @@ namespace lms::scanner::helpers
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
}
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
void updateArtistIfNeeded(db::Artist::pointer artist, const Artist& artistInfo)
{
// MBID may be set
if (artist->getMBID() != artistInfo.mbid)
@@ -68,7 +69,7 @@ namespace lms::scanner::helpers
} // namespace
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
{
assert(artistInfo.mbid.has_value());
db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) };
@@ -99,7 +100,7 @@ namespace lms::scanner::helpers
return artist;
}
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
{
db::Artist::pointer artist;
@@ -139,7 +140,7 @@ namespace lms::scanner::helpers
return artist;
}
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
{
// First try to get by MBID
if (artistInfo.mbid)
@@ -20,9 +20,10 @@
#pragma once
#include "core/TaggedType.hpp"
#include "database/objects/Artist.hpp"
namespace lms::metadata
namespace lms::scanner
{
struct Artist;
}
@@ -31,8 +32,8 @@ namespace lms::scanner::helpers
{
using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>;
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
} // namespace lms::scanner::helpers
@@ -24,19 +24,22 @@
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "metadata/ArtistInfo.hpp"
#include "metadata/Types.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "FileScanOperationBase.hpp"
#include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "helpers/ArtistHelpers.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/artistinfo/ArtistInfoParser.hpp"
#include "types/ArtistInfo.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner
{
@@ -57,7 +60,7 @@ namespace lms::scanner
std::string getArtistNameFromArtistInfoFilePath();
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
std::optional<ArtistInfo> _parsedArtistInfo;
};
void ArtistInfoFileScanOperation::scan()
@@ -72,14 +75,14 @@ namespace lms::scanner
return;
}
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
_parsedArtistInfo = parseArtistInfo(ifs);
if (_parsedArtistInfo->name.empty())
{
_parsedArtistInfo->name = getFilePath().parent_path().filename();
LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'");
}
}
catch (const metadata::ArtistInfoParseException& e)
catch (const ArtistInfoParseException& e)
{
addError<ArtistInfoFileScanError>(getFilePath());
}
@@ -121,7 +124,7 @@ namespace lms::scanner
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary));
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) };
artistInfo.modify()->setArtist(artist);
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID());
@@ -150,7 +153,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
{
return metadata::getSupportedArtistInfoFiles();
return getSupportedArtistInfoFiles();
}
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/ArtistInfo.hpp"
#include "ArtistInfoParser.hpp"
#include <pugixml.hpp>
@@ -25,7 +25,7 @@
#include "core/LiteralString.hpp"
#include "core/String.hpp"
namespace lms::metadata
namespace lms::scanner
{
namespace
{
@@ -77,4 +77,4 @@ namespace lms::metadata
return artistInfo;
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <iosfwd>
#include <span>
#include "core/Exception.hpp"
#include "types/ArtistInfo.hpp"
namespace lms::scanner
{
class ArtistInfoParseException : public core::LmsException
{
public:
using core::LmsException::LmsException;
};
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
ArtistInfo parseArtistInfo(std::istream& is);
} // namespace lms::scanner
@@ -24,6 +24,11 @@
#include "core/PartialDateTime.hpp"
#include "core/Path.hpp"
#include "core/XxHash3.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/Types.hpp"
@@ -40,36 +45,34 @@
#include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackFeatures.hpp"
#include "database/objects/TrackLyrics.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
#include "metadata/Exception.hpp"
#include "metadata/IAudioFileParser.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "IFileScanOperation.hpp"
#include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "helpers/ArtistHelpers.hpp"
#include "scanners/IFileScanOperation.hpp"
#include "scanners/Utils.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms::scanner
{
namespace
{
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
{
for (const metadata::Artist& artistInfo : artists)
for (const Artist& artist : artists)
{
db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) };
db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) };
const bool matchedUsingMbid{ artistInfo.mbid.has_value() && artist->getMBID() == artistInfo.mbid };
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, artist, linkType, role, matchedUsingMbid) };
link.modify()->setArtistName(artistInfo.name);
if (artistInfo.sortName)
link.modify()->setArtistSortName(*artistInfo.sortName);
const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, dbArtist, linkType, role, matchedUsingMbid) };
link.modify()->setArtistName(artist.name);
if (artist.sortName)
link.modify()->setArtistSortName(*artist.sortName);
}
}
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
{
constexpr std::string_view noRole{};
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
@@ -102,128 +105,128 @@ namespace lms::scanner
return label;
}
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release)
{
if (release->getName() != releaseInfo.name)
release.modify()->setName(releaseInfo.name);
if (release->getSortName() != releaseInfo.sortName)
release.modify()->setSortName(releaseInfo.sortName);
if (release->getGroupMBID() != releaseInfo.groupMBID)
release.modify()->setGroupMBID(releaseInfo.groupMBID);
if (release->getTotalDisc() != releaseInfo.mediumCount)
release.modify()->setTotalDisc(releaseInfo.mediumCount);
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
if (release->isCompilation() != releaseInfo.isCompilation)
release.modify()->setCompilation(releaseInfo.isCompilation);
if (release->getBarcode() != releaseInfo.barcode)
release.modify()->setBarcode(releaseInfo.barcode);
if (release->getComment() != releaseInfo.comment)
release.modify()->setComment(releaseInfo.comment);
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
if (dbRelease->getName() != release.name)
dbRelease.modify()->setName(release.name);
if (dbRelease->getSortName() != release.sortName)
dbRelease.modify()->setSortName(release.sortName);
if (dbRelease->getGroupMBID() != release.groupMBID)
dbRelease.modify()->setGroupMBID(release.groupMBID);
if (dbRelease->getTotalDisc() != release.mediumCount)
dbRelease.modify()->setTotalDisc(release.mediumCount);
if (dbRelease->getArtistDisplayName() != release.artistDisplayName)
dbRelease.modify()->setArtistDisplayName(release.artistDisplayName);
if (dbRelease->isCompilation() != release.isCompilation)
dbRelease.modify()->setCompilation(release.isCompilation);
if (dbRelease->getBarcode() != release.barcode)
dbRelease.modify()->setBarcode(release.barcode);
if (dbRelease->getComment() != release.comment)
dbRelease.modify()->setComment(release.comment);
if (dbRelease->getReleaseTypeNames() != release.releaseTypes)
{
release.modify()->clearReleaseTypes();
for (std::string_view releaseType : releaseInfo.releaseTypes)
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
dbRelease.modify()->clearReleaseTypes();
for (std::string_view releaseType : release.releaseTypes)
dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
}
if (release->getCountryNames() != releaseInfo.countries)
if (dbRelease->getCountryNames() != release.countries)
{
release.modify()->clearCountries();
for (std::string_view country : releaseInfo.countries)
release.modify()->addCountry(getOrCreateCountry(session, country));
dbRelease.modify()->clearCountries();
for (std::string_view country : release.countries)
dbRelease.modify()->addCountry(getOrCreateCountry(session, country));
}
if (release->getLabelNames() != releaseInfo.labels)
if (dbRelease->getLabelNames() != release.labels)
{
release.modify()->clearLabels();
for (std::string_view label : releaseInfo.labels)
release.modify()->addLabel(getOrCreateLabel(session, label));
dbRelease.modify()->clearLabels();
for (std::string_view label : release.labels)
dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
}
}
// Compare release level info
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
bool isReleaseMatching(const db::Release::pointer& dbCandidateRelease, const Release& release)
{
// TODO: add more criterias?
return candidateRelease->getName() == releaseInfo.name
&& candidateRelease->getSortName() == releaseInfo.sortName
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
&& candidateRelease->getLabelNames() == releaseInfo.labels
&& candidateRelease->getBarcode() == releaseInfo.barcode;
return dbCandidateRelease->getName() == release.name
&& dbCandidateRelease->getSortName() == release.sortName
&& dbCandidateRelease->getTotalDisc() == release.mediumCount
&& dbCandidateRelease->isCompilation() == release.isCompilation
&& dbCandidateRelease->getLabelNames() == release.labels
&& dbCandidateRelease->getBarcode() == release.barcode;
}
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory)
{
db::Release::pointer release;
db::Release::pointer dbRelease;
// First try to get by MBID: fastest, safest
if (releaseInfo.mbid)
if (release.mbid)
{
release = db::Release::find(session, *releaseInfo.mbid);
if (!release)
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
dbRelease = db::Release::find(session, *release.mbid);
if (!dbRelease)
dbRelease = session.create<db::Release>(release.name, release.mbid);
}
else if (releaseInfo.name.empty())
else if (release.name.empty())
{
// No release name (only mbid) -> nothing to do
return release;
return dbRelease;
}
// Fall back on release name (collisions may occur)
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
if (!dbRelease && release.mediumCount && *release.mediumCount > 1 && parentDirectoryId.isValid())
{
db::Release::FindParameters params;
params.setParentDirectory(parentDirectoryId);
params.setName(releaseInfo.name);
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
params.setName(release.name);
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
// Already found a candidate
if (release)
if (dbRelease)
return;
// Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value())
if (dbCandidateRelease->getMBID().has_value())
return;
if (!isReleaseMatching(candidateRelease, releaseInfo))
if (!isReleaseMatching(dbCandidateRelease, release))
return;
release = candidateRelease;
dbRelease = dbCandidateRelease;
});
}
// Lastly try in the current directory: we do this at last to have
// opportunities to merge releases in case of migration / rescan
if (!release)
if (!dbRelease)
{
db::Release::FindParameters params;
params.setDirectory(currentDirectory->getId());
params.setName(releaseInfo.name);
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
params.setName(release.name);
db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
// Already found a candidate
if (release)
if (dbRelease)
return;
// Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value())
if (dbCandidateRelease->getMBID().has_value())
return;
if (!isReleaseMatching(candidateRelease, releaseInfo))
if (!isReleaseMatching(dbCandidateRelease, release))
return;
release = candidateRelease;
dbRelease = dbCandidateRelease;
});
}
if (!release)
release = session.create<db::Release>(releaseInfo.name);
if (!dbRelease)
dbRelease = session.create<db::Release>(release.name);
updateReleaseIfNeeded(session, release, releaseInfo);
return release;
updateReleaseIfNeeded(session, dbRelease, release);
return dbRelease;
}
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
db::Medium::pointer getOrCreateMedium(db::Session& session, const Medium& medium, const db::Release::pointer& release)
{
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
if (!dbMedium)
@@ -243,7 +246,7 @@ namespace lms::scanner
return dbMedium;
}
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
{
std::vector<db::Cluster::pointer> clusters;
@@ -274,69 +277,69 @@ namespace lms::scanner
return clusters;
}
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
db::TrackLyrics::pointer createLyrics(db::Session& session, const Lyrics& lyrics)
{
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
db::TrackLyrics::pointer dbLyrics{ session.create<db::TrackLyrics>() };
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
lyrics.modify()->setOffset(lyricsInfo.offset);
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
if (!lyricsInfo.synchronizedLines.empty())
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx");
dbLyrics.modify()->setOffset(lyrics.offset);
dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist);
dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle);
if (!lyrics.synchronizedLines.empty())
dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines);
else
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
dbLyrics.modify()->setUnsynchronizedLines(lyrics.unsynchronizedLines);
return lyrics;
return dbLyrics;
}
db::ImageType convertImageType(metadata::Image::Type type)
db::ImageType convertImageType(audio::Image::Type type)
{
switch (type)
{
case metadata::Image::Type::Unknown:
case audio::Image::Type::Unknown:
return db::ImageType::Unknown;
case metadata::Image::Type::Other:
case audio::Image::Type::Other:
return db::ImageType::Other;
case metadata::Image::Type::FileIcon:
case audio::Image::Type::FileIcon:
return db::ImageType::FileIcon;
case metadata::Image::Type::OtherFileIcon:
case audio::Image::Type::OtherFileIcon:
return db::ImageType::OtherFileIcon;
case metadata::Image::Type::FrontCover:
case audio::Image::Type::FrontCover:
return db::ImageType::FrontCover;
case metadata::Image::Type::BackCover:
case audio::Image::Type::BackCover:
return db::ImageType::BackCover;
case metadata::Image::Type::LeafletPage:
case audio::Image::Type::LeafletPage:
return db::ImageType::LeafletPage;
case metadata::Image::Type::Media:
case audio::Image::Type::Media:
return db::ImageType::Media;
case metadata::Image::Type::LeadArtist:
case audio::Image::Type::LeadArtist:
return db::ImageType::LeadArtist;
case metadata::Image::Type::Artist:
case audio::Image::Type::Artist:
return db::ImageType::Artist;
case metadata::Image::Type::Conductor:
case audio::Image::Type::Conductor:
return db::ImageType::Conductor;
case metadata::Image::Type::Band:
case audio::Image::Type::Band:
return db::ImageType::Band;
case metadata::Image::Type::Composer:
case audio::Image::Type::Composer:
return db::ImageType::Composer;
case metadata::Image::Type::Lyricist:
case audio::Image::Type::Lyricist:
return db::ImageType::Lyricist;
case metadata::Image::Type::RecordingLocation:
case audio::Image::Type::RecordingLocation:
return db::ImageType::RecordingLocation;
case metadata::Image::Type::DuringRecording:
case audio::Image::Type::DuringRecording:
return db::ImageType::DuringRecording;
case metadata::Image::Type::DuringPerformance:
case audio::Image::Type::DuringPerformance:
return db::ImageType::DuringPerformance;
case metadata::Image::Type::MovieScreenCapture:
case audio::Image::Type::MovieScreenCapture:
return db::ImageType::MovieScreenCapture;
case metadata::Image::Type::ColouredFish:
case audio::Image::Type::ColouredFish:
return db::ImageType::ColouredFish;
case metadata::Image::Type::Illustration:
case audio::Image::Type::Illustration:
return db::ImageType::Illustration;
case metadata::Image::Type::BandLogo:
case audio::Image::Type::BandLogo:
return db::ImageType::BandLogo;
case metadata::Image::Type::PublisherLogo:
case audio::Image::Type::PublisherLogo:
return db::ImageType::PublisherLogo;
}
@@ -361,10 +364,10 @@ namespace lms::scanner
return image;
}
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& dbTrack, const ImageInfo& imageInfo)
{
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(dbTrack, image) };
imageLink.modify()->setIndex(imageInfo.index);
imageLink.modify()->setType(convertImageType(imageInfo.type));
imageLink.modify()->setDescription(imageInfo.description);
@@ -372,35 +375,35 @@ namespace lms::scanner
return imageLink;
}
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span<const ImageInfo> images)
{
track.modify()->clearEmbeddedImageLinks();
dbTrack.modify()->clearEmbeddedImageLinks();
for (const ImageInfo& imageInfo : images)
{
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
track.modify()->addEmbeddedImageLink(link);
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) };
dbTrack.modify()->addEmbeddedImageLink(link);
}
}
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
db::Advisory getAdvisory(std::optional<Track::Advisory> advisory)
{
if (!advisory)
return db::Advisory::UnSet;
switch (advisory.value())
{
case metadata::Track::Advisory::Clean:
case Track::Advisory::Clean:
return db::Advisory::Clean;
case metadata::Track::Advisory::Explicit:
case Track::Advisory::Explicit:
return db::Advisory::Explicit;
case metadata::Track::Advisory::Unknown:
case Track::Advisory::Unknown:
return db::Advisory::Unknown;
}
return db::Advisory::UnSet;
}
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize)
{
db::Track::FindParameters params;
// Add as many fields as possible to limit errors
@@ -436,9 +439,9 @@ namespace lms::scanner
return res;
}
void fillInArtistsWithMbid(std::span<const metadata::Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
{
for (const metadata::Artist& artist : artists)
for (const Artist& artist : artists)
{
if (artist.mbid.has_value())
{
@@ -448,9 +451,9 @@ namespace lms::scanner
}
}
void fillInMbids(std::span<metadata::Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
{
for (metadata::Artist& artist : artists)
for (Artist& artist : artists)
{
if (!artist.mbid)
{
@@ -461,7 +464,7 @@ namespace lms::scanner
}
}
void fillMissingMbids(metadata::Track& track)
void fillMissingMbids(Track& track)
{
// first pass: collect all artists that have mbids
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
@@ -485,9 +488,10 @@ namespace lms::scanner
}
} // namespace
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser)
AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions)
: FileScanOperationBase{ std::move(fileToScan), db, settings }
, _parser{ parser }
, _metadataParser{ metadataParser }
, _parserOptions{ parserOptions }
{
}
@@ -495,17 +499,20 @@ namespace lms::scanner
void AudioFileScanOperation::scan()
{
std::unique_ptr<metadata::Track> track;
try
{
_parsedTrack = _parser.parseMetaData(getFilePath());
auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) };
_file.emplace();
_file->audioProperties = audioFileInfo->getAudioProperties();
_file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader());
// We fill missing artist mbids with mbids found on other artist roles
fillMissingMbids(*_parsedTrack);
fillMissingMbids(_file->track);
std::size_t index{};
_parser.parseImages(getFilePath(), [&](const metadata::Image& image) {
audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) {
try
{
image::ImageProperties properties{ image::probeImage(image.data) };
@@ -522,7 +529,7 @@ namespace lms::scanner
info.description = image.description;
info.properties = properties;
_parsedImages.push_back(std::move(info));
_file->images.push_back(std::move(info));
}
catch (const image::Exception& e)
{
@@ -532,15 +539,15 @@ namespace lms::scanner
index++;
});
}
catch (const metadata::AudioFileNoAudioPropertiesException&)
catch (const audio::AudioFileNoAudioPropertiesException&)
{
addError<NoAudioTrackFoundError>(getFilePath());
}
catch (const metadata::IOException& e)
catch (const audio::IOException& e)
{
addError<IOScanError>(getFilePath(), e.getErrorCode());
}
catch (const metadata::Exception& e)
catch (const audio::Exception& e)
{
addError<AudioFileScanError>(getFilePath());
}
@@ -552,7 +559,7 @@ namespace lms::scanner
db::Session& dbSession{ getDb().getTLSSession() };
db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) };
if (!_parsedTrack)
if (!_file)
{
if (track)
{
@@ -562,9 +569,9 @@ namespace lms::scanner
return OperationResult::Skipped;
}
if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
if (_file->track.mbid && (!track || getScannerSettings().skipDuplicateTrackMBID))
{
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_file->track.mbid) };
// find for an existing track MBID as the file may have just been moved
if (!track && duplicateTracks.size() == 1)
@@ -616,7 +623,7 @@ namespace lms::scanner
if (!track)
{
// maybe the file just moved?
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize());
track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize());
if (track)
{
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath());
@@ -625,7 +632,7 @@ namespace lms::scanner
}
// We estimate this is an audio file if the duration is not null
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
if (_file->audioProperties.duration == std::chrono::milliseconds::zero())
{
addError<BadAudioDurationError>(getFilePath());
@@ -639,8 +646,8 @@ namespace lms::scanner
// ***** Title
std::string title;
if (!_parsedTrack->title.empty())
title = _parsedTrack->title;
if (!_file->track.title.empty())
title = _file->track.title;
else
{
// TODO parse file name to guess track etc.
@@ -665,18 +672,18 @@ namespace lms::scanner
track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
// Audio properties
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
track.modify()->setBitrate(_file->audioProperties.bitrate ? *_file->audioProperties.bitrate : 0);
track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0);
track.modify()->setChannelCount(_file->audioProperties.channelCount ? *_file->audioProperties.channelCount : 0);
track.modify()->setDuration(_file->audioProperties.duration);
track.modify()->setSampleRate(_file->audioProperties.sampleRate ? *_file->audioProperties.sampleRate : 0);
track.modify()->setFileSize(getFileSize());
track.modify()->setLastWriteTime(getLastWriteTime());
if (_parsedTrack->encodingTime.isValid())
if (_file->track.encodingTime.isValid())
{
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
const core::PartialDateTime& encodingTime{ _file->track.encodingTime };
Wt::WDate date;
Wt::WTime time;
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
@@ -696,61 +703,61 @@ namespace lms::scanner
track.modify()->clearArtistLinks();
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
if (_parsedTrack->medium && _parsedTrack->medium->release)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
if (_file->track.medium && _file->track.medium->release)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _file->track.remixerArtists, allowFallback);
for (const auto& [role, performers] : _parsedTrack->performerArtists)
for (const auto& [role, performers] : _file->track.performerArtists)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_parsedTrack->medium && _parsedTrack->medium->release)
if (_file->track.medium && _file->track.medium->release)
{
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
}
else
{
track.modify()->setRelease({});
track.modify()->setMedium({});
}
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
track.modify()->setName(title);
track.modify()->setTrackNumber(_parsedTrack->position);
track.modify()->setDate(_parsedTrack->date);
track.modify()->setOriginalDate(_parsedTrack->originalDate);
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear });
track.modify()->setTrackNumber(_file->track.position);
track.modify()->setDate(_file->track.date);
track.modify()->setOriginalDate(_file->track.originalDate);
if (!track->getOriginalDate().isValid() && _file->track.originalYear)
track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear });
// If a file has an OriginalDate but no date, set it to ease filtering
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
track.modify()->setDate(_parsedTrack->originalDate);
if (!_file->track.date.isValid() && _file->track.originalDate.isValid())
track.modify()->setDate(_file->track.originalDate);
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
track.modify()->setTrackMBID(_parsedTrack->mbid);
track.modify()->setRecordingMBID(_file->track.recordingMBID);
track.modify()->setTrackMBID(_file->track.mbid);
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
trackFeatures.remove(); // TODO: only if MBID changed?
track.modify()->setCopyright(_parsedTrack->copyright);
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
track.modify()->setReplayGain(_parsedTrack->replayGain);
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
track.modify()->setCopyright(_file->track.copyright);
track.modify()->setCopyrightURL(_file->track.copyrightURL);
track.modify()->setAdvisory(getAdvisory(_file->track.advisory));
track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now
track.modify()->setReplayGain(_file->track.replayGain);
track.modify()->setArtistDisplayName(_file->track.artistDisplayName);
track.modify()->clearEmbeddedLyrics();
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
for (const Lyrics& lyricsInfo : _file->track.lyrics)
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
updateEmbeddedImages(dbSession, track, _parsedImages);
updateEmbeddedImages(dbSession, track, _file->images);
if (added)
{
@@ -19,33 +19,32 @@
#pragma once
#include "IFileScanOperation.hpp"
#include <memory>
#include <vector>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "image/Types.hpp"
#include "metadata/Types.hpp"
#include "FileScanOperationBase.hpp"
#include "FileToScan.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/FileToScan.hpp"
#include "scanners/IFileScanOperation.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::db
{
class IDb;
} // namespace lms::db
namespace lms::metadata
{
class IAudioFileParser;
} // namespace lms::metadata
namespace lms::scanner
{
class TrackMetadataParser;
struct ImageInfo
{
std::size_t index;
metadata::Image::Type type{ metadata::Image::Type::Unknown };
audio::Image::Type type{ audio::Image::Type::Unknown };
std::uint64_t hash{};
std::size_t size{};
image::ImageProperties properties;
@@ -56,7 +55,7 @@ namespace lms::scanner
class AudioFileScanOperation : public FileScanOperationBase
{
public:
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser);
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions);
~AudioFileScanOperation() override;
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
@@ -66,9 +65,15 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
metadata::IAudioFileParser& _parser;
std::unique_ptr<metadata::Track> _parsedTrack;
std::vector<ImageInfo> _parsedImages;
};
const TrackMetadataParser& _metadataParser;
const audio::ParserOptions& _parserOptions;
struct AudioFileInfo
{
audio::AudioProperties audioProperties;
Track track;
std::vector<ImageInfo> images;
};
std::optional<AudioFileInfo> _file;
};
} // namespace lms::scanner
@@ -21,53 +21,64 @@
#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"
#include "database/objects/Track.hpp"
#include "metadata/IAudioFileParser.hpp"
#include "AudioFileScanOperation.hpp"
#include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "scanners/Utils.hpp"
#include "scanners/audiofile/AudioFileScanOperation.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms::scanner
{
namespace
{
metadata::ParserReadStyle getParserReadStyle()
audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
{
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
if (readStyle == "fast")
return metadata::ParserReadStyle::Fast;
return audio::ParserOptions::AudioPropertiesReadStyle::Fast;
if (readStyle == "average")
return metadata::ParserReadStyle::Average;
return audio::ParserOptions::AudioPropertiesReadStyle::Average;
if (readStyle == "accurate")
return metadata::ParserReadStyle::Accurate;
return audio::ParserOptions::AudioPropertiesReadStyle::Accurate;
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
}
metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings)
TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings)
{
metadata::AudioFileParserParameters params;
TrackMetadataParser::Parameters params;
params.userExtraTags = settings.extraTags;
params.artistTagDelimiters = settings.artistTagDelimiters;
params.defaultTagDelimiters = settings.defaultTagDelimiters;
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
params.backend = metadata::ParserBackend::TagLib;
params.readStyle = getParserReadStyle();
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 }
, _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib
, _trackMetadataParser{ createTrackMetadataParserParameters(settings) }
, _parserOptions{ createAudioFileParserOptions() }
{
}
@@ -85,7 +96,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
{
return _metadataParser->getSupportedExtensions();
return audio::getSupportedExtensions(_parserOptions.parser);
}
bool AudioFileScanner::needsScan(const FileToScan& file) const
@@ -101,6 +112,6 @@ namespace lms::scanner
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
{
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser);
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
}
} // namespace lms::scanner
@@ -19,7 +19,10 @@
#pragma once
#include "IFileScanner.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "scanners/IFileScanner.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms
{
@@ -55,6 +58,7 @@ namespace lms::scanner
db::IDb& _db;
const ScannerSettings& _settings;
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
const TrackMetadataParser _trackMetadataParser;
const audio::ParserOptions _parserOptions;
};
} // namespace lms::scanner
@@ -17,30 +17,26 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileParser.hpp"
#include "TrackMetadataParser.hpp"
#include <span>
#include <string>
#include <string_view>
#include "audio/IAudioFileInfo.hpp"
#include "core/ILogger.hpp"
#include "core/PartialDateTime.hpp"
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
#include "Utils.hpp"
#include "avformat/AvFormatImageReader.hpp"
#include "avformat/AvFormatTagReader.hpp"
#include "avformat/Utils.hpp"
#include "taglib/TagLibImageReader.hpp"
#include "taglib/TagLibTagReader.hpp"
#include "taglib/Utils.hpp"
namespace lms::metadata
namespace lms::scanner
{
namespace
{
void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, ITagReader::TagValueVisitor visitor)
void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, audio::ITagReader::TagValueVisitor visitor)
{
tagReader.visitTagValues(tagType, [&](std::string_view value) {
auto visitTagIfNonEmpty{ [&](std::string_view tag) {
@@ -76,11 +72,11 @@ namespace lms::metadata
}
template<typename T>
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters, const WhiteList* whitelist = nullptr)
std::vector<T> getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes, std::span<const std::string> tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr)
{
std::vector<T> res;
for (const TagType tagType : tagTypes)
for (const audio::TagType tagType : tagTypes)
{
tagReader.visitTagValues(tagType, [&](std::string_view value) {
value = core::stringUtils::stringTrim(value);
@@ -150,7 +146,7 @@ namespace lms::metadata
}
template<typename T>
std::optional<T> getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes)
std::optional<T> getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list<audio::TagType> tagTypes)
{
std::optional<T> res;
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
@@ -161,45 +157,38 @@ namespace lms::metadata
}
template<typename T>
std::vector<T> getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span<const std::string> tagDelimiters)
std::vector<T> getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span<const std::string> tagDelimiters)
{
return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
}
template<typename T>
std::optional<T> getTagValueAs(const ITagReader& tagReader, TagType tagType)
std::optional<T> getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType)
{
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
}
std::vector<Lyrics> getLyrics(const ITagReader& tagReader)
std::vector<Lyrics> getLyrics(const audio::ITagReader& tagReader)
{
std::vector<Lyrics> res;
tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) {
std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?)
try
{
Lyrics lyrics{ parseLyrics(iss) };
if (lyrics.language.empty())
lyrics.language = language;
Lyrics lyrics{ parseLyrics(iss) };
if (lyrics.language.empty())
lyrics.language = language;
res.emplace_back(std::move(lyrics));
}
catch (const LyricsException& e)
{
LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() });
}
res.emplace_back(std::move(lyrics));
});
return res;
}
std::vector<Artist> getArtists(const ITagReader& tagReader,
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames,
const AudioFileParserParameters& params)
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
std::initializer_list<audio::TagType> artistTagNames,
std::initializer_list<audio::TagType> artistSortTagNames,
std::initializer_list<audio::TagType> artistMBIDTagNames,
const TrackMetadataParser::Parameters& params)
{
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, &params.artistsToNotSplit) };
if (artistNames.empty())
@@ -224,7 +213,7 @@ namespace lms::metadata
return artists;
}
PerformerContainer getPerformerArtists(const ITagReader& tagReader)
PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
{
PerformerContainer performers;
@@ -298,9 +287,9 @@ namespace lms::metadata
return artistDisplayName;
}
std::optional<Track::Advisory> getAdvisory(const ITagReader& tagReader)
std::optional<Track::Advisory> getAdvisory(const audio::ITagReader& tagReader)
{
if (const auto value{ getTagValueAs<int>(tagReader, TagType::Advisory) })
if (const auto value{ getTagValueAs<int>(tagReader, audio::TagType::Advisory) })
{
switch (*value)
{
@@ -318,92 +307,24 @@ namespace lms::metadata
}
} // namespace
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
{
return std::make_unique<AudioFileParser>(params);
}
AudioFileParser::AudioFileParser(const AudioFileParserParameters& params)
TrackMetadataParser::TrackMetadataParser(const Parameters& params)
: _params{ params }
{
switch (_params.backend)
{
case ParserBackend::TagLib:
LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle));
break;
case ParserBackend::AvFormat:
LMS_LOG(METADATA, INFO, "Using AvFormat parser");
break;
}
}
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
TrackMetadataParser::~TrackMetadataParser() = default;
Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const
{
switch (_params.backend)
{
case ParserBackend::TagLib:
return taglib::utils::getSupportedExtensions();
break;
case ParserBackend::AvFormat:
return avformat::utils::getSupportedExtensions();
break;
}
return {};
}
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
{
std::unique_ptr<ITagReader> tagReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
tagReader = std::make_unique<taglib::TagLibTagReader>(p, _params.readStyle, _params.debug);
break;
case ParserBackend::AvFormat:
tagReader = std::make_unique<avformat::AvFormatTagReader>(p, _params.debug);
break;
}
if (!tagReader)
throw AudioFileParsingException{ "Unhandled parser backend" };
return parseMetaData(*tagReader);
}
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const
{
std::unique_ptr<IImageReader> imageReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
imageReader = std::make_unique<taglib::TagLibImageReader>(p);
break;
case ParserBackend::AvFormat:
imageReader = std::make_unique<avformat::AvFormatImageReader>(p);
break;
}
if (!imageReader)
throw AudioFileParsingException{ "Unhandled parser backend" };
parseImages(*imageReader, std::move(visitor));
}
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
{
auto track{ std::make_unique<Track>() };
track->audioProperties = tagReader.getAudioProperties();
processTags(tagReader, *track);
Track track;
processTags(tagReader, track);
return track;
}
void AudioFileParser::processTags(const ITagReader& tagReader, Track& track) const
void TrackMetadataParser::processTags(const audio::ITagReader& tagReader, Track& track) const
{
using namespace audio;
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
@@ -469,8 +390,10 @@ namespace lms::metadata
track.originalYear = track.originalDate.getYear();
}
std::optional<Medium> AudioFileParser::getMedium(const ITagReader& tagReader) const
std::optional<Medium> TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const
{
using namespace audio;
std::optional<Medium> medium;
medium.emplace();
@@ -499,8 +422,10 @@ namespace lms::metadata
return medium;
}
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader) const
std::optional<Release> TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const
{
using namespace audio;
std::optional<Release> release;
auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) };
@@ -536,11 +461,4 @@ namespace lms::metadata
return release;
}
void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor)
{
reader.visitImages([&](const Image& image) {
visitor(image);
});
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2018 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 <set>
#include <string>
#include "audio/IAudioFileInfo.hpp"
#include "audio/ITagReader.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner
{
class TrackMetadataParser
{
public:
struct SortByLengthDesc
{
bool operator()(const std::string& a, const std::string& b) const
{
if (a.length() != b.length())
return a.length() > b.length();
return a < b; // Break ties using lexicographical order
}
};
using WhiteList = std::set<std::string, SortByLengthDesc>;
struct Parameters
{
std::vector<std::string> artistTagDelimiters;
WhiteList artistsToNotSplit;
std::vector<std::string> defaultTagDelimiters;
std::vector<std::string> userExtraTags;
};
TrackMetadataParser(const Parameters& params = {});
~TrackMetadataParser();
TrackMetadataParser(const TrackMetadataParser&) = delete;
TrackMetadataParser& operator=(const TrackMetadataParser&) = delete;
Track parseTrackMetaData(const audio::ITagReader& reader) const;
private:
void processTags(const audio::ITagReader& reader, Track& track) const;
std::optional<Medium> getMedium(const audio::ITagReader& tagReader) const;
std::optional<Release> getRelease(const audio::ITagReader& tagReader) const;
const Parameters _params;
};
} // namespace lms::scanner
@@ -24,9 +24,7 @@
#include <sstream>
#include <string_view>
#include "core/Exception.hpp"
namespace lms::metadata::utils
namespace lms::scanner::utils
{
Wt::WDate parseDate(std::string_view dateStr)
{
@@ -96,21 +94,6 @@ namespace lms::metadata::utils
return result * sign;
}
std::string_view readStyleToString(ParserReadStyle readStyle)
{
switch (readStyle)
{
case ParserReadStyle::Fast:
return "fast";
case ParserReadStyle::Average:
return "average";
case ParserReadStyle::Accurate:
return "accurate";
}
throw Exception{ "Unknown read style" };
}
PerformerArtist extractPerformerAndRole(std::string_view entry)
{
std::string_view artistName;
@@ -155,4 +138,4 @@ namespace lms::metadata::utils
return PerformerArtist{ Artist{ artistName }, std::string{ role } };
}
} // namespace lms::metadata::utils
} // namespace lms::scanner::utils
@@ -25,13 +25,12 @@
#include <Wt/WDate.h>
#include "metadata/Types.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::metadata::utils
namespace lms::scanner::utils
{
Wt::WDate parseDate(std::string_view dateStr);
std::optional<int> parseYear(std::string_view yearStr);
std::string_view readStyleToString(ParserReadStyle readStyle);
struct PerformerArtist
{
@@ -41,4 +40,4 @@ namespace lms::metadata::utils
// format is "artist name (role)"
PerformerArtist extractPerformerAndRole(std::string_view entry);
} // namespace lms::metadata::utils
} // namespace lms::scanner::utils
@@ -22,17 +22,19 @@
#include <fstream>
#include <optional>
#include "FileScanOperationBase.hpp"
#include "ScannerSettings.hpp"
#include "core/ILogger.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/TrackLyrics.hpp"
#include "metadata/Lyrics.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "Utils.hpp"
#include "ScannerSettings.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
#include "types/Lyrics.hpp"
namespace lms::scanner
{
@@ -48,28 +50,21 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
std::optional<metadata::Lyrics> _parsedLyrics;
std::optional<Lyrics> _parsedLyrics;
};
void LyricsFileScanOperation::scan()
{
try
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedLyrics = metadata::parseLyrics(ifs);
}
catch (const metadata::Exception& e)
{
addError<LyricsFileScanError>(getFilePath());
}
_parsedLyrics = parseLyrics(ifs);
}
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
@@ -140,7 +135,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedLyricsFileExtensions();
return getSupportedLyricsFileExtensions();
}
bool LyricsFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/Lyrics.hpp"
#include "LyricsParser.hpp"
#include <cassert>
#include <regex>
#include "core/String.hpp"
namespace lms::metadata
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
{
@@ -227,4 +227,4 @@ namespace lms::metadata
return lyrics;
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 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 <iosfwd>
#include <span>
#include "types/Lyrics.hpp"
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
Lyrics parseLyrics(std::istream& is);
} // namespace lms::scanner
@@ -27,12 +27,12 @@
#include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/PlayListFile.hpp"
#include "metadata/Exception.hpp"
#include "metadata/PlayList.hpp"
#include "FileScanOperationBase.hpp"
#include "ScanContext.hpp"
#include "Utils.hpp"
#include "services/scanner/ScanErrors.hpp"
#include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/playlist/PlayListParser.hpp"
namespace lms::scanner
{
@@ -51,28 +51,21 @@ namespace lms::scanner
void scan() override;
OperationResult processResult() override;
std::optional<metadata::PlayList> _parsedPlayList;
std::optional<PlayList> _parsedPlayList;
};
void PlayListFileScanOperation::scan()
{
try
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
std::ifstream ifs{ getFilePath() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec);
return;
}
addError<IOScanError>(getFilePath(), ec);
return;
}
_parsedPlayList = metadata::parsePlayList(ifs);
}
catch (const metadata::Exception& e)
{
addError<PlayListFileScanError>(getFilePath());
}
_parsedPlayList = parsePlayList(ifs);
}
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
@@ -139,7 +132,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
{
return metadata::getSupportedPlayListFileExtensions();
return getSupportedPlayListFileExtensions();
}
bool PlayListFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once
#include "IFileScanner.hpp"
#include "scanners/IFileScanner.hpp"
namespace lms::db
{
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata/PlayList.hpp"
#include "PlayListParser.hpp"
#include <array>
#include <string_view>
#include "core/String.hpp"
namespace lms::metadata
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
{
@@ -102,4 +102,4 @@ namespace lms::metadata
return playlist;
}
} // namespace lms::metadata
} // namespace lms::scanner
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 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 <iosfwd>
#include <span>
#include "types/PlayList.hpp"
namespace lms::scanner
{
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
PlayList parsePlayList(std::istream& is);
} // namespace lms::scanner
@@ -30,11 +30,11 @@
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackList.hpp"
#include "metadata/Types.hpp"
#include "ScanContext.hpp"
#include "ScannerSettings.hpp"
#include "helpers/ArtistHelpers.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner
{
@@ -53,7 +53,7 @@ namespace lms::scanner
{
assert(!link->isArtistMBIDMatched());
metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
LMS_LOG(DB, DEBUG, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast<int>(link->getType()) << " from " << link->getArtist() << " to " << newArtist);
@@ -66,7 +66,7 @@ namespace lms::scanner
{
assert(!artistInfo->isMBIDMatched());
const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist);
@@ -19,17 +19,12 @@
#pragma once
#include <filesystem>
#include <iosfwd>
#include <optional>
#include <span>
#include <string>
#include "core/UUID.hpp"
#include "metadata/Exception.hpp"
namespace lms::metadata
namespace lms::scanner
{
// See:
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
@@ -44,13 +39,4 @@ namespace lms::metadata
std::string disambiguation; // mb
std::string biography;
};
class ArtistInfoParseException : public Exception
{
public:
using Exception::Exception;
};
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
ArtistInfo parseArtistInfo(std::istream& is);
} // namespace lms::metadata
} // namespace lms::scanner
@@ -20,16 +20,11 @@
#pragma once
#include <chrono>
#include <filesystem>
#include <iosfwd>
#include <map>
#include <span>
#include <string>
#include <vector>
#include "metadata/Exception.hpp"
namespace lms::metadata
namespace lms::scanner
{
struct Lyrics
{
@@ -42,13 +37,4 @@ namespace lms::metadata
std::map<std::chrono::milliseconds, std::string> synchronizedLines;
std::vector<std::string> unsynchronizedLines;
};
class LyricsException : public metadata::Exception
{
public:
using metadata::Exception::Exception;
};
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions();
Lyrics parseLyrics(std::istream& is);
} // namespace lms::metadata
} // namespace lms::scanner
@@ -20,19 +20,14 @@
#pragma once
#include <filesystem>
#include <iosfwd>
#include <span>
#include <string>
#include <vector>
namespace lms::metadata
namespace lms::scanner
{
struct PlayList
{
std::string name;
std::vector<std::filesystem::path> files;
};
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
PlayList parsePlayList(std::istream& is);
} // namespace lms::metadata
} // namespace lms::scanner
@@ -19,11 +19,8 @@
#pragma once
#include <chrono>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -31,9 +28,9 @@
#include "core/PartialDateTime.hpp"
#include "core/UUID.hpp"
#include "Lyrics.hpp"
#include "types/Lyrics.hpp"
namespace lms::metadata
namespace lms::scanner
{
using Tags = std::map<std::string /* type */, std::vector<std::string> /* values */>;
@@ -94,15 +91,6 @@ namespace lms::metadata
}
};
struct AudioProperties
{
std::size_t bitrate{};
std::size_t bitsPerSample{};
std::size_t channelCount{};
std::chrono::milliseconds duration{};
std::size_t sampleRate{};
};
struct Track
{
enum class Advisory
@@ -111,7 +99,7 @@ namespace lms::metadata
Explicit,
Clean,
};
AudioProperties audioProperties;
std::optional<core::UUID> mbid;
std::optional<core::UUID> recordingMBID;
std::string title;
@@ -143,96 +131,4 @@ namespace lms::metadata
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
struct Image
{
// See TagLib types (based on ID3v2 APIC types)
enum class Type
{
// No information
Unknown,
// A type not enumerated below
Other,
// 32x32 PNG image that should be used as the file icon
FileIcon,
// File icon of a different size or format
OtherFileIcon,
// Front cover image of the album
FrontCover,
// Back cover image of the album
BackCover,
// Inside leaflet page of the album
LeafletPage,
// Image from the album itself
Media,
// Picture of the lead artist or soloist
LeadArtist,
// Picture of the artist or performer
Artist,
// Picture of the conductor
Conductor,
// Picture of the band or orchestra
Band,
// Picture of the composer
Composer,
// Picture of the lyricist or text writer
Lyricist,
// Picture of the recording location or studio
RecordingLocation,
// Picture of the artists during recording
DuringRecording,
// Picture of the artists during performance
DuringPerformance,
// Picture from a movie or video related to the track
MovieScreenCapture,
// Picture of a large, coloured fish
ColouredFish,
// Illustration related to the track
Illustration,
// Logo of the band or performer
BandLogo,
// Logo of the publisher (record company)
PublisherLogo
};
Type type{ Type::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span<const std::byte> data;
};
enum class ParserBackend
{
TagLib,
AvFormat,
};
enum class ParserReadStyle
{
Fast,
Average,
Accurate,
};
struct SortByLengthDesc
{
bool operator()(const std::string& a, const std::string& b) const
{
if (a.length() != b.length())
return a.length() > b.length();
return a < b; // Break ties using lexicographical order
}
};
using WhiteList = std::set<std::string, SortByLengthDesc>;
struct AudioFileParserParameters
{
ParserBackend backend{ ParserBackend::TagLib };
ParserReadStyle readStyle{ ParserReadStyle::Average };
std::vector<std::string> artistTagDelimiters;
WhiteList artistsToNotSplit;
std::vector<std::string> defaultTagDelimiters;
std::vector<std::string> userExtraTags;
bool debug{};
};
} // namespace lms::metadata
} // namespace lms::scanner
@@ -20,9 +20,9 @@
#include <gtest/gtest.h>
#include "metadata/ArtistInfo.hpp"
#include "scanners/artistinfo/ArtistInfoParser.hpp"
namespace lms::metadata::tests
namespace lms::scanner::tests
{
TEST(ArtistInfo, basic)
{
@@ -112,4 +112,4 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl
ASSERT_EQ(artistInfo.sortName, "Artist, My");
ASSERT_EQ(artistInfo.disambiguation, "My Artist");
}
} // namespace lms::metadata::tests
} // namespace lms::scanner::tests
@@ -20,9 +20,9 @@
#include <gtest/gtest.h>
#include "Utils.hpp"
#include "scanners/audiofile/Utils.hpp"
namespace lms::metadata::utils::tests
namespace lms::scanner::utils::tests
{
TEST(MetaData, parseDate)
{
@@ -149,4 +149,4 @@ namespace lms::metadata::utils::tests
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
}
}
} // namespace lms::metadata::utils::tests
} // namespace lms::scanner::utils::tests
@@ -0,0 +1,25 @@
include(GoogleTest)
add_executable(test-scanner
ArtistInfo.cpp
AudioFileUtils.cpp
Lyrics.cpp
PlayList.cpp
Scanner.cpp
TrackMetadataParser.cpp
)
target_include_directories(test-scanner PRIVATE
../impl
)
target_link_libraries(test-scanner PRIVATE
lmsscanner
lmsaudio
GTest::GTest
)
if (NOT CMAKE_CROSSCOMPILING)
gtest_discover_tests(test-scanner)
endif()
@@ -20,9 +20,9 @@
#include <gtest/gtest.h>
#include "metadata/Lyrics.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
namespace lms::metadata::tests
namespace lms::scanner::tests
{
using namespace std::chrono_literals;
@@ -397,4 +397,4 @@ I, I just woke up from a dream
EXPECT_EQ(lyrics.unsynchronizedLines[3], "I, I just woke up from a dream");
EXPECT_EQ(lyrics.unsynchronizedLines[4], "");
}
} // namespace lms::metadata::tests
} // namespace lms::scanner::tests
@@ -21,11 +21,11 @@
#include <gtest/gtest.h>
#include <sstream>
#include "metadata/PlayList.hpp"
#include "scanners/playlist/PlayListParser.hpp"
namespace lms::metadata::tests
namespace lms::scanner::tests
{
TEST(PlayList, basic)
TEST(Scanner, playlist)
{
std::istringstream is{ R"(#EXTM3U
#PLAYLIST:My super playlist
@@ -53,7 +53,7 @@ one to be/../one to be/normalized/foo.mp3)" };
EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3");
}
TEST(PlayList, UTF8_bom)
TEST(Scanner, playlist_UTF8_bom)
{
const unsigned char content[] = { 0xEF, 0xBB, 0xBF, '#', 'E', 'X', 'T', 'M', '3', 'U', '\r', '\n', '\r', '\n', '.', '.', '/', 't', 'e', 's', 't', '.', 'm', 'p', '3', '\r', '\n' };
std::istringstream is{ std::string(reinterpret_cast<const char*>(content), sizeof(content)) };
@@ -64,4 +64,4 @@ one to be/../one to be/normalized/foo.mp3)" };
EXPECT_EQ(playlist.files[0], "../test.mp3");
}
} // namespace lms::metadata::tests
} // namespace lms::scanner::tests
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Emeric Poupon
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
@@ -21,22 +21,14 @@
#include <memory>
#include <vector>
#include "ITagReader.hpp"
#include "audio/ITagReader.hpp"
namespace lms::metadata::tests
namespace lms::scanner::tests
{
class TestTagReader : public ITagReader
class TestTagReader : public audio::ITagReader
{
public:
static constexpr AudioProperties audioProperties{
.bitrate = 128000,
.bitsPerSample = 16,
.channelCount = 2,
.duration = std::chrono::seconds{ 180 },
.sampleRate = 44000,
};
using Tags = std::unordered_map<TagType, std::vector<std::string_view>>;
using Tags = std::unordered_map<audio::TagType, std::vector<std::string_view>>;
using Performers = std::unordered_map<std::string_view /*role*/, std::vector<std::string_view> /*names*/>;
using ExtraUserTags = std::unordered_map<std::string_view, std::vector<std::string_view>>;
using LyricsTags = std::unordered_map<std::string_view /*language*/, std::string_view /*contents*/>;
@@ -63,7 +55,7 @@ namespace lms::metadata::tests
_lyricsTags = std::move(lyricsTags);
}
void visitTagValues(TagType tag, TagValueVisitor visitor) const override
void visitTagValues(audio::TagType tag, TagValueVisitor visitor) const override
{
auto itValues{ _tags.find(tag) };
if (itValues != std::cend(_tags))
@@ -97,8 +89,6 @@ namespace lms::metadata::tests
visitor(language, lyrics);
}
const AudioProperties& getAudioProperties() const override { return audioProperties; }
private:
const Tags _tags;
Performers _performers;
@@ -106,8 +96,10 @@ namespace lms::metadata::tests
LyricsTags _lyricsTags;
};
inline std::unique_ptr<ITagReader> createDefaultPopulatedTestTagReader()
inline std::unique_ptr<audio::ITagReader> createDefaultPopulatedTestTagReader()
{
using namespace audio;
std::unique_ptr<TestTagReader> testTags{ std::make_unique<TestTagReader>(
TestTagReader::Tags{
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
@@ -168,4 +160,4 @@ namespace lms::metadata::tests
return testTags;
}
} // namespace lms::metadata::tests
} // namespace lms::scanner::tests
@@ -0,0 +1,916 @@
/*
* Copyright (C) 2024 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 <vector>
#include <gtest/gtest.h>
#include <Wt/WTime.h>
#include "scanners/audiofile/TrackMetadataParser.hpp"
#include "TestTagReader.hpp"
namespace lms::scanner::tests
{
TEST(TrackMetadataParser, generalTest)
{
TrackMetadataParser::Parameters params;
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
TrackMetadataParser parser{ params };
std::unique_ptr<audio::ITagReader> testTags{ createDefaultPopulatedTestTagReader() };
const Track track{ parser.parseTrackMetaData(*testTags) };
EXPECT_EQ(track.acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
ASSERT_TRUE(track.advisory.has_value());
EXPECT_EQ(track.advisory.value(), Track::Advisory::Clean);
EXPECT_EQ(track.artistDisplayName, "MyArtist1 & MyArtist2");
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "MyArtist1");
EXPECT_EQ(track.artists[0].sortName, "MyArtists1SortName");
EXPECT_EQ(track.artists[0].mbid, core::UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae"));
EXPECT_EQ(track.artists[1].name, "MyArtist2");
EXPECT_EQ(track.artists[1].sortName, "MyArtists2SortName");
EXPECT_EQ(track.artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
ASSERT_EQ(track.comments.size(), 2);
EXPECT_EQ(track.comments[0], "Comment1");
EXPECT_EQ(track.comments[1], "Comment2");
ASSERT_EQ(track.composerArtists.size(), 2);
EXPECT_EQ(track.composerArtists[0].name, "MyComposer1");
EXPECT_EQ(track.composerArtists[0].sortName, "MyComposerSortOrder1");
EXPECT_EQ(track.composerArtists[1].name, "MyComposer2");
EXPECT_EQ(track.composerArtists[1].sortName, "MyComposerSortOrder2");
ASSERT_EQ(track.conductorArtists.size(), 2);
EXPECT_EQ(track.conductorArtists[0].name, "MyConductor1");
EXPECT_EQ(track.conductorArtists[1].name, "MyConductor2");
EXPECT_EQ(track.copyright, "MyCopyright");
EXPECT_EQ(track.copyrightURL, "MyCopyrightURL");
ASSERT_TRUE(track.date.isValid());
EXPECT_EQ(track.date.getYear(), 2020);
EXPECT_EQ(track.date.getMonth(), 3);
EXPECT_EQ(track.date.getDay(), 4);
ASSERT_EQ(track.genres.size(), 2);
EXPECT_EQ(track.genres[0], "Genre1");
EXPECT_EQ(track.genres[1], "Genre2");
ASSERT_EQ(track.groupings.size(), 2);
EXPECT_EQ(track.groupings[0], "Grouping1");
EXPECT_EQ(track.groupings[1], "Grouping2");
ASSERT_EQ(track.languages.size(), 2);
EXPECT_EQ(track.languages[0], "Language1");
EXPECT_EQ(track.languages[1], "Language2");
ASSERT_EQ(track.lyricistArtists.size(), 2);
EXPECT_EQ(track.lyricistArtists[0].name, "MyLyricist1");
EXPECT_EQ(track.lyricistArtists[1].name, "MyLyricist2");
ASSERT_EQ(track.lyrics.size(), 1);
EXPECT_EQ(track.lyrics.front().language, "eng");
ASSERT_EQ(track.lyrics.front().synchronizedLines.size(), 2);
ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 0 }));
EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 0 })->second, "First line");
ASSERT_TRUE(track.lyrics.front().synchronizedLines.contains(std::chrono::milliseconds{ 1000 }));
EXPECT_EQ(track.lyrics.front().synchronizedLines.find(std::chrono::milliseconds{ 1000 })->second, "Second line");
ASSERT_TRUE(track.mbid.has_value());
EXPECT_EQ(track.mbid.value(), core::UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a"));
ASSERT_EQ(track.mixerArtists.size(), 2);
EXPECT_EQ(track.mixerArtists[0].name, "MyMixer1");
EXPECT_EQ(track.mixerArtists[1].name, "MyMixer2");
ASSERT_EQ(track.moods.size(), 2);
EXPECT_EQ(track.moods[0], "Mood1");
EXPECT_EQ(track.moods[1], "Mood2");
ASSERT_TRUE(track.originalDate.isValid());
EXPECT_EQ(track.originalDate.getYear(), 2019);
EXPECT_EQ(track.originalDate.getMonth(), 2);
EXPECT_EQ(track.originalDate.getDay(), 3);
ASSERT_TRUE(track.originalYear.has_value());
EXPECT_EQ(track.originalYear.value(), 2019);
ASSERT_TRUE(track.performerArtists.contains("Rolea"));
ASSERT_EQ(track.performerArtists.at("Rolea").size(), 2);
EXPECT_EQ(track.performerArtists.at("Rolea")[0].name, "MyPerformer1ForRoleA");
EXPECT_EQ(track.performerArtists.at("Rolea")[1].name, "MyPerformer2ForRoleA");
ASSERT_EQ(track.performerArtists.at("Roleb").size(), 2);
EXPECT_EQ(track.performerArtists.at("Roleb")[0].name, "MyPerformer1ForRoleB");
EXPECT_EQ(track.performerArtists.at("Roleb")[1].name, "MyPerformer2ForRoleB");
ASSERT_TRUE(track.position.has_value());
EXPECT_EQ(track.position.value(), 7);
ASSERT_EQ(track.producerArtists.size(), 2);
EXPECT_EQ(track.producerArtists[0].name, "MyProducer1");
EXPECT_EQ(track.producerArtists[1].name, "MyProducer2");
ASSERT_TRUE(track.recordingMBID.has_value());
EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
ASSERT_TRUE(track.replayGain.has_value());
EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33);
ASSERT_EQ(track.remixerArtists.size(), 2);
EXPECT_EQ(track.remixerArtists[0].name, "MyRemixer1");
EXPECT_EQ(track.remixerArtists[1].name, "MyRemixer2");
EXPECT_EQ(track.title, "MyTitle");
ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A").size(), 2);
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[0], "MyTagValue1ForTagA");
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_A")[1], "MyTagValue2ForTagA");
ASSERT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B").size(), 2);
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[0], "MyTagValue1ForTagB");
EXPECT_EQ(track.userExtraTags.at("MY_AWESOME_TAG_B")[1], "MyTagValue2ForTagB");
// Medium
ASSERT_TRUE(track.medium.has_value());
EXPECT_EQ(track.medium->media, "CD");
EXPECT_EQ(track.medium->name, "MySubtitle");
ASSERT_TRUE(track.medium->position.has_value());
EXPECT_EQ(track.medium->position.value(), 2);
ASSERT_TRUE(track.medium->replayGain.has_value());
EXPECT_FLOAT_EQ(track.medium->replayGain.value(), -0.5);
ASSERT_TRUE(track.medium->trackCount.has_value());
EXPECT_EQ(track.medium->trackCount.value(), 12);
// Release
ASSERT_TRUE(track.medium->release.has_value());
const Release& release{ track.medium->release.value() };
EXPECT_EQ(release.artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
ASSERT_EQ(release.artists.size(), 2);
EXPECT_EQ(release.artists[0].name, "MyAlbumArtist1");
EXPECT_EQ(release.artists[0].sortName, "MyAlbumArtists1SortName");
EXPECT_EQ(release.artists[0].mbid, core::UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
EXPECT_EQ(release.artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(release.artists[1].sortName, "MyAlbumArtists2SortName");
EXPECT_EQ(release.artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
EXPECT_TRUE(release.isCompilation);
EXPECT_EQ(release.barcode, "MyBarcode");
ASSERT_EQ(release.labels.size(), 2);
EXPECT_EQ(release.labels[0], "Label1");
EXPECT_EQ(release.labels[1], "Label2");
ASSERT_TRUE(release.mbid.has_value());
EXPECT_EQ(release.mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(release.groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
EXPECT_EQ(release.mediumCount, 3);
EXPECT_EQ(release.name, "MyAlbum");
EXPECT_EQ(release.sortName, "MyAlbumSortName");
EXPECT_EQ(release.comment, "MyAlbumComment");
ASSERT_EQ(release.countries.size(), 2);
EXPECT_EQ(release.countries[0], "MyCountry1");
EXPECT_EQ(release.countries[1], "MyCountry2");
{
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
EXPECT_EQ(release.releaseTypes, expectedReleaseTypes);
}
}
TEST(TrackMetadataParser, trim)
{
const TestTagReader testTags{
{
{ audio::TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.genres.size(), 3);
EXPECT_EQ(track.genres[0], "Genre1");
EXPECT_EQ(track.genres[1], "Genre2");
EXPECT_EQ(track.genres[2], "Genre3");
}
TEST(TrackMetadataParser, customDelimiters)
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
{ audio::TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
{ audio::TagType::Genre, { "Genre1 ; Genre2" } },
{ audio::TagType::Language, { " Lang1/Lang2 / Lang3" } },
}
};
TrackMetadataParser::Parameters params;
params.defaultTagDelimiters = { " ; ", "/" };
params.artistTagDelimiters = { " / ", " feat. " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 3);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artists[2].name, "Artist3");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit
ASSERT_EQ(track.genres.size(), 2);
EXPECT_EQ(track.genres[0], "Genre1");
EXPECT_EQ(track.genres[1], "Genre2");
ASSERT_EQ(track.languages.size(), 3);
EXPECT_EQ(track.languages[0], "Lang1");
EXPECT_EQ(track.languages[1], "Lang2");
EXPECT_EQ(track.languages[2], "Lang3");
// Medium
ASSERT_TRUE(track.medium.has_value());
// Release
ASSERT_TRUE(track.medium->release.has_value());
EXPECT_EQ(track.medium->release->name, "MyAlbum");
ASSERT_EQ(track.medium->release->artists.size(), 2);
EXPECT_EQ(track.medium->release->artists[0].name, "AlbumArtist1");
EXPECT_EQ(track.medium->release->artists[1].name, "AlbumArtist2");
EXPECT_EQ(track.medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist)
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { " AC/DC " } },
{ audio::TagType::Artist, { "AC/DC " } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].name, "AC/DC");
EXPECT_EQ(track.artistDisplayName, "AC/DC");
ASSERT_TRUE(track.medium.has_value());
ASSERT_TRUE(track.medium->release.has_value());
EXPECT_EQ(track.medium->release->name, "MyAlbum");
ASSERT_EQ(track.medium->release->artists.size(), 1);
EXPECT_EQ(track.medium->release->artists[0].name, "AC/DC");
EXPECT_EQ(track.medium->release->artistDisplayName, "AC/DC");
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_artists)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "AC/DC and MyArtist" } },
{ audio::TagType::Artists, { "AC/DC", "MyArtist" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { " AC/DC " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "AC/DC");
EXPECT_EQ(track.artists[1].name, "MyArtist");
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_first)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "AC/DC;MyArtist" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "AC/DC");
EXPECT_EQ(track.artists[1].name, "MyArtist");
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_middle)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2 " } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 3);
EXPECT_EQ(track.artists[0].name, "MyArtist1");
EXPECT_EQ(track.artists[1].name, "AC/DC");
EXPECT_EQ(track.artists[2].name, "MyArtist2");
EXPECT_EQ(track.artistDisplayName, "MyArtist1, AC/DC, MyArtist2"); // Reconstructed since this use case is not handled
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_multi_separators_last)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { ";", "/" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "AC/DC");
EXPECT_EQ(track.artists[1].name, "MyArtist");
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_longest_first)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { ";", "/" };
params.artistsToNotSplit = { "AC", "DC", "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "AC/DC");
EXPECT_EQ(track.artists[1].name, "MyArtist");
EXPECT_EQ(track.artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_begin)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " AC/DC; MyArtist" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].name, "AC/DC; MyArtist");
EXPECT_EQ(track.artistDisplayName, "AC/DC; MyArtist");
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_middle)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " MyArtist1; AC/DC ; MyArtist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].name, "MyArtist1; AC/DC ; MyArtist2");
EXPECT_EQ(track.artistDisplayName, "MyArtist1; AC/DC ; MyArtist2");
}
TEST(TrackMetadataParser, customArtistDelimiters_whitelist_partial_end)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { " MyArtist; AC/DC " } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/" };
params.artistsToNotSplit = { "AC/DC" };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].name, "MyArtist; AC/DC");
EXPECT_EQ(track.artistDisplayName, "MyArtist; AC/DC");
}
TEST(TrackMetadataParser, customDelimiters_foundInArtist)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1; Artist2" } },
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "; " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist
}
TEST(TrackMetadataParser, customDelimiters_foundInArtists)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 feat. Artist2" } },
{ audio::TagType::Artists, { "Artist1; Artist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "; " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1 feat. Artist2");
}
TEST(TrackMetadataParser, customDelimiters_notUsed)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "; " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2");
}
TEST(TrackMetadataParser, customDelimiters_onlyInArtist)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { " & " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(TrackMetadataParser, customDelimitersUsedForArtists)
{
const TestTagReader testTags{
{
{ audio::TagType::Artists, { "Artist1 & Artist2" } },
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { " & " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(TrackMetadataParser, noArtistInArtist)
{
const TestTagReader testTags{
{
// nothing in Artist!
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 0);
EXPECT_EQ(track.artistDisplayName, "");
}
TEST(TrackMetadataParser, singleArtistInArtists)
{
const TestTagReader testTags{
{
// nothing in Artist!
{ audio::TagType::Artists, { "Artist1" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artistDisplayName, "Artist1");
}
TEST(TrackMetadataParser, multipleArtistsInArtist)
{
const TestTagReader testTags{
{
// nothing in Artists!
{ audio::TagType::Artist, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(TrackMetadataParser, multipleArtistsInArtists)
{
const TestTagReader testTags{
{
// nothing in Artist!
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(TrackMetadataParser, multipleArtistsInArtistsWithEndDelimiter)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 & (CV. Artist2)" } },
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artistDisplayName, "Artist1 & (CV. Artist2)");
}
TEST(TrackMetadataParser, singleArtistInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtists, { "Artist1" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium);
ASSERT_TRUE(track.medium->release);
ASSERT_EQ(track.medium->release->artists.size(), 1);
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1");
}
TEST(TrackMetadataParser, multipleArtistsInAlbumArtist)
{
const TestTagReader testTags{
{
// nothing in AlbumArtists!
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium);
ASSERT_TRUE(track.medium->release);
ASSERT_EQ(track.medium->release->artists.size(), 2);
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(TrackMetadataParser, multipleArtistsInAlbumArtists_displayName)
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "Artist1 & Artist2" } },
{ audio::TagType::AlbumArtists, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium);
ASSERT_TRUE(track.medium->release);
ASSERT_EQ(track.medium->release->artists.size(), 2);
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1 & Artist2");
}
TEST(TrackMetadataParser, multipleArtistsInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtists, { "Artist1", "Artist2" } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium);
ASSERT_TRUE(track.medium->release);
ASSERT_EQ(track.medium->release->artists.size(), 2);
EXPECT_EQ(track.medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track.medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track.medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 & Artist2" } },
{ audio::TagType::Artists, { "Artist1", "Artist2" } },
{ audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[0].mbid, std::nullopt);
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artists[1].mbid, std::nullopt);
EXPECT_EQ(track.artistDisplayName, "Artist1 & Artist2");
}
TEST(TrackMetadataParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "Artist1 / Artist2" } },
{ audio::TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { " / " };
TrackMetadataParser parser{ params };
const Track track{ parser.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 2);
EXPECT_EQ(track.artists[0].name, "Artist1");
EXPECT_EQ(track.artists[0].mbid, std::nullopt);
EXPECT_EQ(track.artists[1].name, "Artist2");
EXPECT_EQ(track.artists[1].mbid, std::nullopt);
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
}
TEST(TrackMetadataParser, release_sortNameFallback)
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
// No AlbumSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium.has_value());
ASSERT_TRUE(track.medium->release.has_value());
EXPECT_EQ(track.medium->release->sortName, "MyAlbum");
}
TEST(TrackMetadataParser, artist_sortNameFallback)
{
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "MyArtist" } },
{ audio::TagType::ArtistSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "MyArtist" } },
{ audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ audio::TagType::Artist, { "MyArtist" } },
{ audio::TagType::ArtistSortOrder, { "MyArtistSortNameNotUsed" } },
{ audio::TagType::ArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.artists.size(), 1);
EXPECT_EQ(track.artists[0].sortName, "MyArtistSortName");
}
}
TEST(TrackMetadataParser, albumartist_sortNameFallback)
{
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "MyArtist" } },
{ audio::TagType::AlbumArtistSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium.has_value());
ASSERT_TRUE(track.medium->release.has_value());
const auto& artists{ track.medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "MyArtist" } },
{ audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium.has_value());
ASSERT_TRUE(track.medium->release.has_value());
const auto& artists{ track.medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
{
const TestTagReader testTags{
{
{ audio::TagType::Album, { "MyAlbum" } },
{ audio::TagType::AlbumArtist, { "MyArtist" } },
{ audio::TagType::AlbumArtistSortOrder, { "MyArtistSortNameNotUsed" } },
{ audio::TagType::AlbumArtistsSortOrder, { "MyArtistSortName" } },
// No ArtistSortOrder
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_TRUE(track.medium.has_value());
ASSERT_TRUE(track.medium->release.has_value());
const auto& artists{ track.medium->release->artists };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists[0].sortName, "MyArtistSortName");
}
}
TEST(TrackMetadataParser, advisory)
{
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
const TestTagReader testTags{
{
{ audio::TagType::Advisory, { value } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
if (track.advisory.has_value())
{
EXPECT_EQ(track.advisory.value(), expectedValue);
}
};
doTest("0", Track::Advisory::Unknown);
doTest("1", Track::Advisory::Explicit);
doTest("4", Track::Advisory::Explicit);
doTest("2", Track::Advisory::Clean);
doTest("", std::nullopt);
doTest("3", std::nullopt);
}
TEST(TrackMetadataParser, encodingTime)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
{
{ audio::TagType::EncodingTime, { value } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.encodingTime, expectedValue) << "Value = '" << value << "'";
};
doTest("", core::PartialDateTime{});
doTest("foo", core::PartialDateTime{});
doTest("2020-01-03T09:08:11.075", core::PartialDateTime{ 2020, 01, 03, 9, 8, 11 });
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
}
TEST(TrackMetadataParser, date)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
{
{ audio::TagType::Date, { value } },
}
};
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
ASSERT_EQ(track.date, expectedValue) << "Value = '" << value << "'";
};
doTest("", core::PartialDateTime{});
doTest("foo", core::PartialDateTime{});
doTest("2020-01-03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020-01", core::PartialDateTime{ 2020, 1 });
doTest("2020", core::PartialDateTime{ 2020 });
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
doTest("2020/01", core::PartialDateTime{ 2020, 1 });
doTest("2020", core::PartialDateTime{ 2020 });
}
} // namespace lms::scanner::tests
+3 -3
View File
@@ -1,6 +1,6 @@
add_library(lmstranscoding STATIC
impl/TranscodingResourceHandler.cpp
impl/TranscodingService.cpp
impl/TranscodeResourceHandler.cpp
impl/TranscodeService.cpp
)
target_include_directories(lmstranscoding INTERFACE
@@ -13,7 +13,7 @@ target_include_directories(lmstranscoding PRIVATE
)
target_link_libraries(lmstranscoding PRIVATE
lmsav
lmsaudio
)
target_link_libraries(lmstranscoding PUBLIC
@@ -17,42 +17,38 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TranscodingResourceHandler.hpp"
#include "TranscodeResourceHandler.hpp"
#include "av/Exception.hpp"
#include "av/ITranscoder.hpp"
#include "core/ILogger.hpp"
#include "audio/Exception.hpp"
#include "audio/ITranscoder.hpp"
namespace lms::transcoding
{
std::unique_ptr<core::IResourceHandler> createResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, bool estimateContentLength)
{
return std::make_unique<TranscodingResourceHandler>(inputParameters, outputParameters, estimateContentLength);
}
// TODO set some nice HTTP return code
TranscodingResourceHandler::TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional<std::size_t> estimatedContentLength)
ResourceHandler::ResourceHandler(const audio::TranscodeParameters& parameters, std::optional<std::size_t> estimatedContentLength)
: _estimatedContentLength{ estimatedContentLength }
{
try
{
_transcoder = av::createTranscoder(inputParameters, outputParameters);
_transcoder = createTranscoder(parameters);
if (_estimatedContentLength)
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
else
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length");
}
catch (av::Exception& e)
catch (audio::Exception& e)
{
LMS_LOG(TRANSCODING, ERROR, "Failed to create transcoder: " << e.what());
}
}
TranscodingResourceHandler::~TranscodingResourceHandler() = default;
ResourceHandler::~ResourceHandler() = default;
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
Wt::Http::ResponseContinuation* ResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
if (!_transcoder)
{
@@ -23,19 +23,19 @@
#include <memory>
#include <optional>
#include "av/ITranscoder.hpp"
#include "audio/ITranscoder.hpp"
#include "core/IResourceHandler.hpp"
namespace lms::transcoding
{
class TranscodingResourceHandler final : public core::IResourceHandler
class ResourceHandler final : public core::IResourceHandler
{
public:
TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional<std::size_t> estimatedContentLength);
~TranscodingResourceHandler() override;
ResourceHandler(const audio::TranscodeParameters& parameters, std::optional<std::size_t> estimatedContentLength);
~ResourceHandler() override;
TranscodingResourceHandler(const TranscodingResourceHandler&) = delete;
TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete;
ResourceHandler(const ResourceHandler&) = delete;
ResourceHandler& operator=(const ResourceHandler&) = delete;
private:
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
@@ -46,6 +46,6 @@ namespace lms::transcoding
std::array<std::byte, _chunkSize> _buffer;
std::size_t _bytesReadyCount{};
std::size_t _totalServedByteCount{};
std::unique_ptr<av::ITranscoder> _transcoder;
std::unique_ptr<audio::ITranscoder> _transcoder;
};
} // namespace lms::transcoding
@@ -17,24 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TranscodingService.hpp"
#include "TranscodeService.hpp"
#include "av/ITranscoder.hpp"
#include "audio/ITranscoder.hpp"
#include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "TranscodingResourceHandler.hpp"
#include "TranscodeResourceHandler.hpp"
namespace lms::transcoding
{
namespace
{
av::OutputParameters toAv(const OutputParameters& out)
{
return { .format = static_cast<lms::av::OutputFormat>(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata };
}
std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration)
{
const std::size_t estimatedContentLength{ static_cast<size_t>((bitrate / 8 * duration.count()) / 1000) };
@@ -42,40 +38,35 @@ namespace lms::transcoding
}
} // namespace
std::unique_ptr<ITranscodingService> createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager)
std::unique_ptr<ITranscodeService> createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager)
{
return std::make_unique<TranscodingService>(db, childProcessManager);
return std::make_unique<TranscodeService>(db, childProcessManager);
}
TranscodingService::TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager)
TranscodeService::TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager)
: _db{ db }
, _childProcessManager(childProcessManager)
{
LMS_LOG(TRANSCODING, INFO, "Service started!");
}
TranscodingService::~TranscodingService()
TranscodeService::~TranscodeService()
{
LMS_LOG(TRANSCODING, INFO, "Service stopped!");
}
std::unique_ptr<core::IResourceHandler> TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
std::unique_ptr<core::IResourceHandler> TranscodeService::createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength)
{
av::InputParameters avInputParams;
std::optional<std::size_t> estimatedContentLength;
avInputParams.file = inputParameters.filePath;
avInputParams.offset = inputParameters.offset;
avInputParams.streamIndex = inputParameters.streamIndex;
if (estimateContentLength)
{
if (inputParameters.offset < inputParameters.duration)
estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, inputParameters.duration - inputParameters.offset);
if (parameters.inputParameters.offset < parameters.inputParameters.duration)
estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset);
else
LMS_LOG(TRANSCODING, WARNING, "Offset " << inputParameters.offset << " is greater than audio file duration " << inputParameters.duration << ": not estimating content length");
LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.duration << ": not estimating content length");
}
return std::make_unique<TranscodingResourceHandler>(avInputParams, toAv(outputParameters), estimatedContentLength);
return std::make_unique<transcoding::ResourceHandler>(parameters, estimatedContentLength);
}
} // namespace lms::transcoding
@@ -19,21 +19,21 @@
#pragma once
#include "services/transcoding/ITranscodingService.hpp"
#include "services/transcoding/ITranscodeService.hpp"
namespace lms::transcoding
{
class TranscodingService : public ITranscodingService
class TranscodeService : public ITranscodeService
{
public:
explicit TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager);
~TranscodingService() override;
explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager);
~TranscodeService() override;
TranscodingService(const TranscodingService&) = delete;
TranscodingService& operator=(const TranscodingService&) = delete;
TranscodeService(const TranscodeService&) = delete;
TranscodeService& operator=(const TranscodeService&) = delete;
private:
std::unique_ptr<core::IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override;
std::unique_ptr<core::IResourceHandler> createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) override;
db::IDb& _db;
core::IChildProcessManager& _childProcessManager;
@@ -19,18 +19,13 @@
#pragma once
#include <functional>
#include "core/Exception.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata
namespace lms::transcoding
{
class IImageReader
class Exception : public core::LmsException
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(ImageVisitor visitor) const = 0;
using LmsException::LmsException;
};
} // namespace lms::metadata
} // namespace lms::transcoding
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include "audio/TranscodeTypes.hpp"
namespace lms
{
namespace core
{
class IChildProcessManager;
class IResourceHandler;
} // namespace core
namespace db
{
class IDb;
}
} // namespace lms
namespace lms::transcoding
{
class ITranscodeService
{
public:
virtual ~ITranscodeService() = default;
// virtual std::unique_ptr<IAudioFileInfo> parseAudioFileInfo(const std::filesystem::path& p) const = 0;
virtual std::unique_ptr<core::IResourceHandler> createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength = false) = 0;
};
std::unique_ptr<ITranscodeService> createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager);
} // namespace lms::transcoding
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <memory>
#include <optional>
namespace lms
{
namespace core
{
class IChildProcessManager;
class IResourceHandler;
} // namespace core
namespace db
{
class IDb;
}
} // namespace lms
namespace lms::transcoding
{
struct InputParameters
{
std::filesystem::path filePath;
std::chrono::milliseconds duration{}; // Duration of the audio file
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
std::optional<std::size_t> streamIndex; // Index of the stream to be transcoded (select the "best" audio stream if not set)
};
enum class OutputFormat
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
struct OutputParameters
{
OutputFormat format;
std::size_t bitrate{ 128'000 };
bool stripMetadata{ true };
};
class ITranscodingService
{
public:
virtual ~ITranscodingService() = default;
virtual std::unique_ptr<core::IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0;
};
std::unique_ptr<ITranscodingService> createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager);
} // namespace lms::transcoding

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