diff --git a/README.md b/README.md index b069afa2..d4344ff7 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ __Note__: If no name is provided in the `artist.nfo` file, the name of the conta ### Filtering It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings. -__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_. +__Note__: You can use the `lms-audioinfo` tool to get an idea of the tags parsed by _LMS_. ### Multiple artists _LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved. diff --git a/src/libs/CMakeLists.txt b/src/libs/CMakeLists.txt index 72a280ca..9a52d2f5 100644 --- a/src/libs/CMakeLists.txt +++ b/src/libs/CMakeLists.txt @@ -1,8 +1,7 @@ -add_subdirectory(av) +add_subdirectory(audio) add_subdirectory(core) add_subdirectory(database) add_subdirectory(image) -add_subdirectory(metadata) add_subdirectory(services) add_subdirectory(som) add_subdirectory(subsonic) diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt new file mode 100644 index 00000000..48f071bb --- /dev/null +++ b/src/libs/audio/CMakeLists.txt @@ -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 + ) diff --git a/src/libs/audio/impl/AudioTypes.cpp b/src/libs/audio/impl/AudioTypes.cpp new file mode 100644 index 00000000..433ed0b6 --- /dev/null +++ b/src/libs/audio/impl/AudioTypes.cpp @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/audio/impl/ImageReader.cpp b/src/libs/audio/impl/ImageReader.cpp new file mode 100644 index 00000000..7b314875 --- /dev/null +++ b/src/libs/audio/impl/ImageReader.cpp @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/audio/impl/ParseAudioFileInfo.cpp b/src/libs/audio/impl/ParseAudioFileInfo.cpp new file mode 100644 index 00000000..321d46a7 --- /dev/null +++ b/src/libs/audio/impl/ParseAudioFileInfo.cpp @@ -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 . + */ + +#include + +#include "audio/IAudioFileInfo.hpp" + +#include "ffmpeg/AudioFileInfo.hpp" +#include "ffmpeg/Utils.hpp" +#include "taglib/AudioFileInfo.hpp" +#include "taglib/Utils.hpp" + +namespace lms::audio +{ + std::unique_ptr parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions) + { + switch (parserOptions.parser) + { + case ParserOptions::Parser::TagLib: + return std::make_unique(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs); + case ParserOptions::Parser::FFmpeg: + return std::make_unique(p, parserOptions.enableExtraDebugLogs); + } + + return {}; + } + + std::span getSupportedExtensions(ParserOptions::Parser parser) + { + switch (parser) + { + case ParserOptions::Parser::TagLib: + return taglib::utils::getSupportedExtensions(); + case ParserOptions::Parser::FFmpeg: + return ffmpeg::utils::getSupportedExtensions(); + } + + return {}; + } + +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/impl/TagReader.cpp b/src/libs/audio/impl/TagReader.cpp new file mode 100644 index 00000000..39ef83bd --- /dev/null +++ b/src/libs/audio/impl/TagReader.cpp @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/audio/impl/ffmpeg/AudioFile.cpp similarity index 66% rename from src/libs/av/impl/AudioFile.cpp rename to src/libs/audio/impl/ffmpeg/AudioFile.cpp index 8c3ee0d2..7a72e861 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/audio/impl/ffmpeg/AudioFile.cpp @@ -19,6 +19,9 @@ #include "AudioFile.hpp" +#include +#include + extern "C" { #define __STDC_CONSTANT_MACROS @@ -27,15 +30,13 @@ extern "C" #include } -#include -#include - #include "core/ILogger.hpp" #include "core/String.hpp" -#include "av/Exception.hpp" +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" -namespace lms::av +namespace lms::audio::ffmpeg { namespace { @@ -49,11 +50,11 @@ namespace lms::av return "Unknown error"; } - class AudioFileException : public Exception + class AudioFileException : public AudioFileParsingException { public: AudioFileException(int avError) - : Exception{ "AudioFileException: " + averror_to_string(avError) } + : AudioFileParsingException{ averror_to_string(avError) } { } }; @@ -70,76 +71,104 @@ namespace lms::av } } - DecodingCodec avcodecToDecodingCodec(AVCodecID codec) + std::optional 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 avcodecToCodecType(AVCodecID codec) { switch (codec) { case AV_CODEC_ID_MP3: - return DecodingCodec::MP3; + return CodecType::MP3; case AV_CODEC_ID_AAC: - return DecodingCodec::AAC; + return CodecType::AAC; case AV_CODEC_ID_AC3: - return DecodingCodec::AC3; + return CodecType::AC3; case AV_CODEC_ID_VORBIS: - return DecodingCodec::VORBIS; + return CodecType::Vorbis; case AV_CODEC_ID_WMAV1: - return DecodingCodec::WMAV1; + return CodecType::WMA1; case AV_CODEC_ID_WMAV2: - return DecodingCodec::WMAV2; + return CodecType::WMA2; + case AV_CODEC_ID_WMAPRO: + return CodecType::WMA9Pro; + case AV_CODEC_ID_WMALOSSLESS: + return CodecType::WMA9Lossless; case AV_CODEC_ID_FLAC: - return DecodingCodec::FLAC; + return CodecType::FLAC; case AV_CODEC_ID_ALAC: - return DecodingCodec::ALAC; + return CodecType::ALAC; case AV_CODEC_ID_WAVPACK: - return DecodingCodec::WAVPACK; + return CodecType::WavPack; case AV_CODEC_ID_MUSEPACK7: - return DecodingCodec::MUSEPACK7; + return CodecType::MPC7; case AV_CODEC_ID_MUSEPACK8: - return DecodingCodec::MUSEPACK8; + return CodecType::MPC8; case AV_CODEC_ID_APE: - return DecodingCodec::APE; + return CodecType::APE; case AV_CODEC_ID_EAC3: - return DecodingCodec::EAC3; + return CodecType::EAC3; case AV_CODEC_ID_MP4ALS: - return DecodingCodec::MP4ALS; + return CodecType::MP4ALS; case AV_CODEC_ID_OPUS: - return DecodingCodec::OPUS; + return CodecType::Opus; case AV_CODEC_ID_SHORTEN: - return DecodingCodec::SHORTEN; + return CodecType::Shorten; case AV_CODEC_ID_DSD_LSBF: - return DecodingCodec::DSD_LSBF; case AV_CODEC_ID_DSD_LSBF_PLANAR: - return DecodingCodec::DSD_LSBF_PLANAR; case AV_CODEC_ID_DSD_MSBF: - return DecodingCodec::DSD_MSBF; case AV_CODEC_ID_DSD_MSBF_PLANAR: - return DecodingCodec::DSD_MSBF_PLANAR; + return CodecType::DSD; default: - return DecodingCodec::UNKNOWN; + return std::nullopt; } } } // namespace - std::unique_ptr parseAudioFile(const std::filesystem::path& p) - { - return std::make_unique(p); - } - AudioFile::AudioFile(const std::filesystem::path& p) : _p{ p } { int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) }; if (error < 0) { - LMS_LOG(AV, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); + LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error)); throw AudioFileException{ error }; } error = avformat_find_stream_info(_context, nullptr); if (error < 0) { - LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error)); + LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error)); avformat_close_input(&_context); throw AudioFileException{ error }; } @@ -158,9 +187,12 @@ namespace lms::av ContainerInfo AudioFile::getContainerInfo() const { ContainerInfo info; + + info.container = avdemuxerToContainerType(_context->iformat->name); + info.containerName = _context->iformat->name; + info.bitrate = _context->bit_rate; info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 }; - info.name = _context->iformat->name; return info; } @@ -258,7 +290,7 @@ namespace lms::av if (avstream->codecpar == nullptr) { - LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set"); + LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set"); continue; } @@ -275,7 +307,7 @@ namespace lms::av else { picture.mimeType = "application/octet-stream"; - LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"); + LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"); } const AVPacket& pkt{ avstream->attached_pic }; @@ -297,7 +329,7 @@ namespace lms::av if (!avstream->codecpar) { - LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set"); + LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set"); return res; } @@ -305,19 +337,29 @@ namespace lms::av return res; res.emplace(); + res->index = streamIndex; - res->bitrate = static_cast(avstream->codecpar->bit_rate); - res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100) - res->channelCount = static_cast(avstream->codecpar->channels); -#else - res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); -#endif - res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); + res->codec = avcodecToCodecType(avstream->codecpar->codec_id); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); + + if (avstream->codecpar->bit_rate) + res->bitrate = static_cast(avstream->codecpar->bit_rate); + if (avstream->codecpar->bits_per_coded_sample) + res->bitsPerSample = static_cast(avstream->codecpar->bits_per_coded_sample); + else if (avstream->codecpar->bits_per_raw_sample) + res->bitsPerSample = static_cast(avstream->codecpar->bits_per_raw_sample); + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100) + if (avstream->codecpar->channels) + res->channelCount = static_cast(avstream->codecpar->channels); +#else + if (avstream->codecpar->ch_layout.nb_channels) + res->channelCount = static_cast(avstream->codecpar->ch_layout.nb_channels); +#endif assert(!res->codecName.empty()); // doc says it is never NULL - res->sampleRate = static_cast(avstream->codecpar->sample_rate); + if (avstream->codecpar->sample_rate) + res->sampleRate = static_cast(avstream->codecpar->sample_rate); return res; } -} // namespace lms::av +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFile.hpp b/src/libs/audio/impl/ffmpeg/AudioFile.hpp new file mode 100644 index 00000000..2af7fb1b --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFile.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "audio/AudioTypes.hpp" + +extern "C" +{ + struct AVFormatContext; +} + +namespace lms::audio::ffmpeg +{ + struct Picture + { + std::string mimeType; + std::span data; // valid as long as IAudioFile exists + }; + + struct ContainerInfo + { + std::optional container; + std::string containerName; + + std::size_t bitrate{}; + std::chrono::milliseconds duration{}; + }; + + struct StreamInfo + { + size_t index{}; + std::optional codec; + std::string codecName; + + std::optional bitrate; + std::optional bitsPerSample; + std::optional channelCount; + std::optional 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; + + const std::filesystem::path& getPath() const; + ContainerInfo getContainerInfo() const; + MetadataMap getMetaData() const; + std::vector getStreamInfo() const; + std::optional getBestStreamInfo() const; + std::optional getBestStreamIndex() const; + bool hasAttachedPictures() const; + void visitAttachedPictures(std::function func) const; + + private: + std::optional getStreamInfo(std::size_t streamIndex) const; + + const std::filesystem::path _p; + AVFormatContext* _context{}; + }; +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp new file mode 100644 index 00000000..f8c9f6c6 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.cpp @@ -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 . + */ + +#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(filePath) } + , _audioProperties{ std::make_unique(computeAudioProperties(*_audioFile)) } + , _tagReader{ std::make_unique(*_audioFile, enableExtraDebugLogs) } + , _imageReader{ std::make_unique(*_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 diff --git a/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp new file mode 100644 index 00000000..e8c806a7 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/AudioFileInfo.hpp @@ -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 . + */ + +#pragma once + +#include + +#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; + std::unique_ptr _audioProperties; + std::unique_ptr _tagReader; + std::unique_ptr _imageReader; + }; +} // namespace lms::audio::ffmpeg diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp b/src/libs/audio/impl/ffmpeg/ImageReader.cpp similarity index 61% rename from src/libs/metadata/impl/avformat/AvFormatImageReader.cpp rename to src/libs/audio/impl/ffmpeg/ImageReader.cpp index 8a420534..ad935d1e 100644 --- a/src/libs/metadata/impl/avformat/AvFormatImageReader.cpp +++ b/src/libs/audio/impl/ffmpeg/ImageReader.cpp @@ -17,35 +17,30 @@ * along with LMS. If not, see . */ -#include "AvFormatImageReader.hpp" +#include "ImageReader.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" -#include "metadata/Exception.hpp" +#include -namespace lms::metadata::avformat +#include "core/String.hpp" + +#include "AudioFile.hpp" + +namespace lms::audio::ffmpeg { - AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p) + ImageReader::ImageReader(const AudioFile& audioFile) + : _audioFile{ audioFile } { - try - { - _audioFile = av::parseAudioFile(p); - } - catch (av::Exception& e) - { - throw AudioFileParsingException{ e.what() }; - } } - AvFormatImageReader::~AvFormatImageReader() = default; + ImageReader::~ImageReader() = default; - void AvFormatImageReader::visitImages(ImageVisitor visitor) const + void ImageReader::visitImages(const ImageVisitor& visitor) const { - auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) { + auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) { return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); }); } }; - _audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) { + _audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) { Image image; image.data = picture.data; image.mimeType = picture.mimeType; @@ -58,4 +53,4 @@ namespace lms::metadata::avformat }); } -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/audio/impl/ffmpeg/ImageReader.hpp b/src/libs/audio/impl/ffmpeg/ImageReader.hpp new file mode 100644 index 00000000..27a24b30 --- /dev/null +++ b/src/libs/audio/impl/ffmpeg/ImageReader.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "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 diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp similarity index 86% rename from src/libs/metadata/impl/avformat/AvFormatTagReader.cpp rename to src/libs/audio/impl/ffmpeg/TagReader.cpp index fb7981c1..fa5d9e4f 100644 --- a/src/libs/metadata/impl/avformat/AvFormatTagReader.cpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp @@ -17,15 +17,12 @@ * along with LMS. If not, see . */ -#include "AvFormatTagReader.hpp" +#include "TagReader.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" #include "core/ILogger.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" -namespace lms::metadata::avformat +namespace lms::audio::ffmpeg { namespace { @@ -146,41 +143,20 @@ namespace lms::metadata::avformat }; } // namespace - AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug) + TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs) + : _audioFile{ audioFile } + , _metaDataMap{ audioFile.getMetaData() } { - try + if (enableExtraDebugLogs && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { - const auto audioFile{ av::parseAudioFile(p) }; - - _audioProperties.duration = audioFile->getContainerInfo().duration; - - const auto bestAudioStream{ audioFile->getBestStreamInfo() }; - if (bestAudioStream) - { - _audioProperties.bitrate = bestAudioStream->bitrate; - _audioProperties.bitsPerSample = bestAudioStream->bitsPerSample; - _audioProperties.channelCount = bestAudioStream->channelCount; - _audioProperties.sampleRate = bestAudioStream->sampleRate; - } - - _containerInfo = audioFile->getContainerInfo(); - _metaDataMap = audioFile->getMetaData(); - - if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) - { - for (const auto& [key, value] : _metaDataMap) - LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'"); - } - } - catch (av::Exception& e) - { - throw AudioFileParsingException{ e.what() }; + for (const auto& [key, value] : _metaDataMap) + LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'"); } } - AvFormatTagReader::~AvFormatTagReader() = default; + TagReader::~TagReader() = default; - void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ avFormatTagMapping.find(tag) }; if (itTagNames == std::cend(avFormatTagMapping)) @@ -200,7 +176,7 @@ namespace lms::metadata::avformat } } - void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const + void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const { auto itValues{ _metaDataMap.find(std::string{ key }) }; if (itValues == std::cend(_metaDataMap)) @@ -209,14 +185,14 @@ namespace lms::metadata::avformat visitor(itValues->second); } - void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const + void TagReader::visitPerformerTags(PerformerVisitor visitor) const { visitTagValues("PERFORMER", [&](std::string_view value) { visitor("", value); }); } - void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const + void TagReader::visitLyricsTags(LyricsVisitor visitor) const { // MPEG files: need to visit LYRICS-language entries for (const auto& [tag, value] : _metaDataMap) @@ -234,4 +210,4 @@ namespace lms::metadata::avformat visitor("", value); }); } -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp b/src/libs/audio/impl/ffmpeg/TagReader.hpp similarity index 61% rename from src/libs/metadata/impl/avformat/AvFormatTagReader.hpp rename to src/libs/audio/impl/ffmpeg/TagReader.hpp index 44b38c38..25e7bd7f 100644 --- a/src/libs/metadata/impl/avformat/AvFormatTagReader.hpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.hpp @@ -19,31 +19,27 @@ #pragma once -#include +#include "audio/ITagReader.hpp" -#include "av/IAudioFile.hpp" +#include "AudioFile.hpp" -#include "ITagReader.hpp" - -namespace lms::metadata::avformat +namespace lms::audio::ffmpeg { - class AvFormatTagReader : public ITagReader + class TagReader : public ITagReader { public: - AvFormatTagReader(const std::filesystem::path& path, bool debug); - ~AvFormatTagReader() override; - AvFormatTagReader(const AvFormatTagReader&) = delete; - AvFormatTagReader& operator=(const AvFormatTagReader&) = delete; + TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs); + ~TagReader() override; + TagReader(const TagReader&) = delete; + TagReader& operator=(const TagReader&) = delete; private: void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - const AudioProperties& getAudioProperties() const override { return _audioProperties; } - AudioProperties _audioProperties; - av::IAudioFile::MetadataMap _metaDataMap; - av::ContainerInfo _containerInfo; + const AudioFile& _audioFile; + AudioFile::MetadataMap _metaDataMap; }; -} // namespace lms::metadata::avformat +} // namespace lms::audio::ffmpeg diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/audio/impl/ffmpeg/Transcoder.cpp similarity index 58% rename from src/libs/av/impl/Transcoder.cpp rename to src/libs/audio/impl/ffmpeg/Transcoder.cpp index 8dc4d59d..cd6ea9d5 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.cpp @@ -27,17 +27,21 @@ #include "core/ILogger.hpp" #include "core/Service.hpp" -#include "av/Exception.hpp" +#include "audio/Exception.hpp" +#include "audio/TranscodeTypes.hpp" -namespace lms::av +namespace lms::audio +{ + std::unique_ptr createTranscoder(const TranscodeParameters& parameters) + { + return std::make_unique(parameters); + } +} // namespace lms::audio + +namespace lms::audio::ffmpeg { #define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message) - std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters) - { - return std::make_unique(inputParameters, outputParameters); - } - static std::atomic globalId{}; static std::filesystem::path ffmpegPath; @@ -48,10 +52,10 @@ namespace lms::av throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" }; } - Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams) + Transcoder::Transcoder(const TranscodeParameters& parameters) : _debugId{ globalId++ } - , _inputParams{ inputParams } - , _outputParams{ outputParams } + , _inputParams{ parameters.inputParameters } + , _outputParams{ parameters.outputParameters } { start(); } @@ -65,18 +69,18 @@ namespace lms::av try { - if (!std::filesystem::exists(_inputParams.file)) - throw Exception{ "File " + _inputParams.file.string() + " does not exist!" }; - if (!std::filesystem::is_regular_file(_inputParams.file)) - throw Exception{ "File " + _inputParams.file.string() + " is not regular!" }; + if (!std::filesystem::exists(_inputParams.filePath)) + throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" }; + if (!std::filesystem::is_regular_file(_inputParams.filePath)) + throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" }; } catch (const std::filesystem::filesystem_error& e) { // TODO store/raise e.code() - throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() }; + throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() }; } - LOG(INFO, "Transcoding file " << _inputParams.file); + LOG(INFO, "Transcoding file " << _inputParams.filePath); std::vector args; @@ -101,14 +105,7 @@ namespace lms::av // Input file args.emplace_back("-i"); - args.emplace_back(_inputParams.file.string()); - - // Stream mapping, if set - if (_inputParams.streamIndex) - { - args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex)); - } + args.emplace_back(_inputParams.filePath.string()); if (_outputParams.stripMetadata) { @@ -121,47 +118,53 @@ namespace lms::av args.emplace_back("-vn"); // Output bitrates - args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_outputParams.bitrate)); + if (_outputParams.bitrate) + { + args.emplace_back("-b:a"); + args.emplace_back(std::to_string(*_outputParams.bitrate)); + } // Codecs and formats - switch (_outputParams.format) + if (_outputParams.format) { - case OutputFormat::MP3: - args.emplace_back("-f"); - args.emplace_back("mp3"); - break; + switch (*_outputParams.format) + { + case OutputFormat::MP3: + args.emplace_back("-f"); + args.emplace_back("mp3"); + break; - case OutputFormat::OGG_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case OutputFormat::OGG_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - case OutputFormat::MATROSKA_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("matroska"); - break; + case OutputFormat::MATROSKA_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("matroska"); + break; - case OutputFormat::OGG_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case OutputFormat::OGG_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - case OutputFormat::WEBM_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("webm"); - break; + case OutputFormat::WEBM_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("webm"); + break; - default: - throw Exception{ "Unhandled format (" + std::to_string(static_cast(_outputParams.format)) + ")" }; + default: + throw Exception{ "Unhandled format (" + std::to_string(static_cast(*_outputParams.format)) + ")" }; + } } args.emplace_back("pipe:1"); @@ -199,18 +202,22 @@ namespace lms::av std::string_view Transcoder::getOutputMimeType() const { - switch (_outputParams.format) + // TODO: use input mime type + if (_outputParams.format) { - case OutputFormat::MP3: - return "audio/mpeg"; - case OutputFormat::OGG_OPUS: - return "audio/opus"; - case OutputFormat::MATROSKA_OPUS: - return "audio/x-matroska"; - case OutputFormat::OGG_VORBIS: - return "audio/ogg"; - case OutputFormat::WEBM_VORBIS: - return "audio/webm"; + switch (*_outputParams.format) + { + case OutputFormat::MP3: + return "audio/mpeg"; + case OutputFormat::OGG_OPUS: + return "audio/opus"; + case OutputFormat::MATROSKA_OPUS: + return "audio/x-matroska"; + case OutputFormat::OGG_VORBIS: + return "audio/ogg"; + case OutputFormat::WEBM_VORBIS: + return "audio/webm"; + } } return "application/octet-stream"; // default, should not happen @@ -222,5 +229,4 @@ namespace lms::av return _childProcess->finished(); } - -} // namespace lms::av +} // namespace lms::audio::ffmpeg diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/audio/impl/ffmpeg/Transcoder.hpp similarity index 79% rename from src/libs/av/impl/Transcoder.hpp rename to src/libs/audio/impl/ffmpeg/Transcoder.hpp index 685d18b0..715c6d4f 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/audio/impl/ffmpeg/Transcoder.hpp @@ -19,19 +19,19 @@ #pragma once -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" namespace lms::core { class IChildProcess; } -namespace lms::av +namespace lms::audio::ffmpeg { class Transcoder : public ITranscoder { public: - Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); + Transcoder(const TranscodeParameters& parameters); ~Transcoder() override; Transcoder(const Transcoder&) = delete; Transcoder& operator=(const Transcoder&) = delete; @@ -41,15 +41,15 @@ namespace lms::av std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override; std::string_view getOutputMimeType() const override; - const OutputParameters& getOutputParameters() const override { return _outputParams; } + const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; } bool finished() const override; static void init(); void start(); const std::size_t _debugId{}; - const InputParameters _inputParams; - const OutputParameters _outputParams; + const TranscodeInputParameters _inputParams; + const TranscodeOutputParameters _outputParams; std::unique_ptr _childProcess; }; -} // namespace lms::av \ No newline at end of file +} // namespace lms::audio::ffmpeg \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/Utils.cpp b/src/libs/audio/impl/ffmpeg/Utils.cpp similarity index 89% rename from src/libs/metadata/impl/avformat/Utils.cpp rename to src/libs/audio/impl/ffmpeg/Utils.cpp index 83055d2f..a3ab243c 100644 --- a/src/libs/metadata/impl/avformat/Utils.cpp +++ b/src/libs/audio/impl/ffmpeg/Utils.cpp @@ -19,11 +19,11 @@ #include "Utils.hpp" -namespace lms::metadata::avformat::utils +namespace lms::audio::ffmpeg::utils { std::span getSupportedExtensions() { - // TODO: use av capability to retrieve supported formats + // TODO: list demuxers to retrieve supported formats static const std::array fileExtensions{ ".aac", ".alac", @@ -46,4 +46,4 @@ namespace lms::metadata::avformat::utils }; return fileExtensions; } -} // namespace lms::metadata::avformat::utils \ No newline at end of file +} // namespace lms::audio::ffmpeg::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/avformat/Utils.hpp b/src/libs/audio/impl/ffmpeg/Utils.hpp similarity index 90% rename from src/libs/metadata/impl/avformat/Utils.hpp rename to src/libs/audio/impl/ffmpeg/Utils.hpp index b4f655fa..35d7be48 100644 --- a/src/libs/metadata/impl/avformat/Utils.hpp +++ b/src/libs/audio/impl/ffmpeg/Utils.hpp @@ -22,7 +22,7 @@ #include #include -namespace lms::metadata::avformat::utils +namespace lms::audio::ffmpeg::utils { std::span getSupportedExtensions(); -} // namespace lms::metadata::avformat::utils \ No newline at end of file +} // namespace lms::audio::ffmpeg::utils \ No newline at end of file diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.cpp b/src/libs/audio/impl/taglib/AudioFileInfo.cpp new file mode 100644 index 00000000..2b509804 --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfo.cpp @@ -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 . + */ + +#include "AudioFileInfo.hpp" + +#include + +#include "TagLibDefs.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if LMS_TAGLIB_HAS_DSF + #include +#endif +#if LMS_TAGLIB_HAS_SHORTEN + #include +#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(properties.bitrate() * 1000); + audioProperties.channelCount = static_cast(properties.channels()); + audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() }; + audioProperties.sampleRate = static_cast(properties.sampleRate()); + + // Guess container from the file type + if (const auto* apeFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::APE; + audioProperties.codec = CodecType::APE; // TODO version? + audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample(); + } + else if (const auto* asfFile{ dynamic_cast(&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(&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(&file) }) + { + audioProperties.container = ContainerType::FLAC; + audioProperties.codec = CodecType::FLAC; + audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample(); + } + else if (const auto* mp4File{ dynamic_cast(&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(&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(&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(&file)) + { + audioProperties.container = ContainerType::Ogg; + audioProperties.codec = CodecType::Opus; + } + else if (dynamic_cast(&file)) + { + audioProperties.container = ContainerType::Ogg; + audioProperties.codec = CodecType::Vorbis; + } + else if (const auto* aiffFile{ dynamic_cast(&file) }) + { + audioProperties.container = ContainerType::AIFF; + audioProperties.codec = CodecType::PCM; + audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample(); + } + else if (const auto* wavFile{ dynamic_cast(&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(&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(&file) }) + { + audioProperties.container = ContainerType::TrueAudio; + audioProperties.codec = CodecType::TrueAudio; + audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample(); + } + else if (const auto* wavPackFile{ dynamic_cast(&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 diff --git a/src/libs/audio/impl/taglib/AudioFileInfo.hpp b/src/libs/audio/impl/taglib/AudioFileInfo.hpp new file mode 100644 index 00000000..bd0eaae2 --- /dev/null +++ b/src/libs/audio/impl/taglib/AudioFileInfo.hpp @@ -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 . + */ + +#pragma once + +#include + +#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 diff --git a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp b/src/libs/audio/impl/taglib/ImageReader.cpp similarity index 55% rename from src/libs/metadata/impl/taglib/TagLibImageReader.cpp rename to src/libs/audio/impl/taglib/ImageReader.cpp index 927da6c4..adad0a24 100644 --- a/src/libs/metadata/impl/taglib/TagLibImageReader.cpp +++ b/src/libs/audio/impl/taglib/ImageReader.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TagLibImageReader.hpp" +#include "ImageReader.hpp" #include "TagLibDefs.hpp" @@ -33,16 +33,14 @@ #include #include #include +#include #include #include #include -#include "core/ILogger.hpp" -#include "metadata/Exception.hpp" +#include "core/String.hpp" -#include "taglib/Utils.hpp" - -namespace lms::metadata::taglib +namespace lms::audio::taglib { namespace { @@ -50,47 +48,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::ID3v2::AttachedPictureFrame::Type::Other: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other: return Image::Type::Other; - case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: return Image::Type::BackCover; - case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::ID3v2::AttachedPictureFrame::Type::Media: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media: return Image::Type::Media; - case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::ID3v2::AttachedPictureFrame::Type::Artist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist: return Image::Type::Artist; - case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: return Image::Type::Conductor; - case TagLib::ID3v2::AttachedPictureFrame::Type::Band: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band: return Image::Type::Band; - case TagLib::ID3v2::AttachedPictureFrame::Type::Composer: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer: return Image::Type::Composer; - case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: return Image::Type::Illustration; - case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: + case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -101,47 +99,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::ASF::Picture::Type::Other: + case ::TagLib::ASF::Picture::Type::Other: return Image::Type::Other; - case TagLib::ASF::Picture::Type::FileIcon: + case ::TagLib::ASF::Picture::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::ASF::Picture::Type::OtherFileIcon: + case ::TagLib::ASF::Picture::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::ASF::Picture::Type::FrontCover: + case ::TagLib::ASF::Picture::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::ASF::Picture::Type::BackCover: + case ::TagLib::ASF::Picture::Type::BackCover: return Image::Type::BackCover; - case TagLib::ASF::Picture::Type::LeafletPage: + case ::TagLib::ASF::Picture::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::ASF::Picture::Type::Media: + case ::TagLib::ASF::Picture::Type::Media: return Image::Type::Media; - case TagLib::ASF::Picture::Type::LeadArtist: + case ::TagLib::ASF::Picture::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::ASF::Picture::Type::Artist: + case ::TagLib::ASF::Picture::Type::Artist: return Image::Type::Artist; - case TagLib::ASF::Picture::Type::Conductor: + case ::TagLib::ASF::Picture::Type::Conductor: return Image::Type::Conductor; - case TagLib::ASF::Picture::Type::Band: + case ::TagLib::ASF::Picture::Type::Band: return Image::Type::Band; - case TagLib::ASF::Picture::Type::Composer: + case ::TagLib::ASF::Picture::Type::Composer: return Image::Type::Composer; - case TagLib::ASF::Picture::Type::Lyricist: + case ::TagLib::ASF::Picture::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::ASF::Picture::Type::RecordingLocation: + case ::TagLib::ASF::Picture::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::ASF::Picture::Type::DuringRecording: + case ::TagLib::ASF::Picture::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::ASF::Picture::Type::DuringPerformance: + case ::TagLib::ASF::Picture::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::ASF::Picture::Type::MovieScreenCapture: + case ::TagLib::ASF::Picture::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::ASF::Picture::Type::ColouredFish: + case ::TagLib::ASF::Picture::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::ASF::Picture::Type::Illustration: + case ::TagLib::ASF::Picture::Type::Illustration: return Image::Type::Illustration; - case TagLib::ASF::Picture::Type::BandLogo: + case ::TagLib::ASF::Picture::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::ASF::Picture::Type::PublisherLogo: + case ::TagLib::ASF::Picture::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -152,47 +150,47 @@ namespace lms::metadata::taglib { switch (type) { - case TagLib::FLAC::Picture::Type::Other: + case ::TagLib::FLAC::Picture::Type::Other: return Image::Type::Other; - case TagLib::FLAC::Picture::Type::FileIcon: + case ::TagLib::FLAC::Picture::Type::FileIcon: return Image::Type::FileIcon; - case TagLib::FLAC::Picture::Type::OtherFileIcon: + case ::TagLib::FLAC::Picture::Type::OtherFileIcon: return Image::Type::OtherFileIcon; - case TagLib::FLAC::Picture::Type::FrontCover: + case ::TagLib::FLAC::Picture::Type::FrontCover: return Image::Type::FrontCover; - case TagLib::FLAC::Picture::Type::BackCover: + case ::TagLib::FLAC::Picture::Type::BackCover: return Image::Type::BackCover; - case TagLib::FLAC::Picture::Type::LeafletPage: + case ::TagLib::FLAC::Picture::Type::LeafletPage: return Image::Type::LeafletPage; - case TagLib::FLAC::Picture::Type::Media: + case ::TagLib::FLAC::Picture::Type::Media: return Image::Type::Media; - case TagLib::FLAC::Picture::Type::LeadArtist: + case ::TagLib::FLAC::Picture::Type::LeadArtist: return Image::Type::LeadArtist; - case TagLib::FLAC::Picture::Type::Artist: + case ::TagLib::FLAC::Picture::Type::Artist: return Image::Type::Artist; - case TagLib::FLAC::Picture::Type::Conductor: + case ::TagLib::FLAC::Picture::Type::Conductor: return Image::Type::Conductor; - case TagLib::FLAC::Picture::Type::Band: + case ::TagLib::FLAC::Picture::Type::Band: return Image::Type::Band; - case TagLib::FLAC::Picture::Type::Composer: + case ::TagLib::FLAC::Picture::Type::Composer: return Image::Type::Composer; - case TagLib::FLAC::Picture::Type::Lyricist: + case ::TagLib::FLAC::Picture::Type::Lyricist: return Image::Type::Lyricist; - case TagLib::FLAC::Picture::Type::RecordingLocation: + case ::TagLib::FLAC::Picture::Type::RecordingLocation: return Image::Type::RecordingLocation; - case TagLib::FLAC::Picture::Type::DuringRecording: + case ::TagLib::FLAC::Picture::Type::DuringRecording: return Image::Type::DuringRecording; - case TagLib::FLAC::Picture::Type::DuringPerformance: + case ::TagLib::FLAC::Picture::Type::DuringPerformance: return Image::Type::DuringPerformance; - case TagLib::FLAC::Picture::Type::MovieScreenCapture: + case ::TagLib::FLAC::Picture::Type::MovieScreenCapture: return Image::Type::MovieScreenCapture; - case TagLib::FLAC::Picture::Type::ColouredFish: + case ::TagLib::FLAC::Picture::Type::ColouredFish: return Image::Type::ColouredFish; - case TagLib::FLAC::Picture::Type::Illustration: + case ::TagLib::FLAC::Picture::Type::Illustration: return Image::Type::Illustration; - case TagLib::FLAC::Picture::Type::BandLogo: + case ::TagLib::FLAC::Picture::Type::BandLogo: return Image::Type::BandLogo; - case TagLib::FLAC::Picture::Type::PublisherLogo: + case ::TagLib::FLAC::Picture::Type::PublisherLogo: return Image::Type::PublisherLogo; } @@ -203,44 +201,44 @@ namespace lms::metadata::taglib { switch (format) { - case TagLib::MP4::CoverArt::Format::BMP: + case ::TagLib::MP4::CoverArt::Format::BMP: return "image/bmp"; - case TagLib::MP4::CoverArt::Format::GIF: + case ::TagLib::MP4::CoverArt::Format::GIF: return "image/gif"; - case TagLib::MP4::CoverArt::Format::JPEG: + case ::TagLib::MP4::CoverArt::Format::JPEG: return "image/jpeg"; - case TagLib::MP4::CoverArt::Format::PNG: + case ::TagLib::MP4::CoverArt::Format::PNG: return "image/png"; - case TagLib::MP4::CoverArt::Format::Unknown: + case ::TagLib::MP4::CoverArt::Format::Unknown: return "application/octet-stream"; } return "application/octet-stream"; } -#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES Image::Type imageTypeFromAPEPictureType(std::string_view pictureType) { if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front")) return Image::Type::FrontCover; - else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) + if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back")) return Image::Type::BackCover; return Image::Type::Unknown; } -#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES - void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor) + void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor) { const auto& frameListMap{ id3v2Tags.frameListMap() }; - for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"]) { - const auto* attachedPictureFrame{ dynamic_cast(frame) }; + const auto* attachedPictureFrame{ dynamic_cast(frame) }; if (!attachedPictureFrame) continue; - TagLib::ByteVector picture{ attachedPictureFrame->picture() }; + ::TagLib::ByteVector picture{ attachedPictureFrame->picture() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -252,15 +250,15 @@ namespace lms::metadata::taglib } } - void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor) + void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor) { - for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture")) + for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture")) { - TagLib::ASF::Picture asfPicture{ attribute.toPicture() }; + ::TagLib::ASF::Picture asfPicture{ attribute.toPicture() }; if (!asfPicture.isValid()) continue; - TagLib::ByteVector picture{ asfPicture.picture() }; + ::TagLib::ByteVector picture{ asfPicture.picture() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -273,22 +271,22 @@ namespace lms::metadata::taglib } } - void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor) + void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor) { - const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") }; + const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") }; if (!coverItem.isValid()) return; -#if TAGLIB_HAS_MP4_ITEM_TYPE - if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList) +#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE + if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList) return; #endif - TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; + ::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; bool firstCover{ true }; for (TagLib::MP4::CoverArt& coverArt : coverArtList) { - TagLib::ByteVector picture{ coverArt.data() }; + ::TagLib::ByteVector picture{ coverArt.data() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -303,11 +301,11 @@ namespace lms::metadata::taglib } } - void visitFLACImages(const TagLib::List pictureList, TagLibImageReader::ImageVisitor visitor) + void visitFLACImages(const ::TagLib::List& pictureList, const ImageReader::ImageVisitor& visitor) { for (TagLib::FLAC::Picture* flacPicture : pictureList) { - TagLib::ByteVector picture{ flacPicture->data() }; + ::TagLib::ByteVector picture{ flacPicture->data() }; std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; Image image; @@ -320,14 +318,14 @@ namespace lms::metadata::taglib } } - void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor) + void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor) { -#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES - const TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") }; - for (const TagLib::VariantMap& pictureProperty : pictureProperties) +#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES + const ::TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") }; + for (const ::TagLib::VariantMap& pictureProperty : pictureProperties) { Image image; - TagLib::ByteVector picture; + ::TagLib::ByteVector picture; if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend()) image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true)); @@ -344,81 +342,77 @@ namespace lms::metadata::taglib if (!image.data.empty()) visitor(image); } - -#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES +#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES } } // namespace - TagLibImageReader::TagLibImageReader(const std::filesystem::path& p) - : _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) } + ImageReader::ImageReader(::TagLib::File& file) + : _file{ file } { - if (!_file) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); - throw AudioFileParsingException{}; - } } - void TagLibImageReader::visitImages(ImageVisitor visitor) const + ImageReader::~ImageReader() = default; + + void ImageReader::visitImages(const ImageVisitor& visitor) const { // MP3 - if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) + if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) }) { if (mp3File->hasID3v2Tag()) - visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*mp3File->ID3v2Tag(), visitor); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) + else if (const TagLib::MP4::File * mp4File{ dynamic_cast(&_file) }) { - visitMP4Images(*mp4File, std::move(visitor)); + visitMP4Images(*mp4File, visitor); } // WMA - else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::ASF::File * asfFile{ dynamic_cast(&_file) }) { - if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) - visitASFImages(*tag, std::move(visitor)); + if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() }) + visitASFImages(*tag, visitor); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) }) { if (flacFile->hasID3v2Tag()) // usage discouraged - visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*flacFile->ID3v2Tag(), visitor); else - visitFLACImages(flacFile->pictureList(), std::move(visitor)); + visitFLACImages(flacFile->pictureList(), visitor); } // Ogg vorbis - else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(&_file) }) { - visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor)); + visitFLACImages(vorbisFile->tag()->pictureList(), visitor); } // Ogg Opus - else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(&_file) }) { - visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor)); + visitFLACImages(opusFile->tag()->pictureList(), visitor); } // Aiff - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) }) { if (aiffFile->hasID3v2Tag()) - visitID3V2Images(*aiffFile->tag(), std::move(visitor)); + visitID3V2Images(*aiffFile->tag(), visitor); } // Wav - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) }) { if (wavFile->hasID3v2Tag()) - visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor)); + visitID3V2Images(*wavFile->ID3v2Tag(), visitor); } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) }) { if (mpcFile->hasAPETag()) - visitAPEImages(*mpcFile->APETag(), std::move(visitor)); + visitAPEImages(*mpcFile->APETag(), visitor); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) }) { if (wavPackFile->hasAPETag()) - visitAPEImages(*wavPackFile->APETag(), std::move(visitor)); + visitAPEImages(*wavPackFile->APETag(), visitor); } } -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp b/src/libs/audio/impl/taglib/ImageReader.hpp similarity index 57% rename from src/libs/metadata/impl/avformat/AvFormatImageReader.hpp rename to src/libs/audio/impl/taglib/ImageReader.hpp index 0658b8a7..b4adb32c 100644 --- a/src/libs/metadata/impl/avformat/AvFormatImageReader.hpp +++ b/src/libs/audio/impl/taglib/ImageReader.hpp @@ -19,27 +19,26 @@ #pragma once -#include "IImageReader.hpp" +#include "audio/IImageReader.hpp" -namespace lms::av +namespace TagLib { - class IAudioFile; + class File; } -namespace lms::metadata::avformat +namespace lms::audio::taglib { - class AvFormatImageReader : public IImageReader + class ImageReader : public IImageReader { public: - AvFormatImageReader(const std::filesystem::path& p); - ~AvFormatImageReader() override; - - AvFormatImageReader(const AvFormatImageReader&) = delete; - AvFormatImageReader& operator=(const AvFormatImageReader&) = delete; + ImageReader(TagLib::File& _file); + ~ImageReader() override; + ImageReader(const ImageReader&) = delete; + ImageReader& operator=(const ImageReader&) = delete; private: - void visitImages(ImageVisitor visitor) const override; + void visitImages(const ImageVisitor& visitor) const override; - std::unique_ptr _audioFile; + TagLib::File& _file; }; -} // namespace lms::metadata::avformat +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/TagLibDefs.hpp b/src/libs/audio/impl/taglib/TagLibDefs.hpp similarity index 72% rename from src/libs/metadata/impl/taglib/TagLibDefs.hpp rename to src/libs/audio/impl/taglib/TagLibDefs.hpp index cbcbbf6d..b4f73887 100644 --- a/src/libs/metadata/impl/taglib/TagLibDefs.hpp +++ b/src/libs/audio/impl/taglib/TagLibDefs.hpp @@ -22,15 +22,20 @@ #include #if (TAGLIB_MAJOR_VERSION >= 2) - #define TAGLIB_HAS_DSF 1 + #define LMS_TAGLIB_HAS_DSF 1 #endif -// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1 +// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1 #if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1)) - #define TAGLIB_HAS_MP4_ITEM_TYPE 1 + #define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1 #endif -// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2 +// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2 #if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2)) - #define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1 + #define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1 +#endif + +// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1 +#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1)) + #define LMS_TAGLIB_HAS_SHORTEN 1 #endif diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp b/src/libs/audio/impl/taglib/TagReader.cpp similarity index 72% rename from src/libs/metadata/impl/taglib/TagLibTagReader.cpp rename to src/libs/audio/impl/taglib/TagReader.cpp index b2fd0db8..a905f7b5 100644 --- a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp +++ b/src/libs/audio/impl/taglib/TagReader.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "TagLibTagReader.hpp" +#include "TagReader.hpp" #include @@ -25,7 +25,6 @@ #include #include -#include #include #include #include @@ -38,33 +37,25 @@ #include #include #include +#include #include #include #include #include #include #include -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF #include #include #endif #include "core/ILogger.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" -#include "Utils.hpp" - -namespace lms::metadata::taglib +namespace lms::audio::taglib { namespace { - class TagParsingFailedException : public Exception - { - public: - using Exception::Exception; - }; - // Mapping to internal taglib names and/or common alternative custom names const std::unordered_map> tagLibTagMapping{ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } }, @@ -181,7 +172,7 @@ namespace lms::metadata::taglib { TagType::Writer, { "WRITER" } }, }; - void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src) + void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src) { for (auto&& [tag, values] : src) { @@ -190,49 +181,36 @@ namespace lms::metadata::taglib } } - void dedupTagValues(TagLib::PropertyMap& propertyMap, const std::filesystem::path& file) + void dedupTagValues(TagLib::PropertyMap& propertyMap) { for (auto& [key, values] : propertyMap) { if (values.size() <= 1) continue; - TagLib::StringList newList; - for (const TagLib::String& value : values) + ::TagLib::StringList newList; + for (const ::TagLib::String& value : values) { - if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const TagLib::String& v) { return v == value; })) + if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; })) newList.append(value); } if (values != newList) { - LMS_LOG(METADATA, DEBUG, "File " << file << ": removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)"); + LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)"); values = newList; } } } } // namespace - TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug) - : _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) } + TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs) + : _file{ file } { - if (!_file) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); - throw AudioFileParsingException{ "Parsing failed" }; - } + _propertyMap = _file.properties(); - if (!_file->audioProperties()) - { - LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties"); - throw AudioFileNoAudioPropertiesException{}; - } - - computeAudioProperties(); - - _propertyMap = _file->properties(); - - if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) + enableExtraDebugLogs &= core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG); + if (enableExtraDebugLogs) { for (const auto& [key, values] : _propertyMap) { @@ -245,7 +223,7 @@ namespace lms::metadata::taglib } // Some tags may not be known by TagLib - auto getAPETags = [&](const TagLib::APE::Tag* apeTag) { + auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) { if (!apeTag) return; @@ -254,7 +232,7 @@ namespace lms::metadata::taglib auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) { // Dedup values for some tags that may be written in both a standard tag and in a custom tag - dedupTagValues(_propertyMap, p); + dedupTagValues(_propertyMap); const auto& frameListMap{ id3v2Tags.frameListMap() }; @@ -264,26 +242,30 @@ namespace lms::metadata::taglib // consider each frame hold a different set of lyrics // Synchronized lyrics frames - for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"]) { - const auto* lyricsFrame{ dynamic_cast(frame) }; + const auto* lyricsFrame{ dynamic_cast(frame) }; if (!lyricsFrame) continue; // TODO log or assert? const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() }; std::string lyrics; - for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText()) + for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText()) { std::chrono::milliseconds timestamp{}; switch (lyricsFrame->timestampFormat()) { - case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds: + case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds: timestamp = std::chrono::milliseconds{ synchedText.time }; break; - case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames: - timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 }; + case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames: + { + const ::TagLib::AudioProperties* properties{ file.audioProperties() }; + if (properties && properties->sampleRate()) + timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() }; + } break; - case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown: + case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown: break; } @@ -298,9 +280,9 @@ namespace lms::metadata::taglib } // Unsynchronized lyrics frames - for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"]) + for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"]) { - const auto* lyricsFrame{ dynamic_cast(frame) }; + const auto* lyricsFrame{ dynamic_cast(frame) }; if (!lyricsFrame) continue; // TODO log or assert? @@ -310,9 +292,9 @@ namespace lms::metadata::taglib }; // WMA - if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.get()) }) + if (const ::TagLib::ASF::File * asfFile{ dynamic_cast(&_file) }) { - if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) + if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() }) { for (const auto& [name, attributeList] : tag->attributeListMap()) { @@ -320,19 +302,19 @@ namespace lms::metadata::taglib continue; const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) }; - if (debug) + if (enableExtraDebugLogs) { for (const auto& attribute : attributeList) - LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "" }) << "'"); + LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "" }) << "'"); } if (strName.find("WM/") == 0 || _propertyMap.contains(strName)) continue; - TagLib::StringList strAttributes; - for (const TagLib::ASF::Attribute& attribute : attributeList) + ::TagLib::StringList strAttributes; + for (const ::TagLib::ASF::Attribute& attribute : attributeList) { - if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) + if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType) strAttributes.append(attribute.toString()); } @@ -356,23 +338,23 @@ namespace lms::metadata::taglib } } // MP3 - else if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.get()) }) + else if (TagLib::MPEG::File * mp3File{ dynamic_cast(&_file) }) { if (mp3File->hasID3v2Tag()) - processID3v2Tags(*mp3File->ID3v2Tag()); + processID3v2Tags(*mp3File->ID3v2Tag(false)); getAPETags(mp3File->APETag()); } // MP4 - else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.get()) }) + else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast(&_file) }) { // Taglib does not expose rtng in properties - if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) + if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) { -#if TAGLIB_HAS_MP4_ITEM_TYPE - if (rtngItem.type() == TagLib::MP4::Item::Type::Byte) +#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE + if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte) #endif - _propertyMap["ITUNESADVISORY"] = TagLib::String{ std::to_string(rtngItem.toByte()) }; + _propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) }; } if (!_propertyMap.contains("ORIGINALDATE")) @@ -386,7 +368,7 @@ namespace lms::metadata::taglib auto itOrigDateTag{ tags.find(origDateString) }; if (itOrigDateTag != std::cend(tags)) { - const TagLib::StringList dates{ itOrigDateTag->second.toStringList() }; + const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() }; if (!dates.isEmpty()) { _propertyMap["ORIGINALDATE"] = dates.front(); @@ -397,68 +379,36 @@ namespace lms::metadata::taglib } } // MPC - else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.get()) }) + else if (::TagLib::MPC::File * mpcFile{ dynamic_cast(&_file) }) { getAPETags(mpcFile->APETag()); } // WavPack - else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.get()) }) + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(&_file) }) { getAPETags(wavPackFile->APETag()); } // FLAC - else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.get()) }) + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(&_file) }) { if (flacFile->hasID3v2Tag()) // discouraged usage processID3v2Tags(*flacFile->ID3v2Tag()); } - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(&_file) }) { if (aiffFile->hasID3v2Tag()) processID3v2Tags(*aiffFile->tag()); } - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.get()) }) + else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(&_file) }) { if (wavFile->hasID3v2Tag()) processID3v2Tags(*wavFile->ID3v2Tag()); } } - TagLibTagReader::~TagLibTagReader() = default; + TagReader::~TagReader() = default; - void TagLibTagReader::computeAudioProperties() - { - const TagLib::AudioProperties* properties{ _file->audioProperties() }; - - // Common properties - _audioProperties.bitrate = static_cast(properties->bitrate() * 1000); - _audioProperties.channelCount = static_cast(_file->audioProperties()->channels()); - _audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() }; - _audioProperties.sampleRate = static_cast(properties->sampleRate()); - - if (const auto* apeProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = apeProperties->bitsPerSample(); - if (const auto* asfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = asfProperties->bitsPerSample(); - else if (const auto* flacProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = flacProperties->bitsPerSample(); - else if (const auto* mp4Properties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = mp4Properties->bitsPerSample(); - else if (const auto* wavePackProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = wavePackProperties->bitsPerSample(); - else if (const auto* aiffProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = aiffProperties->bitsPerSample(); - else if (const auto* wavProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = wavProperties->bitsPerSample(); -#if TAGLIB_HAS_DSF - else if (const auto* dsfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); - else if (const auto* dsfProperties{ dynamic_cast(properties) }) - _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); -#endif - } - - void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const { auto itTagNames{ tagLibTagMapping.find(tag) }; if (itTagNames == std::cend(tagLibTagMapping)) @@ -478,19 +428,19 @@ namespace lms::metadata::taglib } } - void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const + void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const { - TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 }; + ::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 }; auto itValues{ _propertyMap.find(key) }; if (itValues == std::cend(_propertyMap)) return; - for (const TagLib::String& value : itValues->second) + for (const ::TagLib::String& value : itValues->second) visitor(value.to8Bit(true)); } - void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const + void TagReader::visitPerformerTags(PerformerVisitor visitor) const { visitTagValues("PERFORMER", [&](std::string_view value) { visitor("", value); @@ -505,7 +455,7 @@ namespace lms::metadata::taglib assert(rolePos != std::string::npos); std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) }; - for (const TagLib::String& value : values) + for (const ::TagLib::String& value : values) { const std::string name{ value.to8Bit(true) }; visitor(role, name); @@ -514,7 +464,7 @@ namespace lms::metadata::taglib } } - void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const + void TagReader::visitLyricsTags(LyricsVisitor visitor) const { if (!_id3v2Lyrics.empty()) { @@ -529,4 +479,4 @@ namespace lms::metadata::taglib }); } } -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp b/src/libs/audio/impl/taglib/TagReader.hpp similarity index 61% rename from src/libs/metadata/impl/taglib/TagLibTagReader.hpp rename to src/libs/audio/impl/taglib/TagReader.hpp index 07034a38..7bb98e84 100644 --- a/src/libs/metadata/impl/taglib/TagLibTagReader.hpp +++ b/src/libs/audio/impl/taglib/TagReader.hpp @@ -19,38 +19,37 @@ #pragma once -#include #include -#include #include -#include #include -#include "ITagReader.hpp" +#include "audio/ITagReader.hpp" -namespace lms::metadata::taglib +namespace TagLib { - class TagLibTagReader : public ITagReader + class File; +} + +namespace lms::audio::taglib +{ + class TagReader : public ITagReader { public: - TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug); - ~TagLibTagReader() override; - TagLibTagReader(const TagLibTagReader&) = delete; - TagLibTagReader& operator=(const TagLibTagReader&) = delete; + TagReader(::TagLib::File& file, bool enableExtraDebugLogs); + ~TagReader() override; + + TagReader(const TagReader&) = delete; + TagReader& operator=(const TagReader&) = delete; private: - void computeAudioProperties(); void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - const AudioProperties& getAudioProperties() const override { return _audioProperties; } - - std::unique_ptr _file; - AudioProperties _audioProperties; - TagLib::PropertyMap _propertyMap; // case-insensitive keys + ::TagLib::File& _file; + ::TagLib::PropertyMap _propertyMap; // case-insensitive keys std::multimap _id3v2Lyrics; }; -} // namespace lms::metadata::taglib +} // namespace lms::audio::taglib diff --git a/src/libs/metadata/impl/taglib/Utils.cpp b/src/libs/audio/impl/taglib/Utils.cpp similarity index 87% rename from src/libs/metadata/impl/taglib/Utils.cpp rename to src/libs/audio/impl/taglib/Utils.cpp index 2d01b6e2..7a7bdd39 100644 --- a/src/libs/metadata/impl/taglib/Utils.cpp +++ b/src/libs/audio/impl/taglib/Utils.cpp @@ -39,15 +39,17 @@ #include #include #include -#if TAGLIB_HAS_DSF - #include +#if LMS_TAGLIB_HAS_DSF #include #endif #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" +#include "core/String.hpp" -namespace lms::metadata::taglib::utils +#include "audio/IAudioFileInfo.hpp" + +namespace lms::audio::taglib::utils { std::span getSupportedExtensions() { @@ -56,23 +58,23 @@ namespace lms::metadata::taglib::utils ".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p", ".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc", ".wav", -#if TAGLIB_HAS_DSF - ".dsf", ".dff", ".dsdiff" +#if LMS_TAGLIB_HAS_DSF + ".dsf" #endif }; return std::span{ supportedExtensions }; } - TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle) + TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle) { switch (readStyle) { - case ParserReadStyle::Fast: + case ParserOptions::AudioPropertiesReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; - case ParserReadStyle::Average: + case ParserOptions::AudioPropertiesReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; - case ParserReadStyle::Accurate: + case ParserOptions::AudioPropertiesReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; } @@ -100,8 +102,9 @@ namespace lms::metadata::taglib::utils return TagLib::FileStream{ fd, true }; } - std::unique_ptr parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties) + std::unique_ptr parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) { + constexpr bool readAudioProperties{ true }; std::unique_ptr file; if (extension.empty()) @@ -149,11 +152,9 @@ namespace lms::metadata::taglib::utils file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); else if (ext == "WAV") file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF else if (ext == "DSF") file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); - else if (ext == "DFF" || ext == "DSDIFF") - file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #endif if (file && !file->isValid()) @@ -162,8 +163,9 @@ namespace lms::metadata::taglib::utils return file; } - std::unique_ptr parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties) + std::unique_ptr parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) { + constexpr bool readAudioProperties{ true }; std::unique_ptr file; if (TagLib::MPEG::File::isSupported(stream)) @@ -200,11 +202,9 @@ namespace lms::metadata::taglib::utils file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); else if (TagLib::RIFF::WAV::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); -#if TAGLIB_HAS_DSF +#if LMS_TAGLIB_HAS_DSF else if (TagLib::DSF::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); - else if (TagLib::DSDIFF::File::isSupported(stream)) - file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #endif if (file && !file->isValid()) @@ -213,20 +213,33 @@ namespace lms::metadata::taglib::utils return file; } - std::unique_ptr parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties) + std::unique_ptr parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle) { LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile"); + const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) }; TagLib::FileStream fileStream{ createFileStream(p) }; - std::unique_ptr file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) }; + std::unique_ptr file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) }; if (!file) { LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension"); - file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value()); + file = parseFileByContent(&fileStream, tagLibReadStyle); if (!file) LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content"); } + if (!file) + { + LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); + throw AudioFileParsingException{ p, "Parsing failed" }; + } + + if (!file->audioProperties()) + { + LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties"); + throw AudioFileNoAudioPropertiesException{ p }; + } + return file; } -} // namespace lms::metadata::taglib::utils \ No newline at end of file +} // namespace lms::audio::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/taglib/TagLibImageReader.hpp b/src/libs/audio/impl/taglib/Utils.hpp similarity index 66% rename from src/libs/metadata/impl/taglib/TagLibImageReader.hpp rename to src/libs/audio/impl/taglib/Utils.hpp index c0c5bea4..f79e96bf 100644 --- a/src/libs/metadata/impl/taglib/TagLibImageReader.hpp +++ b/src/libs/audio/impl/taglib/Utils.hpp @@ -19,19 +19,16 @@ #pragma once -#include "IImageReader.hpp" +#include +#include +#include #include -namespace lms::metadata::taglib +#include "audio/IAudioFileInfo.hpp" + +namespace lms::audio::taglib::utils { - class TagLibImageReader : public IImageReader - { - public: - TagLibImageReader(const std::filesystem::path& p); - - void visitImages(ImageVisitor visitor) const override; - - std::unique_ptr _file; - }; -} // namespace lms::metadata::taglib + std::span getSupportedExtensions(); + std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle); +} // namespace lms::audio::taglib::utils \ No newline at end of file diff --git a/src/libs/audio/include/audio/AudioTypes.hpp b/src/libs/audio/include/audio/AudioTypes.hpp new file mode 100644 index 00000000..802d680d --- /dev/null +++ b/src/libs/audio/include/audio/AudioTypes.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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 container; + std::optional codec; + std::chrono::milliseconds duration{}; + std::optional bitrate; + std::optional bitsPerSample; + std::optional channelCount; + std::optional sampleRate; + }; +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/av/include/av/Exception.hpp b/src/libs/audio/include/audio/Exception.hpp similarity index 94% rename from src/libs/av/include/av/Exception.hpp rename to src/libs/audio/include/audio/Exception.hpp index baf7809b..13f13437 100644 --- a/src/libs/av/include/av/Exception.hpp +++ b/src/libs/audio/include/audio/Exception.hpp @@ -21,11 +21,11 @@ #include "core/Exception.hpp" -namespace lms::av +namespace lms::audio { class Exception : public core::LmsException { public: using LmsException::LmsException; }; -} // namespace lms::av +} // namespace lms::audio diff --git a/src/libs/audio/include/audio/IAudioFileInfo.hpp b/src/libs/audio/include/audio/IAudioFileInfo.hpp new file mode 100644 index 00000000..f528a7bc --- /dev/null +++ b/src/libs/audio/include/audio/IAudioFileInfo.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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 parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{}); + + std::span getSupportedExtensions(ParserOptions::ParserOptions::Parser parser); +} // namespace lms::audio diff --git a/src/libs/audio/include/audio/IImageReader.hpp b/src/libs/audio/include/audio/IImageReader.hpp new file mode 100644 index 00000000..0063151c --- /dev/null +++ b/src/libs/audio/include/audio/IImageReader.hpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#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 data; + }; + + core::LiteralString imageTypeToString(Image::Type type); + + class IImageReader + { + public: + virtual ~IImageReader() = default; + + using ImageVisitor = std::function; + virtual void visitImages(const ImageVisitor& visitor) const = 0; + }; +} // namespace lms::audio diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/audio/include/audio/ITagReader.hpp similarity index 95% rename from src/libs/metadata/impl/ITagReader.hpp rename to src/libs/audio/include/audio/ITagReader.hpp index fe5f76a0..57493f43 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/audio/include/audio/ITagReader.hpp @@ -20,10 +20,11 @@ #pragma once #include +#include -#include "metadata/Types.hpp" +#include "core/LiteralString.hpp" -namespace lms::metadata +namespace lms::audio { // prefer using picard internal names // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html @@ -152,8 +153,12 @@ namespace lms::metadata Website, WorkTitle, Writer, + + Count, // Special value used to count the number of tags }; + core::LiteralString tagTypeToString(TagType type); + class ITagReader { public: @@ -168,7 +173,5 @@ namespace lms::metadata using LyricsVisitor = std::function; virtual void visitLyricsTags(LyricsVisitor visitor) const = 0; - - virtual const AudioProperties& getAudioProperties() const = 0; }; -} // namespace lms::metadata +} // namespace lms::audio diff --git a/src/libs/av/include/av/ITranscoder.hpp b/src/libs/audio/include/audio/ITranscoder.hpp similarity index 58% rename from src/libs/av/include/av/ITranscoder.hpp rename to src/libs/audio/include/audio/ITranscoder.hpp index 165858b0..85273be0 100644 --- a/src/libs/av/include/av/ITranscoder.hpp +++ b/src/libs/audio/include/audio/ITranscoder.hpp @@ -20,37 +20,14 @@ #pragma once #include -#include #include #include -#include #include -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 streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set) - }; - - enum class OutputFormat - { - MP3, - OGG_OPUS, - MATROSKA_OPUS, - OGG_VORBIS, - WEBM_VORBIS, - }; - - struct OutputParameters - { - OutputFormat format; - std::size_t bitrate{ 128'000 }; - bool stripMetadata{ true }; - }; - class ITranscoder { public: @@ -62,10 +39,10 @@ namespace lms::av virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0; virtual std::string_view getOutputMimeType() const = 0; - virtual const OutputParameters& getOutputParameters() const = 0; + virtual const TranscodeOutputParameters& getOutputParameters() const = 0; virtual bool finished() const = 0; }; - std::unique_ptr createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters); -} // namespace lms::av \ No newline at end of file + std::unique_ptr createTranscoder(const TranscodeParameters& parameters); +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/audio/include/audio/TranscodeTypes.hpp b/src/libs/audio/include/audio/TranscodeTypes.hpp new file mode 100644 index 00000000..b2c16a86 --- /dev/null +++ b/src/libs/audio/include/audio/TranscodeTypes.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +// #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 format; + std::optional bitrate; + std::optional bitsPerSample; + std::optional channelCount; + std::optional sampleRate; + bool stripMetadata{ true }; + }; + + struct TranscodeParameters + { + TranscodeInputParameters inputParameters; + TranscodeOutputParameters outputParameters; + }; +} // namespace lms::audio \ No newline at end of file diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt deleted file mode 100644 index 0833bad7..00000000 --- a/src/libs/av/CMakeLists.txt +++ /dev/null @@ -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 - ) diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp deleted file mode 100644 index 41cc3ab0..00000000 --- a/src/libs/av/impl/AudioFile.hpp +++ /dev/null @@ -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 . - */ - -#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 getStreamInfo() const override; - std::optional getBestStreamInfo() const override; - std::optional getBestStreamIndex() const override; - bool hasAttachedPictures() const override; - void visitAttachedPictures(std::function func) const override; - - private: - std::optional getStreamInfo(std::size_t streamIndex) const; - - const std::filesystem::path _p; - AVFormatContext* _context{}; - }; -} // namespace lms::av diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp deleted file mode 100644 index 63087058..00000000 --- a/src/libs/av/include/av/IAudioFile.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 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; - - virtual const std::filesystem::path& getPath() const = 0; - virtual ContainerInfo getContainerInfo() const = 0; - virtual MetadataMap getMetaData() const = 0; - virtual std::vector getStreamInfo() const = 0; - virtual std::optional getBestStreamInfo() const = 0; // none if failure/unknown - virtual std::optional getBestStreamIndex() const = 0; // none if failure/unknown - virtual bool hasAttachedPictures() const = 0; - virtual void visitAttachedPictures(std::function func) const = 0; - }; - - std::unique_ptr parseAudioFile(const std::filesystem::path& p); -} // namespace lms::av diff --git a/src/libs/core/impl/Logger.cpp b/src/libs/core/impl/Logger.cpp index 6173858b..a5f20884 100644 --- a/src/libs/core/impl/Logger.cpp +++ b/src/libs/core/impl/Logger.cpp @@ -37,10 +37,10 @@ namespace lms::core::logging { case Module::API_SUBSONIC: return "API_SUBSONIC"; + case Module::AUDIO: + return "AUDIO"; case Module::AUTH: return "AUTH"; - case Module::AV: - return "AV"; case Module::CHILDPROCESS: return "CHILDPROC"; case Module::COVER: diff --git a/src/libs/core/include/core/ILogger.hpp b/src/libs/core/include/core/ILogger.hpp index 3c300e7d..cb136d41 100644 --- a/src/libs/core/include/core/ILogger.hpp +++ b/src/libs/core/include/core/ILogger.hpp @@ -40,8 +40,8 @@ namespace lms::core::logging enum class Module { API_SUBSONIC, + AUDIO, AUTH, - AV, CHILDPROCESS, COVER, DB, diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt deleted file mode 100644 index 1c3c2967..00000000 --- a/src/libs/metadata/CMakeLists.txt +++ /dev/null @@ -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 - ) diff --git a/src/libs/metadata/bench/CMakeLists.txt b/src/libs/metadata/bench/CMakeLists.txt deleted file mode 100644 index 4e3a392d..00000000 --- a/src/libs/metadata/bench/CMakeLists.txt +++ /dev/null @@ -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 - ) diff --git a/src/libs/metadata/impl/AudioFileParser.hpp b/src/libs/metadata/impl/AudioFileParser.hpp deleted file mode 100644 index 48176296..00000000 --- a/src/libs/metadata/impl/AudioFileParser.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include - -#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 parseMetaData(const std::filesystem::path& p) const override; - std::unique_ptr 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 getSupportedExtensions() const override; - - void processTags(const ITagReader& reader, Track& track) const; - - std::optional getMedium(const ITagReader& tagReader) const; - std::optional getRelease(const ITagReader& tagReader) const; - - const AudioFileParserParameters _params; - }; -} // namespace lms::metadata diff --git a/src/libs/metadata/impl/taglib/Utils.hpp b/src/libs/metadata/impl/taglib/Utils.hpp deleted file mode 100644 index 7ddafdbd..00000000 --- a/src/libs/metadata/impl/taglib/Utils.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include -#include - -#include -#include - -#include "core/TaggedType.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata::taglib::utils -{ - std::span getSupportedExtensions(); - TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle); - - using ReadAudioProperties = core::TaggedBool; - std::unique_ptr parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties); -} // namespace lms::metadata::taglib::utils \ No newline at end of file diff --git a/src/libs/metadata/include/metadata/Exception.hpp b/src/libs/metadata/include/metadata/Exception.hpp deleted file mode 100644 index 2acdda72..00000000 --- a/src/libs/metadata/include/metadata/Exception.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include - -#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 \ No newline at end of file diff --git a/src/libs/metadata/include/metadata/IAudioFileParser.hpp b/src/libs/metadata/include/metadata/IAudioFileParser.hpp deleted file mode 100644 index 599773f0..00000000 --- a/src/libs/metadata/include/metadata/IAudioFileParser.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include "metadata/Exception.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata -{ - class IAudioFileParser - { - public: - virtual ~IAudioFileParser() = default; - - virtual std::unique_ptr parseMetaData(const std::filesystem::path& p) const = 0; - - using ImageVisitor = std::function; - virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) const = 0; - - virtual std::span getSupportedExtensions() const = 0; - }; - - std::unique_ptr createAudioFileParser(const AudioFileParserParameters& params); -} // namespace lms::metadata diff --git a/src/libs/metadata/test/AudioFileParser.cpp b/src/libs/metadata/test/AudioFileParser.cpp deleted file mode 100644 index c98b179c..00000000 --- a/src/libs/metadata/test/AudioFileParser.cpp +++ /dev/null @@ -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 . - */ - -#include - -#include - -#include - -#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 testTags{ createDefaultPopulatedTestTagReader() }; - - const std::unique_ptr 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 expectedReleaseTypes{ "Album", "Compilation" }; - EXPECT_EQ(release.releaseTypes, expectedReleaseTypes); - } - } - - TEST(AudioFileParser, trim) - { - const TestTagReader testTags{ - { - { TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } }, - } - }; - - std::unique_ptr 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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 expectedValue) { - const TestTagReader testTags{ - { - { TagType::Advisory, { value } }, - } - }; - - AudioFileParser parser; - std::unique_ptr 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{ 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{ 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 diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt deleted file mode 100644 index d957cd3b..00000000 --- a/src/libs/metadata/test/CMakeLists.txt +++ /dev/null @@ -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() - diff --git a/src/libs/services/artwork/CMakeLists.txt b/src/libs/services/artwork/CMakeLists.txt index 56488436..3acefa6d 100644 --- a/src/libs/services/artwork/CMakeLists.txt +++ b/src/libs/services/artwork/CMakeLists.txt @@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE ) target_link_libraries(lmsartwork PRIVATE + lmsaudio lmsimage - lmsmetadata ) target_link_libraries(lmsartwork PUBLIC diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index d46bdf0c..7395ed20 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -23,6 +23,9 @@ #include "core/IConfig.hpp" #include "core/ILogger.hpp" + +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -37,7 +40,6 @@ #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" -#include "metadata/IAudioFileParser.hpp" namespace lms::artwork { @@ -50,7 +52,6 @@ namespace lms::artwork const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath) : _db{ db } - , _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) } , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); @@ -107,7 +108,11 @@ namespace lms::artwork { std::size_t currentIndex{}; - _audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) { + audio::ParserOptions options; + options.readStyle = audio::ParserOptions::AudioPropertiesReadStyle::Fast; // only for images + + auto audioFile{ audio::parseAudioFile(p) }; + audioFile->getImageReader().visitImages([&](const audio::Image& parsedImage) { if (currentIndex++ != index) return; @@ -130,7 +135,7 @@ namespace lms::artwork } }); } - catch (const metadata::Exception& e) + catch (const audio::Exception& e) { LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what()); } diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index feb5b837..53bd1455 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -33,11 +33,6 @@ namespace lms::db class Session; } -namespace lms::metadata -{ - class IAudioFileParser; -} - namespace lms::artwork { class ArtworkService : public IArtworkService @@ -67,7 +62,6 @@ namespace lms::artwork db::IDb& _db; - std::unique_ptr _audioFileParser; ImageCache _cache; std::shared_ptr _defaultReleaseCover; std::shared_ptr _defaultArtistImage; diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 20284ce9..055371cc 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -1,12 +1,17 @@ add_library(lmsscanner STATIC impl/helpers/ArtistHelpers.cpp - impl/scanners/ArtistInfoFileScanner.cpp - impl/scanners/AudioFileScanOperation.cpp + impl/scanners/artistinfo/ArtistInfoParser.cpp + impl/scanners/artistinfo/ArtistInfoFileScanner.cpp + impl/scanners/audiofile/AudioFileScanOperation.cpp + impl/scanners/audiofile/AudioFileScanner.cpp + impl/scanners/audiofile/TrackMetadataParser.cpp + impl/scanners/audiofile/Utils.cpp + impl/scanners/lyrics/LyricsFileScanner.cpp + impl/scanners/lyrics/LyricsParser.cpp + impl/scanners/playlist/PlayListFileScanner.cpp + impl/scanners/playlist/PlayListParser.cpp impl/scanners/FileScanOperationBase.cpp - impl/scanners/AudioFileScanner.cpp impl/scanners/ImageFileScanner.cpp - impl/scanners/LyricsFileScanner.cpp - impl/scanners/PlayListFileScanner.cpp impl/scanners/Utils.cpp impl/steps/JobQueue.cpp impl/steps/ScanErrorLogger.cpp @@ -43,13 +48,22 @@ target_include_directories(lmsscanner PRIVATE target_link_libraries(lmsscanner PRIVATE lmscore - lmsdatabase + lmsaudio lmsimage - lmsmetadata lmsrecommendation + pugixml::pugixml ) target_link_libraries(lmsscanner PUBLIC + lmsdatabase std::filesystem Wt::Wt ) + +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +if (BUILD_BENCHMARKS) + add_subdirectory(bench) +endif() diff --git a/src/libs/services/scanner/bench/CMakeLists.txt b/src/libs/services/scanner/bench/CMakeLists.txt new file mode 100644 index 00000000..78d75165 --- /dev/null +++ b/src/libs/services/scanner/bench/CMakeLists.txt @@ -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 + ) diff --git a/src/libs/metadata/bench/LyricsBench.cpp b/src/libs/services/scanner/bench/Lyrics.cpp similarity index 96% rename from src/libs/metadata/bench/LyricsBench.cpp rename to src/libs/services/scanner/bench/Lyrics.cpp index 2e1a213e..a22bfdf8 100644 --- a/src/libs/metadata/bench/LyricsBench.cpp +++ b/src/libs/services/scanner/bench/Lyrics.cpp @@ -22,9 +22,9 @@ #include -#include "metadata/Lyrics.hpp" +#include "scanners/lyrics/LyricsParser.hpp" -namespace lms::metadata::benchmarks +namespace lms::scanner::benchmarks { static void BM_Lyrics(benchmark::State& state) { @@ -89,4 +89,4 @@ namespace lms::metadata::benchmarks BENCHMARK(BM_Lyrics); -} // namespace lms::metadata::benchmarks +} // namespace lms::scanner::benchmarks diff --git a/src/libs/services/scanner/bench/Scanner.cpp b/src/libs/services/scanner/bench/Scanner.cpp new file mode 100644 index 00000000..c394aec4 --- /dev/null +++ b/src/libs/services/scanner/bench/Scanner.cpp @@ -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 . + */ + +#include + +BENCHMARK_MAIN(); \ No newline at end of file diff --git a/src/libs/metadata/bench/Metadata.cpp b/src/libs/services/scanner/bench/TrackMetadataParser.cpp similarity index 75% rename from src/libs/metadata/bench/Metadata.cpp rename to src/libs/services/scanner/bench/TrackMetadataParser.cpp index 16195076..b0aeaa41 100644 --- a/src/libs/metadata/bench/Metadata.cpp +++ b/src/libs/services/scanner/bench/TrackMetadataParser.cpp @@ -19,30 +19,22 @@ #include -#include "AudioFileParser.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" + #include "TestTagReader.hpp" -#include "core/String.hpp" -#include "metadata/Types.hpp" -namespace lms::metadata::benchmarks +namespace lms::scanner::benchmarks { - class TestAudioFileParser : public AudioFileParser - { - public: - using AudioFileParser::AudioFileParser; - using AudioFileParser::parseMetaData; - }; - static void BM_Metadata_parse(benchmark::State& state) { - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" }; - std::unique_ptr testTags{ tests::createDefaultPopulatedTestTagReader() }; - const TestAudioFileParser parser{ params }; + std::unique_ptr testTags{ tests::createDefaultPopulatedTestTagReader() }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(*testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(*testTags)); } } @@ -50,16 +42,15 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - const AudioFileParserParameters params; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -67,11 +58,11 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.artistTagDelimiters = { "/", ";" }; // The list itself is not important, the idea is to have some volume params.artistsToNotSplit = { "AC/DC", @@ -113,10 +104,10 @@ namespace lms::metadata::benchmarks "White/Light", "Yamantaka // Sonic Titan" }; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -124,18 +115,18 @@ namespace lms::metadata::benchmarks { const tests::TestTagReader testTags{ { - { TagType::Artist, { "AC/DC; MyArtist" } }, + { audio::TagType::Artist, { "AC/DC; MyArtist" } }, } }; - AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.artistTagDelimiters = { "/", ";" }; - const TestAudioFileParser parser{ params }; + const TrackMetadataParser parser{ params }; for (auto _ : state) { - std::unique_ptr track{ parser.parseMetaData(testTags) }; + benchmark::DoNotOptimize(parser.parseTrackMetaData(testTags)); } } @@ -144,6 +135,4 @@ namespace lms::metadata::benchmarks BENCHMARK(BM_Metadata_parseArtists_WithWhitelist); BENCHMARK(BM_Metadata_parseArtists_WithoutWhitelist); -} // namespace lms::metadata::benchmarks - -BENCHMARK_MAIN(); \ No newline at end of file +} // namespace lms::scanner::benchmarks diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index d29bdb70..4834547b 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -27,15 +27,16 @@ #include "core/IJobScheduler.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" + #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/ScanSettings.hpp" -#include "scanners/ArtistInfoFileScanner.hpp" -#include "scanners/AudioFileScanner.hpp" #include "scanners/ImageFileScanner.hpp" -#include "scanners/LyricsFileScanner.hpp" -#include "scanners/PlayListFileScanner.hpp" +#include "scanners/artistinfo/ArtistInfoFileScanner.hpp" +#include "scanners/audiofile/AudioFileScanner.hpp" +#include "scanners/lyrics/LyricsFileScanner.hpp" +#include "scanners/playlist/PlayListFileScanner.hpp" #include "steps/ScanStepArtistReconciliation.hpp" #include "steps/ScanStepAssociateArtistImages.hpp" diff --git a/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp b/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp index abb926cc..a380d9a3 100644 --- a/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp +++ b/src/libs/services/scanner/impl/helpers/ArtistHelpers.cpp @@ -21,13 +21,14 @@ #include "core/ILogger.hpp" #include "database/Session.hpp" -#include "metadata/Types.hpp" + +#include "types/TrackMetadata.hpp" namespace lms::scanner::helpers { namespace { - db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo) + db::Artist::pointer createArtist(db::Session& session, const Artist& artistInfo) { db::Artist::pointer artist{ session.create(artistInfo.name) }; @@ -44,7 +45,7 @@ namespace lms::scanner::helpers return uuid ? std::string{ uuid->getAsString() } : ""; } - void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo) + void updateArtistIfNeeded(db::Artist::pointer artist, const Artist& artistInfo) { // MBID may be set if (artist->getMBID() != artistInfo.mbid) @@ -68,7 +69,7 @@ namespace lms::scanner::helpers } // namespace - db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { assert(artistInfo.mbid.has_value()); db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) }; @@ -99,7 +100,7 @@ namespace lms::scanner::helpers return artist; } - db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { db::Artist::pointer artist; @@ -139,7 +140,7 @@ namespace lms::scanner::helpers return artist; } - db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) + db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries) { // First try to get by MBID if (artistInfo.mbid) diff --git a/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp b/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp index 1c81320a..503441aa 100644 --- a/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp +++ b/src/libs/services/scanner/impl/helpers/ArtistHelpers.hpp @@ -20,9 +20,10 @@ #pragma once #include "core/TaggedType.hpp" + #include "database/objects/Artist.hpp" -namespace lms::metadata +namespace lms::scanner { struct Artist; } @@ -31,8 +32,8 @@ namespace lms::scanner::helpers { using AllowFallbackOnMBIDEntry = core::TaggedBool; - db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); - db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); - db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtistByName(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); + db::Artist::pointer getOrCreateArtist(db::Session& session, const Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries); } // namespace lms::scanner::helpers \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp similarity index 91% rename from src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp index c6a984be..5ed7c9c9 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.cpp @@ -24,19 +24,22 @@ #include "core/ILogger.hpp" #include "core/String.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/ArtistInfo.hpp" #include "database/objects/MediaLibrary.hpp" -#include "metadata/ArtistInfo.hpp" -#include "metadata/Types.hpp" + #include "services/scanner/ScanErrors.hpp" -#include "FileScanOperationBase.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/artistinfo/ArtistInfoParser.hpp" +#include "types/ArtistInfo.hpp" +#include "types/TrackMetadata.hpp" namespace lms::scanner { @@ -57,7 +60,7 @@ namespace lms::scanner std::string getArtistNameFromArtistInfoFilePath(); - std::optional _parsedArtistInfo; + std::optional _parsedArtistInfo; }; void ArtistInfoFileScanOperation::scan() @@ -72,14 +75,14 @@ namespace lms::scanner return; } - _parsedArtistInfo = metadata::parseArtistInfo(ifs); + _parsedArtistInfo = parseArtistInfo(ifs); if (_parsedArtistInfo->name.empty()) { _parsedArtistInfo->name = getFilePath().parent_path().filename(); LMS_LOG(DBUPDATER, DEBUG, "No name found in " << getFilePath() << ", using '" << _parsedArtistInfo->name << "'"); } } - catch (const metadata::ArtistInfoParseException& e) + catch (const ArtistInfoParseException& e) { addError(getFilePath()); } @@ -121,7 +124,7 @@ namespace lms::scanner db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, getMediaLibrary().id) }; // may be null if settings are updated in // => next scan will correct this artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary)); - const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional(_parsedArtistInfo->sortName) }; + const Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional(_parsedArtistInfo->sortName) }; db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ getScannerSettings().allowArtistMBIDFallback }) }; artistInfo.modify()->setArtist(artist); artistInfo.modify()->setMBIDMatched(_parsedArtistInfo->mbid.has_value() && _parsedArtistInfo->mbid == artist->getMBID()); @@ -150,7 +153,7 @@ namespace lms::scanner std::span ArtistInfoFileScanner::getSupportedFiles() const { - return metadata::getSupportedArtistInfoFiles(); + return getSupportedArtistInfoFiles(); } std::span ArtistInfoFileScanner::getSupportedExtensions() const diff --git a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp index 8036d4e9..d64e7f19 100644 --- a/src/libs/services/scanner/impl/scanners/ArtistInfoFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/ArtistInfo.cpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp similarity index 97% rename from src/libs/metadata/impl/ArtistInfo.cpp rename to src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp index bcba6b88..4b1d7cea 100644 --- a/src/libs/metadata/impl/ArtistInfo.cpp +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "metadata/ArtistInfo.hpp" +#include "ArtistInfoParser.hpp" #include @@ -25,7 +25,7 @@ #include "core/LiteralString.hpp" #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { namespace { @@ -77,4 +77,4 @@ namespace lms::metadata return artistInfo; } -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp new file mode 100644 index 00000000..7b4ad3d3 --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/artistinfo/ArtistInfoParser.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "core/Exception.hpp" + +#include "types/ArtistInfo.hpp" + +namespace lms::scanner +{ + class ArtistInfoParseException : public core::LmsException + { + public: + using core::LmsException::LmsException; + }; + + std::span getSupportedArtistInfoFiles(); + ArtistInfo parseArtistInfo(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp similarity index 66% rename from src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 87d8b92f..1be58f75 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -24,6 +24,11 @@ #include "core/PartialDateTime.hpp" #include "core/Path.hpp" #include "core/XxHash3.hpp" + +#include "audio/IAudioFileInfo.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/Types.hpp" @@ -40,36 +45,34 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackFeatures.hpp" #include "database/objects/TrackLyrics.hpp" -#include "image/Exception.hpp" -#include "image/Image.hpp" -#include "metadata/Exception.hpp" -#include "metadata/IAudioFileParser.hpp" + #include "services/scanner/ScanErrors.hpp" -#include "IFileScanOperation.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" #include "helpers/ArtistHelpers.hpp" +#include "scanners/IFileScanOperation.hpp" +#include "scanners/Utils.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms::scanner { namespace { - void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) + void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) { - for (const metadata::Artist& artistInfo : artists) + for (const Artist& artist : artists) { - db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) }; + db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) }; - const bool matchedUsingMbid{ artistInfo.mbid.has_value() && artist->getMBID() == artistInfo.mbid }; - db::TrackArtistLink::pointer link{ session.create(track, artist, linkType, role, matchedUsingMbid) }; - link.modify()->setArtistName(artistInfo.name); - if (artistInfo.sortName) - link.modify()->setArtistSortName(*artistInfo.sortName); + const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid }; + db::TrackArtistLink::pointer link{ session.create(track, dbArtist, linkType, role, matchedUsingMbid) }; + link.modify()->setArtistName(artist.name); + if (artist.sortName) + link.modify()->setArtistSortName(*artist.sortName); } } - void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) + void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) { constexpr std::string_view noRole{}; createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback); @@ -102,128 +105,128 @@ namespace lms::scanner return label; } - void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo) + void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release) { - if (release->getName() != releaseInfo.name) - release.modify()->setName(releaseInfo.name); - if (release->getSortName() != releaseInfo.sortName) - release.modify()->setSortName(releaseInfo.sortName); - if (release->getGroupMBID() != releaseInfo.groupMBID) - release.modify()->setGroupMBID(releaseInfo.groupMBID); - if (release->getTotalDisc() != releaseInfo.mediumCount) - release.modify()->setTotalDisc(releaseInfo.mediumCount); - if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) - release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); - if (release->isCompilation() != releaseInfo.isCompilation) - release.modify()->setCompilation(releaseInfo.isCompilation); - if (release->getBarcode() != releaseInfo.barcode) - release.modify()->setBarcode(releaseInfo.barcode); - if (release->getComment() != releaseInfo.comment) - release.modify()->setComment(releaseInfo.comment); - if (release->getReleaseTypeNames() != releaseInfo.releaseTypes) + if (dbRelease->getName() != release.name) + dbRelease.modify()->setName(release.name); + if (dbRelease->getSortName() != release.sortName) + dbRelease.modify()->setSortName(release.sortName); + if (dbRelease->getGroupMBID() != release.groupMBID) + dbRelease.modify()->setGroupMBID(release.groupMBID); + if (dbRelease->getTotalDisc() != release.mediumCount) + dbRelease.modify()->setTotalDisc(release.mediumCount); + if (dbRelease->getArtistDisplayName() != release.artistDisplayName) + dbRelease.modify()->setArtistDisplayName(release.artistDisplayName); + if (dbRelease->isCompilation() != release.isCompilation) + dbRelease.modify()->setCompilation(release.isCompilation); + if (dbRelease->getBarcode() != release.barcode) + dbRelease.modify()->setBarcode(release.barcode); + if (dbRelease->getComment() != release.comment) + dbRelease.modify()->setComment(release.comment); + if (dbRelease->getReleaseTypeNames() != release.releaseTypes) { - release.modify()->clearReleaseTypes(); - for (std::string_view releaseType : releaseInfo.releaseTypes) - release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType)); + dbRelease.modify()->clearReleaseTypes(); + for (std::string_view releaseType : release.releaseTypes) + dbRelease.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType)); } - if (release->getCountryNames() != releaseInfo.countries) + if (dbRelease->getCountryNames() != release.countries) { - release.modify()->clearCountries(); - for (std::string_view country : releaseInfo.countries) - release.modify()->addCountry(getOrCreateCountry(session, country)); + dbRelease.modify()->clearCountries(); + for (std::string_view country : release.countries) + dbRelease.modify()->addCountry(getOrCreateCountry(session, country)); } - if (release->getLabelNames() != releaseInfo.labels) + if (dbRelease->getLabelNames() != release.labels) { - release.modify()->clearLabels(); - for (std::string_view label : releaseInfo.labels) - release.modify()->addLabel(getOrCreateLabel(session, label)); + dbRelease.modify()->clearLabels(); + for (std::string_view label : release.labels) + dbRelease.modify()->addLabel(getOrCreateLabel(session, label)); } } // Compare release level info - bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo) + bool isReleaseMatching(const db::Release::pointer& dbCandidateRelease, const Release& release) { // TODO: add more criterias? - return candidateRelease->getName() == releaseInfo.name - && candidateRelease->getSortName() == releaseInfo.sortName - && candidateRelease->getTotalDisc() == releaseInfo.mediumCount - && candidateRelease->isCompilation() == releaseInfo.isCompilation - && candidateRelease->getLabelNames() == releaseInfo.labels - && candidateRelease->getBarcode() == releaseInfo.barcode; + return dbCandidateRelease->getName() == release.name + && dbCandidateRelease->getSortName() == release.sortName + && dbCandidateRelease->getTotalDisc() == release.mediumCount + && dbCandidateRelease->isCompilation() == release.isCompilation + && dbCandidateRelease->getLabelNames() == release.labels + && dbCandidateRelease->getBarcode() == release.barcode; } - db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory) + db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory) { - db::Release::pointer release; + db::Release::pointer dbRelease; // First try to get by MBID: fastest, safest - if (releaseInfo.mbid) + if (release.mbid) { - release = db::Release::find(session, *releaseInfo.mbid); - if (!release) - release = session.create(releaseInfo.name, releaseInfo.mbid); + dbRelease = db::Release::find(session, *release.mbid); + if (!dbRelease) + dbRelease = session.create(release.name, release.mbid); } - else if (releaseInfo.name.empty()) + else if (release.name.empty()) { // No release name (only mbid) -> nothing to do - return release; + return dbRelease; } // Fall back on release name (collisions may occur) // First try using all sibling directories (case for Album/DiscX), only if the disc number is set const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() }; - if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid()) + if (!dbRelease && release.mediumCount && *release.mediumCount > 1 && parentDirectoryId.isValid()) { db::Release::FindParameters params; params.setParentDirectory(parentDirectoryId); - params.setName(releaseInfo.name); - db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { + params.setName(release.name); + db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) { // Already found a candidate - if (release) + if (dbRelease) return; // Do not fallback on properly tagged releases - if (candidateRelease->getMBID().has_value()) + if (dbCandidateRelease->getMBID().has_value()) return; - if (!isReleaseMatching(candidateRelease, releaseInfo)) + if (!isReleaseMatching(dbCandidateRelease, release)) return; - release = candidateRelease; + dbRelease = dbCandidateRelease; }); } // Lastly try in the current directory: we do this at last to have // opportunities to merge releases in case of migration / rescan - if (!release) + if (!dbRelease) { db::Release::FindParameters params; params.setDirectory(currentDirectory->getId()); - params.setName(releaseInfo.name); - db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) { + params.setName(release.name); + db::Release::find(session, params, [&](const db::Release::pointer& dbCandidateRelease) { // Already found a candidate - if (release) + if (dbRelease) return; // Do not fallback on properly tagged releases - if (candidateRelease->getMBID().has_value()) + if (dbCandidateRelease->getMBID().has_value()) return; - if (!isReleaseMatching(candidateRelease, releaseInfo)) + if (!isReleaseMatching(dbCandidateRelease, release)) return; - release = candidateRelease; + dbRelease = dbCandidateRelease; }); } - if (!release) - release = session.create(releaseInfo.name); + if (!dbRelease) + dbRelease = session.create(release.name); - updateReleaseIfNeeded(session, release, releaseInfo); - return release; + updateReleaseIfNeeded(session, dbRelease, release); + return dbRelease; } - db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release) + db::Medium::pointer getOrCreateMedium(db::Session& session, const Medium& medium, const db::Release::pointer& release) { db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) }; if (!dbMedium) @@ -243,7 +246,7 @@ namespace lms::scanner return dbMedium; } - std::vector getOrCreateClusters(db::Session& session, const metadata::Track& track) + std::vector getOrCreateClusters(db::Session& session, const Track& track) { std::vector clusters; @@ -274,69 +277,69 @@ namespace lms::scanner return clusters; } - db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo) + db::TrackLyrics::pointer createLyrics(db::Session& session, const Lyrics& lyrics) { - db::TrackLyrics::pointer lyrics{ session.create() }; + db::TrackLyrics::pointer dbLyrics{ session.create() }; - lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx"); - lyrics.modify()->setOffset(lyricsInfo.offset); - lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist); - lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle); - if (!lyricsInfo.synchronizedLines.empty()) - lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines); + dbLyrics.modify()->setLanguage(!lyrics.language.empty() ? lyrics.language : "xxx"); + dbLyrics.modify()->setOffset(lyrics.offset); + dbLyrics.modify()->setDisplayArtist(lyrics.displayArtist); + dbLyrics.modify()->setDisplayTitle(lyrics.displayTitle); + if (!lyrics.synchronizedLines.empty()) + dbLyrics.modify()->setSynchronizedLines(lyrics.synchronizedLines); else - lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines); + dbLyrics.modify()->setUnsynchronizedLines(lyrics.unsynchronizedLines); - return lyrics; + return dbLyrics; } - db::ImageType convertImageType(metadata::Image::Type type) + db::ImageType convertImageType(audio::Image::Type type) { switch (type) { - case metadata::Image::Type::Unknown: + case audio::Image::Type::Unknown: return db::ImageType::Unknown; - case metadata::Image::Type::Other: + case audio::Image::Type::Other: return db::ImageType::Other; - case metadata::Image::Type::FileIcon: + case audio::Image::Type::FileIcon: return db::ImageType::FileIcon; - case metadata::Image::Type::OtherFileIcon: + case audio::Image::Type::OtherFileIcon: return db::ImageType::OtherFileIcon; - case metadata::Image::Type::FrontCover: + case audio::Image::Type::FrontCover: return db::ImageType::FrontCover; - case metadata::Image::Type::BackCover: + case audio::Image::Type::BackCover: return db::ImageType::BackCover; - case metadata::Image::Type::LeafletPage: + case audio::Image::Type::LeafletPage: return db::ImageType::LeafletPage; - case metadata::Image::Type::Media: + case audio::Image::Type::Media: return db::ImageType::Media; - case metadata::Image::Type::LeadArtist: + case audio::Image::Type::LeadArtist: return db::ImageType::LeadArtist; - case metadata::Image::Type::Artist: + case audio::Image::Type::Artist: return db::ImageType::Artist; - case metadata::Image::Type::Conductor: + case audio::Image::Type::Conductor: return db::ImageType::Conductor; - case metadata::Image::Type::Band: + case audio::Image::Type::Band: return db::ImageType::Band; - case metadata::Image::Type::Composer: + case audio::Image::Type::Composer: return db::ImageType::Composer; - case metadata::Image::Type::Lyricist: + case audio::Image::Type::Lyricist: return db::ImageType::Lyricist; - case metadata::Image::Type::RecordingLocation: + case audio::Image::Type::RecordingLocation: return db::ImageType::RecordingLocation; - case metadata::Image::Type::DuringRecording: + case audio::Image::Type::DuringRecording: return db::ImageType::DuringRecording; - case metadata::Image::Type::DuringPerformance: + case audio::Image::Type::DuringPerformance: return db::ImageType::DuringPerformance; - case metadata::Image::Type::MovieScreenCapture: + case audio::Image::Type::MovieScreenCapture: return db::ImageType::MovieScreenCapture; - case metadata::Image::Type::ColouredFish: + case audio::Image::Type::ColouredFish: return db::ImageType::ColouredFish; - case metadata::Image::Type::Illustration: + case audio::Image::Type::Illustration: return db::ImageType::Illustration; - case metadata::Image::Type::BandLogo: + case audio::Image::Type::BandLogo: return db::ImageType::BandLogo; - case metadata::Image::Type::PublisherLogo: + case audio::Image::Type::PublisherLogo: return db::ImageType::PublisherLogo; } @@ -361,10 +364,10 @@ namespace lms::scanner return image; } - db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo) + db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& dbTrack, const ImageInfo& imageInfo) { const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) }; - db::TrackEmbeddedImageLink::pointer imageLink{ session.create(track, image) }; + db::TrackEmbeddedImageLink::pointer imageLink{ session.create(dbTrack, image) }; imageLink.modify()->setIndex(imageInfo.index); imageLink.modify()->setType(convertImageType(imageInfo.type)); imageLink.modify()->setDescription(imageInfo.description); @@ -372,35 +375,35 @@ namespace lms::scanner return imageLink; } - void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span images) + void updateEmbeddedImages(db::Session& session, db::Track::pointer& dbTrack, std::span images) { - track.modify()->clearEmbeddedImageLinks(); + dbTrack.modify()->clearEmbeddedImageLinks(); for (const ImageInfo& imageInfo : images) { - db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) }; - track.modify()->addEmbeddedImageLink(link); + db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, dbTrack, imageInfo) }; + dbTrack.modify()->addEmbeddedImageLink(link); } } - db::Advisory getAdvisory(std::optional advisory) + db::Advisory getAdvisory(std::optional advisory) { if (!advisory) return db::Advisory::UnSet; switch (advisory.value()) { - case metadata::Track::Advisory::Clean: + case Track::Advisory::Clean: return db::Advisory::Clean; - case metadata::Track::Advisory::Explicit: + case Track::Advisory::Explicit: return db::Advisory::Explicit; - case metadata::Track::Advisory::Unknown: + case Track::Advisory::Unknown: return db::Advisory::Unknown; } return db::Advisory::UnSet; } - db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize) + db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const Track& parsedTrack, const std::filesystem::path& trackPath, size_t fileSize) { db::Track::FindParameters params; // Add as many fields as possible to limit errors @@ -436,9 +439,9 @@ namespace lms::scanner return res; } - void fillInArtistsWithMbid(std::span artists, std::unordered_map& artistsWithMbid) + void fillInArtistsWithMbid(std::span artists, std::unordered_map& artistsWithMbid) { - for (const metadata::Artist& artist : artists) + for (const Artist& artist : artists) { if (artist.mbid.has_value()) { @@ -448,9 +451,9 @@ namespace lms::scanner } } - void fillInMbids(std::span artists, const std::unordered_map& artistsWithMbid) + void fillInMbids(std::span artists, const std::unordered_map& artistsWithMbid) { - for (metadata::Artist& artist : artists) + for (Artist& artist : artists) { if (!artist.mbid) { @@ -461,7 +464,7 @@ namespace lms::scanner } } - void fillMissingMbids(metadata::Track& track) + void fillMissingMbids(Track& track) { // first pass: collect all artists that have mbids std::unordered_map artistsWithMbid; @@ -485,9 +488,10 @@ namespace lms::scanner } } // namespace - AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser) + AudioFileScanOperation::AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions) : FileScanOperationBase{ std::move(fileToScan), db, settings } - , _parser{ parser } + , _metadataParser{ metadataParser } + , _parserOptions{ parserOptions } { } @@ -495,17 +499,20 @@ namespace lms::scanner void AudioFileScanOperation::scan() { - std::unique_ptr track; - try { - _parsedTrack = _parser.parseMetaData(getFilePath()); + auto audioFileInfo{ audio::parseAudioFile(getFilePath(), _parserOptions) }; + + _file.emplace(); + + _file->audioProperties = audioFileInfo->getAudioProperties(); + _file->track = _metadataParser.parseTrackMetaData(audioFileInfo->getTagReader()); // We fill missing artist mbids with mbids found on other artist roles - fillMissingMbids(*_parsedTrack); + fillMissingMbids(_file->track); std::size_t index{}; - _parser.parseImages(getFilePath(), [&](const metadata::Image& image) { + audioFileInfo->getImageReader().visitImages([&](const audio::Image& image) { try { image::ImageProperties properties{ image::probeImage(image.data) }; @@ -522,7 +529,7 @@ namespace lms::scanner info.description = image.description; info.properties = properties; - _parsedImages.push_back(std::move(info)); + _file->images.push_back(std::move(info)); } catch (const image::Exception& e) { @@ -532,15 +539,15 @@ namespace lms::scanner index++; }); } - catch (const metadata::AudioFileNoAudioPropertiesException&) + catch (const audio::AudioFileNoAudioPropertiesException&) { addError(getFilePath()); } - catch (const metadata::IOException& e) + catch (const audio::IOException& e) { addError(getFilePath(), e.getErrorCode()); } - catch (const metadata::Exception& e) + catch (const audio::Exception& e) { addError(getFilePath()); } @@ -552,7 +559,7 @@ namespace lms::scanner db::Session& dbSession{ getDb().getTLSSession() }; db::Track::pointer track{ db::Track::findByPath(dbSession, getFilePath()) }; - if (!_parsedTrack) + if (!_file) { if (track) { @@ -562,9 +569,9 @@ namespace lms::scanner return OperationResult::Skipped; } - if (_parsedTrack->mbid && (!track || getScannerSettings().skipDuplicateTrackMBID)) + if (_file->track.mbid && (!track || getScannerSettings().skipDuplicateTrackMBID)) { - std::vector duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) }; + std::vector duplicateTracks{ db::Track::findByMBID(dbSession, *_file->track.mbid) }; // find for an existing track MBID as the file may have just been moved if (!track && duplicateTracks.size() == 1) @@ -616,7 +623,7 @@ namespace lms::scanner if (!track) { // maybe the file just moved? - track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, getFilePath(), getFileSize()); + track = findMovedTrackBySizeAndMetaData(dbSession, _file->track, getFilePath(), getFileSize()); if (track) { LMS_LOG(DBUPDATER, DEBUG, "Considering track " << getFilePath() << " moved from " << track->getAbsoluteFilePath()); @@ -625,7 +632,7 @@ namespace lms::scanner } // We estimate this is an audio file if the duration is not null - if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero()) + if (_file->audioProperties.duration == std::chrono::milliseconds::zero()) { addError(getFilePath()); @@ -639,8 +646,8 @@ namespace lms::scanner // ***** Title std::string title; - if (!_parsedTrack->title.empty()) - title = _parsedTrack->title; + if (!_file->track.title.empty()) + title = _file->track.title; else { // TODO parse file name to guess track etc. @@ -665,18 +672,18 @@ namespace lms::scanner track.modify()->setScanVersion(getScannerSettings().audioScanVersion); // Audio properties - track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate); - track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample); - track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount); - track.modify()->setDuration(_parsedTrack->audioProperties.duration); - track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate); + track.modify()->setBitrate(_file->audioProperties.bitrate ? *_file->audioProperties.bitrate : 0); + track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample ? *_file->audioProperties.bitsPerSample : 0); + track.modify()->setChannelCount(_file->audioProperties.channelCount ? *_file->audioProperties.channelCount : 0); + track.modify()->setDuration(_file->audioProperties.duration); + track.modify()->setSampleRate(_file->audioProperties.sampleRate ? *_file->audioProperties.sampleRate : 0); track.modify()->setFileSize(getFileSize()); track.modify()->setLastWriteTime(getLastWriteTime()); - if (_parsedTrack->encodingTime.isValid()) + if (_file->track.encodingTime.isValid()) { - const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime }; + const core::PartialDateTime& encodingTime{ _file->track.encodingTime }; Wt::WDate date; Wt::WTime time; if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day) @@ -696,61 +703,61 @@ namespace lms::scanner track.modify()->clearArtistLinks(); const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback); - if (_parsedTrack->medium && _parsedTrack->medium->release) - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback); + if (_file->track.medium && _file->track.medium->release) + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _parsedTrack->producerArtists, allowFallback); - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _file->track.mixerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Producer, _file->track.producerArtists, allowFallback); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _file->track.remixerArtists, allowFallback); - for (const auto& [role, performers] : _parsedTrack->performerArtists) + for (const auto& [role, performers] : _file->track.performerArtists) createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); // For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set - if (_parsedTrack->medium && _parsedTrack->medium->release) + if (_file->track.medium && _file->track.medium->release) { - db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) }; + db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) }; assert(release); track.modify()->setRelease(release); - track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release)); + track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release)); } else { track.modify()->setRelease({}); track.modify()->setMedium({}); } - track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); + track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track)); track.modify()->setName(title); - track.modify()->setTrackNumber(_parsedTrack->position); - track.modify()->setDate(_parsedTrack->date); - track.modify()->setOriginalDate(_parsedTrack->originalDate); - if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) - track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear }); + track.modify()->setTrackNumber(_file->track.position); + track.modify()->setDate(_file->track.date); + track.modify()->setOriginalDate(_file->track.originalDate); + if (!track->getOriginalDate().isValid() && _file->track.originalYear) + track.modify()->setOriginalDate(core::PartialDateTime{ *_file->track.originalYear }); // If a file has an OriginalDate but no date, set it to ease filtering - if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid()) - track.modify()->setDate(_parsedTrack->originalDate); + if (!_file->track.date.isValid() && _file->track.originalDate.isValid()) + track.modify()->setDate(_file->track.originalDate); - track.modify()->setRecordingMBID(_parsedTrack->recordingMBID); - track.modify()->setTrackMBID(_parsedTrack->mbid); + track.modify()->setRecordingMBID(_file->track.recordingMBID); + track.modify()->setTrackMBID(_file->track.mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setCopyright(_parsedTrack->copyright); - track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); - track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); - track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now - track.modify()->setReplayGain(_parsedTrack->replayGain); - track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); + track.modify()->setCopyright(_file->track.copyright); + track.modify()->setCopyrightURL(_file->track.copyrightURL); + track.modify()->setAdvisory(getAdvisory(_file->track.advisory)); + track.modify()->setComment(!_file->track.comments.empty() ? _file->track.comments.front() : ""); // only take the first one for now + track.modify()->setReplayGain(_file->track.replayGain); + track.modify()->setArtistDisplayName(_file->track.artistDisplayName); track.modify()->clearEmbeddedLyrics(); - for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics) + for (const Lyrics& lyricsInfo : _file->track.lyrics) track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo)); - updateEmbeddedImages(dbSession, track, _parsedImages); + updateEmbeddedImages(dbSession, track, _file->images); if (added) { diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp similarity index 67% rename from src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp index 20344ee9..7d5e00ff 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.hpp @@ -19,33 +19,32 @@ #pragma once -#include "IFileScanOperation.hpp" - -#include #include +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" #include "image/Types.hpp" -#include "metadata/Types.hpp" -#include "FileScanOperationBase.hpp" -#include "FileToScan.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/FileToScan.hpp" +#include "scanners/IFileScanOperation.hpp" + +#include "types/TrackMetadata.hpp" namespace lms::db { class IDb; } // namespace lms::db -namespace lms::metadata -{ - class IAudioFileParser; -} // namespace lms::metadata - namespace lms::scanner { + class TrackMetadataParser; + struct ImageInfo { std::size_t index; - metadata::Image::Type type{ metadata::Image::Type::Unknown }; + audio::Image::Type type{ audio::Image::Type::Unknown }; std::uint64_t hash{}; std::size_t size{}; image::ImageProperties properties; @@ -56,7 +55,7 @@ namespace lms::scanner class AudioFileScanOperation : public FileScanOperationBase { public: - AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, metadata::IAudioFileParser& parser); + AudioFileScanOperation(FileToScan&& fileToScan, db::IDb& db, const ScannerSettings& settings, const TrackMetadataParser& metadataParser, const audio::ParserOptions& parserOptions); ~AudioFileScanOperation() override; AudioFileScanOperation(const AudioFileScanOperation&) = delete; AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete; @@ -66,9 +65,15 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - metadata::IAudioFileParser& _parser; - std::unique_ptr _parsedTrack; - std::vector _parsedImages; - }; + const TrackMetadataParser& _metadataParser; + const audio::ParserOptions& _parserOptions; + struct AudioFileInfo + { + audio::AudioProperties audioProperties; + Track track; + std::vector images; + }; + std::optional _file; + }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp similarity index 71% rename from src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp index 5b8d7a0c..f38d65af 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.cpp @@ -21,53 +21,64 @@ #include "core/IConfig.hpp" #include "core/Service.hpp" + +#include "audio/IAudioFileInfo.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/Track.hpp" -#include "metadata/IAudioFileParser.hpp" -#include "AudioFileScanOperation.hpp" #include "ScannerSettings.hpp" -#include "Utils.hpp" +#include "scanners/Utils.hpp" +#include "scanners/audiofile/AudioFileScanOperation.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms::scanner { namespace { - metadata::ParserReadStyle getParserReadStyle() + audio::ParserOptions::AudioPropertiesReadStyle getParserReadStyle() { std::string_view readStyle{ core::Service::get()->getString("scanner-parser-read-style", "average") }; if (readStyle == "fast") - return metadata::ParserReadStyle::Fast; + return audio::ParserOptions::AudioPropertiesReadStyle::Fast; if (readStyle == "average") - return metadata::ParserReadStyle::Average; + return audio::ParserOptions::AudioPropertiesReadStyle::Average; if (readStyle == "accurate") - return metadata::ParserReadStyle::Accurate; + return audio::ParserOptions::AudioPropertiesReadStyle::Accurate; throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; } - metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings) + TrackMetadataParser::Parameters createTrackMetadataParserParameters(const ScannerSettings& settings) { - metadata::AudioFileParserParameters params; + TrackMetadataParser::Parameters params; params.userExtraTags = settings.extraTags; params.artistTagDelimiters = settings.artistTagDelimiters; params.defaultTagDelimiters = settings.defaultTagDelimiters; params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end()); - params.backend = metadata::ParserBackend::TagLib; - params.readStyle = getParserReadStyle(); return params; } + audio::ParserOptions createAudioFileParserOptions() + { + audio::ParserOptions options; + options.readStyle = getParserReadStyle(); + options.parser = audio::ParserOptions::Parser::TagLib; // For now, always use TagLib + + return options; + } + } // namespace AudioFileScanner::AudioFileScanner(db::IDb& db, const ScannerSettings& settings) : _db{ db } , _settings{ settings } - , _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib + , _trackMetadataParser{ createTrackMetadataParserParameters(settings) } + , _parserOptions{ createAudioFileParserOptions() } { } @@ -85,7 +96,7 @@ namespace lms::scanner std::span AudioFileScanner::getSupportedExtensions() const { - return _metadataParser->getSupportedExtensions(); + return audio::getSupportedExtensions(_parserOptions.parser); } bool AudioFileScanner::needsScan(const FileToScan& file) const @@ -101,6 +112,6 @@ namespace lms::scanner std::unique_ptr AudioFileScanner::createScanOperation(FileToScan&& fileToScan) const { - return std::make_unique(std::move(fileToScan), _db, _settings, *_metadataParser); + return std::make_unique(std::move(fileToScan), _db, _settings, _trackMetadataParser, _parserOptions); } } // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp similarity index 88% rename from src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp index 924f4baf..5a8b5c32 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanner.hpp @@ -19,7 +19,10 @@ #pragma once -#include "IFileScanner.hpp" +#include "audio/IAudioFileInfo.hpp" + +#include "scanners/IFileScanner.hpp" +#include "scanners/audiofile/TrackMetadataParser.hpp" namespace lms { @@ -55,6 +58,7 @@ namespace lms::scanner db::IDb& _db; const ScannerSettings& _settings; - std::unique_ptr _metadataParser; + const TrackMetadataParser _trackMetadataParser; + const audio::ParserOptions _parserOptions; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/metadata/impl/AudioFileParser.cpp b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp similarity index 78% rename from src/libs/metadata/impl/AudioFileParser.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp index 5878e7d4..230d2e30 100644 --- a/src/libs/metadata/impl/AudioFileParser.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp @@ -17,30 +17,26 @@ * along with LMS. If not, see . */ -#include "AudioFileParser.hpp" +#include "TrackMetadataParser.hpp" #include #include #include +#include "audio/IAudioFileInfo.hpp" #include "core/ILogger.hpp" #include "core/PartialDateTime.hpp" #include "core/String.hpp" -#include "metadata/Exception.hpp" + +#include "scanners/lyrics/LyricsParser.hpp" #include "Utils.hpp" -#include "avformat/AvFormatImageReader.hpp" -#include "avformat/AvFormatTagReader.hpp" -#include "avformat/Utils.hpp" -#include "taglib/TagLibImageReader.hpp" -#include "taglib/TagLibTagReader.hpp" -#include "taglib/Utils.hpp" -namespace lms::metadata +namespace lms::scanner { namespace { - void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span tagDelimiters, ITagReader::TagValueVisitor visitor) + void visitTagValues(const audio::ITagReader& tagReader, std::string_view tagType, std::span tagDelimiters, audio::ITagReader::TagValueVisitor visitor) { tagReader.visitTagValues(tagType, [&](std::string_view value) { auto visitTagIfNonEmpty{ [&](std::string_view tag) { @@ -76,11 +72,11 @@ namespace lms::metadata } template - std::vector getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters, const WhiteList* whitelist = nullptr) + std::vector getTagValuesFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters, const TrackMetadataParser::WhiteList* whitelist = nullptr) { std::vector res; - for (const TagType tagType : tagTypes) + for (const audio::TagType tagType : tagTypes) { tagReader.visitTagValues(tagType, [&](std::string_view value) { value = core::stringUtils::stringTrim(value); @@ -150,7 +146,7 @@ namespace lms::metadata } template - std::optional getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes) + std::optional getTagValueFirstMatchAs(const audio::ITagReader& tagReader, std::initializer_list tagTypes) { std::optional res; std::vector values{ getTagValuesFirstMatchAs(tagReader, tagTypes, {} /* don't expect multiple values here */) }; @@ -161,45 +157,38 @@ namespace lms::metadata } template - std::vector getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span tagDelimiters) + std::vector getTagValuesAs(const audio::ITagReader& tagReader, audio::TagType tagType, std::span tagDelimiters) { return getTagValuesFirstMatchAs(tagReader, { tagType }, tagDelimiters); } template - std::optional getTagValueAs(const ITagReader& tagReader, TagType tagType) + std::optional getTagValueAs(const audio::ITagReader& tagReader, audio::TagType tagType) { return getTagValueFirstMatchAs(tagReader, { tagType }); } - std::vector getLyrics(const ITagReader& tagReader) + std::vector getLyrics(const audio::ITagReader& tagReader) { std::vector res; tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) { std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?) - try - { - Lyrics lyrics{ parseLyrics(iss) }; - if (lyrics.language.empty()) - lyrics.language = language; + Lyrics lyrics{ parseLyrics(iss) }; + if (lyrics.language.empty()) + lyrics.language = language; - res.emplace_back(std::move(lyrics)); - } - catch (const LyricsException& e) - { - LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() }); - } + res.emplace_back(std::move(lyrics)); }); return res; } - std::vector getArtists(const ITagReader& tagReader, - std::initializer_list artistTagNames, - std::initializer_list artistSortTagNames, - std::initializer_list artistMBIDTagNames, - const AudioFileParserParameters& params) + std::vector getArtists(const audio::ITagReader& tagReader, + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames, + const TrackMetadataParser::Parameters& params) { std::vector artistNames{ getTagValuesFirstMatchAs(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) }; if (artistNames.empty()) @@ -224,7 +213,7 @@ namespace lms::metadata return artists; } - PerformerContainer getPerformerArtists(const ITagReader& tagReader) + PerformerContainer getPerformerArtists(const audio::ITagReader& tagReader) { PerformerContainer performers; @@ -298,9 +287,9 @@ namespace lms::metadata return artistDisplayName; } - std::optional getAdvisory(const ITagReader& tagReader) + std::optional getAdvisory(const audio::ITagReader& tagReader) { - if (const auto value{ getTagValueAs(tagReader, TagType::Advisory) }) + if (const auto value{ getTagValueAs(tagReader, audio::TagType::Advisory) }) { switch (*value) { @@ -318,92 +307,24 @@ namespace lms::metadata } } // namespace - std::unique_ptr createAudioFileParser(const AudioFileParserParameters& params) - { - return std::make_unique(params); - } - - AudioFileParser::AudioFileParser(const AudioFileParserParameters& params) + TrackMetadataParser::TrackMetadataParser(const Parameters& params) : _params{ params } { - switch (_params.backend) - { - case ParserBackend::TagLib: - LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle)); - break; - - case ParserBackend::AvFormat: - LMS_LOG(METADATA, INFO, "Using AvFormat parser"); - break; - } } - std::span AudioFileParser::getSupportedExtensions() const + TrackMetadataParser::~TrackMetadataParser() = default; + + Track TrackMetadataParser::parseTrackMetaData(const audio::ITagReader& tagReader) const { - switch (_params.backend) - { - case ParserBackend::TagLib: - return taglib::utils::getSupportedExtensions(); - break; - - case ParserBackend::AvFormat: - return avformat::utils::getSupportedExtensions(); - break; - } - - return {}; - } - - std::unique_ptr AudioFileParser::parseMetaData(const std::filesystem::path& p) const - { - std::unique_ptr tagReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - tagReader = std::make_unique(p, _params.readStyle, _params.debug); - break; - - case ParserBackend::AvFormat: - tagReader = std::make_unique(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 imageReader; - switch (_params.backend) - { - case ParserBackend::TagLib: - imageReader = std::make_unique(p); - break; - - case ParserBackend::AvFormat: - imageReader = std::make_unique(p); - break; - } - if (!imageReader) - throw AudioFileParsingException{ "Unhandled parser backend" }; - - parseImages(*imageReader, std::move(visitor)); - } - - std::unique_ptr AudioFileParser::parseMetaData(const ITagReader& tagReader) const - { - auto track{ std::make_unique() }; - - track->audioProperties = tagReader.getAudioProperties(); - processTags(tagReader, *track); - + Track track; + processTags(tagReader, track); return track; } - void AudioFileParser::processTags(const ITagReader& tagReader, Track& track) const + void TrackMetadataParser::processTags(const audio::ITagReader& tagReader, Track& track) const { + using namespace audio; + track.title = getTagValueAs(tagReader, TagType::TrackTitle).value_or(""); track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID); track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); @@ -469,8 +390,10 @@ namespace lms::metadata track.originalYear = track.originalDate.getYear(); } - std::optional AudioFileParser::getMedium(const ITagReader& tagReader) const + std::optional TrackMetadataParser::getMedium(const audio::ITagReader& tagReader) const { + using namespace audio; + std::optional medium; medium.emplace(); @@ -499,8 +422,10 @@ namespace lms::metadata return medium; } - std::optional AudioFileParser::getRelease(const ITagReader& tagReader) const + std::optional TrackMetadataParser::getRelease(const audio::ITagReader& tagReader) const { + using namespace audio; + std::optional release; auto releaseName{ getTagValueAs(tagReader, TagType::Album) }; @@ -536,11 +461,4 @@ namespace lms::metadata return release; } - - void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor) - { - reader.visitImages([&](const Image& image) { - visitor(image); - }); - } -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp new file mode 100644 index 00000000..72ccc19d --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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; + struct Parameters + { + std::vector artistTagDelimiters; + WhiteList artistsToNotSplit; + std::vector defaultTagDelimiters; + std::vector 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 getMedium(const audio::ITagReader& tagReader) const; + std::optional getRelease(const audio::ITagReader& tagReader) const; + + const Parameters _params; + }; +} // namespace lms::scanner diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp similarity index 88% rename from src/libs/metadata/impl/Utils.cpp rename to src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp index 1152b95e..01a67610 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/Utils.cpp @@ -24,9 +24,7 @@ #include #include -#include "core/Exception.hpp" - -namespace lms::metadata::utils +namespace lms::scanner::utils { Wt::WDate parseDate(std::string_view dateStr) { @@ -96,21 +94,6 @@ namespace lms::metadata::utils return result * sign; } - std::string_view readStyleToString(ParserReadStyle readStyle) - { - switch (readStyle) - { - case ParserReadStyle::Fast: - return "fast"; - case ParserReadStyle::Average: - return "average"; - case ParserReadStyle::Accurate: - return "accurate"; - } - - throw Exception{ "Unknown read style" }; - } - PerformerArtist extractPerformerAndRole(std::string_view entry) { std::string_view artistName; @@ -155,4 +138,4 @@ namespace lms::metadata::utils return PerformerArtist{ Artist{ artistName }, std::string{ role } }; } -} // namespace lms::metadata::utils \ No newline at end of file +} // namespace lms::scanner::utils \ No newline at end of file diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp similarity index 86% rename from src/libs/metadata/impl/Utils.hpp rename to src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp index 3302fed0..f734229a 100644 --- a/src/libs/metadata/impl/Utils.hpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/Utils.hpp @@ -25,13 +25,12 @@ #include -#include "metadata/Types.hpp" +#include "types/TrackMetadata.hpp" -namespace lms::metadata::utils +namespace lms::scanner::utils { Wt::WDate parseDate(std::string_view dateStr); std::optional parseYear(std::string_view yearStr); - std::string_view readStyleToString(ParserReadStyle readStyle); struct PerformerArtist { @@ -41,4 +40,4 @@ namespace lms::metadata::utils // format is "artist name (role)" PerformerArtist extractPerformerAndRole(std::string_view entry); -} // namespace lms::metadata::utils +} // namespace lms::scanner::utils diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp similarity index 87% rename from src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp index de85094a..b7c943e7 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.cpp @@ -22,17 +22,19 @@ #include #include -#include "FileScanOperationBase.hpp" -#include "ScannerSettings.hpp" #include "core/ILogger.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/TrackLyrics.hpp" -#include "metadata/Lyrics.hpp" #include "services/scanner/ScanErrors.hpp" -#include "Utils.hpp" +#include "ScannerSettings.hpp" +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/lyrics/LyricsParser.hpp" +#include "types/Lyrics.hpp" namespace lms::scanner { @@ -48,28 +50,21 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - std::optional _parsedLyrics; + std::optional _parsedLyrics; }; void LyricsFileScanOperation::scan() { - try + std::ifstream ifs{ getFilePath() }; + if (!ifs) { - std::ifstream ifs{ getFilePath() }; - if (!ifs) - { - const std::error_code ec{ errno, std::generic_category() }; + const std::error_code ec{ errno, std::generic_category() }; - addError(getFilePath(), ec); - return; - } + addError(getFilePath(), ec); + return; + } - _parsedLyrics = metadata::parseLyrics(ifs); - } - catch (const metadata::Exception& e) - { - addError(getFilePath()); - } + _parsedLyrics = parseLyrics(ifs); } LyricsFileScanOperation::OperationResult LyricsFileScanOperation::processResult() @@ -140,7 +135,7 @@ namespace lms::scanner std::span LyricsFileScanner::getSupportedExtensions() const { - return metadata::getSupportedLyricsFileExtensions(); + return getSupportedLyricsFileExtensions(); } bool LyricsFileScanner::needsScan(const FileToScan& file) const diff --git a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp index 0b265b77..e713bd32 100644 --- a/src/libs/services/scanner/impl/scanners/LyricsFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/Lyrics.cpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp similarity index 98% rename from src/libs/metadata/impl/Lyrics.cpp rename to src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp index 45b66cc8..f096a766 100644 --- a/src/libs/metadata/impl/Lyrics.cpp +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.cpp @@ -17,14 +17,14 @@ * along with LMS. If not, see . */ -#include "metadata/Lyrics.hpp" +#include "LyricsParser.hpp" #include #include #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { std::span getSupportedLyricsFileExtensions() { @@ -227,4 +227,4 @@ namespace lms::metadata return lyrics; } -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp new file mode 100644 index 00000000..a20c40df --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/lyrics/LyricsParser.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +#include "types/Lyrics.hpp" + +namespace lms::scanner +{ + std::span getSupportedLyricsFileExtensions(); + Lyrics parseLyrics(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp similarity index 85% rename from src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp index 809ed1fa..782eeff0 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.cpp @@ -27,12 +27,12 @@ #include "database/Session.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/PlayListFile.hpp" -#include "metadata/Exception.hpp" -#include "metadata/PlayList.hpp" -#include "FileScanOperationBase.hpp" -#include "ScanContext.hpp" -#include "Utils.hpp" +#include "services/scanner/ScanErrors.hpp" + +#include "scanners/FileScanOperationBase.hpp" +#include "scanners/Utils.hpp" +#include "scanners/playlist/PlayListParser.hpp" namespace lms::scanner { @@ -51,28 +51,21 @@ namespace lms::scanner void scan() override; OperationResult processResult() override; - std::optional _parsedPlayList; + std::optional _parsedPlayList; }; void PlayListFileScanOperation::scan() { - try + std::ifstream ifs{ getFilePath() }; + if (!ifs) { - std::ifstream ifs{ getFilePath() }; - if (!ifs) - { - const std::error_code ec{ errno, std::generic_category() }; + const std::error_code ec{ errno, std::generic_category() }; - addError(getFilePath(), ec); - return; - } + addError(getFilePath(), ec); + return; + } - _parsedPlayList = metadata::parsePlayList(ifs); - } - catch (const metadata::Exception& e) - { - addError(getFilePath()); - } + _parsedPlayList = parsePlayList(ifs); } PlayListFileScanOperation::OperationResult PlayListFileScanOperation::processResult() @@ -139,7 +132,7 @@ namespace lms::scanner std::span PlayListFileScanner::getSupportedExtensions() const { - return metadata::getSupportedPlayListFileExtensions(); + return getSupportedPlayListFileExtensions(); } bool PlayListFileScanner::needsScan(const FileToScan& file) const diff --git a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp similarity index 97% rename from src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp index 0bbfb8a4..9da3db17 100644 --- a/src/libs/services/scanner/impl/scanners/PlayListFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListFileScanner.hpp @@ -19,7 +19,7 @@ #pragma once -#include "IFileScanner.hpp" +#include "scanners/IFileScanner.hpp" namespace lms::db { diff --git a/src/libs/metadata/impl/PlayList.cpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp similarity index 97% rename from src/libs/metadata/impl/PlayList.cpp rename to src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp index ca0fadb5..b12c5b85 100644 --- a/src/libs/metadata/impl/PlayList.cpp +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.cpp @@ -17,14 +17,14 @@ * along with LMS. If not, see . */ -#include "metadata/PlayList.hpp" +#include "PlayListParser.hpp" #include #include #include "core/String.hpp" -namespace lms::metadata +namespace lms::scanner { std::span getSupportedPlayListFileExtensions() { @@ -102,4 +102,4 @@ namespace lms::metadata return playlist; } -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp new file mode 100644 index 00000000..edef9ca1 --- /dev/null +++ b/src/libs/services/scanner/impl/scanners/playlist/PlayListParser.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +#include "types/PlayList.hpp" + +namespace lms::scanner +{ + std::span getSupportedPlayListFileExtensions(); + PlayList parsePlayList(std::istream& is); +} // namespace lms::scanner diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp index 77d2bbdd..b98997a8 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp @@ -30,11 +30,11 @@ #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackList.hpp" -#include "metadata/Types.hpp" #include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "helpers/ArtistHelpers.hpp" +#include "types/TrackMetadata.hpp" namespace lms::scanner { @@ -53,7 +53,7 @@ namespace lms::scanner { assert(!link->isArtistMBIDMatched()); - metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional(link->getArtistSortName()) }; + Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional(link->getArtistSortName()) }; db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) }; LMS_LOG(DB, DEBUG, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast(link->getType()) << " from " << link->getArtist() << " to " << newArtist); @@ -66,7 +66,7 @@ namespace lms::scanner { assert(!artistInfo->isMBIDMatched()); - const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional(artistInfo->getSortName()) }; + Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional(artistInfo->getSortName()) }; db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) }; LMS_LOG(DB, DEBUG, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist); diff --git a/src/libs/metadata/include/metadata/ArtistInfo.hpp b/src/libs/services/scanner/impl/types/ArtistInfo.hpp similarity index 76% rename from src/libs/metadata/include/metadata/ArtistInfo.hpp rename to src/libs/services/scanner/impl/types/ArtistInfo.hpp index 75af760f..e39be46c 100644 --- a/src/libs/metadata/include/metadata/ArtistInfo.hpp +++ b/src/libs/services/scanner/impl/types/ArtistInfo.hpp @@ -19,17 +19,12 @@ #pragma once -#include -#include #include -#include #include #include "core/UUID.hpp" -#include "metadata/Exception.hpp" - -namespace lms::metadata +namespace lms::scanner { // See: // - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists @@ -44,13 +39,4 @@ namespace lms::metadata std::string disambiguation; // mb std::string biography; }; - - class ArtistInfoParseException : public Exception - { - public: - using Exception::Exception; - }; - - std::span getSupportedArtistInfoFiles(); - ArtistInfo parseArtistInfo(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/Lyrics.hpp b/src/libs/services/scanner/impl/types/Lyrics.hpp similarity index 74% rename from src/libs/metadata/include/metadata/Lyrics.hpp rename to src/libs/services/scanner/impl/types/Lyrics.hpp index ccfb348c..5b4f9be4 100644 --- a/src/libs/metadata/include/metadata/Lyrics.hpp +++ b/src/libs/services/scanner/impl/types/Lyrics.hpp @@ -20,16 +20,11 @@ #pragma once #include -#include -#include #include -#include #include #include -#include "metadata/Exception.hpp" - -namespace lms::metadata +namespace lms::scanner { struct Lyrics { @@ -42,13 +37,4 @@ namespace lms::metadata std::map synchronizedLines; std::vector unsynchronizedLines; }; - - class LyricsException : public metadata::Exception - { - public: - using metadata::Exception::Exception; - }; - - std::span getSupportedLyricsFileExtensions(); - Lyrics parseLyrics(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/PlayList.hpp b/src/libs/services/scanner/impl/types/PlayList.hpp similarity index 80% rename from src/libs/metadata/include/metadata/PlayList.hpp rename to src/libs/services/scanner/impl/types/PlayList.hpp index b3f51a56..f849a9ba 100644 --- a/src/libs/metadata/include/metadata/PlayList.hpp +++ b/src/libs/services/scanner/impl/types/PlayList.hpp @@ -20,19 +20,14 @@ #pragma once #include -#include -#include #include #include -namespace lms::metadata +namespace lms::scanner { struct PlayList { std::string name; std::vector files; }; - - std::span getSupportedPlayListFileExtensions(); - PlayList parsePlayList(std::istream& is); -} // namespace lms::metadata +} // namespace lms::scanner diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/services/scanner/impl/types/TrackMetadata.hpp similarity index 55% rename from src/libs/metadata/include/metadata/Types.hpp rename to src/libs/services/scanner/impl/types/TrackMetadata.hpp index 873d71f5..319c5fd8 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/services/scanner/impl/types/TrackMetadata.hpp @@ -19,11 +19,8 @@ #pragma once -#include #include #include -#include -#include #include #include #include @@ -31,9 +28,9 @@ #include "core/PartialDateTime.hpp" #include "core/UUID.hpp" -#include "Lyrics.hpp" +#include "types/Lyrics.hpp" -namespace lms::metadata +namespace lms::scanner { using Tags = std::map /* values */>; @@ -94,15 +91,6 @@ namespace lms::metadata } }; - struct AudioProperties - { - std::size_t bitrate{}; - std::size_t bitsPerSample{}; - std::size_t channelCount{}; - std::chrono::milliseconds duration{}; - std::size_t sampleRate{}; - }; - struct Track { enum class Advisory @@ -111,7 +99,7 @@ namespace lms::metadata Explicit, Clean, }; - AudioProperties audioProperties; + std::optional mbid; std::optional recordingMBID; std::string title; @@ -143,96 +131,4 @@ namespace lms::metadata std::vector producerArtists; std::vector remixerArtists; }; - - struct Image - { - // See TagLib types (based on ID3v2 APIC types) - enum class Type - { - // No information - Unknown, - // A type not enumerated below - Other, - // 32x32 PNG image that should be used as the file icon - FileIcon, - // File icon of a different size or format - OtherFileIcon, - // Front cover image of the album - FrontCover, - // Back cover image of the album - BackCover, - // Inside leaflet page of the album - LeafletPage, - // Image from the album itself - Media, - // Picture of the lead artist or soloist - LeadArtist, - // Picture of the artist or performer - Artist, - // Picture of the conductor - Conductor, - // Picture of the band or orchestra - Band, - // Picture of the composer - Composer, - // Picture of the lyricist or text writer - Lyricist, - // Picture of the recording location or studio - RecordingLocation, - // Picture of the artists during recording - DuringRecording, - // Picture of the artists during performance - DuringPerformance, - // Picture from a movie or video related to the track - MovieScreenCapture, - // Picture of a large, coloured fish - ColouredFish, - // Illustration related to the track - Illustration, - // Logo of the band or performer - BandLogo, - // Logo of the publisher (record company) - PublisherLogo - }; - - Type type{ Type::Unknown }; - std::string mimeType{ "application/octet-stream" }; - std::string description; - std::span 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; - struct AudioFileParserParameters - { - ParserBackend backend{ ParserBackend::TagLib }; - ParserReadStyle readStyle{ ParserReadStyle::Average }; - std::vector artistTagDelimiters; - WhiteList artistsToNotSplit; - std::vector defaultTagDelimiters; - std::vector userExtraTags; - bool debug{}; - }; -} // namespace lms::metadata \ No newline at end of file +} // namespace lms::scanner \ No newline at end of file diff --git a/src/libs/metadata/test/ArtistInfo.cpp b/src/libs/services/scanner/test/ArtistInfo.cpp similarity index 98% rename from src/libs/metadata/test/ArtistInfo.cpp rename to src/libs/services/scanner/test/ArtistInfo.cpp index aa84b87e..c8f87b09 100644 --- a/src/libs/metadata/test/ArtistInfo.cpp +++ b/src/libs/services/scanner/test/ArtistInfo.cpp @@ -20,9 +20,9 @@ #include -#include "metadata/ArtistInfo.hpp" +#include "scanners/artistinfo/ArtistInfoParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { TEST(ArtistInfo, basic) { @@ -112,4 +112,4 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl ASSERT_EQ(artistInfo.sortName, "Artist, My"); ASSERT_EQ(artistInfo.disambiguation, "My Artist"); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/Utils.cpp b/src/libs/services/scanner/test/AudioFileUtils.cpp similarity index 98% rename from src/libs/metadata/test/Utils.cpp rename to src/libs/services/scanner/test/AudioFileUtils.cpp index 925675ea..c906a918 100644 --- a/src/libs/metadata/test/Utils.cpp +++ b/src/libs/services/scanner/test/AudioFileUtils.cpp @@ -20,9 +20,9 @@ #include -#include "Utils.hpp" +#include "scanners/audiofile/Utils.hpp" -namespace lms::metadata::utils::tests +namespace lms::scanner::utils::tests { TEST(MetaData, parseDate) { @@ -149,4 +149,4 @@ namespace lms::metadata::utils::tests EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; } } -} // namespace lms::metadata::utils::tests \ No newline at end of file +} // namespace lms::scanner::utils::tests \ No newline at end of file diff --git a/src/libs/services/scanner/test/CMakeLists.txt b/src/libs/services/scanner/test/CMakeLists.txt new file mode 100644 index 00000000..a2210067 --- /dev/null +++ b/src/libs/services/scanner/test/CMakeLists.txt @@ -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() + diff --git a/src/libs/metadata/test/Lyrics.cpp b/src/libs/services/scanner/test/Lyrics.cpp similarity index 99% rename from src/libs/metadata/test/Lyrics.cpp rename to src/libs/services/scanner/test/Lyrics.cpp index aef7835b..06d0ef3b 100644 --- a/src/libs/metadata/test/Lyrics.cpp +++ b/src/libs/services/scanner/test/Lyrics.cpp @@ -20,9 +20,9 @@ #include -#include "metadata/Lyrics.hpp" +#include "scanners/lyrics/LyricsParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { using namespace std::chrono_literals; @@ -397,4 +397,4 @@ I, I just woke up from a dream EXPECT_EQ(lyrics.unsynchronizedLines[3], "I, I just woke up from a dream"); EXPECT_EQ(lyrics.unsynchronizedLines[4], ""); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/PlayList.cpp b/src/libs/services/scanner/test/PlayList.cpp similarity index 92% rename from src/libs/metadata/test/PlayList.cpp rename to src/libs/services/scanner/test/PlayList.cpp index 03be747c..e067a56f 100644 --- a/src/libs/metadata/test/PlayList.cpp +++ b/src/libs/services/scanner/test/PlayList.cpp @@ -21,11 +21,11 @@ #include #include -#include "metadata/PlayList.hpp" +#include "scanners/playlist/PlayListParser.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { - TEST(PlayList, basic) + TEST(Scanner, playlist) { std::istringstream is{ R"(#EXTM3U #PLAYLIST:My super playlist @@ -53,7 +53,7 @@ one to be/../one to be/normalized/foo.mp3)" }; EXPECT_EQ(playlist.files[5], "one to be/normalized/foo.mp3"); } - TEST(PlayList, UTF8_bom) + TEST(Scanner, playlist_UTF8_bom) { const unsigned char content[] = { 0xEF, 0xBB, 0xBF, '#', 'E', 'X', 'T', 'M', '3', 'U', '\r', '\n', '\r', '\n', '.', '.', '/', 't', 'e', 's', 't', '.', 'm', 'p', '3', '\r', '\n' }; std::istringstream is{ std::string(reinterpret_cast(content), sizeof(content)) }; @@ -64,4 +64,4 @@ one to be/../one to be/normalized/foo.mp3)" }; EXPECT_EQ(playlist.files[0], "../test.mp3"); } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/metadata/test/Metadata.cpp b/src/libs/services/scanner/test/Scanner.cpp similarity index 95% rename from src/libs/metadata/test/Metadata.cpp rename to src/libs/services/scanner/test/Scanner.cpp index dc673372..fa26648b 100644 --- a/src/libs/metadata/test/Metadata.cpp +++ b/src/libs/services/scanner/test/Scanner.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Emeric Poupon + * Copyright (C) 2025 Emeric Poupon * * This file is part of LMS. * diff --git a/src/libs/metadata/test/TestTagReader.hpp b/src/libs/services/scanner/test/TestTagReader.hpp similarity index 90% rename from src/libs/metadata/test/TestTagReader.hpp rename to src/libs/services/scanner/test/TestTagReader.hpp index d7f6b11a..3ba949a0 100644 --- a/src/libs/metadata/test/TestTagReader.hpp +++ b/src/libs/services/scanner/test/TestTagReader.hpp @@ -21,22 +21,14 @@ #include #include -#include "ITagReader.hpp" +#include "audio/ITagReader.hpp" -namespace lms::metadata::tests +namespace lms::scanner::tests { - class TestTagReader : public ITagReader + class TestTagReader : public audio::ITagReader { public: - static constexpr AudioProperties audioProperties{ - .bitrate = 128000, - .bitsPerSample = 16, - .channelCount = 2, - .duration = std::chrono::seconds{ 180 }, - .sampleRate = 44000, - }; - - using Tags = std::unordered_map>; + using Tags = std::unordered_map>; using Performers = std::unordered_map /*names*/>; using ExtraUserTags = std::unordered_map>; using LyricsTags = std::unordered_map; @@ -63,7 +55,7 @@ namespace lms::metadata::tests _lyricsTags = std::move(lyricsTags); } - void visitTagValues(TagType tag, TagValueVisitor visitor) const override + void visitTagValues(audio::TagType tag, TagValueVisitor visitor) const override { auto itValues{ _tags.find(tag) }; if (itValues != std::cend(_tags)) @@ -97,8 +89,6 @@ namespace lms::metadata::tests visitor(language, lyrics); } - const AudioProperties& getAudioProperties() const override { return audioProperties; } - private: const Tags _tags; Performers _performers; @@ -106,8 +96,10 @@ namespace lms::metadata::tests LyricsTags _lyricsTags; }; - inline std::unique_ptr createDefaultPopulatedTestTagReader() + inline std::unique_ptr createDefaultPopulatedTestTagReader() { + using namespace audio; + std::unique_ptr testTags{ std::make_unique( TestTagReader::Tags{ { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, @@ -168,4 +160,4 @@ namespace lms::metadata::tests return testTags; } -} // namespace lms::metadata::tests \ No newline at end of file +} // namespace lms::scanner::tests \ No newline at end of file diff --git a/src/libs/services/scanner/test/TrackMetadataParser.cpp b/src/libs/services/scanner/test/TrackMetadataParser.cpp new file mode 100644 index 00000000..ddfb87ab --- /dev/null +++ b/src/libs/services/scanner/test/TrackMetadataParser.cpp @@ -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 . + */ + +#include + +#include + +#include + +#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 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 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 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 diff --git a/src/libs/services/transcoding/CMakeLists.txt b/src/libs/services/transcoding/CMakeLists.txt index e57e0b5f..27caf7b2 100644 --- a/src/libs/services/transcoding/CMakeLists.txt +++ b/src/libs/services/transcoding/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(lmstranscoding STATIC - impl/TranscodingResourceHandler.cpp - impl/TranscodingService.cpp + impl/TranscodeResourceHandler.cpp + impl/TranscodeService.cpp ) target_include_directories(lmstranscoding INTERFACE @@ -13,7 +13,7 @@ target_include_directories(lmstranscoding PRIVATE ) target_link_libraries(lmstranscoding PRIVATE - lmsav + lmsaudio ) target_link_libraries(lmstranscoding PUBLIC diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp b/src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp similarity index 77% rename from src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp rename to src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp index 8c21b93f..40b6393a 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.cpp +++ b/src/libs/services/transcoding/impl/TranscodeResourceHandler.cpp @@ -17,42 +17,38 @@ * along with LMS. If not, see . */ -#include "TranscodingResourceHandler.hpp" +#include "TranscodeResourceHandler.hpp" -#include "av/Exception.hpp" -#include "av/ITranscoder.hpp" #include "core/ILogger.hpp" +#include "audio/Exception.hpp" +#include "audio/ITranscoder.hpp" + namespace lms::transcoding { - std::unique_ptr createResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, bool estimateContentLength) - { - return std::make_unique(inputParameters, outputParameters, estimateContentLength); - } - // TODO set some nice HTTP return code - TranscodingResourceHandler::TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength) + ResourceHandler::ResourceHandler(const audio::TranscodeParameters& parameters, std::optional estimatedContentLength) : _estimatedContentLength{ estimatedContentLength } { try { - _transcoder = av::createTranscoder(inputParameters, outputParameters); + _transcoder = createTranscoder(parameters); if (_estimatedContentLength) LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength); else LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length"); } - catch (av::Exception& e) + catch (audio::Exception& e) { LMS_LOG(TRANSCODING, ERROR, "Failed to create transcoder: " << e.what()); } } - TranscodingResourceHandler::~TranscodingResourceHandler() = default; + ResourceHandler::~ResourceHandler() = default; - Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) + Wt::Http::ResponseContinuation* ResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { if (!_transcoder) { diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp b/src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp similarity index 70% rename from src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp rename to src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp index 0674a1a7..edd2b86f 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp +++ b/src/libs/services/transcoding/impl/TranscodeResourceHandler.hpp @@ -23,19 +23,19 @@ #include #include -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" #include "core/IResourceHandler.hpp" namespace lms::transcoding { - class TranscodingResourceHandler final : public core::IResourceHandler + class ResourceHandler final : public core::IResourceHandler { public: - TranscodingResourceHandler(const av::InputParameters& inputParameters, const av::OutputParameters& outputParameters, std::optional estimatedContentLength); - ~TranscodingResourceHandler() override; + ResourceHandler(const audio::TranscodeParameters& parameters, std::optional estimatedContentLength); + ~ResourceHandler() override; - TranscodingResourceHandler(const TranscodingResourceHandler&) = delete; - TranscodingResourceHandler& operator=(const TranscodingResourceHandler&) = delete; + ResourceHandler(const ResourceHandler&) = delete; + ResourceHandler& operator=(const ResourceHandler&) = delete; private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; @@ -46,6 +46,6 @@ namespace lms::transcoding std::array _buffer; std::size_t _bytesReadyCount{}; std::size_t _totalServedByteCount{}; - std::unique_ptr _transcoder; + std::unique_ptr _transcoder; }; } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.cpp b/src/libs/services/transcoding/impl/TranscodeService.cpp similarity index 50% rename from src/libs/services/transcoding/impl/TranscodingService.cpp rename to src/libs/services/transcoding/impl/TranscodeService.cpp index 31c32f89..f9b503b5 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.cpp +++ b/src/libs/services/transcoding/impl/TranscodeService.cpp @@ -17,24 +17,20 @@ * along with LMS. If not, see . */ -#include "TranscodingService.hpp" +#include "TranscodeService.hpp" -#include "av/ITranscoder.hpp" +#include "audio/ITranscoder.hpp" #include "core/ILogger.hpp" +#include "core/UUID.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" -#include "TranscodingResourceHandler.hpp" +#include "TranscodeResourceHandler.hpp" namespace lms::transcoding { namespace { - av::OutputParameters toAv(const OutputParameters& out) - { - return { .format = static_cast(out.format), .bitrate = out.bitrate, .stripMetadata = out.stripMetadata }; - } - std::size_t doEstimateContentLength(std::size_t bitrate, std::chrono::milliseconds duration) { const std::size_t estimatedContentLength{ static_cast((bitrate / 8 * duration.count()) / 1000) }; @@ -42,40 +38,35 @@ namespace lms::transcoding } } // namespace - std::unique_ptr createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager) + std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) { - return std::make_unique(db, childProcessManager); + return std::make_unique(db, childProcessManager); } - TranscodingService::TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager) + TranscodeService::TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager) : _db{ db } , _childProcessManager(childProcessManager) { LMS_LOG(TRANSCODING, INFO, "Service started!"); } - TranscodingService::~TranscodingService() + TranscodeService::~TranscodeService() { LMS_LOG(TRANSCODING, INFO, "Service stopped!"); } - std::unique_ptr TranscodingService::createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) + std::unique_ptr TranscodeService::createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) { - av::InputParameters avInputParams; std::optional estimatedContentLength; - avInputParams.file = inputParameters.filePath; - avInputParams.offset = inputParameters.offset; - avInputParams.streamIndex = inputParameters.streamIndex; - if (estimateContentLength) { - if (inputParameters.offset < inputParameters.duration) - estimatedContentLength = doEstimateContentLength(outputParameters.bitrate, inputParameters.duration - inputParameters.offset); + if (parameters.inputParameters.offset < parameters.inputParameters.duration) + estimatedContentLength = doEstimateContentLength(*parameters.outputParameters.bitrate, parameters.inputParameters.duration - parameters.inputParameters.offset); else - LMS_LOG(TRANSCODING, WARNING, "Offset " << inputParameters.offset << " is greater than audio file duration " << inputParameters.duration << ": not estimating content length"); + LMS_LOG(TRANSCODING, WARNING, "Offset " << parameters.inputParameters.offset << " is greater than audio file duration " << parameters.inputParameters.duration << ": not estimating content length"); } - return std::make_unique(avInputParams, toAv(outputParameters), estimatedContentLength); + return std::make_unique(parameters, estimatedContentLength); } } // namespace lms::transcoding diff --git a/src/libs/services/transcoding/impl/TranscodingService.hpp b/src/libs/services/transcoding/impl/TranscodeService.hpp similarity index 60% rename from src/libs/services/transcoding/impl/TranscodingService.hpp rename to src/libs/services/transcoding/impl/TranscodeService.hpp index d5ea4471..82b86f88 100644 --- a/src/libs/services/transcoding/impl/TranscodingService.hpp +++ b/src/libs/services/transcoding/impl/TranscodeService.hpp @@ -19,21 +19,21 @@ #pragma once -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" namespace lms::transcoding { - class TranscodingService : public ITranscodingService + class TranscodeService : public ITranscodeService { public: - explicit TranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager); - ~TranscodingService() override; + explicit TranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); + ~TranscodeService() override; - TranscodingService(const TranscodingService&) = delete; - TranscodingService& operator=(const TranscodingService&) = delete; + TranscodeService(const TranscodeService&) = delete; + TranscodeService& operator=(const TranscodeService&) = delete; private: - std::unique_ptr createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) override; + std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength) override; db::IDb& _db; core::IChildProcessManager& _childProcessManager; diff --git a/src/libs/metadata/impl/IImageReader.hpp b/src/libs/services/transcoding/include/services/transcoding/Exception.hpp similarity index 70% rename from src/libs/metadata/impl/IImageReader.hpp rename to src/libs/services/transcoding/include/services/transcoding/Exception.hpp index 84e42cc0..bac62dbc 100644 --- a/src/libs/metadata/impl/IImageReader.hpp +++ b/src/libs/services/transcoding/include/services/transcoding/Exception.hpp @@ -19,18 +19,13 @@ #pragma once -#include +#include "core/Exception.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata +namespace lms::transcoding { - class IImageReader + class Exception : public core::LmsException { public: - virtual ~IImageReader() = default; - - using ImageVisitor = std::function; - virtual void visitImages(ImageVisitor visitor) const = 0; + using LmsException::LmsException; }; -} // namespace lms::metadata +} // namespace lms::transcoding \ No newline at end of file diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp new file mode 100644 index 00000000..4bd6eeb0 --- /dev/null +++ b/src/libs/services/transcoding/include/services/transcoding/ITranscodeService.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "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 parseAudioFileInfo(const std::filesystem::path& p) const = 0; + + virtual std::unique_ptr createTranscodeResourceHandler(const audio::TranscodeParameters& parameters, bool estimateContentLength = false) = 0; + }; + + std::unique_ptr createTranscodeService(db::IDb& db, core::IChildProcessManager& childProcessManager); +} // namespace lms::transcoding diff --git a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp b/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp deleted file mode 100644 index 28bf4943..00000000 --- a/src/libs/services/transcoding/include/services/transcoding/ITranscodingService.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include -#include -#include - -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 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 createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength) = 0; - }; - - std::unique_ptr createTranscodingService(db::IDb& db, core::IChildProcessManager& childProcessManager); -} // namespace lms::transcoding diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 002bdfe6..802f5611 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -50,7 +50,7 @@ target_include_directories(lmssubsonic PRIVATE target_link_libraries(lmssubsonic PRIVATE lmsartwork lmsauth - lmsav + lmsaudio lmsdatabase lmsfeedback lmspodcast diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index bffd1f09..43bb604f 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -27,8 +27,9 @@ #include "core/IResourceHandler.hpp" #include "core/String.hpp" -#include "av/Exception.hpp" -#include "av/IAudioFile.hpp" +#include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/objects/PodcastEpisode.hpp" @@ -39,7 +40,7 @@ #include "services/artwork/IArtworkService.hpp" #include "services/podcast/IPodcastService.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "CoverArtId.hpp" #include "ParameterParsing.hpp" @@ -52,12 +53,12 @@ namespace lms::api::subsonic { namespace { - std::optional subsonicStreamFormatToAvOutputFormat(std::string_view format) + std::optional subsonicStreamFormatToTranscodingOutputFormat(std::string_view format) { - for (const auto& [str, avFormat] : std::initializer_list>{ - { "mp3", transcoding::OutputFormat::MP3 }, - { "opus", transcoding::OutputFormat::OGG_OPUS }, - { "vorbis", transcoding::OutputFormat::OGG_VORBIS }, + for (const auto& [str, avFormat] : std::initializer_list>{ + { "mp3", audio::OutputFormat::MP3 }, + { "opus", audio::OutputFormat::OGG_OPUS }, + { "vorbis", audio::OutputFormat::OGG_VORBIS }, }) { if (core::stringUtils::stringCaseInsensitiveEqual(str, format)) @@ -66,38 +67,39 @@ namespace lms::api::subsonic return std::nullopt; } - transcoding::OutputFormat userTranscodeFormatToAvFormat(db::TranscodingOutputFormat format) + audio::OutputFormat userTranscodeFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return transcoding::OutputFormat::MP3; + return audio::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return transcoding::OutputFormat::OGG_OPUS; + return audio::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return transcoding::OutputFormat::MATROSKA_OPUS; + return audio::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return transcoding::OutputFormat::OGG_VORBIS; + return audio::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return transcoding::OutputFormat::WEBM_VORBIS; + return audio::OutputFormat::WEBM_VORBIS; } - return transcoding::OutputFormat::OGG_OPUS; + + return audio::OutputFormat::OGG_OPUS; } - bool isCodecCompatibleWithOutputFormat(av::DecodingCodec codec, transcoding::OutputFormat outputFormat) + bool isCodecCompatibleWithOutputFormat(audio::CodecType codec, audio::OutputFormat outputFormat) { switch (outputFormat) { - case transcoding::OutputFormat::MP3: - return codec == av::DecodingCodec::MP3; + case audio::OutputFormat::MP3: + return codec == audio::CodecType::MP3; - case transcoding::OutputFormat::OGG_OPUS: - case transcoding::OutputFormat::MATROSKA_OPUS: - return codec == av::DecodingCodec::OPUS; + case audio::OutputFormat::OGG_OPUS: + case audio::OutputFormat::MATROSKA_OPUS: + return codec == audio::CodecType::Opus; - case transcoding::OutputFormat::OGG_VORBIS: - case transcoding::OutputFormat::WEBM_VORBIS: - return codec == av::DecodingCodec::VORBIS; + case audio::OutputFormat::OGG_VORBIS: + case audio::OutputFormat::WEBM_VORBIS: + return codec == audio::CodecType::Vorbis; } return true; @@ -105,26 +107,25 @@ namespace lms::api::subsonic struct StreamParameters { - transcoding::InputParameters inputParameters; - std::string inputMimeType; // set if known - std::optional outputParameters; + std::filesystem::path filePath; + std::string fileMimeType; // set if known + std::optional transcodeParameters; bool estimateContentLength{}; }; - bool isOutputFormatCompatible(const std::filesystem::path& trackPath, transcoding::OutputFormat outputFormat) + bool isOutputFormatCompatible(const std::filesystem::path& trackPath, audio::OutputFormat outputFormat) { // TODO: put this information in db during scan try { - const auto audioFile{ av::parseAudioFile(trackPath) }; + const auto audioFile{ audio::parseAudioFile(trackPath) }; - const auto streamInfo{ audioFile->getBestStreamInfo() }; - if (!streamInfo) + if (!audioFile->getAudioProperties().codec) throw RequestedDataNotFoundError{}; // TODO 404? - return isCodecCompatibleWithOutputFormat(streamInfo->codec, outputFormat); + return isCodecCompatibleWithOutputFormat(*audioFile->getAudioProperties().codec, outputFormat); } - catch (const av::Exception& e) + catch (const audio::Exception& e) { // TODO 404? throw RequestedDataNotFoundError{}; @@ -192,21 +193,18 @@ namespace lms::api::subsonic const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioId) }; StreamParameters parameters; - - parameters.inputParameters.filePath = audioFileInfo.path; - parameters.inputParameters.duration = audioFileInfo.duration; - parameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; - parameters.inputMimeType = audioFileInfo.mimeType; + parameters.filePath = audioFileInfo.path; + parameters.fileMimeType = audioFileInfo.mimeType; parameters.estimateContentLength = estimateContentLength; if (format == "raw") // raw => no transcoding return parameters; // TODO: what if offset is not 0? - std::optional requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) }; + std::optional requestedFormat{ subsonicStreamFormatToTranscodingOutputFormat(format) }; if (!requestedFormat) { if (context.getUser()->getSubsonicEnableTranscodingByDefault()) - requestedFormat = userTranscodeFormatToAvFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); + requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); } if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.bitrate <= maxBitRate)) @@ -231,16 +229,22 @@ namespace lms::api::subsonic // Need to transcode here if (!requestedFormat) - requestedFormat = userTranscodeFormatToAvFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); + requestedFormat = userTranscodeFormatToTranscodingFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); if (!bitrate) bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate(); if (maxBitRate) bitrate = std::min(bitrate, maxBitRate); - transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() }; - outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) - outputParameters.format = *requestedFormat; - outputParameters.bitrate = bitrate; + audio::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; + + transcodeParameters.inputParameters.filePath = audioFileInfo.path; + transcodeParameters.inputParameters.duration = audioFileInfo.duration; + transcodeParameters.inputParameters.offset = std::chrono::seconds{ timeOffset }; + ; + + transcodeParameters.outputParameters.bitrate = bitrate; + transcodeParameters.outputParameters.format = *requestedFormat; + transcodeParameters.outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) return parameters; } @@ -357,10 +361,10 @@ namespace lms::api::subsonic if (!continuation) { StreamParameters streamParameters{ getStreamParameters(context) }; - if (streamParameters.outputParameters) - resourceHandler = core::Service::get()->createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength); + if (streamParameters.transcodeParameters) + resourceHandler = core::Service::get()->createTranscodeResourceHandler(*streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = core::createFileResourceHandler(streamParameters.inputParameters.filePath, streamParameters.inputMimeType); + resourceHandler = core::createFileResourceHandler(streamParameters.filePath, streamParameters.fileMimeType); } else { @@ -371,7 +375,7 @@ namespace lms::api::subsonic if (continuation) continuation->setData(resourceHandler); } - catch (const av::Exception& e) + catch (const audio::Exception& e) { response.setStatus(404); // report not found if something wrong happened LMS_LOG(API_SUBSONIC, ERROR, "Caught Av exception: " << e.what()); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 1d1256cd..98d06c54 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -67,7 +67,7 @@ target_link_libraries(lms PRIVATE Boost::iostreams Wt::Wt Wt::HTTP - lmsav + lmsaudio lmsauth lmsdatabase lmsfeedback diff --git a/src/lms/main.cpp b/src/lms/main.cpp index a217d6d5..14c149d2 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -47,7 +47,7 @@ #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "subsonic/SubsonicResource.hpp" #include "ui/Auth.hpp" #include "ui/LmsApplication.hpp" @@ -428,7 +428,7 @@ namespace lms core::Service recommendationService{ recommendation::createRecommendationService(*database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(*database, *recommendationService) }; core::Service scannerService{ scanner::createScannerService(*database, cachePath) }; - core::Service transcodingService{ transcoding::createTranscodingService(*database, *childProcessManagerService) }; + core::Service transcodingService{ transcoding::createTranscodeService(*database, *childProcessManagerService) }; core::Service podcastService{ podcast::createPodcastService(ioContext, *database, cachePath / "podcasts") }; scannerService->getEvents().scanComplete.connect([&] { diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index c15a1913..413cb7fd 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -27,8 +27,10 @@ #include #include -#include "av/IAudioFile.hpp" #include "core/String.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -141,16 +143,21 @@ namespace lms::ui // TODO: save in DB and aggregate all this for (const db::Track::pointer& track : db::Track::find(LmsApp->getDbSession(), db::Track::FindParameters{}.setRelease(releaseId).setRange(db::Range{ 0, 1 })).results) { - if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) + try { - const std::optional audioStream{ audioFile->getBestStreamInfo() }; - if (audioStream) + if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) { - releaseInfo->setCondition("if-has-codec", true); - releaseInfo->bindString("codec", audioStream->codecName); - break; + if (audioFile->getAudioProperties().codec) + { + releaseInfo->setCondition("if-has-codec", true); + releaseInfo->bindString("codec", audio::codecTypeToString(*audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); + break; + } } } + catch (const audio::Exception& e) + { + } } if (const std::size_t meanBitrate{ release->getMeanBitrate() }) diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index e4f44f30..25821d7c 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -23,8 +23,10 @@ #include #include -#include "av/IAudioFile.hpp" #include "core/Service.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/IAudioFileInfo.hpp" #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -127,15 +129,20 @@ namespace lms::ui::TrackListHelpers } } - if (const auto audioFile{ av::parseAudioFile(track->getAbsoluteFilePath()) }) + try { - const std::optional audioStream{ audioFile->getBestStreamInfo() }; - if (audioStream) + if (const auto audioFile{ audio::parseAudioFile(track->getAbsoluteFilePath()) }) { - trackInfo->setCondition("if-has-codec", true); - trackInfo->bindString("codec", audioStream->codecName, Wt::TextFormat::Plain); + if (audioFile->getAudioProperties().codec) + { + trackInfo->setCondition("if-has-codec", true); + trackInfo->bindString("codec", audio::codecTypeToString(*audioFile->getAudioProperties().codec).c_str(), Wt::TextFormat::Plain); + } } } + catch (const audio::Exception& e) + { + } trackInfo->bindString("duration", utils::durationToString(track->getDuration())); if (track->getBitrate()) diff --git a/src/lms/ui/resource/AudioTranscodingResource.cpp b/src/lms/ui/resource/AudioTranscodingResource.cpp index 8e26930a..a5977b8c 100644 --- a/src/lms/ui/resource/AudioTranscodingResource.cpp +++ b/src/lms/ui/resource/AudioTranscodingResource.cpp @@ -30,7 +30,7 @@ #include "database/Session.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" -#include "services/transcoding/ITranscodingService.hpp" +#include "services/transcoding/ITranscodeService.hpp" #include "LmsApplication.hpp" @@ -72,23 +72,23 @@ namespace lms::ui { namespace { - std::optional AudioFormatToAvFormat(db::TranscodingOutputFormat format) + std::optional AudioFormatToTranscodingFormat(db::TranscodingOutputFormat format) { switch (format) { case db::TranscodingOutputFormat::MP3: - return transcoding::OutputFormat::MP3; + return audio::OutputFormat::MP3; case db::TranscodingOutputFormat::OGG_OPUS: - return transcoding::OutputFormat::OGG_OPUS; + return audio::OutputFormat::OGG_OPUS; case db::TranscodingOutputFormat::MATROSKA_OPUS: - return transcoding::OutputFormat::MATROSKA_OPUS; + return audio::OutputFormat::MATROSKA_OPUS; case db::TranscodingOutputFormat::OGG_VORBIS: - return transcoding::OutputFormat::OGG_VORBIS; + return audio::OutputFormat::OGG_VORBIS; case db::TranscodingOutputFormat::WEBM_VORBIS: - return transcoding::OutputFormat::WEBM_VORBIS; + return audio::OutputFormat::WEBM_VORBIS; } - TRANSCODE_LOG(ERROR, "Cannot convert from audio format to AV format"); + TRANSCODE_LOG(ERROR, "Cannot convert from db audio format to transcoding format"); return std::nullopt; } @@ -110,15 +110,9 @@ namespace lms::ui return res; } - struct TranscodingParameters + std::optional readTranscodingParameters(const Wt::Http::Request& request) { - transcoding::InputParameters inputParameters; - transcoding::OutputParameters outputParameters; - }; - - std::optional readTranscodingParameters(const Wt::Http::Request& request) - { - TranscodingParameters parameters; + audio::TranscodeParameters parameters; // mandatory parameters const std::optional trackId{ readParameterAs(request, "trackid") }; @@ -134,7 +128,7 @@ namespace lms::ui return std::nullopt; } - const std::optional avFormat{ AudioFormatToAvFormat(*format) }; + const std::optional avFormat{ AudioFormatToTranscodingFormat(*format) }; if (!avFormat) return std::nullopt; @@ -181,7 +175,7 @@ namespace lms::ui if (!continuation) { if (const auto& parameters{ readTranscodingParameters(request) }) - resourceHandler = core::Service::get()->createResourceHandler(parameters->inputParameters, parameters->outputParameters, false /* estimate content length */); + resourceHandler = core::Service::get()->createTranscodeResourceHandler(*parameters); } else { diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index a390bd6a..4759645e 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory(db-generator) -add_subdirectory(metadata) +add_subdirectory(audioinfo) add_subdirectory(recommendation) diff --git a/src/tools/audioinfo/CMakeLists.txt b/src/tools/audioinfo/CMakeLists.txt new file mode 100644 index 00000000..ccadf474 --- /dev/null +++ b/src/tools/audioinfo/CMakeLists.txt @@ -0,0 +1,12 @@ + +add_executable(lms-audioinfo + LmsAudioInfo.cpp + ) + +target_link_libraries(lms-audioinfo PRIVATE + lmsaudio + lmscore + Boost::program_options + ) + +install(TARGETS lms-audioinfo DESTINATION bin) diff --git a/src/tools/audioinfo/LmsAudioInfo.cpp b/src/tools/audioinfo/LmsAudioInfo.cpp new file mode 100644 index 00000000..b87ae73b --- /dev/null +++ b/src/tools/audioinfo/LmsAudioInfo.cpp @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include +#include +#include + +#include +#include + +#include "core/ILogger.hpp" +#include "core/String.hpp" + +#include "audio/AudioTypes.hpp" +#include "audio/Exception.hpp" +#include "audio/IAudioFileInfo.hpp" +#include "audio/IImageReader.hpp" +#include "audio/ITagReader.hpp" + +namespace lms::audio +{ + std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) + { + os << "\tDuration: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast>(audioProperties.duration) << std::endl; + + if (audioProperties.container) + os << "\tContainer: " << containerTypeToString(*audioProperties.container) << std::endl; + if (audioProperties.codec) + os << "\tCodec: " << codecTypeToString(*audioProperties.codec) << std::endl; + if (audioProperties.bitrate) + os << "\tBitrate: " << *audioProperties.bitrate << " bps" << std::endl; + if (audioProperties.bitsPerSample) + os << "\tBitsPerSample: " << *audioProperties.bitsPerSample << std::endl; + if (audioProperties.channelCount) + os << "\tChannelCount: " << *audioProperties.channelCount << std::endl; + if (audioProperties.sampleRate) + os << "\tSampleRate: " << *audioProperties.sampleRate << std::endl; + + return os; + } + + void displayTags(const ITagReader& tagReader, TagType type) + { + std::vector strs; + + tagReader.visitTagValues(type, [&](std::string_view value) { + strs.push_back(std::string{ value }); + }); + + if (strs.empty()) + return; + + std::cout << "\t" << tagTypeToString(type) << ": "; + if (strs.size() == 1) + { + std::cout << "'" << strs.front() << "'\n"; + } + else + { + std::cout << "["; + bool first{ true }; + for (std::string_view str : strs) + { + if (!first) + std::cout << ", "; + first = false; + std::cout << "'" << str << "'"; + } + std::cout << "]\n"; + }; + } + + void displayLyrics(const ITagReader& tagReader) + { + tagReader.visitLyricsTags([](std::string_view language, std::string_view lyrics) { + std::cout << "Lyrics"; + if (!language.empty()) + std::cout << " (" << language << ")"; + std::cout << ":\n" + << lyrics << "\n\n"; + }); + } + + void displayPerformers(const ITagReader& tagReader) + { + tagReader.visitPerformerTags([](std::string_view role, std::string_view artist) { + std::cout << "\tPerformer"; + if (!role.empty()) + std::cout << " '" << role << "'"; + std::cout << ": '" << artist << "'\n"; + }); + } + + void displayTags(const ITagReader& tagReader) + { + std::cout << "Tags:\n"; + for (int i{}; i < static_cast(TagType::Count); ++i) + displayTags(tagReader, static_cast(i)); + + displayPerformers(tagReader); + displayLyrics(tagReader); + } + + std::ostream& operator<<(std::ostream& os, const Image& image) + { + os << "\ttype = " << imageTypeToString(image.type) << std::endl; + if (!image.description.empty()) + os << "\tdesc: " << image.description << std::endl; + os << "\tmimeType: " << image.mimeType << std::endl; + os << "\tsize: " << image.data.size() << std::endl; + + return os; + } + + void displayImages(const IImageReader& imageReader) + { + imageReader.visitImages([](const Image& image) { + std::cout << "Image:\n" + << image << "\n"; + }); + } + + void displayInfo(const IAudioFileInfo& fileInfo) + { + std::cout << "Audio properties:\n" + << fileInfo.getAudioProperties() << std::endl; + + displayImages(fileInfo.getImageReader()); + displayTags(fileInfo.getTagReader()); + + std::cout << "\n"; + } +} // namespace lms::audio + +int main(int argc, char* argv[]) +{ + try + { + using namespace lms; + namespace program_options = boost::program_options; + + program_options::options_description options{ "Options" }; + // clang-format off + options.add_options() + ("help,h", "Display this help message") + ("parser", program_options::value()->default_value(std::string{ "taglib" }, "taglib"), "Parser to be used (value can be \"taglib\", \"ffmpeg\")"); + // clang-format on + + program_options::options_description hiddenOptions{ "Hidden options" }; + hiddenOptions.add_options()("file", program_options::value>()->composing(), "file"); + + program_options::options_description allOptions; + allOptions.add(options).add(hiddenOptions); + + program_options::positional_options_description positional; + positional.add("file", -1); // Handle remaining arguments as input files + + program_options::variables_map vm; + // Parse command line arguments with positional option handling + program_options::store(program_options::command_line_parser(argc, argv) + .options(allOptions) + .positional(positional) + .run(), + vm); + + program_options::notify(vm); + + auto displayHelp = [&](std::ostream& os) { + os << "Usage: " << argv[0] << " [options] file..." << std::endl; + os << options << std::endl; + }; + + if (vm.count("help")) + { + displayHelp(std::cout); + return EXIT_SUCCESS; + } + + if (vm.count("file") == 0) + { + std::cerr << "No input file provided" << std::endl; + displayHelp(std::cerr); + return EXIT_FAILURE; + } + + if (vm.count("parser") != 1) + { + std::cerr << "You must specify a parser to be used" << std::endl; + return EXIT_FAILURE; + } + + audio::ParserOptions parserOptions; + parserOptions.enableExtraDebugLogs = true; + if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "taglib")) + parserOptions.parser = audio::ParserOptions::Parser::TagLib; + else if (core::stringUtils::stringCaseInsensitiveEqual(vm["parser"].as(), "ffmpeg")) + parserOptions.parser = audio::ParserOptions::Parser::FFmpeg; + else + throw program_options::validation_error{ program_options::validation_error::invalid_option_value, "parser" }; + + const auto& inputFiles{ vm["file"].as>() }; + // log to stdout + core::Service logger{ core::logging::createLogger(core::logging::Severity::DEBUG) }; + + for (const std::string& inputFile : inputFiles) + { + const std::filesystem::path file{ inputFile }; + + try + { + std::cout << "Parsing file " << file << ":\n"; + const auto audioFileInfo{ audio::parseAudioFile(file, parserOptions) }; + + displayInfo(*audioFileInfo); + } + catch (const audio::Exception& e) + { + std::cerr << "Failed to parse file " << file << ": " << e.what() << std::endl; + continue; + } + } + } + catch (std::exception& e) + { + std::cerr << "Caught exception: " << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/tools/metadata/CMakeLists.txt b/src/tools/metadata/CMakeLists.txt deleted file mode 100644 index dcdd2394..00000000 --- a/src/tools/metadata/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ - -add_executable(lms-metadata - LmsMetadata.cpp - ) - -target_link_libraries(lms-metadata PRIVATE - lmsmetadata - lmscore - Boost::program_options - ) - -install(TARGETS lms-metadata DESTINATION bin) diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp deleted file mode 100644 index 190c4746..00000000 --- a/src/tools/metadata/LmsMetadata.cpp +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright (C) 2019 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "core/EnumSet.hpp" -#include "core/ILogger.hpp" -#include "core/String.hpp" -#include "metadata/Exception.hpp" -#include "metadata/IAudioFileParser.hpp" -#include "metadata/Types.hpp" - -namespace lms::metadata -{ - std::ostream& operator<<(std::ostream& os, const Lyrics& lyrics) - { - os << "\tTitle display name: " << lyrics.displayTitle << std::endl; - os << "\tArtist display name: " << lyrics.displayArtist << std::endl; - os << "\tAlbum display name: " << lyrics.displayAlbum << std::endl; - os << "\tOffset: " << lyrics.offset.count() << "ms" << std::endl; - os << "\tLanguage: " << lyrics.language << std::endl; - os << "\tSynchronized: " << !lyrics.synchronizedLines.empty() << std::endl; - for (const auto& [timestamp, line] : lyrics.synchronizedLines) - os << "\t" << core::stringUtils::formatTimestamp(timestamp) << " '" << line << "'" << std::endl; - for (const auto& line : lyrics.unsynchronizedLines) - os << "\t'" << line << "'" << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const AudioProperties& audioProperties) - { - os << "\tBitrate: " << audioProperties.bitrate << " bps" << std::endl; - os << "\tBitsPerSample: " << audioProperties.bitsPerSample << std::endl; - os << "\tChannelCount: " << audioProperties.channelCount << std::endl; - os << "\tDuration: " << std::fixed << std::setprecision(2) << audioProperties.duration.count() / 1000. << "s" << std::endl; - os << "\tSampleRate: " << audioProperties.sampleRate << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Artist& artist) - { - os << artist.name; - - if (artist.mbid) - os << " (" << artist.mbid->getAsString() << ")"; - - if (artist.sortName) - os << " '" << *artist.sortName << "'"; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Release& release) - { - os << release.name; - if (release.sortName != release.name) - os << " '" << release.sortName << "'"; - os << std::endl; - - for (std::string_view releaseType : release.releaseTypes) - std::cout << "\tRelease type: " << releaseType << std::endl; - - if (release.mbid) - os << "\tRelease MBID = " << release.mbid->getAsString() << std::endl; - - if (release.groupMBID) - os << "\tRelease Group MBID = " << release.groupMBID->getAsString() << std::endl; - - if (release.mediumCount) - std::cout << "\tMediumCount: " << *release.mediumCount << std::endl; - - std::cout << "\tIsCompilation: " << std::boolalpha << release.isCompilation << std::endl; - - for (std::string_view label : release.labels) - std::cout << "\tLabel: " << label << std::endl; - - for (std::string_view country : release.countries) - std::cout << "\tCountry: " << country << std::endl; - - if (!release.barcode.empty()) - std::cout << "\tBarcode: " << release.barcode << std::endl; - - if (!release.comment.empty()) - std::cout << "\tComment: " << release.comment << std::endl; - - if (!release.artistDisplayName.empty()) - std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl; - - for (const Artist& artist : release.artists) - std::cout << "\tRelease artist: " << artist << std::endl; - - return os; - } - - std::ostream& operator<<(std::ostream& os, const Medium& medium) - { - if (!medium.name.empty()) - os << medium.name; - os << std::endl; - - if (medium.position) - os << "\tPosition: " << *medium.position << std::endl; - - if (!medium.media.empty()) - os << "\tMedia: " << medium.media << std::endl; - - if (medium.trackCount) - std::cout << "\tTrackCount: " << *medium.trackCount << std::endl; - - if (medium.replayGain) - std::cout << "\tReplay gain: " << *medium.replayGain << std::endl; - - if (medium.release) - std::cout << "Release: " << *medium.release; - - return os; - } - - std::ostream& operator<<(std::ostream& os, Track::Advisory advisory) - { - switch (advisory) - { - case Track::Advisory::Clean: - os << "clean"; - break; - case Track::Advisory::Explicit: - os << "Explicit"; - break; - case Track::Advisory::Unknown: - os << "Unknown"; - break; - } - - return os; - } - - const char* 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"; - } - - std::ostream& operator<<(std::ostream& os, const Image& image) - { - os << "type = " << imageTypeToString(image.type) << std::endl; - if (!image.description.empty()) - os << "\tdesc = " << image.description << std::endl; - os << "\tmimeType = " << image.mimeType << std::endl; - os << "\tsize = " << image.data.size() << std::endl; - - return os; - } - - void parseMetaData(IAudioFileParser& parser, const std::filesystem::path& file) - { - using namespace metadata; - - const auto start{ std::chrono::steady_clock::now() }; - std::unique_ptr track{ parser.parseMetaData(file) }; - const auto end{ std::chrono::steady_clock::now() }; - - std::cout << "MetaData parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; - - std::cout << "Audio properties:\n" - << track->audioProperties << std::endl; - - std::cout << "Parsed metadata:" << std::endl; - - if (!track->artistDisplayName.empty()) - std::cout << "Display artist: " << track->artistDisplayName << std::endl; - - for (const Artist& artist : track->artists) - std::cout << "Artist: " << artist << std::endl; - - for (const Artist& artist : track->conductorArtists) - std::cout << "Conductor: " << artist << std::endl; - - for (const Artist& artist : track->composerArtists) - std::cout << "Composer: " << artist << std::endl; - - for (const Artist& artist : track->lyricistArtists) - std::cout << "Lyricist: " << artist << std::endl; - - for (const Artist& artist : track->mixerArtists) - std::cout << "Mixer: " << artist << std::endl; - - for (const auto& [role, artists] : track->performerArtists) - { - std::cout << "Performer"; - if (!role.empty()) - std::cout << " (" << role << ")"; - std::cout << ":" << std::endl; - for (const Artist& artist : artists) - std::cout << "\t" << artist << std::endl; - } - - for (const Artist& artist : track->producerArtists) - std::cout << "Producer: " << artist << std::endl; - - for (const Artist& artist : track->remixerArtists) - std::cout << "Remixer: " << artist << std::endl; - - std::cout << "Title: " << track->title << std::endl; - - if (track->mbid) - std::cout << "Track MBID = " << track->mbid->getAsString() << std::endl; - - if (track->recordingMBID) - std::cout << "Recording MBID = " << track->recordingMBID->getAsString() << std::endl; - - for (std::string_view genre : track->genres) - std::cout << "Genre: " << genre << std::endl; - - for (std::string_view genre : track->moods) - std::cout << "Mood: " << genre << std::endl; - - for (std::string_view grouping : track->groupings) - std::cout << "Grouping: " << grouping << std::endl; - - for (std::string_view language : track->languages) - std::cout << "Language: " << language << std::endl; - - for (const auto& [tag, values] : track->userExtraTags) - { - std::cout << "Tag: " << tag << std::endl; - for (const auto& value : values) - { - std::cout << "\t" << value << std::endl; - } - } - - if (track->position) - std::cout << "Position: " << *track->position << std::endl; - - if (track->date.isValid()) - std::cout << "Date: " << track->date.toString() << std::endl; - - if (track->originalDate.isValid()) - std::cout << "Original date: " << track->originalDate.toString() << std::endl; - if (track->originalYear) - std::cout << "Original year: " << *track->originalYear << std::endl; - - if (track->replayGain) - std::cout << "Track replay gain: " << *track->replayGain << std::endl; - - if (track->acoustID) - std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl; - - if (!track->copyright.empty()) - std::cout << "Copyright: " << track->copyright << std::endl; - - for (const Lyrics& lyrics : track->lyrics) - std::cout << "Lyrics:\n" - << lyrics << std::endl; - - for (const auto& comment : track->comments) - std::cout << "Comment: '" << comment << "'" << std::endl; - - if (!track->copyrightURL.empty()) - std::cout << "CopyrightURL: " << track->copyrightURL << std::endl; - - if (track->advisory) - std::cout << "Advisory: " << *track->advisory << std::endl; - - if (track->encodingTime.isValid()) - std::cout << "Encoding time: " << track->encodingTime.toString() << std::endl; - - if (track->medium) - std::cout << "Medium: " << *track->medium; - - std::cout << std::endl; - } - - void parseImages(IAudioFileParser& parser, const std::filesystem::path& file) - { - using namespace metadata; - - const auto start{ std::chrono::steady_clock::now() }; - parser.parseImages(file, [](const Image& image) { - std::cout << "Image: " << image << std::endl; - }); - const auto end{ std::chrono::steady_clock::now() }; - - std::cout << "Image parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; - } - -} // namespace lms::metadata - -int main(int argc, char* argv[]) -{ - try - { - using namespace lms; - namespace program_options = boost::program_options; - - program_options::options_description options{ "Options" }; - // clang-format off - options.add_options() - ("help,h", "Display this help message") - ("tag-delimiter", program_options::value>()->default_value(std::vector{}, "[]"), "Tag delimiters (multiple allowed)") - ("artist-tag-delimiter", program_options::value>()->default_value(std::vector{}, "[]"), "Artist tag delimiters (multiple allowed)") - ("parser", program_options::value>()->default_value(std::vector{ "taglib" }, "[taglib]"), "Parser to be used (value can be \"taglib\", \"ffmpeg\" or \"lyrics\")"); - // clang-format on - - program_options::options_description hiddenOptions{ "Hidden options" }; - hiddenOptions.add_options()("file", program_options::value>()->composing(), "file"); - - program_options::options_description allOptions; - allOptions.add(options).add(hiddenOptions); - - program_options::positional_options_description positional; - positional.add("file", -1); // Handle remaining arguments as input files - - program_options::variables_map vm; - // Parse command line arguments with positional option handling - program_options::store(program_options::command_line_parser(argc, argv) - .options(allOptions) - .positional(positional) - .run(), - vm); - - program_options::notify(vm); - - auto displayHelp = [&](std::ostream& os) { - os << "Usage: " << argv[0] << " [options] file..." << std::endl; - os << options << std::endl; - }; - - if (vm.count("help")) - { - displayHelp(std::cout); - return EXIT_SUCCESS; - } - - if (vm.count("file") == 0) - { - std::cerr << "No input file provided" << std::endl; - displayHelp(std::cerr); - return EXIT_FAILURE; - } - - enum class Parser - { - Lyrics, - Taglib, - Ffmpeg, - }; - - if (vm.count("parser") == 0) - { - std::cerr << "You must specify at least one parser" << std::endl; - return EXIT_FAILURE; - } - - core::EnumSet parsers; - for (const std::string& strParser : vm["parser"].as>()) - { - if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "lyrics")) - parsers.insert(Parser::Lyrics); - else if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "taglib")) - parsers.insert(Parser::Taglib); - else if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "ffmpeg")) - parsers.insert(Parser::Ffmpeg); - else - { - std::cerr << "Invalid parser name '" << strParser << "'" << std::endl; - return EXIT_FAILURE; - } - } - - const auto& inputFiles{ vm["file"].as>() }; - const auto& tagDelimiters{ vm["tag-delimiter"].as>() }; - const auto& artistTagDelimiters{ vm["artist-tag-delimiter"].as>() }; - - for (std::string_view tagDelimiter : tagDelimiters) - { - std::cout << "Tag delimiter: '" << tagDelimiter << "'" << std::endl; - } - - for (std::string_view artistTagDelimiter : artistTagDelimiters) - { - std::cout << "Artist tag delimiter: '" << artistTagDelimiter << "'" << std::endl; - } - - // log to stdout - core::Service logger{ core::logging::createLogger(core::logging::Severity::DEBUG) }; - - for (const std::string& inputFile : inputFiles) - { - std::filesystem::path file{ inputFile }; - - std::cout << "Parsing file " << file << std::endl; - - if (parsers.contains(Parser::Lyrics)) - { - try - { - std::cout << "Using Lyrics:" << std::endl; - - std::ifstream ifs{ file }; - if (ifs) - { - const metadata::Lyrics lyrics{ metadata::parseLyrics(ifs) }; - std::cout << lyrics << std::endl; - } - else - std::cerr << "Cannot open file " << file; - } - catch (metadata::Exception& e) - { - std::cerr << "Parsing failed: " << e.what() << std::endl; - } - } - - auto parseAudioFile{ [&](metadata::ParserBackend backend) { - metadata::AudioFileParserParameters params; - params.artistTagDelimiters = artistTagDelimiters; - params.defaultTagDelimiters = tagDelimiters; - params.readStyle = metadata::ParserReadStyle::Accurate; - params.debug = true; - params.backend = backend; - - try - { - auto parser{ metadata::createAudioFileParser(params) }; - parseMetaData(*parser, file); - parseImages(*parser, file); - } - catch (metadata::Exception& e) - { - std::cerr << "Parsing failed: " << e.what() << std::endl; - } - } }; - - if (parsers.contains(Parser::Ffmpeg)) - parseAudioFile(metadata::ParserBackend::AvFormat); - - if (parsers.contains(Parser::Taglib)) - parseAudioFile(metadata::ParserBackend::TagLib); - } - } - catch (std::exception& e) - { - std::cerr << "Caught exception: " << e.what() << std::endl; - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; -} \ No newline at end of file