/* * 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 . */ #include "Parser.hpp" #include #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 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 std::vector getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters) { std::vector res; for (const TagType tagType : tagTypes) { auto addTagIfNonEmpty{ [&res](std::string_view tag) { tag = StringUtils::stringTrim(tag); if (!tag.empty()) { std::optional val{ StringUtils::readAs(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 std::optional getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes) { std::optional res; std::vector values{ getTagValuesFirstMatchAs(tagReader, tagTypes, {} /* don't expect multiple values here */) }; if (!values.empty()) res = std::move(values.front()); return res; } template std::vector getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span tagDelimiters) { return getTagValuesFirstMatchAs(tagReader, { tagType }, tagDelimiters); } template std::optional getTagValueAs(const ITagReader& tagReader, TagType tagType) { return getTagValueFirstMatchAs(tagReader, { tagType }); } std::vector getArtists(const ITagReader& tagReader, std::initializer_list artistTagNames, std::initializer_list artistSortTagNames, std::initializer_list artistMBIDTagNames, std::span artistTagDelimiters ) { std::vector artistNames{ getTagValuesFirstMatchAs(tagReader, artistTagNames, artistTagDelimiters) }; if (artistNames.empty()) return {}; std::vector artistSortNames{ getTagValuesFirstMatchAs(tagReader, artistSortTagNames, artistTagDelimiters) }; std::vector artistMBIDs{ getTagValuesFirstMatchAs(tagReader, artistMBIDTagNames, artistTagDelimiters) }; std::vector 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 createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle) { return std::make_unique(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 Parser::parse(const std::filesystem::path& p, bool debug) { try { std::unique_ptr tagReader; switch (_parserBackend) { case ParserBackend::TagLib: tagReader = std::make_unique(p, _readStyle, debug); break; case ParserBackend::AvFormat: tagReader = std::make_unique(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 Parser::parse(const ITagReader& tagReader) { auto track{ std::make_unique() }; 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(tagReader, TagType::TrackTitle).value_or(""); track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID); track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); track.acoustID = getTagValueAs(tagReader, TagType::AcoustID); track.position = getTagValueAs(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine if (auto dateStr = getTagValueAs(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(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(tagReader, TagType::OriginalReleaseYear)) { track.originalYear = Utils::parseYear(*dateStr); } track.copyright = getTagValueAs(tagReader, TagType::Copyright).value_or(""); track.copyrightURL = getTagValueAs(tagReader, TagType::CopyrightURL).value_or(""); track.replayGain = getTagValueAs(tagReader, TagType::ReplayGainTrackGain); track.artistDisplayName = getTagValueAs(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(tagReader, TagType::Genre, _defaultTagDelimiters); track.moods = getTagValuesAs(tagReader, TagType::Mood, _defaultTagDelimiters); track.groupings = getTagValuesAs(tagReader, TagType::Grouping, _defaultTagDelimiters); track.labels = getTagValuesAs(tagReader, TagType::RecordLabel, _defaultTagDelimiters); track.languages = getTagValuesAs(tagReader, TagType::Language, _defaultTagDelimiters); std::vector 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 Parser::getMedium(const ITagReader& tagReader) { std::optional medium; medium.emplace(); medium->media = getTagValueAs(tagReader, TagType::Media).value_or(""); medium->name = getTagValueAs(tagReader, TagType::DiscSubtitle).value_or(""); medium->trackCount = getTagValueAs(tagReader, TagType::TotalTracks); if (!medium->trackCount) { // totalTracks may be encoded as "position/count" if (const auto value{ getTagValueAs(tagReader, TagType::TrackNumber) }) { // Expecting 'Number/Total' const std::vector strings{ StringUtils::splitString(*value, '/') }; if (strings.size() == 2) medium->trackCount = StringUtils::readAs(strings[1]); } } // Expecting 'Number[/Total]' medium->position = getTagValueAs(tagReader, TagType::DiscNumber); medium->release = getRelease(tagReader); medium->replayGain = getTagValueAs(tagReader, TagType::ReplayGainAlbumGain); if (medium->isDefault()) medium.reset(); return medium; } std::optional Parser::getRelease(const ITagReader& tagReader) { std::optional release; auto releaseName{ getTagValueAs(tagReader, TagType::Album) }; if (!releaseName) return release; release.emplace(); release->name = std::move(*releaseName); release->artistDisplayName = getTagValueAs(tagReader, TagType::AlbumArtist).value_or(""); // TODO try to join albumartists if present release->mbid = getTagValueAs(tagReader, TagType::MusicBrainzReleaseID); release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters); release->mediumCount = getTagValueAs(tagReader, TagType::TotalDiscs); if (!release->mediumCount) { // mediumCount may be encoded as "position/count" if (const auto value{ getTagValueAs(tagReader, TagType::DiscNumber) }) { // Expecting 'Number/Total' const std::vector strings{ StringUtils::splitString(*value, '/') }; if (strings.size() == 2) release->mediumCount = StringUtils::readAs(strings[1]); } } release->releaseTypes = getTagValuesAs(tagReader, TagType::ReleaseType, _defaultTagDelimiters); return release; } } // namespace MetaData