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()
|
endif()
|
||||||
|
|
||||||
add_library(lmsmetadata STATIC
|
add_library(lmsmetadata STATIC
|
||||||
|
impl/AvFormatImageReader.cpp
|
||||||
impl/AvFormatTagReader.cpp
|
impl/AvFormatTagReader.cpp
|
||||||
impl/Lyrics.cpp
|
impl/Lyrics.cpp
|
||||||
impl/Parser.cpp
|
impl/AudioFileParser.cpp
|
||||||
impl/PlayList.cpp
|
impl/PlayList.cpp
|
||||||
|
impl/TagLibImageReader.cpp
|
||||||
impl/TagLibTagReader.cpp
|
impl/TagLibTagReader.cpp
|
||||||
impl/Utils.cpp
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Parser.hpp"
|
#include "AudioFileParser.hpp"
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
|
#include "AvFormatImageReader.hpp"
|
||||||
#include "AvFormatTagReader.hpp"
|
#include "AvFormatTagReader.hpp"
|
||||||
|
#include "TagLibImageReader.hpp"
|
||||||
#include "TagLibTagReader.hpp"
|
#include "TagLibTagReader.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -316,19 +318,18 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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)
|
AudioFileParser::AudioFileParser(const AudioFileParserParameters& params)
|
||||||
: _parserBackend{ parserBackend }
|
: _params{ params }
|
||||||
, _readStyle{ readStyle }
|
|
||||||
{
|
{
|
||||||
switch (_parserBackend)
|
switch (_params.backend)
|
||||||
{
|
{
|
||||||
case ParserBackend::TagLib:
|
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;
|
break;
|
||||||
|
|
||||||
case ParserBackend::AvFormat:
|
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
|
// TODO: use backend capability to retrieve supported formats
|
||||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||||
@@ -363,34 +364,61 @@ namespace lms::metadata
|
|||||||
return fileExtensions;
|
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
|
try
|
||||||
{
|
{
|
||||||
std::unique_ptr<ITagReader> tagReader;
|
std::unique_ptr<ITagReader> tagReader;
|
||||||
switch (_parserBackend)
|
switch (_params.backend)
|
||||||
{
|
{
|
||||||
case ParserBackend::TagLib:
|
case ParserBackend::TagLib:
|
||||||
tagReader = std::make_unique<TagLibTagReader>(p, _readStyle, debug);
|
tagReader = std::make_unique<TagLibTagReader>(p, _params.readStyle, _params.debug);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ParserBackend::AvFormat:
|
case ParserBackend::AvFormat:
|
||||||
tagReader = std::make_unique<AvFormatTagReader>(p, debug);
|
tagReader = std::make_unique<AvFormatTagReader>(p, _params.debug);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!tagReader)
|
if (!tagReader)
|
||||||
throw ParseException{ "Unhandled parser backend" };
|
throw ParseException{ "Unhandled parser backend" };
|
||||||
|
|
||||||
return parse(*tagReader);
|
return parseMetaData(*tagReader);
|
||||||
}
|
}
|
||||||
catch (const Exception& e)
|
catch (const Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed");
|
||||||
throw ParseException{};
|
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>() };
|
auto track{ std::make_unique<Track>() };
|
||||||
|
|
||||||
@@ -400,10 +428,8 @@ namespace lms::metadata
|
|||||||
return track;
|
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.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
|
||||||
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
||||||
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
||||||
@@ -436,32 +462,32 @@ namespace lms::metadata
|
|||||||
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
||||||
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
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);
|
value = core::stringUtils::stringTrim(value);
|
||||||
if (!value.empty())
|
if (!value.empty())
|
||||||
track.userExtraTags[userExtraTag].push_back(std::string{ value });
|
track.userExtraTags[userExtraTag].push_back(std::string{ value });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _defaultTagDelimiters);
|
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _params.defaultTagDelimiters);
|
||||||
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _defaultTagDelimiters);
|
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _params.defaultTagDelimiters);
|
||||||
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _defaultTagDelimiters);
|
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _params.defaultTagDelimiters);
|
||||||
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _defaultTagDelimiters);
|
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _params.defaultTagDelimiters);
|
||||||
|
|
||||||
std::vector<std::string_view> artistDelimiters{};
|
std::vector<std::string_view> artistDelimiters{};
|
||||||
|
|
||||||
track.medium = getMedium(tagReader);
|
track.medium = getMedium(tagReader);
|
||||||
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
|
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), _artistTagDelimiters);
|
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.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 }, {}, _artistTagDelimiters, _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 }, {}, _artistTagDelimiters, _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 }, {}, _artistTagDelimiters, _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 }, {}, _artistTagDelimiters, _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 }, {}, _artistTagDelimiters, _defaultTagDelimiters);
|
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _params.artistTagDelimiters, _params.defaultTagDelimiters);
|
||||||
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
||||||
|
|
||||||
fillMissingMbids(track);
|
fillMissingMbids(track);
|
||||||
@@ -471,7 +497,7 @@ namespace lms::metadata
|
|||||||
track.originalYear = track.originalDate.getYear();
|
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;
|
std::optional<Medium> medium;
|
||||||
medium.emplace();
|
medium.emplace();
|
||||||
@@ -501,7 +527,7 @@ namespace lms::metadata
|
|||||||
return medium;
|
return medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Release> Parser::getRelease(const ITagReader& tagReader)
|
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader)
|
||||||
{
|
{
|
||||||
std::optional<Release> release;
|
std::optional<Release> release;
|
||||||
|
|
||||||
@@ -512,16 +538,16 @@ namespace lms::metadata
|
|||||||
release.emplace();
|
release.emplace();
|
||||||
release->name = std::move(*releaseName);
|
release->name = std::move(*releaseName);
|
||||||
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or(release->name);
|
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->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), _artistTagDelimiters);
|
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters);
|
||||||
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
||||||
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
||||||
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
||||||
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
|
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
|
||||||
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
|
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->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)
|
if (!release->mediumCount)
|
||||||
{
|
{
|
||||||
// mediumCount may be encoded as "position/count"
|
// 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;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor)
|
||||||
|
{
|
||||||
|
reader.visitImages([&](const Image& image) {
|
||||||
|
visitor(image);
|
||||||
|
});
|
||||||
|
}
|
||||||
} // namespace lms::metadata
|
} // 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();
|
_containerInfo = audioFile->getContainerInfo();
|
||||||
_metaDataMap = audioFile->getMetaData();
|
_metaDataMap = audioFile->getMetaData();
|
||||||
_hasEmbeddedCover = audioFile->hasAttachedPictures();
|
|
||||||
|
|
||||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
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 visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||||
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
|
|
||||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||||
|
|
||||||
AudioProperties _audioProperties;
|
AudioProperties _audioProperties;
|
||||||
av::IAudioFile::MetadataMap _metaDataMap;
|
av::IAudioFile::MetadataMap _metaDataMap;
|
||||||
av::ContainerInfo _containerInfo;
|
av::ContainerInfo _containerInfo;
|
||||||
bool _hasEmbeddedCover{};
|
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // 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)>;
|
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
|
||||||
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
|
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
|
||||||
|
|
||||||
virtual bool hasEmbeddedCover() const = 0;
|
|
||||||
virtual const AudioProperties& getAudioProperties() const = 0;
|
virtual const AudioProperties& getAudioProperties() const = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // 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 "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
|
|
||||||
#if (TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0)
|
#include "TagLibDefs.hpp"
|
||||||
#define TAGLIB_HAS_MP4_ITEM_TYPE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
@@ -216,7 +214,7 @@ namespace lms::metadata
|
|||||||
|
|
||||||
TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle)
|
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
|
return TagLib::FileRef{ p.c_str(), true // read audio properties
|
||||||
,
|
,
|
||||||
@@ -269,10 +267,6 @@ namespace lms::metadata
|
|||||||
|
|
||||||
const auto& frameListMap{ id3v2Tags.frameListMap() };
|
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
|
// Get some extra tags that may not be known by taglib
|
||||||
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
|
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
|
||||||
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
|
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
|
||||||
@@ -329,9 +323,6 @@ namespace lms::metadata
|
|||||||
{
|
{
|
||||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||||
{
|
{
|
||||||
if (tag->attributeListMap().contains("WM/Picture"))
|
|
||||||
_hasEmbeddedCover = true;
|
|
||||||
|
|
||||||
for (const auto& [name, attributeList] : tag->attributeListMap())
|
for (const auto& [name, attributeList] : tag->attributeListMap())
|
||||||
{
|
{
|
||||||
if (attributeList.isEmpty())
|
if (attributeList.isEmpty())
|
||||||
@@ -370,14 +361,6 @@ namespace lms::metadata
|
|||||||
// MP4
|
// MP4
|
||||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
|
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
|
// Taglib does not expose rtng in properties
|
||||||
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||||
{
|
{
|
||||||
@@ -421,18 +404,8 @@ namespace lms::metadata
|
|||||||
// FLAC
|
// FLAC
|
||||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
||||||
{
|
{
|
||||||
if (!flacFile->pictureList().isEmpty())
|
if (flacFile->hasID3v2Tag()) // discouraged usage
|
||||||
_hasEmbeddedCover = true;
|
processID3v2Tags(*flacFile->ID3v2Tag());
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
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/fileref.h>
|
||||||
#include <taglib/tpropertymap.h>
|
#include <taglib/tpropertymap.h>
|
||||||
|
|
||||||
#include "metadata/IParser.hpp"
|
|
||||||
|
|
||||||
#include "ITagReader.hpp"
|
#include "ITagReader.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
@@ -36,24 +34,22 @@ namespace lms::metadata
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
||||||
|
~TagLibTagReader() = default;
|
||||||
private:
|
|
||||||
TagLibTagReader(const TagLibTagReader&) = delete;
|
TagLibTagReader(const TagLibTagReader&) = delete;
|
||||||
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
void computeAudioProperties();
|
void computeAudioProperties();
|
||||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||||
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
|
|
||||||
|
|
||||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||||
|
|
||||||
const TagLib::FileRef _file;
|
const TagLib::FileRef _file;
|
||||||
AudioProperties _audioProperties;
|
AudioProperties _audioProperties;
|
||||||
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||||
bool _hasEmbeddedCover{};
|
|
||||||
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
||||||
};
|
};
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::metadata::utils
|
namespace lms::metadata::utils
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-19
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
@@ -27,30 +28,18 @@
|
|||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
class IParser
|
class IAudioFileParser
|
||||||
{
|
{
|
||||||
public:
|
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 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
|
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& parameters);
|
||||||
{
|
|
||||||
TagLib,
|
|
||||||
AvFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class ParserReadStyle
|
|
||||||
{
|
|
||||||
Fast,
|
|
||||||
Average,
|
|
||||||
Accurate,
|
|
||||||
};
|
|
||||||
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle);
|
|
||||||
} // namespace lms::metadata
|
} // namespace lms::metadata
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -125,7 +126,6 @@ namespace lms::metadata
|
|||||||
core::PartialDateTime originalDate;
|
core::PartialDateTime originalDate;
|
||||||
std::optional<Advisory> advisory;
|
std::optional<Advisory> advisory;
|
||||||
core::PartialDateTime encodingTime;
|
core::PartialDateTime encodingTime;
|
||||||
bool hasCover{};
|
|
||||||
std::optional<core::UUID> acoustID;
|
std::optional<core::UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
@@ -142,4 +142,84 @@ namespace lms::metadata
|
|||||||
std::vector<Artist> producerArtists;
|
std::vector<Artist> producerArtists;
|
||||||
std::vector<Artist> remixerArtists;
|
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
|
} // namespace lms::metadata
|
||||||
@@ -23,14 +23,25 @@
|
|||||||
|
|
||||||
#include <Wt/WTime.h>
|
#include <Wt/WTime.h>
|
||||||
|
|
||||||
#include "Parser.hpp"
|
#include "AudioFileParser.hpp"
|
||||||
#include "TestTagReader.hpp"
|
#include "TestTagReader.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
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{
|
TestTagReader testTags{
|
||||||
{
|
{
|
||||||
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
|
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
|
||||||
@@ -88,9 +99,7 @@ namespace lms::metadata
|
|||||||
{ "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } });
|
{ "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } });
|
||||||
testTags.setLyricsTags({ { "eng", "[00:00.00]First line\n[00:01.00]Second line" } });
|
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.parseMetaData(testTags) };
|
||||||
|
|
||||||
const std::unique_ptr<Track> track{ parser.parse(testTags) };
|
|
||||||
|
|
||||||
// Audio properties
|
// Audio properties
|
||||||
{
|
{
|
||||||
@@ -130,7 +139,6 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->date.getYear(), 2020);
|
EXPECT_EQ(track->date.getYear(), 2020);
|
||||||
EXPECT_EQ(track->date.getMonth(), 3);
|
EXPECT_EQ(track->date.getMonth(), 3);
|
||||||
EXPECT_EQ(track->date.getDay(), 4);
|
EXPECT_EQ(track->date.getDay(), 4);
|
||||||
EXPECT_FALSE(track->hasCover);
|
|
||||||
ASSERT_EQ(track->genres.size(), 2);
|
ASSERT_EQ(track->genres.size(), 2);
|
||||||
EXPECT_EQ(track->genres[0], "Genre1");
|
EXPECT_EQ(track->genres[0], "Genre1");
|
||||||
EXPECT_EQ(track->genres[1], "Genre2");
|
EXPECT_EQ(track->genres[1], "Genre2");
|
||||||
@@ -234,7 +242,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, trim)
|
TEST(AudioFileParser, trim)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
ASSERT_EQ(track->genres.size(), 3);
|
||||||
EXPECT_EQ(track->genres[0], "Genre1");
|
EXPECT_EQ(track->genres[0], "Genre1");
|
||||||
@@ -250,7 +258,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->genres[2], "Genre3");
|
EXPECT_EQ(track->genres[2], "Genre3");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, customDelimiters)
|
TEST(AudioFileParser, customDelimiters)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -263,10 +271,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; ", "/" });
|
params.defaultTagDelimiters = { " ; ", "/" };
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / ", " feat. " });
|
params.artistTagDelimiters = { " / ", " feat. " };
|
||||||
std::unique_ptr<Track> track{ parser.parse(testTags) };
|
TestAudioFileParser parser{ params };
|
||||||
|
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||||
|
|
||||||
ASSERT_EQ(track->artists.size(), 3);
|
ASSERT_EQ(track->artists.size(), 3);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
@@ -292,7 +301,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, customDelimiters_foundInArtist)
|
TEST(AudioFileParser, customDelimiters_foundInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -301,10 +310,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
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{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -321,10 +331,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
@@ -332,7 +343,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2");
|
EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, customDelimiters_notUsed)
|
TEST(AudioFileParser, customDelimiters_notUsed)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -341,10 +352,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
@@ -352,7 +364,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, customDelimiters_onlyInArtist)
|
TEST(AudioFileParser, customDelimiters_onlyInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -360,10 +372,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, customDelimitersUsedForArtists)
|
TEST(AudioFileParser, customDelimitersUsedForArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -379,10 +392,11 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, noArtistInArtist)
|
TEST(AudioFileParser, noArtistInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
ASSERT_EQ(track->artists.size(), 0);
|
||||||
EXPECT_EQ(track->artistDisplayName, "");
|
EXPECT_EQ(track->artistDisplayName, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, singleArtistInArtists)
|
TEST(AudioFileParser, singleArtistInArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
ASSERT_EQ(track->artists.size(), 1);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
EXPECT_EQ(track->artistDisplayName, "Artist1");
|
EXPECT_EQ(track->artistDisplayName, "Artist1");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, multipleArtistsInArtist)
|
TEST(AudioFileParser, multipleArtistsInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, multipleArtistsInArtists)
|
TEST(AudioFileParser, multipleArtistsInArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
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{
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
@@ -471,7 +485,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
|
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, singleArtistInAlbumArtists)
|
TEST(AudioFileParser, singleArtistInAlbumArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
||||||
ASSERT_TRUE(track->medium->release);
|
ASSERT_TRUE(track->medium->release);
|
||||||
@@ -490,7 +504,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
|
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, multipleArtistsInAlbumArtist)
|
TEST(AudioFileParser, multipleArtistsInAlbumArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
||||||
ASSERT_TRUE(track->medium->release);
|
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
|
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{
|
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);
|
||||||
ASSERT_TRUE(track->medium->release);
|
ASSERT_TRUE(track->medium->release);
|
||||||
@@ -530,7 +544,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2");
|
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, multipleArtistsInAlbumArtists)
|
TEST(AudioFileParser, multipleArtistsInAlbumArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
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);
|
||||||
ASSERT_TRUE(track->medium->release);
|
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
|
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{
|
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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
@@ -570,7 +584,7 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
|
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -579,9 +593,10 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParserParameters params;
|
||||||
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / " });
|
params.artistTagDelimiters = { " / " };
|
||||||
std::unique_ptr<Track> track{ parser.parse(testTags) };
|
TestAudioFileParser parser{ params };
|
||||||
|
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||||
|
|
||||||
ASSERT_EQ(track->artists.size(), 2);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, MBIDs_fallback)
|
TEST(AudioFileParser, MBIDs_fallback)
|
||||||
{
|
{
|
||||||
TestTagReader testTags{
|
TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -611,7 +626,7 @@ namespace lms::metadata
|
|||||||
|
|
||||||
testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } },
|
testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } },
|
||||||
{ "RoleB", { "Artist2", "Artist4" } } });
|
{ "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);
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
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"));
|
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{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -708,7 +723,7 @@ namespace lms::metadata
|
|||||||
{ TagType::Composer, { "Artist1" } },
|
{ 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);
|
ASSERT_EQ(track->composerArtists.size(), 1);
|
||||||
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
|
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"));
|
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{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
@@ -724,14 +739,14 @@ namespace lms::metadata
|
|||||||
// No AlbumSortOrder
|
// 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.has_value());
|
||||||
ASSERT_TRUE(track->medium->release.has_value());
|
ASSERT_TRUE(track->medium->release.has_value());
|
||||||
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
|
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, advisory)
|
TEST(AudioFileParser, advisory)
|
||||||
{
|
{
|
||||||
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
|
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
@@ -740,8 +755,8 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
AudioFileParser parser;
|
||||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||||
|
|
||||||
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
|
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
|
||||||
if (track->advisory.has_value())
|
if (track->advisory.has_value())
|
||||||
@@ -758,7 +773,7 @@ namespace lms::metadata
|
|||||||
doTest("3", std::nullopt);
|
doTest("3", std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, encodingTime)
|
TEST(AudioFileParser, encodingTime)
|
||||||
{
|
{
|
||||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
@@ -767,8 +782,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
|
||||||
|
|
||||||
ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'";
|
ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'";
|
||||||
};
|
};
|
||||||
@@ -780,7 +794,7 @@ namespace lms::metadata
|
|||||||
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Parser, date)
|
TEST(AudioFileParser, date)
|
||||||
{
|
{
|
||||||
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
@@ -789,8 +803,7 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser parser;
|
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||||
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
|
||||||
|
|
||||||
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
|
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@ include(GoogleTest)
|
|||||||
add_executable(test-metadata
|
add_executable(test-metadata
|
||||||
Lyrics.cpp
|
Lyrics.cpp
|
||||||
Metadata.cpp
|
Metadata.cpp
|
||||||
Parser.cpp
|
AudioFileParser.cpp
|
||||||
PlayList.cpp
|
PlayList.cpp
|
||||||
Utils.cpp
|
Utils.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include "Parser.hpp"
|
#include "ITagReader.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
@@ -95,8 +95,6 @@ namespace lms::metadata
|
|||||||
visitor(language, lyrics);
|
visitor(language, lyrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasEmbeddedCover() const override { return false; };
|
|
||||||
|
|
||||||
const AudioProperties& getAudioProperties() const override { return audioProperties; }
|
const AudioProperties& getAudioProperties() const override { return audioProperties; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ target_include_directories(lmsartwork PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsartwork PRIVATE
|
target_link_libraries(lmsartwork PRIVATE
|
||||||
lmsav
|
|
||||||
lmsimage
|
lmsimage
|
||||||
|
lmsmetadata
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsartwork PUBLIC
|
target_link_libraries(lmsartwork PUBLIC
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
|
||||||
#include "av/Types.hpp"
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
@@ -35,6 +33,7 @@
|
|||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::cover
|
||||||
{
|
{
|
||||||
@@ -55,6 +54,7 @@ namespace lms::cover
|
|||||||
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
||||||
const std::filesystem::path& defaultArtistImageSvgPath)
|
const std::filesystem::path& defaultArtistImageSvgPath)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
|
, _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) }
|
||||||
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||||
{
|
{
|
||||||
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||||
@@ -66,61 +66,7 @@ namespace lms::cover
|
|||||||
_defaultArtistImage = image::readImage(defaultArtistImageSvgPath); // may throw
|
_defaultArtistImage = image::readImage(defaultArtistImageSvgPath); // may throw
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, std::optional<image::ImageSize> width) const
|
ArtworkService::~ArtworkService() = default;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> ArtworkService::getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
|
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> 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
|
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)
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ namespace lms::db
|
|||||||
class Session;
|
class Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace lms::av
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
class IAudioFile;
|
class IAudioFileParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::cover
|
||||||
@@ -43,7 +43,7 @@ namespace lms::cover
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
ArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
~ArtworkService() override = default;
|
~ArtworkService() override;
|
||||||
ArtworkService(const ArtworkService&) = delete;
|
ArtworkService(const ArtworkService&) = delete;
|
||||||
ArtworkService& operator=(const ArtworkService&) = delete;
|
ArtworkService& operator=(const ArtworkService&) = delete;
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ namespace lms::cover
|
|||||||
void flushCache() override;
|
void flushCache() override;
|
||||||
void setJpegQuality(unsigned quality) 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> 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;
|
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;
|
db::Db& _db;
|
||||||
|
|
||||||
|
std::unique_ptr<metadata::IAudioFileParser> _audioFileParser;
|
||||||
ImageCache _cache;
|
ImageCache _cache;
|
||||||
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
||||||
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
||||||
|
|||||||
@@ -37,13 +37,16 @@
|
|||||||
#include "database/TrackFeatures.hpp"
|
#include "database/TrackFeatures.hpp"
|
||||||
#include "database/TrackLyrics.hpp"
|
#include "database/TrackLyrics.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
#include "image/Image.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
|
||||||
#include "IFileScanOperation.hpp"
|
#include "IFileScanOperation.hpp"
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -345,7 +348,7 @@ namespace lms::scanner
|
|||||||
return db::Advisory::UnSet;
|
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;
|
db::Track::FindParameters params;
|
||||||
// Add as many fields as possible to limit errors
|
// Add as many fields as possible to limit errors
|
||||||
@@ -359,7 +362,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
if (parsedTrack.position)
|
if (parsedTrack.position)
|
||||||
params.setTrackNumber(*parsedTrack.position);
|
params.setTrackNumber(*parsedTrack.position);
|
||||||
params.setHasEmbeddedImage(parsedTrack.hasCover);
|
params.setHasEmbeddedImage(hasEmbeddedCover);
|
||||||
params.setFileSize(fileInfo.fileSize);
|
params.setFileSize(fileInfo.fileSize);
|
||||||
|
|
||||||
bool error{};
|
bool error{};
|
||||||
@@ -387,7 +390,7 @@ namespace lms::scanner
|
|||||||
class AudioFileScanOperation : public IFileScanOperation
|
class AudioFileScanOperation : public IFileScanOperation
|
||||||
{
|
{
|
||||||
public:
|
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 }
|
: _file{ fileToScan.file }
|
||||||
, _mediaLibrary{ fileToScan.mediaLibrary }
|
, _mediaLibrary{ fileToScan.mediaLibrary }
|
||||||
, _db{ db }
|
, _db{ db }
|
||||||
@@ -408,9 +411,10 @@ namespace lms::scanner
|
|||||||
const std::filesystem::path _file;
|
const std::filesystem::path _file;
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
metadata::IParser& _parser;
|
metadata::IAudioFileParser& _parser;
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||||
|
bool _hasEmbeddedCover{};
|
||||||
};
|
};
|
||||||
|
|
||||||
void AudioFileScanOperation::scan()
|
void AudioFileScanOperation::scan()
|
||||||
@@ -420,7 +424,19 @@ namespace lms::scanner
|
|||||||
|
|
||||||
try
|
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)
|
catch (const metadata::Exception& e)
|
||||||
{
|
{
|
||||||
@@ -505,7 +521,7 @@ namespace lms::scanner
|
|||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
// maybe the file just moved?
|
// maybe the file just moved?
|
||||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
|
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, _hasEmbeddedCover, *fileInfo);
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
|
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
|
||||||
@@ -646,7 +662,7 @@ namespace lms::scanner
|
|||||||
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||||
track.modify()->setHasCover(_parsedTrack->hasCover);
|
track.modify()->setHasCover(_hasEmbeddedCover);
|
||||||
track.modify()->setCopyright(_parsedTrack->copyright);
|
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||||
@@ -686,18 +702,26 @@ namespace lms::scanner
|
|||||||
|
|
||||||
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
throw core::LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata::AudioFileParserParameters createAudioFileParserParameters(const ScannerSettings& settings)
|
||||||
|
{
|
||||||
|
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
|
} // namespace
|
||||||
|
|
||||||
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
, _settings{ settings }
|
, _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;
|
AudioFileScanner::~AudioFileScanner() = default;
|
||||||
|
|||||||
@@ -19,9 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "IFileScanner.hpp"
|
#include "IFileScanner.hpp"
|
||||||
|
|
||||||
namespace lms
|
namespace lms
|
||||||
@@ -33,7 +30,7 @@ namespace lms
|
|||||||
|
|
||||||
namespace metadata
|
namespace metadata
|
||||||
{
|
{
|
||||||
class IParser;
|
class IAudioFileParser;
|
||||||
}
|
}
|
||||||
} // namespace lms
|
} // namespace lms
|
||||||
|
|
||||||
@@ -57,7 +54,6 @@ namespace lms::scanner
|
|||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
std::unique_ptr<metadata::IParser> _metadataParser;
|
std::unique_ptr<metadata::IAudioFileParser> _metadataParser;
|
||||||
const std::vector<std::string> _extraTagsToParse;
|
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
#include "core/StreamLogger.hpp"
|
#include "core/StreamLogger.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
@@ -159,15 +160,79 @@ namespace lms::metadata
|
|||||||
return os;
|
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;
|
using namespace metadata;
|
||||||
|
|
||||||
const auto start{ std::chrono::steady_clock::now() };
|
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() };
|
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"
|
std::cout << "Audio properties:\n"
|
||||||
<< track->audioProperties << std::endl;
|
<< track->audioProperties << std::endl;
|
||||||
@@ -248,8 +313,6 @@ namespace lms::metadata
|
|||||||
if (track->originalYear)
|
if (track->originalYear)
|
||||||
std::cout << "Original year: " << *track->originalYear << std::endl;
|
std::cout << "Original year: " << *track->originalYear << std::endl;
|
||||||
|
|
||||||
std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl;
|
|
||||||
|
|
||||||
if (track->replayGain)
|
if (track->replayGain)
|
||||||
std::cout << "Track replay gain: " << *track->replayGain << std::endl;
|
std::cout << "Track replay gain: " << *track->replayGain << std::endl;
|
||||||
|
|
||||||
@@ -280,6 +343,20 @@ namespace lms::metadata
|
|||||||
|
|
||||||
std::cout << std::endl;
|
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
|
} // namespace lms::metadata
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
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
|
try
|
||||||
{
|
{
|
||||||
std::cout << "Using Ffmpeg:" << std::endl;
|
auto parser{ metadata::createAudioFileParser(params) };
|
||||||
|
parseMetaData(*parser, file);
|
||||||
auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) };
|
parseImages(*parser, file);
|
||||||
parser->setArtistTagDelimiters(artistTagDelimiters);
|
|
||||||
parser->setDefaultTagDelimiters(tagDelimiters);
|
|
||||||
|
|
||||||
parse(*parser, file);
|
|
||||||
}
|
}
|
||||||
catch (metadata::Exception& e)
|
catch (metadata::Exception& e)
|
||||||
{
|
{
|
||||||
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
}
|
} };
|
||||||
|
|
||||||
|
if (parsers.contains(Parser::Ffmpeg))
|
||||||
|
parseAudioFile(metadata::ParserBackend::AvFormat);
|
||||||
|
|
||||||
if (parsers.contains(Parser::Taglib))
|
if (parsers.contains(Parser::Taglib))
|
||||||
{
|
parseAudioFile(metadata::ParserBackend::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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
|
|||||||
Reference in New Issue
Block a user