Skip to content

Commit

Permalink
Parse the ID3v2.4 TIPL frame
Browse files Browse the repository at this point in the history
  • Loading branch information
deluan committed Jan 24, 2024
1 parent 1e5e8be commit a6fc84a
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 5 deletions.
49 changes: 49 additions & 0 deletions scanner/metadata/taglib/taglib.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 4,7 @@ import (
"errors"
"os"
"strconv"
"strings"

"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/scanner/metadata"
Expand Down Expand Up @@ -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{})
}
43 changes: 43 additions & 0 deletions scanner/metadata/taglib/taglib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 4,7 @@ import (
"io/fs"
"os"

"github.com/navidrome/navidrome/scanner/metadata"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
Expand Down Expand Up @@ -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
})

})
10 changes: 5 additions & 5 deletions scanner/metadata/taglib/taglib_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a6fc84a

Please sign in to comment.