feat(tags): support multi valued tags like albumartists
This commit is contained in:
2
go.mod
2
go.mod
@@ -23,12 +23,12 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/mmcdole/gofeed v1.2.0
|
github.com/mmcdole/gofeed v1.2.0
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd
|
|
||||||
github.com/oklog/run v1.1.0
|
github.com/oklog/run v1.1.0
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||||
github.com/peterbourgon/ff v1.7.1
|
github.com/peterbourgon/ff v1.7.1
|
||||||
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4
|
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||||
|
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137
|
||||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
|
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -122,8 +122,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
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/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||||
@@ -141,6 +139,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||||
|
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137 h1:K0PSMi/p9ISHpfRFJB03d7VX+jjEsDARsTlAcN1zpac=
|
||||||
|
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137/go.mod h1:nUVlCJ7n2jQoJ5rttpHozZ8pHJIhD9VehL6GP21FoDU=
|
||||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
|
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
|
||||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1ck+so+41uu9VY1gMKs1CPQ2NTq0pzf+OCCQHo=
|
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1ck+so+41uu9VY1gMKs1CPQ2NTq0pzf+OCCQHo=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ type Tags struct {
|
|||||||
RawArtist string
|
RawArtist string
|
||||||
RawAlbum string
|
RawAlbum string
|
||||||
RawAlbumArtist string
|
RawAlbumArtist string
|
||||||
|
RawAlbumArtists []string
|
||||||
RawGenre string
|
RawGenre string
|
||||||
|
|
||||||
RawBitrate int
|
RawBitrate int
|
||||||
@@ -366,6 +367,7 @@ func (m *Tags) BrainzID() string { return "" }
|
|||||||
func (m *Tags) Artist() string { return m.RawArtist }
|
func (m *Tags) Artist() string { return m.RawArtist }
|
||||||
func (m *Tags) Album() string { return m.RawAlbum }
|
func (m *Tags) Album() string { return m.RawAlbum }
|
||||||
func (m *Tags) AlbumArtist() string { return m.RawAlbumArtist }
|
func (m *Tags) AlbumArtist() string { return m.RawAlbumArtist }
|
||||||
|
func (m *Tags) AlbumArtists() []string { return m.RawAlbumArtists }
|
||||||
func (m *Tags) AlbumBrainzID() string { return "" }
|
func (m *Tags) AlbumBrainzID() string { return "" }
|
||||||
func (m *Tags) Genre() string { return m.RawGenre }
|
func (m *Tags) Genre() string { return m.RawGenre }
|
||||||
func (m *Tags) TrackNumber() int { return 1 }
|
func (m *Tags) TrackNumber() int { return 1 }
|
||||||
@@ -375,11 +377,6 @@ func (m *Tags) Year() int { return 2021 }
|
|||||||
func (m *Tags) Length() int { return firstInt(100, m.RawLength) }
|
func (m *Tags) Length() int { return firstInt(100, m.RawLength) }
|
||||||
func (m *Tags) Bitrate() int { return firstInt(100, m.RawBitrate) }
|
func (m *Tags) Bitrate() int { return firstInt(100, m.RawBitrate) }
|
||||||
|
|
||||||
func (m *Tags) SomeAlbum() string { return first("Unknown Album", m.Album()) }
|
|
||||||
func (m *Tags) SomeArtist() string { return first("Unknown Artist", m.Artist()) }
|
|
||||||
func (m *Tags) SomeAlbumArtist() string { return first("Unknown Artist", m.AlbumArtist(), m.Artist()) }
|
|
||||||
func (m *Tags) SomeGenre() string { return first("Unknown Genre", m.Genre()) }
|
|
||||||
|
|
||||||
var _ tags.Parser = (*Tags)(nil)
|
var _ tags.Parser = (*Tags)(nil)
|
||||||
|
|
||||||
func first(or string, strs ...string) string {
|
func first(or string, strs ...string) string {
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
|
|||||||
return fmt.Errorf("%v: %w", err, ErrReadingTags)
|
return fmt.Errorf("%v: %w", err, ErrReadingTags)
|
||||||
}
|
}
|
||||||
|
|
||||||
genreNames := strings.Split(trags.SomeGenre(), s.genreSplit)
|
genreNames := strings.Split(tags.MustGenre(trags), s.genreSplit)
|
||||||
genreIDs, err := populateGenres(tx, genreNames)
|
genreIDs, err := populateGenres(tx, genreNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("populate genres: %w", err)
|
return fmt.Errorf("populate genres: %w", err)
|
||||||
@@ -361,7 +361,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
|
|||||||
|
|
||||||
// metadata for the album table comes only from the the first track's tags
|
// metadata for the album table comes only from the the first track's tags
|
||||||
if i == 0 || album.TagArtist == nil {
|
if i == 0 || album.TagArtist == nil {
|
||||||
albumArtist, err := populateAlbumArtist(tx, parent, trags.SomeAlbumArtist())
|
albumArtist, err := populateAlbumArtist(tx, parent, tags.MustAlbumArtist(trags))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("populate album artist: %w", err)
|
return fmt.Errorf("populate album artist: %w", err)
|
||||||
}
|
}
|
||||||
@@ -387,7 +387,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
|
|||||||
}
|
}
|
||||||
|
|
||||||
func populateAlbum(tx *db.DB, album *db.Album, albumArtist *db.Artist, trags tags.Parser, modTime time.Time) error {
|
func populateAlbum(tx *db.DB, album *db.Album, albumArtist *db.Artist, trags tags.Parser, modTime time.Time) error {
|
||||||
albumName := trags.SomeAlbum()
|
albumName := tags.MustAlbum(trags)
|
||||||
album.TagTitle = albumName
|
album.TagTitle = albumName
|
||||||
album.TagTitleUDec = decoded(albumName)
|
album.TagTitleUDec = decoded(albumName)
|
||||||
album.TagBrainzID = trags.AlbumBrainzID()
|
album.TagBrainzID = trags.AlbumBrainzID()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nicksellen/audiotags"
|
"github.com/sentriz/audiotags"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TagReader struct{}
|
type TagReader struct{}
|
||||||
@@ -15,49 +15,33 @@ func (*TagReader) Read(abspath string) (Parser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Tagger struct {
|
type Tagger struct {
|
||||||
raw map[string]string
|
raw map[string][]string
|
||||||
props *audiotags.AudioProperties
|
props *audiotags.AudioProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tagger) first(keys ...string) string {
|
|
||||||
for _, key := range keys {
|
|
||||||
if v := strings.TrimSpace(t.raw[key]); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tagger) firstInt(sep string, keys ...string) int {
|
|
||||||
for _, key := range keys {
|
|
||||||
if v := intSep(t.raw[key], sep); v > 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||||
|
|
||||||
func (t *Tagger) Title() string { return t.first("title") }
|
func (t *Tagger) Title() string { return first(find(t.raw, "title")) }
|
||||||
func (t *Tagger) BrainzID() string { return t.first("musicbrainz_trackid") } // musicbrainz recording ID
|
func (t *Tagger) BrainzID() string { return first(find(t.raw, "musicbrainz_trackid")) } // musicbrainz recording ID
|
||||||
func (t *Tagger) Artist() string { return t.first("artist") }
|
func (t *Tagger) Artist() string { return first(find(t.raw, "artist")) }
|
||||||
func (t *Tagger) Album() string { return t.first("album") }
|
func (t *Tagger) Album() string { return first(find(t.raw, "album")) }
|
||||||
func (t *Tagger) AlbumArtist() string { return t.first("albumartist", "album artist") }
|
func (t *Tagger) AlbumArtist() string { return first(find(t.raw, "albumartist", "album artist")) }
|
||||||
func (t *Tagger) AlbumBrainzID() string { return t.first("musicbrainz_albumid") } // musicbrainz release ID
|
func (t *Tagger) AlbumArtists() []string { return find(t.raw, "albumartists", "album_artists") }
|
||||||
func (t *Tagger) Genre() string { return t.first("genre") }
|
func (t *Tagger) AlbumBrainzID() string { return first(find(t.raw, "musicbrainz_albumid")) } // musicbrainz release ID
|
||||||
func (t *Tagger) TrackNumber() int { return t.firstInt("/" /* eg. 5/12 */, "tracknumber") }
|
func (t *Tagger) Genre() string { return first(find(t.raw, "genre")) }
|
||||||
func (t *Tagger) DiscNumber() int { return t.firstInt("/" /* eg. 1/2 */, "discnumber") }
|
|
||||||
|
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) Length() int { return t.props.Length }
|
||||||
func (t *Tagger) Bitrate() int { return t.props.Bitrate }
|
func (t *Tagger) Bitrate() int { return t.props.Bitrate }
|
||||||
func (t *Tagger) Year() int { return t.firstInt("-", "originaldate", "date", "year") }
|
|
||||||
|
|
||||||
func (t *Tagger) SomeAlbum() string { return first("Unknown Album", t.Album()) }
|
|
||||||
func (t *Tagger) SomeArtist() string { return first("Unknown Artist", t.Artist()) }
|
|
||||||
func (t *Tagger) SomeAlbumArtist() string {
|
|
||||||
return first("Unknown Artist", t.AlbumArtist(), t.Artist())
|
|
||||||
}
|
|
||||||
func (t *Tagger) SomeGenre() string { return first("Unknown Genre", t.Genre()) }
|
|
||||||
|
|
||||||
type Reader interface {
|
type Reader interface {
|
||||||
Read(abspath string) (Parser, error)
|
Read(abspath string) (Parser, error)
|
||||||
@@ -69,6 +53,7 @@ type Parser interface {
|
|||||||
Artist() string
|
Artist() string
|
||||||
Album() string
|
Album() string
|
||||||
AlbumArtist() string
|
AlbumArtist() string
|
||||||
|
AlbumArtists() []string
|
||||||
AlbumBrainzID() string
|
AlbumBrainzID() string
|
||||||
Genre() string
|
Genre() string
|
||||||
TrackNumber() int
|
TrackNumber() int
|
||||||
@@ -76,11 +61,44 @@ type Parser interface {
|
|||||||
Length() int
|
Length() int
|
||||||
Bitrate() int
|
Bitrate() int
|
||||||
Year() int
|
Year() int
|
||||||
|
}
|
||||||
|
|
||||||
SomeAlbum() string
|
func fallback(or string, strs ...string) string {
|
||||||
SomeArtist() string
|
for _, str := range strs {
|
||||||
SomeAlbumArtist() string
|
if str != "" {
|
||||||
SomeGenre() string
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return or
|
||||||
|
}
|
||||||
|
|
||||||
|
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(in, sep string) int {
|
func intSep(in, sep string) int {
|
||||||
@@ -95,11 +113,46 @@ func intSep(in, sep string) int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func first(or string, strs ...string) string {
|
func MustAlbum(p Parser) string {
|
||||||
for _, str := range strs {
|
if r := p.Album(); r != "" {
|
||||||
if str != "" {
|
return r
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
|
return "Unknown Album"
|
||||||
}
|
}
|
||||||
return or
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if r := p.Artist(); r != "" {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return "Unknown Artist"
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustAlbumArtists(p Parser) []string {
|
||||||
|
if r := p.AlbumArtists(); len(r) > 0 {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if r := p.AlbumArtist(); r != "" {
|
||||||
|
return []string{r}
|
||||||
|
}
|
||||||
|
if r := p.Artist(); r != "" {
|
||||||
|
return []string{r}
|
||||||
|
}
|
||||||
|
return []string{"Unknown Artist"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustGenre(p Parser) string {
|
||||||
|
if r := p.Genre(); r != "" {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return "Unknown Genre"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user