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 }