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

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

View File

@ -5,11 +5,12 @@ import (
. "github.com/onsi/gomega"
)
var _ = Describe("Metadata", func() {
var _ = Describe("ffmpegMetadata", func() {
// TODO Need to mock `ffmpeg`
XContext("ExtractAllMetadata", 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(mds).To(HaveLen(2))
@ -223,13 +224,13 @@ Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗
"May 12, 2016": 0,
}
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))
}
})
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))
})
})

View File

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