Reorganize metadata extractors code

This commit is contained in:
Deluan 2021-07-24 09:53:17 -04:00
parent 6175629bb4
commit d3975d206a
14 changed files with 287 additions and 258 deletions

View File

@ -1,4 +1,4 @@
package metadata
package ffmpeg
import (
"bufio"
@ -13,15 +13,17 @@ import (
"github.com/navidrome/navidrome/log"
)
type ffmpegExtractor struct{}
type Parser struct{}
func (e *ffmpegExtractor) Extract(files ...string) (map[string]*Tags, error) {
type parsedTags = map[string][]string
func (e *Parser) Parse(files ...string) (map[string]parsedTags, error) {
args := e.createProbeCommand(files)
log.Trace("Executing command", "args", args)
cmd := exec.Command(args[0], args[1:]...) // #nosec
output, _ := cmd.CombinedOutput()
fileTags := map[string]*Tags{}
fileTags := map[string]parsedTags{}
if len(output) == 0 {
return fileTags, errors.New("error extracting metadata files")
}
@ -36,6 +38,27 @@ func (e *ffmpegExtractor) Extract(files ...string) (map[string]*Tags, error) {
return fileTags, nil
}
func (e *Parser) extractMetadata(filePath, info string) (parsedTags, error) {
tags := e.parseInfo(info)
if len(tags) == 0 {
log.Trace("Not a media file. Skipping", "filePath", filePath)
return nil, errors.New("not a media file")
}
alternativeTags := map[string][]string{
"disc": {"tpa"},
"has_picture": {"metadata_block_picture"},
}
for tagName, alternatives := range alternativeTags {
for _, altName := range alternatives {
if altValue, ok := tags[altName]; ok {
tags[tagName] = append(tags[tagName], altValue...)
}
}
}
return tags, nil
}
var (
// Input #0, mp3, from 'groovin.mp3':
inputRegex = regexp.MustCompile(`(?m)^Input #\d+,.*,\sfrom\s'(.*)'`)
@ -56,7 +79,7 @@ var (
coverRx = regexp.MustCompile(`^\s{2,4}Stream #\d+:\d+: (Video):.*`)
)
func (e *ffmpegExtractor) parseOutput(output string) map[string]string {
func (e *Parser) parseOutput(output string) map[string]string {
outputs := map[string]string{}
all := inputRegex.FindAllStringSubmatchIndex(output, -1)
for i, loc := range all {
@ -78,21 +101,7 @@ func (e *ffmpegExtractor) parseOutput(output string) map[string]string {
return outputs
}
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")
}
tags := NewTags(filePath, parsedTags, map[string][]string{
"disc": {"tpa"},
"has_picture": {"metadata_block_picture"},
})
return tags, nil
}
func (e *ffmpegExtractor) parseInfo(info string) map[string][]string {
func (e *Parser) parseInfo(info string) map[string][]string {
tags := map[string][]string{}
reader := strings.NewReader(info)
@ -158,7 +167,7 @@ func (e *ffmpegExtractor) parseInfo(info string) map[string][]string {
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
func (e *ffmpegExtractor) parseDuration(tag string) string {
func (e *Parser) parseDuration(tag string) string {
d, err := time.Parse("15:04:05", tag)
if err != nil {
return "0"
@ -167,7 +176,7 @@ func (e *ffmpegExtractor) parseDuration(tag string) string {
}
// Inputs will always be absolute paths
func (e *ffmpegExtractor) createProbeCommand(inputs []string) []string {
func (e *Parser) createProbeCommand(inputs []string) []string {
split := strings.Split(conf.Server.ProbeCommand, " ")
args := make([]string, 0)

View File

@ -0,0 +1,17 @@
package ffmpeg
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestFFMpeg(t *testing.T) {
tests.Init(t, true)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "FFMpeg Suite")
}

View File

@ -1,53 +1,14 @@
package metadata
package ffmpeg
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("ffmpegExtractor", func() {
var e *ffmpegExtractor
var _ = Describe("Parser", func() {
var e *Parser
BeforeEach(func() {
e = &ffmpegExtractor{}
})
// TODO Need to mock `ffmpeg`
XContext("Extract", func() {
It("correctly parses metadata from all files in folder", func() {
mds, err := e.Extract("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
Expect(err).NotTo(HaveOccurred())
Expect(mds).To(HaveLen(2))
m := mds["tests/fixtures/test.mp3"]
Expect(m.Title()).To(Equal("Song"))
Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Compilation()).To(BeTrue())
Expect(m.Genres()).To(Equal("Rock"))
Expect(m.Year()).To(Equal(2014))
n, t := m.TrackNumber()
Expect(n).To(Equal(2))
Expect(t).To(Equal(10))
n, t = m.DiscNumber()
Expect(n).To(Equal(1))
Expect(t).To(Equal(2))
Expect(m.HasPicture()).To(BeTrue())
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(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(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(int64(5065)))
})
e = &Parser{}
})
Context("extractMetadata", func() {
@ -70,13 +31,13 @@ Input #0, ape, from './Capture/02 01 - Symphony No. 5 in C minor, Op. 67 I. Alle
CatalogNumber : PLD 1201
`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.CatalogNum()).To(Equal("PLD 1201"))
Expect(md.MbzTrackID()).To(Equal("ffe06940-727a-415a-b608-b7e45737f9d8"))
Expect(md.MbzAlbumID()).To(Equal("71eb5e4a-90e2-4a31-a2d1-a96485fcb667"))
Expect(md.MbzArtistID()).To(Equal("1f9df192-a621-4f54-8850-2c5373b7eac9"))
Expect(md.MbzAlbumArtistID()).To(Equal("89ad4ac3-39f7-470e-963a-56509c546377"))
Expect(md.MbzAlbumType()).To(Equal("album"))
Expect(md.MbzAlbumComment()).To(Equal("MP3"))
Expect(md).To(HaveKeyWithValue("catalognumber", []string{"PLD 1201"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_trackid", []string{"ffe06940-727a-415a-b608-b7e45737f9d8"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_albumid", []string{"71eb5e4a-90e2-4a31-a2d1-a96485fcb667"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_artistid", []string{"1f9df192-a621-4f54-8850-2c5373b7eac9"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_albumartistid", []string{"89ad4ac3-39f7-470e-963a-56509c546377"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_albumtype", []string{"album"}))
Expect(md).To(HaveKeyWithValue("musicbrainz_albumcomment", []string{"MP3"}))
})
It("detects embedded cover art correctly", func() {
@ -88,7 +49,7 @@ Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s
Stream #0:1: Video: mjpeg, yuvj444p(pc, bt470bg/unknown/unknown), 600x600 [SAR 1:1 DAR 1:1], 90k tbr, 90k tbn, 90k tbc`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.HasPicture()).To(BeTrue())
Expect(md).To(HaveKeyWithValue("has_picture", []string{"true"}))
})
It("detects embedded cover art in ffmpeg 4.4 output", func() {
@ -103,7 +64,7 @@ Input #0, flac, from '/run/media/naomi/Archivio/Musica/Katy Perry/Chained to the
Metadata:
comment : Cover (front)`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.HasPicture()).To(BeTrue())
Expect(md).To(HaveKeyWithValue("has_picture", []string{"true"}))
})
It("detects embedded cover art in ogg containers", func() {
@ -116,7 +77,7 @@ Input #0, ogg, from '/Users/deluan/Music/iTunes/iTunes Media/Music/_Testes/Jamai
metadata_block_picture: AAAAAwAAAAppbWFnZS9qcGVnAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4Id/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ
TITLE : Jamaican In New York (Album Version)`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.HasPicture()).To(BeTrue())
Expect(md).To(HaveKey("has_picture"))
})
It("gets bitrate from the stream, if available", func() {
@ -125,17 +86,7 @@ Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/
Duration: 00:00:01.02, start: 0.000000, bitrate: 477 kb/s
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.BitRate()).To(Equal(192))
})
It("parses correctly the compilation tag", func() {
const output = `
Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3':
Metadata:
compilation : 1
Duration: 00:05:02.63, start: 0.000000, bitrate: 140 kb/s`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Compilation()).To(BeTrue())
Expect(md).To(HaveKeyWithValue("bitrate", []string{"192"}))
})
It("parses duration with milliseconds", func() {
@ -143,7 +94,7 @@ Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/
Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3':
Duration: 00:05:02.63, start: 0.000000, bitrate: 140 kb/s`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Duration()).To(BeNumerically("~", 302.63, 0.001))
Expect(md).To(HaveKeyWithValue("duration", []string{"302.63"}))
})
It("parses stream level tags", func() {
@ -156,7 +107,7 @@ Input #0, ogg, from './01-02 Drive (Teku).opus':
Metadata:
TITLE : Drive (Teku)`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Title()).To(Equal("Drive (Teku)"))
Expect(md).To(HaveKeyWithValue("title", []string{"Drive (Teku)"}))
})
It("does not overlap top level tags with the stream level tags", func() {
@ -168,33 +119,7 @@ Input #0, mp3, from 'groovin.mp3':
Metadata:
title : garbage`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Title()).To(Equal("Groovin' (feat. Daniel Sneijers, Susanne Alt)"))
})
It("ignores case in the tag name", func() {
const output = `
Input #0, flac, from '/Users/deluan/Downloads/06. Back In Black.flac':
Metadata:
ALBUM : Back In Black
DATE : 1980.07.25
disc : 1
GENRE : Hard Rock
TITLE : Back In Black
DISCTOTAL : 1
TRACKTOTAL : 10
track : 6
Duration: 00:04:16.00, start: 0.000000, bitrate: 995 kb/s`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Title()).To(Equal("Back In Black"))
Expect(md.Album()).To(Equal("Back In Black"))
Expect(md.Genres()).To(ConsistOf("Hard Rock"))
n, t := md.TrackNumber()
Expect(n).To(Equal(6))
Expect(t).To(Equal(10))
n, t = md.DiscNumber()
Expect(n).To(Equal(1))
Expect(t).To(Equal(1))
Expect(md.Year()).To(Equal(1980))
Expect(md).To(HaveKeyWithValue("title", []string{"Groovin' (feat. Daniel Sneijers, Susanne Alt)", "garbage"}))
})
It("parses multiline tags", func() {
@ -227,7 +152,7 @@ Tracklist:
07. Wunderbar
08. Quarta Dimensão`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", outputWithMultilineComment)
Expect(md.Comment()).To(Equal(expectedComment))
Expect(md).To(HaveKeyWithValue("comment", []string{expectedComment}))
})
It("parses sort tags correctly", func() {
@ -244,14 +169,14 @@ Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗
ALBUMARTISTSORT : Shiina, Ringo
`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Title()).To(Equal("ドツペルゲンガー"))
Expect(md.Album()).To(Equal("加爾基 精液 栗ノ花"))
Expect(md.Artist()).To(Equal("椎名林檎"))
Expect(md.AlbumArtist()).To(Equal("椎名林檎"))
Expect(md.SortTitle()).To(Equal("Dopperugengā"))
Expect(md.SortAlbum()).To(Equal("Kalk Samen Kuri No Hana"))
Expect(md.SortArtist()).To(Equal("Shiina, Ringo"))
Expect(md.SortAlbumArtist()).To(Equal("Shiina, Ringo"))
Expect(md).To(HaveKeyWithValue("title", []string{"ドツペルゲンガー"}))
Expect(md).To(HaveKeyWithValue("album", []string{"加爾基 精液 栗ノ花"}))
Expect(md).To(HaveKeyWithValue("artist", []string{"椎名林檎"}))
Expect(md).To(HaveKeyWithValue("album_artist", []string{"椎名林檎"}))
Expect(md).To(HaveKeyWithValue("title-sort", []string{"Dopperugengā"}))
Expect(md).To(HaveKeyWithValue("albumsort", []string{"Kalk Samen Kuri No Hana"}))
Expect(md).To(HaveKeyWithValue("artist_sort", []string{"Shiina, Ringo"}))
Expect(md).To(HaveKeyWithValue("albumartistsort", []string{"Shiina, Ringo"}))
})
It("ignores cover comment", func() {
@ -266,7 +191,7 @@ Input #0, mp3, from './Edie Brickell/Picture Perfect Morning/01-01 Tomorrow Come
Metadata:
comment : Cover (front)`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Comment()).To(Equal(""))
Expect(md).ToNot(HaveKey("comment"))
})
It("parses tags with spaces in the name", func() {
@ -276,7 +201,7 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
ALBUM ARTIST : Wyclef Jean
`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.AlbumArtist()).To(Equal("Wyclef Jean"))
Expect(md).To(HaveKeyWithValue("album artist", []string{"Wyclef Jean"}))
})
})
@ -291,7 +216,7 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
Metadata:
TBPM : 123`
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md.Bpm()).To(Equal(123))
Expect(md).To(HaveKeyWithValue("tbpm", []string{"123"}))
})
It("parses and rounds a floating point fBPM tag", func() {
@ -300,6 +225,6 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
Metadata:
FBPM : 141.7`
md, _ := e.extractMetadata("tests/fixtures/test.ogg", output)
Expect(md.Bpm()).To(Equal(142))
Expect(md).To(HaveKeyWithValue("fbpm", []string{"141.7"}))
})
})

View File

@ -10,53 +10,59 @@ import (
"strings"
"time"
"github.com/navidrome/navidrome/scanner/metadata/ffmpeg"
"github.com/navidrome/navidrome/scanner/metadata/taglib"
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
)
type Extractor interface {
Extract(files ...string) (map[string]*Tags, error)
type Parser interface {
Parse(files ...string) (map[string]map[string][]string, error)
}
func Extract(files ...string) (map[string]*Tags, error) {
var e Extractor
var e Parser
switch conf.Server.Scanner.Extractor {
case "taglib":
e = &taglibExtractor{}
e = &taglib.Parser{}
case "ffmpeg":
e = &ffmpegExtractor{}
e = &ffmpeg.Parser{}
default:
log.Warn("Invalid Scanner.Extractor option. Using default taglib", "requested", conf.Server.Scanner.Extractor,
log.Warn("Invalid 'Scanner.Extractor' option. Using default 'taglib'", "requested", conf.Server.Scanner.Extractor,
"validOptions", "ffmpeg,taglib")
e = &taglibExtractor{}
e = &taglib.Parser{}
}
return e.Extract(files...)
extractedTags, err := e.Parse(files...)
if err != nil {
return nil, err
}
result := map[string]*Tags{}
for filePath, tags := range extractedTags {
fileInfo, err := os.Stat(filePath)
if err != nil {
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
continue
}
result[filePath] = &Tags{
filePath: filePath,
fileInfo: fileInfo,
tags: tags,
}
}
return result, nil
}
type Tags struct {
filePath string
suffix string
fileInfo os.FileInfo
tags map[string][]string
custom map[string][]string
}
func NewTags(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
@ -109,11 +115,10 @@ 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) Suffix() string { return strings.ToLower(strings.TrimPrefix(path.Ext(t.filePath), ".")) }
func (t *Tags) getTags(tagNames ...string) []string {
allTags := append(tagNames, t.custom[tagNames[0]]...)
for _, tag := range allTags {
for _, tag := range tagNames {
if v, ok := t.tags[tag]; ok {
return v
}
@ -130,7 +135,6 @@ func (t *Tags) getFirstTagValue(tagNames ...string) string {
}
func (t *Tags) getAllTagValues(tagNames ...string) []string {
tagNames = append(tagNames, t.custom[tagNames[0]]...)
var values []string
for _, tag := range tagNames {
if v, ok := t.tags[tag]; ok {

View File

@ -9,7 +9,7 @@ import (
. "github.com/onsi/gomega"
)
func TestScanner(t *testing.T) {
func TestMetadata(t *testing.T) {
tests.Init(t, true)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)

View File

@ -1,11 +1,55 @@
package metadata
import (
"github.com/navidrome/navidrome/conf"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Tags", func() {
Context("Extract", func() {
BeforeEach(func() {
conf.Server.Scanner.Extractor = "taglib"
})
It("correctly parses metadata from all files in folder", func() {
mds, err := Extract("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
Expect(err).NotTo(HaveOccurred())
Expect(mds).To(HaveLen(2))
m := mds["tests/fixtures/test.mp3"]
Expect(m.Title()).To(Equal("Song"))
Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Compilation()).To(BeTrue())
Expect(m.Genres()).To(Equal([]string{"Rock"}))
Expect(m.Year()).To(Equal(2014))
n, t := m.TrackNumber()
Expect(n).To(Equal(2))
Expect(t).To(Equal(10))
n, t = m.DiscNumber()
Expect(n).To(Equal(1))
Expect(t).To(Equal(2))
Expect(m.HasPicture()).To(BeTrue())
Expect(m.Duration()).To(BeNumerically("~", 1, 0.01))
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(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(BeNumerically("~", 1.00, 0.01))
Expect(m.BitRate()).To(Equal(18))
Expect(m.Suffix()).To(Equal("ogg"))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
Expect(m.Size()).To(Equal(int64(5065)))
})
})
Describe("getYear", func() {
It("parses the year correctly", func() {
var examples = map[string]int{
@ -65,12 +109,23 @@ var _ = Describe("Tags", func() {
It("returns values from all tag names", func() {
md := &Tags{}
md.tags = map[string][]string{
"genre": {"Rock", "Pop"},
"_genre": {"New Wave"},
"genre": {"Rock", "Pop", "New Wave"},
}
md.custom = map[string][]string{"genre": {"_genre"}}
Expect(md.Genres()).To(ConsistOf("Rock", "Pop", "New Wave"))
})
})
Describe("Bpm", func() {
var t *Tags
BeforeEach(func() {
t = &Tags{tags: map[string][]string{
"fbpm": []string{"141.7"},
}}
})
It("rounds a floating point fBPM tag", func() {
Expect(t.Bpm()).To(Equal(142))
})
})
})

View File

@ -1,36 +0,0 @@
package metadata
import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/scanner/metadata/taglib"
)
type taglibExtractor struct{}
func (e *taglibExtractor) Extract(paths ...string) (map[string]*Tags, error) {
fileTags := map[string]*Tags{}
for _, path := range paths {
tags, err := e.extractMetadata(path)
if err == nil {
fileTags[path] = tags
}
}
return fileTags, nil
}
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)
}
tags := NewTags(filePath, parsedTags, map[string][]string{
"title": {"_track", "titlesort"},
"album": {"_album", "albumsort"},
"artist": {"_artist", "artistsort"},
"date": {"_year"},
"track": {"_track"},
})
return tags, nil
}

View File

@ -0,0 +1,43 @@
package taglib
import (
"github.com/navidrome/navidrome/log"
)
type Parser struct{}
type parsedTags = map[string][]string
func (e *Parser) Parse(paths ...string) (map[string]parsedTags, error) {
fileTags := map[string]parsedTags{}
for _, path := range paths {
tags, err := e.extractMetadata(path)
if err == nil {
fileTags[path] = tags
}
}
return fileTags, nil
}
func (e *Parser) extractMetadata(filePath string) (parsedTags, error) {
tags, err := Read(filePath)
if err != nil {
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
}
alternativeTags := map[string][]string{
"title": {"titlesort"},
"album": {"albumsort"},
"artist": {"artistsort"},
"tracknumber": {"trck", "_track"},
}
for tagName, alternatives := range alternativeTags {
for _, altName := range alternatives {
if altValue, ok := tags[altName]; ok {
tags[tagName] = append(tags[tagName], altValue...)
}
}
}
return tags, nil
}

View File

@ -0,0 +1,17 @@
package taglib
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestTagLib(t *testing.T) {
tests.Init(t, true)
log.SetLevel(log.LevelCritical)
RegisterFailHandler(Fail)
RunSpecs(t, "TagLib Suite")
}

View File

@ -0,0 +1,49 @@
package taglib
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Parser", func() {
var e *Parser
BeforeEach(func() {
e = &Parser{}
})
Context("Parse", func() {
It("correctly parses metadata from all files in folder", func() {
mds, err := e.Parse("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
Expect(err).NotTo(HaveOccurred())
Expect(mds).To(HaveLen(2))
m := mds["tests/fixtures/test.mp3"]
Expect(m).To(HaveKeyWithValue("title", []string{"Song", "Song"}))
Expect(m).To(HaveKeyWithValue("album", []string{"Album", "Album"}))
Expect(m).To(HaveKeyWithValue("artist", []string{"Artist", "Artist"}))
Expect(m).To(HaveKeyWithValue("albumartist", []string{"Album Artist"}))
Expect(m).To(HaveKeyWithValue("tcmp", []string{"1"})) // Compilation
Expect(m).To(HaveKeyWithValue("genre", []string{"Rock"}))
Expect(m).To(HaveKeyWithValue("date", []string{"2014", "2014"}))
Expect(m).To(HaveKeyWithValue("tracknumber", []string{"2/10", "2/10", "2"}))
Expect(m).To(HaveKeyWithValue("discnumber", []string{"1/2"}))
Expect(m).To(HaveKeyWithValue("has_picture", []string{"true"}))
Expect(m).To(HaveKeyWithValue("duration", []string{"1"}))
Expect(m).To(HaveKeyWithValue("bitrate", []string{"192"}))
Expect(m).To(HaveKeyWithValue("comment", []string{"Comment1\nComment2"}))
Expect(m).To(HaveKeyWithValue("lyrics", []string{"Lyrics 1\rLyrics 2"}))
Expect(m).To(HaveKeyWithValue("bpm", []string{"123"}))
m = mds["tests/fixtures/test.ogg"]
Expect(err).To(BeNil())
Expect(m).ToNot(HaveKey("title"))
Expect(m).ToNot(HaveKey("has_picture"))
Expect(m).To(HaveKeyWithValue("duration", []string{"1"}))
Expect(m).To(HaveKeyWithValue("fbpm", []string{"141.7"}))
// TabLib 1.12 returns 18, previous versions return 39.
// See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b
Expect(m).To(HaveKey("bitrate"))
Expect(m["bitrate"][0]).To(BeElementOf("18", "39"))
})
})
})

View File

@ -13,7 +13,7 @@
#include <tpropertymap.h>
#include <vorbisfile.h>
#include "taglib_parser.h"
#include "taglib_wrapper.h"
char has_cover(const TagLib::FileRef f);
@ -39,16 +39,16 @@ int taglib_read(const char *filename, unsigned long id) {
TagLib::Tag *basic = f.file()->tag();
if (!basic->isEmpty()) {
if (!basic->title().isEmpty()) {
tags.insert("_title", basic->title());
tags.insert("title", basic->title());
}
if (!basic->artist().isEmpty()) {
tags.insert("_artist", basic->artist());
tags.insert("artist", basic->artist());
}
if (!basic->album().isEmpty()) {
tags.insert("_album", basic->album());
tags.insert("album", basic->album());
}
if (basic->year() > 0) {
tags.insert("_year", TagLib::String::number(basic->year()));
tags.insert("date", TagLib::String::number(basic->year()));
}
if (basic->track() > 0) {
tags.insert("_track", TagLib::String::number(basic->track()));

View File

@ -7,7 +7,7 @@ package taglib
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "taglib_parser.h"
#include "taglib_wrapper.h"
*/
import "C"
import (

View File

@ -1,54 +0,0 @@
package metadata
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("taglibExtractor", func() {
Context("Extract", func() {
It("correctly parses metadata from all files in folder", func() {
e := &taglibExtractor{}
mds, err := e.Extract("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
Expect(err).NotTo(HaveOccurred())
Expect(mds).To(HaveLen(2))
m := mds["tests/fixtures/test.mp3"]
Expect(m.Title()).To(Equal("Song"))
Expect(m.Album()).To(Equal("Album"))
Expect(m.Artist()).To(Equal("Artist"))
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
Expect(m.Compilation()).To(BeTrue())
Expect(m.Genres()).To(ConsistOf("Rock"))
Expect(m.Year()).To(Equal(2014))
n, t := m.TrackNumber()
Expect(n).To(Equal(2))
Expect(t).To(Equal(10))
n, t = m.DiscNumber()
Expect(n).To(Equal(1))
Expect(t).To(Equal(2))
Expect(m.HasPicture()).To(BeTrue())
Expect(m.Duration()).To(Equal(float32(1)))
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(int64(51876)))
Expect(m.Comment()).To(Equal("Comment1\nComment2"))
Expect(m.Bpm()).To(Equal(123))
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(float32(1)))
Expect(m.Suffix()).To(Equal("ogg"))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
Expect(m.Size()).To(Equal(int64(5065)))
Expect(m.Bpm()).To(Equal(142)) // This file has a floating point BPM set to 141.7 under the fBPM tag. Ensure we parse and round correctly.
// TabLib 1.12 returns 18, previous versions return 39.
// See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b
Expect(m.BitRate()).To(BeElementOf(18, 39))
})
})
})