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
+1
View File
@@ -84,6 +84,7 @@ namespace Av
public:
virtual ~IAudioFile() = default;
// Keys are forced to be in upper case
using MetadataMap = std::unordered_map<std::string, std::string>;
virtual const std::filesystem::path& getPath() const = 0;
+10 -2
View File
@@ -215,7 +215,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend");
}
@@ -359,7 +359,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred
))");
// Migrate data, with the new media_library_id field set to 1
// Migrate data, with the new media_library_id field set to 1
session.getDboSession().execute(R"(INSERT INTO track_backup
SELECT
id,
@@ -394,6 +394,13 @@ SELECT
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
}
void migrateFromV51(Session& session)
{
// Add custom artist tag delimiters, no need to rescan since it has no effect when empty
session.getDboSession().execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''");
session.getDboSession().execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''");
}
void doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -422,6 +429,7 @@ SELECT
{48, migrateFromV48},
{49, migrateFromV49},
{50, migrateFromV50},
{51, migrateFromV51},
};
{
+1 -1
View File
@@ -26,7 +26,7 @@ namespace Database
class Session;
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 51 };
static constexpr Version LMS_DATABASE_VERSION{ 52 };
class VersionInfo
{
public:
+32 -2
View File
@@ -46,7 +46,7 @@ namespace Database
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
{
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
const auto extensions{ StringUtils::splitString(_audioFileExtensions, ' ') };
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
@@ -57,7 +57,17 @@ namespace Database
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
{
return StringUtils::splitString(_extraTagsToScan, ";");
return StringUtils::splitString(_extraTagsToScan, ';');
}
std::vector<std::string> ScanSettings::getArtistTagDelimiters() const
{
return StringUtils::splitEscapedStrings(_artistTagDelimiters, ';', '\\');
}
std::vector<std::string> ScanSettings::getDefaultTagDelimiters() const
{
return StringUtils::splitEscapedStrings(_defaultTagDelimiters, ';', '\\');
}
void ScanSettings::setExtraTagsToScan(const std::vector<std::string_view>& extraTags)
@@ -69,6 +79,26 @@ namespace Database
_extraTagsToScan = std::move(newTagsToScan);
}
void ScanSettings::setArtistTagDelimiters(std::span<const std::string_view> delimiters)
{
std::string tagDelimiters{ StringUtils::escapeAndJoinStrings(delimiters, ';', '\\') };
if (tagDelimiters != _artistTagDelimiters)
{
_artistTagDelimiters.swap(tagDelimiters);
incScanVersion();
}
}
void ScanSettings::setDefaultTagDelimiters(std::span<const std::string_view> delimiters)
{
std::string tagDelimiters{ StringUtils::escapeAndJoinStrings(delimiters, ';', '\\') };
if (tagDelimiters != _defaultTagDelimiters)
{
_defaultTagDelimiters.swap(tagDelimiters);
incScanVersion();
}
}
void ScanSettings::incScanVersion()
{
_scanVersion += 1;
@@ -20,6 +20,7 @@
#pragma once
#include <filesystem>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -62,18 +63,22 @@ namespace Database
static pointer get(Session& session);
// Getters
std::size_t getScanVersion() const { return _scanVersion; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<std::string_view> getExtraTagsToScan() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
std::size_t getScanVersion() const { return _scanVersion; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<std::string_view> getExtraTagsToScan() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const;
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
std::vector<std::string> getArtistTagDelimiters() const;
std::vector<std::string> getDefaultTagDelimiters() const;
// Setters
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setExtraTagsToScan(const std::vector<std::string_view>& extraTags);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
void incScanVersion();
template<class Action>
@@ -85,6 +90,8 @@ namespace Database
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters");
}
private:
@@ -94,6 +101,7 @@ namespace Database
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
std::string _extraTagsToScan;
std::string _artistTagDelimiters;
std::string _defaultTagDelimiters;
};
} // namespace Database
+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;
};
}
@@ -19,7 +19,6 @@
#include "ScanStepScanFiles.hpp"
#include "metadata/IParser.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -29,15 +28,17 @@
#include "database/Track.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackArtistLink.hpp"
#include "metadata/Exception.hpp"
#include "metadata/IParser.hpp"
#include "utils/Exception.hpp"
#include "utils/IConfig.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
using namespace Database;
namespace Scanner
{
using namespace Database;
namespace
{
Artist::pointer createArtist(Session& session, const MetaData::Artist& artistInfo)
@@ -179,25 +180,34 @@ namespace Scanner
return Release::pointer{};
}
std::vector<Cluster::pointer> getOrCreateClusters(Session& session, const MetaData::Tags& tags)
std::vector<Cluster::pointer> getOrCreateClusters(Session& session, const MetaData::Track& track)
{
std::vector<Cluster::pointer> clusters;
for (const auto& [tag, values] : tags)
auto getOrCreateClusters {[&](std::string tag, std::span<const std::string> values)
{
auto clusterType = ClusterType::find(session, tag);
if (!clusterType)
clusterType = session.create<ClusterType>(tag);
for (const auto& clusterName : values)
for (const auto& value : values)
{
auto cluster = clusterType->getCluster(clusterName);
auto cluster{ clusterType->getCluster(value) };
if (!cluster)
cluster = session.create<Cluster>(clusterType, clusterName);
cluster = session.create<Cluster>(clusterType, value);
clusters.push_back(cluster);
}
}
}};
// TODO: migrate these fields in dedicated tables in DB
getOrCreateClusters("GENRE", track.genres);
getOrCreateClusters("MOOD", track.genres);
getOrCreateClusters("LANGUAGE", track.languages);
getOrCreateClusters("GROUPING", track.groupings);
for (const auto& [tag, values] : track.userExtraTags)
getOrCreateClusters(tag, values);
return clusters;
}
@@ -232,7 +242,7 @@ namespace Scanner
, _scanContextRunner{ _scanContext, threadCount }
{}
void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path path)
void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path)
{
{
std::scoped_lock lock{ _mutex };
@@ -241,12 +251,21 @@ namespace Scanner
_scanContext.post([=, this]
{
std::optional<MetaData::Track> trackMetaData{ _metadataParser.parse(path) };
std::unique_ptr<MetaData::Track> track;
try
{
track = _metadataParser.parse(path);
}
catch(const MetaData::Exception& e)
{
LMS_LOG(DBUPDATER, INFO, "Failed to parse '" << path.string() << "'");
}
{
std::scoped_lock lock{ _mutex };
_scanResults.emplace_back(std::make_unique<MetaDataScanResult>(path, std::move(trackMetaData)));
_scanResults.emplace_back(MetaDataScanResult{ std::move(path), std::move(track) });
_ongoingScanCount -= 1;
}
_condVar.notify_all();
@@ -259,7 +278,7 @@ namespace Scanner
return _scanResults.size();
}
size_t ScanStepScanFiles::MetadataScanQueue::popResults(std::vector<std::unique_ptr<MetaDataScanResult>>& results, std::size_t maxCount)
size_t ScanStepScanFiles::MetadataScanQueue::popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount)
{
results.clear();
results.reserve(maxCount);
@@ -285,7 +304,7 @@ namespace Scanner
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
: ScanStepBase{ initParams }
, _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib
, _metadataParser{ MetaData::createParser(MetaData::ParserBackend::TagLib, getParserReadStyle()) } // For now, always use TagLib
, _metadataScanQueue{ *_metadataParser, getScanMetaDataThreadCount() }
{
LMS_LOG(DBUPDATER, INFO, "Using " << _metadataScanQueue.getThreadCount() << " thread(s) for scanning file metadata");
@@ -300,9 +319,11 @@ namespace Scanner
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);
}
std::vector<std::unique_ptr<MetaDataScanResult>> scanResults;
std::vector<MetaDataScanResult> scanResults;
context.currentStepStats.totalElems = context.stats.filesScanned;
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
@@ -400,21 +421,21 @@ namespace Scanner
return true; // need to scan
}
void ScanStepScanFiles::processMetaDataScanResults(ScanContext& context, std::span<const std::unique_ptr<MetaDataScanResult>> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
void ScanStepScanFiles::processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo)
{
Database::Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createWriteTransaction() };
for (const auto& scanResult : scanResults)
for (const MetaDataScanResult& scanResult : scanResults)
{
if (_abortScan)
return;
if (scanResult->trackMetaData)
if (scanResult.trackMetaData)
{
context.stats.scans++;
processFileMetaData(context, scanResult->path, *scanResult->trackMetaData, libraryInfo);
processFileMetaData(context, scanResult.path, *scanResult.trackMetaData, libraryInfo);
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
if ((context.stats.scans % 1'000) == 0)
@@ -422,7 +443,7 @@ namespace Scanner
}
else
{
context.stats.errors.emplace_back(scanResult->path, ScanErrorType::CannotParseFile);
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotParseFile);
}
}
}
@@ -583,7 +604,7 @@ namespace Scanner
track.modify()->setTotalTrack(trackMetadata.medium ? trackMetadata.medium->trackCount : std::nullopt);
track.modify()->setReleaseReplayGain(trackMetadata.medium ? trackMetadata.medium->replayGain : std::nullopt);
track.modify()->setDiscSubtitle(trackMetadata.medium ? trackMetadata.medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata.userExtraTags));
track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata));
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration(trackMetadata.duration);
@@ -47,13 +47,13 @@ namespace Scanner
struct MetaDataScanResult
{
std::filesystem::path path;
std::optional<MetaData::Track> trackMetaData;
std::unique_ptr<MetaData::Track> trackMetaData;
};
void processMetaDataScanResults(ScanContext& context, std::span<const std::unique_ptr<MetaDataScanResult>> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processFileMetaData(ScanContext& context, const std::filesystem::path& file, const MetaData::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
std::unique_ptr<MetaData::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
const std::vector<std::string> _extraTagsToParse;
class MetadataScanQueue
{
@@ -62,10 +62,10 @@ namespace Scanner
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
void pushScanRequest(const std::filesystem::path path);
void pushScanRequest(const std::filesystem::path& path);
std::size_t getResultsCount() const;
size_t popResults(std::vector<std::unique_ptr<MetaDataScanResult>>& results, std::size_t maxCount);
size_t popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount);
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
@@ -76,11 +76,11 @@ namespace Scanner
mutable std::mutex _mutex ;
std::size_t _ongoingScanCount{};
std::deque<std::unique_ptr<MetaDataScanResult>> _scanResults;
std::deque<MetaDataScanResult> _scanResults;
std::condition_variable _condVar;
};
MetadataScanQueue _metadataScanQueue;
std::deque<std::unique_ptr<MetaDataScanResult>> _metaDataScanResults;
std::deque<MetaDataScanResult> _metaDataScanResults;
};
}
@@ -383,6 +383,9 @@ namespace Scanner
const auto& tags{ scanSettings->getExtraTagsToScan() };
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag };});
}
newSettings.artistTagDelimiters = scanSettings->getArtistTagDelimiters();
newSettings.defaultTagDelimiters = scanSettings->getDefaultTagDelimiters();
}
return newSettings;
@@ -30,31 +30,24 @@ namespace Scanner
{
struct ScannerSettings
{
std::size_t scanVersion{};
Wt::WTime startTime;
Database::ScanSettings::UpdatePeriod updatePeriod{ Database::ScanSettings::UpdatePeriod::Never };
std::vector<std::filesystem::path> supportedExtensions;
bool skipDuplicateMBID{};
std::vector<std::string> extraTags;
std::size_t scanVersion{};
Wt::WTime startTime;
Database::ScanSettings::UpdatePeriod updatePeriod{ Database::ScanSettings::UpdatePeriod::Never };
std::vector<std::filesystem::path> supportedExtensions;
bool skipDuplicateMBID{};
std::vector<std::string> extraTags;
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
struct MediaLibraryInfo
{
Database::MediaLibraryId id;
std::filesystem::path rootDirectory;
bool operator==(const MediaLibraryInfo& other) const { return id == other.id && rootDirectory == other.rootDirectory; }
bool operator<=>(const MediaLibraryInfo& other) const = default;
};
std::vector<MediaLibraryInfo> mediaLibraries;
std::vector<MediaLibraryInfo> mediaLibraries;
bool operator==(const ScannerSettings& rhs) const
{
return scanVersion == rhs.scanVersion
&& startTime == rhs.startTime
&& updatePeriod == rhs.updatePeriod
&& supportedExtensions == rhs.supportedExtensions
&& mediaLibraries == rhs.mediaLibraries
&& skipDuplicateMBID == rhs.skipDuplicateMBID
&& extraTags == rhs.extraTags;
}
bool operator<=>(const ScannerSettings& rhs) const = default;
};
}
+1 -1
View File
@@ -25,7 +25,7 @@ namespace StringUtils
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
{
// Expects "X.Y.Z"
const auto numbers{ StringUtils::splitString(str, ".") };
const auto numbers{ StringUtils::splitString(str, '.') };
if (numbers.size() < 2 || numbers.size() > 3)
return std::nullopt;
+4 -4
View File
@@ -63,7 +63,7 @@ namespace StringUtils
template<>
std::optional<Database::ArtistId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values{ StringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
@@ -88,7 +88,7 @@ namespace StringUtils
template<>
std::optional<Database::ReleaseId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values{ StringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
@@ -113,7 +113,7 @@ namespace StringUtils
template<>
std::optional<Database::TrackId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values{ StringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
@@ -129,7 +129,7 @@ namespace StringUtils
template<>
std::optional<Database::TrackListId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values{ StringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
@@ -49,7 +49,7 @@ namespace API::Subsonic
if (context.clientInfo.name == "Symfonium")
query = StringUtils::stringTrim(query, "\"");
std::vector<std::string_view> keywords{ StringUtils::splitString(query, " ") };
std::vector<std::string_view> keywords{ StringUtils::splitString(query, ' ') };
// Optional params
std::size_t artistCount{ getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20) };
+1
View File
@@ -68,6 +68,7 @@ Log::Log(ILogger& logger, Module module, Severity severity)
Log::~Log()
{
assert(_logger.isSeverityActive(_severity));
_logger.processLog(*this);
}
+3 -2
View File
@@ -17,6 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cassert>
#include <thread>
#include "utils/StreamLogger.hpp"
@@ -29,7 +30,7 @@ StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
void StreamLogger::processLog(const Log& log)
{
if (_severities.contains(log.getSeverity()))
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
assert(isSeverityActive(log.getSeverity()));
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
}
+131 -67
View File
@@ -23,17 +23,13 @@
#include <iomanip>
#include <utility>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string.hpp>
#include <Wt/WDateTime.h>
#include <Wt/WDate.h>
namespace StringUtils
{
namespace
namespace details
{
constexpr std::pair<char, std::string_view> jsEscapeChars[]
{
@@ -87,32 +83,23 @@ namespace StringUtils
os << c;
}
}
}
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
{
std::string curStr;
for (char c : str)
template <typename StringType>
std::string joinStrings(std::span<const StringType> strings, std::string_view delimiter)
{
if (separators.find(c) != std::string::npos) {
if (!curStr.empty()) {
results.push_back(curStr);
curStr.clear();
}
}
else {
if (curStr.empty() && std::isspace(c))
continue;
std::string res;
bool first{ true };
curStr.push_back(c);
for (const StringType& str : strings)
{
if (!first)
res += delimiter;
res += str;
first = false;
}
return res;
}
if (!curStr.empty())
results.push_back(curStr);
return !str.empty();
}
template<>
@@ -138,57 +125,102 @@ namespace StringUtils
return std::nullopt;
}
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
std::vector<std::string_view> splitString(std::string_view str, char separator)
{
std::string str{ stringTrim(string, separators) };
std::vector<std::string> res;
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
return res;
return splitString(str, std::string_view{ &separator, 1 });
}
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
std::vector<std::string_view> splitString(std::string_view str, std::string_view separator)
{
std::vector<std::string_view> res;
std::string_view::size_type strBegin{};
if (separator.empty())
return { str };
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
size_t pos{};
size_t found{ str.find(separator) };
while (found != std::string_view::npos)
{
auto strEnd{ str.find_first_of(separators, strBegin + 1) };
if (strEnd == std::string_view::npos)
res.push_back(str.substr(pos, found - pos));
pos = found + separator.size();
found = str.find(separator, pos);
}
res.push_back(str.substr(pos));
return res;
}
std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter)
{
return details::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter)
{
return details::joinStrings(strings, delimiter);
}
std::string joinStrings(std::span<const std::string> strings, char delimiter)
{
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
}
std::string joinStrings(std::span<const std::string_view> strings, char delimiter)
{
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
}
std::string escapeAndJoinStrings(std::span<const std::string_view> strings, char delimiter, char escapeChar)
{
std::string result;
for (const std::string_view str : strings)
{
if (!result.empty())
result.push_back(delimiter);
for (char c : str)
{
res.push_back(str.substr(strBegin, str.size() - strBegin));
break;
if (c == delimiter || c == escapeChar)
result.push_back(escapeChar);
result.push_back(c);
}
res.push_back(str.substr(strBegin, strEnd - strBegin));
strBegin = strEnd + 1;
}
return res;
return result;
}
std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter)
std::vector<std::string> splitEscapedStrings(std::string_view str, char delimiter, char escapeChar)
{
std::string res;
bool first{ true };
std::vector<std::string> result;
std::string current;
bool escaped{};
for (std::string_view str : strings)
for (char c : str)
{
if (!first)
res += delimiter;
res += str;
first = false;
if (escaped) {
current.push_back(c);
escaped = false;
}
else
{
if (c == delimiter)
{
result.push_back(std::move(current));
current.clear();
}
else if (c == escapeChar)
escaped = true;
else
current.push_back(c);
}
}
return res;
}
if (!current.empty())
result.push_back(std::move(current));
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
{
return boost::algorithm::join(strings, delimiter);
return result;
}
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
@@ -237,7 +269,7 @@ namespace StringUtils
return res;
}
std::string bufferToString(const std::vector<unsigned char>& data)
std::string bufferToString(std::span<const unsigned char> data)
{
std::ostringstream oss;
@@ -253,7 +285,7 @@ namespace StringUtils
{
if (strA.size() != strB.size())
return false;
for (std::size_t i{}; i < strA.size(); ++i)
{
if (std::tolower(strA[i]) != std::tolower(strB[i]))
@@ -293,22 +325,22 @@ namespace StringUtils
std::string jsEscape(std::string_view str)
{
return escape(str, jsEscapeChars);
return details::escape(str, details::jsEscapeChars);
}
void writeJSEscapedString(std::ostream& os, std::string_view str)
{
writeEscapedString(os, str, jsEscapeChars);
details::writeEscapedString(os, str, details::jsEscapeChars);
}
std::string jsonEscape(std::string_view str)
{
return escape(str, jsonEscapeChars);
return details::escape(str, details::jsonEscapeChars);
}
void writeJsonEscapedString(std::ostream& os, std::string_view str)
{
writeEscapedString(os, str, jsonEscapeChars);
details::writeEscapedString(os, str, details::jsonEscapeChars);
}
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
@@ -327,9 +359,41 @@ namespace StringUtils
return res;
}
bool stringEndsWith(const std::string& str, const std::string& ending)
std::string unescapeString(std::string_view str, char escapeChar)
{
return boost::algorithm::ends_with(str, ending);
std::string res;
res.reserve(str.size());
bool escaped{};
for (char c : str)
{
if (escaped)
{
res += c;
escaped = false;
}
else
{
if (c == escapeChar)
escaped = true;
else
res += c;
}
}
if (escaped)
res += escapeChar;
return res;
}
bool stringEndsWith(std::string_view str, std::string_view ending)
{
if (str.length() < ending.length())
return false;
return str.substr(str.length() - ending.length()) == ending;
}
std::optional<std::string> stringFromHex(const std::string& str)
@@ -25,11 +25,12 @@
class StreamLogger final : public ILogger
{
public:
static constexpr EnumSet<Severity> allSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO, Severity::DEBUG };
static constexpr EnumSet<Severity> defaultSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO };
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
bool isSeverityActive(Severity) const override { return true; }
bool isSeverityActive(Severity severity) const override { return _severities.contains(severity); }
void processLog(const Log& log) override;
private:
+14 -7
View File
@@ -21,6 +21,7 @@
#include <initializer_list>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <sstream>
@@ -35,13 +36,18 @@ namespace Wt
class WDateTime;
}
namespace StringUtils {
namespace StringUtils
{
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, char separator);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separator);
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
[[nodiscard]] std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter);
[[nodiscard]] std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter);
[[nodiscard]] std::string joinStrings(std::span<const std::string> strings, char delimiter);
[[nodiscard]] std::string joinStrings(std::span<const std::string_view> strings, char delimiter);
[[nodiscard]] std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter);
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]] std::string escapeAndJoinStrings(std::span<const std::string_view> strings, char delimiter, char escapeChar);
[[nodiscard]] std::vector<std::string> splitEscapedStrings(std::string_view string, char delimiter, char escapeChar);
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
@@ -50,7 +56,7 @@ namespace StringUtils {
void stringToLower(std::string& str);
[[nodiscard]] std::string stringToUpper(const std::string& str);
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
[[nodiscard]] std::string bufferToString(std::span<const unsigned char> data);
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB);
@@ -86,8 +92,9 @@ namespace StringUtils {
void writeJsonEscapedString(std::ostream& os, std::string_view str);
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
[[nodiscard]] std::string unescapeString(std::string_view str, char escapeChar);
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
[[nodiscard]] bool stringEndsWith(std::string_view str, std::string_view ending);
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
+2
View File
@@ -33,6 +33,8 @@ public:
std::string_view getAsString() const { return _value; }
bool operator<=>(const UUID&) const = default;
private:
UUID(std::string_view value);
std::string _value;
+132 -60
View File
@@ -24,80 +24,77 @@
#include <Wt/WTime.h>
#include "utils/String.hpp"
TEST(StringUtils, splitString)
TEST(StringUtils, splitString_charDelim)
{
struct TestCase
{
const std::string test{ "a" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
std::string_view input;
char delimiter;
std::vector<std::string_view> expectedOutput;
};
TestCase tests[]
{
const std::string test{ "a b" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "|") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a b");
}
{"abc", '-', {"abc"}},
{"a", '-', {"a"}},
{"", '-', {""}},
{"a-b-c", '-', {"a", "b", "c"}},
{"a|b|c", '|', {"a", "b", "c"}},
{"a;b;c", ';', {"a", "b", "c"}},
{";b;c", ';', {"", "b", "c"}},
{" ;b;c", ';', {" ", "b", "c"}},
{" ;;c", ';', {" ", "", "c"}},
{" ; ;c", ';', {" ", " ", "c"}},
{"a;b; ", ';', {"a", "b", " "}},
{"a;b", ';', {"a", "b"}},
{";b", ';', {"", "b"}},
{";", ';', {"", ""}},
{";;", ';', {"", "", ""}},
{";;;", ';', {"", "", "", ""}},
{";;a;;b;;", ';', {"", "", "a", "", "b", "", ""}},
{"a b", ' ', {"a", "b"}},
{"", ' ', {""}},
{"a-b|c", '-', {"a","b|c"}},
{"a|b-c", '-', {"a|b", "c"}},
{"test=foo bar", '=', {"test", "foo bar"}},
};
for (const TestCase& test : tests)
{
const std::string test{ " a" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
{
const std::string test{ "a " };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
{
const std::string test{ "a b" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings.front(), "a");
EXPECT_EQ(strings.back(), "b");
}
{
const std::string test{ "a b,c|defgh " };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ,|") };
ASSERT_EQ(strings.size(), 4);
EXPECT_EQ(strings[0], "a");
EXPECT_EQ(strings[1], "b");
EXPECT_EQ(strings[2], "c");
EXPECT_EQ(strings[3], "defgh");
const std::vector<std::string_view> res{ StringUtils::splitString(test.input, test.delimiter) };
EXPECT_EQ(res, test.expectedOutput) << "Input = '" << test.input << "', delims = '" << test.delimiter << "'";
}
}
TEST(StringUtils, splitStringCopy)
TEST(StringUtils, splitString_stringDelim)
{
struct TestCase
{
const std::string test{ "test=foo" };
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo");
}
std::string_view input;
std::string_view delimiter;
std::vector<std::string_view> expectedOutput;
};
TestCase tests[]
{
const std::string test{ "test=foo bar" };
{"abc", "", {"abc"}},
{"abc", "-", {"abc"}},
{"abc", "b", {"a", "c"}},
{"ab/cd", "/", {"ab", "cd"}},
{"ab/cd", "/ ", {"ab/cd"}},
{"ab/cd", " /", {"ab/cd"}},
{"ab /cd", " /", {"ab", "cd"}},
{"ab/ cd", "/ ", {"ab", "cd"}},
{"ab / cd", " / ", {"ab", "cd"}},
{"ab/cd", " / ", {"ab/cd"}},
{"ab/cd / ", " / ", {"ab/cd", ""}},
};
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo bar");
for (const TestCase& test : tests)
{
const std::vector<std::string_view> res{ StringUtils::splitString(test.input, test.delimiter) };
EXPECT_EQ(res, test.expectedOutput) << "Input = '" << test.input << "', delims = '" << test.delimiter << "'";
}
}
@@ -128,6 +125,58 @@ TEST(StringUtils, joinStrings)
}
}
TEST(StringUtils, escapeAndJoinStrings)
{
struct TestCase
{
std::vector<std::string_view> input;
char delimiter;
char escapeChar;
std::string expectedOutput;
};
TestCase tests[]
{
{{""}, ';', '\\', ""},
{{";"}, ';', '\\', "\\;"},
{{";;"}, ';', '\\', "\\;\\;"},
{{"a;", "b"}, ';', '\\', "a\\;;b"},
{{"a;", "b;"}, ';', '\\', "a\\;;b\\;"},
};
for (const TestCase& test : tests)
{
const std::string str{ StringUtils::escapeAndJoinStrings(test.input, test.delimiter, test.escapeChar) };
EXPECT_EQ(str, test.expectedOutput);
}
}
TEST(StringUtils, splitEscapedStrings)
{
struct TestCase
{
std::string input;
char delimiter;
char escapeChar;
std::vector<std::string> expectedOutput;
};
TestCase tests[]
{
{"", ';', '\\', {}},
{"\\;", ';', '\\', {";"}},
{"\\;\\;", ';', '\\', {";;"}},
{"a\\;;b", ';', '\\', {"a;", "b"}},
{"a\\;;b\\;", ';', '\\', {"a;", "b;"}},
};
for (const TestCase& test : tests)
{
const std::vector<std::string> str{ StringUtils::splitEscapedStrings(test.input, test.delimiter, test.escapeChar) };
EXPECT_EQ(str, test.expectedOutput);
}
}
TEST(StringUtils, escapeJSString)
{
EXPECT_EQ(StringUtils::jsEscape(""), "");
@@ -153,6 +202,17 @@ TEST(StringUtils, escapeString)
EXPECT_EQ(StringUtils::escapeString("*a*", "*", '_'), "_*a_*");
EXPECT_EQ(StringUtils::escapeString("*a|", "*|", '_'), "_*a_|");
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
EXPECT_EQ(StringUtils::escapeString("one;two", ";", '\\'), "one\\;two");
EXPECT_EQ(StringUtils::escapeString("one\\;two", ";", '\\'), "one\\\\;two");
EXPECT_EQ(StringUtils::escapeString("one;", ";", '\\'), "one\\;");
}
TEST(StringUtils, unescapeString)
{
EXPECT_EQ(StringUtils::unescapeString("one\\", '\\'), "one\\");
EXPECT_EQ(StringUtils::unescapeString("\\\\one", '\\'), "\\one");
EXPECT_EQ(StringUtils::unescapeString("one\\;two", '\\'), "one;two");
EXPECT_EQ(StringUtils::unescapeString("one\\\\;two", '\\'), "one\\;two");
}
TEST(StringUtils, readAs_bool)
@@ -220,4 +280,16 @@ TEST(Stringutils, dateTime)
{
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
}
TEST(StringUtils, stringEndsWith)
{
EXPECT_TRUE(StringUtils::stringEndsWith("FooBar", "Bar"));
EXPECT_TRUE(StringUtils::stringEndsWith("FooBar", ""));
EXPECT_TRUE(StringUtils::stringEndsWith("", ""));
EXPECT_TRUE(StringUtils::stringEndsWith("FooBar", "ar"));
EXPECT_TRUE(StringUtils::stringEndsWith("FooBar", "FooBar"));
EXPECT_FALSE(StringUtils::stringEndsWith("FooBar", "1FooBar"));
EXPECT_FALSE(StringUtils::stringEndsWith("FooBar", "1FooBar"));
EXPECT_FALSE(StringUtils::stringEndsWith("FooBar", "R"));
}