navidrome/scanner/metadata/metadata.go

204 lines
5.9 KiB
Go

package metadata
import (
"fmt"
"math"
"os"
"path"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
type Extractor interface {
Extract(files ...string) (map[string]*Tags, error)
}
func Extract(files ...string) (map[string]*Tags, error) {
var e Extractor
switch conf.Server.Scanner.Extractor {
case "taglib":
e = &taglibExtractor{}
case "ffmpeg":
e = &ffmpegExtractor{}
default:
log.Warn("Invalid Scanner.Extractor option. Using default ffmpeg", "requested", conf.Server.Scanner.Extractor,
"validOptions", "ffmpeg,taglib")
e = &ffmpegExtractor{}
}
return e.Extract(files...)
}
type Tags struct {
filePath string
suffix string
fileInfo os.FileInfo
tags map[string][]string
custom map[string][]string
}
func NewTag(filePath string, tags, custom map[string][]string) *Tags {
fileInfo, err := os.Stat(filePath)
if err != nil {
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
return nil
}
return &Tags{
filePath: filePath,
suffix: strings.ToLower(strings.TrimPrefix(path.Ext(filePath), ".")),
fileInfo: fileInfo,
tags: tags,
custom: custom,
}
}
// Common tags
func (t *Tags) Title() string { return t.getTag("title", "sort_name", "titlesort") }
func (t *Tags) Album() string { return t.getTag("album", "sort_album", "albumsort") }
func (t *Tags) Artist() string { return t.getTag("artist", "sort_artist", "artistsort") }
func (t *Tags) AlbumArtist() string { return t.getTag("album_artist", "album artist", "albumartist") }
func (t *Tags) SortTitle() string { return t.getSortTag("", "title", "name") }
func (t *Tags) SortAlbum() string { return t.getSortTag("", "album") }
func (t *Tags) SortArtist() string { return t.getSortTag("", "artist") }
func (t *Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") }
func (t *Tags) Genre() string { return t.getTag("genre") }
func (t *Tags) Year() int { return t.getYear("date") }
func (t *Tags) Comment() string { return t.getTag("comment") }
func (t *Tags) Lyrics() string { return t.getTag("lyrics", "lyrics-eng") }
func (t *Tags) Compilation() bool { return t.getBool("tcmp", "compilation") }
func (t *Tags) TrackNumber() (int, int) { return t.getTuple("track", "tracknumber") }
func (t *Tags) DiscNumber() (int, int) { return t.getTuple("disc", "discnumber") }
func (t *Tags) DiscSubtitle() string { return t.getTag("tsst", "discsubtitle", "setsubtitle") }
func (t *Tags) CatalogNum() string { return t.getTag("catalognumber") }
func (t *Tags) Bpm() int { return (int)(math.Round(t.getFloat("tbpm", "bpm", "fbpm"))) }
func (t *Tags) HasPicture() bool { return t.getTag("has_picture") != "" }
// MusicBrainz Identifiers
func (t *Tags) MbzTrackID() string { return t.getMbzID("musicbrainz_trackid", "musicbrainz track id") }
func (t *Tags) MbzAlbumID() string { return t.getMbzID("musicbrainz_albumid", "musicbrainz album id") }
func (t *Tags) MbzArtistID() string {
return t.getMbzID("musicbrainz_artistid", "musicbrainz artist id")
}
func (t *Tags) MbzAlbumArtistID() string {
return t.getMbzID("musicbrainz_albumartistid", "musicbrainz album artist id")
}
func (t *Tags) MbzAlbumType() string {
return t.getTag("musicbrainz_albumtype", "musicbrainz album type")
}
func (t *Tags) MbzAlbumComment() string {
return t.getTag("musicbrainz_albumcomment", "musicbrainz album comment")
}
// File properties
func (t *Tags) Duration() float32 { return float32(t.getFloat("duration")) }
func (t *Tags) BitRate() int { return t.getInt("bitrate") }
func (t *Tags) ModificationTime() time.Time { return t.fileInfo.ModTime() }
func (t *Tags) Size() int64 { return t.fileInfo.Size() }
func (t *Tags) FilePath() string { return t.filePath }
func (t *Tags) Suffix() string { return t.suffix }
func (t *Tags) getTags(tags ...string) []string {
allTags := append(tags, t.custom[tags[0]]...)
for _, tag := range allTags {
if v, ok := t.tags[tag]; ok {
return v
}
}
return nil
}
func (t *Tags) getTag(tags ...string) string {
ts := t.getTags(tags...)
if len(ts) > 0 {
return ts[0]
}
return ""
}
func (t *Tags) getSortTag(originalTag string, tags ...string) string {
formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"}
all := []string{originalTag}
for _, tag := range tags {
for _, format := range formats {
name := fmt.Sprintf(format, tag)
all = append(all, name)
}
}
return t.getTag(all...)
}
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
func (t *Tags) getYear(tags ...string) int {
tag := t.getTag(tags...)
if tag == "" {
return 0
}
match := dateRegex.FindStringSubmatch(tag)
if len(match) == 0 {
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
return 0
}
year, _ := strconv.Atoi(match[1])
return year
}
func (t *Tags) getBool(tags ...string) bool {
tag := t.getTag(tags...)
if tag == "" {
return false
}
i, _ := strconv.Atoi(strings.TrimSpace(tag))
return i == 1
}
func (t *Tags) getTuple(tags ...string) (int, int) {
tag := t.getTag(tags...)
if tag == "" {
return 0, 0
}
tuple := strings.Split(tag, "/")
t1, t2 := 0, 0
t1, _ = strconv.Atoi(tuple[0])
if len(tuple) > 1 {
t2, _ = strconv.Atoi(tuple[1])
} else {
t2tag := t.getTag(tags[0] + "total")
t2, _ = strconv.Atoi(t2tag)
}
return t1, t2
}
func (t *Tags) getMbzID(tags ...string) string {
tag := t.getTag(tags...)
if _, err := uuid.Parse(tag); err != nil {
return ""
}
return tag
}
func (t *Tags) getInt(tags ...string) int {
tag := t.getTag(tags...)
i, _ := strconv.Atoi(tag)
return i
}
func (t *Tags) getFloat(tags ...string) float64 {
var tag = t.getTag(tags...)
var value, err = strconv.ParseFloat(tag, 64)
if err != nil {
return 0
}
return value
}