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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()) })
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata::utils
|
||||
{
|
||||
|
||||
+8
-19
@@ -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
|
||||
@@ -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 << "'";
|
||||
};
|
||||
@@ -3,7 +3,7 @@ include(GoogleTest)
|
||||
add_executable(test-metadata
|
||||
Lyrics.cpp
|
||||
Metadata.cpp
|
||||
Parser.cpp
|
||||
AudioFileParser.cpp
|
||||
PlayList.cpp
|
||||
Utils.cpp
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user