mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +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"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/scanner/metadata"
|
"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)}
|
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
|
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() {
|
func init() {
|
||||||
metadata.RegisterExtractor(ExtractorID, &Extractor{})
|
metadata.RegisterExtractor(ExtractorID, &Extractor{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/scanner/metadata"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "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 lock sync.RWMutex
|
||||||
var maps = make(map[uint32]map[string][]string)
|
var allMaps = make(map[uint32]map[string][]string)
|
||||||
var mapsNextID uint32
|
var mapsNextID uint32
|
||||||
|
|
||||||
func newMap() (id uint32, m map[string][]string) {
|
func newMap() (id uint32, m map[string][]string) {
|
||||||
|
@ -78,14 +78,14 @@ func newMap() (id uint32, m map[string][]string) {
|
||||||
id = mapsNextID
|
id = mapsNextID
|
||||||
mapsNextID++
|
mapsNextID++
|
||||||
m = make(map[string][]string)
|
m = make(map[string][]string)
|
||||||
maps[id] = m
|
allMaps[id] = m
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteMap(id uint32) {
|
func deleteMap(id uint32) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
delete(maps, id)
|
delete(allMaps, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export go_map_put_m4a_str
|
//export go_map_put_m4a_str
|
||||||
|
@ -116,7 +116,7 @@ func do_put_map(id C.ulong, key string, val *C.char) {
|
||||||
|
|
||||||
lock.RLock()
|
lock.RLock()
|
||||||
defer lock.RUnlock()
|
defer lock.RUnlock()
|
||||||
m := maps[uint32(id)]
|
m := allMaps[uint32(id)]
|
||||||
v := strings.TrimSpace(C.GoString(val))
|
v := strings.TrimSpace(C.GoString(val))
|
||||||
m[key] = append(m[key], v)
|
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
|
key := "lyrics-" + language
|
||||||
|
|
||||||
m := maps[uint32(id)]
|
m := allMaps[uint32(id)]
|
||||||
existing, ok := m[key]
|
existing, ok := m[key]
|
||||||
if ok {
|
if ok {
|
||||||
existing[0] += formatted_line
|
existing[0] += formatted_line
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue