mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +03:00
Initial implementation of taglib
MetadataExtractor
This commit is contained in:
parent
b6aa6eb7b2
commit
df4328819d
6 changed files with 227 additions and 129 deletions
3
go.mod
3
go.mod
|
@ -23,6 +23,9 @@ require (
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||||
github.com/microcosm-cc/bluemonday v1.0.4
|
github.com/microcosm-cc/bluemonday v1.0.4
|
||||||
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
||||||
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
||||||
|
github.com/onsi/ginkgo v1.14.0
|
||||||
|
github.com/onsi/gomega v1.10.1
|
||||||
github.com/onsi/ginkgo v1.14.1
|
github.com/onsi/ginkgo v1.14.1
|
||||||
github.com/onsi/gomega v1.10.2
|
github.com/onsi/gomega v1.10.2
|
||||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -253,6 +253,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI=
|
||||||
|
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||||
|
|
|
@ -3,59 +3,29 @@ package metadata
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/deluan/navidrome/conf"
|
"github.com/deluan/navidrome/conf"
|
||||||
"github.com/deluan/navidrome/log"
|
"github.com/deluan/navidrome/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ffmpegMetadata struct {
|
type ffmpegMetadata struct {
|
||||||
filePath string
|
baseMetadata
|
||||||
suffix string
|
|
||||||
fileInfo os.FileInfo
|
|
||||||
tags map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ffmpegMetadata) Title() string { return m.getTag("title", "sort_name") }
|
func (m *ffmpegMetadata) Duration() float32 { return m.parseDuration("duration") }
|
||||||
func (m *ffmpegMetadata) Album() string { return m.getTag("album", "sort_album") }
|
func (m *ffmpegMetadata) BitRate() int { return m.parseInt("bitrate") }
|
||||||
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 *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 {
|
func (m *ffmpegMetadata) HasPicture() bool {
|
||||||
return m.getTag("has_picture", "metadata_block_picture") != ""
|
return m.getTag("has_picture", "metadata_block_picture") != ""
|
||||||
}
|
}
|
||||||
func (m *ffmpegMetadata) Comment() string { return m.getTag("comment") }
|
func (m *ffmpegMetadata) DiscNumber() (int, int) { return m.parseTuple("tpa", "disc") }
|
||||||
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() }
|
|
||||||
|
|
||||||
type ffmpegMetadataExtractor struct{}
|
type ffmpegExtractor struct{}
|
||||||
|
|
||||||
func (e *ffmpegMetadataExtractor) Extract(files ...string) (map[string]Metadata, error) {
|
func (e *ffmpegExtractor) Extract(files ...string) (map[string]Metadata, error) {
|
||||||
args := createProbeCommand(files)
|
args := createProbeCommand(files)
|
||||||
|
|
||||||
log.Trace("Executing command", "args", args)
|
log.Trace("Executing command", "args", args)
|
||||||
|
@ -116,8 +86,9 @@ func parseOutput(output string) map[string]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractMetadata(filePath, info string) (*ffmpegMetadata, error) {
|
func extractMetadata(filePath, info string) (*ffmpegMetadata, error) {
|
||||||
m := &ffmpegMetadata{filePath: filePath, tags: map[string]string{}}
|
m := &ffmpegMetadata{}
|
||||||
m.suffix = strings.ToLower(strings.TrimPrefix(path.Ext(filePath), "."))
|
m.filePath = filePath
|
||||||
|
m.tags = map[string]string{}
|
||||||
var err error
|
var err error
|
||||||
m.fileInfo, err = os.Stat(filePath)
|
m.fileInfo, err = os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -175,88 +146,6 @@ func (m *ffmpegMetadata) parseInfo(info string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ffmpegMetadata) parseInt(tagName string) int {
|
|
||||||
if v, ok := m.tags[tagName]; ok {
|
|
||||||
i, _ := strconv.Atoi(v)
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateRegex = regexp.MustCompile(`^([12]\d\d\d)`)
|
|
||||||
|
|
||||||
func (m *ffmpegMetadata) parseYear(tagName string) int {
|
|
||||||
if v, ok := m.tags[tagName]; ok {
|
|
||||||
match := dateRegex.FindStringSubmatch(v)
|
|
||||||
if len(match) == 0 {
|
|
||||||
log.Warn("Error parsing year from ffmpeg date field", "file", m.filePath, "date", v)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
year, _ := strconv.Atoi(match[1])
|
|
||||||
return year
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ffmpegMetadata) getTag(tags ...string) string {
|
|
||||||
for _, t := range tags {
|
|
||||||
if v, ok := m.tags[t]; ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
for _, format := range formats {
|
|
||||||
name := fmt.Sprintf(format, tag)
|
|
||||||
all = append(all, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m.getTag(all...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ffmpegMetadata) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ffmpegMetadata) parseBool(tagName string) bool {
|
|
||||||
if v, ok := m.tags[tagName]; ok {
|
|
||||||
i, _ := strconv.Atoi(strings.TrimSpace(v))
|
|
||||||
return i == 1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return float32(d.Sub(zeroTime).Seconds())
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inputs will always be absolute paths
|
// Inputs will always be absolute paths
|
||||||
func createProbeCommand(inputs []string) []string {
|
func createProbeCommand(inputs []string) []string {
|
||||||
split := strings.Split(conf.Server.ProbeCommand, " ")
|
split := strings.Split(conf.Server.ProbeCommand, " ")
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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() {
|
||||||
e := &ffmpegMetadataExtractor{}
|
e := &ffmpegExtractor{}
|
||||||
mds, err := e.Extract("tests/fixtures/test.mp3", "tests/fixtures/test.ogg")
|
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))
|
||||||
|
@ -224,13 +224,15 @@ 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 := &ffmpegMetadata{tags: map[string]string{"date": tag}}
|
md := &ffmpegMetadata{}
|
||||||
|
md.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 := &ffmpegMetadata{tags: map[string]string{"date": "invalid"}}
|
md := &ffmpegMetadata{}
|
||||||
|
md.tags = map[string]string{"date": "invalid"}
|
||||||
Expect(md.Year()).To(Equal(0))
|
Expect(md.Year()).To(Equal(0))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,25 @@
|
||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/deluan/navidrome/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Extractor interface {
|
||||||
|
Extract(files ...string) (map[string]Metadata, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Extract(files ...string) (map[string]Metadata, error) {
|
||||||
|
e := &taglibExtractor{}
|
||||||
|
return e.Extract(files...)
|
||||||
|
}
|
||||||
|
|
||||||
type Metadata interface {
|
type Metadata interface {
|
||||||
Title() string
|
Title() string
|
||||||
|
@ -28,11 +47,122 @@ type Metadata interface {
|
||||||
Size() int64
|
Size() int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Extractor interface {
|
type baseMetadata struct {
|
||||||
Extract(files ...string) (map[string]Metadata, error)
|
filePath string
|
||||||
|
fileInfo os.FileInfo
|
||||||
|
tags map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Extract(files ...string) (map[string]Metadata, error) {
|
func (m *baseMetadata) Title() string { return m.getTag("title", "sort_name") }
|
||||||
e := &ffmpegMetadataExtractor{}
|
func (m *baseMetadata) Album() string { return m.getTag("album", "sort_album") }
|
||||||
return e.Extract(files...)
|
func (m *baseMetadata) Artist() string { return m.getTag("artist", "sort_artist") }
|
||||||
|
func (m *baseMetadata) AlbumArtist() string { return m.getTag("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) Compilation() bool { return m.parseBool("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) 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) parseInt(tagName string) int {
|
||||||
|
if v, ok := m.tags[tagName]; ok {
|
||||||
|
i, _ := strconv.Atoi(v)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateRegex = regexp.MustCompile(`^([12]\d\d\d)`)
|
||||||
|
|
||||||
|
func (m *baseMetadata) parseYear(tagName string) int {
|
||||||
|
if v, ok := m.tags[tagName]; ok {
|
||||||
|
match := dateRegex.FindStringSubmatch(v)
|
||||||
|
if len(match) == 0 {
|
||||||
|
log.Warn("Error parsing year from ffmpeg date field", "file", m.filePath, "date", v)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
year, _ := strconv.Atoi(match[1])
|
||||||
|
return year
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *baseMetadata) getTag(tags ...string) string {
|
||||||
|
for _, t := range tags {
|
||||||
|
if v, ok := m.tags[t]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *baseMetadata) 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 {
|
||||||
|
for _, format := range formats {
|
||||||
|
name := fmt.Sprintf(format, tag)
|
||||||
|
all = append(all, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *baseMetadata) parseBool(tagName string) bool {
|
||||||
|
if v, ok := m.tags[tagName]; ok {
|
||||||
|
i, _ := strconv.Atoi(strings.TrimSpace(v))
|
||||||
|
return i == 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
72
scanner/metadata/taglib.go
Normal file
72
scanner/metadata/taglib.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/deluan/navidrome/log"
|
||||||
|
"github.com/dhowden/tag"
|
||||||
|
"github.com/nicksellen/audiotags"
|
||||||
|
)
|
||||||
|
|
||||||
|
type taglibMetadata struct {
|
||||||
|
baseMetadata
|
||||||
|
props *audiotags.AudioProperties
|
||||||
|
hasPicture bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *taglibMetadata) Duration() float32 { return float32(m.props.Length) }
|
||||||
|
func (m *taglibMetadata) BitRate() int { return m.props.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{}
|
||||||
|
var err error
|
||||||
|
for _, path := range paths {
|
||||||
|
md, err := e.extractMetadata(path)
|
||||||
|
if err == nil {
|
||||||
|
mds[path] = md
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mds, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, md.props, err = audiotags.Read(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
|
||||||
|
return nil, errors.New("error reading tags")
|
||||||
|
}
|
||||||
|
md.hasPicture = hasEmbeddedImage(filePath)
|
||||||
|
return md, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEmbeddedImage(path string) bool {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error opening file", "filePath", path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
m, err := tag.ReadFrom(f)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Error reading tags from file", "filePath", path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Picture() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Metadata = (*taglibMetadata)(nil)
|
||||||
|
var _ Extractor = (*taglibExtractor)(nil)
|
Loading…
Add table
Add a link
Reference in a new issue