Foundational work to enable multi-valued tags

This commit is contained in:
Deluan
2021-05-31 17:02:12 -04:00
parent 519c89345e
commit cd242695ba
9 changed files with 244 additions and 312 deletions
+9 -9
View File
@@ -25,7 +25,7 @@ func newMediaFileMapper(rootFolder string) *mediaFileMapper {
return &mediaFileMapper{rootFolder: rootFolder, policy: bluemonday.UGCPolicy()} return &mediaFileMapper{rootFolder: rootFolder, policy: bluemonday.UGCPolicy()}
} }
func (s *mediaFileMapper) toMediaFile(md metadata.Metadata) model.MediaFile { func (s *mediaFileMapper) toMediaFile(md *metadata.Tags) model.MediaFile {
mf := &model.MediaFile{} mf := &model.MediaFile{}
mf.ID = s.trackID(md) mf.ID = s.trackID(md)
mf.Title = s.mapTrackTitle(md) mf.Title = s.mapTrackTitle(md)
@@ -76,7 +76,7 @@ func sanitizeFieldForSorting(originalValue string) string {
return utils.NoArticle(v) return utils.NoArticle(v)
} }
func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string { func (s *mediaFileMapper) mapTrackTitle(md *metadata.Tags) string {
if md.Title() == "" { if md.Title() == "" {
s := strings.TrimPrefix(md.FilePath(), s.rootFolder+string(os.PathSeparator)) s := strings.TrimPrefix(md.FilePath(), s.rootFolder+string(os.PathSeparator))
e := filepath.Ext(s) e := filepath.Ext(s)
@@ -85,7 +85,7 @@ func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string {
return md.Title() return md.Title()
} }
func (s *mediaFileMapper) mapAlbumArtistName(md metadata.Metadata) string { func (s *mediaFileMapper) mapAlbumArtistName(md *metadata.Tags) string {
switch { switch {
case md.Compilation(): case md.Compilation():
return consts.VariousArtists return consts.VariousArtists
@@ -98,14 +98,14 @@ func (s *mediaFileMapper) mapAlbumArtistName(md metadata.Metadata) string {
} }
} }
func (s *mediaFileMapper) mapArtistName(md metadata.Metadata) string { func (s *mediaFileMapper) mapArtistName(md *metadata.Tags) string {
if md.Artist() != "" { if md.Artist() != "" {
return md.Artist() return md.Artist()
} }
return consts.UnknownArtist return consts.UnknownArtist
} }
func (s *mediaFileMapper) mapAlbumName(md metadata.Metadata) string { func (s *mediaFileMapper) mapAlbumName(md *metadata.Tags) string {
name := md.Album() name := md.Album()
if name == "" { if name == "" {
return "[Unknown Album]" return "[Unknown Album]"
@@ -113,19 +113,19 @@ func (s *mediaFileMapper) mapAlbumName(md metadata.Metadata) string {
return name return name
} }
func (s *mediaFileMapper) trackID(md metadata.Metadata) string { func (s *mediaFileMapper) trackID(md *metadata.Tags) string {
return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath()))) return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath())))
} }
func (s *mediaFileMapper) albumID(md metadata.Metadata) string { func (s *mediaFileMapper) albumID(md *metadata.Tags) string {
albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md))) albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md)))
return fmt.Sprintf("%x", md5.Sum([]byte(albumPath))) return fmt.Sprintf("%x", md5.Sum([]byte(albumPath)))
} }
func (s *mediaFileMapper) artistID(md metadata.Metadata) string { func (s *mediaFileMapper) artistID(md *metadata.Tags) string {
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapArtistName(md))))) return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapArtistName(md)))))
} }
func (s *mediaFileMapper) albumArtistID(md metadata.Metadata) string { func (s *mediaFileMapper) albumArtistID(md *metadata.Tags) string {
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md))))) return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(s.mapAlbumArtistName(md)))))
} }
+48 -51
View File
@@ -3,54 +3,37 @@ package metadata
import ( import (
"bufio" "bufio"
"errors" "errors"
"os"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time"
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
) )
type ffmpegMetadata struct {
baseMetadata
}
func (m *ffmpegMetadata) Duration() float32 { return m.parseDuration("duration") }
func (m *ffmpegMetadata) BitRate() int { return m.parseInt("bitrate") }
func (m *ffmpegMetadata) HasPicture() bool {
return m.getTag("has_picture", "metadata_block_picture") != ""
}
func (m *ffmpegMetadata) DiscNumber() (int, int) { return m.parseTuple("tpa", "disc", "discnumber") }
func (m *ffmpegMetadata) Comment() string {
comment := m.baseMetadata.Comment()
if comment == "Cover (front)" {
return ""
}
return comment
}
type ffmpegExtractor struct{} type ffmpegExtractor struct{}
func (e *ffmpegExtractor) Extract(files ...string) (map[string]Metadata, error) { func (e *ffmpegExtractor) Extract(files ...string) (map[string]*Tags, error) {
args := e.createProbeCommand(files) args := e.createProbeCommand(files)
log.Trace("Executing command", "args", args) log.Trace("Executing command", "args", args)
cmd := exec.Command(args[0], args[1:]...) // #nosec cmd := exec.Command(args[0], args[1:]...) // #nosec
output, _ := cmd.CombinedOutput() output, _ := cmd.CombinedOutput()
mds := map[string]Metadata{} fileTags := map[string]*Tags{}
if len(output) == 0 { if len(output) == 0 {
return mds, errors.New("error extracting metadata files") return fileTags, errors.New("error extracting metadata files")
} }
infos := e.parseOutput(string(output)) infos := e.parseOutput(string(output))
for file, info := range infos { for file, info := range infos {
md, err := e.extractMetadata(file, info) tags, err := e.extractMetadata(file, info)
// Skip files with errors // Skip files with errors
if err == nil { if err == nil {
mds[file] = md fileTags[file] = tags
} }
} }
return mds, nil return fileTags, nil
} }
var ( var (
@@ -95,26 +78,23 @@ func (e *ffmpegExtractor) parseOutput(output string) map[string]string {
return outputs return outputs
} }
func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*ffmpegMetadata, error) { func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*Tags, error) {
m := &ffmpegMetadata{} parsedTags := e.parseInfo(info)
m.filePath = filePath if len(parsedTags) == 0 {
m.tags = map[string]string{}
var err error
m.fileInfo, err = os.Stat(filePath)
if err != nil {
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
return nil, errors.New("error stating file")
}
m.parseInfo(info)
if len(m.tags) == 0 {
log.Trace("Not a media file. Skipping", "filePath", filePath) log.Trace("Not a media file. Skipping", "filePath", filePath)
return nil, errors.New("not a media file") return nil, errors.New("not a media file")
} }
return m, nil
tags := NewTag(filePath, parsedTags, map[string][]string{
"disc": {"tpa"},
"has_picture": {"metadata_block_picture"},
})
return tags, nil
} }
func (m *ffmpegMetadata) parseInfo(info string) { func (e *ffmpegExtractor) parseInfo(info string) map[string][]string {
tags := map[string][]string{}
reader := strings.NewReader(info) reader := strings.NewReader(info)
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
lastTag := "" lastTag := ""
@@ -128,11 +108,8 @@ func (m *ffmpegMetadata) parseInfo(info string) {
tagName := strings.TrimSpace(strings.ToLower(match[1])) tagName := strings.TrimSpace(strings.ToLower(match[1]))
if tagName != "" { if tagName != "" {
tagValue := strings.TrimSpace(match[2]) tagValue := strings.TrimSpace(match[2])
// Skip when the tag was previously found tags[tagName] = append(tags[tagName], tagValue)
if _, ok := m.tags[tagName]; !ok { lastTag = tagName
m.tags[tagName] = tagValue
lastTag = tagName
}
continue continue
} }
} }
@@ -140,8 +117,11 @@ func (m *ffmpegMetadata) parseInfo(info string) {
if lastTag != "" { if lastTag != "" {
match = continuationRx.FindStringSubmatch(line) match = continuationRx.FindStringSubmatch(line)
if len(match) > 0 { if len(match) > 0 {
tagValue := m.tags[lastTag] if tags[lastTag] == nil {
m.tags[lastTag] = tagValue + "\n" + strings.TrimSpace(match[1]) tags[lastTag] = []string{""}
}
tagValue := tags[lastTag][0]
tags[lastTag][0] = tagValue + "\n" + strings.TrimSpace(match[1])
continue continue
} }
} }
@@ -149,24 +129,41 @@ func (m *ffmpegMetadata) parseInfo(info string) {
lastTag = "" lastTag = ""
match = coverRx.FindStringSubmatch(line) match = coverRx.FindStringSubmatch(line)
if len(match) > 0 { if len(match) > 0 {
m.tags["has_picture"] = "true" tags["has_picture"] = []string{"true"}
continue continue
} }
match = durationRx.FindStringSubmatch(line) match = durationRx.FindStringSubmatch(line)
if len(match) > 0 { if len(match) > 0 {
m.tags["duration"] = match[1] tags["duration"] = []string{e.parseDuration(match[1])}
if len(match) > 1 { if len(match) > 1 {
m.tags["bitrate"] = match[2] tags["bitrate"] = []string{match[2]}
} }
continue continue
} }
match = bitRateRx.FindStringSubmatch(line) match = bitRateRx.FindStringSubmatch(line)
if len(match) > 0 { if len(match) > 0 {
m.tags["bitrate"] = match[2] tags["bitrate"] = []string{match[2]}
} }
} }
comment := tags["comment"]
if len(comment) > 0 && comment[0] == "Cover (front)" {
delete(tags, "comment")
}
return tags
}
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
func (e *ffmpegExtractor) parseDuration(tag string) string {
d, err := time.Parse("15:04:05", tag)
if err != nil {
return "0"
}
return strconv.FormatFloat(d.Sub(zeroTime).Seconds(), 'f', 2, 32)
} }
// Inputs will always be absolute paths // Inputs will always be absolute paths
+6 -7
View File
@@ -22,7 +22,6 @@ var _ = Describe("ffmpegExtractor", func() {
Expect(m.Album()).To(Equal("Album")) Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist")) Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist")) Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Composer()).To(Equal("Composer"))
Expect(m.Compilation()).To(BeTrue()) Expect(m.Compilation()).To(BeTrue())
Expect(m.Genre()).To(Equal("Rock")) Expect(m.Genre()).To(Equal("Rock"))
Expect(m.Year()).To(Equal(2014)) Expect(m.Year()).To(Equal(2014))
@@ -33,21 +32,21 @@ var _ = Describe("ffmpegExtractor", func() {
Expect(n).To(Equal(1)) Expect(n).To(Equal(1))
Expect(t).To(Equal(2)) Expect(t).To(Equal(2))
Expect(m.HasPicture()).To(BeTrue()) Expect(m.HasPicture()).To(BeTrue())
Expect(m.Duration()).To(Equal(1)) Expect(m.Duration()).To(BeNumerically("~", 1.03, 0.001))
Expect(m.BitRate()).To(Equal(476)) Expect(m.BitRate()).To(Equal(192))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.mp3")) Expect(m.FilePath()).To(Equal("tests/fixtures/test.mp3"))
Expect(m.Suffix()).To(Equal("mp3")) Expect(m.Suffix()).To(Equal("mp3"))
Expect(m.Size()).To(Equal(60845)) Expect(m.Size()).To(Equal(int64(51876)))
m = mds["tests/fixtures/test.ogg"] m = mds["tests/fixtures/test.ogg"]
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(m.Title()).To(BeEmpty()) Expect(m.Title()).To(BeEmpty())
Expect(m.HasPicture()).To(BeFalse()) Expect(m.HasPicture()).To(BeFalse())
Expect(m.Duration()).To(Equal(3)) Expect(m.Duration()).To(BeNumerically("~", 1.04, 0.001))
Expect(m.BitRate()).To(Equal(9)) Expect(m.BitRate()).To(Equal(16))
Expect(m.Suffix()).To(Equal("ogg")) Expect(m.Suffix()).To(Equal("ogg"))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg")) Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
Expect(m.Size()).To(Equal(4408)) Expect(m.Size()).To(Equal(int64(5065)))
}) })
}) })
+134 -179
View File
@@ -16,10 +16,10 @@ import (
) )
type Extractor interface { type Extractor interface {
Extract(files ...string) (map[string]Metadata, error) Extract(files ...string) (map[string]*Tags, error)
} }
func Extract(files ...string) (map[string]Metadata, error) { func Extract(files ...string) (map[string]*Tags, error) {
var e Extractor var e Extractor
switch conf.Server.Scanner.Extractor { switch conf.Server.Scanner.Extractor {
@@ -35,167 +35,97 @@ func Extract(files ...string) (map[string]Metadata, error) {
return e.Extract(files...) return e.Extract(files...)
} }
type Metadata interface { type Tags struct {
Title() string
Album() string
Artist() string
AlbumArtist() string
SortTitle() string
SortAlbum() string
SortArtist() string
SortAlbumArtist() string
Composer() string
Genre() string
Year() int
TrackNumber() (int, int)
DiscNumber() (int, int)
DiscSubtitle() string
HasPicture() bool
Comment() string
Lyrics() string
Compilation() bool
CatalogNum() string
MbzTrackID() string
MbzAlbumID() string
MbzArtistID() string
MbzAlbumArtistID() string
MbzAlbumType() string
MbzAlbumComment() string
Duration() float32
BitRate() int
ModificationTime() time.Time
FilePath() string
Suffix() string
Size() int64
Bpm() int
}
type baseMetadata struct {
filePath string filePath string
suffix string
fileInfo os.FileInfo fileInfo os.FileInfo
tags map[string]string tags map[string][]string
custom map[string][]string
} }
func (m *baseMetadata) Title() string { return m.getTag("title", "sort_name", "titlesort") } func NewTag(filePath string, tags, custom map[string][]string) *Tags {
func (m *baseMetadata) Album() string { return m.getTag("album", "sort_album", "albumsort") } fileInfo, err := os.Stat(filePath)
func (m *baseMetadata) Artist() string { return m.getTag("artist", "sort_artist", "artistsort") } if err != nil {
func (m *baseMetadata) AlbumArtist() string { log.Warn("Error stating file. Skipping", "filePath", filePath, err)
return m.getTag("album_artist", "album artist", "albumartist") return nil
} }
func (m *baseMetadata) SortTitle() string { return m.getSortTag("", "title", "name") }
func (m *baseMetadata) SortAlbum() string { return m.getSortTag("", "album") }
func (m *baseMetadata) SortArtist() string { return m.getSortTag("", "artist") }
func (m *baseMetadata) SortAlbumArtist() string {
return m.getSortTag("tso2", "albumartist", "album_artist")
}
func (m *baseMetadata) Composer() string { return m.getTag("composer", "tcm", "sort_composer") }
func (m *baseMetadata) Genre() string { return m.getTag("genre") }
func (m *baseMetadata) Year() int { return m.parseYear("date") }
func (m *baseMetadata) Comment() string { return m.getTag("comment") }
func (m *baseMetadata) Lyrics() string { return m.getTag("lyrics", "lyrics-eng") }
func (m *baseMetadata) Compilation() bool { return m.parseBool("tcmp", "compilation") }
func (m *baseMetadata) TrackNumber() (int, int) { return m.parseTuple("track", "tracknumber") }
func (m *baseMetadata) DiscNumber() (int, int) { return m.parseTuple("disc", "discnumber") }
func (m *baseMetadata) DiscSubtitle() string {
return m.getTag("tsst", "discsubtitle", "setsubtitle")
}
func (m *baseMetadata) CatalogNum() string { return m.getTag("catalognumber") }
func (m *baseMetadata) MbzTrackID() string {
return m.getMbzID("musicbrainz_trackid", "musicbrainz track id")
}
func (m *baseMetadata) MbzAlbumID() string {
return m.getMbzID("musicbrainz_albumid", "musicbrainz album id")
}
func (m *baseMetadata) MbzArtistID() string {
return m.getMbzID("musicbrainz_artistid", "musicbrainz artist id")
}
func (m *baseMetadata) MbzAlbumArtistID() string {
return m.getMbzID("musicbrainz_albumartistid", "musicbrainz album artist id")
}
func (m *baseMetadata) MbzAlbumType() string {
return m.getTag("musicbrainz_albumtype", "musicbrainz album type")
}
func (m *baseMetadata) MbzAlbumComment() string {
return m.getTag("musicbrainz_albumcomment", "musicbrainz album comment")
}
func (m *baseMetadata) ModificationTime() time.Time { return m.fileInfo.ModTime() } return &Tags{
func (m *baseMetadata) Size() int64 { return m.fileInfo.Size() } filePath: filePath,
func (m *baseMetadata) FilePath() string { return m.filePath } suffix: strings.ToLower(strings.TrimPrefix(path.Ext(filePath), ".")),
func (m *baseMetadata) Suffix() string { fileInfo: fileInfo,
return strings.ToLower(strings.TrimPrefix(path.Ext(m.FilePath()), ".")) tags: tags,
} custom: custom,
func (m *baseMetadata) Duration() float32 { panic("not implemented") }
func (m *baseMetadata) BitRate() int { panic("not implemented") }
func (m *baseMetadata) HasPicture() bool { panic("not implemented") }
func (m *baseMetadata) Bpm() int {
var bpmStr = m.getTag("tbpm", "bpm", "fbpm")
var bpmFloat, err = strconv.ParseFloat(bpmStr, 64)
if err == nil {
return (int)(math.Round(bpmFloat))
} else {
return 0
} }
} }
func (m *baseMetadata) parseInt(tagName string) int { // Common tags
if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(v) func (t *Tags) Title() string { return t.getTag("title", "sort_name", "titlesort") }
return i func (t *Tags) Album() string { return t.getTag("album", "sort_album", "albumsort") }
} func (t *Tags) Artist() string { return t.getTag("artist", "sort_artist", "artistsort") }
return 0 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")
} }
func (m *baseMetadata) parseFloat(tagName string) float32 { // File properties
if v, ok := m.tags[tagName]; ok {
f, _ := strconv.ParseFloat(v, 32)
return float32(f)
}
return 0
}
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`) 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 (m *baseMetadata) parseYear(tags ...string) int { func (t *Tags) getTags(tags ...string) []string {
for _, t := range tags { allTags := append(tags, t.custom[tags[0]]...)
if v, ok := m.tags[t]; ok { for _, tag := range allTags {
match := dateRegex.FindStringSubmatch(v) if v, ok := t.tags[tag]; ok {
if len(match) == 0 {
log.Warn("Error parsing year date field", "file", m.filePath, "date", v)
return 0
}
year, _ := strconv.Atoi(match[1])
return year
}
}
return 0
}
func (m *baseMetadata) getMbzID(tags ...string) string {
var value string
for _, t := range tags {
if v, ok := m.tags[t]; ok {
value = v
break
}
}
if _, err := uuid.Parse(value); err != nil {
return ""
}
return value
}
func (m *baseMetadata) getTag(tags ...string) string {
for _, t := range tags {
if v, ok := m.tags[t]; ok {
return v return v
} }
} }
return nil
}
func (t *Tags) getTag(tags ...string) string {
ts := t.getTags(tags...)
if len(ts) > 0 {
return ts[0]
}
return "" return ""
} }
func (m *baseMetadata) getSortTag(originalTag string, tags ...string) string { func (t *Tags) getSortTag(originalTag string, tags ...string) string {
formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"} formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"}
all := []string{originalTag} all := []string{originalTag}
for _, tag := range tags { for _, tag := range tags {
@@ -204,45 +134,70 @@ func (m *baseMetadata) getSortTag(originalTag string, tags ...string) string {
all = append(all, name) all = append(all, name)
} }
} }
return m.getTag(all...) return t.getTag(all...)
} }
func (m *baseMetadata) parseTuple(tags ...string) (int, int) { var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
for _, tagName := range tags {
if v, ok := m.tags[tagName]; ok { func (t *Tags) getYear(tags ...string) int {
tuple := strings.Split(v, "/") tag := t.getTag(tags...)
t1, t2 := 0, 0 if tag == "" {
t1, _ = strconv.Atoi(tuple[0]) return 0
if len(tuple) > 1 {
t2, _ = strconv.Atoi(tuple[1])
} else {
t2, _ = strconv.Atoi(m.tags[tagName+"total"])
}
return t1, t2
}
} }
return 0, 0 match := dateRegex.FindStringSubmatch(tag)
} if len(match) == 0 {
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
func (m *baseMetadata) parseBool(tags ...string) bool { return 0
for _, tagName := range tags {
if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(strings.TrimSpace(v))
return i == 1
}
} }
return false year, _ := strconv.Atoi(match[1])
return year
} }
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC) func (t *Tags) getBool(tags ...string) bool {
tag := t.getTag(tags...)
func (m *baseMetadata) parseDuration(tagName string) float32 { if tag == "" {
if v, ok := m.tags[tagName]; ok { return false
d, err := time.Parse("15:04:05", v)
if err != nil {
return 0
}
return float32(d.Sub(zeroTime).Seconds())
} }
return 0 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
} }
+18 -18
View File
@@ -5,8 +5,8 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("ffmpegMetadata", func() { var _ = Describe("Tags", func() {
Describe("parseYear", func() { Describe("getYear", func() {
It("parses the year correctly", func() { It("parses the year correctly", func() {
var examples = map[string]int{ var examples = map[string]int{
"1985": 1985, "1985": 1985,
@@ -19,27 +19,27 @@ var _ = Describe("ffmpegMetadata", func() {
"01/10/1990": 1990, "01/10/1990": 1990,
} }
for tag, expected := range examples { for tag, expected := range examples {
md := &baseMetadata{} md := &Tags{}
md.tags = map[string]string{"date": tag} md.tags = map[string][]string{"date": {tag}}
Expect(md.Year()).To(Equal(expected)) Expect(md.Year()).To(Equal(expected))
} }
}) })
It("returns 0 if year is invalid", func() { It("returns 0 if year is invalid", func() {
md := &baseMetadata{} md := &Tags{}
md.tags = map[string]string{"date": "invalid"} md.tags = map[string][]string{"date": {"invalid"}}
Expect(md.Year()).To(Equal(0)) Expect(md.Year()).To(Equal(0))
}) })
}) })
Describe("getMbzID", func() { Describe("getMbzID", func() {
It("return a valid MBID", func() { It("return a valid MBID", func() {
md := &baseMetadata{} md := &Tags{}
md.tags = map[string]string{ md.tags = map[string][]string{
"musicbrainz_trackid": "8f84da07-09a0-477b-b216-cc982dabcde1", "musicbrainz_trackid": {"8f84da07-09a0-477b-b216-cc982dabcde1"},
"musicbrainz_albumid": "f68c985d-f18b-4f4a-b7f0-87837cf3fbf9", "musicbrainz_albumid": {"f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"},
"musicbrainz_artistid": "89ad4ac3-39f7-470e-963a-56509c546377", "musicbrainz_artistid": {"89ad4ac3-39f7-470e-963a-56509c546377"},
"musicbrainz_albumartistid": "ada7a83c-e3e1-40f1-93f9-3e73dbc9298a", "musicbrainz_albumartistid": {"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"},
} }
Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1")) Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9")) Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"))
@@ -47,12 +47,12 @@ var _ = Describe("ffmpegMetadata", func() {
Expect(md.MbzAlbumArtistID()).To(Equal("ada7a83c-e3e1-40f1-93f9-3e73dbc9298a")) Expect(md.MbzAlbumArtistID()).To(Equal("ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"))
}) })
It("return empty string for invalid MBID", func() { It("return empty string for invalid MBID", func() {
md := &baseMetadata{} md := &Tags{}
md.tags = map[string]string{ md.tags = map[string][]string{
"musicbrainz_trackid": "11406732-6", "musicbrainz_trackid": {"11406732-6"},
"musicbrainz_albumid": "11406732", "musicbrainz_albumid": {"11406732"},
"musicbrainz_artistid": "200455", "musicbrainz_artistid": {"200455"},
"musicbrainz_albumartistid": "194", "musicbrainz_albumartistid": {"194"},
} }
Expect(md.MbzTrackID()).To(Equal("")) Expect(md.MbzTrackID()).To(Equal(""))
Expect(md.MbzAlbumID()).To(Equal("")) Expect(md.MbzAlbumID()).To(Equal(""))
+22 -38
View File
@@ -1,7 +1,6 @@
package metadata package metadata
import ( import (
"errors"
"os" "os"
"github.com/dhowden/tag" "github.com/dhowden/tag"
@@ -9,51 +8,39 @@ import (
"github.com/navidrome/navidrome/scanner/metadata/taglib" "github.com/navidrome/navidrome/scanner/metadata/taglib"
) )
type taglibMetadata struct {
baseMetadata
hasPicture bool
}
func (m *taglibMetadata) Title() string { return m.getTag("title", "titlesort", "_track") }
func (m *taglibMetadata) Album() string { return m.getTag("album", "albumsort", "_album") }
func (m *taglibMetadata) Artist() string { return m.getTag("artist", "artistsort", "_artist") }
func (m *taglibMetadata) Genre() string { return m.getTag("genre", "_genre") }
func (m *taglibMetadata) Year() int { return m.parseYear("date", "_year") }
func (m *taglibMetadata) TrackNumber() (int, int) {
return m.parseTuple("track", "tracknumber", "_track")
}
func (m *taglibMetadata) Duration() float32 { return m.parseFloat("length") }
func (m *taglibMetadata) BitRate() int { return m.parseInt("bitrate") }
func (m *taglibMetadata) HasPicture() bool { return m.hasPicture }
type taglibExtractor struct{} type taglibExtractor struct{}
func (e *taglibExtractor) Extract(paths ...string) (map[string]Metadata, error) { func (e *taglibExtractor) Extract(paths ...string) (map[string]*Tags, error) {
mds := map[string]Metadata{} fileTags := map[string]*Tags{}
for _, path := range paths { for _, path := range paths {
md, err := e.extractMetadata(path) tags, err := e.extractMetadata(path)
if err == nil { if err == nil {
mds[path] = md fileTags[path] = tags
} }
} }
return mds, nil return fileTags, nil
} }
func (e *taglibExtractor) extractMetadata(filePath string) (*taglibMetadata, error) { func (e *taglibExtractor) extractMetadata(filePath string) (*Tags, error) {
var err error parsedTags, err := taglib.Read(filePath)
md := &taglibMetadata{}
md.filePath = filePath
md.fileInfo, err = os.Stat(filePath)
if err != nil {
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
return nil, errors.New("error stating file")
}
md.tags, err = taglib.Read(filePath)
if err != nil { if err != nil {
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err) log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
} }
md.hasPicture = hasEmbeddedImage(filePath) if hasEmbeddedImage(filePath) {
return md, nil parsedTags["has_picture"] = []string{"true"}
}
tags := NewTag(filePath, parsedTags, map[string][]string{
"title": {"_track", "titlesort"},
"album": {"_album", "albumsort"},
"artist": {"_artist", "artistsort"},
"genre": {"_genre"},
"date": {"_year"},
"track": {"_track"},
"duration": {"length"},
})
return tags, nil
} }
func hasEmbeddedImage(path string) bool { func hasEmbeddedImage(path string) bool {
@@ -77,6 +64,3 @@ func hasEmbeddedImage(path string) bool {
return m.Picture() != nil return m.Picture() != nil
} }
var _ Metadata = (*taglibMetadata)(nil)
var _ Extractor = (*taglibExtractor)(nil)
+6 -8
View File
@@ -20,7 +20,7 @@ import (
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
) )
func Read(filename string) (map[string]string, error) { func Read(filename string) (map[string][]string, error) {
fp := C.CString(filename) fp := C.CString(filename)
defer C.free(unsafe.Pointer(fp)) defer C.free(unsafe.Pointer(fp))
id, m := newMap() id, m := newMap()
@@ -44,15 +44,15 @@ func Read(filename string) (map[string]string, error) {
} }
var lock sync.RWMutex var lock sync.RWMutex
var maps = make(map[uint32]map[string]string) var maps = 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) {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
id = mapsNextID id = mapsNextID
mapsNextID++ mapsNextID++
m = make(map[string]string) m = make(map[string][]string)
maps[id] = m maps[id] = m
return return
} }
@@ -69,10 +69,8 @@ func go_map_put_str(id C.ulong, key *C.char, val *C.char) {
defer lock.RUnlock() defer lock.RUnlock()
m := maps[uint32(id)] m := maps[uint32(id)]
k := strings.ToLower(C.GoString(key)) k := strings.ToLower(C.GoString(key))
if _, ok := m[k]; !ok { v := strings.TrimSpace(C.GoString(val))
v := strings.TrimSpace(C.GoString(val)) m[k] = append(m[k], v)
m[k] = v
}
} }
//export go_map_put_int //export go_map_put_int
-1
View File
@@ -18,7 +18,6 @@ var _ = Describe("taglibExtractor", func() {
Expect(m.Album()).To(Equal("Album")) Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist")) Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist")) Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Composer()).To(Equal("Composer"))
Expect(m.Compilation()).To(BeTrue()) Expect(m.Compilation()).To(BeTrue())
Expect(m.Genre()).To(Equal("Rock")) Expect(m.Genre()).To(Equal("Rock"))
Expect(m.Year()).To(Equal(2014)) Expect(m.Year()).To(Equal(2014))
+1 -1
View File
@@ -51,7 +51,7 @@ const (
filesBatchSize = 100 filesBatchSize = 100
) )
// TagScanner algorithm overview: // Scan algorithm overview:
// Load all directories from the DB // Load all directories from the DB
// Traverse the music folder, collecting each subfolder's ModTime (self or any non-dir children, whichever is newer) // Traverse the music folder, collecting each subfolder's ModTime (self or any non-dir children, whichever is newer)
// For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file: // For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file: