Foundational work to enable multi-valued tags
This commit is contained in:
+9
-9
@@ -25,7 +25,7 @@ func newMediaFileMapper(rootFolder string) *mediaFileMapper {
|
|||||||
return &mediaFileMapper{rootFolder: rootFolder, policy: bluemonday.UGCPolicy()}
|
return &mediaFileMapper{rootFolder: rootFolder, policy: bluemonday.UGCPolicy()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mediaFileMapper) toMediaFile(md metadata.Metadata) model.MediaFile {
|
func (s *mediaFileMapper) toMediaFile(md *metadata.Tags) 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)
|
||||||
@@ -76,7 +76,7 @@ func sanitizeFieldForSorting(originalValue string) string {
|
|||||||
return utils.NoArticle(v)
|
return utils.NoArticle(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string {
|
func (s *mediaFileMapper) mapTrackTitle(md *metadata.Tags) 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)
|
||||||
@@ -85,7 +85,7 @@ func (s *mediaFileMapper) mapTrackTitle(md metadata.Metadata) string {
|
|||||||
return md.Title()
|
return md.Title()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mediaFileMapper) mapAlbumArtistName(md metadata.Metadata) string {
|
func (s *mediaFileMapper) mapAlbumArtistName(md *metadata.Tags) string {
|
||||||
switch {
|
switch {
|
||||||
case md.Compilation():
|
case md.Compilation():
|
||||||
return consts.VariousArtists
|
return consts.VariousArtists
|
||||||
@@ -98,14 +98,14 @@ func (s *mediaFileMapper) mapAlbumArtistName(md metadata.Metadata) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mediaFileMapper) mapArtistName(md metadata.Metadata) string {
|
func (s *mediaFileMapper) mapArtistName(md *metadata.Tags) 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.Metadata) string {
|
func (s *mediaFileMapper) mapAlbumName(md *metadata.Tags) string {
|
||||||
name := md.Album()
|
name := md.Album()
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return "[Unknown Album]"
|
return "[Unknown Album]"
|
||||||
@@ -113,19 +113,19 @@ func (s *mediaFileMapper) mapAlbumName(md metadata.Metadata) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mediaFileMapper) trackID(md metadata.Metadata) string {
|
func (s *mediaFileMapper) trackID(md *metadata.Tags) 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.Metadata) string {
|
func (s *mediaFileMapper) albumID(md *metadata.Tags) 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.Metadata) string {
|
func (s *mediaFileMapper) artistID(md *metadata.Tags) 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.Metadata) string {
|
func (s *mediaFileMapper) albumArtistID(md *metadata.Tags) 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)))))
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-50
@@ -3,54 +3,37 @@ package metadata
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ffmpegMetadata struct {
|
|
||||||
baseMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ffmpegMetadata) Duration() float32 { return m.parseDuration("duration") }
|
|
||||||
func (m *ffmpegMetadata) BitRate() int { return m.parseInt("bitrate") }
|
|
||||||
func (m *ffmpegMetadata) HasPicture() bool {
|
|
||||||
return m.getTag("has_picture", "metadata_block_picture") != ""
|
|
||||||
}
|
|
||||||
func (m *ffmpegMetadata) DiscNumber() (int, int) { return m.parseTuple("tpa", "disc", "discnumber") }
|
|
||||||
func (m *ffmpegMetadata) Comment() string {
|
|
||||||
comment := m.baseMetadata.Comment()
|
|
||||||
if comment == "Cover (front)" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return comment
|
|
||||||
}
|
|
||||||
|
|
||||||
type ffmpegExtractor struct{}
|
type ffmpegExtractor struct{}
|
||||||
|
|
||||||
func (e *ffmpegExtractor) Extract(files ...string) (map[string]Metadata, error) {
|
func (e *ffmpegExtractor) Extract(files ...string) (map[string]*Tags, error) {
|
||||||
args := e.createProbeCommand(files)
|
args := e.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{}
|
fileTags := map[string]*Tags{}
|
||||||
if len(output) == 0 {
|
if len(output) == 0 {
|
||||||
return mds, errors.New("error extracting metadata files")
|
return fileTags, errors.New("error extracting metadata files")
|
||||||
}
|
}
|
||||||
infos := e.parseOutput(string(output))
|
infos := e.parseOutput(string(output))
|
||||||
for file, info := range infos {
|
for file, info := range infos {
|
||||||
md, err := e.extractMetadata(file, info)
|
tags, err := e.extractMetadata(file, info)
|
||||||
// Skip files with errors
|
// Skip files with errors
|
||||||
if err == nil {
|
if err == nil {
|
||||||
mds[file] = md
|
fileTags[file] = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mds, nil
|
return fileTags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -95,26 +78,23 @@ func (e *ffmpegExtractor) parseOutput(output string) map[string]string {
|
|||||||
return outputs
|
return outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*ffmpegMetadata, error) {
|
func (e *ffmpegExtractor) extractMetadata(filePath, info string) (*Tags, error) {
|
||||||
m := &ffmpegMetadata{}
|
parsedTags := e.parseInfo(info)
|
||||||
m.filePath = filePath
|
if len(parsedTags) == 0 {
|
||||||
m.tags = map[string]string{}
|
|
||||||
var err error
|
|
||||||
m.fileInfo, err = os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
|
|
||||||
return nil, errors.New("error stating file")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.parseInfo(info)
|
|
||||||
if len(m.tags) == 0 {
|
|
||||||
log.Trace("Not a media file. Skipping", "filePath", filePath)
|
log.Trace("Not a media file. Skipping", "filePath", filePath)
|
||||||
return nil, errors.New("not a media file")
|
return nil, errors.New("not a media file")
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
|
tags := NewTag(filePath, parsedTags, map[string][]string{
|
||||||
|
"disc": {"tpa"},
|
||||||
|
"has_picture": {"metadata_block_picture"},
|
||||||
|
})
|
||||||
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ffmpegMetadata) parseInfo(info string) {
|
func (e *ffmpegExtractor) parseInfo(info string) map[string][]string {
|
||||||
|
tags := map[string][]string{}
|
||||||
|
|
||||||
reader := strings.NewReader(info)
|
reader := strings.NewReader(info)
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
lastTag := ""
|
lastTag := ""
|
||||||
@@ -128,11 +108,8 @@ func (m *ffmpegMetadata) parseInfo(info string) {
|
|||||||
tagName := strings.TrimSpace(strings.ToLower(match[1]))
|
tagName := strings.TrimSpace(strings.ToLower(match[1]))
|
||||||
if tagName != "" {
|
if tagName != "" {
|
||||||
tagValue := strings.TrimSpace(match[2])
|
tagValue := strings.TrimSpace(match[2])
|
||||||
// Skip when the tag was previously found
|
tags[tagName] = append(tags[tagName], tagValue)
|
||||||
if _, ok := m.tags[tagName]; !ok {
|
|
||||||
m.tags[tagName] = tagValue
|
|
||||||
lastTag = tagName
|
lastTag = tagName
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,8 +117,11 @@ func (m *ffmpegMetadata) parseInfo(info string) {
|
|||||||
if lastTag != "" {
|
if lastTag != "" {
|
||||||
match = continuationRx.FindStringSubmatch(line)
|
match = continuationRx.FindStringSubmatch(line)
|
||||||
if len(match) > 0 {
|
if len(match) > 0 {
|
||||||
tagValue := m.tags[lastTag]
|
if tags[lastTag] == nil {
|
||||||
m.tags[lastTag] = tagValue + "\n" + strings.TrimSpace(match[1])
|
tags[lastTag] = []string{""}
|
||||||
|
}
|
||||||
|
tagValue := tags[lastTag][0]
|
||||||
|
tags[lastTag][0] = tagValue + "\n" + strings.TrimSpace(match[1])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,24 +129,41 @@ func (m *ffmpegMetadata) parseInfo(info string) {
|
|||||||
lastTag = ""
|
lastTag = ""
|
||||||
match = coverRx.FindStringSubmatch(line)
|
match = coverRx.FindStringSubmatch(line)
|
||||||
if len(match) > 0 {
|
if len(match) > 0 {
|
||||||
m.tags["has_picture"] = "true"
|
tags["has_picture"] = []string{"true"}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
match = durationRx.FindStringSubmatch(line)
|
match = durationRx.FindStringSubmatch(line)
|
||||||
if len(match) > 0 {
|
if len(match) > 0 {
|
||||||
m.tags["duration"] = match[1]
|
tags["duration"] = []string{e.parseDuration(match[1])}
|
||||||
if len(match) > 1 {
|
if len(match) > 1 {
|
||||||
m.tags["bitrate"] = match[2]
|
tags["bitrate"] = []string{match[2]}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
match = bitRateRx.FindStringSubmatch(line)
|
match = bitRateRx.FindStringSubmatch(line)
|
||||||
if len(match) > 0 {
|
if len(match) > 0 {
|
||||||
m.tags["bitrate"] = match[2]
|
tags["bitrate"] = []string{match[2]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comment := tags["comment"]
|
||||||
|
if len(comment) > 0 && comment[0] == "Cover (front)" {
|
||||||
|
delete(tags, "comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func (e *ffmpegExtractor) parseDuration(tag string) string {
|
||||||
|
d, err := time.Parse("15:04:05", tag)
|
||||||
|
if err != nil {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return strconv.FormatFloat(d.Sub(zeroTime).Seconds(), 'f', 2, 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inputs will always be absolute paths
|
// Inputs will always be absolute paths
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ var _ = Describe("ffmpegExtractor", func() {
|
|||||||
Expect(m.Album()).To(Equal("Album"))
|
Expect(m.Album()).To(Equal("Album"))
|
||||||
Expect(m.Artist()).To(Equal("Artist"))
|
Expect(m.Artist()).To(Equal("Artist"))
|
||||||
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
||||||
Expect(m.Composer()).To(Equal("Composer"))
|
|
||||||
Expect(m.Compilation()).To(BeTrue())
|
Expect(m.Compilation()).To(BeTrue())
|
||||||
Expect(m.Genre()).To(Equal("Rock"))
|
Expect(m.Genre()).To(Equal("Rock"))
|
||||||
Expect(m.Year()).To(Equal(2014))
|
Expect(m.Year()).To(Equal(2014))
|
||||||
@@ -33,21 +32,21 @@ var _ = Describe("ffmpegExtractor", func() {
|
|||||||
Expect(n).To(Equal(1))
|
Expect(n).To(Equal(1))
|
||||||
Expect(t).To(Equal(2))
|
Expect(t).To(Equal(2))
|
||||||
Expect(m.HasPicture()).To(BeTrue())
|
Expect(m.HasPicture()).To(BeTrue())
|
||||||
Expect(m.Duration()).To(Equal(1))
|
Expect(m.Duration()).To(BeNumerically("~", 1.03, 0.001))
|
||||||
Expect(m.BitRate()).To(Equal(476))
|
Expect(m.BitRate()).To(Equal(192))
|
||||||
Expect(m.FilePath()).To(Equal("tests/fixtures/test.mp3"))
|
Expect(m.FilePath()).To(Equal("tests/fixtures/test.mp3"))
|
||||||
Expect(m.Suffix()).To(Equal("mp3"))
|
Expect(m.Suffix()).To(Equal("mp3"))
|
||||||
Expect(m.Size()).To(Equal(60845))
|
Expect(m.Size()).To(Equal(int64(51876)))
|
||||||
|
|
||||||
m = mds["tests/fixtures/test.ogg"]
|
m = mds["tests/fixtures/test.ogg"]
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(m.Title()).To(BeEmpty())
|
Expect(m.Title()).To(BeEmpty())
|
||||||
Expect(m.HasPicture()).To(BeFalse())
|
Expect(m.HasPicture()).To(BeFalse())
|
||||||
Expect(m.Duration()).To(Equal(3))
|
Expect(m.Duration()).To(BeNumerically("~", 1.04, 0.001))
|
||||||
Expect(m.BitRate()).To(Equal(9))
|
Expect(m.BitRate()).To(Equal(16))
|
||||||
Expect(m.Suffix()).To(Equal("ogg"))
|
Expect(m.Suffix()).To(Equal("ogg"))
|
||||||
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
|
Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
|
||||||
Expect(m.Size()).To(Equal(4408))
|
Expect(m.Size()).To(Equal(int64(5065)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+123
-168
@@ -16,10 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Extractor interface {
|
type Extractor interface {
|
||||||
Extract(files ...string) (map[string]Metadata, error)
|
Extract(files ...string) (map[string]*Tags, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Extract(files ...string) (map[string]Metadata, error) {
|
func Extract(files ...string) (map[string]*Tags, error) {
|
||||||
var e Extractor
|
var e Extractor
|
||||||
|
|
||||||
switch conf.Server.Scanner.Extractor {
|
switch conf.Server.Scanner.Extractor {
|
||||||
@@ -35,167 +35,97 @@ func Extract(files ...string) (map[string]Metadata, error) {
|
|||||||
return e.Extract(files...)
|
return e.Extract(files...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata interface {
|
type Tags struct {
|
||||||
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
|
|
||||||
Lyrics() string
|
|
||||||
Compilation() bool
|
|
||||||
CatalogNum() string
|
|
||||||
MbzTrackID() string
|
|
||||||
MbzAlbumID() string
|
|
||||||
MbzArtistID() string
|
|
||||||
MbzAlbumArtistID() string
|
|
||||||
MbzAlbumType() string
|
|
||||||
MbzAlbumComment() string
|
|
||||||
Duration() float32
|
|
||||||
BitRate() int
|
|
||||||
ModificationTime() time.Time
|
|
||||||
FilePath() string
|
|
||||||
Suffix() string
|
|
||||||
Size() int64
|
|
||||||
Bpm() int
|
|
||||||
}
|
|
||||||
|
|
||||||
type baseMetadata struct {
|
|
||||||
filePath string
|
filePath string
|
||||||
|
suffix string
|
||||||
fileInfo os.FileInfo
|
fileInfo os.FileInfo
|
||||||
tags map[string]string
|
tags map[string][]string
|
||||||
|
custom map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) Title() string { return m.getTag("title", "sort_name", "titlesort") }
|
func NewTag(filePath string, tags, custom map[string][]string) *Tags {
|
||||||
func (m *baseMetadata) Album() string { return m.getTag("album", "sort_album", "albumsort") }
|
fileInfo, err := os.Stat(filePath)
|
||||||
func (m *baseMetadata) Artist() string { return m.getTag("artist", "sort_artist", "artistsort") }
|
if err != nil {
|
||||||
func (m *baseMetadata) AlbumArtist() string {
|
log.Warn("Error stating file. Skipping", "filePath", filePath, err)
|
||||||
return m.getTag("album_artist", "album artist", "albumartist")
|
return nil
|
||||||
}
|
}
|
||||||
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) Lyrics() string { return m.getTag("lyrics", "lyrics-eng") }
|
|
||||||
func (m *baseMetadata) Compilation() bool { return m.parseBool("tcmp", "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) CatalogNum() string { return m.getTag("catalognumber") }
|
|
||||||
func (m *baseMetadata) MbzTrackID() string {
|
|
||||||
return m.getMbzID("musicbrainz_trackid", "musicbrainz track id")
|
|
||||||
}
|
|
||||||
func (m *baseMetadata) MbzAlbumID() string {
|
|
||||||
return m.getMbzID("musicbrainz_albumid", "musicbrainz album id")
|
|
||||||
}
|
|
||||||
func (m *baseMetadata) MbzArtistID() string {
|
|
||||||
return m.getMbzID("musicbrainz_artistid", "musicbrainz artist id")
|
|
||||||
}
|
|
||||||
func (m *baseMetadata) MbzAlbumArtistID() string {
|
|
||||||
return m.getMbzID("musicbrainz_albumartistid", "musicbrainz album artist id")
|
|
||||||
}
|
|
||||||
func (m *baseMetadata) MbzAlbumType() string {
|
|
||||||
return m.getTag("musicbrainz_albumtype", "musicbrainz album type")
|
|
||||||
}
|
|
||||||
func (m *baseMetadata) MbzAlbumComment() string {
|
|
||||||
return m.getTag("musicbrainz_albumcomment", "musicbrainz album comment")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *baseMetadata) ModificationTime() time.Time { return m.fileInfo.ModTime() }
|
return &Tags{
|
||||||
func (m *baseMetadata) Size() int64 { return m.fileInfo.Size() }
|
filePath: filePath,
|
||||||
func (m *baseMetadata) FilePath() string { return m.filePath }
|
suffix: strings.ToLower(strings.TrimPrefix(path.Ext(filePath), ".")),
|
||||||
func (m *baseMetadata) Suffix() string {
|
fileInfo: fileInfo,
|
||||||
return strings.ToLower(strings.TrimPrefix(path.Ext(m.FilePath()), "."))
|
tags: tags,
|
||||||
}
|
custom: custom,
|
||||||
|
|
||||||
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) Bpm() int {
|
|
||||||
var bpmStr = m.getTag("tbpm", "bpm", "fbpm")
|
|
||||||
var bpmFloat, err = strconv.ParseFloat(bpmStr, 64)
|
|
||||||
if err == nil {
|
|
||||||
return (int)(math.Round(bpmFloat))
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) parseInt(tagName string) int {
|
// Common tags
|
||||||
if v, ok := m.tags[tagName]; ok {
|
|
||||||
i, _ := strconv.Atoi(v)
|
func (t *Tags) Title() string { return t.getTag("title", "sort_name", "titlesort") }
|
||||||
return i
|
func (t *Tags) Album() string { return t.getTag("album", "sort_album", "albumsort") }
|
||||||
}
|
func (t *Tags) Artist() string { return t.getTag("artist", "sort_artist", "artistsort") }
|
||||||
return 0
|
func (t *Tags) AlbumArtist() string { return t.getTag("album_artist", "album artist", "albumartist") }
|
||||||
|
func (t *Tags) SortTitle() string { return t.getSortTag("", "title", "name") }
|
||||||
|
func (t *Tags) SortAlbum() string { return t.getSortTag("", "album") }
|
||||||
|
func (t *Tags) SortArtist() string { return t.getSortTag("", "artist") }
|
||||||
|
func (t *Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") }
|
||||||
|
func (t *Tags) Genre() string { return t.getTag("genre") }
|
||||||
|
func (t *Tags) Year() int { return t.getYear("date") }
|
||||||
|
func (t *Tags) Comment() string { return t.getTag("comment") }
|
||||||
|
func (t *Tags) Lyrics() string { return t.getTag("lyrics", "lyrics-eng") }
|
||||||
|
func (t *Tags) Compilation() bool { return t.getBool("tcmp", "compilation") }
|
||||||
|
func (t *Tags) TrackNumber() (int, int) { return t.getTuple("track", "tracknumber") }
|
||||||
|
func (t *Tags) DiscNumber() (int, int) { return t.getTuple("disc", "discnumber") }
|
||||||
|
func (t *Tags) DiscSubtitle() string { return t.getTag("tsst", "discsubtitle", "setsubtitle") }
|
||||||
|
func (t *Tags) CatalogNum() string { return t.getTag("catalognumber") }
|
||||||
|
func (t *Tags) Bpm() int { return (int)(math.Round(t.getFloat("tbpm", "bpm", "fbpm"))) }
|
||||||
|
func (t *Tags) HasPicture() bool { return t.getTag("has_picture") != "" }
|
||||||
|
|
||||||
|
// MusicBrainz Identifiers
|
||||||
|
|
||||||
|
func (t *Tags) MbzTrackID() string { return t.getMbzID("musicbrainz_trackid", "musicbrainz track id") }
|
||||||
|
func (t *Tags) MbzAlbumID() string { return t.getMbzID("musicbrainz_albumid", "musicbrainz album id") }
|
||||||
|
func (t *Tags) MbzArtistID() string {
|
||||||
|
return t.getMbzID("musicbrainz_artistid", "musicbrainz artist id")
|
||||||
|
}
|
||||||
|
func (t *Tags) MbzAlbumArtistID() string {
|
||||||
|
return t.getMbzID("musicbrainz_albumartistid", "musicbrainz album artist id")
|
||||||
|
}
|
||||||
|
func (t *Tags) MbzAlbumType() string {
|
||||||
|
return t.getTag("musicbrainz_albumtype", "musicbrainz album type")
|
||||||
|
}
|
||||||
|
func (t *Tags) MbzAlbumComment() string {
|
||||||
|
return t.getTag("musicbrainz_albumcomment", "musicbrainz album comment")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) parseFloat(tagName string) float32 {
|
// File properties
|
||||||
if v, ok := m.tags[tagName]; ok {
|
|
||||||
f, _ := strconv.ParseFloat(v, 32)
|
|
||||||
return float32(f)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
|
func (t *Tags) Duration() float32 { return float32(t.getFloat("duration")) }
|
||||||
|
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 (m *baseMetadata) parseYear(tags ...string) int {
|
func (t *Tags) getTags(tags ...string) []string {
|
||||||
for _, t := range tags {
|
allTags := append(tags, t.custom[tags[0]]...)
|
||||||
if v, ok := m.tags[t]; ok {
|
for _, tag := range allTags {
|
||||||
match := dateRegex.FindStringSubmatch(v)
|
if v, ok := t.tags[tag]; ok {
|
||||||
if len(match) == 0 {
|
|
||||||
log.Warn("Error parsing year date field", "file", m.filePath, "date", v)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
year, _ := strconv.Atoi(match[1])
|
|
||||||
return year
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *baseMetadata) getMbzID(tags ...string) string {
|
|
||||||
var value string
|
|
||||||
for _, t := range tags {
|
|
||||||
if v, ok := m.tags[t]; ok {
|
|
||||||
value = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := uuid.Parse(value); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *baseMetadata) getTag(tags ...string) string {
|
|
||||||
for _, t := range tags {
|
|
||||||
if v, ok := m.tags[t]; ok {
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tags) getTag(tags ...string) string {
|
||||||
|
ts := t.getTags(tags...)
|
||||||
|
if len(ts) > 0 {
|
||||||
|
return ts[0]
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) getSortTag(originalTag string, tags ...string) string {
|
func (t *Tags) 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 {
|
||||||
@@ -204,45 +134,70 @@ func (m *baseMetadata) getSortTag(originalTag string, tags ...string) string {
|
|||||||
all = append(all, name)
|
all = append(all, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.getTag(all...)
|
return t.getTag(all...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) parseTuple(tags ...string) (int, int) {
|
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
|
||||||
for _, tagName := range tags {
|
|
||||||
if v, ok := m.tags[tagName]; ok {
|
func (t *Tags) getYear(tags ...string) int {
|
||||||
tuple := strings.Split(v, "/")
|
tag := t.getTag(tags...)
|
||||||
|
if tag == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
match := dateRegex.FindStringSubmatch(tag)
|
||||||
|
if len(match) == 0 {
|
||||||
|
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
year, _ := strconv.Atoi(match[1])
|
||||||
|
return year
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tags) getBool(tags ...string) bool {
|
||||||
|
tag := t.getTag(tags...)
|
||||||
|
if tag == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i, _ := strconv.Atoi(strings.TrimSpace(tag))
|
||||||
|
return i == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tags) getTuple(tags ...string) (int, int) {
|
||||||
|
tag := t.getTag(tags...)
|
||||||
|
if tag == "" {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
tuple := strings.Split(tag, "/")
|
||||||
t1, t2 := 0, 0
|
t1, t2 := 0, 0
|
||||||
t1, _ = strconv.Atoi(tuple[0])
|
t1, _ = strconv.Atoi(tuple[0])
|
||||||
if len(tuple) > 1 {
|
if len(tuple) > 1 {
|
||||||
t2, _ = strconv.Atoi(tuple[1])
|
t2, _ = strconv.Atoi(tuple[1])
|
||||||
} else {
|
} else {
|
||||||
t2, _ = strconv.Atoi(m.tags[tagName+"total"])
|
t2tag := t.getTag(tags[0] + "total")
|
||||||
|
t2, _ = strconv.Atoi(t2tag)
|
||||||
}
|
}
|
||||||
return t1, t2
|
return t1, t2
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) parseBool(tags ...string) bool {
|
func (t *Tags) getMbzID(tags ...string) string {
|
||||||
for _, tagName := range tags {
|
tag := t.getTag(tags...)
|
||||||
if v, ok := m.tags[tagName]; ok {
|
if _, err := uuid.Parse(tag); err != nil {
|
||||||
i, _ := strconv.Atoi(strings.TrimSpace(v))
|
return ""
|
||||||
return i == 1
|
|
||||||
}
|
}
|
||||||
}
|
return tag
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
func (t *Tags) getInt(tags ...string) int {
|
||||||
|
tag := t.getTag(tags...)
|
||||||
|
i, _ := strconv.Atoi(tag)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
func (m *baseMetadata) parseDuration(tagName string) float32 {
|
func (t *Tags) getFloat(tags ...string) float64 {
|
||||||
if v, ok := m.tags[tagName]; ok {
|
var tag = t.getTag(tags...)
|
||||||
d, err := time.Parse("15:04:05", v)
|
var value, err = strconv.ParseFloat(tag, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return float32(d.Sub(zeroTime).Seconds())
|
return value
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("ffmpegMetadata", func() {
|
var _ = Describe("Tags", func() {
|
||||||
Describe("parseYear", func() {
|
Describe("getYear", func() {
|
||||||
It("parses the year correctly", func() {
|
It("parses the year correctly", func() {
|
||||||
var examples = map[string]int{
|
var examples = map[string]int{
|
||||||
"1985": 1985,
|
"1985": 1985,
|
||||||
@@ -19,27 +19,27 @@ var _ = Describe("ffmpegMetadata", func() {
|
|||||||
"01/10/1990": 1990,
|
"01/10/1990": 1990,
|
||||||
}
|
}
|
||||||
for tag, expected := range examples {
|
for tag, expected := range examples {
|
||||||
md := &baseMetadata{}
|
md := &Tags{}
|
||||||
md.tags = map[string]string{"date": tag}
|
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 := &baseMetadata{}
|
md := &Tags{}
|
||||||
md.tags = map[string]string{"date": "invalid"}
|
md.tags = map[string][]string{"date": {"invalid"}}
|
||||||
Expect(md.Year()).To(Equal(0))
|
Expect(md.Year()).To(Equal(0))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("getMbzID", func() {
|
Describe("getMbzID", func() {
|
||||||
It("return a valid MBID", func() {
|
It("return a valid MBID", func() {
|
||||||
md := &baseMetadata{}
|
md := &Tags{}
|
||||||
md.tags = map[string]string{
|
md.tags = map[string][]string{
|
||||||
"musicbrainz_trackid": "8f84da07-09a0-477b-b216-cc982dabcde1",
|
"musicbrainz_trackid": {"8f84da07-09a0-477b-b216-cc982dabcde1"},
|
||||||
"musicbrainz_albumid": "f68c985d-f18b-4f4a-b7f0-87837cf3fbf9",
|
"musicbrainz_albumid": {"f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"},
|
||||||
"musicbrainz_artistid": "89ad4ac3-39f7-470e-963a-56509c546377",
|
"musicbrainz_artistid": {"89ad4ac3-39f7-470e-963a-56509c546377"},
|
||||||
"musicbrainz_albumartistid": "ada7a83c-e3e1-40f1-93f9-3e73dbc9298a",
|
"musicbrainz_albumartistid": {"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"},
|
||||||
}
|
}
|
||||||
Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
|
Expect(md.MbzTrackID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1"))
|
||||||
Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"))
|
Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"))
|
||||||
@@ -47,12 +47,12 @@ var _ = Describe("ffmpegMetadata", func() {
|
|||||||
Expect(md.MbzAlbumArtistID()).To(Equal("ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"))
|
Expect(md.MbzAlbumArtistID()).To(Equal("ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"))
|
||||||
})
|
})
|
||||||
It("return empty string for invalid MBID", func() {
|
It("return empty string for invalid MBID", func() {
|
||||||
md := &baseMetadata{}
|
md := &Tags{}
|
||||||
md.tags = map[string]string{
|
md.tags = map[string][]string{
|
||||||
"musicbrainz_trackid": "11406732-6",
|
"musicbrainz_trackid": {"11406732-6"},
|
||||||
"musicbrainz_albumid": "11406732",
|
"musicbrainz_albumid": {"11406732"},
|
||||||
"musicbrainz_artistid": "200455",
|
"musicbrainz_artistid": {"200455"},
|
||||||
"musicbrainz_albumartistid": "194",
|
"musicbrainz_albumartistid": {"194"},
|
||||||
}
|
}
|
||||||
Expect(md.MbzTrackID()).To(Equal(""))
|
Expect(md.MbzTrackID()).To(Equal(""))
|
||||||
Expect(md.MbzAlbumID()).To(Equal(""))
|
Expect(md.MbzAlbumID()).To(Equal(""))
|
||||||
|
|||||||
+22
-38
@@ -1,7 +1,6 @@
|
|||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/dhowden/tag"
|
"github.com/dhowden/tag"
|
||||||
@@ -9,51 +8,39 @@ import (
|
|||||||
"github.com/navidrome/navidrome/scanner/metadata/taglib"
|
"github.com/navidrome/navidrome/scanner/metadata/taglib"
|
||||||
)
|
)
|
||||||
|
|
||||||
type taglibMetadata struct {
|
|
||||||
baseMetadata
|
|
||||||
hasPicture bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *taglibMetadata) Title() string { return m.getTag("title", "titlesort", "_track") }
|
|
||||||
func (m *taglibMetadata) Album() string { return m.getTag("album", "albumsort", "_album") }
|
|
||||||
func (m *taglibMetadata) Artist() string { return m.getTag("artist", "artistsort", "_artist") }
|
|
||||||
func (m *taglibMetadata) Genre() string { return m.getTag("genre", "_genre") }
|
|
||||||
func (m *taglibMetadata) Year() int { return m.parseYear("date", "_year") }
|
|
||||||
func (m *taglibMetadata) TrackNumber() (int, int) {
|
|
||||||
return m.parseTuple("track", "tracknumber", "_track")
|
|
||||||
}
|
|
||||||
func (m *taglibMetadata) Duration() float32 { return m.parseFloat("length") }
|
|
||||||
func (m *taglibMetadata) BitRate() int { return m.parseInt("bitrate") }
|
|
||||||
func (m *taglibMetadata) HasPicture() bool { return m.hasPicture }
|
|
||||||
|
|
||||||
type taglibExtractor struct{}
|
type taglibExtractor struct{}
|
||||||
|
|
||||||
func (e *taglibExtractor) Extract(paths ...string) (map[string]Metadata, error) {
|
func (e *taglibExtractor) Extract(paths ...string) (map[string]*Tags, error) {
|
||||||
mds := map[string]Metadata{}
|
fileTags := map[string]*Tags{}
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
md, err := e.extractMetadata(path)
|
tags, err := e.extractMetadata(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
mds[path] = md
|
fileTags[path] = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mds, nil
|
return fileTags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *taglibExtractor) extractMetadata(filePath string) (*taglibMetadata, error) {
|
func (e *taglibExtractor) extractMetadata(filePath string) (*Tags, error) {
|
||||||
var err error
|
parsedTags, err := taglib.Read(filePath)
|
||||||
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, err = taglib.Read(filePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
|
log.Warn("Error reading metadata from file. Skipping", "filePath", filePath, err)
|
||||||
}
|
}
|
||||||
md.hasPicture = hasEmbeddedImage(filePath)
|
if hasEmbeddedImage(filePath) {
|
||||||
return md, nil
|
parsedTags["has_picture"] = []string{"true"}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := NewTag(filePath, parsedTags, map[string][]string{
|
||||||
|
"title": {"_track", "titlesort"},
|
||||||
|
"album": {"_album", "albumsort"},
|
||||||
|
"artist": {"_artist", "artistsort"},
|
||||||
|
"genre": {"_genre"},
|
||||||
|
"date": {"_year"},
|
||||||
|
"track": {"_track"},
|
||||||
|
"duration": {"length"},
|
||||||
|
})
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasEmbeddedImage(path string) bool {
|
func hasEmbeddedImage(path string) bool {
|
||||||
@@ -77,6 +64,3 @@ func hasEmbeddedImage(path string) bool {
|
|||||||
|
|
||||||
return m.Picture() != nil
|
return m.Picture() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Metadata = (*taglibMetadata)(nil)
|
|
||||||
var _ Extractor = (*taglibExtractor)(nil)
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Read(filename string) (map[string]string, error) {
|
func Read(filename string) (map[string][]string, error) {
|
||||||
fp := C.CString(filename)
|
fp := C.CString(filename)
|
||||||
defer C.free(unsafe.Pointer(fp))
|
defer C.free(unsafe.Pointer(fp))
|
||||||
id, m := newMap()
|
id, m := newMap()
|
||||||
@@ -44,15 +44,15 @@ func Read(filename string) (map[string]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lock sync.RWMutex
|
var lock sync.RWMutex
|
||||||
var maps = make(map[uint32]map[string]string)
|
var maps = make(map[uint32]map[string][]string)
|
||||||
var mapsNextID uint32
|
var mapsNextID uint32
|
||||||
|
|
||||||
func newMap() (id uint32, m map[string]string) {
|
func newMap() (id uint32, m map[string][]string) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
id = mapsNextID
|
id = mapsNextID
|
||||||
mapsNextID++
|
mapsNextID++
|
||||||
m = make(map[string]string)
|
m = make(map[string][]string)
|
||||||
maps[id] = m
|
maps[id] = m
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -69,10 +69,8 @@ func go_map_put_str(id C.ulong, key *C.char, val *C.char) {
|
|||||||
defer lock.RUnlock()
|
defer lock.RUnlock()
|
||||||
m := maps[uint32(id)]
|
m := maps[uint32(id)]
|
||||||
k := strings.ToLower(C.GoString(key))
|
k := strings.ToLower(C.GoString(key))
|
||||||
if _, ok := m[k]; !ok {
|
|
||||||
v := strings.TrimSpace(C.GoString(val))
|
v := strings.TrimSpace(C.GoString(val))
|
||||||
m[k] = v
|
m[k] = append(m[k], v)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export go_map_put_int
|
//export go_map_put_int
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ var _ = Describe("taglibExtractor", func() {
|
|||||||
Expect(m.Album()).To(Equal("Album"))
|
Expect(m.Album()).To(Equal("Album"))
|
||||||
Expect(m.Artist()).To(Equal("Artist"))
|
Expect(m.Artist()).To(Equal("Artist"))
|
||||||
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
||||||
Expect(m.Composer()).To(Equal("Composer"))
|
|
||||||
Expect(m.Compilation()).To(BeTrue())
|
Expect(m.Compilation()).To(BeTrue())
|
||||||
Expect(m.Genre()).To(Equal("Rock"))
|
Expect(m.Genre()).To(Equal("Rock"))
|
||||||
Expect(m.Year()).To(Equal(2014))
|
Expect(m.Year()).To(Equal(2014))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const (
|
|||||||
filesBatchSize = 100
|
filesBatchSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagScanner algorithm overview:
|
// Scan algorithm overview:
|
||||||
// Load all directories from the DB
|
// Load all directories from the DB
|
||||||
// Traverse the music folder, collecting each subfolder's ModTime (self or any non-dir children, whichever is newer)
|
// Traverse the music folder, collecting each subfolder's ModTime (self or any non-dir children, whichever is newer)
|
||||||
// For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file:
|
// For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file:
|
||||||
|
|||||||
Reference in New Issue
Block a user