feat: store and expose individual track artists
a
This commit is contained in:
@@ -77,6 +77,7 @@ password can then be changed from the web interface
|
||||
| `GONIC_PODCAST_PURGE_AGE` | `-podcast-purge-age` | **optional** age (in days) to purge podcast episodes if not accessed |
|
||||
| `GONIC_EXCLUDE_PATTERN` | `-exclude-pattern` | **optional** files matching this regex pattern will not be imported |
|
||||
| `GONIC_MULTI_VALUE_GENRE` | `-multi-value-genre` | **optional** setting for multi-valued genre tags when scanning ([see more](#multi-valued-tags)) |
|
||||
| `GONIC_MULTI_VALUE_ARTIST` | `-multi-value-artist` | **optional** setting for multi-valued artist tags when scanning ([see more](#multi-valued-tags)) |
|
||||
| `GONIC_MULTI_VALUE_ALBUM_ARTIST` | `-multi-value-album-artist` | **optional** setting for multi-valued album artist tags when scanning ([see more](#multi-valued-tags)) |
|
||||
| `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) |
|
||||
|
||||
|
||||
@@ -81,8 +81,9 @@ func main() {
|
||||
|
||||
confExcludePattern := set.String("exclude-pattern", "", "regex pattern to exclude files from scan (optional)")
|
||||
|
||||
var confMultiValueGenre, confMultiValueAlbumArtist multiValueSetting
|
||||
var confMultiValueGenre, confMultiValueArtist, confMultiValueAlbumArtist multiValueSetting
|
||||
set.Var(&confMultiValueGenre, "multi-value-genre", "setting for mutli-valued genre scanning (optional)")
|
||||
set.Var(&confMultiValueArtist, "multi-value-artist", "setting for mutli-valued track artist scanning (optional)")
|
||||
set.Var(&confMultiValueAlbumArtist, "multi-value-album-artist", "setting for mutli-valued album artist scanning (optional)")
|
||||
|
||||
confExpvar := set.Bool("expvar", false, "enable the /debug/vars endpoint (optional)")
|
||||
@@ -184,6 +185,7 @@ func main() {
|
||||
dbc,
|
||||
map[scanner.Tag]scanner.MultiValueSetting{
|
||||
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
|
||||
scanner.Artist: scanner.MultiValueSetting(confMultiValueArtist),
|
||||
scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist),
|
||||
},
|
||||
tagReader,
|
||||
|
||||
8
db/db.go
8
db/db.go
@@ -202,6 +202,7 @@ type Track struct {
|
||||
FilenameUDec string `sql:"default: null"`
|
||||
Album *Album
|
||||
AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
Artists []*Artist `gorm:"many2many:track_artists"`
|
||||
Genres []*Genre `gorm:"many2many:track_genres"`
|
||||
Size int `sql:"default: null"`
|
||||
Length int `sql:"default: null"`
|
||||
@@ -372,6 +373,13 @@ type AlbumArtist struct {
|
||||
ArtistID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
}
|
||||
|
||||
type TrackArtist struct {
|
||||
Track *Track
|
||||
TrackID int `gorm:"not null; unique_index:idx_track_id_artist_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
|
||||
Artist *Artist
|
||||
ArtistID int `gorm:"not null; unique_index:idx_track_id_artist_id" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
}
|
||||
|
||||
type TrackGenre struct {
|
||||
Track *Track
|
||||
TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
|
||||
|
||||
@@ -67,6 +67,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
|
||||
construct(ctx, "202309131743", migrateArtistInfo),
|
||||
construct(ctx, "202309161411", migratePlaylistsPaths),
|
||||
construct(ctx, "202310252205", migrateAlbumTagArtistString),
|
||||
construct(ctx, "202310281803", migrateTrackArtists),
|
||||
}
|
||||
|
||||
return gormigrate.
|
||||
@@ -734,3 +735,12 @@ func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error {
|
||||
func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error {
|
||||
return tx.AutoMigrate(Album{}).Error
|
||||
}
|
||||
|
||||
func migrateTrackArtists(tx *gorm.DB, _ MigrationContext) error {
|
||||
// gorms seems to want to create the table automatically without ON DELETE rules
|
||||
step := tx.DropTableIfExists(TrackArtist{})
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop prev: %w", err)
|
||||
}
|
||||
return tx.AutoMigrate(TrackArtist{}).Error
|
||||
}
|
||||
|
||||
@@ -339,6 +339,7 @@ func (m *tagReader) Read(absPath string) (tagcommon.Info, error) {
|
||||
type TagInfo struct {
|
||||
RawTitle string
|
||||
RawArtist string
|
||||
RawArtists []string
|
||||
RawAlbum string
|
||||
RawAlbumArtist string
|
||||
RawAlbumArtists []string
|
||||
@@ -351,6 +352,7 @@ type TagInfo struct {
|
||||
func (i *TagInfo) Title() string { return i.RawTitle }
|
||||
func (i *TagInfo) BrainzID() string { return "" }
|
||||
func (i *TagInfo) Artist() string { return i.RawArtist }
|
||||
func (i *TagInfo) Artists() []string { return i.RawArtists }
|
||||
func (i *TagInfo) Album() string { return i.RawAlbum }
|
||||
func (i *TagInfo) AlbumArtist() string { return i.RawAlbumArtist }
|
||||
func (i *TagInfo) AlbumArtists() []string { return i.RawAlbumArtists }
|
||||
|
||||
@@ -296,7 +296,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
||||
sort.Strings(tracks)
|
||||
for i, basename := range tracks {
|
||||
absPath := filepath.Join(musicDir, relPath, basename)
|
||||
if err := s.populateTrackAndAlbumArtists(tx, c, i, &album, basename, absPath); err != nil {
|
||||
if err := s.populateTrackAndArtists(tx, c, i, &album, basename, absPath); err != nil {
|
||||
return fmt.Errorf("populate track %q: %w", basename, err)
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, album *db.Album, basename string, absPath string) error {
|
||||
func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *db.Album, basename string, absPath string) error {
|
||||
stat, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stating %q: %w", basename, err)
|
||||
@@ -362,6 +362,19 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb
|
||||
return fmt.Errorf("populate track genres: %w", err)
|
||||
}
|
||||
|
||||
trackArtistNames := parseMulti(trags, s.multiValueSettings[Artist], tagcommon.MustArtists, tagcommon.MustArtist)
|
||||
var trackArtistIDs []int
|
||||
for _, trackArtistName := range trackArtistNames {
|
||||
trackArtist, err := populateArtist(tx, trackArtistName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("populate track artist: %w", err)
|
||||
}
|
||||
trackArtistIDs = append(trackArtistIDs, trackArtist.ID)
|
||||
}
|
||||
if err := populateTrackArtists(tx, &track, trackArtistIDs); err != nil {
|
||||
return fmt.Errorf("populate track artists: %w", err)
|
||||
}
|
||||
|
||||
c.seenTracks[track.ID] = struct{}{}
|
||||
c.seenTracksNew++
|
||||
|
||||
@@ -498,6 +511,17 @@ func populateAlbumArtists(tx *db.DB, album *db.Album, albumArtistIDs []int) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateTrackArtists(tx *db.DB, track *db.Track, trackArtistIDs []int) error {
|
||||
if err := tx.Where("track_id=?", track.ID).Delete(db.TrackArtist{}).Error; err != nil {
|
||||
return fmt.Errorf("delete old track artists: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.InsertBulkLeftMany("track_artists", []string{"track_id", "artist_id"}, track.ID, trackArtistIDs); err != nil {
|
||||
return fmt.Errorf("insert bulk track artists: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) cleanTracks(c *Context) error {
|
||||
start := time.Now()
|
||||
defer func() { log.Printf("finished clean tracks in %s, %d removed", durSince(start), c.TracksMissing()) }()
|
||||
@@ -546,15 +570,15 @@ func (s *Scanner) cleanArtists(c *Context) error {
|
||||
start := time.Now()
|
||||
defer func() { log.Printf("finished clean artists in %s, %d removed", durSince(start), c.ArtistsMissing()) }()
|
||||
|
||||
sub := s.db.
|
||||
Select("artists.id").
|
||||
Model(&db.Artist{}).
|
||||
Joins("LEFT JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Where("album_artists.artist_id IS NULL").
|
||||
SubQuery()
|
||||
q := s.db.
|
||||
Where("artists.id IN ?", sub).
|
||||
Delete(&db.Artist{})
|
||||
// gorm doesn't seem to support subqueries without parens for UNION
|
||||
q := s.db.Exec(`
|
||||
DELETE FROM artists
|
||||
WHERE id NOT IN (
|
||||
SELECT artist_id FROM track_artists
|
||||
UNION
|
||||
SELECT artist_id FROM album_artists
|
||||
)
|
||||
`)
|
||||
if err := q.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -654,6 +678,7 @@ type Tag uint8
|
||||
|
||||
const (
|
||||
Genre Tag = iota
|
||||
Artist
|
||||
AlbumArtist
|
||||
)
|
||||
|
||||
|
||||
@@ -562,7 +562,7 @@ func TestCompilationAlbumWithoutAlbumArtist(t *testing.T) {
|
||||
assert.Equal(t, 5, trackCount)
|
||||
|
||||
var artists []*db.Artist
|
||||
assert.NoError(t, m.DB().Find(&artists).Error)
|
||||
assert.NoError(t, m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Group("artists.id").Find(&artists).Error)
|
||||
assert.Equal(t, 1, len(artists)) // we only have one album artist
|
||||
assert.Equal(t, "artist 0", artists[0].Name) // it came from the first track's fallback to artist tag
|
||||
|
||||
@@ -656,7 +656,7 @@ func TestMultiArtistSupport(t *testing.T) {
|
||||
m.ScanAndClean()
|
||||
|
||||
var artists []*db.Artist
|
||||
assert.NoError(t, m.DB().Find(&artists).Error)
|
||||
assert.NoError(t, m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Group("artists.id").Find(&artists).Error)
|
||||
assert.Len(t, artists, 3) // alan, liz, mercury
|
||||
|
||||
var albumArtists []*db.AlbumArtist
|
||||
@@ -695,7 +695,7 @@ func TestMultiArtistSupport(t *testing.T) {
|
||||
|
||||
m.ScanAndClean()
|
||||
|
||||
assert.NoError(t, m.DB().Find(&artists).Error)
|
||||
assert.NoError(t, m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Group("artists.id").Find(&artists).Error)
|
||||
assert.Len(t, artists, 2) // alan, liz
|
||||
|
||||
assert.NoError(t, m.DB().Find(&albumArtists).Error)
|
||||
@@ -745,7 +745,7 @@ func TestMultiArtistPreload(t *testing.T) {
|
||||
}
|
||||
|
||||
var artists []*db.Artist
|
||||
assert.NoError(t, m.DB().Preload("Albums").Find(&artists).Error)
|
||||
assert.NoError(t, m.DB().Preload("Albums").Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Group("artists.id").Find(&artists).Error)
|
||||
assert.Equal(t, 3, len(artists))
|
||||
|
||||
for _, artist := range artists {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
go test fuzz v1
|
||||
[]byte("001")
|
||||
int64(3472329395739373616)
|
||||
@@ -1,3 +0,0 @@
|
||||
go test fuzz v1
|
||||
[]byte("001")
|
||||
int64(3472329395739373616)
|
||||
@@ -1,3 +0,0 @@
|
||||
go test fuzz v1
|
||||
[]byte("")
|
||||
int64(0)
|
||||
@@ -91,6 +91,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
||||
Where("album_id=?", id.Value).
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Order("filename").
|
||||
@@ -255,7 +256,9 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
||||
for _, s := range queries {
|
||||
q = q.Where(`filename LIKE ? OR filename LIKE ?`, s, s)
|
||||
}
|
||||
q = q.Preload("TrackStar", "user_id=?", user.ID).
|
||||
q = q.
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Offset(params.GetOrInt("songOffset", 0)).
|
||||
Limit(params.GetOrInt("songCount", 20))
|
||||
@@ -338,6 +341,7 @@ func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response {
|
||||
Preload("Album").
|
||||
Joins("JOIN track_stars ON tracks.id=track_stars.track_id").
|
||||
Where("track_stars.user_id=?", user.ID).
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID)
|
||||
if m := getMusicFolder(c.musicPaths, params); m != "" {
|
||||
|
||||
@@ -108,6 +108,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
||||
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Order("tracks.tag_disc_number, tracks.tag_track_number").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID)
|
||||
}).
|
||||
@@ -272,6 +273,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Genres").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID)
|
||||
for _, s := range queries {
|
||||
@@ -409,6 +411,7 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||
Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Offset(params.GetOrInt("offset", 0)).
|
||||
@@ -490,6 +493,7 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
|
||||
Order("track_stars.star_date DESC").
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID)
|
||||
if m := getMusicFolder(c.musicPaths, params); m != "" {
|
||||
@@ -562,6 +566,7 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
|
||||
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
|
||||
Where("album_artists.artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames).
|
||||
Limit(count).
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Group("tracks.id").
|
||||
@@ -622,6 +627,7 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
|
||||
err = c.dbc.
|
||||
Select("tracks.*").
|
||||
Preload("Album").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Where("tracks.tag_title IN (?)", similarTrackNames).
|
||||
@@ -685,6 +691,7 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
|
||||
var tracks []*db.Track
|
||||
err = c.dbc.
|
||||
Preload("Album").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Joins("JOIN album_artists ON album_artists.album_id=tracks.album_id").
|
||||
|
||||
@@ -212,6 +212,7 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response {
|
||||
c.dbc.
|
||||
Where("id=?", id.Value).
|
||||
Preload("Album").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Find(&track)
|
||||
@@ -268,6 +269,7 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
|
||||
Where("id=?", id.Value).
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
First(&track).
|
||||
@@ -294,6 +296,7 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
||||
Limit(params.GetOrInt("size", 10)).
|
||||
Preload("Album").
|
||||
Preload("Album.Artists").
|
||||
Preload("Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||
|
||||
@@ -218,7 +218,7 @@ func playlistRender(c *Controller, params params.Params, playlistID string, play
|
||||
switch id := file.SID(); id.Type {
|
||||
case specid.Track:
|
||||
var track db.Track
|
||||
if err := c.dbc.Where("id=?", id.Value).Preload("Album").Preload("Album.Artists").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).Find(&track).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := c.dbc.Where("id=?", id.Value).Preload("Album").Preload("Album.Artists").Preload("Artists").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).Find(&track).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("load track by id: %w", err)
|
||||
}
|
||||
trch = spec.NewTCTrackByFolder(&track, track.Album)
|
||||
|
||||
@@ -96,6 +96,9 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
|
||||
for _, g := range t.Genres {
|
||||
trCh.Genres = append(trCh.Genres, &GenreRef{Name: g.Name})
|
||||
}
|
||||
for _, a := range t.Artists {
|
||||
trCh.Artists = append(trCh.Artists, &ArtistRef{ID: a.SID(), Name: a.Name})
|
||||
}
|
||||
return trCh
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
|
||||
for _, g := range t.Genres {
|
||||
ret.Genres = append(ret.Genres, &GenreRef{Name: g.Name})
|
||||
}
|
||||
for _, a := range t.Artists {
|
||||
ret.Artists = append(ret.Artists, &ArtistRef{ID: a.SID(), Name: a.Name})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ type TrackChild struct {
|
||||
AlbumID *specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
ArtistID *specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Artists []*ArtistRef `xml:"artists,omitempty" json:"artists,omitempty"`
|
||||
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate,omitempty"`
|
||||
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
|
||||
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -49,6 +50,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -71,6 +73,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"id": "tr-1",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -34,6 +35,7 @@
|
||||
"id": "tr-2",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -54,6 +56,7 @@
|
||||
"id": "tr-3",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -37,6 +38,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -61,6 +63,7 @@
|
||||
"albumId": "al-3",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -85,6 +88,7 @@
|
||||
"albumId": "al-4",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -109,6 +113,7 @@
|
||||
"albumId": "al-4",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -133,6 +138,7 @@
|
||||
"albumId": "al-4",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -157,6 +163,7 @@
|
||||
"albumId": "al-5",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -181,6 +188,7 @@
|
||||
"albumId": "al-5",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -205,6 +213,7 @@
|
||||
"albumId": "al-5",
|
||||
"artist": "artist-0",
|
||||
"artistId": "ar-1",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -229,6 +238,7 @@
|
||||
"albumId": "al-7",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -253,6 +263,7 @@
|
||||
"albumId": "al-7",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -277,6 +288,7 @@
|
||||
"albumId": "al-7",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -301,6 +313,7 @@
|
||||
"albumId": "al-8",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -325,6 +338,7 @@
|
||||
"albumId": "al-8",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -349,6 +363,7 @@
|
||||
"albumId": "al-8",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -373,6 +388,7 @@
|
||||
"albumId": "al-9",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -397,6 +413,7 @@
|
||||
"albumId": "al-9",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -421,6 +438,7 @@
|
||||
"albumId": "al-9",
|
||||
"artist": "artist-1",
|
||||
"artistId": "ar-2",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -445,6 +463,7 @@
|
||||
"albumId": "al-11",
|
||||
"artist": "artist-2",
|
||||
"artistId": "ar-3",
|
||||
"artists": [{ "id": "ar-3", "name": "artist-2" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-11",
|
||||
@@ -469,6 +488,7 @@
|
||||
"albumId": "al-11",
|
||||
"artist": "artist-2",
|
||||
"artistId": "ar-3",
|
||||
"artists": [{ "id": "ar-3", "name": "artist-2" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-11",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"id": "tr-1",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -31,6 +32,7 @@
|
||||
"id": "tr-2",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -51,6 +53,7 @@
|
||||
"id": "tr-3",
|
||||
"album": "album-0",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-3",
|
||||
@@ -71,6 +74,7 @@
|
||||
"id": "tr-4",
|
||||
"album": "album-1",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -91,6 +95,7 @@
|
||||
"id": "tr-5",
|
||||
"album": "album-1",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -111,6 +116,7 @@
|
||||
"id": "tr-6",
|
||||
"album": "album-1",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-4",
|
||||
@@ -131,6 +137,7 @@
|
||||
"id": "tr-7",
|
||||
"album": "album-2",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -151,6 +158,7 @@
|
||||
"id": "tr-8",
|
||||
"album": "album-2",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -171,6 +179,7 @@
|
||||
"id": "tr-9",
|
||||
"album": "album-2",
|
||||
"artist": "artist-0",
|
||||
"artists": [{ "id": "ar-1", "name": "artist-0" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-5",
|
||||
@@ -191,6 +200,7 @@
|
||||
"id": "tr-10",
|
||||
"album": "album-0",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -211,6 +221,7 @@
|
||||
"id": "tr-11",
|
||||
"album": "album-0",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -231,6 +242,7 @@
|
||||
"id": "tr-12",
|
||||
"album": "album-0",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-7",
|
||||
@@ -251,6 +263,7 @@
|
||||
"id": "tr-13",
|
||||
"album": "album-1",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -271,6 +284,7 @@
|
||||
"id": "tr-14",
|
||||
"album": "album-1",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -291,6 +305,7 @@
|
||||
"id": "tr-15",
|
||||
"album": "album-1",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-8",
|
||||
@@ -311,6 +326,7 @@
|
||||
"id": "tr-16",
|
||||
"album": "album-2",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -331,6 +347,7 @@
|
||||
"id": "tr-17",
|
||||
"album": "album-2",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -351,6 +368,7 @@
|
||||
"id": "tr-18",
|
||||
"album": "album-2",
|
||||
"artist": "artist-1",
|
||||
"artists": [{ "id": "ar-2", "name": "artist-1" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-9",
|
||||
@@ -371,6 +389,7 @@
|
||||
"id": "tr-19",
|
||||
"album": "album-0",
|
||||
"artist": "artist-2",
|
||||
"artists": [{ "id": "ar-3", "name": "artist-2" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-11",
|
||||
@@ -391,6 +410,7 @@
|
||||
"id": "tr-20",
|
||||
"album": "album-0",
|
||||
"artist": "artist-2",
|
||||
"artists": [{ "id": "ar-3", "name": "artist-2" }],
|
||||
"bitRate": 100,
|
||||
"contentType": "audio/flac",
|
||||
"coverArt": "al-11",
|
||||
|
||||
@@ -15,6 +15,7 @@ type Info interface {
|
||||
Title() string
|
||||
BrainzID() string
|
||||
Artist() string
|
||||
Artists() []string
|
||||
Album() string
|
||||
AlbumArtist() string
|
||||
AlbumArtists() []string
|
||||
@@ -42,6 +43,13 @@ func MustArtist(p Info) string {
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
func MustArtists(p Info) []string {
|
||||
if r := p.Artists(); len(r) > 0 {
|
||||
return r
|
||||
}
|
||||
return []string{MustArtist(p)}
|
||||
}
|
||||
|
||||
func MustAlbumArtist(p Info) string {
|
||||
if r := p.AlbumArtist(); r != "" {
|
||||
return r
|
||||
|
||||
@@ -34,6 +34,7 @@ type info struct {
|
||||
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) Artists() []string { return find(i.raw, "artists") }
|
||||
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") }
|
||||
|
||||
Reference in New Issue
Block a user