feat(subsonic): add support for multi-valued album artist tags
closes #103 a a a r a a a a a a a a a a
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
@@ -55,6 +56,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
|
||||
construct(ctx, "202211111057", migratePlaylistsQueuesToFullID),
|
||||
construct(ctx, "202304221528", migratePlaylistsToM3U),
|
||||
construct(ctx, "202305301718", migratePlayCountToLength),
|
||||
construct(ctx, "202307281628", migrateAlbumArtistsMany2Many),
|
||||
}
|
||||
|
||||
return gormigrate.
|
||||
@@ -538,3 +540,52 @@ func migratePlayCountToLength(tx *gorm.DB, _ MigrationContext) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateAlbumArtistsMany2Many(tx *gorm.DB, _ MigrationContext) error {
|
||||
// gorms seems to want to create the table automatically without ON DELETE rules
|
||||
step := tx.DropTableIfExists(AlbumArtist{})
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop prev: %w", err)
|
||||
}
|
||||
|
||||
step = tx.AutoMigrate(
|
||||
AlbumArtist{},
|
||||
Album{},
|
||||
Artist{},
|
||||
)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step auto migrate: %w", err)
|
||||
}
|
||||
|
||||
if tx.Dialect().HasColumn("albums", "tag_artist_id") {
|
||||
tx = tx.LogMode(false)
|
||||
step = tx.Exec(`
|
||||
INSERT INTO album_artists (album_id, artist_id)
|
||||
SELECT id album_id, tag_artist_id artist_id
|
||||
FROM albums
|
||||
WHERE tag_artist_id IS NOT NULL;
|
||||
`)
|
||||
if err := step.Error; err != nil && !strings.Contains(err.Error(), "no such column") {
|
||||
return fmt.Errorf("step insert from albums: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`DROP INDEX idx_albums_tag_artist_id`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop index: %w", err)
|
||||
}
|
||||
|
||||
step = tx.Exec(`ALTER TABLE albums DROP COLUMN tag_artist_id;`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop albums tag artist id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tx.Dialect().HasColumn("tracks", "artist_id") {
|
||||
step = tx.Exec(`ALTER TABLE tracks DROP COLUMN artist_id;`)
|
||||
if err := step.Error; err != nil {
|
||||
return fmt.Errorf("step drop track tag artist: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
51
db/model.go
51
db/model.go
@@ -9,6 +9,7 @@ package db
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -46,7 +47,7 @@ type Artist struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
Name string `gorm:"not null; unique_index"`
|
||||
NameUDec string `sql:"default: null"`
|
||||
Albums []*Album `gorm:"foreignkey:TagArtistID"`
|
||||
Albums []*Album `gorm:"many2many:album_artists"`
|
||||
AlbumCount int `sql:"-"`
|
||||
Cover string `sql:"default: null"`
|
||||
ArtistStar *ArtistStar
|
||||
@@ -89,9 +90,7 @@ 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"`
|
||||
Artist *Artist
|
||||
ArtistID int `gorm:"not null" sql:"default: null; type:int REFERENCES artists(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"`
|
||||
Size int `sql:"default: null"`
|
||||
Length int `sql:"default: null"`
|
||||
@@ -118,10 +117,6 @@ func (t *Track) AlbumSID() *specid.ID {
|
||||
return &specid.ID{Type: specid.Album, Value: t.AlbumID}
|
||||
}
|
||||
|
||||
func (t *Track) ArtistSID() *specid.ID {
|
||||
return &specid.ID{Type: specid.Artist, Value: t.ArtistID}
|
||||
}
|
||||
|
||||
func (t *Track) Ext() string {
|
||||
return filepath.Ext(t.Filename)
|
||||
}
|
||||
@@ -190,7 +185,7 @@ type Play struct {
|
||||
AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
Time time.Time `sql:"default: null"`
|
||||
Count int
|
||||
Length int
|
||||
Length int
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
@@ -202,16 +197,15 @@ type Album struct {
|
||||
RightPath string `gorm:"not null; unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
RightPathUDec string `sql:"default: null"`
|
||||
Parent *Album
|
||||
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
RootDir string `gorm:"unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
Genres []*Genre `gorm:"many2many:album_genres"`
|
||||
Cover string `sql:"default: null"`
|
||||
TagArtist *Artist
|
||||
TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||
TagTitle string `sql:"default: null"`
|
||||
TagTitleUDec string `sql:"default: null"`
|
||||
TagBrainzID string `sql:"default: null"`
|
||||
TagYear int `sql:"default: null"`
|
||||
ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
RootDir string `gorm:"unique_index:idx_album_abs_path" sql:"default: null"`
|
||||
Genres []*Genre `gorm:"many2many:album_genres"`
|
||||
Cover string `sql:"default: null"`
|
||||
Artists []*Artist `gorm:"many2many:album_artists"`
|
||||
TagTitle string `sql:"default: null"`
|
||||
TagTitleUDec string `sql:"default: null"`
|
||||
TagBrainzID string `sql:"default: null"`
|
||||
TagYear int `sql:"default: null"`
|
||||
Tracks []*Track
|
||||
ChildCount int `sql:"-"`
|
||||
Duration int `sql:"-"`
|
||||
@@ -243,6 +237,18 @@ func (a *Album) GenreStrings() []string {
|
||||
return strs
|
||||
}
|
||||
|
||||
func (a *Album) ArtistsString() string {
|
||||
var artists = append([]*Artist(nil), a.Artists...)
|
||||
sort.Slice(artists, func(i, j int) bool {
|
||||
return artists[i].ID < artists[j].ID
|
||||
})
|
||||
var names []string
|
||||
for _, artist := range artists {
|
||||
names = append(names, artist.Name)
|
||||
}
|
||||
return strings.Join(names, " & ")
|
||||
}
|
||||
|
||||
type PlayQueue struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
CreatedAt time.Time
|
||||
@@ -275,6 +281,13 @@ type TranscodePreference struct {
|
||||
Profile string `gorm:"not null" sql:"default: null"`
|
||||
}
|
||||
|
||||
type AlbumArtist struct {
|
||||
Album *Album
|
||||
AlbumID int `gorm:"not null; unique_index:idx_album_id_artist_id" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
|
||||
Artist *Artist
|
||||
ArtistID int `gorm:"not null; unique_index:idx_album_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"`
|
||||
|
||||
26
go.mod
26
go.mod
@@ -4,7 +4,7 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||
github.com/andybalholm/cascadia v1.3.1
|
||||
github.com/andybalholm/cascadia v1.3.2
|
||||
github.com/dexterlb/mpvipc v0.0.0-20221227161445-38b9935eae9d
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
@@ -16,12 +16,11 @@ require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414
|
||||
github.com/josephburnett/jd v1.5.2
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/mmcdole/gofeed v1.2.0
|
||||
github.com/mmcdole/gofeed v1.2.1
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||
@@ -31,9 +30,10 @@ require (
|
||||
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137
|
||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
||||
golang.org/x/net v0.14.0
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -46,14 +46,14 @@ require (
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lib/pq v1.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.0 // indirect
|
||||
@@ -61,13 +61,13 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/image v0.5.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.12.0 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
)
|
||||
|
||||
51
go.sum
51
go.sum
@@ -10,8 +10,9 @@ github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBK
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -64,8 +65,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 h1:JkXdZo2OKW1t+GcTx5eb1kD2qW5lt1CDLrL2Ep9t+j4=
|
||||
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
@@ -98,12 +99,13 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
@@ -111,8 +113,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mmcdole/gofeed v1.2.0 h1:kuq7tJnDf0pnsDzF820ukuySHxFimAcizpG15gYHIns=
|
||||
github.com/mmcdole/gofeed v1.2.0/go.mod h1:TEyTG4gw4Q5Co+Hgahx/Oi3E0JHLM8BXtWC+mkJtRsw=
|
||||
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
|
||||
github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
|
||||
github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
|
||||
github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -139,6 +141,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137 h1:K0PSMi/p9ISHpfRFJB03d7VX+jjEsDARsTlAcN1zpac=
|
||||
github.com/sentriz/audiotags v0.0.0-20230419125925-8886243b2137/go.mod h1:nUVlCJ7n2jQoJ5rttpHozZ8pHJIhD9VehL6GP21FoDU=
|
||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
|
||||
@@ -160,14 +165,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
|
||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -177,10 +183,14 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -190,20 +200,26 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -221,7 +237,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||
|
||||
@@ -208,8 +208,8 @@ func (m *MockFS) LogAlbums() {
|
||||
|
||||
m.t.Logf("\nalbums")
|
||||
for _, album := range albums {
|
||||
m.t.Logf("id %-3d root %-3s lr %-15s %-10s pid %-3d aid %-3d cov %-10s",
|
||||
album.ID, album.RootDir, album.LeftPath, album.RightPath, album.ParentID, album.TagArtistID, album.Cover)
|
||||
m.t.Logf("id %-3d root %-3s lr %-15s %-10s pid %-3d cov %-10s",
|
||||
album.ID, album.RootDir, album.LeftPath, album.RightPath, album.ParentID, album.Cover)
|
||||
}
|
||||
m.t.Logf("total %d", len(albums))
|
||||
}
|
||||
@@ -341,7 +341,7 @@ func (m *MockFS) DumpDB(suffix ...string) {
|
||||
m.t.Fatalf("backing up: %v", err)
|
||||
}
|
||||
|
||||
m.t.Error("DumpDB left behind")
|
||||
m.t.Error(destPath)
|
||||
}
|
||||
|
||||
type tagReaderResult struct {
|
||||
|
||||
@@ -360,14 +360,24 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
|
||||
}
|
||||
|
||||
// metadata for the album table comes only from the the first track's tags
|
||||
if i == 0 || album.TagArtist == nil {
|
||||
albumArtist, err := populateAlbumArtist(tx, parent, tags.MustAlbumArtist(trags))
|
||||
if err != nil {
|
||||
return fmt.Errorf("populate album artist: %w", err)
|
||||
if i == 0 {
|
||||
albumArtists := tags.MustAlbumArtists(trags)
|
||||
var albumArtistIDs []int
|
||||
for _, albumArtistName := range albumArtists {
|
||||
albumArtist, err := populateArtist(tx, parent, albumArtistName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("populate album artist: %w", err)
|
||||
}
|
||||
albumArtistIDs = append(albumArtistIDs, albumArtist.ID)
|
||||
}
|
||||
if err := populateAlbum(tx, album, albumArtist, trags, stat.ModTime()); err != nil {
|
||||
if err := populateAlbumArtists(tx, album, albumArtistIDs); err != nil {
|
||||
return fmt.Errorf("populate album artists: %w", err)
|
||||
}
|
||||
|
||||
if err := populateAlbum(tx, album, trags, stat.ModTime()); err != nil {
|
||||
return fmt.Errorf("populate album: %w", err)
|
||||
}
|
||||
|
||||
if err := populateAlbumGenres(tx, album, genreIDs); err != nil {
|
||||
return fmt.Errorf("populate album genres: %w", err)
|
||||
}
|
||||
@@ -386,13 +396,12 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, par
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAlbum(tx *db.DB, album *db.Album, albumArtist *db.Artist, trags tags.Parser, modTime time.Time) error {
|
||||
func populateAlbum(tx *db.DB, album *db.Album, trags tags.Parser, modTime time.Time) error {
|
||||
albumName := tags.MustAlbum(trags)
|
||||
album.TagTitle = albumName
|
||||
album.TagTitleUDec = decoded(albumName)
|
||||
album.TagBrainzID = trags.AlbumBrainzID()
|
||||
album.TagYear = trags.Year()
|
||||
album.TagArtist = albumArtist
|
||||
|
||||
album.ModifiedAt = modTime
|
||||
album.CreatedAt = modTime
|
||||
@@ -434,7 +443,6 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tags.Parse
|
||||
track.FilenameUDec = decoded(basename)
|
||||
track.Size = size
|
||||
track.AlbumID = album.ID
|
||||
track.ArtistID = album.TagArtist.ID
|
||||
|
||||
track.TagTitle = trags.Title()
|
||||
track.TagTitleUDec = decoded(trags.Title())
|
||||
@@ -453,7 +461,7 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tags.Parse
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAlbumArtist(tx *db.DB, parent *db.Album, artistName string) (*db.Artist, error) {
|
||||
func populateArtist(tx *db.DB, parent *db.Album, artistName string) (*db.Artist, error) {
|
||||
var update db.Artist
|
||||
update.Name = artistName
|
||||
update.NameUDec = decoded(artistName)
|
||||
@@ -510,6 +518,17 @@ func populateAlbumGenres(tx *db.DB, album *db.Album, genreIDs []int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAlbumArtists(tx *db.DB, album *db.Album, albumArtistIDs []int) error {
|
||||
if err := tx.Where("album_id=?", album.ID).Delete(db.AlbumArtist{}).Error; err != nil {
|
||||
return fmt.Errorf("delete old album album artists: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.InsertBulkLeftMany("album_artists", []string{"album_id", "artist_id"}, album.ID, albumArtistIDs); err != nil {
|
||||
return fmt.Errorf("insert bulk album 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()) }()
|
||||
@@ -561,8 +580,8 @@ func (s *Scanner) cleanArtists(c *Context) error {
|
||||
sub := s.db.
|
||||
Select("artists.id").
|
||||
Model(&db.Artist{}).
|
||||
Joins("LEFT JOIN albums ON albums.tag_artist_id=artists.id").
|
||||
Where("albums.id IS NULL").
|
||||
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).
|
||||
|
||||
@@ -115,9 +115,12 @@ func TestCoverBeforeTracks(t *testing.T) {
|
||||
m.ScanAndClean()
|
||||
|
||||
var album db.Album
|
||||
require.NoError(m.DB().Preload("TagArtist").Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album has cover
|
||||
require.Equal("cover.jpg", album.Cover) // album has cover
|
||||
require.Equal("artist-2", album.TagArtist.Name) // album artist
|
||||
require.NoError(m.DB().Where("left_path=? AND right_path=?", "artist-2/", "album-2").Find(&album).Error) // album has cover
|
||||
require.Equal("cover.jpg", album.Cover) // album has cover
|
||||
|
||||
var albumArtist db.Artist
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", album.ID).Find(&albumArtist).Error) // album has cover
|
||||
require.Equal("artist-2", albumArtist.Name) // album artist
|
||||
|
||||
var tracks []*db.Track
|
||||
require.NoError(m.DB().Where("album_id=?", album.ID).Find(&tracks).Error) // album has tracks
|
||||
@@ -141,11 +144,14 @@ func TestUpdatedTags(t *testing.T) {
|
||||
m.ScanAndClean()
|
||||
|
||||
var track db.Track
|
||||
require.NoError(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&track).Error) // track has tags
|
||||
require.Equal("artist", track.TagTrackArtist) // track has tags
|
||||
require.Equal("album-artist", track.Artist.Name) // track has tags
|
||||
require.Equal("album", track.Album.TagTitle) // track has tags
|
||||
require.Equal("title", track.TagTitle) // track has tags
|
||||
require.NoError(m.DB().Preload("Album").Where("filename=?", "track-10.flac").Find(&track).Error) // track has tags
|
||||
require.Equal("artist", track.TagTrackArtist) // track has tags
|
||||
require.Equal("album", track.Album.TagTitle) // track has tags
|
||||
require.Equal("title", track.TagTitle) // track has tags
|
||||
|
||||
var trackArtistA db.Artist
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", track.AlbumID).Limit(1).Find(&trackArtistA).Error) // updated has tags
|
||||
require.Equal("album-artist", trackArtistA.Name) // track has tags
|
||||
|
||||
m.SetTags("artist-10/album-10/track-10.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawArtist = "artist-upd"
|
||||
@@ -158,12 +164,15 @@ func TestUpdatedTags(t *testing.T) {
|
||||
m.ScanAndClean()
|
||||
|
||||
var updated db.Track
|
||||
require.NoError(m.DB().Preload("Album").Preload("Artist").Where("filename=?", "track-10.flac").Find(&updated).Error) // updated has tags
|
||||
require.Equal(track.ID, updated.ID) // updated has tags
|
||||
require.Equal("artist-upd", updated.TagTrackArtist) // updated has tags
|
||||
require.Equal("album-artist-upd", updated.Artist.Name) // updated has tags
|
||||
require.Equal("album-upd", updated.Album.TagTitle) // updated has tags
|
||||
require.Equal("title-upd", updated.TagTitle) // updated has tags
|
||||
require.NoError(m.DB().Preload("Album").Where("filename=?", "track-10.flac").Find(&updated).Error) // updated has tags
|
||||
require.Equal(track.ID, updated.ID) // updated has tags
|
||||
require.Equal("artist-upd", updated.TagTrackArtist) // updated has tags
|
||||
require.Equal("album-upd", updated.Album.TagTitle) // updated has tags
|
||||
require.Equal("title-upd", updated.TagTitle) // updated has tags
|
||||
|
||||
var trackArtistB db.Artist
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.artist_id=artists.id").Where("album_artists.album_id=?", track.AlbumID).Limit(1).Find(&trackArtistB).Error) // updated has tags
|
||||
require.Equal("album-artist-upd", trackArtistB.Name) // updated has tags
|
||||
}
|
||||
|
||||
// https://github.com/sentriz/gonic/issues/225
|
||||
@@ -409,21 +418,22 @@ func TestMultiFolderWithSharedArtist(t *testing.T) {
|
||||
})
|
||||
m.ScanAndClean()
|
||||
|
||||
sq := func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Select("*, count(sub.id) child_count, sum(sub.length) duration").
|
||||
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
|
||||
Group("albums.id")
|
||||
}
|
||||
|
||||
var artist db.Artist
|
||||
require.NoError(m.DB().Where("name=?", artistName).Preload("Albums", sq).First(&artist).Error)
|
||||
require.NoError(m.DB().Where("name=?", artistName).First(&artist).Error)
|
||||
require.Equal(artistName, artist.Name)
|
||||
require.Equal(2, len(artist.Albums))
|
||||
|
||||
for _, album := range artist.Albums {
|
||||
var artistAlbums []*db.Album
|
||||
require.NoError(m.DB().
|
||||
Select("*, count(sub.id) child_count, sum(sub.length) duration").
|
||||
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
|
||||
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
|
||||
Where("album_artists.artist_id=?", artist.ID).
|
||||
Group("albums.id").
|
||||
Find(&artistAlbums).Error)
|
||||
require.Equal(2, len(artistAlbums))
|
||||
|
||||
for _, album := range artistAlbums {
|
||||
require.Greater(album.TagYear, 0)
|
||||
require.Equal(artist.ID, album.TagArtistID)
|
||||
require.Greater(album.ChildCount, 0)
|
||||
require.Greater(album.Duration, 0)
|
||||
}
|
||||
@@ -574,12 +584,15 @@ func TestCompilationAlbumWithoutAlbumArtist(t *testing.T) {
|
||||
require.Equal(5, trackCount)
|
||||
|
||||
var artists []*db.Artist
|
||||
require.NoError(m.DB().Preload("Albums").Find(&artists).Error)
|
||||
require.NoError(m.DB().Find(&artists).Error)
|
||||
require.Equal(1, len(artists)) // we only have one album artist
|
||||
require.Equal("artist 0", artists[0].Name) // it came from the first track's fallback to artist tag
|
||||
require.Equal(1, len(artists[0].Albums)) // the artist has one album
|
||||
require.Equal(pathAlbum, artists[0].Albums[0].RightPath)
|
||||
require.Equal(pathArtist+"/", artists[0].Albums[0].LeftPath)
|
||||
|
||||
var artistAlbums []*db.Album
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Where("album_artists.artist_id=?", artists[0].ID).Find(&artistAlbums).Error)
|
||||
require.Equal(1, len(artistAlbums)) // the artist has one album
|
||||
require.Equal(pathAlbum, artistAlbums[0].RightPath)
|
||||
require.Equal(pathArtist+"/", artistAlbums[0].LeftPath)
|
||||
}
|
||||
|
||||
func TestIncrementalScanNoChangeNoUpdatedAt(t *testing.T) {
|
||||
@@ -591,11 +604,11 @@ func TestIncrementalScanNoChangeNoUpdatedAt(t *testing.T) {
|
||||
|
||||
m.ScanAndClean()
|
||||
var albumA db.Album
|
||||
require.NoError(m.DB().Where("tag_artist_id NOT NULL").Order("updated_at DESC").Find(&albumA).Error)
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Order("updated_at DESC").Find(&albumA).Error)
|
||||
|
||||
m.ScanAndClean()
|
||||
var albumB db.Album
|
||||
require.NoError(m.DB().Where("tag_artist_id NOT NULL").Order("updated_at DESC").Find(&albumB).Error)
|
||||
require.NoError(m.DB().Joins("JOIN album_artists ON album_artists.album_id=albums.id").Order("updated_at DESC").Find(&albumB).Error)
|
||||
|
||||
require.Equal(albumB.UpdatedAt, albumA.UpdatedAt)
|
||||
}
|
||||
@@ -646,3 +659,139 @@ func TestNoOrphanedGenres(t *testing.T) {
|
||||
require.NoError(m.DB().Model(&db.Genre{}).Count(&genreCount).Error)
|
||||
require.Equal(0, genreCount)
|
||||
}
|
||||
|
||||
func TestMultiArtistSupport(t *testing.T) {
|
||||
t.Parallel()
|
||||
require := assert.New(t)
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Mutator"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Yerself Is Steam"
|
||||
tags.RawAlbumArtist = "Mercury Rev"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
|
||||
var artists []*db.Artist
|
||||
require.NoError(m.DB().Find(&artists).Error)
|
||||
require.Len(artists, 3) // alan, liz, mercury
|
||||
|
||||
var albumArtists []*db.AlbumArtist
|
||||
require.NoError(m.DB().Find(&albumArtists).Error)
|
||||
require.Len(albumArtists, 5)
|
||||
|
||||
type row struct{ Artist, Albums string }
|
||||
state := func() []row {
|
||||
var table []row
|
||||
require.NoError(m.DB().
|
||||
Select("artists.name artist, group_concat(albums.tag_title, ';') albums").
|
||||
Model(db.Artist{}).
|
||||
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Joins("JOIN albums ON albums.id=album_artists.album_id").
|
||||
Order("artists.name, albums.tag_title").
|
||||
Group("artists.id").
|
||||
Scan(&table).
|
||||
Error)
|
||||
return table
|
||||
}
|
||||
|
||||
require.Equal(
|
||||
[]row{
|
||||
{"Alan Vega", "Mutator;Dead Man"},
|
||||
{"Liz Lamere", "Mutator"},
|
||||
{"Mercury Rev", "Dead Man;Yerself Is Steam"},
|
||||
},
|
||||
state(),
|
||||
)
|
||||
|
||||
m.RemoveAll("artist-0/album-2")
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega"}
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
|
||||
require.NoError(m.DB().Find(&artists).Error)
|
||||
require.Len(artists, 2) // alan, liz
|
||||
|
||||
require.NoError(m.DB().Find(&albumArtists).Error)
|
||||
require.Len(albumArtists, 3)
|
||||
|
||||
require.Equal(
|
||||
[]row{
|
||||
{"Alan Vega", "Mutator;Dead Man"},
|
||||
{"Liz Lamere", "Mutator"},
|
||||
},
|
||||
state(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestMultiArtistPreload(t *testing.T) {
|
||||
t.Parallel()
|
||||
require := assert.New(t)
|
||||
m := mockfs.New(t)
|
||||
|
||||
m.AddItemsGlob("artist-0/album-[012]/track-0.*")
|
||||
m.SetTags("artist-0/album-0/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Mutator"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Liz Lamere"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-1/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Dead Man"
|
||||
tags.RawAlbumArtists = []string{"Alan Vega", "Mercury Rev"}
|
||||
return nil
|
||||
})
|
||||
m.SetTags("artist-0/album-2/track-0.flac", func(tags *mockfs.Tags) error {
|
||||
tags.RawAlbum = "Yerself Is Steam"
|
||||
tags.RawAlbumArtist = "Mercury Rev"
|
||||
return nil
|
||||
})
|
||||
|
||||
m.ScanAndClean()
|
||||
|
||||
var albums []*db.Album
|
||||
require.NoError(m.DB().Preload("Artists").Find(&albums).Error)
|
||||
require.GreaterOrEqual(len(albums), 3)
|
||||
|
||||
for _, album := range albums {
|
||||
switch album.TagTitle {
|
||||
case "Mutator":
|
||||
require.Len(album.Artists, 2)
|
||||
case "Dead Man":
|
||||
require.Len(album.Artists, 2)
|
||||
case "Yerself Is Steam":
|
||||
require.Len(album.Artists, 1)
|
||||
}
|
||||
}
|
||||
|
||||
var artists []*db.Artist
|
||||
require.NoError(m.DB().Preload("Albums").Find(&artists).Error)
|
||||
require.Equal(3, len(artists))
|
||||
|
||||
for _, artist := range artists {
|
||||
switch artist.Name {
|
||||
case "Alan Vega":
|
||||
require.Len(artist.Albums, 2)
|
||||
case "Mercury Rev":
|
||||
require.Len(artist.Albums, 2)
|
||||
case "Liz Lamere":
|
||||
require.Len(artist.Albums, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,16 +127,6 @@ func MustArtist(p Parser) string {
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
func MustAlbumArtist(p Parser) string {
|
||||
if r := p.AlbumArtist(); r != "" {
|
||||
return r
|
||||
}
|
||||
if r := p.Artist(); r != "" {
|
||||
return r
|
||||
}
|
||||
return "Unknown Artist"
|
||||
}
|
||||
|
||||
func MustAlbumArtists(p Parser) []string {
|
||||
if r := p.AlbumArtists(); len(r) > 0 {
|
||||
return r
|
||||
|
||||
@@ -29,6 +29,10 @@ func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, su
|
||||
if user.LastFMSession == "" {
|
||||
return nil
|
||||
}
|
||||
if track.Album == nil || len(track.Album.Artists) == 0 {
|
||||
return fmt.Errorf("track has no album artists")
|
||||
}
|
||||
|
||||
apiKey, err := s.db.GetSetting("lastfm_api_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get api key: %w", err)
|
||||
@@ -46,13 +50,14 @@ func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, su
|
||||
} else {
|
||||
params.Add("method", "track.updateNowPlaying")
|
||||
}
|
||||
|
||||
params.Add("api_key", apiKey)
|
||||
params.Add("sk", user.LastFMSession)
|
||||
params.Add("artist", track.TagTrackArtist)
|
||||
params.Add("track", track.TagTitle)
|
||||
params.Add("trackNumber", strconv.Itoa(track.TagTrackNumber))
|
||||
params.Add("album", track.Album.TagTitle)
|
||||
params.Add("albumArtist", track.Artist.Name)
|
||||
params.Add("albumArtist", track.Album.ArtistsString())
|
||||
params.Add("duration", strconv.Itoa(track.Length))
|
||||
|
||||
// make sure we provide a valid uuid, since some users may have an incorrect mbid in their tags
|
||||
|
||||
@@ -62,9 +62,8 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
|
||||
// recent folders box
|
||||
c.DB.
|
||||
Where("tag_artist_id IS NOT NULL").
|
||||
Order("created_at DESC").
|
||||
Limit(8).
|
||||
Limit(20).
|
||||
Find(&data.RecentFolders)
|
||||
data.IsScanning = c.Scanner.IsScanning()
|
||||
if tStr, err := c.DB.GetSetting("last_scan_time"); err != nil {
|
||||
|
||||
@@ -90,7 +90,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
||||
c.DB.
|
||||
Where("album_id=?", id.Value).
|
||||
Preload("Album").
|
||||
Preload("Album.TagArtist").
|
||||
Preload("Album.Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Order("filename").
|
||||
@@ -178,7 +178,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
|
||||
Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration").
|
||||
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
|
||||
Group("albums.id").
|
||||
Where("albums.tag_artist_id IS NOT NULL").
|
||||
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
|
||||
Offset(params.GetOrInt("offset", 0)).
|
||||
Limit(params.GetOrInt("size", 10)).
|
||||
Preload("Parent").
|
||||
@@ -236,7 +236,7 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
||||
|
||||
// search "albums"
|
||||
var albums []*db.Album
|
||||
q = c.DB.Where(`tag_artist_id IS NOT NULL`)
|
||||
q = c.DB.Joins("JOIN album_artists ON album_artists.album_id=albums.id")
|
||||
for _, s := range queries {
|
||||
q = q.Where(`right_path LIKE ? OR right_path_u_dec LIKE ?`, s, s)
|
||||
}
|
||||
@@ -323,7 +323,7 @@ func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response {
|
||||
// "albums"
|
||||
var albums []*db.Album
|
||||
q = c.DB.
|
||||
Where("tag_artist_id IS NOT NULL").
|
||||
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
|
||||
Joins("JOIN album_stars ON albums.id=album_stars.album_id").
|
||||
Where("album_stars.user_id=?", user.ID).
|
||||
Preload("AlbumStar", "user_id=?", user.ID).
|
||||
|
||||
@@ -24,7 +24,8 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
||||
var artists []*db.Artist
|
||||
q := c.DB.
|
||||
Select("*, count(sub.id) album_count").
|
||||
Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id").
|
||||
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Joins("JOIN albums sub ON sub.id=album_artists.album_id").
|
||||
Preload("ArtistStar", "user_id=?", user.ID).
|
||||
Preload("ArtistRating", "user_id=?", user.ID).
|
||||
Group("artists.id").
|
||||
@@ -69,11 +70,10 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
|
||||
return db.
|
||||
Select("*, count(sub.id) child_count, sum(sub.length) duration").
|
||||
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
|
||||
Preload("AlbumStar", "user_id=?", user.ID).
|
||||
Preload("AlbumRating", "user_id=?", user.ID).
|
||||
Order("albums.right_path").
|
||||
Group("albums.id")
|
||||
}).
|
||||
Preload("Albums.Artists").
|
||||
Preload("ArtistStar", "user_id=?", user.ID).
|
||||
Preload("ArtistRating", "user_id=?", user.ID).
|
||||
First(artist, id.Value)
|
||||
@@ -81,7 +81,7 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
|
||||
sub.Artist = spec.NewArtistByTags(artist)
|
||||
sub.Artist.Albums = make([]*spec.Album, len(artist.Albums))
|
||||
for i, album := range artist.Albums {
|
||||
sub.Artist.Albums[i] = spec.NewAlbumByTags(album, artist)
|
||||
sub.Artist.Albums[i] = spec.NewAlbumByTags(album, album.Artists)
|
||||
}
|
||||
sub.Artist.AlbumCount = len(artist.Albums)
|
||||
return sub
|
||||
@@ -98,7 +98,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
||||
err = c.DB.
|
||||
Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration").
|
||||
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
|
||||
Preload("TagArtist").
|
||||
Preload("Artists").
|
||||
Preload("Genres").
|
||||
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
@@ -114,7 +114,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
||||
return spec.NewError(10, "couldn't find an album with that id")
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.Album = spec.NewAlbumByTags(album, album.TagArtist)
|
||||
sub.Album = spec.NewAlbumByTags(album, album.Artists)
|
||||
sub.Album.Tracks = make([]*spec.TrackChild, len(album.Tracks))
|
||||
|
||||
transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", ""))
|
||||
@@ -140,7 +140,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||
q := c.DB.DB
|
||||
switch listType {
|
||||
case "alphabeticalByArtist":
|
||||
q = q.Joins("JOIN artists ON albums.tag_artist_id=artists.id")
|
||||
q = q.Joins("JOIN artists ON artists.id=album_artists.artist_id")
|
||||
q = q.Order("artists.name")
|
||||
case "alphabeticalByName":
|
||||
q = q.Order("tag_title")
|
||||
@@ -186,10 +186,10 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||
Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration").
|
||||
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
|
||||
Group("albums.id").
|
||||
Where("albums.tag_artist_id IS NOT NULL").
|
||||
Joins("JOIN album_artists ON album_artists.album_id=albums.id").
|
||||
Offset(params.GetOrInt("offset", 0)).
|
||||
Limit(params.GetOrInt("size", 10)).
|
||||
Preload("TagArtist").
|
||||
Preload("Artists").
|
||||
Preload("AlbumStar", "user_id=?", user.ID).
|
||||
Preload("AlbumRating", "user_id=?", user.ID).
|
||||
Find(&albums)
|
||||
@@ -198,7 +198,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||
List: make([]*spec.Album, len(albums)),
|
||||
}
|
||||
for i, album := range albums {
|
||||
sub.AlbumsTwo.List[i] = spec.NewAlbumByTags(album, album.TagArtist)
|
||||
sub.AlbumsTwo.List[i] = spec.NewAlbumByTags(album, album.Artists)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
@@ -225,7 +225,9 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
for _, s := range queries {
|
||||
q = q.Where(`name LIKE ? OR name_u_dec LIKE ?`, s, s)
|
||||
}
|
||||
q = q.Joins("JOIN albums ON albums.tag_artist_id=artists.id").
|
||||
q = q.
|
||||
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Joins("JOIN albums ON albums.id=album_artists.album_id").
|
||||
Preload("ArtistStar", "user_id=?", user.ID).
|
||||
Preload("ArtistRating", "user_id=?", user.ID).
|
||||
Offset(params.GetOrInt("artistOffset", 0)).
|
||||
@@ -243,14 +245,15 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
// search albums
|
||||
var albums []*db.Album
|
||||
q = c.DB.
|
||||
Preload("TagArtist").
|
||||
Preload("Artists").
|
||||
Preload("Genres").
|
||||
Preload("AlbumStar", "user_id=?", user.ID).
|
||||
Preload("AlbumRating", "user_id=?", user.ID)
|
||||
for _, s := range queries {
|
||||
q = q.Where(`tag_title LIKE ? OR tag_title_u_dec LIKE ?`, s, s)
|
||||
}
|
||||
q = q.Offset(params.GetOrInt("albumOffset", 0)).
|
||||
q = q.
|
||||
Offset(params.GetOrInt("albumOffset", 0)).
|
||||
Limit(params.GetOrInt("albumCount", 20))
|
||||
if m := getMusicFolder(c.MusicPaths, params); m != "" {
|
||||
q = q.Where("root_dir=?", m)
|
||||
@@ -259,19 +262,19 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
return spec.NewError(0, "find albums: %v", err)
|
||||
}
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.TagArtist))
|
||||
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.Artists))
|
||||
}
|
||||
|
||||
// search tracks
|
||||
var tracks []*db.Track
|
||||
q = c.DB.
|
||||
Preload("Album").
|
||||
Preload("Album.TagArtist").
|
||||
Preload("Album.Artists").
|
||||
Preload("Genres").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID)
|
||||
for _, s := range queries {
|
||||
q = q.Where(`tag_title LIKE ? OR tag_title_u_dec LIKE ?`, s, s)
|
||||
q = q.Where(`tracks.tag_title LIKE ? OR tracks.tag_title_u_dec LIKE ?`, s, s)
|
||||
}
|
||||
q = q.Offset(params.GetOrInt("songOffset", 0)).
|
||||
Limit(params.GetOrInt("songCount", 20))
|
||||
@@ -369,7 +372,8 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
||||
err = c.DB.
|
||||
Select("artists.*, count(albums.id) album_count").
|
||||
Where("name=?", similarInfo.Name).
|
||||
Joins("LEFT JOIN albums ON artists.id=albums.tag_artist_id").
|
||||
Joins("LEFT JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Joins("LEFT JOIN albums ON albums.id=album_artists.album_id").
|
||||
Group("artists.id").
|
||||
Find(&artist).
|
||||
Error
|
||||
@@ -421,7 +425,7 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||
Joins("JOIN track_genres ON track_genres.track_id=tracks.id").
|
||||
Joins("JOIN genres ON track_genres.genre_id=genres.id AND genres.name=?", genre).
|
||||
Preload("Album").
|
||||
Preload("Album.TagArtist").
|
||||
Preload("Album.Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Offset(params.GetOrInt("offset", 0)).
|
||||
@@ -429,6 +433,7 @@ func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||
if m := getMusicFolder(c.MusicPaths, params); m != "" {
|
||||
q = q.Where("albums.root_dir=?", m)
|
||||
}
|
||||
q = q.Group("tracks.id")
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
return spec.NewError(0, "error finding tracks: %v", err)
|
||||
}
|
||||
@@ -457,11 +462,13 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
|
||||
// artists
|
||||
var artists []*db.Artist
|
||||
q := c.DB.
|
||||
Group("artists.id").
|
||||
Joins("JOIN artist_stars ON artist_stars.artist_id=artists.id").
|
||||
Where("artist_stars.user_id=?", user.ID).
|
||||
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
|
||||
Joins("JOIN albums ON albums.id=album_artists.album_id").
|
||||
Preload("ArtistStar", "user_id=?", user.ID).
|
||||
Preload("ArtistRating", "user_id=?", user.ID)
|
||||
Preload("ArtistRating", "user_id=?", user.ID).
|
||||
Group("artists.id")
|
||||
if m := getMusicFolder(c.MusicPaths, params); m != "" {
|
||||
q = q.Where("albums.root_dir=?", m)
|
||||
}
|
||||
@@ -477,7 +484,7 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
|
||||
q = c.DB.
|
||||
Joins("JOIN album_stars ON album_stars.album_id=albums.id").
|
||||
Where("album_stars.user_id=?", user.ID).
|
||||
Preload("TagArtist").
|
||||
Preload("Artists").
|
||||
Preload("AlbumStar", "user_id=?", user.ID).
|
||||
Preload("AlbumRating", "user_id=?", user.ID)
|
||||
if m := getMusicFolder(c.MusicPaths, params); m != "" {
|
||||
@@ -487,7 +494,7 @@ func (c *Controller) ServeGetStarredTwo(r *http.Request) *spec.Response {
|
||||
return spec.NewError(0, "find albums: %v", err)
|
||||
}
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.TagArtist))
|
||||
results.Albums = append(results.Albums, spec.NewAlbumByTags(a, a.Artists))
|
||||
}
|
||||
|
||||
// tracks
|
||||
@@ -572,10 +579,13 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
|
||||
var tracks []*db.Track
|
||||
err = c.DB.
|
||||
Preload("Album").
|
||||
Where("artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames).
|
||||
Joins("JOIN albums ON albums.id=tracks.album_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).
|
||||
Limit(count).
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Group("tracks.id").
|
||||
Find(&tracks).
|
||||
Error
|
||||
if err != nil {
|
||||
@@ -611,7 +621,6 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
|
||||
|
||||
var track db.Track
|
||||
err = c.DB.
|
||||
Preload("Artist").
|
||||
Preload("Album").
|
||||
Where("id=?", id.Value).
|
||||
First(&track).
|
||||
@@ -620,7 +629,7 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
|
||||
return spec.NewError(10, "couldn't find a track with that id")
|
||||
}
|
||||
|
||||
similarTracks, err := c.LastFMClient.TrackGetSimilarTracks(apiKey, track.Artist.Name, track.TagTitle)
|
||||
similarTracks, err := c.LastFMClient.TrackGetSimilarTracks(apiKey, track.TagTrackArtist, track.TagTitle)
|
||||
if err != nil {
|
||||
return spec.NewError(0, "fetching track similar tracks: %v", err)
|
||||
}
|
||||
@@ -635,11 +644,10 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
|
||||
|
||||
var tracks []*db.Track
|
||||
err = c.DB.
|
||||
Preload("Artist").
|
||||
Select("tracks.*").
|
||||
Preload("Album").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Select("tracks.*").
|
||||
Where("tracks.tag_title IN (?)", similarTrackNames).
|
||||
Order(gorm.Expr("random()")).
|
||||
Limit(count).
|
||||
@@ -708,9 +716,11 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
|
||||
Preload("Album").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Joins("JOIN artists on tracks.artist_id=artists.id").
|
||||
Joins("JOIN album_artists ON album_artists.album_id=tracks.album_id").
|
||||
Joins("JOIN artists ON artists.id=album_artists.artist_id").
|
||||
Where("artists.name IN (?)", artistNames).
|
||||
Order(gorm.Expr("random()")).
|
||||
Group("tracks.id").
|
||||
Limit(count).
|
||||
Find(&tracks).
|
||||
Error
|
||||
|
||||
@@ -62,7 +62,7 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
||||
}
|
||||
|
||||
track := &db.Track{}
|
||||
if err := c.DB.Preload("Album").Preload("Artist").First(track, id.Value).Error; err != nil {
|
||||
if err := c.DB.Preload("Album").Preload("Album.Artists").First(track, id.Value).Error; err != nil {
|
||||
return spec.NewError(0, "error finding track: %v", err)
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
|
||||
err = c.DB.
|
||||
Where("id=?", id.Value).
|
||||
Preload("Album").
|
||||
Preload("Album.TagArtist").
|
||||
Preload("Album.Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
First(&track).
|
||||
@@ -256,7 +256,7 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
||||
q := c.DB.DB.
|
||||
Limit(params.GetOrInt("size", 10)).
|
||||
Preload("Album").
|
||||
Preload("Album.TagArtist").
|
||||
Preload("Album.Artists").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||
|
||||
@@ -217,7 +217,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.DB.Where("id=?", id.Value).Preload("Album").Preload("Album.TagArtist").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).Find(&track).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := c.DB.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) {
|
||||
return nil, fmt.Errorf("load track by id: %w", err)
|
||||
}
|
||||
trch = spec.NewTCTrackByFolder(&track, track.Album)
|
||||
|
||||
@@ -150,8 +150,9 @@ func coverGetPathArtist(dbc *db.DB, id int) (string, error) {
|
||||
folder := &db.Album{}
|
||||
err := dbc.DB.
|
||||
Select("parent.id, parent.root_dir, parent.left_path, parent.right_path, parent.cover").
|
||||
Joins("JOIN album_artists ON album_artists.album_id").
|
||||
Where("album_artists.artist_id=?", id).
|
||||
Joins("JOIN albums parent ON parent.id=albums.parent_id").
|
||||
Where("albums.tag_artist_id=?", id).
|
||||
Find(folder).
|
||||
Error
|
||||
if err != nil {
|
||||
|
||||
@@ -2,12 +2,13 @@ package spec
|
||||
|
||||
import (
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
)
|
||||
|
||||
func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
|
||||
func NewAlbumByTags(a *db.Album, artists []*db.Artist) *Album {
|
||||
ret := &Album{
|
||||
Created: a.CreatedAt,
|
||||
ID: a.SID(),
|
||||
@@ -27,9 +28,16 @@ func NewAlbumByTags(a *db.Album, artist *db.Artist) *Album {
|
||||
if a.AlbumRating != nil {
|
||||
ret.UserRating = a.AlbumRating.Rating
|
||||
}
|
||||
if artist != nil {
|
||||
ret.Artist = artist.Name
|
||||
ret.ArtistID = artist.SID()
|
||||
sort.Slice(artists, func(i, j int) bool {
|
||||
return artists[i].ID < artists[j].ID
|
||||
})
|
||||
if len(artists) > 0 {
|
||||
ret.Artist = artists[0].Name
|
||||
ret.ArtistID = artists[0].SID()
|
||||
}
|
||||
for _, a := range artists {
|
||||
ret.Artists = append(ret.Artists, a.Name)
|
||||
ret.ArtistIDs = append(ret.ArtistIDs, a.SID())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -69,8 +77,11 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
|
||||
if t.TrackRating != nil {
|
||||
ret.UserRating = t.TrackRating.Rating
|
||||
}
|
||||
if album.TagArtist != nil {
|
||||
ret.ArtistID = album.TagArtist.SID()
|
||||
if len(album.Artists) > 0 {
|
||||
sort.Slice(album.Artists, func(i, j int) bool {
|
||||
return album.Artists[i].ID < album.Artists[j].ID
|
||||
})
|
||||
ret.ArtistID = album.Artists[0].SID()
|
||||
}
|
||||
// replace tags that we're present
|
||||
if ret.Title == "" {
|
||||
|
||||
@@ -113,11 +113,13 @@ type Albums struct {
|
||||
|
||||
type Album struct {
|
||||
// common
|
||||
ID *specid.ID `xml:"id,attr,omitempty" json:"id"`
|
||||
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
ArtistID *specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
Created time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
|
||||
ID *specid.ID `xml:"id,attr,omitempty" json:"id"`
|
||||
CoverID *specid.ID `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
ArtistID *specid.ID `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
ArtistIDs []*specid.ID `xml:"artistIds,attr,omitempty" json:"artistIds,omitempty"`
|
||||
Artists []string `xml:"artists,attr,omitempty" json:"artists,omitempty"`
|
||||
Created time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
|
||||
// browsing by folder (eg. getAlbumList)
|
||||
Title string `xml:"title,attr,omitempty" json:"title"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album"`
|
||||
|
||||
@@ -6,32 +6,6 @@
|
||||
"serverVersion": "",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-2",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
@@ -45,6 +19,58 @@
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-2",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-2",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-2",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-5",
|
||||
"coverArt": "al-5",
|
||||
@@ -71,32 +97,6 @@
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-0",
|
||||
"album": "",
|
||||
"parent": "al-2",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-8",
|
||||
"coverArt": "al-8",
|
||||
@@ -111,13 +111,13 @@
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
"artist": "artist-0",
|
||||
"id": "al-12",
|
||||
"coverArt": "al-12",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "album-1",
|
||||
"album": "",
|
||||
"parent": "al-2",
|
||||
"parent": "al-10",
|
||||
"isDir": true,
|
||||
"name": "",
|
||||
"songCount": 3,
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -24,6 +26,8 @@
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -37,6 +41,8 @@
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -50,6 +56,8 @@
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -63,6 +71,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -76,6 +86,8 @@
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -89,6 +101,8 @@
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -102,6 +116,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -115,6 +131,8 @@
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -24,6 +26,8 @@
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -37,6 +41,8 @@
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -50,6 +56,8 @@
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -63,6 +71,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -76,6 +86,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -89,6 +101,8 @@
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -102,6 +116,8 @@
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -115,6 +131,8 @@
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -24,6 +26,8 @@
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -37,6 +41,8 @@
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -50,6 +56,8 @@
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -63,6 +71,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -76,6 +86,8 @@
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -89,6 +101,8 @@
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -102,6 +116,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -115,6 +131,8 @@
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -6,24 +6,13 @@
|
||||
"serverVersion": "",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-2",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-4",
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -33,49 +22,12 @@
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"id": "al-13",
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-5",
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -84,11 +36,43 @@
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-3",
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-11",
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-9",
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -102,6 +86,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -115,6 +101,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -122,6 +110,36 @@
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-5",
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-2",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
},
|
||||
{
|
||||
"id": "al-7",
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
"name": "album-0",
|
||||
"songCount": 3,
|
||||
"duration": 300,
|
||||
"year": 2021
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -27,6 +29,8 @@
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -40,6 +44,8 @@
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -27,6 +29,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -40,6 +44,8 @@
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -27,6 +29,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -40,6 +44,8 @@
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"coverArt": "al-3",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -25,6 +27,8 @@
|
||||
"coverArt": "al-4",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -39,6 +43,8 @@
|
||||
"coverArt": "al-5",
|
||||
"artistId": "ar-1",
|
||||
"artist": "artist-0",
|
||||
"artistIds": ["ar-1"],
|
||||
"artists": ["artist-0"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -53,6 +59,8 @@
|
||||
"coverArt": "al-7",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -67,6 +75,8 @@
|
||||
"coverArt": "al-8",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -81,6 +91,8 @@
|
||||
"coverArt": "al-9",
|
||||
"artistId": "ar-2",
|
||||
"artist": "artist-1",
|
||||
"artistIds": ["ar-2"],
|
||||
"artists": ["artist-1"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -95,6 +107,8 @@
|
||||
"coverArt": "al-11",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -109,6 +123,8 @@
|
||||
"coverArt": "al-12",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
@@ -123,6 +139,8 @@
|
||||
"coverArt": "al-13",
|
||||
"artistId": "ar-3",
|
||||
"artist": "artist-2",
|
||||
"artistIds": ["ar-3"],
|
||||
"artists": ["artist-2"],
|
||||
"created": "2019-11-30T00:00:00Z",
|
||||
"title": "",
|
||||
"album": "",
|
||||
|
||||
Reference in New Issue
Block a user