feat(subsonic): make it easier to add more tag reading backends
related https://github.com/sentriz/gonic/issues/379 related https://github.com/sentriz/gonic/issues/324 related https://github.com/sentriz/gonic/issues/244
This commit is contained in:
@@ -19,8 +19,7 @@ import (
|
||||
"github.com/rainycape/unidecode"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/mime"
|
||||
"go.senan.xyz/gonic/scanner/tags"
|
||||
"go.senan.xyz/gonic/scanner/tags/tagcommon"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -32,7 +31,7 @@ type Scanner struct {
|
||||
db *db.DB
|
||||
musicDirs []string
|
||||
multiValueSettings map[Tag]MultiValueSetting
|
||||
tagger tags.Reader
|
||||
tagReader tagcommon.Reader
|
||||
excludePattern *regexp.Regexp
|
||||
scanning *int32
|
||||
watcher *fsnotify.Watcher
|
||||
@@ -40,7 +39,7 @@ type Scanner struct {
|
||||
watchDone chan bool
|
||||
}
|
||||
|
||||
func New(musicDirs []string, db *db.DB, multiValueSettings map[Tag]MultiValueSetting, tagger tags.Reader, excludePattern string) *Scanner {
|
||||
func New(musicDirs []string, db *db.DB, multiValueSettings map[Tag]MultiValueSetting, tagReader tagcommon.Reader, excludePattern string) *Scanner {
|
||||
var excludePatternRegExp *regexp.Regexp
|
||||
if excludePattern != "" {
|
||||
excludePatternRegExp = regexp.MustCompile(excludePattern)
|
||||
@@ -50,7 +49,7 @@ func New(musicDirs []string, db *db.DB, multiValueSettings map[Tag]MultiValueSet
|
||||
db: db,
|
||||
musicDirs: musicDirs,
|
||||
multiValueSettings: multiValueSettings,
|
||||
tagger: tagger,
|
||||
tagReader: tagReader,
|
||||
excludePattern: excludePatternRegExp,
|
||||
scanning: new(int32),
|
||||
watchMap: make(map[string]string),
|
||||
@@ -282,9 +281,12 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, musicDir string, absPath string
|
||||
var tracks []string
|
||||
var cover string
|
||||
for _, item := range items {
|
||||
fullpath := filepath.Join(absPath, item.Name())
|
||||
if s.excludePattern != nil && s.excludePattern.MatchString(fullpath) {
|
||||
log.Printf("excluding path `%s`", fullpath)
|
||||
absPath := filepath.Join(absPath, item.Name())
|
||||
if s.excludePattern != nil && s.excludePattern.MatchString(absPath) {
|
||||
log.Printf("excluding path `%s`", absPath)
|
||||
continue
|
||||
}
|
||||
if item.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -292,7 +294,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, musicDir string, absPath string
|
||||
cover = item.Name()
|
||||
continue
|
||||
}
|
||||
if mime := mime.TypeByAudioExtension(filepath.Ext(item.Name())); mime != "" {
|
||||
if s.tagReader.CanRead(absPath) {
|
||||
tracks = append(tracks, item.Name())
|
||||
continue
|
||||
}
|
||||
@@ -342,12 +344,12 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb
|
||||
return nil
|
||||
}
|
||||
|
||||
trags, err := s.tagger.Read(absPath)
|
||||
trags, err := s.tagReader.Read(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", err, ErrReadingTags)
|
||||
}
|
||||
|
||||
genreNames := parseMulti(trags, s.multiValueSettings[Genre], tags.MustGenres, tags.MustGenre)
|
||||
genreNames := parseMulti(trags, s.multiValueSettings[Genre], tagcommon.MustGenres, tagcommon.MustGenre)
|
||||
genreIDs, err := populateGenres(tx, genreNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("populate genres: %w", err)
|
||||
@@ -355,7 +357,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb
|
||||
|
||||
// metadata for the album table comes only from the first track's tags
|
||||
if i == 0 {
|
||||
albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tags.MustAlbumArtists, tags.MustAlbumArtist)
|
||||
albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tagcommon.MustAlbumArtists, tagcommon.MustAlbumArtist)
|
||||
var albumArtistIDs []int
|
||||
for _, albumArtistName := range albumArtistNames {
|
||||
albumArtist, err := populateArtist(tx, albumArtistName)
|
||||
@@ -390,8 +392,8 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAlbum(tx *db.DB, album *db.Album, trags tags.Parser, modTime time.Time) error {
|
||||
albumName := tags.MustAlbum(trags)
|
||||
func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime time.Time) error {
|
||||
albumName := tagcommon.MustAlbum(trags)
|
||||
album.TagTitle = albumName
|
||||
album.TagTitleUDec = decoded(albumName)
|
||||
album.TagBrainzID = trags.AlbumBrainzID()
|
||||
@@ -431,7 +433,7 @@ func populateAlbumBasics(tx *db.DB, musicDir string, parent, album *db.Album, di
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tags.Parser, absPath string, size int) error {
|
||||
func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon.Info, absPath string, size int) error {
|
||||
basename := filepath.Base(absPath)
|
||||
track.Filename = basename
|
||||
track.FilenameUDec = decoded(basename)
|
||||
@@ -684,7 +686,7 @@ type MultiValueSetting struct {
|
||||
Delim string
|
||||
}
|
||||
|
||||
func parseMulti(parser tags.Parser, setting MultiValueSetting, getMulti func(tags.Parser) []string, get func(tags.Parser) string) []string {
|
||||
func parseMulti(parser tagcommon.Info, setting MultiValueSetting, getMulti func(tagcommon.Info) []string, get func(tagcommon.Info) string) []string {
|
||||
var parts []string
|
||||
switch setting.Mode {
|
||||
case Multi:
|
||||
|
||||
@@ -33,9 +33,8 @@ func FuzzScanner(f *testing.F) {
|
||||
for i := 0; i < toAdd; i++ {
|
||||
path := fmt.Sprintf("artist-%d/album-%d/track-%d.flac", i/6, i/3, i)
|
||||
m.AddTrack(path)
|
||||
m.SetTags(path, func(tags *mockfs.Tags) error {
|
||||
m.SetTags(path, func(tags *mockfs.TagInfo) {
|
||||
fuzzStruct(i, data, seed, tags)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/mockfs"
|
||||
@@ -126,12 +127,11 @@ func TestUpdatedTags(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddTrack("artist-10/album-10/track-10.flac")
|
||||
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = "artist"
|
||||
tags.RawAlbumArtist = "album-artist"
|
||||
tags.RawAlbum = "album"
|
||||
tags.RawTitle = "title"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -146,12 +146,11 @@ func TestUpdatedTags(t *testing.T) {
|
||||
assert.NoError(t, m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", track.AlbumID).Limit(1).Find(&trackArtistA).Error) // updated has tags
|
||||
assert.Equal(t, "album-artist", trackArtistA.Name) // track has tags
|
||||
|
||||
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = "artist-upd"
|
||||
tags.RawAlbumArtist = "album-artist-upd"
|
||||
tags.RawAlbum = "album-upd"
|
||||
tags.RawTitle = "title-upd"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -174,9 +173,8 @@ func TestUpdatedAlbumGenre(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItems()
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawGenre = "gen-a;gen-b"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -185,9 +183,8 @@ func TestUpdatedAlbumGenre(t *testing.T) {
|
||||
assert.NoError(t, m.DB().Preload("Genres").Where("left_path=? AND right_path=?", "artist-0/", "album-0").Find(&album).Error)
|
||||
assert.Equal(t, []string{"gen-a", "gen-b"}, album.GenreStrings())
|
||||
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawGenre = "gen-a-upd;gen-b-upd"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -274,10 +271,10 @@ func TestGenres(t *testing.T) {
|
||||
}
|
||||
|
||||
m.AddItems()
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-a;genre-b"; return nil })
|
||||
m.SetTags("artist-0/album-0/track-1.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-c;genre-d"; return nil })
|
||||
m.SetTags("artist-1/album-2/track-0.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-e;genre-f"; return nil })
|
||||
m.SetTags("artist-1/album-2/track-1.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-g;genre-h"; return nil })
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-a;genre-b" })
|
||||
m.SetTags("artist-0/album-0/track-1.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-c;genre-d" })
|
||||
m.SetTags("artist-1/album-2/track-0.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-e;genre-f" })
|
||||
m.SetTags("artist-1/album-2/track-1.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-g;genre-h" })
|
||||
m.ScanAndClean()
|
||||
|
||||
isGenre("genre-a") // genre exists
|
||||
@@ -297,7 +294,7 @@ func TestGenres(t *testing.T) {
|
||||
isAlbumGenre("artist-0/", "album-0", "genre-a") // album genre exists
|
||||
isAlbumGenre("artist-0/", "album-0", "genre-b") // album genre exists
|
||||
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-aa;genre-bb"; return nil })
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-aa;genre-bb" })
|
||||
m.ScanAndClean()
|
||||
|
||||
isTrackGenre("artist-0/", "album-0", "track-0.flac", "genre-aa") // updated track genre exists
|
||||
@@ -360,12 +357,11 @@ func TestNewAlbumForExistingArtist(t *testing.T) {
|
||||
|
||||
for tr := 0; tr < 3; tr++ {
|
||||
m.AddTrack(fmt.Sprintf("artist-2/new-album/track-%d.mp3", tr))
|
||||
m.SetTags(fmt.Sprintf("artist-2/new-album/track-%d.mp3", tr), func(tags *mockfs.Tags) error {
|
||||
m.SetTags(fmt.Sprintf("artist-2/new-album/track-%d.mp3", tr), func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = "artist-2"
|
||||
tags.RawAlbumArtist = "artist-2"
|
||||
tags.RawAlbum = "new-album"
|
||||
tags.RawTitle = fmt.Sprintf("title-%d", tr)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -385,22 +381,20 @@ func TestMultiFolderWithSharedArtist(t *testing.T) {
|
||||
const artistName = "artist-a"
|
||||
|
||||
m.AddTrack(fmt.Sprintf("m-0/%s/album-a/track-1.flac", artistName))
|
||||
m.SetTags(fmt.Sprintf("m-0/%s/album-a/track-1.flac", artistName), func(tags *mockfs.Tags) error {
|
||||
m.SetTags(fmt.Sprintf("m-0/%s/album-a/track-1.flac", artistName), func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = artistName
|
||||
tags.RawAlbumArtist = artistName
|
||||
tags.RawAlbum = "album-a"
|
||||
tags.RawTitle = "track-1"
|
||||
return nil
|
||||
})
|
||||
m.ScanAndClean()
|
||||
|
||||
m.AddTrack(fmt.Sprintf("m-1/%s/album-a/track-1.flac", artistName))
|
||||
m.SetTags(fmt.Sprintf("m-1/%s/album-a/track-1.flac", artistName), func(tags *mockfs.Tags) error {
|
||||
m.SetTags(fmt.Sprintf("m-1/%s/album-a/track-1.flac", artistName), func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = artistName
|
||||
tags.RawAlbumArtist = artistName
|
||||
tags.RawAlbum = "album-a"
|
||||
tags.RawTitle = "track-1"
|
||||
return nil
|
||||
})
|
||||
m.ScanAndClean()
|
||||
|
||||
@@ -441,15 +435,15 @@ func TestSymlinkedAlbum(t *testing.T) {
|
||||
m.LogAlbums()
|
||||
|
||||
var track db.Track
|
||||
assert.NoError(t, m.DB().Preload("Album.Parent").Find(&track).Error) // track exists
|
||||
assert.NotNil(t, track.Album) // track has album
|
||||
assert.NotZero(t, track.Album.Cover) // album has cover
|
||||
assert.Equal(t, "artist-sym", track.Album.Parent.RightPath) // artist is sym
|
||||
require.NoError(t, m.DB().Preload("Album.Parent").Find(&track).Error) // track exists
|
||||
require.NotNil(t, track.Album) // track has album
|
||||
require.NotZero(t, track.Album.Cover) // album has cover
|
||||
require.Equal(t, "artist-sym", track.Album.Parent.RightPath) // artist is sym
|
||||
|
||||
info, err := os.Stat(track.AbsPath())
|
||||
assert.NoError(t, err) // track resolves
|
||||
assert.False(t, info.IsDir()) // track resolves
|
||||
assert.NotZero(t, info.ModTime()) // track resolves
|
||||
require.NoError(t, err) // track resolves
|
||||
require.False(t, info.IsDir()) // track resolves
|
||||
require.NotZero(t, info.ModTime()) // track resolves
|
||||
}
|
||||
|
||||
func TestSymlinkedSubdiscs(t *testing.T) {
|
||||
@@ -459,12 +453,11 @@ func TestSymlinkedSubdiscs(t *testing.T) {
|
||||
addItem := func(prefix, artist, album, disc, track string) {
|
||||
p := fmt.Sprintf("%s/%s/%s/%s/%s", prefix, artist, album, disc, track)
|
||||
m.AddTrack(p)
|
||||
m.SetTags(p, func(tags *mockfs.Tags) error {
|
||||
m.SetTags(p, func(tags *mockfs.TagInfo) {
|
||||
tags.RawArtist = artist
|
||||
tags.RawAlbumArtist = artist
|
||||
tags.RawAlbum = album
|
||||
tags.RawTitle = track
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -499,11 +492,11 @@ func TestTagErrors(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItemsWithCovers()
|
||||
m.SetTags("artist-1/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
return scanner.ErrReadingTags
|
||||
m.SetTags("artist-1/album-0/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.Error = scanner.ErrReadingTags
|
||||
})
|
||||
m.SetTags("artist-1/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
return scanner.ErrReadingTags
|
||||
m.SetTags("artist-1/album-1/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.Error = scanner.ErrReadingTags
|
||||
})
|
||||
|
||||
ctx, err := m.ScanAndCleanErr()
|
||||
@@ -537,12 +530,11 @@ func TestCompilationAlbumWithoutAlbumArtist(t *testing.T) {
|
||||
for i := 0; i < toAdd; i++ {
|
||||
p := fmt.Sprintf("%s/%s/track-%d.flac", pathArtist, pathAlbum, i)
|
||||
m.AddTrack(p)
|
||||
m.SetTags(p, func(tags *mockfs.Tags) error {
|
||||
m.SetTags(p, func(tags *mockfs.TagInfo) {
|
||||
// don't set an album artist
|
||||
tags.RawTitle = fmt.Sprintf("track %d", i)
|
||||
tags.RawArtist = fmt.Sprintf("artist %d", i)
|
||||
tags.RawAlbum = pathArtist
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -590,7 +582,7 @@ func TestAlbumAndArtistSameNameWeirdness(t *testing.T) {
|
||||
|
||||
add := func(path string, a ...interface{}) {
|
||||
m.AddTrack(fmt.Sprintf(path, a...))
|
||||
m.SetTags(fmt.Sprintf(path, a...), func(tags *mockfs.Tags) error { return nil })
|
||||
m.SetTags(fmt.Sprintf(path, a...), func(tags *mockfs.TagInfo) {})
|
||||
}
|
||||
|
||||
add("an-artist/%s/track-1.flac", name)
|
||||
@@ -610,10 +602,10 @@ func TestNoOrphanedGenres(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItems()
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-a;genre-b"; return nil })
|
||||
m.SetTags("artist-0/album-0/track-1.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-c;genre-d"; return nil })
|
||||
m.SetTags("artist-1/album-2/track-0.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-e;genre-f"; return nil })
|
||||
m.SetTags("artist-1/album-2/track-1.flac", func(tags *mockfs.Tags) error { tags.RawGenre = "genre-g;genre-h"; return nil })
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-a;genre-b" })
|
||||
m.SetTags("artist-0/album-0/track-1.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-c;genre-d" })
|
||||
m.SetTags("artist-1/album-2/track-0.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-e;genre-f" })
|
||||
m.SetTags("artist-1/album-2/track-1.flac", func(tags *mockfs.TagInfo) { tags.RawGenre = "genre-g;genre-h" })
|
||||
m.ScanAndClean()
|
||||
|
||||
m.RemoveAll("artist-0")
|
||||
@@ -631,20 +623,17 @@ func TestMultiArtistSupport(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Mutator"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Yerself Is Steam"
|
||||
tags.RawAlbumArtist = "Mercury Rev"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -682,10 +671,9 @@ func TestMultiArtistSupport(t *testing.T) {
|
||||
state())
|
||||
|
||||
m.RemoveAll("artist-0/album-2")
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega"}
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
@@ -709,20 +697,17 @@ func TestMultiArtistPreload(t *testing.T) {
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Mutator"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.TagInfo) {
|
||||
tags.RawAlbum = "Yerself Is Steam"
|
||||
tags.RawAlbumArtist = "Mercury Rev"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
|
||||
91
scanner/tags/tagcommon/tagcommmon.go
Normal file
91
scanner/tags/tagcommon/tagcommmon.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package tagcommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrUnsupported = errors.New("filetype unsupported")
|
||||
|
||||
type Reader interface {
|
||||
CanRead(absPath string) bool
|
||||
Read(absPath string) (Info, error)
|
||||
}
|
||||
|
||||
type Info interface {
|
||||
Title() string
|
||||
BrainzID() string
|
||||
Artist() string
|
||||
Album() string
|
||||
AlbumArtist() string
|
||||
AlbumArtists() []string
|
||||
AlbumBrainzID() string
|
||||
Genre() string
|
||||
Genres() []string
|
||||
TrackNumber() int
|
||||
DiscNumber() int
|
||||
Length() int
|
||||
Bitrate() int
|
||||
Year() int
|
||||
}
|
||||
|
||||
func MustAlbum(p Info) string {
|
||||
if r := p.Album(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Album"
|
||||
}
|
||||
|
||||
func MustArtist(p Info) string {
|
||||
if r := p.Artist(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
func MustAlbumArtist(p Info) string {
|
||||
if r := p.AlbumArtist(); r != "" {
|
||||
return r
|
||||
}
|
||||
return MustArtist(p)
|
||||
}
|
||||
|
||||
func MustAlbumArtists(p Info) []string {
|
||||
if r := p.AlbumArtists(); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
return []string{MustAlbumArtist(p)}
|
||||
}
|
||||
|
||||
func MustGenre(p Info) string {
|
||||
if r := p.Genre(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Genre"
|
||||
}
|
||||
|
||||
func MustGenres(p Info) []string {
|
||||
if r := p.Genres(); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
return []string{MustGenre(p)}
|
||||
}
|
||||
|
||||
type ChainReader []Reader
|
||||
|
||||
func (cr ChainReader) CanRead(absPath string) bool {
|
||||
for _, reader := range cr {
|
||||
if reader.CanRead(absPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cr ChainReader) Read(absPath string) (Info, error) {
|
||||
for _, reader := range cr {
|
||||
if reader.CanRead(absPath) {
|
||||
return reader.Read(absPath)
|
||||
}
|
||||
}
|
||||
return nil, ErrUnsupported
|
||||
}
|
||||
82
scanner/tags/taglib/taglib.go
Normal file
82
scanner/tags/taglib/taglib.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package taglib
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sentriz/audiotags"
|
||||
"go.senan.xyz/gonic/scanner/tags/tagcommon"
|
||||
)
|
||||
|
||||
type TagLib struct{}
|
||||
|
||||
func (TagLib) CanRead(absPath string) bool {
|
||||
switch ext := filepath.Ext(absPath); ext {
|
||||
case ".mp3", ".flac", ".aac", ".m4a", ".m4b", ".ogg", ".opus", ".wma", ".wav", ".wv":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (TagLib) Read(absPath string) (tagcommon.Info, error) {
|
||||
raw, props, err := audiotags.Read(absPath)
|
||||
return &info{raw, props}, err
|
||||
}
|
||||
|
||||
type info struct {
|
||||
raw map[string][]string
|
||||
props *audiotags.AudioProperties
|
||||
}
|
||||
|
||||
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||
|
||||
func (i *info) Title() string { return first(find(i.raw, "title")) }
|
||||
func (i *info) BrainzID() string { return first(find(i.raw, "musicbrainz_trackid")) } // musicbrainz recording ID
|
||||
func (i *info) Artist() string { return first(find(i.raw, "artist")) }
|
||||
func (i *info) Album() string { return first(find(i.raw, "album")) }
|
||||
func (i *info) AlbumArtist() string { return first(find(i.raw, "albumartist", "album artist")) }
|
||||
func (i *info) AlbumArtists() []string { return find(i.raw, "albumartists", "album_artists") }
|
||||
func (i *info) AlbumBrainzID() string { return first(find(i.raw, "musicbrainz_albumid")) } // musicbrainz release ID
|
||||
func (i *info) Genre() string { return first(find(i.raw, "genre")) }
|
||||
func (i *info) Genres() []string { return find(i.raw, "genres") }
|
||||
func (i *info) TrackNumber() int { return intSep("/", first(find(i.raw, "tracknumber"))) } // eg. 5/12
|
||||
func (i *info) DiscNumber() int { return intSep("/", first(find(i.raw, "discnumber"))) } // eg. 1/2
|
||||
func (i *info) Year() int { return intSep("-", first(find(i.raw, "originaldate", "date", "year"))) } // eg. 2023-12-01
|
||||
func (i *info) Length() int { return i.props.Length }
|
||||
func (i *info) Bitrate() int { return i.props.Bitrate }
|
||||
|
||||
func first[T comparable](is []T) T {
|
||||
var z T
|
||||
for _, i := range is {
|
||||
if i != z {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
func find(m map[string][]string, keys ...string) []string {
|
||||
for _, k := range keys {
|
||||
if r := filterStr(m[k]); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterStr(ss []string) []string {
|
||||
var r []string
|
||||
for _, s := range ss {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
r = append(r, s)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func intSep(sep, in string) int {
|
||||
start, _, _ := strings.Cut(in, sep)
|
||||
out, _ := strconv.Atoi(start)
|
||||
return out
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sentriz/audiotags"
|
||||
)
|
||||
|
||||
type TagReader struct{}
|
||||
|
||||
func (*TagReader) Read(abspath string) (Parser, error) {
|
||||
raw, props, err := audiotags.Read(abspath)
|
||||
return &Tagger{raw, props}, err
|
||||
}
|
||||
|
||||
type Tagger struct {
|
||||
raw map[string][]string
|
||||
props *audiotags.AudioProperties
|
||||
}
|
||||
|
||||
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||
|
||||
func (t *Tagger) Title() string { return first(find(t.raw, "title")) }
|
||||
func (t *Tagger) BrainzID() string { return first(find(t.raw, "musicbrainz_trackid")) } // musicbrainz recording ID
|
||||
func (t *Tagger) Artist() string { return first(find(t.raw, "artist")) }
|
||||
func (t *Tagger) Album() string { return first(find(t.raw, "album")) }
|
||||
func (t *Tagger) AlbumArtist() string { return first(find(t.raw, "albumartist", "album artist")) }
|
||||
func (t *Tagger) AlbumArtists() []string { return find(t.raw, "albumartists", "album_artists") }
|
||||
func (t *Tagger) AlbumBrainzID() string { return first(find(t.raw, "musicbrainz_albumid")) } // musicbrainz release ID
|
||||
func (t *Tagger) Genre() string { return first(find(t.raw, "genre")) }
|
||||
func (t *Tagger) Genres() []string { return find(t.raw, "genres") }
|
||||
|
||||
func (t *Tagger) TrackNumber() int {
|
||||
return intSep("/" /* eg. 5/12 */, first(find(t.raw, "tracknumber")))
|
||||
}
|
||||
|
||||
func (t *Tagger) DiscNumber() int {
|
||||
return intSep("/" /* eg. 1/2 */, first(find(t.raw, "discnumber")))
|
||||
}
|
||||
|
||||
func (t *Tagger) Year() int {
|
||||
return intSep("-" /* 2023-12-01 */, first(find(t.raw, "originaldate", "date", "year")))
|
||||
}
|
||||
|
||||
func (t *Tagger) Length() int { return t.props.Length }
|
||||
func (t *Tagger) Bitrate() int { return t.props.Bitrate }
|
||||
|
||||
type Reader interface {
|
||||
Read(abspath string) (Parser, error)
|
||||
}
|
||||
|
||||
type Parser interface {
|
||||
Title() string
|
||||
BrainzID() string
|
||||
Artist() string
|
||||
Album() string
|
||||
AlbumArtist() string
|
||||
AlbumArtists() []string
|
||||
AlbumBrainzID() string
|
||||
Genre() string
|
||||
Genres() []string
|
||||
TrackNumber() int
|
||||
DiscNumber() int
|
||||
Length() int
|
||||
Bitrate() int
|
||||
Year() int
|
||||
}
|
||||
|
||||
func first[T comparable](is []T) T {
|
||||
var z T
|
||||
for _, i := range is {
|
||||
if i != z {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
func find(m map[string][]string, keys ...string) []string {
|
||||
for _, k := range keys {
|
||||
if r := filterStr(m[k]); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterStr(ss []string) []string {
|
||||
var r []string
|
||||
for _, s := range ss {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
r = append(r, s)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func intSep(sep, in string) int {
|
||||
start, _, _ := strings.Cut(in, sep)
|
||||
out, _ := strconv.Atoi(start)
|
||||
return out
|
||||
}
|
||||
|
||||
func MustAlbum(p Parser) string {
|
||||
if r := p.Album(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Album"
|
||||
}
|
||||
|
||||
func MustArtist(p Parser) string {
|
||||
if r := p.Artist(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
func MustAlbumArtist(p Parser) string {
|
||||
if r := p.AlbumArtist(); r != "" {
|
||||
return r
|
||||
}
|
||||
return MustArtist(p)
|
||||
}
|
||||
|
||||
func MustAlbumArtists(p Parser) []string {
|
||||
if r := p.AlbumArtists(); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
return []string{MustAlbumArtist(p)}
|
||||
}
|
||||
|
||||
func MustGenre(p Parser) string {
|
||||
if r := p.Genre(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Genre"
|
||||
}
|
||||
|
||||
func MustGenres(p Parser) []string {
|
||||
if r := p.Genres(); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
return []string{MustGenre(p)}
|
||||
}
|
||||
Reference in New Issue
Block a user