From cce11c5416f9321942748626c217a4f0d1d3a445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 26 Oct 2025 19:38:34 -0400 Subject: [PATCH] fix(scanner): restore basic tag extraction fallback mechanism for improved metadata parsing (#4401) * feat: add basic tag extraction fallback mechanism Added basic tag extraction from TagLib's generic Tag interface as a fallback when PropertyMap doesn't contain standard metadata fields. This ensures that essential tags like title, artist, album, comment, genre, year, and track are always available even when they're not present in format-specific property maps. Changes include: - Extract basic tags (__title, __artist, etc.) in C++ wrapper - Add parseBasicTag function to process basic tags in Go extractor - Refactor parseProp function to be reusable across property parsing - Ensure basic tags are preferred over PropertyMap when available * feat(taglib): update tag parsing to use double underscores for properties Signed-off-by: Deluan --------- Signed-off-by: Deluan --- adapters/taglib/taglib.go | 57 ++++++++++++++++++++--------- adapters/taglib/taglib_wrapper.cpp | 58 +++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/adapters/taglib/taglib.go b/adapters/taglib/taglib.go index 62a949d85..d32adf4ed 100644 --- a/adapters/taglib/taglib.go +++ b/adapters/taglib/taglib.go @@ -43,23 +43,21 @@ func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) { // Parse audio properties ap := metadata.AudioProperties{} - if length, ok := tags["_lengthinmilliseconds"]; ok && len(length) > 0 { - millis, _ := strconv.Atoi(length[0]) - if millis > 0 { - ap.Duration = (time.Millisecond * time.Duration(millis)).Round(time.Millisecond * 10) - } - delete(tags, "_lengthinmilliseconds") - } - parseProp := func(prop string, target *int) { - if value, ok := tags[prop]; ok && len(value) > 0 { - *target, _ = strconv.Atoi(value[0]) - delete(tags, prop) - } - } - parseProp("_bitrate", &ap.BitRate) - parseProp("_channels", &ap.Channels) - parseProp("_samplerate", &ap.SampleRate) - parseProp("_bitspersample", &ap.BitDepth) + ap.BitRate = parseProp(tags, "__bitrate") + ap.Channels = parseProp(tags, "__channels") + ap.SampleRate = parseProp(tags, "__samplerate") + ap.BitDepth = parseProp(tags, "__bitspersample") + length := parseProp(tags, "__lengthinmilliseconds") + ap.Duration = (time.Millisecond * time.Duration(length)).Round(time.Millisecond * 10) + + // Extract basic tags + parseBasicTag(tags, "__title", "title") + parseBasicTag(tags, "__artist", "artist") + parseBasicTag(tags, "__album", "album") + parseBasicTag(tags, "__comment", "comment") + parseBasicTag(tags, "__genre", "genre") + parseBasicTag(tags, "__year", "year") + parseBasicTag(tags, "__track", "tracknumber") // Parse track/disc totals parseTuple := func(prop string) { @@ -107,6 +105,31 @@ var tiplMapping = map[string]string{ "DJ-mix": "djmixer", } +// parseProp parses a property from the tags map and sets it to the target integer. +// It also deletes the property from the tags map after parsing. +func parseProp(tags map[string][]string, prop string) int { + if value, ok := tags[prop]; ok && len(value) > 0 { + v, _ := strconv.Atoi(value[0]) + delete(tags, prop) + return v + } + return 0 +} + +// parseBasicTag checks if a basic tag (like __title, __artist, etc.) exists in the tags map. +// If it does, it moves the value to a more appropriate tag name (like title, artist, etc.), +// and deletes the basic tag from the map. If the target tag already exists, it ignores the basic tag. +func parseBasicTag(tags map[string][]string, basicName string, tagName string) { + basicValue := tags[basicName] + if len(basicValue) == 0 { + return + } + delete(tags, basicName) + if len(tags[tagName]) == 0 { + tags[tagName] = basicValue + } +} + // parseTIPL parses the ID3v2.4 TIPL frame string, which is received from TagLib in the format: // // "arranger Andrew Powell engineer Chris Blair engineer Pat Stapley producer Eric Woolfson". diff --git a/adapters/taglib/taglib_wrapper.cpp b/adapters/taglib/taglib_wrapper.cpp index 224642c6d..2985e8f18 100644 --- a/adapters/taglib/taglib_wrapper.cpp +++ b/adapters/taglib/taglib_wrapper.cpp @@ -45,31 +45,63 @@ int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) { // Add audio properties to the tags const TagLib::AudioProperties *props(f.audioProperties()); - goPutInt(id, (char *)"_lengthinmilliseconds", props->lengthInMilliseconds()); - goPutInt(id, (char *)"_bitrate", props->bitrate()); - goPutInt(id, (char *)"_channels", props->channels()); - goPutInt(id, (char *)"_samplerate", props->sampleRate()); + goPutInt(id, (char *)"__lengthinmilliseconds", props->lengthInMilliseconds()); + goPutInt(id, (char *)"__bitrate", props->bitrate()); + goPutInt(id, (char *)"__channels", props->channels()); + goPutInt(id, (char *)"__samplerate", props->sampleRate()); + // Extract bits per sample for supported formats + int bitsPerSample = 0; if (const auto* apeProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", apeProperties->bitsPerSample()); - if (const auto* asfProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", asfProperties->bitsPerSample()); + bitsPerSample = apeProperties->bitsPerSample(); + else if (const auto* asfProperties{ dynamic_cast(props) }) + bitsPerSample = asfProperties->bitsPerSample(); else if (const auto* flacProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", flacProperties->bitsPerSample()); + bitsPerSample = flacProperties->bitsPerSample(); else if (const auto* mp4Properties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", mp4Properties->bitsPerSample()); + bitsPerSample = mp4Properties->bitsPerSample(); else if (const auto* wavePackProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", wavePackProperties->bitsPerSample()); + bitsPerSample = wavePackProperties->bitsPerSample(); else if (const auto* aiffProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", aiffProperties->bitsPerSample()); + bitsPerSample = aiffProperties->bitsPerSample(); else if (const auto* wavProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", wavProperties->bitsPerSample()); + bitsPerSample = wavProperties->bitsPerSample(); else if (const auto* dsfProperties{ dynamic_cast(props) }) - goPutInt(id, (char *)"_bitspersample", dsfProperties->bitsPerSample()); + bitsPerSample = dsfProperties->bitsPerSample(); + + if (bitsPerSample > 0) { + goPutInt(id, (char *)"__bitspersample", bitsPerSample); + } // Send all properties to the Go map TagLib::PropertyMap tags = f.file()->properties(); + // Make sure at least the basic properties are extracted + TagLib::Tag *basic = f.file()->tag(); + if (!basic->isEmpty()) { + if (!basic->title().isEmpty()) { + tags.insert("__title", basic->title()); + } + if (!basic->artist().isEmpty()) { + tags.insert("__artist", basic->artist()); + } + if (!basic->album().isEmpty()) { + tags.insert("__album", basic->album()); + } + if (!basic->comment().isEmpty()) { + tags.insert("__comment", basic->comment()); + } + if (!basic->genre().isEmpty()) { + tags.insert("__genre", basic->genre()); + } + if (basic->year() > 0) { + tags.insert("__year", TagLib::String::number(basic->year())); + } + if (basic->track() > 0) { + tags.insert("__track", TagLib::String::number(basic->track())); + } + } + TagLib::ID3v2::Tag *id3Tags = NULL; // Get some extended/non-standard ID3-only tags (ex: iTunes extended frames)