From cd242695ba6556fdf47a93a38e2675b3c49267ee Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 31 May 2021 17:02:12 -0400 Subject: [PATCH] Foundational work to enable multi-valued tags --- scanner/mapping.go | 18 +- scanner/metadata/ffmpeg.go | 99 ++++--- scanner/metadata/ffmpeg_test.go | 13 +- scanner/metadata/metadata.go | 313 ++++++++++------------- scanner/metadata/metadata_test.go | 36 +-- scanner/metadata/taglib.go | 60 ++--- scanner/metadata/taglib/taglib_parser.go | 14 +- scanner/metadata/taglib_test.go | 1 - scanner/tag_scanner.go | 2 +- 9 files changed, 244 insertions(+), 312 deletions(-) diff --git a/scanner/mapping.go b/scanner/mapping.go index 7f99e5377..ab16ea4e0 100644 --- a/scanner/mapping.go +++ b/scanner/mapping.go @@ -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))))) } diff --git a/scanner/metadata/ffmpeg.go b/scanner/metadata/ffmpeg.go index 2d84bb3d7..395c62dbf 100644 --- a/scanner/metadata/ffmpeg.go +++ b/scanner/metadata/ffmpeg.go @@ -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 diff --git a/scanner/metadata/ffmpeg_test.go b/scanner/metadata/ffmpeg_test.go index 33c0db9aa..3bd058622 100644 --- a/scanner/metadata/ffmpeg_test.go +++ b/scanner/metadata/ffmpeg_test.go @@ -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))) }) }) diff --git a/scanner/metadata/metadata.go b/scanner/metadata/metadata.go index a4ca658c8..762553007 100644 --- a/scanner/metadata/metadata.go +++ b/scanner/metadata/metadata.go @@ -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 } diff --git a/scanner/metadata/metadata_test.go b/scanner/metadata/metadata_test.go index f41134eb5..d502fd20c 100644 --- a/scanner/metadata/metadata_test.go +++ b/scanner/metadata/metadata_test.go @@ -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("")) diff --git a/scanner/metadata/taglib.go b/scanner/metadata/taglib.go index 8465225d5..9f15e838c 100644 --- a/scanner/metadata/taglib.go +++ b/scanner/metadata/taglib.go @@ -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) diff --git a/scanner/metadata/taglib/taglib_parser.go b/scanner/metadata/taglib/taglib_parser.go index 9e8d8217b..27b3a129e 100644 --- a/scanner/metadata/taglib/taglib_parser.go +++ b/scanner/metadata/taglib/taglib_parser.go @@ -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 diff --git a/scanner/metadata/taglib_test.go b/scanner/metadata/taglib_test.go index 1a76955f0..31beef7a9 100644 --- a/scanner/metadata/taglib_test.go +++ b/scanner/metadata/taglib_test.go @@ -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)) diff --git a/scanner/tag_scanner.go b/scanner/tag_scanner.go index 00437b3a9..3f2b191c2 100644 --- a/scanner/tag_scanner.go +++ b/scanner/tag_scanner.go @@ -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: