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

View File

@ -25,7 +25,7 @@ func newMediaFileMapper(rootFolder string) *mediaFileMapper {
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.ID = s.trackID(md)
mf.Title = s.mapTrackTitle(md)
@ -76,7 +76,7 @@ func sanitizeFieldForSorting(originalValue string) string {
return utils.NoArticle(v)
}
func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string {
func (s *mediaFileMapper) mapTrackTitle(md *metadata.Tags) string {
if md.Title() == "" {
s := strings.TrimPrefix(md.FilePath(), s.rootFolder+string(os.PathSeparator))
e := filepath.Ext(s)
@ -85,7 +85,7 @@ func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string {
return md.Title()
}
func (s *mediaFileMapper) mapAlbumArtistName(md metadata.Metadata) string {
func (s *mediaFileMapper) mapAlbumArtistName(md *metadata.Tags) string {
switch {
case md.Compilation():
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() != "" {
return md.Artist()
}
return consts.UnknownArtist
}
func (s *mediaFileMapper) mapAlbumName(md metadata.Metadata) string {
func (s *mediaFileMapper) mapAlbumName(md *metadata.Tags) string {
name := md.Album()
if name == "" {
return "[Unknown Album]"
@ -113,19 +113,19 @@ func (s *mediaFileMapper) mapAlbumName(md metadata.Metadata) string {
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())))
}
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)))
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)))))
}
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)))))
}

View File

@ -3,54 +3,37 @@ package metadata
import (
"bufio"
"errors"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/navidrome/navidrome/conf"
"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{}
func (e *ffmpegExtractor) Extract(files ...string) (map[string]Metadata, error) {
func (e *ffmpegExtractor) Extract(files ...string) (map[string]*Tags, error) {
args := e.createProbeCommand(files)
log.Trace("Executing command", "args", args)
cmd := exec.Command(args[0], args[1:]...) // #nosec
output, _ := cmd.CombinedOutput()
mds := map[string]Metadata{}
fileTags := map[string]*Tags{}
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))
for file, info := range infos {
md, err := e.extractMetadata(file, info)
tags, err := e.extractMetadata(file, info)
// Skip files with errors
if err == nil {
mds[file] = md
fileTags[file] = tags
}
}
return mds, nil
return fileTags, nil
}
var (
@ -95,26 +78,23 @@ func (e *ffmpegExtractor) parseOutput(output string) map[string]string {
return outputs
}
func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*ffmpegMetadata, error) {
m := &ffmpegMetadata{}
m.filePath = filePath
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 {
func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*Tags, error) {
parsedTags := e.parseInfo(info)
if len(parsedTags) == 0 {
log.Trace("Not a media file. Skipping", "filePath", filePath)
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)
scanner := bufio.NewScanner(reader)
lastTag := ""
@ -128,11 +108,8 @@ func (m *ffmpegMetadata) parseInfo(info string) {
tagName := strings.TrimSpace(strings.ToLower(match[1]))
if tagName != "" {
tagValue := strings.TrimSpace(match[2])
// Skip when the tag was previously found
if _, ok := m.tags[tagName]; !ok {
m.tags[tagName] = tagValue
lastTag = tagName
}
tags[tagName] = append(tags[tagName], tagValue)
lastTag = tagName
continue
}
}
@ -140,8 +117,11 @@ func (m *ffmpegMetadata) parseInfo(info string) {
if lastTag != "" {
match = continuationRx.FindStringSubmatch(line)
if len(match) > 0 {
tagValue := m.tags[lastTag]
m.tags[lastTag] = tagValue + "\n" + strings.TrimSpace(match[1])
if tags[lastTag] == nil {
tags[lastTag] = []string{""}
}
tagValue := tags[lastTag][0]
tags[lastTag][0] = tagValue + "\n" + strings.TrimSpace(match[1])
continue
}
}
@ -149,24 +129,41 @@ func (m *ffmpegMetadata) parseInfo(info string) {
lastTag = ""
match = coverRx.FindStringSubmatch(line)
if len(match) > 0 {
m.tags["has_picture"] = "true"
tags["has_picture"] = []string{"true"}
continue
}
match = durationRx.FindStringSubmatch(line)
if len(match) > 0 {
m.tags["duration"] = match[1]
tags["duration"] = []string{e.parseDuration(match[1])}
if len(match) > 1 {
m.tags["bitrate"] = match[2]
tags["bitrate"] = []string{match[2]}
}
continue
}
match = bitRateRx.FindStringSubmatch(line)
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

View File

@ -22,7 +22,6 @@ var _ = Describe("ffmpegExtractor", func() {
Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Composer()).To(Equal("Composer"))
Expect(m.Compilation()).To(BeTrue())
Expect(m.Genre()).To(Equal("Rock"))
Expect(m.Year()).To(Equal(2014))
@ -33,21 +32,21 @@ var _ = Describe("ffmpegExtractor", func() {
Expect(n).To(Equal(1))
Expect(t).To(Equal(2))
Expect(m.HasPicture()).To(BeTrue())
Expect(m.Duration()).To(Equal(1))
Expect(m.BitRate()).To(Equal(476))
Expect(m.Duration()).To(BeNumerically("~", 1.03, 0.001))
Expect(m.BitRate()).To(Equal(192))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.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"]
Expect(err).To(BeNil())
Expect(m.Title()).To(BeEmpty())
Expect(m.HasPicture()).To(BeFalse())
Expect(m.Duration()).To(Equal(3))
Expect(m.BitRate()).To(Equal(9))
Expect(m.Duration()).To(BeNumerically("~", 1.04, 0.001))
Expect(m.BitRate()).To(Equal(16))
Expect(m.Suffix()).To(Equal("ogg"))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
Expect(m.Size()).To(Equal(4408))
Expect(m.Size()).To(Equal(int64(5065)))
})
})

View File

@ -16,10 +16,10 @@ import (
)
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
switch conf.Server.Scanner.Extractor {
@ -35,167 +35,97 @@ func Extract(files ...string) (map[string]Metadata, error) {
return e.Extract(files...)
}
type Metadata interface {
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 {
type Tags struct {
filePath string
suffix string
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 (m *baseMetadata) Album() string { return m.getTag("album", "sort_album", "albumsort") }
func (m *baseMetadata) Artist() string { return m.getTag("artist", "sort_artist", "artistsort") }
func (m *baseMetadata) AlbumArtist() string {
return m.getTag("album_artist", "album artist", "albumartist")
}
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 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
}
func (m *baseMetadata) ModificationTime() time.Time { return m.fileInfo.ModTime() }
func (m *baseMetadata) Size() int64 { return m.fileInfo.Size() }
func (m *baseMetadata) FilePath() string { return m.filePath }
func (m *baseMetadata) Suffix() string {
return strings.ToLower(strings.TrimPrefix(path.Ext(m.FilePath()), "."))
}
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
return &Tags{
filePath: filePath,
suffix: strings.ToLower(strings.TrimPrefix(path.Ext(filePath), ".")),
fileInfo: fileInfo,
tags: tags,
custom: custom,
}
}
func (m *baseMetadata) parseInt(tagName string) int {
if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(v)
return i
}
return 0
// 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")
}
func (m *baseMetadata) parseFloat(tagName string) float32 {
if v, ok := m.tags[tagName]; ok {
f, _ := strconv.ParseFloat(v, 32)
return float32(f)
}
return 0
}
// File properties
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 {
for _, t := range tags {
if v, ok := m.tags[t]; ok {
match := dateRegex.FindStringSubmatch(v)
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 {
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 (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"}
all := []string{originalTag}
for _, tag := range tags {
@ -204,45 +134,70 @@ func (m *baseMetadata) getSortTag(originalTag string, tags ...string) string {
all = append(all, name)
}
}
return m.getTag(all...)
return t.getTag(all...)
}
func (m *baseMetadata) parseTuple(tags ...string) (int, int) {
for _, tagName := range tags {
if v, ok := m.tags[tagName]; ok {
tuple := strings.Split(v, "/")
t1, t2 := 0, 0
t1, _ = strconv.Atoi(tuple[0])
if len(tuple) > 1 {
t2, _ = strconv.Atoi(tuple[1])
} else {
t2, _ = strconv.Atoi(m.tags[tagName+"total"])
}
return t1, t2
}
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
func (t *Tags) getYear(tags ...string) int {
tag := t.getTag(tags...)
if tag == "" {
return 0
}
return 0, 0
}
func (m *baseMetadata) parseBool(tags ...string) bool {
for _, tagName := range tags {
if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(strings.TrimSpace(v))
return i == 1
}
match := dateRegex.FindStringSubmatch(tag)
if len(match) == 0 {
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
return 0
}
return false
year, _ := strconv.Atoi(match[1])
return year
}
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
func (m *baseMetadata) parseDuration(tagName string) float32 {
if v, ok := m.tags[tagName]; ok {
d, err := time.Parse("15:04:05", v)
if err != nil {
return 0
}
return float32(d.Sub(zeroTime).Seconds())
func (t *Tags) getBool(tags ...string) bool {
tag := t.getTag(tags...)
if tag == "" {
return false
}
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
}

View File

@ -5,8 +5,8 @@ import (
. "github.com/onsi/gomega"
)
var _ = Describe("ffmpegMetadata", func() {
Describe("parseYear", func() {
var _ = Describe("Tags", func() {
Describe("getYear", func() {
It("parses the year correctly", func() {
var examples = map[string]int{
"1985": 1985,
@ -19,27 +19,27 @@ var _ = Describe("ffmpegMetadata", func() {
"01/10/1990": 1990,
}
for tag, expected := range examples {
md := &baseMetadata{}
md.tags = map[string]string{"date": tag}
md := &Tags{}
md.tags = map[string][]string{"date": {tag}}
Expect(md.Year()).To(Equal(expected))
}
})
It("returns 0 if year is invalid", func() {
md := &baseMetadata{}
md.tags = map[string]string{"date": "invalid"}
md := &Tags{}
md.tags = map[string][]string{"date": {"invalid"}}
Expect(md.Year()).To(Equal(0))
})
})
Describe("getMbzID", func() {
It("return a valid MBID", func() {
md := &baseMetadata{}
md.tags = map[string]string{
"musicbrainz_trackid": "8f84da07-09a0-477b-b216-cc982dabcde1",
"musicbrainz_albumid": "f68c985d-f18b-4f4a-b7f0-87837cf3fbf9",
"musicbrainz_artistid": "89ad4ac3-39f7-470e-963a-56509c546377",
"musicbrainz_albumartistid": "ada7a83c-e3e1-40f1-93f9-3e73dbc9298a",
md := &Tags{}
md.tags = map[string][]string{
"musicbrainz_trackid": {"8f84da07-09a0-477b-b216-cc982dabcde1"},
"musicbrainz_albumid": {"f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"},
"musicbrainz_artistid": {"89ad4ac3-39f7-470e-963a-56509c546377"},
"musicbrainz_albumartistid": {"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"},
}
Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
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"))
})
It("return empty string for invalid MBID", func() {
md := &baseMetadata{}
md.tags = map[string]string{
"musicbrainz_trackid": "11406732-6",
"musicbrainz_albumid": "11406732",
"musicbrainz_artistid": "200455",
"musicbrainz_albumartistid": "194",
md := &Tags{}
md.tags = map[string][]string{
"musicbrainz_trackid": {"11406732-6"},
"musicbrainz_albumid": {"11406732"},
"musicbrainz_artistid": {"200455"},
"musicbrainz_albumartistid": {"194"},
}
Expect(md.MbzTrackID()).To(Equal(""))
Expect(md.MbzAlbumID()).To(Equal(""))

View File

@ -1,7 +1,6 @@
package metadata
import (
"errors"
"os"
"github.com/dhowden/tag"
@ -9,51 +8,39 @@ import (
"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{}
func (e *taglibExtractor) Extract(paths ...string) (map[string]Metadata, error) {
mds := map[string]Metadata{}
func (e *taglibExtractor) Extract(paths ...string) (map[string]*Tags, error) {
fileTags := map[string]*Tags{}
for _, path := range paths {
md, err := e.extractMetadata(path)
tags, err := e.extractMetadata(path)
if err == nil {
mds[path] = md
fileTags[path] = tags
}
}
return mds, nil
return fileTags, nil
}
func (e *taglibExtractor) extractMetadata(filePath string) (*taglibMetadata, error) {
var err error
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)
func (e *taglibExtractor) extractMetadata(filePath string) (*Tags, error) {
parsedTags, err := taglib.Read(filePath)
if err != nil {
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
}
md.hasPicture = hasEmbeddedImage(filePath)
return md, nil
if hasEmbeddedImage(filePath) {
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 {
@ -77,6 +64,3 @@ func hasEmbeddedImage(path string) bool {
return m.Picture() != nil
}
var _ Metadata = (*taglibMetadata)(nil)
var _ Extractor = (*taglibExtractor)(nil)

View File

@ -20,7 +20,7 @@ import (
"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)
defer C.free(unsafe.Pointer(fp))
id, m := newMap()
@ -44,15 +44,15 @@ func Read(filename string) (map[string]string, error) {
}
var lock sync.RWMutex
var maps = make(map[uint32]map[string]string)
var maps = make(map[uint32]map[string][]string)
var mapsNextID uint32
func newMap() (id uint32, m map[string]string) {
func newMap() (id uint32, m map[string][]string) {
lock.Lock()
defer lock.Unlock()
id = mapsNextID
mapsNextID++
m = make(map[string]string)
m = make(map[string][]string)
maps[id] = m
return
}
@ -69,10 +69,8 @@ func go_map_put_str(id C.ulong, key *C.char, val *C.char) {
defer lock.RUnlock()
m := maps[uint32(id)]
k := strings.ToLower(C.GoString(key))
if _, ok := m[k]; !ok {
v := strings.TrimSpace(C.GoString(val))
m[k] = v
}
v := strings.TrimSpace(C.GoString(val))
m[k] = append(m[k], v)
}
//export go_map_put_int

View File

@ -18,7 +18,6 @@ var _ = Describe("taglibExtractor", func() {
Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Composer()).To(Equal("Composer"))
Expect(m.Compilation()).To(BeTrue())
Expect(m.Genre()).To(Equal("Rock"))
Expect(m.Year()).To(Equal(2014))

View File

@ -51,7 +51,7 @@ const (
filesBatchSize = 100
)
// TagScanner algorithm overview:
// Scan algorithm overview:
// Load all directories from the DB
// 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: