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 ### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings. It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, 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 ### 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. _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(core)
add_subdirectory(database) add_subdirectory(database)
add_subdirectory(image) add_subdirectory(image)
add_subdirectory(metadata)
add_subdirectory(services) add_subdirectory(services)
add_subdirectory(som) add_subdirectory(som)
add_subdirectory(subsonic) 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 "AudioFile.hpp"
#include <array>
#include <unordered_map>
extern "C" extern "C"
{ {
#define __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS
@@ -27,15 +30,13 @@ extern "C"
#include <libavutil/error.h> #include <libavutil/error.h>
} }
#include <array>
#include <unordered_map>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.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 namespace
{ {
@@ -49,11 +50,11 @@ namespace lms::av
return "Unknown error"; return "Unknown error";
} }
class AudioFileException : public Exception class AudioFileException : public AudioFileParsingException
{ {
public: public:
AudioFileException(int avError) 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) switch (codec)
{ {
case AV_CODEC_ID_MP3: case AV_CODEC_ID_MP3:
return DecodingCodec::MP3; return CodecType::MP3;
case AV_CODEC_ID_AAC: case AV_CODEC_ID_AAC:
return DecodingCodec::AAC; return CodecType::AAC;
case AV_CODEC_ID_AC3: case AV_CODEC_ID_AC3:
return DecodingCodec::AC3; return CodecType::AC3;
case AV_CODEC_ID_VORBIS: case AV_CODEC_ID_VORBIS:
return DecodingCodec::VORBIS; return CodecType::Vorbis;
case AV_CODEC_ID_WMAV1: case AV_CODEC_ID_WMAV1:
return DecodingCodec::WMAV1; return CodecType::WMA1;
case AV_CODEC_ID_WMAV2: 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: case AV_CODEC_ID_FLAC:
return DecodingCodec::FLAC; return CodecType::FLAC;
case AV_CODEC_ID_ALAC: case AV_CODEC_ID_ALAC:
return DecodingCodec::ALAC; return CodecType::ALAC;
case AV_CODEC_ID_WAVPACK: case AV_CODEC_ID_WAVPACK:
return DecodingCodec::WAVPACK; return CodecType::WavPack;
case AV_CODEC_ID_MUSEPACK7: case AV_CODEC_ID_MUSEPACK7:
return DecodingCodec::MUSEPACK7; return CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8: case AV_CODEC_ID_MUSEPACK8:
return DecodingCodec::MUSEPACK8; return CodecType::MPC8;
case AV_CODEC_ID_APE: case AV_CODEC_ID_APE:
return DecodingCodec::APE; return CodecType::APE;
case AV_CODEC_ID_EAC3: case AV_CODEC_ID_EAC3:
return DecodingCodec::EAC3; return CodecType::EAC3;
case AV_CODEC_ID_MP4ALS: case AV_CODEC_ID_MP4ALS:
return DecodingCodec::MP4ALS; return CodecType::MP4ALS;
case AV_CODEC_ID_OPUS: case AV_CODEC_ID_OPUS:
return DecodingCodec::OPUS; return CodecType::Opus;
case AV_CODEC_ID_SHORTEN: case AV_CODEC_ID_SHORTEN:
return DecodingCodec::SHORTEN; return CodecType::Shorten;
case AV_CODEC_ID_DSD_LSBF: case AV_CODEC_ID_DSD_LSBF:
return DecodingCodec::DSD_LSBF;
case AV_CODEC_ID_DSD_LSBF_PLANAR: case AV_CODEC_ID_DSD_LSBF_PLANAR:
return DecodingCodec::DSD_LSBF_PLANAR;
case AV_CODEC_ID_DSD_MSBF: case AV_CODEC_ID_DSD_MSBF:
return DecodingCodec::DSD_MSBF;
case AV_CODEC_ID_DSD_MSBF_PLANAR: case AV_CODEC_ID_DSD_MSBF_PLANAR:
return DecodingCodec::DSD_MSBF_PLANAR; return CodecType::DSD;
default: default:
return DecodingCodec::UNKNOWN; return std::nullopt;
} }
} }
} // namespace } // namespace
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
{
return std::make_unique<AudioFile>(p);
}
AudioFile::AudioFile(const std::filesystem::path& p) AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p } : _p{ p }
{ {
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) }; int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0) if (error < 0)
{ {
LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
throw AudioFileException{ error }; throw AudioFileException{ error };
} }
error = avformat_find_stream_info(_context, nullptr); error = avformat_find_stream_info(_context, nullptr);
if (error < 0) 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); avformat_close_input(&_context);
throw AudioFileException{ error }; throw AudioFileException{ error };
} }
@@ -158,9 +187,12 @@ namespace lms::av
ContainerInfo AudioFile::getContainerInfo() const ContainerInfo AudioFile::getContainerInfo() const
{ {
ContainerInfo info; ContainerInfo info;
info.container = avdemuxerToContainerType(_context->iformat->name);
info.containerName = _context->iformat->name;
info.bitrate = _context->bit_rate; info.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 }; 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; return info;
} }
@@ -258,7 +290,7 @@ namespace lms::av
if (avstream->codecpar == nullptr) 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; continue;
} }
@@ -275,7 +307,7 @@ namespace lms::av
else else
{ {
picture.mimeType = "application/octet-stream"; 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 }; const AVPacket& pkt{ avstream->attached_pic };
@@ -297,7 +329,7 @@ namespace lms::av
if (!avstream->codecpar) 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; return res;
} }
@@ -305,19 +337,29 @@ namespace lms::av
return res; return res;
res.emplace(); res.emplace();
res->index = streamIndex; res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate); res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
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->codecName = ::avcodec_get_name(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 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; 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/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "AvFormatImageReader.hpp" #include "ImageReader.hpp"
#include "av/Exception.hpp" #include <algorithm>
#include "av/IAudioFile.hpp"
#include "metadata/Exception.hpp"
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); }); 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 image;
image.data = picture.data; image.data = picture.data;
image.mimeType = picture.mimeType; 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/>. * 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/ILogger.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "metadata/Exception.hpp"
namespace lms::metadata::avformat namespace lms::audio::ffmpeg
{ {
namespace namespace
{ {
@@ -146,41 +143,20 @@ namespace lms::metadata::avformat
}; };
} // namespace } // 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) }; for (const auto& [key, value] : _metaDataMap)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
_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() };
} }
} }
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) }; auto itTagNames{ avFormatTagMapping.find(tag) };
if (itTagNames == std::cend(avFormatTagMapping)) 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 }) }; auto itValues{ _metaDataMap.find(std::string{ key }) };
if (itValues == std::cend(_metaDataMap)) if (itValues == std::cend(_metaDataMap))
@@ -209,14 +185,14 @@ namespace lms::metadata::avformat
visitor(itValues->second); visitor(itValues->second);
} }
void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{ {
visitTagValues("PERFORMER", [&](std::string_view value) { visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value); visitor("", value);
}); });
} }
void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{ {
// MPEG files: need to visit LYRICS-language entries // MPEG files: need to visit LYRICS-language entries
for (const auto& [tag, value] : _metaDataMap) for (const auto& [tag, value] : _metaDataMap)
@@ -234,4 +210,4 @@ namespace lms::metadata::avformat
visitor("", value); visitor("", value);
}); });
} }
} // namespace lms::metadata::avformat } // namespace lms::audio::ffmpeg
@@ -19,31 +19,27 @@
#pragma once #pragma once
#include <filesystem> #include "audio/ITagReader.hpp"
#include "av/IAudioFile.hpp" #include "AudioFile.hpp"
#include "ITagReader.hpp" namespace lms::audio::ffmpeg
namespace lms::metadata::avformat
{ {
class AvFormatTagReader : public ITagReader class TagReader : public ITagReader
{ {
public: public:
AvFormatTagReader(const std::filesystem::path& path, bool debug); TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
~AvFormatTagReader() override; ~TagReader() override;
AvFormatTagReader(const AvFormatTagReader&) = delete; TagReader(const TagReader&) = delete;
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete; TagReader& operator=(const TagReader&) = delete;
private: private:
void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override;
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
AudioProperties _audioProperties; const AudioFile& _audioFile;
av::IAudioFile::MetadataMap _metaDataMap; AudioFile::MetadataMap _metaDataMap;
av::ContainerInfo _containerInfo;
}; };
} // namespace lms::metadata::avformat } // namespace lms::audio::ffmpeg
@@ -27,17 +27,21 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/Service.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) #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::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath; static std::filesystem::path ffmpegPath;
@@ -48,10 +52,10 @@ namespace lms::av
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
} }
Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams) Transcoder::Transcoder(const TranscodeParameters& parameters)
: _debugId{ globalId++ } : _debugId{ globalId++ }
, _inputParams{ inputParams } , _inputParams{ parameters.inputParameters }
, _outputParams{ outputParams } , _outputParams{ parameters.outputParameters }
{ {
start(); start();
} }
@@ -65,18 +69,18 @@ namespace lms::av
try try
{ {
if (!std::filesystem::exists(_inputParams.file)) if (!std::filesystem::exists(_inputParams.filePath))
throw Exception{ "File " + _inputParams.file.string() + " does not exist!" }; throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
if (!std::filesystem::is_regular_file(_inputParams.file)) if (!std::filesystem::is_regular_file(_inputParams.filePath))
throw Exception{ "File " + _inputParams.file.string() + " is not regular!" }; throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
} }
catch (const std::filesystem::filesystem_error& e) catch (const std::filesystem::filesystem_error& e)
{ {
// TODO store/raise e.code() // 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; std::vector<std::string> args;
@@ -101,14 +105,7 @@ namespace lms::av
// Input file // Input file
args.emplace_back("-i"); args.emplace_back("-i");
args.emplace_back(_inputParams.file.string()); args.emplace_back(_inputParams.filePath.string());
// Stream mapping, if set
if (_inputParams.streamIndex)
{
args.emplace_back("-map");
args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex));
}
if (_outputParams.stripMetadata) if (_outputParams.stripMetadata)
{ {
@@ -121,47 +118,53 @@ namespace lms::av
args.emplace_back("-vn"); args.emplace_back("-vn");
// Output bitrates // Output bitrates
args.emplace_back("-b:a"); if (_outputParams.bitrate)
args.emplace_back(std::to_string(_outputParams.bitrate)); {
args.emplace_back("-b:a");
args.emplace_back(std::to_string(*_outputParams.bitrate));
}
// Codecs and formats // Codecs and formats
switch (_outputParams.format) if (_outputParams.format)
{ {
case OutputFormat::MP3: switch (*_outputParams.format)
args.emplace_back("-f"); {
args.emplace_back("mp3"); case OutputFormat::MP3:
break; args.emplace_back("-f");
args.emplace_back("mp3");
break;
case OutputFormat::OGG_OPUS: case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libopus"); args.emplace_back("libopus");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("ogg"); args.emplace_back("ogg");
break; break;
case OutputFormat::MATROSKA_OPUS: case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libopus"); args.emplace_back("libopus");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("matroska"); args.emplace_back("matroska");
break; break;
case OutputFormat::OGG_VORBIS: case OutputFormat::OGG_VORBIS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libvorbis"); args.emplace_back("libvorbis");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("ogg"); args.emplace_back("ogg");
break; break;
case OutputFormat::WEBM_VORBIS: case OutputFormat::WEBM_VORBIS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libvorbis"); args.emplace_back("libvorbis");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("webm"); args.emplace_back("webm");
break; break;
default: default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParams.format)) + ")" }; throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
}
} }
args.emplace_back("pipe:1"); args.emplace_back("pipe:1");
@@ -199,18 +202,22 @@ namespace lms::av
std::string_view Transcoder::getOutputMimeType() const std::string_view Transcoder::getOutputMimeType() const
{ {
switch (_outputParams.format) // TODO: use input mime type
if (_outputParams.format)
{ {
case OutputFormat::MP3: switch (*_outputParams.format)
return "audio/mpeg"; {
case OutputFormat::OGG_OPUS: case OutputFormat::MP3:
return "audio/opus"; return "audio/mpeg";
case OutputFormat::MATROSKA_OPUS: case OutputFormat::OGG_OPUS:
return "audio/x-matroska"; return "audio/opus";
case OutputFormat::OGG_VORBIS: case OutputFormat::MATROSKA_OPUS:
return "audio/ogg"; return "audio/x-matroska";
case OutputFormat::WEBM_VORBIS: case OutputFormat::OGG_VORBIS:
return "audio/webm"; return "audio/ogg";
case OutputFormat::WEBM_VORBIS:
return "audio/webm";
}
} }
return "application/octet-stream"; // default, should not happen return "application/octet-stream"; // default, should not happen
@@ -222,5 +229,4 @@ namespace lms::av
return _childProcess->finished(); return _childProcess->finished();
} }
} // namespace lms::audio::ffmpeg
} // namespace lms::av
@@ -19,19 +19,19 @@
#pragma once #pragma once
#include "av/ITranscoder.hpp" #include "audio/ITranscoder.hpp"
namespace lms::core namespace lms::core
{ {
class IChildProcess; class IChildProcess;
} }
namespace lms::av namespace lms::audio::ffmpeg
{ {
class Transcoder : public ITranscoder class Transcoder : public ITranscoder
{ {
public: public:
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); Transcoder(const TranscodeParameters& parameters);
~Transcoder() override; ~Transcoder() override;
Transcoder(const Transcoder&) = delete; Transcoder(const Transcoder&) = delete;
Transcoder& operator=(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::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
std::string_view getOutputMimeType() const 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; bool finished() const override;
static void init(); static void init();
void start(); void start();
const std::size_t _debugId{}; const std::size_t _debugId{};
const InputParameters _inputParams; const TranscodeInputParameters _inputParams;
const OutputParameters _outputParams; const TranscodeOutputParameters _outputParams;
std::unique_ptr<core::IChildProcess> _childProcess; std::unique_ptr<core::IChildProcess> _childProcess;
}; };
} // namespace lms::av } // namespace lms::audio::ffmpeg
@@ -19,11 +19,11 @@
#include "Utils.hpp" #include "Utils.hpp"
namespace lms::metadata::avformat::utils namespace lms::audio::ffmpeg::utils
{ {
std::span<const std::filesystem::path> getSupportedExtensions() 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{ static const std::array<std::filesystem::path, 18> fileExtensions{
".aac", ".aac",
".alac", ".alac",
@@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils
}; };
return fileExtensions; return fileExtensions;
} }
} // namespace lms::metadata::avformat::utils } // namespace lms::audio::ffmpeg::utils
@@ -22,7 +22,7 @@
#include <filesystem> #include <filesystem>
#include <span> #include <span>
namespace lms::metadata::avformat::utils namespace lms::audio::ffmpeg::utils
{ {
std::span<const std::filesystem::path> getSupportedExtensions(); 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/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "TagLibImageReader.hpp" #include "ImageReader.hpp"
#include "TagLibDefs.hpp" #include "TagLibDefs.hpp"
@@ -33,16 +33,14 @@
#include <taglib/mpcfile.h> #include <taglib/mpcfile.h>
#include <taglib/mpegfile.h> #include <taglib/mpegfile.h>
#include <taglib/opusfile.h> #include <taglib/opusfile.h>
#include <taglib/tfile.h>
#include <taglib/vorbisfile.h> #include <taglib/vorbisfile.h>
#include <taglib/wavfile.h> #include <taglib/wavfile.h>
#include <taglib/wavpackfile.h> #include <taglib/wavpackfile.h>
#include "core/ILogger.hpp" #include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "taglib/Utils.hpp" namespace lms::audio::taglib
namespace lms::metadata::taglib
{ {
namespace namespace
{ {
@@ -50,47 +48,47 @@ namespace lms::metadata::taglib
{ {
switch (type) switch (type)
{ {
case TagLib::ID3v2::AttachedPictureFrame::Type::Other: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other; return Image::Type::Other;
case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon; return Image::Type::FileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon; return Image::Type::OtherFileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover; return Image::Type::FrontCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover; return Image::Type::BackCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage; return Image::Type::LeafletPage;
case TagLib::ID3v2::AttachedPictureFrame::Type::Media: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media; return Image::Type::Media;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist; return Image::Type::LeadArtist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Artist: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist; return Image::Type::Artist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor; return Image::Type::Conductor;
case TagLib::ID3v2::AttachedPictureFrame::Type::Band: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band; return Image::Type::Band;
case TagLib::ID3v2::AttachedPictureFrame::Type::Composer: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer; return Image::Type::Composer;
case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist; return Image::Type::Lyricist;
case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation; return Image::Type::RecordingLocation;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording; return Image::Type::DuringRecording;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance; return Image::Type::DuringPerformance;
case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture; return Image::Type::MovieScreenCapture;
case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish; return Image::Type::ColouredFish;
case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration; return Image::Type::Illustration;
case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo; return Image::Type::BandLogo;
case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo; return Image::Type::PublisherLogo;
} }
@@ -101,47 +99,47 @@ namespace lms::metadata::taglib
{ {
switch (type) switch (type)
{ {
case TagLib::ASF::Picture::Type::Other: case ::TagLib::ASF::Picture::Type::Other:
return Image::Type::Other; return Image::Type::Other;
case TagLib::ASF::Picture::Type::FileIcon: case ::TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon; return Image::Type::FileIcon;
case TagLib::ASF::Picture::Type::OtherFileIcon: case ::TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon; return Image::Type::OtherFileIcon;
case TagLib::ASF::Picture::Type::FrontCover: case ::TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover; return Image::Type::FrontCover;
case TagLib::ASF::Picture::Type::BackCover: case ::TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover; return Image::Type::BackCover;
case TagLib::ASF::Picture::Type::LeafletPage: case ::TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage; return Image::Type::LeafletPage;
case TagLib::ASF::Picture::Type::Media: case ::TagLib::ASF::Picture::Type::Media:
return Image::Type::Media; return Image::Type::Media;
case TagLib::ASF::Picture::Type::LeadArtist: case ::TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist; return Image::Type::LeadArtist;
case TagLib::ASF::Picture::Type::Artist: case ::TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist; return Image::Type::Artist;
case TagLib::ASF::Picture::Type::Conductor: case ::TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor; return Image::Type::Conductor;
case TagLib::ASF::Picture::Type::Band: case ::TagLib::ASF::Picture::Type::Band:
return Image::Type::Band; return Image::Type::Band;
case TagLib::ASF::Picture::Type::Composer: case ::TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer; return Image::Type::Composer;
case TagLib::ASF::Picture::Type::Lyricist: case ::TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist; return Image::Type::Lyricist;
case TagLib::ASF::Picture::Type::RecordingLocation: case ::TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation; return Image::Type::RecordingLocation;
case TagLib::ASF::Picture::Type::DuringRecording: case ::TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording; return Image::Type::DuringRecording;
case TagLib::ASF::Picture::Type::DuringPerformance: case ::TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance; return Image::Type::DuringPerformance;
case TagLib::ASF::Picture::Type::MovieScreenCapture: case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture; return Image::Type::MovieScreenCapture;
case TagLib::ASF::Picture::Type::ColouredFish: case ::TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish; return Image::Type::ColouredFish;
case TagLib::ASF::Picture::Type::Illustration: case ::TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration; return Image::Type::Illustration;
case TagLib::ASF::Picture::Type::BandLogo: case ::TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo; return Image::Type::BandLogo;
case TagLib::ASF::Picture::Type::PublisherLogo: case ::TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo; return Image::Type::PublisherLogo;
} }
@@ -152,47 +150,47 @@ namespace lms::metadata::taglib
{ {
switch (type) switch (type)
{ {
case TagLib::FLAC::Picture::Type::Other: case ::TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other; return Image::Type::Other;
case TagLib::FLAC::Picture::Type::FileIcon: case ::TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon; return Image::Type::FileIcon;
case TagLib::FLAC::Picture::Type::OtherFileIcon: case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon; return Image::Type::OtherFileIcon;
case TagLib::FLAC::Picture::Type::FrontCover: case ::TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover; return Image::Type::FrontCover;
case TagLib::FLAC::Picture::Type::BackCover: case ::TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover; return Image::Type::BackCover;
case TagLib::FLAC::Picture::Type::LeafletPage: case ::TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage; return Image::Type::LeafletPage;
case TagLib::FLAC::Picture::Type::Media: case ::TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media; return Image::Type::Media;
case TagLib::FLAC::Picture::Type::LeadArtist: case ::TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist; return Image::Type::LeadArtist;
case TagLib::FLAC::Picture::Type::Artist: case ::TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist; return Image::Type::Artist;
case TagLib::FLAC::Picture::Type::Conductor: case ::TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor; return Image::Type::Conductor;
case TagLib::FLAC::Picture::Type::Band: case ::TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band; return Image::Type::Band;
case TagLib::FLAC::Picture::Type::Composer: case ::TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer; return Image::Type::Composer;
case TagLib::FLAC::Picture::Type::Lyricist: case ::TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist; return Image::Type::Lyricist;
case TagLib::FLAC::Picture::Type::RecordingLocation: case ::TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation; return Image::Type::RecordingLocation;
case TagLib::FLAC::Picture::Type::DuringRecording: case ::TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording; return Image::Type::DuringRecording;
case TagLib::FLAC::Picture::Type::DuringPerformance: case ::TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance; return Image::Type::DuringPerformance;
case TagLib::FLAC::Picture::Type::MovieScreenCapture: case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture; return Image::Type::MovieScreenCapture;
case TagLib::FLAC::Picture::Type::ColouredFish: case ::TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish; return Image::Type::ColouredFish;
case TagLib::FLAC::Picture::Type::Illustration: case ::TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration; return Image::Type::Illustration;
case TagLib::FLAC::Picture::Type::BandLogo: case ::TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo; return Image::Type::BandLogo;
case TagLib::FLAC::Picture::Type::PublisherLogo: case ::TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo; return Image::Type::PublisherLogo;
} }
@@ -203,44 +201,44 @@ namespace lms::metadata::taglib
{ {
switch (format) switch (format)
{ {
case TagLib::MP4::CoverArt::Format::BMP: case ::TagLib::MP4::CoverArt::Format::BMP:
return "image/bmp"; return "image/bmp";
case TagLib::MP4::CoverArt::Format::GIF: case ::TagLib::MP4::CoverArt::Format::GIF:
return "image/gif"; return "image/gif";
case TagLib::MP4::CoverArt::Format::JPEG: case ::TagLib::MP4::CoverArt::Format::JPEG:
return "image/jpeg"; return "image/jpeg";
case TagLib::MP4::CoverArt::Format::PNG: case ::TagLib::MP4::CoverArt::Format::PNG:
return "image/png"; return "image/png";
case TagLib::MP4::CoverArt::Format::Unknown: case ::TagLib::MP4::CoverArt::Format::Unknown:
return "application/octet-stream"; return "application/octet-stream";
} }
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) Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
{ {
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front")) if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover; return Image::Type::FrontCover;
else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover; return Image::Type::BackCover;
return Image::Type::Unknown; 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() }; 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) if (!attachedPictureFrame)
continue; 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() }; std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image; 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()) if (!asfPicture.isValid())
continue; 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() }; std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image; 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()) if (!coverItem.isValid())
return; return;
#if TAGLIB_HAS_MP4_ITEM_TYPE #if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList) if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
return; return;
#endif #endif
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; ::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
bool firstCover{ true }; bool firstCover{ true };
for (TagLib::MP4::CoverArt& coverArt : coverArtList) 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() }; std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image; 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) 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() }; std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image; 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 #if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") }; const ::TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const TagLib::VariantMap& pictureProperty : pictureProperties) for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
{ {
Image image; Image image;
TagLib::ByteVector picture; ::TagLib::ByteVector picture;
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend()) if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true)); image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
@@ -344,81 +342,77 @@ namespace lms::metadata::taglib
if (!image.data.empty()) if (!image.data.empty())
visitor(image); visitor(image);
} }
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
} }
} // namespace } // namespace
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p) ImageReader::ImageReader(::TagLib::File& file)
: _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) } : _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 // 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()) if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor)); visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
} }
// MP4 // 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 // 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() }) if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, std::move(visitor)); visitASFImages(*tag, visitor);
} }
// FLAC // 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 if (flacFile->hasID3v2Tag()) // usage discouraged
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor)); visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
else else
visitFLACImages(flacFile->pictureList(), std::move(visitor)); visitFLACImages(flacFile->pictureList(), visitor);
} }
// Ogg vorbis // 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 // 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 // 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()) if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), std::move(visitor)); visitID3V2Images(*aiffFile->tag(), visitor);
} }
// Wav // 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()) if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor)); visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
} }
// MPC // 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()) if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), std::move(visitor)); visitAPEImages(*mpcFile->APETag(), visitor);
} }
// WavPack // 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()) 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 #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: public:
AvFormatImageReader(const std::filesystem::path& p); ImageReader(TagLib::File& _file);
~AvFormatImageReader() override; ~ImageReader() override;
ImageReader(const ImageReader&) = delete;
AvFormatImageReader(const AvFormatImageReader&) = delete; ImageReader& operator=(const ImageReader&) = delete;
AvFormatImageReader& operator=(const AvFormatImageReader&) = delete;
private: 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> #include <taglib/taglib.h>
#if (TAGLIB_MAJOR_VERSION >= 2) #if (TAGLIB_MAJOR_VERSION >= 2)
#define TAGLIB_HAS_DSF 1 #define LMS_TAGLIB_HAS_DSF 1
#endif #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)) #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 #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)) #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 #endif
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "TagLibTagReader.hpp" #include "TagReader.hpp"
#include <unordered_map> #include <unordered_map>
@@ -25,7 +25,6 @@
#include <taglib/aifffile.h> #include <taglib/aifffile.h>
#include <taglib/apefile.h> #include <taglib/apefile.h>
#include <taglib/apeproperties.h>
#include <taglib/apetag.h> #include <taglib/apetag.h>
#include <taglib/asffile.h> #include <taglib/asffile.h>
#include <taglib/flacfile.h> #include <taglib/flacfile.h>
@@ -38,33 +37,25 @@
#include <taglib/speexfile.h> #include <taglib/speexfile.h>
#include <taglib/synchronizedlyricsframe.h> #include <taglib/synchronizedlyricsframe.h>
#include <taglib/tag.h> #include <taglib/tag.h>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h> #include <taglib/tpropertymap.h>
#include <taglib/trueaudiofile.h> #include <taglib/trueaudiofile.h>
#include <taglib/unsynchronizedlyricsframe.h> #include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/vorbisfile.h> #include <taglib/vorbisfile.h>
#include <taglib/wavfile.h> #include <taglib/wavfile.h>
#include <taglib/wavpackfile.h> #include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF #if LMS_TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h> #include <taglib/dsdifffile.h>
#include <taglib/dsffile.h> #include <taglib/dsffile.h>
#endif #endif
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "Utils.hpp" namespace lms::audio::taglib
namespace lms::metadata::taglib
{ {
namespace namespace
{ {
class TagParsingFailedException : public Exception
{
public:
using Exception::Exception;
};
// Mapping to internal taglib names and/or common alternative custom names // Mapping to internal taglib names and/or common alternative custom names
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{ const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
@@ -181,7 +172,7 @@ namespace lms::metadata::taglib
{ TagType::Writer, { "WRITER" } }, { TagType::Writer, { "WRITER" } },
}; };
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src) void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
{ {
for (auto&& [tag, values] : 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) for (auto& [key, values] : propertyMap)
{ {
if (values.size() <= 1) if (values.size() <= 1)
continue; continue;
TagLib::StringList newList; ::TagLib::StringList newList;
for (const TagLib::String& value : values) 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); newList.append(value);
} }
if (values != newList) 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; values = newList;
} }
} }
} }
} // namespace } // namespace
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug) TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
: _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) } : _file{ file }
{ {
if (!_file) _propertyMap = _file.properties();
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw AudioFileParsingException{ "Parsing failed" };
}
if (!_file->audioProperties()) enableExtraDebugLogs &= core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG);
{ if (enableExtraDebugLogs)
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))
{ {
for (const auto& [key, values] : _propertyMap) for (const auto& [key, values] : _propertyMap)
{ {
@@ -245,7 +223,7 @@ namespace lms::metadata::taglib
} }
// Some tags may not be known by 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) if (!apeTag)
return; return;
@@ -254,7 +232,7 @@ namespace lms::metadata::taglib
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) { 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 // 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() }; const auto& frameListMap{ id3v2Tags.frameListMap() };
@@ -264,26 +242,30 @@ namespace lms::metadata::taglib
// consider each frame hold a different set of lyrics // consider each frame hold a different set of lyrics
// Synchronized lyrics frames // 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) if (!lyricsFrame)
continue; // TODO log or assert? continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() }; const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
std::string lyrics; 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{}; std::chrono::milliseconds timestamp{};
switch (lyricsFrame->timestampFormat()) switch (lyricsFrame->timestampFormat())
{ {
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds: case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
timestamp = std::chrono::milliseconds{ synchedText.time }; timestamp = std::chrono::milliseconds{ synchedText.time };
break; break;
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames: case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 }; {
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
if (properties && properties->sampleRate())
timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
}
break; break;
case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown: case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
break; break;
} }
@@ -298,9 +280,9 @@ namespace lms::metadata::taglib
} }
// Unsynchronized lyrics frames // 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) if (!lyricsFrame)
continue; // TODO log or assert? continue; // TODO log or assert?
@@ -310,9 +292,9 @@ namespace lms::metadata::taglib
}; };
// WMA // 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()) for (const auto& [name, attributeList] : tag->attributeListMap())
{ {
@@ -320,19 +302,19 @@ namespace lms::metadata::taglib
continue; continue;
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) }; const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
if (debug) if (enableExtraDebugLogs)
{ {
for (const auto& attribute : attributeList) 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)) if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
continue; continue;
TagLib::StringList strAttributes; ::TagLib::StringList strAttributes;
for (const TagLib::ASF::Attribute& attribute : attributeList) 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()); strAttributes.append(attribute.toString());
} }
@@ -356,23 +338,23 @@ namespace lms::metadata::taglib
} }
} }
// MP3 // 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()) if (mp3File->hasID3v2Tag())
processID3v2Tags(*mp3File->ID3v2Tag()); processID3v2Tags(*mp3File->ID3v2Tag(false));
getAPETags(mp3File->APETag()); getAPETags(mp3File->APETag());
} }
// MP4 // 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 // 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 LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (rtngItem.type() == TagLib::MP4::Item::Type::Byte) if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
#endif #endif
_propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) }; _propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
} }
if (!_propertyMap.contains("ORIGINALDATE")) if (!_propertyMap.contains("ORIGINALDATE"))
@@ -386,7 +368,7 @@ namespace lms::metadata::taglib
auto itOrigDateTag{ tags.find(origDateString) }; auto itOrigDateTag{ tags.find(origDateString) };
if (itOrigDateTag != std::cend(tags)) if (itOrigDateTag != std::cend(tags))
{ {
const TagLib::StringList dates{ itOrigDateTag->second.toStringList() }; const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
if (!dates.isEmpty()) if (!dates.isEmpty())
{ {
_propertyMap["ORIGINALDATE"] = dates.front(); _propertyMap["ORIGINALDATE"] = dates.front();
@@ -397,68 +379,36 @@ namespace lms::metadata::taglib
} }
} }
// MPC // 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()); getAPETags(mpcFile->APETag());
} }
// WavPack // 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()); getAPETags(wavPackFile->APETag());
} }
// FLAC // 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 if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag()); 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()) if (aiffFile->hasID3v2Tag())
processID3v2Tags(*aiffFile->tag()); 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()) if (wavFile->hasID3v2Tag())
processID3v2Tags(*wavFile->ID3v2Tag()); processID3v2Tags(*wavFile->ID3v2Tag());
} }
} }
TagLibTagReader::~TagLibTagReader() = default; TagReader::~TagReader() = default;
void TagLibTagReader::computeAudioProperties() void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
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
{ {
auto itTagNames{ tagLibTagMapping.find(tag) }; auto itTagNames{ tagLibTagMapping.find(tag) };
if (itTagNames == std::cend(tagLibTagMapping)) 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) }; auto itValues{ _propertyMap.find(key) };
if (itValues == std::cend(_propertyMap)) if (itValues == std::cend(_propertyMap))
return; return;
for (const TagLib::String& value : itValues->second) for (const ::TagLib::String& value : itValues->second)
visitor(value.to8Bit(true)); visitor(value.to8Bit(true));
} }
void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{ {
visitTagValues("PERFORMER", [&](std::string_view value) { visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value); visitor("", value);
@@ -505,7 +455,7 @@ namespace lms::metadata::taglib
assert(rolePos != std::string::npos); assert(rolePos != std::string::npos);
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) }; 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) }; const std::string name{ value.to8Bit(true) };
visitor(role, name); 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()) 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 #pragma once
#include <filesystem>
#include <map> #include <map>
#include <span>
#include <string> #include <string>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.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: public:
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug); TagReader(::TagLib::File& file, bool enableExtraDebugLogs);
~TagLibTagReader() override; ~TagReader() override;
TagLibTagReader(const TagLibTagReader&) = delete;
TagLibTagReader& operator=(const TagLibTagReader&) = delete; TagReader(const TagReader&) = delete;
TagReader& operator=(const TagReader&) = delete;
private: private:
void computeAudioProperties();
void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override;
const AudioProperties& getAudioProperties() const override { return _audioProperties; } ::TagLib::File& _file;
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
std::unique_ptr<TagLib::File> _file;
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics; 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/vorbisfile.h>
#include <taglib/wavfile.h> #include <taglib/wavfile.h>
#include <taglib/wavpackfile.h> #include <taglib/wavpackfile.h>
#if TAGLIB_HAS_DSF #if LMS_TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#include <taglib/dsffile.h> #include <taglib/dsffile.h>
#endif #endif
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.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() std::span<const std::filesystem::path> getSupportedExtensions()
{ {
@@ -56,23 +58,23 @@ namespace lms::metadata::taglib::utils
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p", ".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc", ".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
".wav", ".wav",
#if TAGLIB_HAS_DSF #if LMS_TAGLIB_HAS_DSF
".dsf", ".dff", ".dsdiff" ".dsf"
#endif #endif
}; };
return std::span<const std::filesystem::path>{ supportedExtensions }; return std::span<const std::filesystem::path>{ supportedExtensions };
} }
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle) TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
{ {
switch (readStyle) switch (readStyle)
{ {
case ParserReadStyle::Fast: case ParserOptions::AudioPropertiesReadStyle::Fast:
return TagLib::AudioProperties::ReadStyle::Fast; return TagLib::AudioProperties::ReadStyle::Fast;
case ParserReadStyle::Average: case ParserOptions::AudioPropertiesReadStyle::Average:
return TagLib::AudioProperties::ReadStyle::Average; return TagLib::AudioProperties::ReadStyle::Average;
case ParserReadStyle::Accurate: case ParserOptions::AudioPropertiesReadStyle::Accurate:
return TagLib::AudioProperties::ReadStyle::Accurate; return TagLib::AudioProperties::ReadStyle::Accurate;
} }
@@ -100,8 +102,9 @@ namespace lms::metadata::taglib::utils
return TagLib::FileStream{ fd, true }; 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; std::unique_ptr<TagLib::File> file;
if (extension.empty()) if (extension.empty())
@@ -149,11 +152,9 @@ namespace lms::metadata::taglib::utils
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle); file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "WAV") else if (ext == "WAV")
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle); file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if TAGLIB_HAS_DSF #if LMS_TAGLIB_HAS_DSF
else if (ext == "DSF") else if (ext == "DSF")
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle); 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 #endif
if (file && !file->isValid()) if (file && !file->isValid())
@@ -162,8 +163,9 @@ namespace lms::metadata::taglib::utils
return file; 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; std::unique_ptr<TagLib::File> file;
if (TagLib::MPEG::File::isSupported(stream)) 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); file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::RIFF::WAV::File::isSupported(stream)) else if (TagLib::RIFF::WAV::File::isSupported(stream))
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle); 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)) else if (TagLib::DSF::File::isSupported(stream))
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle); 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 #endif
if (file && !file->isValid()) if (file && !file->isValid())
@@ -213,20 +213,33 @@ namespace lms::metadata::taglib::utils
return file; 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"); LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
TagLib::FileStream fileStream{ createFileStream(p) }; 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) if (!file)
{ {
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension"); LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value()); file = parseFileByContent(&fileStream, tagLibReadStyle);
if (!file) if (!file)
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content"); 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; return file;
} }
} // namespace lms::metadata::taglib::utils } // namespace lms::audio::taglib::utils
@@ -19,19 +19,16 @@
#pragma once #pragma once
#include "IImageReader.hpp" #include <filesystem>
#include <memory>
#include <span>
#include <taglib/tfile.h> #include <taglib/tfile.h>
namespace lms::metadata::taglib #include "audio/IAudioFileInfo.hpp"
namespace lms::audio::taglib::utils
{ {
class TagLibImageReader : public IImageReader std::span<const std::filesystem::path> getSupportedExtensions();
{ std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
public: } // namespace lms::audio::taglib::utils
TagLibImageReader(const std::filesystem::path& p);
void visitImages(ImageVisitor visitor) const override;
std::unique_ptr<TagLib::File> _file;
};
} // namespace lms::metadata::taglib
@@ -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" #include "core/Exception.hpp"
namespace lms::av namespace lms::audio
{ {
class Exception : public core::LmsException class Exception : public core::LmsException
{ {
public: public:
using LmsException::LmsException; 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 #pragma once
#include <functional> #include <functional>
#include <string_view>
#include "metadata/Types.hpp" #include "core/LiteralString.hpp"
namespace lms::metadata namespace lms::audio
{ {
// prefer using picard internal names // prefer using picard internal names
// see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
@@ -152,8 +153,12 @@ namespace lms::metadata
Website, Website,
WorkTitle, WorkTitle,
Writer, Writer,
Count, // Special value used to count the number of tags
}; };
core::LiteralString tagTypeToString(TagType type);
class ITagReader class ITagReader
{ {
public: public:
@@ -168,7 +173,5 @@ namespace lms::metadata
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>; using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0; 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 #pragma once
#include <cstddef> #include <cstddef>
#include <filesystem>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional>
#include <string_view> #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 class ITranscoder
{ {
public: public:
@@ -62,10 +39,10 @@ namespace lms::av
virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0; virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0;
virtual std::string_view getOutputMimeType() const = 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; virtual bool finished() const = 0;
}; };
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters);
} // namespace lms::av } // 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: case Module::API_SUBSONIC:
return "API_SUBSONIC"; return "API_SUBSONIC";
case Module::AUDIO:
return "AUDIO";
case Module::AUTH: case Module::AUTH:
return "AUTH"; return "AUTH";
case Module::AV:
return "AV";
case Module::CHILDPROCESS: case Module::CHILDPROCESS:
return "CHILDPROC"; return "CHILDPROC";
case Module::COVER: case Module::COVER:
+1 -1
View File
@@ -40,8 +40,8 @@ namespace lms::core::logging
enum class Module enum class Module
{ {
API_SUBSONIC, API_SUBSONIC,
AUDIO,
AUTH, AUTH,
AV,
CHILDPROCESS, CHILDPROCESS,
COVER, COVER,
DB, 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 target_link_libraries(lmsartwork PRIVATE
lmsaudio
lmsimage lmsimage
lmsmetadata
) )
target_link_libraries(lmsartwork PUBLIC target_link_libraries(lmsartwork PUBLIC
@@ -23,6 +23,9 @@
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -37,7 +40,6 @@
#include "image/Exception.hpp" #include "image/Exception.hpp"
#include "image/IEncodedImage.hpp" #include "image/IEncodedImage.hpp"
#include "image/Image.hpp" #include "image/Image.hpp"
#include "metadata/IAudioFileParser.hpp"
namespace lms::artwork namespace lms::artwork
{ {
@@ -50,7 +52,6 @@ namespace lms::artwork
const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultReleaseCoverSvgPath,
const std::filesystem::path& defaultArtistImageSvgPath) const std::filesystem::path& defaultArtistImageSvgPath)
: _db{ db } : _db{ db }
, _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) }
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
{ {
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
@@ -107,7 +108,11 @@ namespace lms::artwork
{ {
std::size_t currentIndex{}; 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) if (currentIndex++ != index)
return; 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()); LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
} }
@@ -33,11 +33,6 @@ namespace lms::db
class Session; class Session;
} }
namespace lms::metadata
{
class IAudioFileParser;
}
namespace lms::artwork namespace lms::artwork
{ {
class ArtworkService : public IArtworkService class ArtworkService : public IArtworkService
@@ -67,7 +62,6 @@ namespace lms::artwork
db::IDb& _db; db::IDb& _db;
std::unique_ptr<metadata::IAudioFileParser> _audioFileParser;
ImageCache _cache; ImageCache _cache;
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover; std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
std::shared_ptr<image::IEncodedImage> _defaultArtistImage; std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
+21 -7
View File
@@ -1,12 +1,17 @@
add_library(lmsscanner STATIC add_library(lmsscanner STATIC
impl/helpers/ArtistHelpers.cpp impl/helpers/ArtistHelpers.cpp
impl/scanners/ArtistInfoFileScanner.cpp impl/scanners/artistinfo/ArtistInfoParser.cpp
impl/scanners/AudioFileScanOperation.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/FileScanOperationBase.cpp
impl/scanners/AudioFileScanner.cpp
impl/scanners/ImageFileScanner.cpp impl/scanners/ImageFileScanner.cpp
impl/scanners/LyricsFileScanner.cpp
impl/scanners/PlayListFileScanner.cpp
impl/scanners/Utils.cpp impl/scanners/Utils.cpp
impl/steps/JobQueue.cpp impl/steps/JobQueue.cpp
impl/steps/ScanErrorLogger.cpp impl/steps/ScanErrorLogger.cpp
@@ -43,13 +48,22 @@ target_include_directories(lmsscanner PRIVATE
target_link_libraries(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE
lmscore lmscore
lmsdatabase lmsaudio
lmsimage lmsimage
lmsmetadata
lmsrecommendation lmsrecommendation
pugixml::pugixml
) )
target_link_libraries(lmsscanner PUBLIC target_link_libraries(lmsscanner PUBLIC
lmsdatabase
std::filesystem std::filesystem
Wt::Wt 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 <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) static void BM_Lyrics(benchmark::State& state)
{ {
@@ -89,4 +89,4 @@ namespace lms::metadata::benchmarks
BENCHMARK(BM_Lyrics); 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 <benchmark/benchmark.h>
#include "AudioFileParser.hpp" #include "scanners/audiofile/TrackMetadataParser.hpp"
#include "TestTagReader.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) 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" }; params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
std::unique_ptr<ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() }; std::unique_ptr<audio::ITagReader> testTags{ tests::createDefaultPopulatedTestTagReader() };
const TestAudioFileParser parser{ params }; const TrackMetadataParser parser{ params };
for (auto _ : state) 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{ const tests::TestTagReader testTags{
{ {
{ TagType::Artist, { "AC/DC; MyArtist" } }, { audio::TagType::Artist, { "AC/DC; MyArtist" } },
} }
}; };
const AudioFileParserParameters params; const TrackMetadataParser parser;
const TestAudioFileParser parser{ params };
for (auto _ : state) 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{ const tests::TestTagReader testTags{
{ {
{ TagType::Artist, { "AC/DC; MyArtist" } }, { audio::TagType::Artist, { "AC/DC; MyArtist" } },
} }
}; };
AudioFileParserParameters params; TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" }; params.artistTagDelimiters = { "/", ";" };
// The list itself is not important, the idea is to have some volume // The list itself is not important, the idea is to have some volume
params.artistsToNotSplit = { "AC/DC", params.artistsToNotSplit = { "AC/DC",
@@ -113,10 +104,10 @@ namespace lms::metadata::benchmarks
"White/Light", "White/Light",
"Yamantaka // Sonic Titan" }; "Yamantaka // Sonic Titan" };
const TestAudioFileParser parser{ params }; const TrackMetadataParser parser{ params };
for (auto _ : state) 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{ const tests::TestTagReader testTags{
{ {
{ TagType::Artist, { "AC/DC; MyArtist" } }, { audio::TagType::Artist, { "AC/DC; MyArtist" } },
} }
}; };
AudioFileParserParameters params; TrackMetadataParser::Parameters params;
params.artistTagDelimiters = { "/", ";" }; params.artistTagDelimiters = { "/", ";" };
const TestAudioFileParser parser{ params }; const TrackMetadataParser parser{ params };
for (auto _ : state) 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_WithWhitelist);
BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist); BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist);
} // namespace lms::metadata::benchmarks } // namespace lms::scanner::benchmarks
BENCHMARK_MAIN();
@@ -27,15 +27,16 @@
#include "core/IJobScheduler.hpp" #include "core/IJobScheduler.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "scanners/ArtistInfoFileScanner.hpp"
#include "scanners/AudioFileScanner.hpp"
#include "scanners/ImageFileScanner.hpp" #include "scanners/ImageFileScanner.hpp"
#include "scanners/LyricsFileScanner.hpp" #include "scanners/artistinfo/ArtistInfoFileScanner.hpp"
#include "scanners/PlayListFileScanner.hpp" #include "scanners/audiofile/AudioFileScanner.hpp"
#include "scanners/lyrics/LyricsFileScanner.hpp"
#include "scanners/playlist/PlayListFileScanner.hpp"
#include "steps/ScanStepArtistReconciliation.hpp" #include "steps/ScanStepArtistReconciliation.hpp"
#include "steps/ScanStepAssociateArtistImages.hpp" #include "steps/ScanStepAssociateArtistImages.hpp"
@@ -21,13 +21,14 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "metadata/Types.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner::helpers namespace lms::scanner::helpers
{ {
namespace 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) }; 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>"; 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 // MBID may be set
if (artist->getMBID() != artistInfo.mbid) if (artist->getMBID() != artistInfo.mbid)
@@ -68,7 +69,7 @@ namespace lms::scanner::helpers
} // namespace } // 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()); assert(artistInfo.mbid.has_value());
db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) }; db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) };
@@ -99,7 +100,7 @@ namespace lms::scanner::helpers
return artist; 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; db::Artist::pointer artist;
@@ -139,7 +140,7 @@ namespace lms::scanner::helpers
return artist; 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 // First try to get by MBID
if (artistInfo.mbid) if (artistInfo.mbid)
@@ -20,9 +20,10 @@
#pragma once #pragma once
#include "core/TaggedType.hpp" #include "core/TaggedType.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
namespace lms::metadata namespace lms::scanner
{ {
struct Artist; struct Artist;
} }
@@ -31,8 +32,8 @@ namespace lms::scanner::helpers
{ {
using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>; using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>;
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);
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 getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
} // namespace lms::scanner::helpers } // namespace lms::scanner::helpers
@@ -24,19 +24,22 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp" #include "database/objects/ArtistInfo.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "metadata/ArtistInfo.hpp"
#include "metadata/Types.hpp"
#include "services/scanner/ScanErrors.hpp" #include "services/scanner/ScanErrors.hpp"
#include "FileScanOperationBase.hpp"
#include "ScannerSettings.hpp" #include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "helpers/ArtistHelpers.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 namespace lms::scanner
{ {
@@ -57,7 +60,7 @@ namespace lms::scanner
std::string getArtistNameFromArtistInfoFilePath(); std::string getArtistNameFromArtistInfoFilePath();
std::optional<metadata::ArtistInfo> _parsedArtistInfo; std::optional<ArtistInfo> _parsedArtistInfo;
}; };
void ArtistInfoFileScanOperation::scan() void ArtistInfoFileScanOperation::scan()
@@ -72,14 +75,14 @@ namespace lms::scanner
return; return;
} }
_parsedArtistInfo = metadata::parseArtistInfo(ifs); _parsedArtistInfo = parseArtistInfo(ifs);
if (_parsedArtistInfo->name.empty()) if (_parsedArtistInfo->name.empty())
{ {
_parsedArtistInfo->name = getFilePath().parent_path().filename(); _parsedArtistInfo->name = getFilePath().parent_path().filename();
LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'"); LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'");
} }
} }
catch (const metadata::ArtistInfoParseException& e) catch (const ArtistInfoParseException& e)
{ {
addError<ArtistInfoFileScanError>(getFilePath()); 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 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)); 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 }) }; db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) };
artistInfo.modify()->setArtist(artist); artistInfo.modify()->setArtist(artist);
artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID()); 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 std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedFiles() const
{ {
return metadata::getSupportedArtistInfoFiles(); return getSupportedArtistInfoFiles();
} }
std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const std::span<const std::filesystem::path> ArtistInfoFileScanner::getSupportedExtensions() const
@@ -19,7 +19,7 @@
#pragma once #pragma once
#include "IFileScanner.hpp" #include "scanners/IFileScanner.hpp"
namespace lms::db namespace lms::db
{ {
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "metadata/ArtistInfo.hpp" #include "ArtistInfoParser.hpp"
#include <pugixml.hpp> #include <pugixml.hpp>
@@ -25,7 +25,7 @@
#include "core/LiteralString.hpp" #include "core/LiteralString.hpp"
#include "core/String.hpp" #include "core/String.hpp"
namespace lms::metadata namespace lms::scanner
{ {
namespace namespace
{ {
@@ -77,4 +77,4 @@ namespace lms::metadata
return artistInfo; 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/PartialDateTime.hpp"
#include "core/Path.hpp" #include "core/Path.hpp"
#include "core/XxHash3.hpp" #include "core/XxHash3.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -40,36 +45,34 @@
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackFeatures.hpp" #include "database/objects/TrackFeatures.hpp"
#include "database/objects/TrackLyrics.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 "services/scanner/ScanErrors.hpp"
#include "IFileScanOperation.hpp"
#include "ScannerSettings.hpp" #include "ScannerSettings.hpp"
#include "Utils.hpp"
#include "helpers/ArtistHelpers.hpp" #include "helpers/ArtistHelpers.hpp"
#include "scanners/IFileScanOperation.hpp"
#include "scanners/Utils.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms::scanner namespace lms::scanner
{ {
namespace 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 }; const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, artist, linkType, role, matchedUsingMbid) }; db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, dbArtist, linkType, role, matchedUsingMbid) };
link.modify()->setArtistName(artistInfo.name); link.modify()->setArtistName(artist.name);
if (artistInfo.sortName) if (artist.sortName)
link.modify()->setArtistSortName(*artistInfo.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{}; constexpr std::string_view noRole{};
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback); createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
@@ -102,128 +105,128 @@ namespace lms::scanner
return label; 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) if (dbRelease->getName() != release.name)
release.modify()->setName(releaseInfo.name); dbRelease.modify()->setName(release.name);
if (release->getSortName() != releaseInfo.sortName) if (dbRelease->getSortName() != release.sortName)
release.modify()->setSortName(releaseInfo.sortName); dbRelease.modify()->setSortName(release.sortName);
if (release->getGroupMBID() != releaseInfo.groupMBID) if (dbRelease->getGroupMBID() != release.groupMBID)
release.modify()->setGroupMBID(releaseInfo.groupMBID); dbRelease.modify()->setGroupMBID(release.groupMBID);
if (release->getTotalDisc() != releaseInfo.mediumCount) if (dbRelease->getTotalDisc() != release.mediumCount)
release.modify()->setTotalDisc(releaseInfo.mediumCount); dbRelease.modify()->setTotalDisc(release.mediumCount);
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) if (dbRelease->getArtistDisplayName() != release.artistDisplayName)
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); dbRelease.modify()->setArtistDisplayName(release.artistDisplayName);
if (release->isCompilation() != releaseInfo.isCompilation) if (dbRelease->isCompilation() != release.isCompilation)
release.modify()->setCompilation(releaseInfo.isCompilation); dbRelease.modify()->setCompilation(release.isCompilation);
if (release->getBarcode() != releaseInfo.barcode) if (dbRelease->getBarcode() != release.barcode)
release.modify()->setBarcode(releaseInfo.barcode); dbRelease.modify()->setBarcode(release.barcode);
if (release->getComment() != releaseInfo.comment) if (dbRelease->getComment() != release.comment)
release.modify()->setComment(releaseInfo.comment); dbRelease.modify()->setComment(release.comment);
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes) if (dbRelease->getReleaseTypeNames() != release.releaseTypes)
{ {
release.modify()->clearReleaseTypes(); dbRelease.modify()->clearReleaseTypes();
for (std::string_view releaseType : releaseInfo.releaseTypes) for (std::string_view releaseType : release.releaseTypes)
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType)); dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
} }
if (release->getCountryNames() != releaseInfo.countries) if (dbRelease->getCountryNames() != release.countries)
{ {
release.modify()->clearCountries(); dbRelease.modify()->clearCountries();
for (std::string_view country : releaseInfo.countries) for (std::string_view country : release.countries)
release.modify()->addCountry(getOrCreateCountry(session, country)); dbRelease.modify()->addCountry(getOrCreateCountry(session, country));
} }
if (release->getLabelNames() != releaseInfo.labels) if (dbRelease->getLabelNames() != release.labels)
{ {
release.modify()->clearLabels(); dbRelease.modify()->clearLabels();
for (std::string_view label : releaseInfo.labels) for (std::string_view label : release.labels)
release.modify()->addLabel(getOrCreateLabel(session, label)); dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
} }
} }
// Compare release level info // 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? // TODO: add more criterias?
return candidateRelease->getName() == releaseInfo.name return dbCandidateRelease->getName() == release.name
&& candidateRelease->getSortName() == releaseInfo.sortName && dbCandidateRelease->getSortName() == release.sortName
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount && dbCandidateRelease->getTotalDisc() == release.mediumCount
&& candidateRelease->isCompilation() == releaseInfo.isCompilation && dbCandidateRelease->isCompilation() == release.isCompilation
&& candidateRelease->getLabelNames() == releaseInfo.labels && dbCandidateRelease->getLabelNames() == release.labels
&& candidateRelease->getBarcode() == releaseInfo.barcode; && 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 // First try to get by MBID: fastest, safest
if (releaseInfo.mbid) if (release.mbid)
{ {
release = db::Release::find(session, *releaseInfo.mbid); dbRelease = db::Release::find(session, *release.mbid);
if (!release) if (!dbRelease)
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid); 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 // No release name (only mbid) -> nothing to do
return release; return dbRelease;
} }
// Fall back on release name (collisions may occur) // 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 // First try using all sibling directories (case for Album/DiscX), only if the disc number is set
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() }; 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; db::Release::FindParameters params;
params.setParentDirectory(parentDirectoryId); params.setParentDirectory(parentDirectoryId);
params.setName(releaseInfo.name); params.setName(release.name);
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
// Already found a candidate // Already found a candidate
if (release) if (dbRelease)
return; return;
// Do not fallback on properly tagged releases // Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value()) if (dbCandidateRelease->getMBID().has_value())
return; return;
if (!isReleaseMatching(candidateRelease, releaseInfo)) if (!isReleaseMatching(dbCandidateRelease, release))
return; return;
release = candidateRelease; dbRelease = dbCandidateRelease;
}); });
} }
// Lastly try in the current directory: we do this at last to have // Lastly try in the current directory: we do this at last to have
// opportunities to merge releases in case of migration / rescan // opportunities to merge releases in case of migration / rescan
if (!release) if (!dbRelease)
{ {
db::Release::FindParameters params; db::Release::FindParameters params;
params.setDirectory(currentDirectory->getId()); params.setDirectory(currentDirectory->getId());
params.setName(releaseInfo.name); params.setName(release.name);
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) {
// Already found a candidate // Already found a candidate
if (release) if (dbRelease)
return; return;
// Do not fallback on properly tagged releases // Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value()) if (dbCandidateRelease->getMBID().has_value())
return; return;
if (!isReleaseMatching(candidateRelease, releaseInfo)) if (!isReleaseMatching(dbCandidateRelease, release))
return; return;
release = candidateRelease; dbRelease = dbCandidateRelease;
}); });
} }
if (!release) if (!dbRelease)
release = session.create<db::Release>(releaseInfo.name); dbRelease = session.create<db::Release>(release.name);
updateReleaseIfNeeded(session, release, releaseInfo); updateReleaseIfNeeded(session, dbRelease, release);
return 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) }; db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
if (!dbMedium) if (!dbMedium)
@@ -243,7 +246,7 @@ namespace lms::scanner
return dbMedium; 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; std::vector<db::Cluster::pointer> clusters;
@@ -274,69 +277,69 @@ namespace lms::scanner
return clusters; 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"); dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx");
lyrics.modify()->setOffset(lyricsInfo.offset); dbLyrics.modify()->setOffset(lyrics.offset);
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist); dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist);
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle); dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle);
if (!lyricsInfo.synchronizedLines.empty()) if (!lyrics.synchronizedLines.empty())
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines); dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines);
else 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) switch (type)
{ {
case metadata::Image::Type::Unknown: case audio::Image::Type::Unknown:
return db::ImageType::Unknown; return db::ImageType::Unknown;
case metadata::Image::Type::Other: case audio::Image::Type::Other:
return db::ImageType::Other; return db::ImageType::Other;
case metadata::Image::Type::FileIcon: case audio::Image::Type::FileIcon:
return db::ImageType::FileIcon; return db::ImageType::FileIcon;
case metadata::Image::Type::OtherFileIcon: case audio::Image::Type::OtherFileIcon:
return db::ImageType::OtherFileIcon; return db::ImageType::OtherFileIcon;
case metadata::Image::Type::FrontCover: case audio::Image::Type::FrontCover:
return db::ImageType::FrontCover; return db::ImageType::FrontCover;
case metadata::Image::Type::BackCover: case audio::Image::Type::BackCover:
return db::ImageType::BackCover; return db::ImageType::BackCover;
case metadata::Image::Type::LeafletPage: case audio::Image::Type::LeafletPage:
return db::ImageType::LeafletPage; return db::ImageType::LeafletPage;
case metadata::Image::Type::Media: case audio::Image::Type::Media:
return db::ImageType::Media; return db::ImageType::Media;
case metadata::Image::Type::LeadArtist: case audio::Image::Type::LeadArtist:
return db::ImageType::LeadArtist; return db::ImageType::LeadArtist;
case metadata::Image::Type::Artist: case audio::Image::Type::Artist:
return db::ImageType::Artist; return db::ImageType::Artist;
case metadata::Image::Type::Conductor: case audio::Image::Type::Conductor:
return db::ImageType::Conductor; return db::ImageType::Conductor;
case metadata::Image::Type::Band: case audio::Image::Type::Band:
return db::ImageType::Band; return db::ImageType::Band;
case metadata::Image::Type::Composer: case audio::Image::Type::Composer:
return db::ImageType::Composer; return db::ImageType::Composer;
case metadata::Image::Type::Lyricist: case audio::Image::Type::Lyricist:
return db::ImageType::Lyricist; return db::ImageType::Lyricist;
case metadata::Image::Type::RecordingLocation: case audio::Image::Type::RecordingLocation:
return db::ImageType::RecordingLocation; return db::ImageType::RecordingLocation;
case metadata::Image::Type::DuringRecording: case audio::Image::Type::DuringRecording:
return db::ImageType::DuringRecording; return db::ImageType::DuringRecording;
case metadata::Image::Type::DuringPerformance: case audio::Image::Type::DuringPerformance:
return db::ImageType::DuringPerformance; return db::ImageType::DuringPerformance;
case metadata::Image::Type::MovieScreenCapture: case audio::Image::Type::MovieScreenCapture:
return db::ImageType::MovieScreenCapture; return db::ImageType::MovieScreenCapture;
case metadata::Image::Type::ColouredFish: case audio::Image::Type::ColouredFish:
return db::ImageType::ColouredFish; return db::ImageType::ColouredFish;
case metadata::Image::Type::Illustration: case audio::Image::Type::Illustration:
return db::ImageType::Illustration; return db::ImageType::Illustration;
case metadata::Image::Type::BandLogo: case audio::Image::Type::BandLogo:
return db::ImageType::BandLogo; return db::ImageType::BandLogo;
case metadata::Image::Type::PublisherLogo: case audio::Image::Type::PublisherLogo:
return db::ImageType::PublisherLogo; return db::ImageType::PublisherLogo;
} }
@@ -361,10 +364,10 @@ namespace lms::scanner
return image; 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) }; 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()->setIndex(imageInfo.index);
imageLink.modify()->setType(convertImageType(imageInfo.type)); imageLink.modify()->setType(convertImageType(imageInfo.type));
imageLink.modify()->setDescription(imageInfo.description); imageLink.modify()->setDescription(imageInfo.description);
@@ -372,35 +375,35 @@ namespace lms::scanner
return imageLink; 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) for (const ImageInfo& imageInfo : images)
{ {
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) }; db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) };
track.modify()->addEmbeddedImageLink(link); dbTrack.modify()->addEmbeddedImageLink(link);
} }
} }
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory) db::Advisory getAdvisory(std::optional<Track::Advisory> advisory)
{ {
if (!advisory) if (!advisory)
return db::Advisory::UnSet; return db::Advisory::UnSet;
switch (advisory.value()) switch (advisory.value())
{ {
case metadata::Track::Advisory::Clean: case Track::Advisory::Clean:
return db::Advisory::Clean; return db::Advisory::Clean;
case metadata::Track::Advisory::Explicit: case Track::Advisory::Explicit:
return db::Advisory::Explicit; return db::Advisory::Explicit;
case metadata::Track::Advisory::Unknown: case Track::Advisory::Unknown:
return db::Advisory::Unknown; return db::Advisory::Unknown;
} }
return db::Advisory::UnSet; 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; db::Track::FindParameters params;
// Add as many fields as possible to limit errors // Add as many fields as possible to limit errors
@@ -436,9 +439,9 @@ namespace lms::scanner
return res; 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()) 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) 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 // first pass: collect all artists that have mbids
std::unordered_map<std::string_view, core::UUID> artistsWithMbid; std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
@@ -485,9 +488,10 @@ namespace lms::scanner
} }
} // namespace } // 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 } : FileScanOperationBase{ std::move(fileToScan), db, settings }
, _parser{ parser } , _metadataParser{ metadataParser }
, _parserOptions{ parserOptions }
{ {
} }
@@ -495,17 +499,20 @@ namespace lms::scanner
void AudioFileScanOperation::scan() void AudioFileScanOperation::scan()
{ {
std::unique_ptr<metadata::Track> track;
try 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 // We fill missing artist mbids with mbids found on other artist roles
fillMissingMbids(*_parsedTrack); fillMissingMbids(_file->track);
std::size_t index{}; std::size_t index{};
_parser.parseImages(getFilePath(), [&](const metadata::Image& image) { audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) {
try try
{ {
image::ImageProperties properties{ image::probeImage(image.data) }; image::ImageProperties properties{ image::probeImage(image.data) };
@@ -522,7 +529,7 @@ namespace lms::scanner
info.description = image.description; info.description = image.description;
info.properties = properties; info.properties = properties;
_parsedImages.push_back(std::move(info)); _file->images.push_back(std::move(info));
} }
catch (const image::Exception& e) catch (const image::Exception& e)
{ {
@@ -532,15 +539,15 @@ namespace lms::scanner
index++; index++;
}); });
} }
catch (const metadata::AudioFileNoAudioPropertiesException&) catch (const audio::AudioFileNoAudioPropertiesException&)
{ {
addError<NoAudioTrackFoundError>(getFilePath()); addError<NoAudioTrackFoundError>(getFilePath());
} }
catch (const metadata::IOException& e) catch (const audio::IOException& e)
{ {
addError<IOScanError>(getFilePath(), e.getErrorCode()); addError<IOScanError>(getFilePath(), e.getErrorCode());
} }
catch (const metadata::Exception& e) catch (const audio::Exception& e)
{ {
addError<AudioFileScanError>(getFilePath()); addError<AudioFileScanError>(getFilePath());
} }
@@ -552,7 +559,7 @@ namespace lms::scanner
db::Session& dbSession{ getDb().getTLSSession() }; db::Session& dbSession{ getDb().getTLSSession() };
db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) }; db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) };
if (!_parsedTrack) if (!_file)
{ {
if (track) if (track)
{ {
@@ -562,9 +569,9 @@ namespace lms::scanner
return OperationResult::Skipped; 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 // find for an existing track MBID as the file may have just been moved
if (!track && duplicateTracks.size() == 1) if (!track && duplicateTracks.size() == 1)
@@ -616,7 +623,7 @@ namespace lms::scanner
if (!track) if (!track)
{ {
// maybe the file just moved? // maybe the file just moved?
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize()); track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize());
if (track) if (track)
{ {
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath()); 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 // 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()); addError<BadAudioDurationError>(getFilePath());
@@ -639,8 +646,8 @@ namespace lms::scanner
// ***** Title // ***** Title
std::string title; std::string title;
if (!_parsedTrack->title.empty()) if (!_file->track.title.empty())
title = _parsedTrack->title; title = _file->track.title;
else else
{ {
// TODO parse file name to guess track etc. // TODO parse file name to guess track etc.
@@ -665,18 +672,18 @@ namespace lms::scanner
track.modify()->setScanVersion(getScannerSettings().audioScanVersion); track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
// Audio properties // Audio properties
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate); track.modify()->setBitrate(_file->audioProperties.bitrate ? *_file->audioProperties.bitrate : 0);
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample); track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0);
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount); track.modify()->setChannelCount(_file->audioProperties.channelCount ? *_file->audioProperties.channelCount : 0);
track.modify()->setDuration(_parsedTrack->audioProperties.duration); track.modify()->setDuration(_file->audioProperties.duration);
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate); track.modify()->setSampleRate(_file->audioProperties.sampleRate ? *_file->audioProperties.sampleRate : 0);
track.modify()->setFileSize(getFileSize()); track.modify()->setFileSize(getFileSize());
track.modify()->setLastWriteTime(getLastWriteTime()); 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::WDate date;
Wt::WTime time; Wt::WTime time;
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day) if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
@@ -696,61 +703,61 @@ namespace lms::scanner
track.modify()->clearArtistLinks(); track.modify()->clearArtistLinks();
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
if (_parsedTrack->medium && _parsedTrack->medium->release) if (_file->track.medium && _file->track.medium->release)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback); 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::Conductor, _file->track.conductorArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, 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); 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 // 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); assert(release);
track.modify()->setRelease(release); track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release)); track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
} }
else else
{ {
track.modify()->setRelease({}); track.modify()->setRelease({});
track.modify()->setMedium({}); track.modify()->setMedium({});
} }
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setTrackNumber(_parsedTrack->position); track.modify()->setTrackNumber(_file->track.position);
track.modify()->setDate(_parsedTrack->date); track.modify()->setDate(_file->track.date);
track.modify()->setOriginalDate(_parsedTrack->originalDate); track.modify()->setOriginalDate(_file->track.originalDate);
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) if (!track->getOriginalDate().isValid() && _file->track.originalYear)
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear }); track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear });
// If a file has an OriginalDate but no date, set it to ease filtering // If a file has an OriginalDate but no date, set it to ease filtering
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid()) if (!_file->track.date.isValid() && _file->track.originalDate.isValid())
track.modify()->setDate(_parsedTrack->originalDate); track.modify()->setDate(_file->track.originalDate);
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID); track.modify()->setRecordingMBID(_file->track.recordingMBID);
track.modify()->setTrackMBID(_parsedTrack->mbid); track.modify()->setTrackMBID(_file->track.mbid);
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
trackFeatures.remove(); // TODO: only if MBID changed? trackFeatures.remove(); // TODO: only if MBID changed?
track.modify()->setCopyright(_parsedTrack->copyright); track.modify()->setCopyright(_file->track.copyright);
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); track.modify()->setCopyrightURL(_file->track.copyrightURL);
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); track.modify()->setAdvisory(getAdvisory(_file->track.advisory));
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now
track.modify()->setReplayGain(_parsedTrack->replayGain); track.modify()->setReplayGain(_file->track.replayGain);
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); track.modify()->setArtistDisplayName(_file->track.artistDisplayName);
track.modify()->clearEmbeddedLyrics(); track.modify()->clearEmbeddedLyrics();
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics) for (const Lyrics& lyricsInfo : _file->track.lyrics)
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo)); track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
updateEmbeddedImages(dbSession, track, _parsedImages); updateEmbeddedImages(dbSession, track, _file->images);
if (added) if (added)
{ {
@@ -19,33 +19,32 @@
#pragma once #pragma once
#include "IFileScanOperation.hpp"
#include <memory>
#include <vector> #include <vector>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "image/Types.hpp" #include "image/Types.hpp"
#include "metadata/Types.hpp"
#include "FileScanOperationBase.hpp" #include "scanners/FileScanOperationBase.hpp"
#include "FileToScan.hpp" #include "scanners/FileToScan.hpp"
#include "scanners/IFileScanOperation.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::db namespace lms::db
{ {
class IDb; class IDb;
} // namespace lms::db } // namespace lms::db
namespace lms::metadata
{
class IAudioFileParser;
} // namespace lms::metadata
namespace lms::scanner namespace lms::scanner
{ {
class TrackMetadataParser;
struct ImageInfo struct ImageInfo
{ {
std::size_t index; 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::uint64_t hash{};
std::size_t size{}; std::size_t size{};
image::ImageProperties properties; image::ImageProperties properties;
@@ -56,7 +55,7 @@ namespace lms::scanner
class AudioFileScanOperation : public FileScanOperationBase class AudioFileScanOperation : public FileScanOperationBase
{ {
public: public:
AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser); AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions);
~AudioFileScanOperation() override; ~AudioFileScanOperation() override;
AudioFileScanOperation(const AudioFileScanOperation&) = delete; AudioFileScanOperation(const AudioFileScanOperation&) = delete;
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete; AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
@@ -66,9 +65,15 @@ namespace lms::scanner
void scan() override; void scan() override;
OperationResult processResult() override; OperationResult processResult() override;
metadata::IAudioFileParser& _parser; const TrackMetadataParser& _metadataParser;
std::unique_ptr<metadata::Track> _parsedTrack; const audio::ParserOptions& _parserOptions;
std::vector<ImageInfo> _parsedImages;
};
struct AudioFileInfo
{
audio::AudioProperties audioProperties;
Track track;
std::vector<ImageInfo> images;
};
std::optional<AudioFileInfo> _file;
};
} // namespace lms::scanner } // namespace lms::scanner
@@ -21,53 +21,64 @@
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "metadata/IAudioFileParser.hpp"
#include "AudioFileScanOperation.hpp"
#include "ScannerSettings.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 lms::scanner
{ {
namespace namespace
{ {
metadata::ParserReadStyle getParserReadStyle() audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle()
{ {
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") }; std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
if (readStyle == "fast") if (readStyle == "fast")
return metadata::ParserReadStyle::Fast; return audio::ParserOptions::AudioPropertiesReadStyle::Fast;
if (readStyle == "average") if (readStyle == "average")
return metadata::ParserReadStyle::Average; return audio::ParserOptions::AudioPropertiesReadStyle::Average;
if (readStyle == "accurate") if (readStyle == "accurate")
return metadata::ParserReadStyle::Accurate; return audio::ParserOptions::AudioPropertiesReadStyle::Accurate;
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; 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.userExtraTags = settings.extraTags;
params.artistTagDelimiters = settings.artistTagDelimiters; params.artistTagDelimiters = settings.artistTagDelimiters;
params.defaultTagDelimiters = settings.defaultTagDelimiters; params.defaultTagDelimiters = settings.defaultTagDelimiters;
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end()); params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
params.backend = metadata::ParserBackend::TagLib;
params.readStyle = getParserReadStyle();
return params; return params;
} }
audio::ParserOptions createAudioFileParserOptions()
{
audio::ParserOptions options;
options.readStyle = getParserReadStyle();
options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib
return options;
}
} // namespace } // namespace
AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings) AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings)
: _db{ db } : _db{ db }
, _settings{ settings } , _settings{ settings }
, _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 std::span<const std::filesystem::path> AudioFileScanner::getSupportedExtensions() const
{ {
return _metadataParser->getSupportedExtensions(); return audio::getSupportedExtensions(_parserOptions.parser);
} }
bool AudioFileScanner::needsScan(const FileToScan& file) const bool AudioFileScanner::needsScan(const FileToScan& file) const
@@ -101,6 +112,6 @@ namespace lms::scanner
std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const std::unique_ptr<IFileScanOperation> AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const
{ {
return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, *_metadataParser); return std::make_unique<AudioFileScanOperation>(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions);
} }
} // namespace lms::scanner } // namespace lms::scanner
@@ -19,7 +19,10 @@
#pragma once #pragma once
#include "IFileScanner.hpp" #include "audio/IAudioFileInfo.hpp"
#include "scanners/IFileScanner.hpp"
#include "scanners/audiofile/TrackMetadataParser.hpp"
namespace lms namespace lms
{ {
@@ -55,6 +58,7 @@ namespace lms::scanner
db::IDb& _db; db::IDb& _db;
const ScannerSettings& _settings; const ScannerSettings& _settings;
std::unique_ptr<metadata::IAudioFileParser> _metadataParser; const TrackMetadataParser _trackMetadataParser;
const audio::ParserOptions _parserOptions;
}; };
} // namespace lms::scanner } // namespace lms::scanner
@@ -17,30 +17,26 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "AudioFileParser.hpp" #include "TrackMetadataParser.hpp"
#include <span> #include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "audio/IAudioFileInfo.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/PartialDateTime.hpp" #include "core/PartialDateTime.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "scanners/lyrics/LyricsParser.hpp"
#include "Utils.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 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) { tagReader.visitTagValues(tagType, [&](std::string_view value) {
auto visitTagIfNonEmpty{ [&](std::string_view tag) { auto visitTagIfNonEmpty{ [&](std::string_view tag) {
@@ -76,11 +72,11 @@ namespace lms::metadata
} }
template<typename T> 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; std::vector<T> res;
for (const TagType tagType : tagTypes) for (const audio::TagType tagType : tagTypes)
{ {
tagReader.visitTagValues(tagType, [&](std::string_view value) { tagReader.visitTagValues(tagType, [&](std::string_view value) {
value = core::stringUtils::stringTrim(value); value = core::stringUtils::stringTrim(value);
@@ -150,7 +146,7 @@ namespace lms::metadata
} }
template<typename T> 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::optional<T> res;
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) }; std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
@@ -161,45 +157,38 @@ namespace lms::metadata
} }
template<typename T> 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); return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
} }
template<typename T> 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 }); return getTagValueFirstMatchAs<T>(tagReader, { tagType });
} }
std::vector<Lyrics> getLyrics(const ITagReader& tagReader) std::vector<Lyrics> getLyrics(const audio::ITagReader& tagReader)
{ {
std::vector<Lyrics> res; std::vector<Lyrics> res;
tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) { tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) {
std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?) std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?)
try Lyrics lyrics{ parseLyrics(iss) };
{ if (lyrics.language.empty())
Lyrics lyrics{ parseLyrics(iss) }; lyrics.language = language;
if (lyrics.language.empty())
lyrics.language = language;
res.emplace_back(std::move(lyrics)); res.emplace_back(std::move(lyrics));
}
catch (const LyricsException& e)
{
LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() });
}
}); });
return res; return res;
} }
std::vector<Artist> getArtists(const ITagReader& tagReader, std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
std::initializer_list<TagType> artistTagNames, std::initializer_list<audio::TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames, std::initializer_list<audio::TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames, std::initializer_list<audio::TagType> artistMBIDTagNames,
const AudioFileParserParameters& params) const TrackMetadataParser::Parameters& params)
{ {
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, &params.artistsToNotSplit) }; std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, &params.artistsToNotSplit) };
if (artistNames.empty()) if (artistNames.empty())
@@ -224,7 +213,7 @@ namespace lms::metadata
return artists; return artists;
} }
PerformerContainer getPerformerArtists(const ITagReader& tagReader) PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader)
{ {
PerformerContainer performers; PerformerContainer performers;
@@ -298,9 +287,9 @@ namespace lms::metadata
return artistDisplayName; 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) switch (*value)
{ {
@@ -318,92 +307,24 @@ namespace lms::metadata
} }
} // namespace } // namespace
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params) TrackMetadataParser::TrackMetadataParser(const Parameters& params)
{
return std::make_unique<AudioFileParser>(params);
}
AudioFileParser::AudioFileParser(const AudioFileParserParameters& params)
: _params{ 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) Track track;
{ processTags(tagReader, track);
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);
return 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.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID); track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID); track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
@@ -469,8 +390,10 @@ namespace lms::metadata
track.originalYear = track.originalDate.getYear(); 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; std::optional<Medium> medium;
medium.emplace(); medium.emplace();
@@ -499,8 +422,10 @@ namespace lms::metadata
return medium; 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; std::optional<Release> release;
auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) }; auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) };
@@ -536,11 +461,4 @@ namespace lms::metadata
return release; return release;
} }
} // namespace lms::scanner
void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor)
{
reader.visitImages([&](const Image& image) {
visitor(image);
});
}
} // namespace lms::metadata
@@ -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 <sstream>
#include <string_view> #include <string_view>
#include "core/Exception.hpp" namespace lms::scanner::utils
namespace lms::metadata::utils
{ {
Wt::WDate parseDate(std::string_view dateStr) Wt::WDate parseDate(std::string_view dateStr)
{ {
@@ -96,21 +94,6 @@ namespace lms::metadata::utils
return result * sign; 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) PerformerArtist extractPerformerAndRole(std::string_view entry)
{ {
std::string_view artistName; std::string_view artistName;
@@ -155,4 +138,4 @@ namespace lms::metadata::utils
return PerformerArtist{ Artist{ artistName }, std::string{ role } }; return PerformerArtist{ Artist{ artistName }, std::string{ role } };
} }
} // namespace lms::metadata::utils } // namespace lms::scanner::utils
@@ -25,13 +25,12 @@
#include <Wt/WDate.h> #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); Wt::WDate parseDate(std::string_view dateStr);
std::optional<int> parseYear(std::string_view yearStr); std::optional<int> parseYear(std::string_view yearStr);
std::string_view readStyleToString(ParserReadStyle readStyle);
struct PerformerArtist struct PerformerArtist
{ {
@@ -41,4 +40,4 @@ namespace lms::metadata::utils
// format is "artist name (role)" // format is "artist name (role)"
PerformerArtist extractPerformerAndRole(std::string_view entry); PerformerArtist extractPerformerAndRole(std::string_view entry);
} // namespace lms::metadata::utils } // namespace lms::scanner::utils
@@ -22,17 +22,19 @@
#include <fstream> #include <fstream>
#include <optional> #include <optional>
#include "FileScanOperationBase.hpp"
#include "ScannerSettings.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "metadata/Lyrics.hpp"
#include "services/scanner/ScanErrors.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 namespace lms::scanner
{ {
@@ -48,28 +50,21 @@ namespace lms::scanner
void scan() override; void scan() override;
OperationResult processResult() override; OperationResult processResult() override;
std::optional<metadata::Lyrics> _parsedLyrics; std::optional<Lyrics> _parsedLyrics;
}; };
void LyricsFileScanOperation::scan() void LyricsFileScanOperation::scan()
{ {
try std::ifstream ifs{ getFilePath() };
if (!ifs)
{ {
std::ifstream ifs{ getFilePath() }; const std::error_code ec{ errno, std::generic_category() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec); addError<IOScanError>(getFilePath(), ec);
return; return;
} }
_parsedLyrics = metadata::parseLyrics(ifs); _parsedLyrics = parseLyrics(ifs);
}
catch (const metadata::Exception& e)
{
addError<LyricsFileScanError>(getFilePath());
}
} }
LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult() LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult()
@@ -140,7 +135,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const std::span<const std::filesystem::path> LyricsFileScanner::getSupportedExtensions() const
{ {
return metadata::getSupportedLyricsFileExtensions(); return getSupportedLyricsFileExtensions();
} }
bool LyricsFileScanner::needsScan(const FileToScan& file) const bool LyricsFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once #pragma once
#include "IFileScanner.hpp" #include "scanners/IFileScanner.hpp"
namespace lms::db namespace lms::db
{ {
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "metadata/Lyrics.hpp" #include "LyricsParser.hpp"
#include <cassert> #include <cassert>
#include <regex> #include <regex>
#include "core/String.hpp" #include "core/String.hpp"
namespace lms::metadata namespace lms::scanner
{ {
std::span<const std::filesystem::path> getSupportedLyricsFileExtensions() std::span<const std::filesystem::path> getSupportedLyricsFileExtensions()
{ {
@@ -227,4 +227,4 @@ namespace lms::metadata
return lyrics; 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/Session.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "metadata/Exception.hpp"
#include "metadata/PlayList.hpp"
#include "FileScanOperationBase.hpp" #include "services/scanner/ScanErrors.hpp"
#include "ScanContext.hpp"
#include "Utils.hpp" #include "scanners/FileScanOperationBase.hpp"
#include "scanners/Utils.hpp"
#include "scanners/playlist/PlayListParser.hpp"
namespace lms::scanner namespace lms::scanner
{ {
@@ -51,28 +51,21 @@ namespace lms::scanner
void scan() override; void scan() override;
OperationResult processResult() override; OperationResult processResult() override;
std::optional<metadata::PlayList> _parsedPlayList; std::optional<PlayList> _parsedPlayList;
}; };
void PlayListFileScanOperation::scan() void PlayListFileScanOperation::scan()
{ {
try std::ifstream ifs{ getFilePath() };
if (!ifs)
{ {
std::ifstream ifs{ getFilePath() }; const std::error_code ec{ errno, std::generic_category() };
if (!ifs)
{
const std::error_code ec{ errno, std::generic_category() };
addError<IOScanError>(getFilePath(), ec); addError<IOScanError>(getFilePath(), ec);
return; return;
} }
_parsedPlayList = metadata::parsePlayList(ifs); _parsedPlayList = parsePlayList(ifs);
}
catch (const metadata::Exception& e)
{
addError<PlayListFileScanError>(getFilePath());
}
} }
PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult() PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult()
@@ -139,7 +132,7 @@ namespace lms::scanner
std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const std::span<const std::filesystem::path> PlayListFileScanner::getSupportedExtensions() const
{ {
return metadata::getSupportedPlayListFileExtensions(); return getSupportedPlayListFileExtensions();
} }
bool PlayListFileScanner::needsScan(const FileToScan& file) const bool PlayListFileScanner::needsScan(const FileToScan& file) const
@@ -19,7 +19,7 @@
#pragma once #pragma once
#include "IFileScanner.hpp" #include "scanners/IFileScanner.hpp"
namespace lms::db namespace lms::db
{ {
@@ -17,14 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "metadata/PlayList.hpp" #include "PlayListParser.hpp"
#include <array> #include <array>
#include <string_view> #include <string_view>
#include "core/String.hpp" #include "core/String.hpp"
namespace lms::metadata namespace lms::scanner
{ {
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions() std::span<const std::filesystem::path> getSupportedPlayListFileExtensions()
{ {
@@ -102,4 +102,4 @@ namespace lms::metadata
return playlist; 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/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
#include "metadata/Types.hpp"
#include "ScanContext.hpp" #include "ScanContext.hpp"
#include "ScannerSettings.hpp" #include "ScannerSettings.hpp"
#include "helpers/ArtistHelpers.hpp" #include "helpers/ArtistHelpers.hpp"
#include "types/TrackMetadata.hpp"
namespace lms::scanner namespace lms::scanner
{ {
@@ -53,7 +53,7 @@ namespace lms::scanner
{ {
assert(!link->isArtistMBIDMatched()); 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 }) }; 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); 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()); 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 }) }; 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); LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist);
@@ -19,17 +19,12 @@
#pragma once #pragma once
#include <filesystem>
#include <iosfwd>
#include <optional> #include <optional>
#include <span>
#include <string> #include <string>
#include "core/UUID.hpp" #include "core/UUID.hpp"
#include "metadata/Exception.hpp" namespace lms::scanner
namespace lms::metadata
{ {
// See: // See:
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists // - 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 disambiguation; // mb
std::string biography; std::string biography;
}; };
} // namespace lms::scanner
class ArtistInfoParseException : public Exception
{
public:
using Exception::Exception;
};
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
ArtistInfo parseArtistInfo(std::istream& is);
} // namespace lms::metadata
@@ -20,16 +20,11 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <filesystem>
#include <iosfwd>
#include <map> #include <map>
#include <span>
#include <string> #include <string>
#include <vector> #include <vector>
#include "metadata/Exception.hpp" namespace lms::scanner
namespace lms::metadata
{ {
struct Lyrics struct Lyrics
{ {
@@ -42,13 +37,4 @@ namespace lms::metadata
std::map<std::chrono::milliseconds, std::string> synchronizedLines; std::map<std::chrono::milliseconds, std::string> synchronizedLines;
std::vector<std::string> unsynchronizedLines; std::vector<std::string> unsynchronizedLines;
}; };
} // namespace lms::scanner
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
@@ -20,19 +20,14 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <iosfwd>
#include <span>
#include <string> #include <string>
#include <vector> #include <vector>
namespace lms::metadata namespace lms::scanner
{ {
struct PlayList struct PlayList
{ {
std::string name; std::string name;
std::vector<std::filesystem::path> files; std::vector<std::filesystem::path> files;
}; };
} // namespace lms::scanner
std::span<const std::filesystem::path> getSupportedPlayListFileExtensions();
PlayList parsePlayList(std::istream& is);
} // namespace lms::metadata
@@ -19,11 +19,8 @@
#pragma once #pragma once
#include <chrono>
#include <map> #include <map>
#include <optional> #include <optional>
#include <set>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -31,9 +28,9 @@
#include "core/PartialDateTime.hpp" #include "core/PartialDateTime.hpp"
#include "core/UUID.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 */>; 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 struct Track
{ {
enum class Advisory enum class Advisory
@@ -111,7 +99,7 @@ namespace lms::metadata
Explicit, Explicit,
Clean, Clean,
}; };
AudioProperties audioProperties;
std::optional<core::UUID> mbid; std::optional<core::UUID> mbid;
std::optional<core::UUID> recordingMBID; std::optional<core::UUID> recordingMBID;
std::string title; std::string title;
@@ -143,96 +131,4 @@ namespace lms::metadata
std::vector<Artist> producerArtists; std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists; std::vector<Artist> remixerArtists;
}; };
} // namespace lms::scanner
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
@@ -20,9 +20,9 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "metadata/ArtistInfo.hpp" #include "scanners/artistinfo/ArtistInfoParser.hpp"
namespace lms::metadata::tests namespace lms::scanner::tests
{ {
TEST(ArtistInfo, basic) 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.sortName, "Artist, My");
ASSERT_EQ(artistInfo.disambiguation, "My Artist"); ASSERT_EQ(artistInfo.disambiguation, "My Artist");
} }
} // namespace lms::metadata::tests } // namespace lms::scanner::tests
@@ -20,9 +20,9 @@
#include <gtest/gtest.h> #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) TEST(MetaData, parseDate)
{ {
@@ -149,4 +149,4 @@ namespace lms::metadata::utils::tests
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; 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 <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; 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[3], "I, I just woke up from a dream");
EXPECT_EQ(lyrics.unsynchronizedLines[4], ""); EXPECT_EQ(lyrics.unsynchronizedLines[4], "");
} }
} // namespace lms::metadata::tests } // namespace lms::scanner::tests
@@ -21,11 +21,11 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <sstream> #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 std::istringstream is{ R"(#EXTM3U
#PLAYLIST:My super playlist #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"); 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' }; 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)) }; 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"); 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. * This file is part of LMS.
* *
@@ -21,22 +21,14 @@
#include <memory> #include <memory>
#include <vector> #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: public:
static constexpr AudioProperties audioProperties{ using Tags = std::unordered_map<audio::TagType, std::vector<std::string_view>>;
.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 Performers = std::unordered_map<std::string_view /*role*/, std::vector<std::string_view> /*names*/>; 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 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*/>; 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); _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) }; auto itValues{ _tags.find(tag) };
if (itValues != std::cend(_tags)) if (itValues != std::cend(_tags))
@@ -97,8 +89,6 @@ namespace lms::metadata::tests
visitor(language, lyrics); visitor(language, lyrics);
} }
const AudioProperties& getAudioProperties() const override { return audioProperties; }
private: private:
const Tags _tags; const Tags _tags;
Performers _performers; Performers _performers;
@@ -106,8 +96,10 @@ namespace lms::metadata::tests
LyricsTags _lyricsTags; 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>( std::unique_ptr<TestTagReader> testTags{ std::make_unique<TestTagReader>(
TestTagReader::Tags{ TestTagReader::Tags{
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
@@ -168,4 +160,4 @@ namespace lms::metadata::tests
return testTags; 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 add_library(lmstranscoding STATIC
impl/TranscodingResourceHandler.cpp impl/TranscodeResourceHandler.cpp
impl/TranscodingService.cpp impl/TranscodeService.cpp
) )
target_include_directories(lmstranscoding INTERFACE target_include_directories(lmstranscoding INTERFACE
@@ -13,7 +13,7 @@ target_include_directories(lmstranscoding PRIVATE
) )
target_link_libraries(lmstranscoding PRIVATE target_link_libraries(lmstranscoding PRIVATE
lmsav lmsaudio
) )
target_link_libraries(lmstranscoding PUBLIC target_link_libraries(lmstranscoding PUBLIC
@@ -17,42 +17,38 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * 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 "core/ILogger.hpp"
#include "audio/Exception.hpp"
#include "audio/ITranscoder.hpp"
namespace lms::transcoding 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 // 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 } : _estimatedContentLength{ estimatedContentLength }
{ {
try try
{ {
_transcoder = av::createTranscoder(inputParameters, outputParameters); _transcoder = createTranscoder(parameters);
if (_estimatedContentLength) if (_estimatedContentLength)
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
else else
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length"); 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()); 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) if (!_transcoder)
{ {
@@ -23,19 +23,19 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include "av/ITranscoder.hpp" #include "audio/ITranscoder.hpp"
#include "core/IResourceHandler.hpp" #include "core/IResourceHandler.hpp"
namespace lms::transcoding namespace lms::transcoding
{ {
class TranscodingResourceHandler final : public core::IResourceHandler class ResourceHandler final : public core::IResourceHandler
{ {
public: public:
TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional<std::size_t> estimatedContentLength); ResourceHandler(const audio::TranscodeParameters& parameters, std::optional<std::size_t> estimatedContentLength);
~TranscodingResourceHandler() override; ~ResourceHandler() override;
TranscodingResourceHandler(const TranscodingResourceHandler&) = delete; ResourceHandler(const ResourceHandler&) = delete;
TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete; ResourceHandler& operator=(const ResourceHandler&) = delete;
private: private:
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; 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::array<std::byte, _chunkSize> _buffer;
std::size_t _bytesReadyCount{}; std::size_t _bytesReadyCount{};
std::size_t _totalServedByteCount{}; std::size_t _totalServedByteCount{};
std::unique_ptr<av::ITranscoder> _transcoder; std::unique_ptr<audio::ITranscoder> _transcoder;
}; };
} // namespace lms::transcoding } // namespace lms::transcoding
@@ -17,24 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * 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/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "TranscodingResourceHandler.hpp" #include "TranscodeResourceHandler.hpp"
namespace lms::transcoding namespace lms::transcoding
{ {
namespace 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) 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) }; const std::size_t estimatedContentLength{ static_cast<size_t>((bitrate / 8 * duration.count()) / 1000) };
@@ -42,40 +38,35 @@ namespace lms::transcoding
} }
} // namespace } // 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 } : _db{ db }
, _childProcessManager(childProcessManager) , _childProcessManager(childProcessManager)
{ {
LMS_LOG(TRANSCODING, INFO, "Service started!"); LMS_LOG(TRANSCODING, INFO, "Service started!");
} }
TranscodingService::~TranscodingService() TranscodeService::~TranscodeService()
{ {
LMS_LOG(TRANSCODING, INFO, "Service stopped!"); 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; std::optional<std::size_t> estimatedContentLength;
avInputParams.file = inputParameters.filePath;
avInputParams.offset = inputParameters.offset;
avInputParams.streamIndex = inputParameters.streamIndex;
if (estimateContentLength) if (estimateContentLength)
{ {
if (inputParameters.offset < inputParameters.duration) if (parameters.inputParameters.offset < parameters.inputParameters.duration)
estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, inputParameters.duration - inputParameters.offset); estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset);
else 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 } // namespace lms::transcoding
@@ -19,21 +19,21 @@
#pragma once #pragma once
#include "services/transcoding/ITranscodingService.hpp" #include "services/transcoding/ITranscodeService.hpp"
namespace lms::transcoding namespace lms::transcoding
{ {
class TranscodingService : public ITranscodingService class TranscodeService : public ITranscodeService
{ {
public: public:
explicit TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager); explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager);
~TranscodingService() override; ~TranscodeService() override;
TranscodingService(const TranscodingService&) = delete; TranscodeService(const TranscodeService&) = delete;
TranscodingService& operator=(const TranscodingService&) = delete; TranscodeService& operator=(const TranscodeService&) = delete;
private: 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; db::IDb& _db;
core::IChildProcessManager& _childProcessManager; core::IChildProcessManager& _childProcessManager;
@@ -19,18 +19,13 @@
#pragma once #pragma once
#include <functional> #include "core/Exception.hpp"
#include "metadata/Types.hpp" namespace lms::transcoding
namespace lms::metadata
{ {
class IImageReader class Exception : public core::LmsException
{ {
public: public:
virtual ~IImageReader() = default; using LmsException::LmsException;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(ImageVisitor visitor) const = 0;
}; };
} // 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