mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +03:00
Introduce Metadata and MetadataExtractor interfaces
This commit is contained in:
parent
6a6d4c3f87
commit
0beec552b1
5 changed files with 96 additions and 51 deletions
|
@ -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
33
scanner/metadata.go
Normal 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)
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue