Reworked scanner to use taglib to extract images, now support images in APE tags. This is a step forward multiple image support per track

This commit is contained in:
emeric
2025-03-09 11:20:30 +01:00
parent b5e79d48a6
commit 5effbf99a1
27 changed files with 1151 additions and 353 deletions
+3 -1
View File
@@ -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
)
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Parser.hpp"
#include "AudioFileParser.hpp"
#include <span>
@@ -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<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
{
return std::make_unique<Parser>(parserBackend, parserReadStyle);
return std::make_unique<AudioFileParser>(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<const std::filesystem::path> Parser::getSupportedExtensions() const
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
{
// TODO: use backend capability to retrieve supported formats
static const std::array<std::filesystem::path, 18> fileExtensions{
@@ -363,34 +364,61 @@ namespace lms::metadata
return fileExtensions;
}
std::unique_ptr<Track> Parser::parse(const std::filesystem::path& p, bool debug)
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p)
{
try
{
std::unique_ptr<ITagReader> tagReader;
switch (_parserBackend)
switch (_params.backend)
{
case ParserBackend::TagLib:
tagReader = std::make_unique<TagLibTagReader>(p, _readStyle, debug);
tagReader = std::make_unique<TagLibTagReader>(p, _params.readStyle, _params.debug);
break;
case ParserBackend::AvFormat:
tagReader = std::make_unique<AvFormatTagReader>(p, debug);
tagReader = std::make_unique<AvFormatTagReader>(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<Track> Parser::parse(const ITagReader& tagReader)
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor)
{
try
{
std::unique_ptr<IImageReader> imageReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
imageReader = std::make_unique<TagLibImageReader>(p);
break;
case ParserBackend::AvFormat:
imageReader = std::make_unique<AvFormatImageReader>(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<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader)
{
auto track{ std::make_unique<Track>() };
@@ -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<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
@@ -436,32 +462,32 @@ namespace lms::metadata
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
track.replayGain = getTagValueAs<float>(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<std::string>(tagReader, TagType::Genre, _defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _defaultTagDelimiters);
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _params.defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _params.defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _params.defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _params.defaultTagDelimiters);
std::vector<std::string_view> 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<std::string>(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<std::string>(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<Medium> Parser::getMedium(const ITagReader& tagReader)
std::optional<Medium> AudioFileParser::getMedium(const ITagReader& tagReader)
{
std::optional<Medium> medium;
medium.emplace();
@@ -501,7 +527,7 @@ namespace lms::metadata
return medium;
}
std::optional<Release> Parser::getRelease(const ITagReader& tagReader)
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader)
{
std::optional<Release> release;
@@ -512,16 +538,16 @@ namespace lms::metadata
release.emplace();
release->name = std::move(*releaseName);
release->sortName = getTagValueAs<std::string>(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<std::string>(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<std::string>(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters);
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _params.defaultTagDelimiters);
release->comment = getTagValueAs<std::string>(tagReader, TagType::AlbumComment).value_or("");
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _defaultTagDelimiters);
release->countries = getTagValuesAs<std::string>(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<std::string>(tagReader, TagType::ReleaseType, _defaultTagDelimiters);
release->releaseTypes = getTagValuesAs<std::string>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include "metadata/IAudioFileParser.hpp"
namespace lms::metadata
{
class IImageReader;
class ITagReader;
class AudioFileParser : public IAudioFileParser
{
public:
AudioFileParser(const AudioFileParserParameters& parameters = {});
~AudioFileParser() override = default;
AudioFileParser(const AudioFileParser&) = delete;
AudioFileParser& operator=(const AudioFileParser&) = delete;
protected:
std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) override;
std::unique_ptr<Track> parseMetaData(const ITagReader& reader);
void parseImages(const IImageReader& reader, ImageVisitor visitor);
private:
void parseImages(const std::filesystem::path& p, ImageVisitor visitor) override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
void processTags(const ITagReader& reader, Track& track);
std::optional<Medium> getMedium(const ITagReader& tagReader);
std::optional<Release> getRelease(const ITagReader& tagReader);
const AudioFileParserParameters _params;
};
} // namespace lms::metadata
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<av::IAudioFile> _audioFile;
};
} // namespace lms::metadata
@@ -156,7 +156,6 @@ namespace lms::metadata
_containerInfo = audioFile->getContainerInfo();
_metaDataMap = audioFile->getMetaData();
_hasEmbeddedCover = audioFile->hasAttachedPictures();
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
@@ -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
+36
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include "metadata/Types.hpp"
namespace lms::metadata
{
class IImageReader
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(ImageVisitor visitor) const = 0;
};
} // namespace lms::metadata
-1
View File
@@ -161,7 +161,6 @@ namespace lms::metadata
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
virtual bool hasEmbeddedCover() const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
};
} // namespace lms::metadata
-54
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Track> parse(const std::filesystem::path& p, bool debug = false) override;
std::unique_ptr<Track> parse(const ITagReader& reader);
private:
std::span<const std::filesystem::path> getSupportedExtensions() const override;
void setUserExtraTags(std::span<const std::string> extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); }
void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void processTags(const ITagReader& reader, Track& track);
std::optional<Medium> getMedium(const ITagReader& tagReader);
std::optional<Release> getRelease(const ITagReader& tagReader);
const ParserBackend _parserBackend;
const ParserReadStyle _readStyle;
std::vector<std::string> _userExtraTags;
std::vector<std::string> _artistTagDelimiters;
std::vector<std::string> _defaultTagDelimiters;
};
} // namespace lms::metadata
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <taglib/taglib.h>
// 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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "TagLibImageReader.hpp"
#include <taglib/aifffile.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/attachedpictureframe.h>
#include <taglib/flacfile.h>
#include <taglib/flacpicture.h>
#include <taglib/id3v2tag.h>
#include <taglib/mp4coverart.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#include <tbytevector.h>
#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<const TagLib::ID3v2::AttachedPictureFrame*>(frame) };
if (!attachedPictureFrame)
continue;
TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.mimeType = mp4ImageFormatToMimeType(coverArt.format());
image.data = pictureData;
visitor(image);
}
}
void visitFLACImages(const TagLib::List<TagLib::FLAC::Picture*> pictureList, TagLibImageReader::ImageVisitor visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
TagLib::ByteVector picture{ flacPicture->data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(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<TagLib::VariantMap> 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<const std::byte*>(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<TagLib::MPEG::File*>(_file.file()) })
{
if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
{
visitMP4Images(*mp4File, std::move(visitor));
}
// WMA
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, std::move(visitor));
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_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<TagLib::Ogg::Vorbis::File*>(_file.file()) })
{
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
}
// Ogg Opus
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
{
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
}
// Aiff
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
{
if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
}
// Wav
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
{
if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
{
if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
{
if (wavPackFile->hasAPETag())
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
}
}
} // namespace lms::metadata
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "IImageReader.hpp"
#include <taglib/fileref.h>
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
+4 -31
View File
@@ -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<TagLib::MP4::File*>(_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<TagLib::FLAC::File*>(_file.file()) })
{
if (!flacFile->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
{
if (!vorbisFile->tag()->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_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<TagLib::RIFF::AIFF::File*>(_file.file()) })
{
+2 -6
View File
@@ -26,8 +26,6 @@
#include <taglib/fileref.h>
#include <taglib/tpropertymap.h>
#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<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::metadata
+1 -1
View File
@@ -25,7 +25,7 @@
#include <Wt/WDate.h>
#include "metadata/IParser.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata::utils
{
@@ -20,6 +20,7 @@
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <span>
@@ -27,30 +28,18 @@
namespace lms::metadata
{
class IParser
class IAudioFileParser
{
public:
virtual ~IParser() = default;
virtual ~IAudioFileParser() = default;
virtual std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
virtual std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) = 0;
using ImageVisitor = std::function<void(const Image&)>;
virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) = 0;
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
virtual void setUserExtraTags(std::span<const std::string> extraTags) = 0;
virtual void setArtistTagDelimiters(std::span<const std::string> delimiters) = 0;
virtual void setDefaultTagDelimiters(std::span<const std::string> delimiters) = 0;
};
enum class ParserBackend
{
TagLib,
AvFormat,
};
enum class ParserReadStyle
{
Fast,
Average,
Accurate,
};
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle);
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& parameters);
} // namespace lms::metadata
+81 -1
View File
@@ -22,6 +22,7 @@
#include <chrono>
#include <map>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -125,7 +126,6 @@ namespace lms::metadata
core::PartialDateTime originalDate;
std::optional<Advisory> advisory;
core::PartialDateTime encodingTime;
bool hasCover{};
std::optional<core::UUID> acoustID;
std::string copyright;
std::string copyrightURL;
@@ -142,4 +142,84 @@ namespace lms::metadata
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
struct Image
{
// See TagLib types (based on ID3v2 APIC types)
enum class Type
{
// No information
Unknown,
// A type not enumerated below
Other,
// 32x32 PNG image that should be used as the file icon
FileIcon,
// File icon of a different size or format
OtherFileIcon,
// Front cover image of the album
FrontCover,
// Back cover image of the album
BackCover,
// Inside leaflet page of the album
LeafletPage,
// Image from the album itself
Media,
// Picture of the lead artist or soloist
LeadArtist,
// Picture of the artist or performer
Artist,
// Picture of the conductor
Conductor,
// Picture of the band or orchestra
Band,
// Picture of the composer
Composer,
// Picture of the lyricist or text writer
Lyricist,
// Picture of the recording location or studio
RecordingLocation,
// Picture of the artists during recording
DuringRecording,
// Picture of the artists during performance
DuringPerformance,
// Picture from a movie or video related to the track
MovieScreenCapture,
// Picture of a large, coloured fish
ColouredFish,
// Illustration related to the track
Illustration,
// Logo of the band or performer
BandLogo,
// Logo of the publisher (record company)
PublisherLogo
};
Type type{ Type::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span<const std::byte> data;
};
enum class ParserBackend
{
TagLib,
AvFormat,
};
enum class ParserReadStyle
{
Fast,
Average,
Accurate,
};
struct AudioFileParserParameters
{
ParserBackend backend{ ParserBackend::TagLib };
ParserReadStyle readStyle{ ParserReadStyle::Average };
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
std::vector<std::string> userExtraTags;
bool debug{};
};
} // namespace lms::metadata
@@ -23,14 +23,25 @@
#include <Wt/WTime.h>
#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<IParser&>(parser).setUserExtraTags(std::vector<std::string>{ "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" });
const std::unique_ptr<Track> track{ parser.parse(testTags) };
const std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; ", "/" });
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / ", " feat. " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
AudioFileParserParameters params;
params.defaultTagDelimiters = { " ; ", "/" };
params.artistTagDelimiters = { " / ", " feat. " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 3);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artistDisplayName, "Artist1");
}
TEST(Parser, multipleArtistsInArtist)
TEST(AudioFileParser, multipleArtistsInArtist)
{
const TestTagReader testTags{
{
@@ -429,7 +443,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
AudioFileParserParameters params;
params.artistTagDelimiters = { " / " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
}
TEST(Parser, advisory)
TEST(AudioFileParser, advisory)
{
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
const TestTagReader testTags{
@@ -740,8 +755,8 @@ namespace lms::metadata
}
};
Parser parser;
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
AudioFileParser parser;
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
if (track->advisory.has_value())
@@ -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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> 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> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
};
+1 -1
View File
@@ -3,7 +3,7 @@ include(GoogleTest)
add_executable(test-metadata
Lyrics.cpp
Metadata.cpp
Parser.cpp
AudioFileParser.cpp
PlayList.cpp
Utils.cpp
)
+1 -3
View File
@@ -22,7 +22,7 @@
#include <gtest/gtest.h>
#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:
+1 -1
View File
@@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE
)
target_link_libraries(lmsartwork PRIVATE
lmsav
lmsimage
lmsmetadata
)
target_link_libraries(lmsartwork PUBLIC
@@ -22,8 +22,6 @@
#include <algorithm>
#include <functional>
#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<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
{
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
@@ -66,61 +66,7 @@ namespace lms::cover
_defaultArtistImage = image::readImage(defaultArtistImageSvgPath); // may throw
}
std::unique_ptr<image::IEncodedImage> ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, std::optional<image::ImageSize> 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<CandidatePicture> 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::IEncodedImage> 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<image::IEncodedImage> ArtworkService::getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
{
@@ -175,18 +121,62 @@ namespace lms::cover
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
{
std::unique_ptr<image::IEncodedImage> image;
struct CandidateImage
{
std::unique_ptr<image::IEncodedImage> 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<CandidateImage> candidateImages;
std::size_t pictureIndex{};
try
{
image = getFromAvMediaFile(*av::parseAudioFile(p), width);
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
std::unique_ptr<image::IEncodedImage> 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<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
@@ -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<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, std::optional<image::ImageSize> width) const;
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::optional<image::ImageSize> width) const;
@@ -64,6 +63,7 @@ namespace lms::cover
db::Db& _db;
std::unique_ptr<metadata::IAudioFileParser> _audioFileParser;
ImageCache _cache;
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
@@ -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<metadata::Track> _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<std::string> 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;
@@ -19,9 +19,6 @@
#pragma once
#include <string>
#include <vector>
#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<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse;
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
};
} // namespace lms::scanner
+99 -32
View File
@@ -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> track{ parser.parse(file, true) };
std::unique_ptr<Track> 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<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
std::cout << "MetaData parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast<std::chrono::microseconds>(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<std::chrono::microseconds>(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)