feat: store and expose individual track artists

a
This commit is contained in:
sentriz
2023-10-28 18:27:17 +01:00
committed by Senan Kelly
parent 1a45356fa2
commit c1a34dc021
24 changed files with 176 additions and 64 deletions

View File

@@ -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_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_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_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_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) | | `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) |

View File

@@ -81,8 +81,9 @@ func main() {
confExcludePattern := set.String("exclude-pattern", "", "regex pattern to exclude files from scan (optional)") 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(&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)") 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)") confExpvar := set.Bool("expvar", false, "enable the /debug/vars endpoint (optional)")
@@ -184,6 +185,7 @@ func main() {
dbc, dbc,
map[scanner.Tag]scanner.MultiValueSetting{ map[scanner.Tag]scanner.MultiValueSetting{
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre), scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
scanner.Artist: scanner.MultiValueSetting(confMultiValueArtist),
scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist), scanner.AlbumArtist: scanner.MultiValueSetting(confMultiValueAlbumArtist),
}, },
tagReader, tagReader,

View File

@@ -201,17 +201,18 @@ type Track struct {
Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"` Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"`
FilenameUDec string `sql:"default: null"` FilenameUDec string `sql:"default: null"`
Album *Album Album *Album
AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
Genres []*Genre `gorm:"many2many:track_genres"` Artists []*Artist `gorm:"many2many:track_artists"`
Size int `sql:"default: null"` Genres []*Genre `gorm:"many2many:track_genres"`
Length int `sql:"default: null"` Size int `sql:"default: null"`
Bitrate int `sql:"default: null"` Length int `sql:"default: null"`
TagTitle string `sql:"default: null"` Bitrate int `sql:"default: null"`
TagTitleUDec string `sql:"default: null"` TagTitle string `sql:"default: null"`
TagTrackArtist string `sql:"default: null"` TagTitleUDec string `sql:"default: null"`
TagTrackNumber int `sql:"default: null"` TagTrackArtist string `sql:"default: null"`
TagDiscNumber int `sql:"default: null"` TagTrackNumber int `sql:"default: null"`
TagBrainzID string `sql:"default: null"` TagDiscNumber int `sql:"default: null"`
TagBrainzID string `sql:"default: null"`
TrackStar *TrackStar TrackStar *TrackStar
TrackRating *TrackRating TrackRating *TrackRating
AverageRating float64 `sql:"default: null"` AverageRating float64 `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"` 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 { type TrackGenre struct {
Track *Track 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"` TrackID int `gorm:"not null; unique_index:idx_track_id_genre_id" sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`

View File

@@ -67,6 +67,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202309131743", migrateArtistInfo), construct(ctx, "202309131743", migrateArtistInfo),
construct(ctx, "202309161411", migratePlaylistsPaths), construct(ctx, "202309161411", migratePlaylistsPaths),
construct(ctx, "202310252205", migrateAlbumTagArtistString), construct(ctx, "202310252205", migrateAlbumTagArtistString),
construct(ctx, "202310281803", migrateTrackArtists),
} }
return gormigrate. return gormigrate.
@@ -734,3 +735,12 @@ func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error {
func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error { func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error {
return tx.AutoMigrate(Album{}).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
}

View File

@@ -339,6 +339,7 @@ func (m *tagReader) Read(absPath string) (tagcommon.Info, error) {
type TagInfo struct { type TagInfo struct {
RawTitle string RawTitle string
RawArtist string RawArtist string
RawArtists []string
RawAlbum string RawAlbum string
RawAlbumArtist string RawAlbumArtist string
RawAlbumArtists []string RawAlbumArtists []string
@@ -351,6 +352,7 @@ type TagInfo struct {
func (i *TagInfo) Title() string { return i.RawTitle } func (i *TagInfo) Title() string { return i.RawTitle }
func (i *TagInfo) BrainzID() string { return "" } func (i *TagInfo) BrainzID() string { return "" }
func (i *TagInfo) Artist() string { return i.RawArtist } 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) Album() string { return i.RawAlbum }
func (i *TagInfo) AlbumArtist() string { return i.RawAlbumArtist } func (i *TagInfo) AlbumArtist() string { return i.RawAlbumArtist }
func (i *TagInfo) AlbumArtists() []string { return i.RawAlbumArtists } func (i *TagInfo) AlbumArtists() []string { return i.RawAlbumArtists }

View File

@@ -296,7 +296,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
sort.Strings(tracks) sort.Strings(tracks)
for i, basename := range tracks { for i, basename := range tracks {
absPath := filepath.Join(musicDir, relPath, basename) 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) 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 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) stat, err := os.Stat(absPath)
if err != nil { if err != nil {
return fmt.Errorf("stating %q: %w", basename, err) 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) 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.seenTracks[track.ID] = struct{}{}
c.seenTracksNew++ c.seenTracksNew++
@@ -498,6 +511,17 @@ func populateAlbumArtists(tx *db.DB, album *db.Album, albumArtistIDs []int) erro
return nil 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 { func (s *Scanner) cleanTracks(c *Context) error {
start := time.Now() start := time.Now()
defer func() { log.Printf("finished clean tracks in %s, %d removed", durSince(start), c.TracksMissing()) }() 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() start := time.Now()
defer func() { log.Printf("finished clean artists in %s, %d removed", durSince(start), c.ArtistsMissing()) }() defer func() { log.Printf("finished clean artists in %s, %d removed", durSince(start), c.ArtistsMissing()) }()
sub := s.db. // gorm doesn't seem to support subqueries without parens for UNION
Select("artists.id"). q := s.db.Exec(`
Model(&db.Artist{}). DELETE FROM artists
Joins("LEFT JOIN album_artists ON album_artists.artist_id=artists.id"). WHERE id NOT IN (
Where("album_artists.artist_id IS NULL"). SELECT artist_id FROM track_artists
SubQuery() UNION
q := s.db. SELECT artist_id FROM album_artists
Where("artists.id IN ?", sub). )
Delete(&db.Artist{}) `)
if err := q.Error; err != nil { if err := q.Error; err != nil {
return err return err
} }
@@ -654,6 +678,7 @@ type Tag uint8
const ( const (
Genre Tag = iota Genre Tag = iota
Artist
AlbumArtist AlbumArtist
) )

View File

@@ -562,7 +562,7 @@ func TestCompilationAlbumWithoutAlbumArtist(t *testing.T) {
assert.Equal(t, 5, trackCount) assert.Equal(t, 5, trackCount)
var artists []*db.Artist 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, 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 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() m.ScanAndClean()
var artists []*db.Artist 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 assert.Len(t, artists, 3) // alan, liz, mercury
var albumArtists []*db.AlbumArtist var albumArtists []*db.AlbumArtist
@@ -695,7 +695,7 @@ func TestMultiArtistSupport(t *testing.T) {
m.ScanAndClean() 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.Len(t, artists, 2) // alan, liz
assert.NoError(t, m.DB().Find(&albumArtists).Error) assert.NoError(t, m.DB().Find(&albumArtists).Error)
@@ -745,7 +745,7 @@ func TestMultiArtistPreload(t *testing.T) {
} }
var artists []*db.Artist 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)) assert.Equal(t, 3, len(artists))
for _, artist := range artists { for _, artist := range artists {

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
go test fuzz v1
[]byte("001")
int64(3472329395739373616)

View File

@@ -1,3 +0,0 @@
go test fuzz v1
[]byte("001")
int64(3472329395739373616)

View File

@@ -1,3 +0,0 @@
go test fuzz v1
[]byte("")
int64(0)

View File

@@ -91,6 +91,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
Where("album_id=?", id.Value). Where("album_id=?", id.Value).
Preload("Album"). Preload("Album").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Order("filename"). Order("filename").
@@ -255,7 +256,9 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
for _, s := range queries { for _, s := range queries {
q = q.Where(`filename LIKE ? OR filename LIKE ?`, s, s) 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). Preload("TrackRating", "user_id=?", user.ID).
Offset(params.GetOrInt("songOffset", 0)). Offset(params.GetOrInt("songOffset", 0)).
Limit(params.GetOrInt("songCount", 20)) Limit(params.GetOrInt("songCount", 20))
@@ -338,6 +341,7 @@ func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response {
Preload("Album"). Preload("Album").
Joins("JOIN track_stars ON tracks.id=track_stars.track_id"). Joins("JOIN track_stars ON tracks.id=track_stars.track_id").
Where("track_stars.user_id=?", user.ID). Where("track_stars.user_id=?", user.ID).
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID) Preload("TrackRating", "user_id=?", user.ID)
if m := getMusicFolder(c.musicPaths, params); m != "" { if m := getMusicFolder(c.musicPaths, params); m != "" {

View File

@@ -108,6 +108,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
Preload("Tracks", func(db *gorm.DB) *gorm.DB { Preload("Tracks", func(db *gorm.DB) *gorm.DB {
return db. return db.
Order("tracks.tag_disc_number, tracks.tag_track_number"). Order("tracks.tag_disc_number, tracks.tag_track_number").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "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").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Genres"). Preload("Genres").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID) Preload("TrackRating", "user_id=?", user.ID)
for _, s := range queries { 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). Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
Preload("Album"). Preload("Album").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Offset(params.GetOrInt("offset", 0)). Offset(params.GetOrInt("offset", 0)).
@@ -490,6 +493,7 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
Order("track_stars.star_date DESC"). Order("track_stars.star_date DESC").
Preload("Album"). Preload("Album").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID) Preload("TrackRating", "user_id=?", user.ID)
if m := getMusicFolder(c.musicPaths, params); m != "" { 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"). Joins("JOIN album_artists ON album_artists.album_id=albums.id").
Where("album_artists.artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames). Where("album_artists.artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames).
Limit(count). Limit(count).
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Group("tracks.id"). Group("tracks.id").
@@ -622,6 +627,7 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
err = c.dbc. err = c.dbc.
Select("tracks.*"). Select("tracks.*").
Preload("Album"). Preload("Album").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Where("tracks.tag_title IN (?)", similarTrackNames). Where("tracks.tag_title IN (?)", similarTrackNames).
@@ -685,6 +691,7 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
var tracks []*db.Track var tracks []*db.Track
err = c.dbc. err = c.dbc.
Preload("Album"). Preload("Album").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Joins("JOIN album_artists ON album_artists.album_id=tracks.album_id"). Joins("JOIN album_artists ON album_artists.album_id=tracks.album_id").

View File

@@ -212,6 +212,7 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response {
c.dbc. c.dbc.
Where("id=?", id.Value). Where("id=?", id.Value).
Preload("Album"). Preload("Album").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Find(&track) Find(&track)
@@ -268,6 +269,7 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
Where("id=?", id.Value). Where("id=?", id.Value).
Preload("Album"). Preload("Album").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
First(&track). First(&track).
@@ -294,6 +296,7 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
Limit(params.GetOrInt("size", 10)). Limit(params.GetOrInt("size", 10)).
Preload("Album"). Preload("Album").
Preload("Album.Artists"). Preload("Album.Artists").
Preload("Artists").
Preload("TrackStar", "user_id=?", user.ID). Preload("TrackStar", "user_id=?", user.ID).
Preload("TrackRating", "user_id=?", user.ID). Preload("TrackRating", "user_id=?", user.ID).
Joins("JOIN albums ON tracks.album_id=albums.id"). Joins("JOIN albums ON tracks.album_id=albums.id").

View File

@@ -218,7 +218,7 @@ func playlistRender(c *Controller, params params.Params, playlistID string, play
switch id := file.SID(); id.Type { switch id := file.SID(); id.Type {
case specid.Track: case specid.Track:
var track db.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) return nil, fmt.Errorf("load track by id: %w", err)
} }
trch = spec.NewTCTrackByFolder(&track, track.Album) trch = spec.NewTCTrackByFolder(&track, track.Album)

View File

@@ -96,6 +96,9 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
for _, g := range t.Genres { for _, g := range t.Genres {
trCh.Genres = append(trCh.Genres, &GenreRef{Name: g.Name}) 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 return trCh
} }

View File

@@ -94,6 +94,9 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
for _, g := range t.Genres { for _, g := range t.Genres {
ret.Genres = append(ret.Genres, &GenreRef{Name: g.Name}) 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 return ret
} }

View File

@@ -160,29 +160,30 @@ type TranscodeMeta struct {
} }
type TrackChild struct { type TrackChild struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"` ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
Album string `xml:"album,attr,omitempty" json:"album,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
AlbumID *specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` AlbumID *specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
ArtistID *specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` ArtistID *specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate,omitempty"` Artists []*ArtistRef `xml:"artists,omitempty" json:"artists,omitempty"`
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"` Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate,omitempty"`
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"` CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"` CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
Genres []*GenreRef `xml:"genres,omitempty" json:"genres,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
IsDir bool `xml:"isDir,attr" json:"isDir"` Genres []*GenreRef `xml:"genres,omitempty" json:"genres,omitempty"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"` IsDir bool `xml:"isDir,attr" json:"isDir"`
ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
Path string `xml:"path,attr,omitempty" json:"path,omitempty"` ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
Size int `xml:"size,attr,omitempty" json:"size,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"` Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
Title string `xml:"title,attr" json:"title"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
TrackNumber int `xml:"track,attr,omitempty" json:"track,omitempty"` Title string `xml:"title,attr" json:"title"`
DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"` TrackNumber int `xml:"track,attr,omitempty" json:"track,omitempty"`
Type string `xml:"type,attr,omitempty" json:"type,omitempty"` DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"` Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
// star / rating // star / rating
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"` Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"` UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`

View File

@@ -27,6 +27,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -49,6 +50,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -71,6 +73,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",

View File

@@ -14,6 +14,7 @@
"id": "tr-1", "id": "tr-1",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -34,6 +35,7 @@
"id": "tr-2", "id": "tr-2",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -54,6 +56,7 @@
"id": "tr-3", "id": "tr-3",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",

View File

@@ -13,6 +13,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -37,6 +38,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -61,6 +63,7 @@
"albumId": "al-3", "albumId": "al-3",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -85,6 +88,7 @@
"albumId": "al-4", "albumId": "al-4",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -109,6 +113,7 @@
"albumId": "al-4", "albumId": "al-4",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -133,6 +138,7 @@
"albumId": "al-4", "albumId": "al-4",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -157,6 +163,7 @@
"albumId": "al-5", "albumId": "al-5",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -181,6 +188,7 @@
"albumId": "al-5", "albumId": "al-5",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -205,6 +213,7 @@
"albumId": "al-5", "albumId": "al-5",
"artist": "artist-0", "artist": "artist-0",
"artistId": "ar-1", "artistId": "ar-1",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -229,6 +238,7 @@
"albumId": "al-7", "albumId": "al-7",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -253,6 +263,7 @@
"albumId": "al-7", "albumId": "al-7",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -277,6 +288,7 @@
"albumId": "al-7", "albumId": "al-7",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -301,6 +313,7 @@
"albumId": "al-8", "albumId": "al-8",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -325,6 +338,7 @@
"albumId": "al-8", "albumId": "al-8",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -349,6 +363,7 @@
"albumId": "al-8", "albumId": "al-8",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -373,6 +388,7 @@
"albumId": "al-9", "albumId": "al-9",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -397,6 +413,7 @@
"albumId": "al-9", "albumId": "al-9",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -421,6 +438,7 @@
"albumId": "al-9", "albumId": "al-9",
"artist": "artist-1", "artist": "artist-1",
"artistId": "ar-2", "artistId": "ar-2",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -445,6 +463,7 @@
"albumId": "al-11", "albumId": "al-11",
"artist": "artist-2", "artist": "artist-2",
"artistId": "ar-3", "artistId": "ar-3",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-11", "coverArt": "al-11",
@@ -469,6 +488,7 @@
"albumId": "al-11", "albumId": "al-11",
"artist": "artist-2", "artist": "artist-2",
"artistId": "ar-3", "artistId": "ar-3",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-11", "coverArt": "al-11",

View File

@@ -11,6 +11,7 @@
"id": "tr-1", "id": "tr-1",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -31,6 +32,7 @@
"id": "tr-2", "id": "tr-2",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -51,6 +53,7 @@
"id": "tr-3", "id": "tr-3",
"album": "album-0", "album": "album-0",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-3", "coverArt": "al-3",
@@ -71,6 +74,7 @@
"id": "tr-4", "id": "tr-4",
"album": "album-1", "album": "album-1",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -91,6 +95,7 @@
"id": "tr-5", "id": "tr-5",
"album": "album-1", "album": "album-1",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -111,6 +116,7 @@
"id": "tr-6", "id": "tr-6",
"album": "album-1", "album": "album-1",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-4", "coverArt": "al-4",
@@ -131,6 +137,7 @@
"id": "tr-7", "id": "tr-7",
"album": "album-2", "album": "album-2",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -151,6 +158,7 @@
"id": "tr-8", "id": "tr-8",
"album": "album-2", "album": "album-2",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -171,6 +179,7 @@
"id": "tr-9", "id": "tr-9",
"album": "album-2", "album": "album-2",
"artist": "artist-0", "artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-5", "coverArt": "al-5",
@@ -191,6 +200,7 @@
"id": "tr-10", "id": "tr-10",
"album": "album-0", "album": "album-0",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -211,6 +221,7 @@
"id": "tr-11", "id": "tr-11",
"album": "album-0", "album": "album-0",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -231,6 +242,7 @@
"id": "tr-12", "id": "tr-12",
"album": "album-0", "album": "album-0",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-7", "coverArt": "al-7",
@@ -251,6 +263,7 @@
"id": "tr-13", "id": "tr-13",
"album": "album-1", "album": "album-1",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -271,6 +284,7 @@
"id": "tr-14", "id": "tr-14",
"album": "album-1", "album": "album-1",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -291,6 +305,7 @@
"id": "tr-15", "id": "tr-15",
"album": "album-1", "album": "album-1",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-8", "coverArt": "al-8",
@@ -311,6 +326,7 @@
"id": "tr-16", "id": "tr-16",
"album": "album-2", "album": "album-2",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -331,6 +347,7 @@
"id": "tr-17", "id": "tr-17",
"album": "album-2", "album": "album-2",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -351,6 +368,7 @@
"id": "tr-18", "id": "tr-18",
"album": "album-2", "album": "album-2",
"artist": "artist-1", "artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-9", "coverArt": "al-9",
@@ -371,6 +389,7 @@
"id": "tr-19", "id": "tr-19",
"album": "album-0", "album": "album-0",
"artist": "artist-2", "artist": "artist-2",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-11", "coverArt": "al-11",
@@ -391,6 +410,7 @@
"id": "tr-20", "id": "tr-20",
"album": "album-0", "album": "album-0",
"artist": "artist-2", "artist": "artist-2",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"bitRate": 100, "bitRate": 100,
"contentType": "audio/flac", "contentType": "audio/flac",
"coverArt": "al-11", "coverArt": "al-11",

View File

@@ -15,6 +15,7 @@ type Info interface {
Title() string Title() string
BrainzID() string BrainzID() string
Artist() string Artist() string
Artists() []string
Album() string Album() string
AlbumArtist() string AlbumArtist() string
AlbumArtists() []string AlbumArtists() []string
@@ -42,6 +43,13 @@ func MustArtist(p Info) string {
return "Unknown Artist" 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 { func MustAlbumArtist(p Info) string {
if r := p.AlbumArtist(); r != "" { if r := p.AlbumArtist(); r != "" {
return r return r

View File

@@ -34,6 +34,7 @@ type info struct {
func (i *info) Title() string { return first(find(i.raw, "title")) } 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) 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) 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) Album() string { return first(find(i.raw, "album")) }
func (i *info) AlbumArtist() string { return first(find(i.raw, "albumartist", "album artist")) } 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) AlbumArtists() []string { return find(i.raw, "albumartists", "album_artists") }