Added a way to set custom tag delimiters for artists and for other fields, fixes #417

This commit is contained in:
emeric
2024-03-01 23:58:49 +01:00
parent 194b9981c1
commit a6d11f4b4b
45 changed files with 2261 additions and 1121 deletions
+3 -3
View File
@@ -4,9 +4,9 @@ if(BUILD_TESTING)
endif()
add_library(lmsmetadata SHARED
impl/AvFormatParser.cpp
impl/Factory.cpp
impl/TagLibParser.cpp
impl/AvFormatTagReader.cpp
impl/Parser.cpp
impl/TagLibTagReader.cpp
impl/Utils.cpp
)
-250
View File
@@ -1,250 +0,0 @@
/*
* Copyright (C) 2013 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 "AvFormatParser.hpp"
#include <algorithm>
#include <iostream>
#include "av/IAudioFile.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "Utils.hpp"
namespace MetaData
{
namespace
{
template <typename T>
std::optional<T> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
{
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
if (it == std::cend(metadataMap))
return std::nullopt;
return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
}
template <>
std::optional<std::vector<UUID>> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
{
std::optional<std::string> str{ findFirstValueOfAs<std::string>(metadataMap, tags) };
if (!str)
return std::nullopt;
const std::vector<std::string_view> strUuids{ StringUtils::splitString(*str, "/") };
std::vector<UUID> res;
for (std::string_view strUuid : strUuids)
{
std::optional<UUID> uuid{ UUID::fromString(strUuid) };
if (!uuid)
return std::nullopt;
res.push_back(std::move(*uuid));
}
return res;
}
std::vector<Artist> getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::vector<Artist> res;
auto name{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"}) };
if (!name)
return res;
auto mbid{ findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
return { Artist {mbid, *name, std::nullopt} };
}
std::vector<Artist> getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::vector<Artist> artists;
std::vector<std::string_view> artistNames;
if (metadataMap.find("ARTISTS") != metadataMap.end())
{
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
}
else if (metadataMap.find("ARTIST") != metadataMap.end())
{
artistNames = { metadataMap.find("ARTIST")->second };
}
auto artistMBIDs{ findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
for (std::size_t i{}; i < artistNames.size(); ++i)
{
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
artists.emplace_back(Artist{ (*artistMBIDs)[i], artistNames[i], std::nullopt });
else
artists.emplace_back(Artist{ std::nullopt, artistNames[i], std::nullopt });
}
return artists;
}
std::optional<Release> getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::optional<Release> res;
std::optional<std::string> releaseName{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
if (!releaseName)
return res;
res.emplace();
res->name = std::move(*releaseName);
res->mbid = findFirstValueOfAs<UUID>(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
res->artists = getReleaseArtists(metadataMap);
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
if (!res->mediumCount)
{
// mediumCount may be encoded as position/count
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
return res;
}
std::optional<Medium> getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::optional<Medium> res;
res.emplace();
res->type = findFirstValueOfAs<std::string>(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
res->name = findFirstValueOfAs<std::string>(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
if (!res->trackCount)
{
// totalTracks may be encoded as "position/count"
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
res->position = findFirstValueOfAs<std::size_t>(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
res->release = getRelease(metadataMap);
if (res->type.empty()
&& res->name.empty()
&& !res->trackCount
&& !res->position
&& !res->release
&& !res->replayGain)
{
res.reset();
}
return res;
}
}
std::optional<Track> AvFormatParser::parse(const std::filesystem::path& p, bool debug)
{
Track track;
try
{
const auto mediaFile{ Av::parseAudioFile(p) };
Av::ContainerInfo info{ mediaFile->getContainerInfo() };
track.duration = info.duration;
track.bitrate = info.bitrate;
track.hasCover = mediaFile->hasAttachedPictures();
MetaData::Tags tags;
const Av::IAudioFile::MetadataMap metadataMap{ mediaFile->getMetaData() };
track.artists = getArtists(metadataMap);
track.medium = getMedium(metadataMap);
for (const auto& [tag, value] : metadataMap)
{
if (debug)
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
if (tag == "TITLE")
track.title = value;
else if (tag == "TRACK")
{
// Expecting 'Number/Total'
track.position = StringUtils::readAs<std::size_t>(value);
}
else if (tag == "DATE"
|| tag == "YEAR"
|| tag == "WM/YEAR")
{
track.date = Utils::parseDate(value);
}
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|| tag == "TORY") // Original release year
{
track.originalDate = Utils::parseDate(value);
}
else if (tag == "ACOUSTID ID")
{
track.acoustID = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ_RELEASETRACKID")
{
track.mbid = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ/TRACK ID")
{
track.recordingMBID = UUID::fromString(value);
}
else if (std::find(std::cbegin(_userExtraTags), std::cend(_userExtraTags), tag) != std::cend(_userExtraTags))
{
const std::vector<std::string_view> tagValues{ StringUtils::splitString(value, "/,;") };
if (!tagValues.empty())
{
std::vector<std::string> values;
std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
track.userExtraTags[tag] = std::move(values);
}
}
}
}
catch (Av::Exception& e)
{
return std::nullopt;
}
return track;
}
} // namespace MetaData
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2013 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 "AvFormatTagReader.hpp"
#include <algorithm>
#include <iostream>
#include "av/IAudioFile.hpp"
#include "metadata/Exception.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "Utils.hpp"
namespace MetaData
{
namespace
{
// Mapping to internal taglib names and/or common alternative custom names
static const std::unordered_map<TagType, std::vector<std::string>> tagMapping
{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
{ TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } },
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
{ TagType::ArtistSortOrder, { "ARTISTSORT", "ARTIST-SORT", "WM/ARTISTSORTORDER" } },
{ TagType::Artists, { "ARTISTS", "WM/ARTISTS" } },
{ TagType::ASIN, { "ASIN" } },
{ TagType::Barcode, { "BARCODE", "WM/BARCODE" } },
{ TagType::BPM, { "BPM" } },
{ TagType::CatalogNumber, { "CATALOGNUMBER", "WM/CATALOGNO" } },
{ TagType::Comment, { "COMMENT" } },
{ TagType::Compilation, { "COMPILATION", "TCMP" } },
{ TagType::Composer, { "COMPOSER" } },
{ TagType::Composers, { "COMPOSERS" } },
{ TagType::ComposerSortOrder, { "COMPOSERSORT", "TSOC" } },
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
{ TagType::Conductor, { "CONDUCTOR" } },
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
{ TagType::Conductors, { "CONDUCTORS" } },
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
{ TagType::Copyright, { "COPYRIGHT" } },
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
{ TagType::Date, { "DATE", "YEAR", "WM/YEAR" } },
{ TagType::Director, { "DIRECTOR" } },
{ TagType::DiscNumber, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" } },
{ TagType::DiscSubtitle, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" } },
{ TagType::EncodedBy, { "ENCODEDBY" } },
{ TagType::Engineer, { "ENGINEER" } },
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
{ TagType::Genre, { "GENRE" } },
{ TagType::Grouping, { "GROUPING", "WM/CONTENTGROUPDESCRIPTION" } },
{ TagType::InitialKey, { "INITIALKEY" } },
{ TagType::ISRC, { "ISRC", "WM/ISRC", "TSRC" } },
{ TagType::Language, { "LANGUAGE" } },
{ TagType::License, { "LICENSE" } },
{ TagType::Lyricist, { "LYRICIST" } },
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Lyrics, { "LYRICS" } },
{ TagType::Media, { "TMED", "MEDIA", "WM/MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
{ TagType::MixerSortOrder, { "MIXERSORT" } },
{ TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
{ TagType::OriginalReleaseDate, { "ORIGINALDATE", "TDOR", "WM/ORIGINALRELEASETIME" } },
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR", "TORY", "WM/ORIGINALRELEASEYEAR" } },
{ TagType::Podcast, { "PODCAST" } },
{ TagType::PodcastURL, { "PODCASTURL" } },
{ TagType::Producer, { "PRODUCER" } },
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
{ TagType::Producers, { "PRODUCERS" } },
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
{ TagType::RecordLabel, { "LABEL", "PUBLISHER" } },
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
{ TagType::ReleaseDate, { "RELEASEDATE" } },
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
{ TagType::Remixers, { "REMIXERS" } },
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
{ TagType::Script, { "SCRIPT", "WM/SCRIPT" } },
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
{ TagType::Subtitle, { "SUBTITLE" } },
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS"} },
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
{ TagType::TrackNumber, { "TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER" } },
{ TagType::TrackTitle, { "TITLE" } },
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
{ TagType::WorkTitle, { "WORK" } },
{ TagType::Writer, { "WRITER" } },
};
}
AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug)
{
try
{
const auto audioFile{ Av::parseAudioFile(p) };
_containerInfo = audioFile->getContainerInfo();
_metaDataMap = audioFile->getMetaData();
_hasEmbeddedCover = audioFile->hasAttachedPictures();
if (debug && Service<ILogger>::get()->isSeverityActive(Severity::DEBUG))
{
for (const auto& [key, value] : _metaDataMap)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
}
}
catch (Av::Exception& e)
{
throw ParseException{};
}
}
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ tagMapping.find(tag) };
if (itTagNames == std::cend(tagMapping))
return;
for (const std::string& tagName : itTagNames->second)
{
bool visited{};
visitTagValues(tagName, [&](std::string_view value)
{
visited = true;
visitor(value);
});
if (visited)
break;
}
}
void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
{
auto itValues{ _metaDataMap.find(std::string{ key }) };
if (itValues == std::cend(_metaDataMap))
return;
visitor(itValues->second);
}
void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value)
{
visitor("", value);
});
}
} // namespace 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 <filesystem>
#include "av/IAudioFile.hpp"
#include "metadata/IParser.hpp"
#include "ITagReader.hpp"
namespace MetaData
{
class AvFormatTagReader : public ITagReader
{
public:
AvFormatTagReader(const std::filesystem::path& path, bool debug);
private:
AvFormatTagReader(const AvFormatTagReader&) = delete;
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
bool hasMultiValuedTags() const override { return false; /* not supported */}
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
std::chrono::milliseconds getDuration() const override { return _containerInfo.duration; }
std::size_t getBitrate() const override { return _containerInfo.bitrate; }
std::size_t getBitsPerSample() const override { return 0; }
std::size_t getSampleRate() const override { return 0; }
Av::IAudioFile::MetadataMap _metaDataMap;
Av::ContainerInfo _containerInfo;
bool _hasEmbeddedCover{};
};
} // namespace MetaData
-47
View File
@@ -1,47 +0,0 @@
/*
* Copyright (C) 2022 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 "metadata/IParser.hpp"
#include "utils/Exception.hpp"
#include "utils/ILogger.hpp"
#include "AvFormatParser.hpp"
#include "TagLibParser.hpp"
#include "Utils.hpp"
namespace MetaData
{
std::unique_ptr<IParser>
createParser(ParserType parserType, ParserReadStyle parserReadStyle)
{
switch (parserType)
{
case ParserType::TagLib:
LMS_LOG(METADATA, INFO, "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle));
return std::make_unique<TagLibParser>(parserReadStyle);
case ParserType::AvFormat:
LMS_LOG(METADATA, INFO, "Creating AvFormat parser");
return std::make_unique<AvFormatParser>();
}
throw LmsException {"Unhandled parser type"};
}
}
+164
View File
@@ -0,0 +1,164 @@
/*
* 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 <chrono>
#include <functional>
namespace MetaData
{
// using picard internal names
// see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
enum class TagType
{
AcoustID,
AcoustIDFingerprint,
Album,
AlbumArtist,
AlbumArtists, // non standard
AlbumArtistSortOrder,
AlbumArtistsSortOrder, // non standard
AlbumSortOrder,
Arranger,
Artist,
ArtistSortOrder,
Artists,
ASIN,
Barcode,
BPM,
CatalogNumber,
Comment,
Compilation,
Composer,
ComposerSortOrder,
Composers, // non standard
ComposersSortOrder, // non standard
Conductor,
ConductorSortOrder, // non standard
Conductors, // non standard
ConductorsSortOrder, // non standard
Copyright,
CopyrightURL, // non standard
Date,
Director,
DiscNumber,
DiscSubtitle,
EncodedBy,
EncoderSettings,
Engineer,
GaplessPlayback,
Genre,
Grouping,
InitialKey,
ISRC,
Language,
License,
Lyricist,
LyricistSortOrder, // non standard
Lyricists, // non standard
LyricistsSortOrder, // non standard
Lyrics,
Media,
MixDJ,
Mixer,
MixerSortOrder, // non standard
Mixers, // non standard
MixersSortOrder, // non standard
Mood,
Movement,
MovementCount,
MovementNumber,
MusicBrainzArtistID,
MusicBrainzDiscID,
MusicBrainzOriginalArtistID,
MusicBrainzOriginalReleaseID,
MusicBrainzRecordingID,
MusicBrainzReleaseArtistID,
MusicBrainzReleaseGroupID,
MusicBrainzReleaseID,
MusicBrainzTrackID,
MusicBrainzWorkID,
MusicIPFingerprint,
MusicIPPUID,
OriginalAlbum,
OriginalArtist,
OriginalFilename,
OriginalReleaseDate,
OriginalReleaseYear,
Podcast,
PodcastURL,
Producer,
ProducerSortOrder, // non standard
Producers, // non standard
ProducersSortOrder, // non standard
Rating,
RecordLabel,
ReleaseCountry,
ReleaseDate,
ReleaseStatus,
ReleaseType,
Remixer,
RemixerSortOrder,
Remixers,
RemixersSortOrder,
ReplayGainAlbumGain,
ReplayGainAlbumPeak,
ReplayGainAlbumRange,
ReplayGainReferenceLoudness,
ReplayGainTrackGain,
ReplayGainTrackPeak,
ReplayGainTrackRange,
Script,
ShowName,
ShowNameSortOrder,
ShowWorkAndMovement,
Subtitle,
TotalDiscs,
TotalTracks,
TrackNumber,
TrackTitle,
TrackTitleSortOrder,
Website,
WorkTitle,
Writer,
};
class ITagReader
{
public:
virtual ~ITagReader() = default;
virtual bool hasMultiValuedTags() const = 0;
using TagValueVisitor = std::function<void(std::string_view value)>;
virtual void visitTagValues(TagType tag, TagValueVisitor visitor) const = 0;
virtual void visitTagValues(std::string_view tag, TagValueVisitor visitor) const = 0;
using PerformerVisitor = std::function<void(std::string_view role, std::string_view artist)>;
virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
virtual bool hasEmbeddedCover() const = 0;
virtual std::chrono::milliseconds getDuration() const = 0;
virtual std::size_t getBitrate() const = 0;
virtual std::size_t getBitsPerSample() const = 0;
virtual std::size_t getSampleRate() const = 0;
};
} // namespace MetaData
+396
View File
@@ -0,0 +1,396 @@
/*
* Copyright (C) 2016 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 "Parser.hpp"
#include <span>
#include "metadata/Exception.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "AvFormatTagReader.hpp"
#include "TagLibTagReader.hpp"
#include "Utils.hpp"
namespace MetaData
{
namespace
{
void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span<const std::string> tagDelimiters, ITagReader::TagValueVisitor visitor)
{
tagReader.visitTagValues(tagType, [&](std::string_view value)
{
auto visitTagIfNonEmpty{ [&](std::string_view tag)
{
tag = StringUtils::stringTrim(tag);
if (!tag.empty())
visitor(tag);
} };
if (!tagReader.hasMultiValuedTags())
{
for (std::string_view tagDelimiter : tagDelimiters)
{
if (value.find(tagDelimiter) != std::string_view::npos)
{
for (std::string_view splitTag : StringUtils::splitString(value, tagDelimiter))
visitTagIfNonEmpty(splitTag);
return;
}
}
}
visitTagIfNonEmpty(value);
});
}
template<typename T>
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters)
{
std::vector<T> res;
for (const TagType tagType : tagTypes)
{
auto addTagIfNonEmpty{ [&res](std::string_view tag)
{
tag = StringUtils::stringTrim(tag);
if (!tag.empty())
{
std::optional<T> val{ StringUtils::readAs<T>(tag) };
if (val)
res.emplace_back(std::move(*val));
}
} };
tagReader.visitTagValues(tagType, [&](std::string_view value)
{
if (!tagReader.hasMultiValuedTags())
{
for (std::string_view tagDelimiter : tagDelimiters)
{
if (value.find(tagDelimiter) != std::string_view::npos)
{
for (std::string_view splitTag : StringUtils::splitString(value, tagDelimiter))
addTagIfNonEmpty(splitTag);
return;
}
}
}
// no delimiter found, or no delimiter to be used
addTagIfNonEmpty(value);
});
if (!res.empty())
break;
}
return res;
}
template <typename T>
std::optional<T> getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes)
{
std::optional<T> res;
std::vector<T> values{ getTagValuesFirstMatchAs<T>(tagReader, tagTypes, {} /* don't expect multiple values here */) };
if (!values.empty())
res = std::move(values.front());
return res;
}
template <typename T>
std::vector<T> getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span<const std::string> tagDelimiters)
{
return getTagValuesFirstMatchAs<T>(tagReader, { tagType }, tagDelimiters);
}
template <typename T>
std::optional<T> getTagValueAs(const ITagReader& tagReader, TagType tagType)
{
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
}
std::vector<Artist> getArtists(const ITagReader& tagReader,
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames,
std::span<const std::string> artistTagDelimiters
)
{
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, artistTagDelimiters) };
if (artistNames.empty())
return {};
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, artistTagDelimiters) };
std::vector<UUID> artistMBIDs{ getTagValuesFirstMatchAs<UUID>(tagReader, artistMBIDTagNames, artistTagDelimiters) };
std::vector<Artist> artists;
artists.reserve(artistNames.size());
for (std::size_t i{}; i < artistNames.size(); ++i)
{
Artist& artist{ artists.emplace_back(std::move(artistNames[i])) };
if (artistNames.size() == artistSortNames.size())
artist.sortName = std::move(artistSortNames[i]);
if (artistNames.size() == artistMBIDs.size())
artist.mbid = std::move(artistMBIDs[i]);
}
return artists;
}
PerformerContainer getPerformerArtists(const ITagReader& tagReader)
{
PerformerContainer performers;
tagReader.visitPerformerTags([&](std::string_view role, std::string_view name)
{
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
// We consider we may hit both styles for the same track
if (role.empty())
{
// "PERFORMER" "artist (role)"
Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(name) };
StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
}
else
{
// "PERFORMER:role", "artist" (MP3)
std::string roleCapitalized{ StringUtils::stringToLower(role) };
StringUtils::capitalize(roleCapitalized);
performers[roleCapitalized].push_back(Artist{ name });
}
});
return performers;
}
}
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
{
return std::make_unique<Parser>(parserBackend, parserReadStyle);
}
Parser::Parser(ParserBackend parserBackend, ParserReadStyle readStyle)
: _parserBackend{ parserBackend }
, _readStyle{ readStyle }
{
switch (_parserBackend)
{
case ParserBackend::TagLib:
LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << Utils::readStyleToString(readStyle));
break;
case ParserBackend::AvFormat:
LMS_LOG(METADATA, INFO, "Using AvFormat parser");
break;
}
}
std::unique_ptr<Track> Parser::parse(const std::filesystem::path& p, bool debug)
{
try
{
std::unique_ptr<ITagReader> tagReader;
switch (_parserBackend)
{
case ParserBackend::TagLib:
tagReader = std::make_unique<TagLibTagReader>(p, _readStyle, debug);
break;
case ParserBackend::AvFormat:
tagReader = std::make_unique<AvFormatTagReader>(p, debug);
break;
}
if (!tagReader)
throw ParseException{ "Unhandled parser backend" };
return parse(*tagReader);
}
catch (const Exception& e)
{
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
throw ParseException{};
}
}
std::unique_ptr<Track> Parser::parse(const ITagReader& tagReader)
{
auto track{ std::make_unique<Track>() };
processAudioProperties(tagReader, *track);
processTags(tagReader, *track);
return track;
}
void Parser::processAudioProperties(const ITagReader& tagReader, Track& track)
{
track.duration = tagReader.getDuration();
track.bitrate = tagReader.getBitrate();
}
void Parser::processTags(const ITagReader& tagReader, Track& track)
{
track.hasCover = tagReader.hasEmbeddedCover();
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<UUID>(tagReader, TagType::MusicBrainzRecordingID);
track.acoustID = getTagValueAs<UUID>(tagReader, TagType::AcoustID);
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
if (auto dateStr = getTagValueAs<std::string>(tagReader, TagType::Date))
{
if (const Wt::WDate date{ Utils::parseDate(*dateStr) }; date.isValid())
{
track.date = date;
track.year = date.year();
}
else
{
track.year = Utils::parseYear(*dateStr);
}
}
if (auto dateStr = getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseDate))
{
if (const Wt::WDate date{ Utils::parseDate(*dateStr) }; date.isValid())
{
track.originalDate = date;
track.originalYear = date.year();
}
else
{
track.originalYear = Utils::parseYear(*dateStr);
}
}
if (auto dateStr = getTagValueAs<std::string>(tagReader, TagType::OriginalReleaseYear))
{
track.originalYear = Utils::parseYear(*dateStr);
}
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
track.artistDisplayName = getTagValueAs<std::string>(tagReader, TagType::Artist).value_or(""); // TODO join on artists if present
for (const std::string& userExtraTag : _userExtraTags)
{
visitTagValues(tagReader, userExtraTag, _defaultTagDelimiters, [&](std::string_view value)
{
value = StringUtils::stringTrim(value);
if (!value.empty())
track.userExtraTags[userExtraTag].push_back(std::string{ value });
});
}
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _defaultTagDelimiters);
track.labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _defaultTagDelimiters);
std::vector<std::string_view> artistDelimiters{};
track.medium = getMedium(tagReader);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters);
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters);
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters);
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters);
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters);
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters);
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters);
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
// If a file has date but no year, set it
if (!track.year && track.date.isValid())
track.year = track.date.year();
// If a file has originalDate but no originalYear, set it
if (!track.originalYear && track.originalDate.isValid())
track.originalYear = track.originalDate.year();
}
std::optional<Medium> Parser::getMedium(const ITagReader& tagReader)
{
std::optional<Medium> medium;
medium.emplace();
medium->media = getTagValueAs<std::string>(tagReader, TagType::Media).value_or("");
medium->name = getTagValueAs<std::string>(tagReader, TagType::DiscSubtitle).value_or("");
medium->trackCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalTracks);
if (!medium->trackCount)
{
// totalTracks may be encoded as "position/count"
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::TrackNumber) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, '/') };
if (strings.size() == 2)
medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
// Expecting 'Number[/Total]'
medium->position = getTagValueAs<std::size_t>(tagReader, TagType::DiscNumber);
medium->release = getRelease(tagReader);
medium->replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainAlbumGain);
if (medium->isDefault())
medium.reset();
return medium;
}
std::optional<Release> Parser::getRelease(const ITagReader& tagReader)
{
std::optional<Release> release;
auto releaseName{ getTagValueAs<std::string>(tagReader, TagType::Album) };
if (!releaseName)
return release;
release.emplace();
release->name = std::move(*releaseName);
release->artistDisplayName = getTagValueAs<std::string>(tagReader, TagType::AlbumArtist).value_or(""); // TODO try to join albumartists if present
release->mbid = getTagValueAs<UUID>(tagReader, TagType::MusicBrainzReleaseID);
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters);
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
if (const auto value{ getTagValueAs<std::string>(tagReader, TagType::DiscNumber) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, '/') };
if (strings.size() == 2)
release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
release->releaseTypes = getTagValuesAs<std::string>(tagReader, TagType::ReleaseType, _defaultTagDelimiters);
return release;
}
} // namespace MetaData
+54
View File
@@ -0,0 +1,54 @@
/*
* 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 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:
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 processAudioProperties(const ITagReader& reader, Track& track);
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 MetaData
-521
View File
@@ -1,521 +0,0 @@
/*
* Copyright (C) 2016 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 "TagLibParser.hpp"
#include <map>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/id3v2tag.h>
#include <taglib/fileref.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavpackfile.h>
#include "utils/IConfig.hpp"
#include "utils/Exception.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "Utils.hpp"
namespace MetaData
{
namespace
{
// TODO use string_views here for values
using TagMap = std::map<std::string, std::vector<std::string>>;
template<typename T>
std::vector<T> getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
{
std::vector<T> res;
for (std::string_view key : keys)
{
const auto itValues{ tags.find(std::string {key}) };
if (itValues == std::cend(tags))
continue;
const std::vector<std::string>& values{ itValues->second };
if (values.empty())
continue;
res.reserve(values.size());
for (const auto& value : values)
{
std::optional<T> val{ StringUtils::readAs<T>(value) };
if (!val)
continue;
res.emplace_back(std::move(*val));
}
break;
}
return res;
}
template <typename T>
std::optional<T> getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
{
std::optional<T> res;
std::vector<T> values{ getPropertyValuesFirstMatchAs<T>(tags, keys) };
if (!values.empty())
res = std::move(values.front());
return res;
}
template <typename T>
std::vector<T> getPropertyValuesAs(const TagMap& tags, std::string_view key)
{
return getPropertyValuesFirstMatchAs<T>(tags, { key });
}
template <typename T>
std::optional<T> getPropertyValueAs(const TagMap& tags, std::string_view key)
{
return getPropertyValueFirstMatchAs<T>(tags, { key });
}
std::vector<std::string_view> splitAndTrimString(std::string_view str, std::string_view delimiters)
{
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
for (std::string_view& s : strings)
s = StringUtils::stringTrim(s);
return strings;
}
std::vector<Artist> getArtists(const TagMap& tags,
std::initializer_list<std::string_view> artistTagNames,
std::initializer_list<std::string_view> artistSortTagNames,
std::initializer_list<std::string_view> artistMBIDTagNames
)
{
const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) };
if (artistNames.empty())
return {};
std::vector<Artist> artists;
artists.reserve(artistNames.size());
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
[&](std::string_view name) { return Artist{ name }; });
{
const std::vector<std::string_view> artistSortNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistSortTagNames) };
if (artistSortNames.size() == artists.size())
{
for (std::size_t i{}; i < artistSortNames.size(); ++i)
artists[i].sortName = artistSortNames[i];
}
}
{
const std::vector<UUID> artistsMBID{ getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames) };
if (artistNames.size() == artistsMBID.size())
{
for (std::size_t i{}; i < artistsMBID.size(); ++i)
artists[i].mbid = artistsMBID[i];
}
}
return artists;
}
PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list<std::string_view> artistTagNames)
{
PerformerContainer performers;
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
// We may hit both styles for the same track
// PERFORMER: artist (role)
if (const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) }; !artistNames.empty())
{
for (std::string_view entry : artistNames)
{
Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) };
StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
}
}
// PERFORMER:role (MP3)
for (const auto& [key, values] : tags)
{
if (key.find("PERFORMER:") == 0)
{
std::string performerStr{ key };
std::string role;
if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos)
{
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
StringUtils::capitalize(role);
}
for (const auto& value : values)
performers[role].push_back(Artist{ value });
}
}
return performers;
}
std::optional<Release> getRelease(const TagMap& tags)
{
std::optional<Release> release;
auto releaseName{ getPropertyValueAs<std::string>(tags, "ALBUM") };
if (!releaseName)
return release;
release.emplace();
release->name = std::move(*releaseName);
release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or("");
release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" });
release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" });
release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
if (const auto value{ getPropertyValueAs<std::string_view>(tags, "DISCNUMBER") })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
release->releaseTypes = getPropertyValuesFirstMatchAs<std::string>(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
return release;
}
std::optional<Medium> getMedium(const TagMap& tags)
{
std::optional<Medium> medium;
medium.emplace();
medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or("");
medium->name = getPropertyValueFirstMatchAs<std::string>(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL");
if (!medium->trackCount)
{
// totalTracks may be encoded as "position/count"
if (const auto value{ getPropertyValueAs<std::string_view>(tags, "TRACKNUMBER") })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
// Expecting 'Number[/Total]'
medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER");
medium->release = getRelease(tags);
medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
if (medium->type.empty()
&& medium->name.empty()
&& !medium->trackCount
&& !medium->position
&& !medium->release
&& !medium->replayGain)
{
medium.reset();
}
return medium;
}
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
{
switch (readStyle)
{
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
}
throw LmsException{ "Cannot convert read style" };
}
TagMap constructTagMap(const TagLib::PropertyMap& properties)
{
TagMap tagMap;
for (const auto& [propertyName, propertyValues] : properties)
{
std::vector<std::string>& values{ tagMap[propertyName.upper().to8Bit(true)] };
for (const TagLib::String& propertyValue : propertyValues)
{
std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) };
if (!trimedValue.empty())
values.emplace_back(std::move(trimedValue));
}
}
return tagMap;
}
void mergeTagMaps(TagMap& dst, TagMap&& src)
{
for (auto&& [tag, values] : src)
{
if (dst.find(tag) == std::cend(dst))
dst[tag] = std::move(values);
}
}
}
TagLibParser::TagLibParser(ParserReadStyle readStyle)
: _readStyle{ readStyleToTagLibReadStyle(readStyle) }
{
}
void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
{
if (debug)
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
if (tag.empty() || values.empty())
return;
std::string_view value{ values.front() };
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ/RELEASE TRACK ID")
{
track.mbid = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID")
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
track.position = StringUtils::readAs<std::size_t>(value);
}
else if (tag == "DATE")
{
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
track.date = date;
else if (!track.year)
track.year = Utils::parseYear(value);
}
else if (tag == "YEAR")
track.year = Utils::parseYear(value);
else if (tag == "ORIGINALDATE")
{
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
track.originalDate = date;
else if (!track.originalYear)
track.originalYear = Utils::parseYear(value);
}
else if (tag == "ORIGINALYEAR")
track.originalYear = Utils::parseYear(value);
else if (tag == "METADATA_BLOCK_PICTURE")
track.hasCover = true;
else if (tag == "COPYRIGHT")
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.replayGain = StringUtils::readAs<float>(value);
else if (tag == "ARTIST")
track.artistDisplayName = value;
else if (std::find(std::cbegin(_userExtraTags), std::cend(_userExtraTags), tag) != std::cend(_userExtraTags))
{
std::vector<std::string> tagValues;
for (std::string_view valueList : values)
{
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
for (std::string_view value : splittedValues)
tagValues.push_back(std::string{ value });
}
if (!tagValues.empty())
track.userExtraTags[tag] = std::move(tagValues);
}
}
std::optional<Track> TagLibParser::parse(const std::filesystem::path& p, bool debug)
{
TagLib::FileRef f{ p.string().c_str(),
true, // read audio properties
_readStyle };
if (f.isNull())
{
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
return std::nullopt;
}
Track track;
if (const TagLib::AudioProperties* properties{ f.audioProperties() })
{
track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
track.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
}
else
{
LMS_LOG(METADATA, INFO, "File '" << p.string() << "': no audio properties");
return std::nullopt;
}
TagMap tags{ constructTagMap(f.file()->properties()) };
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{
if (!apeTag)
return;
mergeTagMaps(tags, constructTagMap(apeTag->properties()));
};
// Not that good embedded pictures handling
// WMA
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(f.file()) })
{
const TagLib::ASF::Tag* tag{ asfFile->tag() };
if (tag)
{
if (tag->attributeListMap().contains("WM/Picture"))
track.hasCover = true;
for (const auto& [name, attributeList] : tag->attributeListMap())
{
std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) };
if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
continue;
std::vector<std::string> attributes;
for (const auto& attribute : attributeList)
{
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
attributes.emplace_back(attribute.toString().to8Bit(true));
}
if (!attributes.empty())
{
if (debug)
std::cout << "ASF property: '" << name << "'" << std::endl;
tags.emplace(strName, std::move(attributes));
}
}
}
}
// MP3
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(f.file()) })
{
if (mp3File->ID3v2Tag())
{
const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() };
if (!frameListMap["APIC"].isEmpty())
track.hasCover = true;
if (!frameListMap["TSST"].isEmpty())
tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) };
}
getAPETags(mp3File->APETag());
}
//MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(f.file()) })
{
TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") };
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
if (!coverArtList.isEmpty())
track.hasCover = true;
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(f.file()) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(f.file()) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(f.file()) })
{
if (!flacFile->pictureList().isEmpty())
track.hasCover = true;
}
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(f.file()) })
{
if (!vorbisFile->tag()->pictureList().isEmpty())
track.hasCover = true;
}
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(f.file()) })
{
if (!opusFile->tag()->pictureList().isEmpty())
track.hasCover = true;
}
track.medium = getMedium(tags);
track.artists = getArtists(tags, { "ARTISTS", "ARTIST" }, { "ARTISTSORT" }, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" });
track.conductorArtists = getArtists(tags, { "CONDUCTORS", "CONDUCTOR" }, { "CONDUCTORSSORT", "CONDUCTORSORT" }, {});
track.composerArtists = getArtists(tags, { "COMPOSERS", "COMPOSER" }, { "COMPOSERSSORT", "COMPOSERSORT" }, {});
track.lyricistArtists = getArtists(tags, { "LYRICISTS", "LYRICIST" }, { "LYRICISTSSORT", "LYRICISTSORT" }, {});
track.mixerArtists = getArtists(tags, { "MIXERS", "MIXER" }, { "MIXERSSORT", "MIXERSORT" }, {});
track.producerArtists = getArtists(tags, { "PRODUCERS", "PRODUCER" }, { "PRODUCERSSORT", "PRODUCERSORT" }, {});
track.remixerArtists = getArtists(tags, { "REMIXERS", "REMIXER", "ModifiedBy" }, { "REMIXERSSORT", "REMIXERSORT" }, {});
track.performerArtists = getPerformerArtists(tags, { "PERFORMERS", "PERFORMER" });
for (const auto& [tag, values] : tags)
processTag(track, tag, values, debug);
// If a file has date but no year, set it
if (!track.year && track.date.isValid())
track.year = track.date.year();
// If a file has originalDate but no originalYear, set it
if (!track.originalYear && track.originalDate.isValid())
track.originalYear = track.originalDate.year();
return track;
}
} // namespace MetaData
-47
View File
@@ -1,47 +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 <taglib/audioproperties.h>
#include "metadata/IParser.hpp"
namespace TagLib
{
class StringList;
}
namespace MetaData
{
// Parse that makes use of AvFormat
class TagLibParser : public IParser
{
public:
TagLibParser(ParserReadStyle readStyle);
private:
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
void processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug);
const TagLib::AudioProperties::ReadStyle _readStyle;
};
} // namespace MetaData
+374
View File
@@ -0,0 +1,374 @@
/*
* Copyright (C) 2016 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 "TagLibTagReader.hpp"
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/id3v2tag.h>
#include <taglib/fileref.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavpackfile.h>
#include "metadata/Exception.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
namespace MetaData
{
namespace
{
class ParsingFailedException : public Exception {};
// Mapping to internal taglib names and/or common alternative custom names
static const std::unordered_map<TagType, std::vector<std::string>> tagMapping
{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
{ TagType::Album, { "ALBUM" } },
{ TagType::AlbumArtist, { "ALBUMARTIST" } },
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
{ TagType::ArtistSortOrder, { "ARTISTSORT" } },
{ TagType::Artists, { "ARTISTS" } },
{ TagType::ASIN, { "ASIN" } },
{ TagType::Barcode, { "BARCODE" } },
{ TagType::BPM, { "BPM" } },
{ TagType::CatalogNumber, { "CATALOGNUMBER" } },
{ TagType::Comment, { "COMMENT" } },
{ TagType::Compilation, { "COMPILATION" } },
{ TagType::Composer, { "COMPOSER" } },
{ TagType::Composers, { "COMPOSERS" } },
{ TagType::ComposerSortOrder, { "COMPOSERSORT" } },
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
{ TagType::Conductor, { "CONDUCTOR" } },
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
{ TagType::Conductors, { "CONDUCTORS" } },
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
{ TagType::Copyright, { "COPYRIGHT" } },
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
{ TagType::Date, { "DATE", "YEAR" } },
{ TagType::Director, { "DIRECTOR" } },
{ TagType::DiscNumber, { "DISCNUMBER", "DISC" } },
{ TagType::DiscSubtitle, { "DISCSUBTITLE", "SETSUBTITLE" } },
{ TagType::EncodedBy, { "ENCODEDBY" } },
{ TagType::Engineer, { "ENGINEER" } },
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
{ TagType::Genre, { "GENRE" } },
{ TagType::Grouping, { "GROUPING" } },
{ TagType::InitialKey, { "INITIALKEY" } },
{ TagType::ISRC, { "ISRC" } },
{ TagType::Language, { "LANGUAGE" } },
{ TagType::License, { "LICENSE" } },
{ TagType::Lyricist, { "LYRICIST" } },
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Lyrics, { "LYRICS" } },
{ TagType::Media, { "MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
{ TagType::MixerSortOrder, { "MIXERSORT" } },
{ TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
{ TagType::OriginalReleaseDate, { "ORIGINALDATE" } },
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR" } },
{ TagType::Podcast, { "PODCAST" } },
{ TagType::PodcastURL, { "PODCASTURL" } },
{ TagType::Producer, { "PRODUCER" } },
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
{ TagType::Producers, { "PRODUCERS" } },
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
{ TagType::RecordLabel, { "LABEL" } },
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
{ TagType::ReleaseDate, { "RELEASEDATE" } },
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
{ TagType::Remixers, { "REMIXERS" } },
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
{ TagType::Script, { "SCRIPT" } },
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
{ TagType::Subtitle, { "SUBTITLE" } },
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS"} },
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
{ TagType::TrackNumber, { "TRACKNUMBER" } },
{ TagType::TrackTitle, { "TITLE" } },
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
{ TagType::WorkTitle, { "WORK" } },
{ TagType::Writer, { "WRITER" } },
};
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
{
switch (readStyle)
{
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
}
throw LmsException{ "Cannot convert read style" };
}
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
{
for (auto&& [tag, values] : src)
{
if (dst.find(tag) == std::cend(dst))
dst[tag] = std::move(values);
}
}
}
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
: _file{ p.string().c_str()
, true // read audio properties
, readStyleToTagLibReadStyle(parserReadStyle) }
{
if (_file.isNull())
{
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
throw ParsingFailedException{};
}
if (!_file.audioProperties())
{
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': no audio properties");
throw ParsingFailedException{};
}
_propertyMap = _file.file()->properties();
// Some tags may not be known by TagLib
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{
if (!apeTag)
return;
mergeTagMaps(_propertyMap, apeTag->properties());
};
// Not that good embedded pictures handling
// + get some extra tags that may not be known by taglib
// WMA
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
{
if (tag->attributeListMap().contains("WM/Picture"))
_hasEmbeddedCover = true;
for (const auto& [name, attributeList] : tag->attributeListMap())
{
if (attributeList.isEmpty())
continue;
std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) };
if (strName.find("WM/") == 0 || _propertyMap.find(strName) != std::cend(_propertyMap))
continue;
TagLib::StringList attributes;
for (const TagLib::ASF::Attribute& attribute : attributeList)
{
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
attributes.append(attribute.toString());
}
if (!attributes.isEmpty())
_propertyMap[strName] = std::move(attributes);
}
}
}
// MP3
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
{
if (mp3File->ID3v2Tag())
{
const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() };
if (!frameListMap["APIC"].isEmpty())
_hasEmbeddedCover = true;
if (!frameListMap["TSST"].isEmpty())
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) };
}
getAPETags(mp3File->APETag());
}
//MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
{
TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") };
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
if (!coverArtList.isEmpty())
_hasEmbeddedCover = true;
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
{
if (!flacFile->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
{
if (!vorbisFile->tag()->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
{
if (!opusFile->tag()->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
if (debug && Service<ILogger>::get()->isSeverityActive(Severity::DEBUG))
{
for (const auto& [key, values] : _propertyMap)
{
for (const auto& value : values)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value.to8Bit(true) << "'");
}
}
_hasMultiValuedTags = std::any_of(std::cbegin(_propertyMap), std::cend(_propertyMap), [](const auto& entry) { return entry.second.size() > 1; });
}
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ tagMapping.find(tag) };
if (itTagNames == std::cend(tagMapping))
return;
for (const std::string& tagName : itTagNames->second)
{
bool visited{};
visitTagValues(tagName, [&](std::string_view value)
{
visited = true;
visitor(value);
});
if (visited)
break;
}
}
void TagLibTagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
{
TagLib::String key{ tag.data() /* assume null terminated */, TagLib::String::Type::UTF8 };
auto itValues{ _propertyMap.find(key) };
if (itValues == std::cend(_propertyMap))
return;
for (const TagLib::String& value : itValues->second)
visitor(value.to8Bit(true));
}
void TagLibTagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value)
{
visitor("", value);
});
for (const auto& [key, values] : _propertyMap)
{
if (key.startsWith("PERFORMER:")) // startsWith is not case sensitive
{
std::string performerStr{ key.to8Bit(true) };
const std::size_t rolePos{ performerStr.find(':') };
assert(rolePos != std::string::npos);
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
for (const TagLib::String& value : values)
{
const std::string name{ value.to8Bit(true) };
visitor(role, name);
}
}
}
}
std::chrono::milliseconds TagLibTagReader::getDuration() const
{
return std::chrono::milliseconds{ _file.audioProperties()->lengthInMilliseconds() };
}
std::size_t TagLibTagReader::getBitrate() const
{
return static_cast<std::size_t>(_file.audioProperties()->bitrate() * 1000);
}
std::size_t TagLibTagReader::getBitsPerSample() const
{
return 0; // TODO
}
std::size_t TagLibTagReader::getSampleRate() const
{
return static_cast<std::size_t>(_file.audioProperties()->sampleRate());
}
} // namespace MetaData
@@ -0,0 +1,57 @@
/*
* 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 <filesystem>
#include <taglib/fileref.h>
#include <taglib/tpropertymap.h>
#include "metadata/IParser.hpp"
#include "ITagReader.hpp"
namespace MetaData
{
class TagLibTagReader : public ITagReader
{
public:
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
private:
TagLibTagReader(const TagLibTagReader&) = delete;
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
bool hasMultiValuedTags() const override { return _hasMultiValuedTags; }
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
std::chrono::milliseconds getDuration() const override;
std::size_t getBitrate() const override;
std::size_t getBitsPerSample() const override;
std::size_t getSampleRate() const override;
TagLib::FileRef _file;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
bool _hasEmbeddedCover{};
bool _hasMultiValuedTags{};
};
} // namespace MetaData
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Emeric Poupon
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,17 +19,19 @@
#pragma once
#include "metadata/IParser.hpp"
#include "utils/Exception.hpp"
namespace MetaData
{
class Exception : public LmsException
{
public:
using LmsException::LmsException;
};
// Parse that makes use of AvFormat
class AvFormatParser : public IParser
{
public:
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
};
} // namespace MetaData
class ParseException : public Exception
{
public:
using Exception::Exception;
};
}
+22 -9
View File
@@ -23,6 +23,7 @@
#include <filesystem>
#include <map>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -44,6 +45,8 @@ namespace MetaData
Artist(std::string_view _name) : name{ _name } {}
Artist(std::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid{ std::move(_mbid) }, name{ _name }, sortName{ std::move(_sortName) } {}
bool operator<=>(const Artist&) const = default;
};
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
@@ -56,16 +59,26 @@ namespace MetaData
std::vector<Artist> artists;
std::optional<std::size_t> mediumCount;
std::vector<std::string> releaseTypes;
bool operator<=>(const Release&) const = default;
};
struct Medium
{
std::string type; // CD, etc.
std::string media; // CD, etc.
std::string name;
std::optional<Release> release;
std::optional<std::size_t> position; // in release
std::optional<std::size_t> trackCount;
std::optional<float> replayGain;
bool operator<=>(const Medium&) const = default;
bool isDefault() const
{
static Medium defaultMedium;
return *this == defaultMedium;
}
};
struct Track
@@ -75,9 +88,10 @@ namespace MetaData
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
std::vector<std::string> grouping;
std::vector<std::string> groupings;
std::vector<std::string> genres;
std::vector<std::string> moods;
std::vector<std::string> labels;
std::vector<std::string> languages;
Tags userExtraTags;
std::chrono::milliseconds duration{};
@@ -107,15 +121,14 @@ namespace MetaData
public:
virtual ~IParser() = default;
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
virtual std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
void setUserExtraTags(const std::vector<std::string>& extraTags) { _userExtraTags = std::vector(extraTags.cbegin(), extraTags.cend()); }
protected:
std::vector<std::string> _userExtraTags;
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 ParserType
enum class ParserBackend
{
TagLib,
AvFormat,
@@ -127,5 +140,5 @@ namespace MetaData
Average,
Accurate,
};
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle);
} // namespace MetaData
+1
View File
@@ -2,6 +2,7 @@ include(GoogleTest)
add_executable(test-metadata
Metadata.cpp
Parser.cpp
Utils.cpp
)
+265
View File
@@ -0,0 +1,265 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <map>
#include <vector>
#include <gtest/gtest.h>
#include "TestTagReader.hpp"
#include "Parser.hpp"
namespace MetaData
{
TEST(Parser, generalTest)
{
Parser parser;
TestTagReader testTags{
{
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
{ TagType::Album, { "MyAlbum" } },
{ TagType::Artist, { "MyArtist1 & MyArtist2" } },
{ TagType::Artists, { "MyArtist1", "MyArtist2" } },
{ TagType::ArtistSortOrder, { "MyArtist1SortName", "MyArtist2SortName" } },
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
{ TagType::Copyright, { "MyCopyright" } },
{ TagType::CopyrightURL, { "MyCopyrightURL" } },
{ TagType::Date, { "2020/03/04" } },
{ TagType::DiscNumber, { "2" } },
{ TagType::DiscSubtitle, { "MySubtitle" } },
{ TagType::Genre, { "Genre1", "Genre2" } },
{ TagType::Grouping, { "Grouping1", "Grouping2" } },
{ TagType::Media, { "CD" } },
{ TagType::Mixer, { "MyMixer1", "MyMixer2" } },
{ TagType::Mood, { "Mood1", "Mood2" } },
{ TagType::MusicBrainzArtistID, { "9d2e0c8c-8c5e-4372-a061-590955eaeaae", "5e2cf87f-c8d7-4504-8a86-954dc0840229" } },
{ TagType::MusicBrainzTrackID, { "0afb190a-6735-46df-a16d-199f48206e4a" } },
{ TagType::MusicBrainzReleaseArtistID, { "6fbf097c-1487-43e8-874b-50dd074398a7", "5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1" } },
{ TagType::MusicBrainzReleaseID, { "3fa39992-b786-4585-a70e-85d5cc15ef69" } },
{ TagType::MusicBrainzRecordingID, { "bd3fc666-89de-4ac8-93f6-2dbf028ad8d5" } },
{ TagType::Producer, { "MyProducer1", "MyProducer2" } },
{ TagType::Remixer, { "MyRemixer1", "MyRemixer2" } },
{ TagType::RecordLabel, { "Label1", "Label2" } },
{ TagType::Language, { "Language1", "Language2" } },
{ TagType::Lyricist, { "MyLyricist1", "MyLyricist2" } },
{ TagType::OriginalReleaseDate, { "2019/02/03" } },
{ TagType::ReleaseType, {"Album", "Compilation"} },
{ TagType::ReplayGainTrackGain, {"-0.33"} },
{ TagType::ReplayGainAlbumGain, {"-0.5"} },
{ TagType::TrackTitle, {"MyTitle"} },
{ TagType::TrackNumber, { "7" } },
{ TagType::TotalTracks, { "12" } },
{ TagType::TotalDiscs, { "3" } },
}
,
{
{ "RoleA", { "MyPerformer1ForRoleA", "MyPerformer2ForRoleA" } },
{ "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } }
},
{
{ "MY_AWESOME_TAG_A", { "MyTagValue1ForTagA", "MyTagValue2ForTagA" } },
{ "MY_AWESOME_TAG_B", { "MyTagValue1ForTagB", "MyTagValue2ForTagB" } }
}
};
static_cast<IParser&>(parser).setUserExtraTags(std::vector<std::string>{ "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" });
std::unique_ptr<Track> track{ parser.parse(testTags) };
EXPECT_EQ(track->acoustID, UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2");
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "MyArtist1");
EXPECT_EQ(track->artists[0].sortName, "MyArtist1SortName");
EXPECT_EQ(track->artists[0].mbid, UUID::fromString("9d2e0c8c-8c5e-4372-a061-590955eaeaae"));
EXPECT_EQ(track->artists[1].name, "MyArtist2");
EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName");
EXPECT_EQ(track->artists[1].mbid, UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
EXPECT_EQ(track->bitrate, TestTagReader::trackBitrate);
ASSERT_EQ(track->composerArtists.size(), 2);
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
EXPECT_EQ(track->composerArtists[1].name, "MyComposer2");
EXPECT_EQ(track->composerArtists[1].sortName, "MyComposerSortOrder2");
ASSERT_EQ(track->conductorArtists.size(), 2);
EXPECT_EQ(track->conductorArtists[0].name, "MyConductor1");
EXPECT_EQ(track->conductorArtists[1].name, "MyConductor2");
EXPECT_EQ(track->copyright, "MyCopyright");
EXPECT_EQ(track->copyrightURL, "MyCopyrightURL");
ASSERT_TRUE(track->date.isValid());
EXPECT_EQ(track->date.year(), 2020);
EXPECT_EQ(track->date.month(), 3);
EXPECT_EQ(track->date.day(), 4);
EXPECT_EQ(track->duration, TestTagReader::trackDuration);
EXPECT_FALSE(track->hasCover);
ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
ASSERT_EQ(track->groupings.size(), 2);
EXPECT_EQ(track->groupings[0], "Grouping1");
EXPECT_EQ(track->groupings[1], "Grouping2");
ASSERT_EQ(track->labels.size(), 2);
EXPECT_EQ(track->labels[0], "Label1");
EXPECT_EQ(track->labels[1], "Label2");
ASSERT_EQ(track->languages.size(), 2);
EXPECT_EQ(track->languages[0], "Language1");
EXPECT_EQ(track->languages[1], "Language2");
ASSERT_EQ(track->lyricistArtists.size(), 2);
EXPECT_EQ(track->lyricistArtists[0].name, "MyLyricist1");
EXPECT_EQ(track->lyricistArtists[1].name, "MyLyricist2");
ASSERT_TRUE(track->mbid.has_value());
EXPECT_EQ(track->mbid.value(), UUID::fromString("0afb190a-6735-46df-a16d-199f48206e4a"));
ASSERT_EQ(track->mixerArtists.size(), 2);
EXPECT_EQ(track->mixerArtists[0].name, "MyMixer1");
EXPECT_EQ(track->mixerArtists[1].name, "MyMixer2");
ASSERT_EQ(track->moods.size(), 2);
EXPECT_EQ(track->moods[0], "Mood1");
EXPECT_EQ(track->moods[1], "Mood2");
ASSERT_TRUE(track->originalDate.isValid());
EXPECT_EQ(track->originalDate.year(), 2019);
EXPECT_EQ(track->originalDate.month(), 2);
EXPECT_EQ(track->originalDate.day(), 3);
ASSERT_TRUE(track->originalYear.has_value());
EXPECT_EQ(track->originalYear.value(), 2019);
ASSERT_TRUE(track->performerArtists.contains("Rolea"));
ASSERT_EQ(track->performerArtists["Rolea"].size(), 2);
EXPECT_EQ(track->performerArtists["Rolea"][0].name, "MyPerformer1ForRoleA");
EXPECT_EQ(track->performerArtists["Rolea"][1].name, "MyPerformer2ForRoleA");
ASSERT_EQ(track->performerArtists["Roleb"].size(), 2);
EXPECT_EQ(track->performerArtists["Roleb"][0].name, "MyPerformer1ForRoleB");
EXPECT_EQ(track->performerArtists["Roleb"][1].name, "MyPerformer2ForRoleB");
ASSERT_TRUE(track->position.has_value());
EXPECT_EQ(track->position.value(), 7);
ASSERT_EQ(track->producerArtists.size(), 2);
EXPECT_EQ(track->producerArtists[0].name, "MyProducer1");
EXPECT_EQ(track->producerArtists[1].name, "MyProducer2");
ASSERT_TRUE(track->recordingMBID.has_value());
EXPECT_EQ(track->recordingMBID.value(), UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
ASSERT_TRUE(track->replayGain.has_value());
EXPECT_FLOAT_EQ(track->replayGain.value(), -0.33);
ASSERT_EQ(track->remixerArtists.size(), 2);
EXPECT_EQ(track->remixerArtists[0].name, "MyRemixer1");
EXPECT_EQ(track->remixerArtists[1].name, "MyRemixer2");
EXPECT_EQ(track->title, "MyTitle");
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"].size(), 2);
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][0], "MyTagValue1ForTagA");
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_A"][1], "MyTagValue2ForTagA");
ASSERT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"].size(), 2);
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][0], "MyTagValue1ForTagB");
EXPECT_EQ(track->userExtraTags["MY_AWESOME_TAG_B"][1], "MyTagValue2ForTagB");
ASSERT_TRUE(track->year.has_value());
EXPECT_EQ(track->year.value(), 2020);
// Medium
ASSERT_TRUE(track->medium.has_value());
EXPECT_EQ(track->medium->media, "CD");
EXPECT_EQ(track->medium->name, "MySubtitle");
ASSERT_TRUE(track->medium->position.has_value());
EXPECT_EQ(track->medium->position.value(), 2);
ASSERT_TRUE(track->medium->replayGain.has_value());
EXPECT_FLOAT_EQ(track->medium->replayGain.value(), -0.5);
ASSERT_TRUE(track->medium->trackCount.has_value());
EXPECT_EQ(track->medium->trackCount.value(), 12);
// Release
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->artistDisplayName, "MyAlbumArtist1 & MyAlbumArtist2");
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "MyAlbumArtist1");
EXPECT_EQ(track->medium->release->artists[0].sortName, "MyAlbumArtist1SortName");
EXPECT_EQ(track->medium->release->artists[0].mbid, UUID::fromString("6fbf097c-1487-43e8-874b-50dd074398a7"));
EXPECT_EQ(track->medium->release->artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(track->medium->release->artists[1].sortName, "MyAlbumArtist2SortName");
EXPECT_EQ(track->medium->release->artists[1].mbid, UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
ASSERT_TRUE(track->medium->release->mbid.has_value());
EXPECT_EQ(track->medium->release->mbid.value(), UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(track->medium->release->mediumCount, 3);
EXPECT_EQ(track->medium->release->name, "MyAlbum");
{
std::vector<std::string> expectedReleaseTypes{ "Album", "Compilation" };
EXPECT_EQ(track->medium->release->releaseTypes, expectedReleaseTypes);
}
}
TEST(Parser, trim)
{
const TestTagReader testTags{
{
{ TagType::Genre, { "Genre1 ", " Genre2", " Genre3 " } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_EQ(track->genres.size(), 3);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
EXPECT_EQ(track->genres[2], "Genre3");
}
TEST(Parser, customDelimiters)
{
const TestTagReader testTags{
{
{ TagType::Genre, { "Genre1; Genre2" } },
{ TagType::Language, { " Lang1 ; Lang2 ; " } },
{ TagType::Artist, { " This / is ; One Artist \\ Other Artist " } },
}
};
Parser parser;
static_cast<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; " });
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " \\ ", " / " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
ASSERT_EQ(track->genres.size(), 1);
EXPECT_EQ(track->genres[0], "Genre1; Genre2");
ASSERT_EQ(track->languages.size(), 2);
EXPECT_EQ(track->languages[0], "Lang1");
EXPECT_EQ(track->languages[1], "Lang2");
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "This / is ; One Artist");
EXPECT_EQ(track->artists[1].name, "Other Artist");
EXPECT_EQ(track->artistDisplayName, "This / is ; One Artist \\ Other Artist");
}
TEST(Parser, customDelimiters_notWithMultiValuedTags)
{
const TestTagReader testTags{
{
{ TagType::Genre, { "Genre1 ; Genre2" } },
{ TagType::Language, { "Lang1", "Lang2" } },
}
};
Parser parser;
static_cast<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
ASSERT_EQ(track->genres.size(), 1);
EXPECT_EQ(track->genres[0], "Genre1 ; Genre2");
ASSERT_EQ(track->languages.size(), 2);
EXPECT_EQ(track->languages[0], "Lang1");
EXPECT_EQ(track->languages[1], "Lang2");
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <map>
#include <vector>
#include <gtest/gtest.h>
#include "Parser.hpp"
namespace MetaData
{
namespace
{
template <typename TagMapType>
bool tagMapHasMultiValuedTags(const TagMapType& m)
{
return std::any_of(std::cbegin(m), std::cend(m), [](const auto& tagPair)
{
return tagPair.second.size() > 1;
});
}
}
class TestTagReader : public ITagReader
{
public:
static constexpr std::chrono::milliseconds trackDuration{ 180 };
static constexpr std::size_t trackBitrate{ 128000 };
static constexpr std::size_t trackBitsPerSample{ 16 };
static constexpr std::size_t trackSampleRate{ 44000 };
using Tags = std::unordered_map<TagType, std::vector<std::string_view>>;
using Performers = std::unordered_map<std::string_view, std::vector<std::string_view>>;
using ExtraUserTags = std::unordered_map<std::string_view, std::vector<std::string_view>>;
TestTagReader(Tags&& tags, Performers&& performers = {}, ExtraUserTags&& extraUserTags = {})
: _tags{ std::move(tags) }
, _performers{ std::move(performers) }
, _extraUserTags{ std::move(extraUserTags) }
{
_hasMultiValuedTags = tagMapHasMultiValuedTags(_tags)
|| tagMapHasMultiValuedTags(_performers)
|| tagMapHasMultiValuedTags(_extraUserTags);
}
bool hasMultiValuedTags() const override
{
return _hasMultiValuedTags;
}
void visitTagValues(TagType tag, TagValueVisitor visitor) const override
{
auto itValues{ _tags.find(tag) };
if (itValues != std::cend(_tags))
{
for (std::string_view value : itValues->second)
visitor(value);
}
}
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override
{
auto itValues{ _extraUserTags.find(tag) };
if (itValues == std::cend(_extraUserTags))
return;
for (std::string_view value : itValues->second)
visitor(value);
}
void visitPerformerTags(PerformerVisitor visitor) const override
{
for (const auto& [role, names] : _performers)
{
for (const auto& name : names)
visitor(role, name);
}
}
bool hasEmbeddedCover() const override { return false; };
std::chrono::milliseconds getDuration() const override { return trackDuration; }
std::size_t getBitrate() const override { return trackBitrate; }
std::size_t getBitsPerSample() const override { return trackBitsPerSample; }
std::size_t getSampleRate() const override { return trackSampleRate; }
private:
const Tags _tags;
const Performers _performers;
const ExtraUserTags _extraUserTags;
bool _hasMultiValuedTags;
};
}