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_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) |
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
30
db/db.go
30
db/db.go
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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).
|
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 != "" {
|
||||||
|
|||||||
@@ -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").
|
||||||
|
|||||||
@@ -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").
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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") }
|
||||||
|
|||||||
Reference in New Issue
Block a user