mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
Parse the ID3v2.4 TIPL frame
This commit is contained in:
parent
1e5e8be192
commit
a6fc84a2e1
3 changed files with 97 additions and 5 deletions
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue