mirror of
https://github.com/navidrome/navidrome.git
synced 2025-08-10 00:52:20 +00:00
* chore: update cross-taglib * fix(taglib): add logging for TagLib version Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
package taglib
|
|
|
|
import (
|
|
"io/fs"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/storage/local"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model/metadata"
|
|
)
|
|
|
|
type extractor struct {
|
|
baseDir string
|
|
}
|
|
|
|
func (e extractor) Parse(files ...string) (map[string]metadata.Info, error) {
|
|
results := make(map[string]metadata.Info)
|
|
for _, path := range files {
|
|
props, err := e.extractMetadata(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
results[path] = *props
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (e extractor) Version() string {
|
|
return Version()
|
|
}
|
|
|
|
func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) {
|
|
fullPath := filepath.Join(e.baseDir, filePath)
|
|
tags, err := Read(fullPath)
|
|
if err != nil {
|
|
log.Warn("extractor: Error reading metadata from file. Skipping", "filePath", fullPath, err)
|
|
return nil, err
|
|
}
|
|
|
|
// Parse audio properties
|
|
ap := metadata.AudioProperties{}
|
|
if length, ok := tags["_lengthinmilliseconds"]; ok && len(length) > 0 {
|
|
millis, _ := strconv.Atoi(length[0])
|
|
if millis > 0 {
|
|
ap.Duration = (time.Millisecond * time.Duration(millis)).Round(time.Millisecond * 10)
|
|
}
|
|
delete(tags, "_lengthinmilliseconds")
|
|
}
|
|
parseProp := func(prop string, target *int) {
|
|
if value, ok := tags[prop]; ok && len(value) > 0 {
|
|
*target, _ = strconv.Atoi(value[0])
|
|
delete(tags, prop)
|
|
}
|
|
}
|
|
parseProp("_bitrate", &ap.BitRate)
|
|
parseProp("_channels", &ap.Channels)
|
|
parseProp("_samplerate", &ap.SampleRate)
|
|
parseProp("_bitspersample", &ap.BitDepth)
|
|
|
|
// Parse track/disc totals
|
|
parseTuple := func(prop string) {
|
|
tagName := prop + "number"
|
|
tagTotal := prop + "total"
|
|
if value, ok := tags[tagName]; ok && len(value) > 0 {
|
|
parts := strings.Split(value[0], "/")
|
|
tags[tagName] = []string{parts[0]}
|
|
if len(parts) == 2 {
|
|
tags[tagTotal] = []string{parts[1]}
|
|
}
|
|
}
|
|
}
|
|
parseTuple("track")
|
|
parseTuple("disc")
|
|
|
|
// Adjust some ID3 tags
|
|
parseLyrics(tags)
|
|
parseTIPL(tags)
|
|
delete(tags, "tmcl") // TMCL is already parsed by TagLib
|
|
|
|
return &metadata.Info{
|
|
Tags: tags,
|
|
AudioProperties: ap,
|
|
HasPicture: tags["has_picture"] != nil && len(tags["has_picture"]) > 0 && tags["has_picture"][0] == "true",
|
|
}, nil
|
|
}
|
|
|
|
// parseLyrics make sure lyrics tags have language
|
|
func parseLyrics(tags map[string][]string) {
|
|
lyrics := tags["lyrics"]
|
|
if len(lyrics) > 0 {
|
|
tags["lyrics:xxx"] = lyrics
|
|
delete(tags, "lyrics")
|
|
}
|
|
}
|
|
|
|
// 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 map[string][]string) {
|
|
tipl := tags["tipl"]
|
|
if len(tipl) == 0 {
|
|
return
|
|
}
|
|
|
|
addRole := func(currentRole string, currentValue []string) {
|
|
if currentRole != "" && len(currentValue) > 0 {
|
|
role := tiplMapping[currentRole]
|
|
tags[role] = append(tags[role], strings.Join(currentValue, " "))
|
|
}
|
|
}
|
|
|
|
var currentRole string
|
|
var currentValue []string
|
|
for _, part := range strings.Split(tipl[0], " ") {
|
|
if _, ok := tiplMapping[part]; ok {
|
|
addRole(currentRole, currentValue)
|
|
currentRole = part
|
|
currentValue = nil
|
|
continue
|
|
}
|
|
currentValue = append(currentValue, part)
|
|
}
|
|
addRole(currentRole, currentValue)
|
|
delete(tags, "tipl")
|
|
}
|
|
|
|
var _ local.Extractor = (*extractor)(nil)
|
|
|
|
func init() {
|
|
local.RegisterExtractor("taglib", func(_ fs.FS, baseDir string) local.Extractor {
|
|
// ignores fs, as taglib extractor only works with local files
|
|
return &extractor{baseDir}
|
|
})
|
|
conf.AddHook(func() {
|
|
log.Debug("TagLib version", "version", Version())
|
|
})
|
|
}
|