From a6fc84a2e1b1b78fe34416759fe41ee72e4af9a3 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 23 Jan 2024 20:50:43 -0500 Subject: [PATCH] Parse the ID3v2.4 TIPL frame --- scanner/metadata/taglib/taglib.go | 49 +++++++++++++++++++++++ scanner/metadata/taglib/taglib_test.go | 43 ++++++++++++++++++++ scanner/metadata/taglib/taglib_wrapper.go | 10 ++--- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/scanner/metadata/taglib/taglib.go b/scanner/metadata/taglib/taglib.go index 0a2aa009..3b3f4104 100644 --- a/scanner/metadata/taglib/taglib.go +++ b/scanner/metadata/taglib/taglib.go @@ -4,6 +4,7 @@ import ( "errors" "os" "strconv" + "strings" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/scanner/metadata" @@ -46,10 +47,58 @@ func (e *Extractor) extractMetadata(filePath string) (metadata.ParsedTags, error tags["duration"] = []string{strconv.FormatFloat(duration, 'f', 2, 32)} } } + // Adjust some ID3 tags + parseTIPL(tags) + delete(tags, "tmcl") // TMCL is already parsed by TagLib return tags, nil } +// These are the only roles we support, based on Picard's tag map: +// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html +var tiplMapping = map[string]string{ + "arranger": "arranger", + "engineer": "engineer", + "producer": "producer", + "mix": "mixer", + "dj-mix": "djmixer", +} + +// 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". +// +// and breaks it down into a map of roles and names, e.g.: +// +// {"arranger": ["Andrew Powell"], "engineer": ["Chris Blair", "Pat Stapley"], "producer": ["Eric Woolfson"]}. +func parseTIPL(tags metadata.ParsedTags) { + tipl := tags["tipl"] + if len(tipl) == 0 { + return + } + + addRole := func(tags metadata.ParsedTags, currentRole string, currentValue []string) { + if currentRole != "" { + role := tiplMapping[currentRole] + tags[role] = append(tags[currentRole], strings.Join(currentValue, " ")) + } + } + + var currentRole string + var currentValue []string + for _, part := range strings.Split(tipl[0], " ") { + if _, ok := tiplMapping[part]; ok { + addRole(tags, currentRole, currentValue) + currentRole = part + currentValue = nil + continue + } + currentValue = append(currentValue, part) + } + addRole(tags, currentRole, currentValue) + delete(tags, "tipl") +} + func init() { metadata.RegisterExtractor(ExtractorID, &Extractor{}) } diff --git a/scanner/metadata/taglib/taglib_test.go b/scanner/metadata/taglib/taglib_test.go index e499ab18..9a21f0be 100644 --- a/scanner/metadata/taglib/taglib_test.go +++ b/scanner/metadata/taglib/taglib_test.go @@ -4,6 +4,7 @@ import ( "io/fs" "os" + "github.com/navidrome/navidrome/scanner/metadata" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -195,4 +196,46 @@ var _ = Describe("Extractor", func() { }) }) + Describe("parseTIPL", func() { + var tags metadata.ParsedTags + + BeforeEach(func() { + tags = metadata.ParsedTags{} + }) + + Context("when the TIPL string is populated", func() { + It("correctly parses roles and names", func() { + tags["tipl"] = []string{"arranger Andrew Powell dj-mix François Kevorkian engineer Chris Blair"} + parseTIPL(tags) + Expect(tags["arranger"]).To(Equal([]string{"Andrew Powell"})) + Expect(tags["engineer"]).To(Equal([]string{"Chris Blair"})) + Expect(tags["djmixer"]).To(Equal([]string{"François Kevorkian"})) + }) + + It("handles multiple names for a single role", func() { + tags["tipl"] = []string{"engineer Pat Stapley producer Eric Woolfson engineer Chris Blair"} + parseTIPL(tags) + Expect(tags["producer"]).To(Equal([]string{"Eric Woolfson"})) + Expect(tags["engineer"]).To(ConsistOf("Pat Stapley", "Chris Blair")) + }) + }) + + Context("when the TIPL string is empty", func() { + It("does nothing", func() { + tags["tipl"] = []string{""} + parseTIPL(tags) + Expect(tags).To(BeEmpty()) + }) + }) + + Context("when the TIPL is not present", func() { + It("does nothing", func() { + parseTIPL(tags) + Expect(tags).To(BeEmpty()) + }) + }) + + // Add any additional edge cases if necessary + }) + }) diff --git a/scanner/metadata/taglib/taglib_wrapper.go b/scanner/metadata/taglib/taglib_wrapper.go index ec196ab6..81afe5fc 100644 --- a/scanner/metadata/taglib/taglib_wrapper.go +++ b/scanner/metadata/taglib/taglib_wrapper.go @@ -69,7 +69,7 @@ func Read(filename string) (tags map[string][]string, err error) { } var lock sync.RWMutex -var maps = make(map[uint32]map[string][]string) +var allMaps = make(map[uint32]map[string][]string) var mapsNextID uint32 func newMap() (id uint32, m map[string][]string) { @@ -78,14 +78,14 @@ func newMap() (id uint32, m map[string][]string) { id = mapsNextID mapsNextID++ m = make(map[string][]string) - maps[id] = m + allMaps[id] = m return } func deleteMap(id uint32) { lock.Lock() defer lock.Unlock() - delete(maps, id) + delete(allMaps, id) } //export go_map_put_m4a_str @@ -116,7 +116,7 @@ func do_put_map(id C.ulong, key string, val *C.char) { lock.RLock() defer lock.RUnlock() - m := maps[uint32(id)] + m := allMaps[uint32(id)] v := strings.TrimSpace(C.GoString(val)) m[key] = append(m[key], v) } @@ -151,7 +151,7 @@ func go_map_put_lyric_line(id C.ulong, lang *C.char, text *C.char, time C.int) { key := "lyrics-" + language - m := maps[uint32(id)] + m := allMaps[uint32(id)] existing, ok := m[key] if ok { existing[0] += formatted_line