Now manually parse sync/unsync lyrics frames in id3v2 tags

This commit is contained in:
emeric
2024-10-27 15:21:54 +01:00
parent 1e3dad7acc
commit f613d16fc2
14 changed files with 193 additions and 58 deletions
+19 -1
View File
@@ -79,7 +79,6 @@ namespace lms::metadata
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Lyrics, { "LYRICS" } },
{ TagType::Media, { "TMED", "MEDIA", "WM/MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
@@ -207,4 +206,23 @@ namespace lms::metadata
visitor("", value);
});
}
void AvFormatTagReader::visitLyricsTags(LyricsVisitor visitor) const
{
// MPEG files: need to visit LYRICS-language entries
for (const auto& [tag, value] : _metaDataMap)
{
constexpr std::string_view lyricsPrefix{ "LYRICS-" };
if (tag.starts_with(lyricsPrefix))
{
const std::string language{ core::stringUtils::stringToLower(tag.substr(lyricsPrefix.size())) };
visitor(language, value);
}
}
// otherwise, just visit regular LYRICS tag with no language
visitTagValues("LYRICS", [&](std::string_view value) {
visitor("", value);
});
}
} // namespace lms::metadata
@@ -40,6 +40,7 @@ namespace lms::metadata
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
+5 -1
View File
@@ -76,7 +76,7 @@ namespace lms::metadata
LyricistSortOrder, // non standard
Lyricists, // non standard
LyricistsSortOrder, // non standard
Lyrics,
// Lyrics, Handled separately
Media,
MixDJ,
Mixer,
@@ -104,6 +104,7 @@ namespace lms::metadata
OriginalFilename,
OriginalReleaseDate,
OriginalReleaseYear,
// Performers, Handled separately
Podcast,
PodcastURL,
Producer,
@@ -154,6 +155,9 @@ namespace lms::metadata
using PerformerVisitor = std::function<void(std::string_view role, std::string_view artist)>;
virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
virtual bool hasEmbeddedCover() const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
};
+24 -14
View File
@@ -121,6 +121,29 @@ namespace lms::metadata
return getTagValueFirstMatchAs<T>(tagReader, { tagType });
}
std::vector<Lyrics> getLyrics(const ITagReader& tagReader)
{
std::vector<Lyrics> res;
tagReader.visitLyricsTags([&](std::string_view language, std::string_view lyricsText) {
std::istringstream iss{ std::string{ lyricsText } }; // TODO avoid copies (ispanstream?)
try
{
Lyrics lyrics{ parseLyrics(iss) };
if (lyrics.language.empty())
lyrics.language = language;
res.emplace_back(std::move(lyrics));
}
catch (const LyricsException& e)
{
LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() });
}
});
return res;
}
std::vector<Artist> getArtists(const ITagReader& tagReader,
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
@@ -321,20 +344,7 @@ namespace lms::metadata
track.originalYear = utils::parseYear(*dateStr);
}
std::vector<std::string> lyricsEntries{ getTagValuesAs<std::string>(tagReader, TagType::Lyrics, {} /* no custom delimiter on lyrics */) };
for (const std::string& lyrics : lyricsEntries)
{
std::istringstream iss{ lyrics };
try
{
track.lyrics.emplace_back(parseLyrics(iss));
}
catch (const LyricsException& e)
{
LMS_LOG(METADATA, ERROR, "Failed to parse lyrics: " + std::string{ e.what() });
}
}
track.lyrics = getLyrics(tagReader); // no custom delimiter on lyrics
track.comments = getTagValuesAs<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
+79 -25
View File
@@ -35,8 +35,10 @@
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/synchronizedlyricsframe.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
@@ -100,7 +102,6 @@ namespace lms::metadata
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Lyrics, { "LYRICS" } },
{ TagType::Media, { "MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
@@ -220,8 +221,63 @@ namespace lms::metadata
mergeTagMaps(_propertyMap, apeTag->properties());
};
// Not that good embedded pictures handling
// + get some extra tags that may not be known by taglib
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
const auto& frameListMap{ id3v2Tags.frameListMap() };
// Not that good embedded pictures handling
if (!frameListMap["APIC"].isEmpty())
_hasEmbeddedCover = true;
// Get some extra tags that may not be known by taglib
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
// consider each frame hold a different set of lyrics
// Synchronized lyrics frames
for (const TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
{
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
std::string lyrics;
for (const TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
{
std::chrono::milliseconds timestamp{};
switch (lyricsFrame->timestampFormat())
{
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
timestamp = std::chrono::milliseconds{ synchedText.time };
break;
case TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
timestamp = std::chrono::milliseconds{ _audioProperties.sampleRate ? (synchedText.time * 1000) / _audioProperties.sampleRate : 0 };
break;
case TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
break;
}
if (!lyrics.empty())
lyrics += '\n';
lyrics += core::stringUtils::formatTimestamp(timestamp);
lyrics += synchedText.text.to8Bit(true);
}
_id3v2Lyrics.emplace(language, std::move(lyrics));
}
// Unsynchronized lyrics frames
for (const TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
{
const auto* lyricsFrame{ dynamic_cast<const TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
_id3v2Lyrics.emplace(language, lyricsFrame->text().to8Bit(true));
}
};
// WMA
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
@@ -255,16 +311,8 @@ namespace lms::metadata
// 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.contains("DISCSUBTITLE"))
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
}
if (mp3File->hasID3v2Tag())
processID3v2Tags(*mp3File->ID3v2Tag());
getAPETags(mp3File->APETag());
}
@@ -326,22 +374,12 @@ namespace lms::metadata
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
{
if (aiffFile->hasID3v2Tag())
{
const auto& frameListMap{ aiffFile->tag()->frameListMap() };
if (!frameListMap["APIC"].isEmpty())
_hasEmbeddedCover = true;
}
processID3v2Tags(*aiffFile->tag());
}
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
{
if (wavFile->hasID3v2Tag())
{
const auto& frameListMap{ wavFile->ID3v2Tag()->frameListMap() };
if (!frameListMap["APIC"].isEmpty())
_hasEmbeddedCover = true;
}
processID3v2Tags(*wavFile->ID3v2Tag());
}
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
@@ -442,4 +480,20 @@ namespace lms::metadata
}
}
}
void TagLibTagReader::visitLyricsTags(LyricsVisitor visitor) const
{
if (!_id3v2Lyrics.empty())
{
for (const auto& [language, lyrics] : _id3v2Lyrics)
visitor(language, lyrics);
}
else
{
// otherwise, just visit regular LYRICS tag with no language
visitTagValues("LYRICS", [&](std::string_view value) {
visitor("", value);
});
}
}
} // namespace lms::metadata
@@ -20,6 +20,8 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <taglib/fileref.h>
#include <taglib/tpropertymap.h>
@@ -43,6 +45,7 @@ namespace lms::metadata
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
@@ -51,5 +54,6 @@ namespace lms::metadata
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
bool _hasEmbeddedCover{};
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::metadata