Introduce Metadata and MetadataExtractor interfaces

This commit is contained in:
Deluan 2020-09-04 10:55:06 -04:00 committed by Deluan Quintão
parent 6a6d4c3f87
commit 0beec552b1
5 changed files with 96 additions and 51 deletions

View file

@ -21,7 +21,7 @@ func newMediaFileMapper(rootFolder string) *mediaFileMapper {
return &mediaFileMapper{rootFolder: rootFolder} return &mediaFileMapper{rootFolder: rootFolder}
} }
func (s *mediaFileMapper) toMediaFile(md *Metadata) model.MediaFile { func (s *mediaFileMapper) toMediaFile(md Metadata) 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)
@ -64,7 +64,7 @@ func sanitizeFieldForSorting(originalValue string) string {
return utils.NoArticle(v) return utils.NoArticle(v)
} }
func (s *mediaFileMapper) mapTrackTitle(md *Metadata) string { func (s *mediaFileMapper) mapTrackTitle(md Metadata) 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)
@ -73,7 +73,7 @@ func (s *mediaFileMapper) mapTrackTitle(md *Metadata) string {
return md.Title() return md.Title()
} }
func (s *mediaFileMapper) mapAlbumArtistName(md *Metadata) string { func (s *mediaFileMapper) mapAlbumArtistName(md Metadata) string {
switch { switch {
case md.Compilation(): case md.Compilation():
return consts.VariousArtists return consts.VariousArtists
@ -86,14 +86,14 @@ func (s *mediaFileMapper) mapAlbumArtistName(md *Metadata) string {
} }
} }
func (s *mediaFileMapper) mapArtistName(md *Metadata) string { func (s *mediaFileMapper) mapArtistName(md Metadata) 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) string { func (s *mediaFileMapper) mapAlbumName(md Metadata) string {
name := md.Album() name := md.Album()
if name == "" { if name == "" {
return "[Unknown Album]" return "[Unknown Album]"
@ -101,19 +101,19 @@ func (s *mediaFileMapper) mapAlbumName(md *Metadata) string {
return name return name
} }
func (s *mediaFileMapper) trackID(md *Metadata) string { func (s *mediaFileMapper) trackID(md Metadata) 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) string { func (s *mediaFileMapper) albumID(md Metadata) 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) string { func (s *mediaFileMapper) artistID(md Metadata) 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) string { func (s *mediaFileMapper) albumArtistID(md Metadata) 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)))))
} }

33
scanner/metadata.go Normal file
View file

@ -0,0 +1,33 @@
package scanner
import "time"
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
Compilation() bool
Duration() float32
BitRate() int
ModificationTime() time.Time
FilePath() string
Suffix() string
Size() int64
}
type MetadataExtractor interface {
Extract(files ...string) (map[string]Metadata, error)
}

View file

@ -16,46 +16,52 @@ import (
"github.com/deluan/navidrome/log" "github.com/deluan/navidrome/log"
) )
type Metadata struct { type ffmpegMetadata struct {
filePath string filePath string
suffix string suffix string
fileInfo os.FileInfo fileInfo os.FileInfo
tags map[string]string tags map[string]string
} }
func (m *Metadata) Title() string { return m.getTag("title", "sort_name") } func (m *ffmpegMetadata) Title() string { return m.getTag("title", "sort_name") }
func (m *Metadata) Album() string { return m.getTag("album", "sort_album") } func (m *ffmpegMetadata) Album() string { return m.getTag("album", "sort_album") }
func (m *Metadata) Artist() string { return m.getTag("artist", "sort_artist") } func (m *ffmpegMetadata) Artist() string { return m.getTag("artist", "sort_artist") }
func (m *Metadata) AlbumArtist() string { return m.getTag("album_artist", "albumartist") } func (m *ffmpegMetadata) AlbumArtist() string { return m.getTag("album_artist", "albumartist") }
func (m *Metadata) SortTitle() string { return m.getSortTag("", "title", "name") } func (m *ffmpegMetadata) SortTitle() string { return m.getSortTag("", "title", "name") }
func (m *Metadata) SortAlbum() string { return m.getSortTag("", "album") } func (m *ffmpegMetadata) SortAlbum() string { return m.getSortTag("", "album") }
func (m *Metadata) SortArtist() string { return m.getSortTag("", "artist") } func (m *ffmpegMetadata) SortArtist() string { return m.getSortTag("", "artist") }
func (m *Metadata) SortAlbumArtist() string { func (m *ffmpegMetadata) SortAlbumArtist() string {
return m.getSortTag("tso2", "albumartist", "album_artist") return m.getSortTag("tso2", "albumartist", "album_artist")
} }
func (m *Metadata) Composer() string { return m.getTag("composer", "tcm", "sort_composer") } func (m *ffmpegMetadata) Composer() string { return m.getTag("composer", "tcm", "sort_composer") }
func (m *Metadata) Genre() string { return m.getTag("genre") } func (m *ffmpegMetadata) Genre() string { return m.getTag("genre") }
func (m *Metadata) Year() int { return m.parseYear("date") } func (m *ffmpegMetadata) Year() int { return m.parseYear("date") }
func (m *Metadata) TrackNumber() (int, int) { return m.parseTuple("track") } func (m *ffmpegMetadata) TrackNumber() (int, int) { return m.parseTuple("track") }
func (m *Metadata) DiscNumber() (int, int) { return m.parseTuple("tpa", "disc") } func (m *ffmpegMetadata) DiscNumber() (int, int) { return m.parseTuple("tpa", "disc") }
func (m *Metadata) DiscSubtitle() string { return m.getTag("tsst", "discsubtitle", "setsubtitle") } func (m *ffmpegMetadata) DiscSubtitle() string {
func (m *Metadata) HasPicture() bool { return m.getTag("has_picture", "metadata_block_picture") != "" } return m.getTag("tsst", "discsubtitle", "setsubtitle")
func (m *Metadata) Comment() string { return m.getTag("comment") } }
func (m *Metadata) Compilation() bool { return m.parseBool("compilation") } func (m *ffmpegMetadata) HasPicture() bool {
func (m *Metadata) Duration() float32 { return m.parseDuration("duration") } return m.getTag("has_picture", "metadata_block_picture") != ""
func (m *Metadata) BitRate() int { return m.parseInt("bitrate") } }
func (m *Metadata) ModificationTime() time.Time { return m.fileInfo.ModTime() } func (m *ffmpegMetadata) Comment() string { return m.getTag("comment") }
func (m *Metadata) FilePath() string { return m.filePath } func (m *ffmpegMetadata) Compilation() bool { return m.parseBool("compilation") }
func (m *Metadata) Suffix() string { return m.suffix } func (m *ffmpegMetadata) Duration() float32 { return m.parseDuration("duration") }
func (m *Metadata) Size() int64 { return m.fileInfo.Size() } func (m *ffmpegMetadata) BitRate() int { return m.parseInt("bitrate") }
func (m *ffmpegMetadata) ModificationTime() time.Time { return m.fileInfo.ModTime() }
func (m *ffmpegMetadata) FilePath() string { return m.filePath }
func (m *ffmpegMetadata) Suffix() string { return m.suffix }
func (m *ffmpegMetadata) Size() int64 { return m.fileInfo.Size() }
func ExtractAllMetadata(inputs []string) (map[string]*Metadata, error) { type ffmpegMetadataExtractor struct{}
args := createProbeCommand(inputs)
func (e *ffmpegMetadataExtractor) Extract(files ...string) (map[string]Metadata, error) {
args := 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{} mds := map[string]Metadata{}
if len(output) == 0 { if len(output) == 0 {
return mds, errors.New("error extracting metadata files") return mds, errors.New("error extracting metadata files")
} }
@ -109,8 +115,8 @@ func parseOutput(output string) map[string]string {
return outputs return outputs
} }
func extractMetadata(filePath, info string) (*Metadata, error) { func extractMetadata(filePath, info string) (*ffmpegMetadata, error) {
m := &Metadata{filePath: filePath, tags: map[string]string{}} m := &ffmpegMetadata{filePath: filePath, tags: map[string]string{}}
m.suffix = strings.ToLower(strings.TrimPrefix(path.Ext(filePath), ".")) m.suffix = strings.ToLower(strings.TrimPrefix(path.Ext(filePath), "."))
var err error var err error
m.fileInfo, err = os.Stat(filePath) m.fileInfo, err = os.Stat(filePath)
@ -127,7 +133,7 @@ func extractMetadata(filePath, info string) (*Metadata, error) {
return m, nil return m, nil
} }
func (m *Metadata) parseInfo(info string) { func (m *ffmpegMetadata) parseInfo(info string) {
reader := strings.NewReader(info) reader := strings.NewReader(info)
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
for scanner.Scan() { for scanner.Scan() {
@ -169,7 +175,7 @@ func (m *Metadata) parseInfo(info string) {
} }
} }
func (m *Metadata) parseInt(tagName string) int { func (m *ffmpegMetadata) parseInt(tagName string) int {
if v, ok := m.tags[tagName]; ok { if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(v) i, _ := strconv.Atoi(v)
return i return i
@ -179,7 +185,7 @@ func (m *Metadata) parseInt(tagName string) int {
var dateRegex = regexp.MustCompile(`^([12]\d\d\d)`) var dateRegex = regexp.MustCompile(`^([12]\d\d\d)`)
func (m *Metadata) parseYear(tagName string) int { func (m *ffmpegMetadata) parseYear(tagName string) int {
if v, ok := m.tags[tagName]; ok { if v, ok := m.tags[tagName]; ok {
match := dateRegex.FindStringSubmatch(v) match := dateRegex.FindStringSubmatch(v)
if len(match) == 0 { if len(match) == 0 {
@ -192,7 +198,7 @@ func (m *Metadata) parseYear(tagName string) int {
return 0 return 0
} }
func (m *Metadata) getTag(tags ...string) string { func (m *ffmpegMetadata) getTag(tags ...string) string {
for _, t := range tags { for _, t := range tags {
if v, ok := m.tags[t]; ok { if v, ok := m.tags[t]; ok {
return v return v
@ -201,7 +207,7 @@ func (m *Metadata) getTag(tags ...string) string {
return "" return ""
} }
func (m *Metadata) getSortTag(originalTag string, tags ...string) string { func (m *ffmpegMetadata) 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 {
@ -213,7 +219,7 @@ func (m *Metadata) getSortTag(originalTag string, tags ...string) string {
return m.getTag(all...) return m.getTag(all...)
} }
func (m *Metadata) parseTuple(tags ...string) (int, int) { func (m *ffmpegMetadata) parseTuple(tags ...string) (int, int) {
for _, tagName := range tags { for _, tagName := range tags {
if v, ok := m.tags[tagName]; ok { if v, ok := m.tags[tagName]; ok {
tuple := strings.Split(v, "/") tuple := strings.Split(v, "/")
@ -230,7 +236,7 @@ func (m *Metadata) parseTuple(tags ...string) (int, int) {
return 0, 0 return 0, 0
} }
func (m *Metadata) parseBool(tagName string) bool { func (m *ffmpegMetadata) parseBool(tagName string) bool {
if v, ok := m.tags[tagName]; ok { if v, ok := m.tags[tagName]; ok {
i, _ := strconv.Atoi(strings.TrimSpace(v)) i, _ := strconv.Atoi(strings.TrimSpace(v))
return i == 1 return i == 1
@ -240,7 +246,7 @@ func (m *Metadata) parseBool(tagName string) bool {
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC) var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
func (m *Metadata) parseDuration(tagName string) float32 { func (m *ffmpegMetadata) parseDuration(tagName string) float32 {
if v, ok := m.tags[tagName]; ok { if v, ok := m.tags[tagName]; ok {
d, err := time.Parse("15:04:05", v) d, err := time.Parse("15:04:05", v)
if err != nil { if err != nil {

View file

@ -5,11 +5,12 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Metadata", func() { var _ = Describe("ffmpegMetadata", func() {
// TODO Need to mock `ffmpeg` // TODO Need to mock `ffmpeg`
XContext("ExtractAllMetadata", func() { XContext("ExtractAllMetadata", func() {
It("correctly parses metadata from all files in folder", func() { It("correctly parses metadata from all files in folder", func() {
mds, err := ExtractAllMetadata([]string{"tests/fixtures/test.mp3", "tests/fixtures/test.ogg"}) e := &ffmpegMetadataExtractor{}
mds, err := e.Extract("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(mds).To(HaveLen(2)) Expect(mds).To(HaveLen(2))
@ -223,13 +224,13 @@ Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗
"May 12, 2016": 0, "May 12, 2016": 0,
} }
for tag, expected := range examples { for tag, expected := range examples {
md := &Metadata{tags: map[string]string{"date": tag}} md := &ffmpegMetadata{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 := &Metadata{tags: map[string]string{"date": "invalid"}} md := &ffmpegMetadata{tags: map[string]string{"date": "invalid"}}
Expect(md.Year()).To(Equal(0)) Expect(md.Year()).To(Equal(0))
}) })
}) })

View file

@ -340,8 +340,13 @@ func (s *TagScanner) addOrUpdateTracksInDB(ctx context.Context, dir string, curr
return numUpdatedTracks, nil return numUpdatedTracks, nil
} }
func (s *TagScanner) newMetadataExtractor() MetadataExtractor {
return &ffmpegMetadataExtractor{}
}
func (s *TagScanner) loadTracks(filePaths []string) (model.MediaFiles, error) { func (s *TagScanner) loadTracks(filePaths []string) (model.MediaFiles, error) {
mds, err := ExtractAllMetadata(filePaths) e := s.newMetadataExtractor()
mds, err := e.Extract(filePaths...)
if err != nil { if err != nil {
return nil, err return nil, err
} }