From 5effbf99a1512beca071cb9d9d1ad46a534fb303 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 3 Mar 2025 19:32:00 +0100 Subject: [PATCH] Reworked scanner to use taglib to extract images, now support images in APE tags. This is a step forward multiple image support per track --- src/libs/metadata/CMakeLists.txt | 4 +- .../impl/{Parser.cpp => AudioFileParser.cpp} | 113 +++-- src/libs/metadata/impl/AudioFileParser.hpp | 55 +++ .../metadata/impl/AvFormatImageReader.cpp | 59 +++ .../metadata/impl/AvFormatImageReader.hpp | 45 ++ src/libs/metadata/impl/AvFormatTagReader.cpp | 1 - src/libs/metadata/impl/AvFormatTagReader.hpp | 2 - src/libs/metadata/impl/IImageReader.hpp | 36 ++ src/libs/metadata/impl/ITagReader.hpp | 1 - src/libs/metadata/impl/Parser.hpp | 54 --- src/libs/metadata/impl/TagLibDefs.hpp | 32 ++ src/libs/metadata/impl/TagLibImageReader.cpp | 431 ++++++++++++++++++ src/libs/metadata/impl/TagLibImageReader.hpp | 37 ++ src/libs/metadata/impl/TagLibTagReader.cpp | 35 +- src/libs/metadata/impl/TagLibTagReader.hpp | 8 +- src/libs/metadata/impl/Utils.hpp | 2 +- .../{IParser.hpp => IAudioFileParser.hpp} | 27 +- src/libs/metadata/include/metadata/Types.hpp | 82 +++- .../test/{Parser.cpp => AudioFileParser.cpp} | 159 ++++--- src/libs/metadata/test/CMakeLists.txt | 2 +- src/libs/metadata/test/TestTagReader.hpp | 4 +- src/libs/services/artwork/CMakeLists.txt | 2 +- .../services/artwork/impl/ArtworkService.cpp | 114 +++-- .../services/artwork/impl/ArtworkService.hpp | 8 +- .../impl/scanners/AudioFileScanner.cpp | 52 ++- .../impl/scanners/AudioFileScanner.hpp | 8 +- src/tools/metadata/LmsMetadata.cpp | 131 ++++-- 27 files changed, 1151 insertions(+), 353 deletions(-) rename src/libs/metadata/impl/{Parser.cpp => AudioFileParser.cpp} (85%) create mode 100644 src/libs/metadata/impl/AudioFileParser.hpp create mode 100644 src/libs/metadata/impl/AvFormatImageReader.cpp create mode 100644 src/libs/metadata/impl/AvFormatImageReader.hpp create mode 100644 src/libs/metadata/impl/IImageReader.hpp delete mode 100644 src/libs/metadata/impl/Parser.hpp create mode 100644 src/libs/metadata/impl/TagLibDefs.hpp create mode 100644 src/libs/metadata/impl/TagLibImageReader.cpp create mode 100644 src/libs/metadata/impl/TagLibImageReader.hpp rename src/libs/metadata/include/metadata/{IParser.hpp => IAudioFileParser.hpp} (58%) rename src/libs/metadata/test/{Parser.cpp => AudioFileParser.cpp} (87%) diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index 8fa1167c..63d93046 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -9,10 +9,12 @@ if (BUILD_BENCHMARKS) endif() add_library(lmsmetadata STATIC + impl/AvFormatImageReader.cpp impl/AvFormatTagReader.cpp impl/Lyrics.cpp - impl/Parser.cpp + impl/AudioFileParser.cpp impl/PlayList.cpp + impl/TagLibImageReader.cpp impl/TagLibTagReader.cpp impl/Utils.cpp ) diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/AudioFileParser.cpp similarity index 85% rename from src/libs/metadata/impl/Parser.cpp rename to src/libs/metadata/impl/AudioFileParser.cpp index 4c922f5a..54eaf31e 100644 --- a/src/libs/metadata/impl/Parser.cpp +++ b/src/libs/metadata/impl/AudioFileParser.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "Parser.hpp" +#include "AudioFileParser.hpp" #include @@ -26,7 +26,9 @@ #include "core/String.hpp" #include "metadata/Exception.hpp" +#include "AvFormatImageReader.hpp" #include "AvFormatTagReader.hpp" +#include "TagLibImageReader.hpp" #include "TagLibTagReader.hpp" #include "Utils.hpp" @@ -316,19 +318,18 @@ namespace lms::metadata } } // namespace - std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle) + std::unique_ptr createAudioFileParser(const AudioFileParserParameters& params) { - return std::make_unique(parserBackend, parserReadStyle); + return std::make_unique(params); } - Parser::Parser(ParserBackend parserBackend, ParserReadStyle readStyle) - : _parserBackend{ parserBackend } - , _readStyle{ readStyle } + AudioFileParser::AudioFileParser(const AudioFileParserParameters& params) + : _params{ params } { - switch (_parserBackend) + switch (_params.backend) { case ParserBackend::TagLib: - LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(readStyle)); + LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle)); break; case ParserBackend::AvFormat: @@ -337,7 +338,7 @@ namespace lms::metadata } } - std::span Parser::getSupportedExtensions() const + std::span AudioFileParser::getSupportedExtensions() const { // TODO: use backend capability to retrieve supported formats static const std::array fileExtensions{ @@ -363,34 +364,61 @@ namespace lms::metadata return fileExtensions; } - std::unique_ptr Parser::parse(const std::filesystem::path& p, bool debug) + std::unique_ptr AudioFileParser::parseMetaData(const std::filesystem::path& p) { try { std::unique_ptr tagReader; - switch (_parserBackend) + switch (_params.backend) { case ParserBackend::TagLib: - tagReader = std::make_unique(p, _readStyle, debug); + tagReader = std::make_unique(p, _params.readStyle, _params.debug); break; case ParserBackend::AvFormat: - tagReader = std::make_unique(p, debug); + tagReader = std::make_unique(p, _params.debug); break; } if (!tagReader) throw ParseException{ "Unhandled parser backend" }; - return parse(*tagReader); + return parseMetaData(*tagReader); } catch (const Exception& e) { - LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); + LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed"); throw ParseException{}; } } - std::unique_ptr Parser::parse(const ITagReader& tagReader) + void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) + { + try + { + 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 ParseException{ "Unhandled parser backend" }; + + parseImages(*imageReader, std::move(visitor)); + } + catch (const Exception& e) + { + LMS_LOG(METADATA, ERROR, "File " << p << ": image parsing failed"); + throw ParseException{}; + } + } + + std::unique_ptr AudioFileParser::parseMetaData(const ITagReader& tagReader) { auto track{ std::make_unique() }; @@ -400,10 +428,8 @@ namespace lms::metadata return track; } - void Parser::processTags(const ITagReader& tagReader, Track& track) + void AudioFileParser::processTags(const ITagReader& tagReader, Track& track) { - track.hasCover = tagReader.hasEmbeddedCover(); - track.title = getTagValueAs(tagReader, TagType::TrackTitle).value_or(""); track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID); track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); @@ -436,32 +462,32 @@ namespace lms::metadata track.copyrightURL = getTagValueAs(tagReader, TagType::CopyrightURL).value_or(""); track.replayGain = getTagValueAs(tagReader, TagType::ReplayGainTrackGain); - for (const std::string& userExtraTag : _userExtraTags) + for (const std::string& userExtraTag : _params.userExtraTags) { - visitTagValues(tagReader, userExtraTag, _defaultTagDelimiters, [&](std::string_view value) { + visitTagValues(tagReader, userExtraTag, _params.defaultTagDelimiters, [&](std::string_view value) { value = core::stringUtils::stringTrim(value); if (!value.empty()) track.userExtraTags[userExtraTag].push_back(std::string{ value }); }); } - track.genres = getTagValuesAs(tagReader, TagType::Genre, _defaultTagDelimiters); - track.moods = getTagValuesAs(tagReader, TagType::Mood, _defaultTagDelimiters); - track.groupings = getTagValuesAs(tagReader, TagType::Grouping, _defaultTagDelimiters); - track.languages = getTagValuesAs(tagReader, TagType::Language, _defaultTagDelimiters); + track.genres = getTagValuesAs(tagReader, TagType::Genre, _params.defaultTagDelimiters); + track.moods = getTagValuesAs(tagReader, TagType::Mood, _params.defaultTagDelimiters); + track.groupings = getTagValuesAs(tagReader, TagType::Grouping, _params.defaultTagDelimiters); + track.languages = getTagValuesAs(tagReader, TagType::Language, _params.defaultTagDelimiters); std::vector artistDelimiters{}; track.medium = getMedium(tagReader); - track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters); - track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs(tagReader, TagType::Artist), _artistTagDelimiters); + track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs(tagReader, TagType::Artist), _params.artistTagDelimiters); - track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); - track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); - track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); - track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); - track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); - track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters); + track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); + track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters); track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported fillMissingMbids(track); @@ -471,7 +497,7 @@ namespace lms::metadata track.originalYear = track.originalDate.getYear(); } - std::optional Parser::getMedium(const ITagReader& tagReader) + std::optional AudioFileParser::getMedium(const ITagReader& tagReader) { std::optional medium; medium.emplace(); @@ -501,7 +527,7 @@ namespace lms::metadata return medium; } - std::optional Parser::getRelease(const ITagReader& tagReader) + std::optional AudioFileParser::getRelease(const ITagReader& tagReader) { std::optional release; @@ -512,16 +538,16 @@ namespace lms::metadata release.emplace(); release->name = std::move(*releaseName); release->sortName = getTagValueAs(tagReader, TagType::AlbumSortOrder).value_or(release->name); - release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters); - release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs(tagReader, TagType::AlbumArtist), _artistTagDelimiters); + release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _params.artistTagDelimiters, _params.defaultTagDelimiters); + release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters); release->mbid = getTagValueAs(tagReader, TagType::MusicBrainzReleaseID); release->groupMBID = getTagValueAs(tagReader, TagType::MusicBrainzReleaseGroupID); release->mediumCount = getTagValueAs(tagReader, TagType::TotalDiscs); release->isCompilation = getTagValueAs(tagReader, TagType::Compilation).value_or(false); release->barcode = getTagValueAs(tagReader, TagType::Barcode).value_or(""); - release->labels = getTagValuesAs(tagReader, TagType::RecordLabel, _defaultTagDelimiters); + release->labels = getTagValuesAs(tagReader, TagType::RecordLabel, _params.defaultTagDelimiters); release->comment = getTagValueAs(tagReader, TagType::AlbumComment).value_or(""); - release->countries = getTagValuesAs(tagReader, TagType::ReleaseCountry, _defaultTagDelimiters); + release->countries = getTagValuesAs(tagReader, TagType::ReleaseCountry, _params.defaultTagDelimiters); if (!release->mediumCount) { // mediumCount may be encoded as "position/count" @@ -534,8 +560,15 @@ namespace lms::metadata } } - release->releaseTypes = getTagValuesAs(tagReader, TagType::ReleaseType, _defaultTagDelimiters); + release->releaseTypes = getTagValuesAs(tagReader, TagType::ReleaseType, _params.defaultTagDelimiters); 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 diff --git a/src/libs/metadata/impl/AudioFileParser.hpp b/src/libs/metadata/impl/AudioFileParser.hpp new file mode 100644 index 00000000..7c8e1376 --- /dev/null +++ b/src/libs/metadata/impl/AudioFileParser.hpp @@ -0,0 +1,55 @@ +/* + * 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& parameters = {}); + ~AudioFileParser() override = default; + AudioFileParser(const AudioFileParser&) = delete; + AudioFileParser& operator=(const AudioFileParser&) = delete; + + protected: + std::unique_ptr parseMetaData(const std::filesystem::path& p) override; + std::unique_ptr parseMetaData(const ITagReader& reader); + void parseImages(const IImageReader& reader, ImageVisitor visitor); + + private: + void parseImages(const std::filesystem::path& p, ImageVisitor visitor) override; + std::span getSupportedExtensions() const override; + + void processTags(const ITagReader& reader, Track& track); + + std::optional getMedium(const ITagReader& tagReader); + std::optional getRelease(const ITagReader& tagReader); + + const AudioFileParserParameters _params; + }; +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/AvFormatImageReader.cpp b/src/libs/metadata/impl/AvFormatImageReader.cpp new file mode 100644 index 00000000..104b4349 --- /dev/null +++ b/src/libs/metadata/impl/AvFormatImageReader.cpp @@ -0,0 +1,59 @@ +/* + * 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 "AvFormatImageReader.hpp" +#include "av/IAudioFile.hpp" +#include "av/Types.hpp" + +namespace lms::metadata +{ + AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p) + { + try + { + _audioFile = av::parseAudioFile(p); + } + catch (av::Exception& e) + { + throw ParseException{}; + } + } + + AvFormatImageReader::~AvFormatImageReader() = default; + + void AvFormatImageReader::visitImages(ImageVisitor visitor) const + { + auto metaDataHasKeyword{ [](const av::IAudioFile::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) { + Image image; + image.data = picture.data; + image.mimeType = picture.mimeType; + if (metaDataHasKeyword(metaData, "front")) + image.type = Image::Type::FrontCover; + else if (metaDataHasKeyword(metaData, "back")) + image.type = Image::Type::BackCover; + + visitor(image); + }); + } + +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/AvFormatImageReader.hpp b/src/libs/metadata/impl/AvFormatImageReader.hpp new file mode 100644 index 00000000..033d1e45 --- /dev/null +++ b/src/libs/metadata/impl/AvFormatImageReader.hpp @@ -0,0 +1,45 @@ +/* + * 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 "IImageReader.hpp" + +namespace lms::av +{ + class IAudioFile; +} + +namespace lms::metadata +{ + class AvFormatImageReader : public IImageReader + { + public: + AvFormatImageReader(const std::filesystem::path& p); + ~AvFormatImageReader() override; + + AvFormatImageReader(const AvFormatImageReader&) = delete; + AvFormatImageReader& operator=(const AvFormatImageReader&) = delete; + + private: + void visitImages(ImageVisitor visitor) const override; + + std::unique_ptr _audioFile; + }; +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/AvFormatTagReader.cpp index ea59601d..9bcb6574 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.cpp +++ b/src/libs/metadata/impl/AvFormatTagReader.cpp @@ -156,7 +156,6 @@ namespace lms::metadata _containerInfo = audioFile->getContainerInfo(); _metaDataMap = audioFile->getMetaData(); - _hasEmbeddedCover = audioFile->hasAttachedPictures(); if (debug && core::Service::get()->isSeverityActive(core::logging::Severity::DEBUG)) { diff --git a/src/libs/metadata/impl/AvFormatTagReader.hpp b/src/libs/metadata/impl/AvFormatTagReader.hpp index c18eed1c..a6cd7c4f 100644 --- a/src/libs/metadata/impl/AvFormatTagReader.hpp +++ b/src/libs/metadata/impl/AvFormatTagReader.hpp @@ -40,12 +40,10 @@ namespace lms::metadata void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override; void visitLyricsTags(LyricsVisitor visitor) const override; - bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } const AudioProperties& getAudioProperties() const override { return _audioProperties; } AudioProperties _audioProperties; av::IAudioFile::MetadataMap _metaDataMap; av::ContainerInfo _containerInfo; - bool _hasEmbeddedCover{}; }; } // namespace lms::metadata diff --git a/src/libs/metadata/impl/IImageReader.hpp b/src/libs/metadata/impl/IImageReader.hpp new file mode 100644 index 00000000..84e42cc0 --- /dev/null +++ b/src/libs/metadata/impl/IImageReader.hpp @@ -0,0 +1,36 @@ +/* + * 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 "metadata/Types.hpp" + +namespace lms::metadata +{ + class IImageReader + { + public: + virtual ~IImageReader() = default; + + using ImageVisitor = std::function; + virtual void visitImages(ImageVisitor visitor) const = 0; + }; +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp index 3e2fb85e..7526815a 100644 --- a/src/libs/metadata/impl/ITagReader.hpp +++ b/src/libs/metadata/impl/ITagReader.hpp @@ -161,7 +161,6 @@ namespace lms::metadata using LyricsVisitor = std::function; virtual void visitLyricsTags(LyricsVisitor visitor) const = 0; - virtual bool hasEmbeddedCover() const = 0; virtual const AudioProperties& getAudioProperties() const = 0; }; } // namespace lms::metadata diff --git a/src/libs/metadata/impl/Parser.hpp b/src/libs/metadata/impl/Parser.hpp deleted file mode 100644 index 8f80be21..00000000 --- a/src/libs/metadata/impl/Parser.hpp +++ /dev/null @@ -1,54 +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 "metadata/IParser.hpp" - -#include "ITagReader.hpp" - -namespace lms::metadata -{ - class Parser : public IParser - { - public: - Parser(ParserBackend parserBackend = ParserBackend::TagLib, ParserReadStyle readStyle = ParserReadStyle::Average); - - std::unique_ptr parse(const std::filesystem::path& p, bool debug = false) override; - std::unique_ptr parse(const ITagReader& reader); - - private: - std::span getSupportedExtensions() const override; - void setUserExtraTags(std::span extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); } - void setArtistTagDelimiters(std::span delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } - void setDefaultTagDelimiters(std::span delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } - - void processTags(const ITagReader& reader, Track& track); - - std::optional getMedium(const ITagReader& tagReader); - std::optional getRelease(const ITagReader& tagReader); - - const ParserBackend _parserBackend; - const ParserReadStyle _readStyle; - - std::vector _userExtraTags; - std::vector _artistTagDelimiters; - std::vector _defaultTagDelimiters; - }; -} // namespace lms::metadata diff --git a/src/libs/metadata/impl/TagLibDefs.hpp b/src/libs/metadata/impl/TagLibDefs.hpp new file mode 100644 index 00000000..69e97a8a --- /dev/null +++ b/src/libs/metadata/impl/TagLibDefs.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +// 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 +#endif + +// 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 +#endif diff --git a/src/libs/metadata/impl/TagLibImageReader.cpp b/src/libs/metadata/impl/TagLibImageReader.cpp new file mode 100644 index 00000000..fcbc2eca --- /dev/null +++ b/src/libs/metadata/impl/TagLibImageReader.cpp @@ -0,0 +1,431 @@ +/* + * 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 "TagLibImageReader.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" + +#include "core/String.hpp" +#include "metadata/Exception.hpp" + +#include "TagLibDefs.hpp" + +namespace lms::metadata +{ + namespace + { + class ParsingFailedException : public Exception + { + }; + + Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type) + { + switch (type) + { + case TagLib::ID3v2::AttachedPictureFrame::Type::Other: + return Image::Type::Other; + case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon: + return Image::Type::FileIcon; + case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon: + return Image::Type::OtherFileIcon; + case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover: + return Image::Type::FrontCover; + case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover: + return Image::Type::BackCover; + case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage: + return Image::Type::LeafletPage; + case TagLib::ID3v2::AttachedPictureFrame::Type::Media: + return Image::Type::Media; + case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist: + return Image::Type::LeadArtist; + case TagLib::ID3v2::AttachedPictureFrame::Type::Artist: + return Image::Type::Artist; + case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor: + return Image::Type::Conductor; + case TagLib::ID3v2::AttachedPictureFrame::Type::Band: + return Image::Type::Band; + case TagLib::ID3v2::AttachedPictureFrame::Type::Composer: + return Image::Type::Composer; + case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist: + return Image::Type::Lyricist; + case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation: + return Image::Type::RecordingLocation; + case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording: + return Image::Type::DuringRecording; + case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance: + return Image::Type::DuringPerformance; + case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture: + return Image::Type::MovieScreenCapture; + case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish: + return Image::Type::ColouredFish; + case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration: + return Image::Type::Illustration; + case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo: + return Image::Type::BandLogo; + case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo: + return Image::Type::PublisherLogo; + } + + return Image::Type::Unknown; + } + + Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type) + { + switch (type) + { + case TagLib::ASF::Picture::Type::Other: + return Image::Type::Other; + case TagLib::ASF::Picture::Type::FileIcon: + return Image::Type::FileIcon; + case TagLib::ASF::Picture::Type::OtherFileIcon: + return Image::Type::OtherFileIcon; + case TagLib::ASF::Picture::Type::FrontCover: + return Image::Type::FrontCover; + case TagLib::ASF::Picture::Type::BackCover: + return Image::Type::BackCover; + case TagLib::ASF::Picture::Type::LeafletPage: + return Image::Type::LeafletPage; + case TagLib::ASF::Picture::Type::Media: + return Image::Type::Media; + case TagLib::ASF::Picture::Type::LeadArtist: + return Image::Type::LeadArtist; + case TagLib::ASF::Picture::Type::Artist: + return Image::Type::Artist; + case TagLib::ASF::Picture::Type::Conductor: + return Image::Type::Conductor; + case TagLib::ASF::Picture::Type::Band: + return Image::Type::Band; + case TagLib::ASF::Picture::Type::Composer: + return Image::Type::Composer; + case TagLib::ASF::Picture::Type::Lyricist: + return Image::Type::Lyricist; + case TagLib::ASF::Picture::Type::RecordingLocation: + return Image::Type::RecordingLocation; + case TagLib::ASF::Picture::Type::DuringRecording: + return Image::Type::DuringRecording; + case TagLib::ASF::Picture::Type::DuringPerformance: + return Image::Type::DuringPerformance; + case TagLib::ASF::Picture::Type::MovieScreenCapture: + return Image::Type::MovieScreenCapture; + case TagLib::ASF::Picture::Type::ColouredFish: + return Image::Type::ColouredFish; + case TagLib::ASF::Picture::Type::Illustration: + return Image::Type::Illustration; + case TagLib::ASF::Picture::Type::BandLogo: + return Image::Type::BandLogo; + case TagLib::ASF::Picture::Type::PublisherLogo: + return Image::Type::PublisherLogo; + } + + return Image::Type::Unknown; + } + + Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type) + { + switch (type) + { + case TagLib::FLAC::Picture::Type::Other: + return Image::Type::Other; + case TagLib::FLAC::Picture::Type::FileIcon: + return Image::Type::FileIcon; + case TagLib::FLAC::Picture::Type::OtherFileIcon: + return Image::Type::OtherFileIcon; + case TagLib::FLAC::Picture::Type::FrontCover: + return Image::Type::FrontCover; + case TagLib::FLAC::Picture::Type::BackCover: + return Image::Type::BackCover; + case TagLib::FLAC::Picture::Type::LeafletPage: + return Image::Type::LeafletPage; + case TagLib::FLAC::Picture::Type::Media: + return Image::Type::Media; + case TagLib::FLAC::Picture::Type::LeadArtist: + return Image::Type::LeadArtist; + case TagLib::FLAC::Picture::Type::Artist: + return Image::Type::Artist; + case TagLib::FLAC::Picture::Type::Conductor: + return Image::Type::Conductor; + case TagLib::FLAC::Picture::Type::Band: + return Image::Type::Band; + case TagLib::FLAC::Picture::Type::Composer: + return Image::Type::Composer; + case TagLib::FLAC::Picture::Type::Lyricist: + return Image::Type::Lyricist; + case TagLib::FLAC::Picture::Type::RecordingLocation: + return Image::Type::RecordingLocation; + case TagLib::FLAC::Picture::Type::DuringRecording: + return Image::Type::DuringRecording; + case TagLib::FLAC::Picture::Type::DuringPerformance: + return Image::Type::DuringPerformance; + case TagLib::FLAC::Picture::Type::MovieScreenCapture: + return Image::Type::MovieScreenCapture; + case TagLib::FLAC::Picture::Type::ColouredFish: + return Image::Type::ColouredFish; + case TagLib::FLAC::Picture::Type::Illustration: + return Image::Type::Illustration; + case TagLib::FLAC::Picture::Type::BandLogo: + return Image::Type::BandLogo; + case TagLib::FLAC::Picture::Type::PublisherLogo: + return Image::Type::PublisherLogo; + } + + return Image::Type::Unknown; + } + + const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format) + { + switch (format) + { + case TagLib::MP4::CoverArt::Format::BMP: + return "image/bmp"; + case TagLib::MP4::CoverArt::Format::GIF: + return "image/gif"; + case TagLib::MP4::CoverArt::Format::JPEG: + return "image/jpeg"; + case TagLib::MP4::CoverArt::Format::PNG: + return "image/png"; + case TagLib::MP4::CoverArt::Format::Unknown: + return "application/octet-stream"; + } + + return "application/octet-stream"; + } + +#if 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")) + return Image::Type::BackCover; + + return Image::Type::Unknown; + } +#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES + + void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor) + { + const auto& frameListMap{ id3v2Tags.frameListMap() }; + + for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"]) + { + const auto* attachedPictureFrame{ dynamic_cast(frame) }; + if (!attachedPictureFrame) + continue; + + TagLib::ByteVector picture{ attachedPictureFrame->picture() }; + std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; + + Image image; + image.type = imageTypeFromfromIDv2(attachedPictureFrame->type()); + image.description = attachedPictureFrame->description().to8Bit(true); + image.mimeType = attachedPictureFrame->mimeType().to8Bit(true); + image.data = pictureData; + visitor(image); + } + } + + void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor) + { + for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture")) + { + TagLib::ASF::Picture asfPicture{ attribute.toPicture() }; + if (!asfPicture.isValid()) + continue; + + TagLib::ByteVector picture{ asfPicture.picture() }; + std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; + + Image image; + image.type = imageTypeFromfromASF(asfPicture.type()); + image.description = asfPicture.description().to8Bit(true); + image.mimeType = asfPicture.mimeType().to8Bit(true); + image.data = pictureData; + + visitor(image); + } + } + + void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor) + { + 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) + return; +#endif + TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() }; + for (TagLib::MP4::CoverArt& coverArt : coverArtList) + { + TagLib::ByteVector picture{ coverArt.data() }; + std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; + + Image image; + image.mimeType = mp4ImageFormatToMimeType(coverArt.format()); + image.data = pictureData; + + visitor(image); + } + } + + void visitFLACImages(const TagLib::List pictureList, TagLibImageReader::ImageVisitor visitor) + { + for (TagLib::FLAC::Picture* flacPicture : pictureList) + { + TagLib::ByteVector picture{ flacPicture->data() }; + std::span pictureData{ reinterpret_cast(picture.data()), picture.size() }; + + Image image; + image.type = imageTypeFromfromFLAC(flacPicture->type()); + image.description = flacPicture->description().to8Bit(true); + image.mimeType = flacPicture->mimeType().to8Bit(true); + image.data = pictureData; + + visitor(image); + } + } + + void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor) + { +#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES + const TagLib::List pictureProperties{ apeTags.complexProperties("PICTURE") }; + for (const TagLib::VariantMap& pictureProperty : pictureProperties) + { + Image image; + TagLib::ByteVector picture; + + if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend()) + image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true)); + if (auto it{ pictureProperty.find("mimeType") }; it != pictureProperty.cend()) + image.mimeType = it->second.toString().to8Bit(true); + if (auto it{ pictureProperty.find("description") }; it != pictureProperty.cend()) + image.description = it->second.toString().to8Bit(true); + if (auto it{ pictureProperty.find("data") }; it != pictureProperty.cend()) + { + picture = it->second.toByteVector(); + image.data = { reinterpret_cast(picture.data()), picture.size() }; + } + + if (!image.data.empty()) + visitor(image); + } + +#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES + } + } // namespace + + TagLib::FileRef parseFile(const std::filesystem::path& p) + { + LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForImages"); + + return TagLib::FileRef{ p.c_str(), false }; + } + + TagLibImageReader::TagLibImageReader(const std::filesystem::path& p) + : _file{ parseFile(p) } + { + if (_file.isNull()) + { + LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed"); + throw ParsingFailedException{}; + } + } + + void TagLibImageReader::visitImages(ImageVisitor visitor) const + { + // MP3 + if (TagLib::MPEG::File * mp3File{ dynamic_cast(_file.file()) }) + { + if (mp3File->hasID3v2Tag()) + visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor)); + } + // MP4 + else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) + { + visitMP4Images(*mp4File, std::move(visitor)); + } + // WMA + else if (TagLib::ASF::File * asfFile{ dynamic_cast(_file.file()) }) + { + if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) + visitASFImages(*tag, std::move(visitor)); + } + // FLAC + else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.file()) }) + { + if (flacFile->hasID3v2Tag()) // usage discouraged + visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor)); + else + visitFLACImages(flacFile->pictureList(), std::move(visitor)); + } + // Ogg vorbis + else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.file()) }) + { + visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor)); + } + // Ogg Opus + else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.file()) }) + { + visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor)); + } + // Aiff + else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.file()) }) + { + if (aiffFile->hasID3v2Tag()) + visitID3V2Images(*aiffFile->tag(), std::move(visitor)); + } + // Wav + else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(_file.file()) }) + { + if (wavFile->hasID3v2Tag()) + visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor)); + } + // MPC + else if (TagLib::MPC::File * mpcFile{ dynamic_cast(_file.file()) }) + { + if (mpcFile->hasAPETag()) + visitAPEImages(*mpcFile->APETag(), std::move(visitor)); + } + // WavPack + else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast(_file.file()) }) + { + if (wavPackFile->hasAPETag()) + visitAPEImages(*wavPackFile->APETag(), std::move(visitor)); + } + } +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/TagLibImageReader.hpp b/src/libs/metadata/impl/TagLibImageReader.hpp new file mode 100644 index 00000000..075169af --- /dev/null +++ b/src/libs/metadata/impl/TagLibImageReader.hpp @@ -0,0 +1,37 @@ +/* + * 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 "IImageReader.hpp" + +#include + +namespace lms::metadata +{ + class TagLibImageReader : public IImageReader + { + public: + TagLibImageReader(const std::filesystem::path& p); + + void visitImages(ImageVisitor visitor) const override; + + const TagLib::FileRef _file; + }; +} // namespace lms::metadata diff --git a/src/libs/metadata/impl/TagLibTagReader.cpp b/src/libs/metadata/impl/TagLibTagReader.cpp index f64f2612..62951618 100644 --- a/src/libs/metadata/impl/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/TagLibTagReader.cpp @@ -49,9 +49,7 @@ #include "core/String.hpp" #include "metadata/Exception.hpp" -#if (TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) - #define TAGLIB_HAS_MP4_ITEM_TYPE -#endif +#include "TagLibDefs.hpp" namespace lms::metadata { @@ -216,7 +214,7 @@ namespace lms::metadata TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle) { - LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile"); + LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForMetaData"); return TagLib::FileRef{ p.c_str(), true // read audio properties , @@ -269,10 +267,6 @@ namespace lms::metadata const auto& frameListMap{ id3v2Tags.frameListMap() }; - // Not that good embedded pictures handling - if (!frameListMap["APIC"].isEmpty()) - _hasEmbeddedCover = true; - // Get some extra tags that may not be known by taglib if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE")) _propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() }; @@ -329,9 +323,6 @@ namespace lms::metadata { if (const TagLib::ASF::Tag * tag{ asfFile->tag() }) { - if (tag->attributeListMap().contains("WM/Picture")) - _hasEmbeddedCover = true; - for (const auto& [name, attributeList] : tag->attributeListMap()) { if (attributeList.isEmpty()) @@ -370,14 +361,6 @@ namespace lms::metadata // MP4 else if (TagLib::MP4::File * mp4File{ dynamic_cast(_file.file()) }) { - if (const TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; coverItem.isValid()) - { -#if TAGLIB_HAS_MP4_ITEM_TYPE - if (coverItem.type() == TagLib::MP4::Item::Type::CoverArtList) -#endif - _hasEmbeddedCover = true; - } - // Taglib does not expose rtng in properties if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid()) { @@ -421,18 +404,8 @@ namespace lms::metadata // FLAC else if (TagLib::FLAC::File * flacFile{ dynamic_cast(_file.file()) }) { - if (!flacFile->pictureList().isEmpty()) - _hasEmbeddedCover = true; - } - else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast(_file.file()) }) - { - if (!vorbisFile->tag()->pictureList().isEmpty()) - _hasEmbeddedCover = true; - } - else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast(_file.file()) }) - { - if (!opusFile->tag()->pictureList().isEmpty()) - _hasEmbeddedCover = true; + if (flacFile->hasID3v2Tag()) // discouraged usage + processID3v2Tags(*flacFile->ID3v2Tag()); } else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(_file.file()) }) { diff --git a/src/libs/metadata/impl/TagLibTagReader.hpp b/src/libs/metadata/impl/TagLibTagReader.hpp index 86341ba4..43d938a7 100644 --- a/src/libs/metadata/impl/TagLibTagReader.hpp +++ b/src/libs/metadata/impl/TagLibTagReader.hpp @@ -26,8 +26,6 @@ #include #include -#include "metadata/IParser.hpp" - #include "ITagReader.hpp" namespace lms::metadata @@ -36,24 +34,22 @@ namespace lms::metadata { public: TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug); - - private: + ~TagLibTagReader() = default; TagLibTagReader(const TagLibTagReader&) = delete; TagLibTagReader& operator=(const TagLibTagReader&) = 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; - bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } const AudioProperties& getAudioProperties() const override { return _audioProperties; } const TagLib::FileRef _file; AudioProperties _audioProperties; TagLib::PropertyMap _propertyMap; // case-insensitive keys - bool _hasEmbeddedCover{}; std::multimap _id3v2Lyrics; }; } // namespace lms::metadata diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/metadata/impl/Utils.hpp index dc7671e8..3302fed0 100644 --- a/src/libs/metadata/impl/Utils.hpp +++ b/src/libs/metadata/impl/Utils.hpp @@ -25,7 +25,7 @@ #include -#include "metadata/IParser.hpp" +#include "metadata/Types.hpp" namespace lms::metadata::utils { diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IAudioFileParser.hpp similarity index 58% rename from src/libs/metadata/include/metadata/IParser.hpp rename to src/libs/metadata/include/metadata/IAudioFileParser.hpp index 3f35cfc8..f95d84b5 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IAudioFileParser.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -27,30 +28,18 @@ namespace lms::metadata { - class IParser + class IAudioFileParser { public: - virtual ~IParser() = default; + virtual ~IAudioFileParser() = default; - virtual std::unique_ptr parse(const std::filesystem::path& p, bool debug = false) = 0; + virtual std::unique_ptr parseMetaData(const std::filesystem::path& p) = 0; + + using ImageVisitor = std::function; + virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) = 0; virtual std::span getSupportedExtensions() const = 0; - virtual void setUserExtraTags(std::span extraTags) = 0; - virtual void setArtistTagDelimiters(std::span delimiters) = 0; - virtual void setDefaultTagDelimiters(std::span delimiters) = 0; }; - enum class ParserBackend - { - TagLib, - AvFormat, - }; - - enum class ParserReadStyle - { - Fast, - Average, - Accurate, - }; - std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle); + std::unique_ptr createAudioFileParser(const AudioFileParserParameters& parameters); } // namespace lms::metadata diff --git a/src/libs/metadata/include/metadata/Types.hpp b/src/libs/metadata/include/metadata/Types.hpp index 017fd62e..e1cb9351 100644 --- a/src/libs/metadata/include/metadata/Types.hpp +++ b/src/libs/metadata/include/metadata/Types.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -125,7 +126,6 @@ namespace lms::metadata core::PartialDateTime originalDate; std::optional advisory; core::PartialDateTime encodingTime; - bool hasCover{}; std::optional acoustID; std::string copyright; std::string copyrightURL; @@ -142,4 +142,84 @@ 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 AudioFileParserParameters + { + ParserBackend backend{ ParserBackend::TagLib }; + ParserReadStyle readStyle{ ParserReadStyle::Average }; + std::vector artistTagDelimiters; + std::vector defaultTagDelimiters; + std::vector userExtraTags; + bool debug{}; + }; } // namespace lms::metadata \ No newline at end of file diff --git a/src/libs/metadata/test/Parser.cpp b/src/libs/metadata/test/AudioFileParser.cpp similarity index 87% rename from src/libs/metadata/test/Parser.cpp rename to src/libs/metadata/test/AudioFileParser.cpp index 5fdffacf..6aeeebaf 100644 --- a/src/libs/metadata/test/Parser.cpp +++ b/src/libs/metadata/test/AudioFileParser.cpp @@ -23,14 +23,25 @@ #include -#include "Parser.hpp" +#include "AudioFileParser.hpp" #include "TestTagReader.hpp" +#include "metadata/Types.hpp" namespace lms::metadata { - TEST(Parser, generalTest) + class TestAudioFileParser : public AudioFileParser { - Parser parser; + 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 }; TestTagReader testTags{ { { TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } }, @@ -88,9 +99,7 @@ namespace lms::metadata { "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } }); testTags.setLyricsTags({ { "eng", "[00:00.00]First line\n[00:01.00]Second line" } }); - static_cast(parser).setUserExtraTags(std::vector{ "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" }); - - const std::unique_ptr track{ parser.parse(testTags) }; + const std::unique_ptr track{ parser.parseMetaData(testTags) }; // Audio properties { @@ -130,7 +139,6 @@ namespace lms::metadata EXPECT_EQ(track->date.getYear(), 2020); EXPECT_EQ(track->date.getMonth(), 3); EXPECT_EQ(track->date.getDay(), 4); - EXPECT_FALSE(track->hasCover); ASSERT_EQ(track->genres.size(), 2); EXPECT_EQ(track->genres[0], "Genre1"); EXPECT_EQ(track->genres[1], "Genre2"); @@ -234,7 +242,7 @@ namespace lms::metadata } } - TEST(Parser, trim) + TEST(AudioFileParser, trim) { const TestTagReader testTags{ { @@ -242,7 +250,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->genres.size(), 3); EXPECT_EQ(track->genres[0], "Genre1"); @@ -250,7 +258,7 @@ namespace lms::metadata EXPECT_EQ(track->genres[2], "Genre3"); } - TEST(Parser, customDelimiters) + TEST(AudioFileParser, customDelimiters) { const TestTagReader testTags{ { @@ -263,10 +271,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setDefaultTagDelimiters(std::vector{ " ; ", "/" }); - static_cast(parser).setArtistTagDelimiters(std::vector{ " / ", " feat. " }); - std::unique_ptr track{ parser.parse(testTags) }; + 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"); @@ -292,7 +301,7 @@ namespace lms::metadata EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2"); } - TEST(Parser, customDelimiters_foundInArtist) + TEST(AudioFileParser, customDelimiters_foundInArtist) { const TestTagReader testTags{ { @@ -301,10 +310,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ "; " }); + AudioFileParserParameters params; + params.artistTagDelimiters = { "; " }; + TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parse(testTags) }; + std::unique_ptr track{ parser.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -312,7 +322,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist } - TEST(Parser, customDelimiters_foundInArtists) + TEST(AudioFileParser, customDelimiters_foundInArtists) { const TestTagReader testTags{ { @@ -321,10 +331,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ "; " }); + AudioFileParserParameters params; + params.artistTagDelimiters = { "; " }; + TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parse(testTags) }; + std::unique_ptr track{ parser.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -332,7 +343,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2"); } - TEST(Parser, customDelimiters_notUsed) + TEST(AudioFileParser, customDelimiters_notUsed) { const TestTagReader testTags{ { @@ -341,10 +352,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ "; " }); + AudioFileParserParameters params; + params.artistTagDelimiters = { "; " }; + TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parse(testTags) }; + std::unique_ptr track{ parser.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -352,7 +364,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2"); } - TEST(Parser, customDelimiters_onlyInArtist) + TEST(AudioFileParser, customDelimiters_onlyInArtist) { const TestTagReader testTags{ { @@ -360,10 +372,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ " & " }); + AudioFileParserParameters params; + params.artistTagDelimiters = { " & " }; + TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parse(testTags) }; + std::unique_ptr track{ parser.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -371,7 +384,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing } - TEST(Parser, customDelimitersUsedForArtists) + TEST(AudioFileParser, customDelimitersUsedForArtists) { const TestTagReader testTags{ { @@ -379,10 +392,11 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ " & " }); + AudioFileParserParameters params; + params.artistTagDelimiters = { " & " }; + TestAudioFileParser parser{ params }; - std::unique_ptr track{ parser.parse(testTags) }; + std::unique_ptr track{ parser.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -390,7 +404,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing } - TEST(Parser, noArtistInArtist) + TEST(AudioFileParser, noArtistInArtist) { const TestTagReader testTags{ { @@ -398,13 +412,13 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 0); EXPECT_EQ(track->artistDisplayName, ""); } - TEST(Parser, singleArtistInArtists) + TEST(AudioFileParser, singleArtistInArtists) { const TestTagReader testTags{ { @@ -413,14 +427,14 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + 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(Parser, multipleArtistsInArtist) + TEST(AudioFileParser, multipleArtistsInArtist) { const TestTagReader testTags{ { @@ -429,7 +443,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -437,7 +451,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found } - TEST(Parser, multipleArtistsInArtists) + TEST(AudioFileParser, multipleArtistsInArtists) { const TestTagReader testTags{ { @@ -446,7 +460,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -454,7 +468,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist } - TEST(Parser, multipleArtistsInArtistsWithEndDelimiter) + TEST(AudioFileParser, multipleArtistsInArtistsWithEndDelimiter) { const TestTagReader testTags{ { @@ -463,7 +477,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -471,7 +485,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)"); } - TEST(Parser, singleArtistInAlbumArtists) + TEST(AudioFileParser, singleArtistInAlbumArtists) { const TestTagReader testTags{ { @@ -481,7 +495,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_TRUE(track->medium); ASSERT_TRUE(track->medium->release); @@ -490,7 +504,7 @@ namespace lms::metadata EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1"); } - TEST(Parser, multipleArtistsInAlbumArtist) + TEST(AudioFileParser, multipleArtistsInAlbumArtist) { const TestTagReader testTags{ { @@ -500,7 +514,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_TRUE(track->medium); ASSERT_TRUE(track->medium->release); @@ -510,7 +524,7 @@ namespace lms::metadata EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found } - TEST(Parser, multipleArtistsInAlbumArtists_displayName) + TEST(AudioFileParser, multipleArtistsInAlbumArtists_displayName) { const TestTagReader testTags{ { @@ -520,7 +534,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_TRUE(track->medium); ASSERT_TRUE(track->medium->release); @@ -530,7 +544,7 @@ namespace lms::metadata EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2"); } - TEST(Parser, multipleArtistsInAlbumArtists) + TEST(AudioFileParser, multipleArtistsInAlbumArtists) { const TestTagReader testTags{ { @@ -540,7 +554,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_TRUE(track->medium); ASSERT_TRUE(track->medium->release); @@ -550,7 +564,7 @@ namespace lms::metadata EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist } - TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs) + TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs) { const TestTagReader testTags{ { @@ -560,7 +574,7 @@ namespace lms::metadata } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -570,7 +584,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2"); } - TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters) + TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters) { const TestTagReader testTags{ { @@ -579,9 +593,10 @@ namespace lms::metadata } }; - Parser parser; - static_cast(parser).setArtistTagDelimiters(std::vector{ " / " }); - std::unique_ptr track{ parser.parse(testTags) }; + 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"); @@ -591,7 +606,7 @@ namespace lms::metadata EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } - TEST(Parser, MBIDs_fallback) + TEST(AudioFileParser, MBIDs_fallback) { TestTagReader testTags{ { @@ -611,7 +626,7 @@ namespace lms::metadata testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } }, { "RoleB", { "Artist2", "Artist4" } } }); - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->artists.size(), 2); EXPECT_EQ(track->artists[0].name, "Artist1"); @@ -696,7 +711,7 @@ namespace lms::metadata EXPECT_EQ(track->performerArtists["Roleb"][1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b")); } - TEST(Parser, MBIDs_fallback_priority) + TEST(AudioFileParser, MBIDs_fallback_priority) { const TestTagReader testTags{ { @@ -708,7 +723,7 @@ namespace lms::metadata { TagType::Composer, { "Artist1" } }, } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->composerArtists.size(), 1); EXPECT_EQ(track->composerArtists[0].name, "Artist1"); @@ -716,7 +731,7 @@ namespace lms::metadata EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673")); } - TEST(Parser, release_sortNameFallback) + TEST(AudioFileParser, release_sortNameFallback) { const TestTagReader testTags{ { @@ -724,14 +739,14 @@ namespace lms::metadata // No AlbumSortOrder } }; - std::unique_ptr track{ Parser{}.parse(testTags) }; + 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(Parser, advisory) + TEST(AudioFileParser, advisory) { auto doTest = [](std::string_view value, std::optional expectedValue) { const TestTagReader testTags{ @@ -740,8 +755,8 @@ namespace lms::metadata } }; - Parser parser; - std::unique_ptr track{ Parser{}.parse(testTags) }; + 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()) @@ -758,7 +773,7 @@ namespace lms::metadata doTest("3", std::nullopt); } - TEST(Parser, encodingTime) + TEST(AudioFileParser, encodingTime) { auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { const TestTagReader testTags{ @@ -767,8 +782,7 @@ namespace lms::metadata } }; - Parser parser; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'"; }; @@ -780,7 +794,7 @@ namespace lms::metadata doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 }); } - TEST(Parser, date) + TEST(AudioFileParser, date) { auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) { const TestTagReader testTags{ @@ -789,8 +803,7 @@ namespace lms::metadata } }; - Parser parser; - std::unique_ptr track{ Parser{}.parse(testTags) }; + std::unique_ptr track{ TestAudioFileParser{}.parseMetaData(testTags) }; ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'"; }; diff --git a/src/libs/metadata/test/CMakeLists.txt b/src/libs/metadata/test/CMakeLists.txt index 895fc49d..e51d98c9 100644 --- a/src/libs/metadata/test/CMakeLists.txt +++ b/src/libs/metadata/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(GoogleTest) add_executable(test-metadata Lyrics.cpp Metadata.cpp - Parser.cpp + AudioFileParser.cpp PlayList.cpp Utils.cpp ) diff --git a/src/libs/metadata/test/TestTagReader.hpp b/src/libs/metadata/test/TestTagReader.hpp index 77ebcdf5..4977df63 100644 --- a/src/libs/metadata/test/TestTagReader.hpp +++ b/src/libs/metadata/test/TestTagReader.hpp @@ -22,7 +22,7 @@ #include -#include "Parser.hpp" +#include "ITagReader.hpp" namespace lms::metadata { @@ -95,8 +95,6 @@ namespace lms::metadata visitor(language, lyrics); } - bool hasEmbeddedCover() const override { return false; }; - const AudioProperties& getAudioProperties() const override { return audioProperties; } private: diff --git a/src/libs/services/artwork/CMakeLists.txt b/src/libs/services/artwork/CMakeLists.txt index 74c3f91c..56488436 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 - lmsav 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 3b02b1ef..9dca6e5a 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -22,8 +22,6 @@ #include #include -#include "av/IAudioFile.hpp" -#include "av/Types.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/String.hpp" @@ -35,6 +33,7 @@ #include "image/Exception.hpp" #include "image/IEncodedImage.hpp" #include "image/Image.hpp" +#include "metadata/IAudioFileParser.hpp" namespace lms::cover { @@ -55,6 +54,7 @@ namespace lms::cover 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)); @@ -66,61 +66,7 @@ namespace lms::cover _defaultArtistImage = image::readImage(defaultArtistImageSvgPath); // may throw } - std::unique_ptr ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, std::optional width) const - { - struct CandidatePicture - { - av::Picture picture; - bool isFront{}; - std::size_t index; - - // > means is better candidate - bool operator>(const CandidatePicture& other) const - { - if (!isFront && other.isFront) - return false; - if (isFront && !other.isFront) - return true; - - return index < other.index; - } - }; - - auto metadataHasFrontKeyword{ [](const av::IAudioFile::MetadataMap& metadata) { - return std::any_of(std::cbegin(metadata), std::cend(metadata), [](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, "front"); }); - } }; - - std::vector candidatePictures; - std::size_t pictureIndex{}; - input.visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metadata) { - candidatePictures.emplace_back(CandidatePicture{ picture, metadataHasFrontKeyword(metadata), pictureIndex++ }); - }); - std::stable_sort(std::begin(candidatePictures), std::end(candidatePictures), std::greater<>()); - - std::unique_ptr image; - for (const CandidatePicture& candidatePicture : candidatePictures) - { - try - { - if (!width) - { - image = image::readImage(candidatePicture.picture.data, candidatePicture.picture.mimeType); - } - else - { - auto rawImage{ image::decodeImage(candidatePicture.picture.data) }; - rawImage->resize(*width); - image = image::encodeToJPEG(*rawImage, _jpegQuality); - } - } - catch (const image::Exception& e) - { - LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what()); - } - } - - return image; - } + ArtworkService::~ArtworkService() = default; std::unique_ptr ArtworkService::getFromImageFile(const std::filesystem::path& p, std::optional width) const { @@ -175,18 +121,62 @@ namespace lms::cover std::unique_ptr ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional width) const { - std::unique_ptr image; + struct CandidateImage + { + std::unique_ptr image; + bool isFront{}; + std::size_t index; + + // > means is better candidate + bool operator>(const CandidateImage& other) const + { + if (!isFront && other.isFront) + return false; + if (isFront && !other.isFront) + return true; + + return index < other.index; + } + }; + + std::vector candidateImages; + std::size_t pictureIndex{}; try { - image = getFromAvMediaFile(*av::parseAudioFile(p), width); + _audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) { + std::unique_ptr image; + try + { + if (!width) + { + image = image::readImage(parsedImage.data, parsedImage.mimeType); + } + else + { + auto rawImage{ image::decodeImage(parsedImage.data) }; + rawImage->resize(*width); + image = image::encodeToJPEG(*rawImage, _jpegQuality); + } + } + catch (const image::Exception& e) + { + LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what()); + } + + candidateImages.emplace_back(CandidateImage{ .image = std::move(image), .isFront = parsedImage.type == metadata::Image::Type::FrontCover, .index = pictureIndex++ }); + }); } - catch (av::Exception& e) + catch (const metadata::Exception& e) { - LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p << ": " << e.what()); + LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what()); } - return image; + std::stable_sort(std::begin(candidateImages), std::end(candidateImages), std::greater<>()); + if (!candidateImages.empty()) + return std::move(candidateImages.front().image); + + return {}; } std::shared_ptr ArtworkService::getImage(db::ImageId imageId, std::optional width) diff --git a/src/libs/services/artwork/impl/ArtworkService.hpp b/src/libs/services/artwork/impl/ArtworkService.hpp index 1dc1f5cd..ab04d5ad 100644 --- a/src/libs/services/artwork/impl/ArtworkService.hpp +++ b/src/libs/services/artwork/impl/ArtworkService.hpp @@ -32,9 +32,9 @@ namespace lms::db class Session; } -namespace lms::av +namespace lms::metadata { - class IAudioFile; + class IAudioFileParser; } namespace lms::cover @@ -43,7 +43,7 @@ namespace lms::cover { public: ArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath); - ~ArtworkService() override = default; + ~ArtworkService() override; ArtworkService(const ArtworkService&) = delete; ArtworkService& operator=(const ArtworkService&) = delete; @@ -56,7 +56,6 @@ namespace lms::cover void flushCache() override; void setJpegQuality(unsigned quality) override; - std::unique_ptr getFromAvMediaFile(const av::IAudioFile& input, std::optional width) const; std::unique_ptr getFromImageFile(const std::filesystem::path& p, std::optional width) const; std::unique_ptr getTrackImage(const std::filesystem::path& path, std::optional width) const; @@ -64,6 +63,7 @@ namespace lms::cover db::Db& _db; + std::unique_ptr _audioFileParser; ImageCache _cache; std::shared_ptr _defaultReleaseCover; std::shared_ptr _defaultArtistImage; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp index ba2fc2ca..a82fd73c 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.cpp @@ -37,13 +37,16 @@ #include "database/TrackFeatures.hpp" #include "database/TrackLyrics.hpp" #include "database/Types.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" #include "metadata/Exception.hpp" -#include "metadata/IParser.hpp" +#include "metadata/IAudioFileParser.hpp" #include "IFileScanOperation.hpp" #include "ScanContext.hpp" #include "ScannerSettings.hpp" #include "Utils.hpp" +#include "metadata/Types.hpp" namespace lms::scanner { @@ -345,7 +348,7 @@ namespace lms::scanner return db::Advisory::UnSet; } - db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo) + db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, bool hasEmbeddedCover, const FileInfo& fileInfo) { db::Track::FindParameters params; // Add as many fields as possible to limit errors @@ -359,7 +362,7 @@ namespace lms::scanner } if (parsedTrack.position) params.setTrackNumber(*parsedTrack.position); - params.setHasEmbeddedImage(parsedTrack.hasCover); + params.setHasEmbeddedImage(hasEmbeddedCover); params.setFileSize(fileInfo.fileSize); bool error{}; @@ -387,7 +390,7 @@ namespace lms::scanner class AudioFileScanOperation : public IFileScanOperation { public: - AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IParser& parser, const ScannerSettings& settings) + AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IAudioFileParser& parser, const ScannerSettings& settings) : _file{ fileToScan.file } , _mediaLibrary{ fileToScan.mediaLibrary } , _db{ db } @@ -408,9 +411,10 @@ namespace lms::scanner const std::filesystem::path _file; const MediaLibraryInfo _mediaLibrary; db::Db& _db; - metadata::IParser& _parser; + metadata::IAudioFileParser& _parser; const ScannerSettings& _settings; std::unique_ptr _parsedTrack; + bool _hasEmbeddedCover{}; }; void AudioFileScanOperation::scan() @@ -420,7 +424,19 @@ namespace lms::scanner try { - _parsedTrack = _parser.parse(_file); + _parsedTrack = _parser.parseMetaData(_file); + + _parser.parseImages(_file, [&](const metadata::Image& image) { + try + { + image::probeImage(image.data); + _hasEmbeddedCover = true; + } + catch (const image::Exception& e) + { + LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file); + } + }); } catch (const metadata::Exception& e) { @@ -505,7 +521,7 @@ namespace lms::scanner if (!track) { // maybe the file just moved? - track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo); + track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, _hasEmbeddedCover, *fileInfo); if (track) { LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath()); @@ -646,7 +662,7 @@ namespace lms::scanner track.modify()->setTrackMBID(_parsedTrack->mbid); if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) }) trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setHasCover(_parsedTrack->hasCover); + track.modify()->setHasCover(_hasEmbeddedCover); track.modify()->setCopyright(_parsedTrack->copyright); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); @@ -686,18 +702,26 @@ namespace lms::scanner throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" }; } + + metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings) + { + metadata::AudioFileParserParameters params; + params.userExtraTags = settings.extraTags; + params.artistTagDelimiters = settings.artistTagDelimiters; + params.defaultTagDelimiters = settings.defaultTagDelimiters; + params.backend = metadata::ParserBackend::TagLib; + params.readStyle = getParserReadStyle(); + + return params; + } + } // namespace AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings) : _db{ db } , _settings{ settings } - , _metadataParser{ metadata::createParser(metadata::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib + , _metadataParser{ metadata::createAudioFileParser(createAudioFileParserParameters(settings)) } // For now, always use TagLib { - std::vector tagsToParse{ _extraTagsToParse }; - tagsToParse.insert(std::end(tagsToParse), std::cbegin(settings.extraTags), std::cend(settings.extraTags)); - _metadataParser->setUserExtraTags(tagsToParse); - _metadataParser->setArtistTagDelimiters(settings.artistTagDelimiters); - _metadataParser->setDefaultTagDelimiters(settings.defaultTagDelimiters); } AudioFileScanner::~AudioFileScanner() = default; diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp b/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp index a79fbe4a..dd6a9247 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanner.hpp @@ -19,9 +19,6 @@ #pragma once -#include -#include - #include "IFileScanner.hpp" namespace lms @@ -33,7 +30,7 @@ namespace lms namespace metadata { - class IParser; + class IAudioFileParser; } } // namespace lms @@ -57,7 +54,6 @@ namespace lms::scanner db::Db& _db; const ScannerSettings& _settings; - std::unique_ptr _metadataParser; - const std::vector _extraTagsToParse; + std::unique_ptr _metadataParser; }; } // namespace lms::scanner \ No newline at end of file diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index c147e3ec..15234522 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -31,7 +31,8 @@ #include "core/StreamLogger.hpp" #include "core/String.hpp" #include "metadata/Exception.hpp" -#include "metadata/IParser.hpp" +#include "metadata/IAudioFileParser.hpp" +#include "metadata/Types.hpp" namespace lms::metadata { @@ -159,15 +160,79 @@ namespace lms::metadata return os; } - void parse(IParser& parser, const std::filesystem::path& file) + 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.parse(file, true) }; + std::unique_ptr track{ parser.parseMetaData(file) }; const auto end{ std::chrono::steady_clock::now() }; - std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast(end - start).count() / 1000. << "ms" << std::endl; + 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; @@ -248,8 +313,6 @@ namespace lms::metadata if (track->originalYear) std::cout << "Original year: " << *track->originalYear << std::endl; - std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl; - if (track->replayGain) std::cout << "Track replay gain: " << *track->replayGain << std::endl; @@ -280,6 +343,20 @@ namespace lms::metadata 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[]) @@ -408,41 +485,31 @@ int main(int argc, char* argv[]) } } - if (parsers.contains(Parser::Ffmpeg)) - { + 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 { - std::cout << "Using Ffmpeg:" << std::endl; - - auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) }; - parser->setArtistTagDelimiters(artistTagDelimiters); - parser->setDefaultTagDelimiters(tagDelimiters); - - parse(*parser, file); + 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)) - { - try - { - std::cout << "Using TagLib:" << std::endl; - - auto parser{ metadata::createParser(metadata::ParserBackend::TagLib, metadata::ParserReadStyle::Accurate) }; - parser->setArtistTagDelimiters(artistTagDelimiters); - parser->setDefaultTagDelimiters(tagDelimiters); - - parse(*parser, file); - } - catch (metadata::Exception& e) - { - std::cerr << "Parsing failed: " << e.what() << std::endl; - } - } + parseAudioFile(metadata::ParserBackend::TagLib); } } catch (std::exception& e)