diff --git a/README.md b/README.md
index 05b0eda1..397fa481 100644
--- a/README.md
+++ b/README.md
@@ -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) |
diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go
index fa6eb49e..c594ea85 100644
--- a/cmd/gonic/gonic.go
+++ b/cmd/gonic/gonic.go
@@ -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,
@@ -266,13 +268,7 @@ func main() {
if *confExpvar {
mux.Handle("/debug/vars", expvar.Handler())
expvar.Publish("stats", expvar.Func(func() any {
- var stats struct{ Folders, Albums, Tracks, Artists, InternetRadioStations, Podcasts uint }
- dbc.Model(db.Track{}).Count(&stats.Tracks)
- dbc.Model(db.Album{}).Count(&stats.Folders)
- dbc.Model(db.Album{}).Joins("JOIN album_artists ON album_artists.album_id=albums.id").Group("albums.id").Count(&stats.Albums)
- dbc.Model(db.Artist{}).Count(&stats.Artists)
- dbc.Model(db.InternetRadioStation{}).Count(&stats.InternetRadioStations)
- dbc.Model(db.Podcast{}).Count(&stats.Podcasts)
+ stats, _ := dbc.Stats()
return stats
}))
}
diff --git a/db/db.go b/db/db.go
index 94e06795..ab261aa5 100644
--- a/db/db.go
+++ b/db/db.go
@@ -80,6 +80,22 @@ func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []in
return db.Exec(q, values...).Error
}
+type Stats struct {
+ Folders, Albums, Artists, AlbumArtists, Tracks, InternetRadioStations, Podcasts uint
+}
+
+func (db *DB) Stats() (Stats, error) {
+ var stats Stats
+ db.Model(Album{}).Count(&stats.Folders)
+ db.Model(AlbumArtist{}).Group("album_id").Count(&stats.Albums)
+ db.Model(TrackArtist{}).Group("artist_id").Count(&stats.Artists)
+ db.Model(AlbumArtist{}).Group("artist_id").Count(&stats.AlbumArtists)
+ db.Model(Track{}).Count(&stats.Tracks)
+ db.Model(InternetRadioStation{}).Count(&stats.InternetRadioStations)
+ db.Model(Podcast{}).Count(&stats.Podcasts)
+ return stats, nil
+}
+
func (db *DB) GetUserByID(id int) *User {
var user User
err := db.
@@ -201,17 +217,18 @@ type Track struct {
Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"`
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"`
- Genres []*Genre `gorm:"many2many:track_genres"`
- Size int `sql:"default: null"`
- Length int `sql:"default: null"`
- Bitrate int `sql:"default: null"`
- TagTitle string `sql:"default: null"`
- TagTitleUDec string `sql:"default: null"`
- TagTrackArtist string `sql:"default: null"`
- TagTrackNumber int `sql:"default: null"`
- TagDiscNumber int `sql:"default: null"`
- TagBrainzID string `sql:"default: null"`
+ 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"`
+ Bitrate int `sql:"default: null"`
+ TagTitle string `sql:"default: null"`
+ TagTitleUDec string `sql:"default: null"`
+ TagTrackArtist string `sql:"default: null"`
+ TagTrackNumber int `sql:"default: null"`
+ TagDiscNumber int `sql:"default: null"`
+ TagBrainzID string `sql:"default: null"`
TrackStar *TrackStar
TrackRating *TrackRating
AverageRating float64 `sql:"default: null"`
@@ -372,6 +389,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"`
diff --git a/db/migrations.go b/db/migrations.go
index 587d4f6e..8e9d6c62 100644
--- a/db/migrations.go
+++ b/db/migrations.go
@@ -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
+}
diff --git a/mockfs/mockfs.go b/mockfs/mockfs.go
index ea332eef..23eabc54 100644
--- a/mockfs/mockfs.go
+++ b/mockfs/mockfs.go
@@ -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 }
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 3c471c70..24d814a2 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -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
)
diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go
index bf49a389..0fd24549 100644
--- a/scanner/scanner_test.go
+++ b/scanner/scanner_test.go
@@ -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 {
diff --git a/scanner/testdata/fuzz/FuzzScanner/22023bdee30f39396809ddfa903114a127d16fbf11dbd52256541a17c9f60b07 b/scanner/testdata/fuzz/FuzzScanner/22023bdee30f39396809ddfa903114a127d16fbf11dbd52256541a17c9f60b07
deleted file mode 100644
index 7865f865..00000000
--- a/scanner/testdata/fuzz/FuzzScanner/22023bdee30f39396809ddfa903114a127d16fbf11dbd52256541a17c9f60b07
+++ /dev/null
@@ -1,3 +0,0 @@
-go test fuzz v1
-[]byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
-int64(0)
diff --git a/scanner/testdata/fuzz/FuzzScanner/50640a7a67794863f262b749f288aff8610f2cac65b7cf65b8708eb0ea104519 b/scanner/testdata/fuzz/FuzzScanner/50640a7a67794863f262b749f288aff8610f2cac65b7cf65b8708eb0ea104519
deleted file mode 100644
index e35970af..00000000
--- a/scanner/testdata/fuzz/FuzzScanner/50640a7a67794863f262b749f288aff8610f2cac65b7cf65b8708eb0ea104519
+++ /dev/null
@@ -1,3 +0,0 @@
-go test fuzz v1
-[]byte("001")
-int64(3472329395739373616)
diff --git a/scanner/testdata/fuzz/FuzzScanner/b1f8e3fd8087149215418297f28d6e1e447100179409d69a827c6651370d96cc b/scanner/testdata/fuzz/FuzzScanner/b1f8e3fd8087149215418297f28d6e1e447100179409d69a827c6651370d96cc
deleted file mode 100644
index e35970af..00000000
--- a/scanner/testdata/fuzz/FuzzScanner/b1f8e3fd8087149215418297f28d6e1e447100179409d69a827c6651370d96cc
+++ /dev/null
@@ -1,3 +0,0 @@
-go test fuzz v1
-[]byte("001")
-int64(3472329395739373616)
diff --git a/scanner/testdata/fuzz/FuzzScanner/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9 b/scanner/testdata/fuzz/FuzzScanner/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9
deleted file mode 100644
index ffa7e5c5..00000000
--- a/scanner/testdata/fuzz/FuzzScanner/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9
+++ /dev/null
@@ -1,3 +0,0 @@
-go test fuzz v1
-[]byte("")
-int64(0)
diff --git a/server/ctrladmin/adminui/pages/home.tmpl b/server/ctrladmin/adminui/pages/home.tmpl
index baa0aaf7..d330d504 100644
--- a/server/ctrladmin/adminui/pages/home.tmpl
+++ b/server/ctrladmin/adminui/pages/home.tmpl
@@ -7,12 +7,20 @@
"Desc" "total items found in all watched folders"
) }}
-
artists
-
{{ .ArtistCount }}
+
folders
+
{{ .Stats.Folders }}
albums
-
{{ .AlbumCount }}
+
{{ .Stats.Albums }}
+
artists
+
{{ .Stats.Artists }}
+
album artists
+
{{ .Stats.AlbumArtists }}
tracks
-
{{ .TrackCount }}
+
{{ .Stats.Tracks }}
+
internet radio stations
+
{{ .Stats.InternetRadioStations }}
+
podcasts
+
{{ .Stats.Podcasts }}
{{ end }}
diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go
index 5b9fa78a..063869f6 100644
--- a/server/ctrladmin/ctrl.go
+++ b/server/ctrladmin/ctrl.go
@@ -274,10 +274,9 @@ type templateData struct {
Flashes []interface{}
User *db.User
Version string
+
// home
- AlbumCount int
- ArtistCount int
- TrackCount int
+ Stats db.Stats
RequestRoot string
RecentFolders []*db.Album
AllUsers []*db.User
diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go
index c5f80037..cb081983 100644
--- a/server/ctrladmin/handlers.go
+++ b/server/ctrladmin/handlers.go
@@ -36,9 +36,7 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
data := &templateData{}
// stats box
- c.dbc.Model(&db.Artist{}).Count(&data.ArtistCount)
- c.dbc.Model(&db.Album{}).Count(&data.AlbumCount)
- c.dbc.Table("tracks").Count(&data.TrackCount)
+ data.Stats, _ = c.dbc.Stats()
// lastfm box
data.RequestRoot = handlerutil.BaseURL(r)
data.CurrentLastFMAPIKey, _ = c.dbc.GetSetting(db.LastFMAPIKey)
diff --git a/server/ctrlsubsonic/handlers_by_folder.go b/server/ctrlsubsonic/handlers_by_folder.go
index b542d303..7ea90ec8 100644
--- a/server/ctrlsubsonic/handlers_by_folder.go
+++ b/server/ctrlsubsonic/handlers_by_folder.go
@@ -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 != "" {
diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go
index c3830985..bf5ab1f6 100644
--- a/server/ctrlsubsonic/handlers_by_tags.go
+++ b/server/ctrlsubsonic/handlers_by_tags.go
@@ -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").
diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go
index d42caf9c..b435f533 100644
--- a/server/ctrlsubsonic/handlers_common.go
+++ b/server/ctrlsubsonic/handlers_common.go
@@ -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").
diff --git a/server/ctrlsubsonic/handlers_playlist.go b/server/ctrlsubsonic/handlers_playlist.go
index d196b70e..de03430f 100644
--- a/server/ctrlsubsonic/handlers_playlist.go
+++ b/server/ctrlsubsonic/handlers_playlist.go
@@ -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)
diff --git a/server/ctrlsubsonic/spec/construct_by_folder.go b/server/ctrlsubsonic/spec/construct_by_folder.go
index f1c7c203..fb42786e 100644
--- a/server/ctrlsubsonic/spec/construct_by_folder.go
+++ b/server/ctrlsubsonic/spec/construct_by_folder.go
@@ -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
}
diff --git a/server/ctrlsubsonic/spec/construct_by_tags.go b/server/ctrlsubsonic/spec/construct_by_tags.go
index 01b9c6cc..ea59b68d 100644
--- a/server/ctrlsubsonic/spec/construct_by_tags.go
+++ b/server/ctrlsubsonic/spec/construct_by_tags.go
@@ -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
}
diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go
index 1f9c055d..a47c0074 100644
--- a/server/ctrlsubsonic/spec/spec.go
+++ b/server/ctrlsubsonic/spec/spec.go
@@ -160,29 +160,30 @@ type TranscodeMeta struct {
}
type TrackChild struct {
- ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
- Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
- 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"`
- 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"`
- CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
- Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
- Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
- Genres []*GenreRef `xml:"genres,omitempty" json:"genres,omitempty"`
- IsDir bool `xml:"isDir,attr" json:"isDir"`
- IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
- ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
- Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
- Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
- Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
- Title string `xml:"title,attr" json:"title"`
- TrackNumber int `xml:"track,attr,omitempty" json:"track,omitempty"`
- DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
- Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
- Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
+ ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
+ Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
+ 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"`
+ CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
+ Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
+ Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
+ Genres []*GenreRef `xml:"genres,omitempty" json:"genres,omitempty"`
+ IsDir bool `xml:"isDir,attr" json:"isDir"`
+ IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
+ ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
+ Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
+ Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
+ Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
+ Title string `xml:"title,attr" json:"title"`
+ TrackNumber int `xml:"track,attr,omitempty" json:"track,omitempty"`
+ DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
+ Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
+ Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
// star / rating
Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"`
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
diff --git a/server/ctrlsubsonic/testdata/test_get_album_with_cover b/server/ctrlsubsonic/testdata/test_get_album_with_cover
index 0481e27e..1e24d68e 100644
--- a/server/ctrlsubsonic/testdata/test_get_album_with_cover
+++ b/server/ctrlsubsonic/testdata/test_get_album_with_cover
@@ -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",
diff --git a/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks b/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks
index 32430f7f..ddd27204 100644
--- a/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks
+++ b/server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks
@@ -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",
diff --git a/server/ctrlsubsonic/testdata/test_search_three_q_tra b/server/ctrlsubsonic/testdata/test_search_three_q_tra
index 5ac9afe0..a3df7198 100644
--- a/server/ctrlsubsonic/testdata/test_search_three_q_tra
+++ b/server/ctrlsubsonic/testdata/test_search_three_q_tra
@@ -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",
diff --git a/server/ctrlsubsonic/testdata/test_search_two_q_tra b/server/ctrlsubsonic/testdata/test_search_two_q_tra
index 6ea777e5..18e27d89 100644
--- a/server/ctrlsubsonic/testdata/test_search_two_q_tra
+++ b/server/ctrlsubsonic/testdata/test_search_two_q_tra
@@ -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",
diff --git a/tags/tagcommon/tagcommmon.go b/tags/tagcommon/tagcommmon.go
index df20553a..eb7b657c 100644
--- a/tags/tagcommon/tagcommmon.go
+++ b/tags/tagcommon/tagcommmon.go
@@ -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
diff --git a/tags/taglib/taglib.go b/tags/taglib/taglib.go
index 87a4c3f6..ff660165 100644
--- a/tags/taglib/taglib.go
+++ b/tags/taglib/taglib.go
@@ -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") }