Parse the ID3v2.4 TIPL frame

This commit is contained in:
Deluan 2024-01-23 20:50:43 -05:00
parent 1e5e8be192
commit a6fc84a2e1
3 changed files with 97 additions and 5 deletions

View File

@ -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{})
}

View File

@ -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
})
})

View File

@ -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