From cc1a99f03381a5afcebdbe95aaa42fb969f98b9f Mon Sep 17 00:00:00 2001 From: sentriz Date: Tue, 7 Nov 2023 23:43:11 +0000 Subject: [PATCH] feat(subsonic): add getAlbumInfo with cache Release-As: 0.16.1 --- cmd/gonic/gonic.go | 6 +- db/db.go | 9 +++ db/migrations.go | 8 ++ go.mod | 16 ++-- go.sum | 31 ++++--- infocache/albuminfocache/albuminfocache.go | 81 +++++++++++++++++++ .../artistinfocache}/artistinfocache.go | 3 + .../artistinfocache}/artistinfocache_test.go | 0 lastfm/client.go | 20 +++++ lastfm/model.go | 49 +++++++++++ server/ctrlsubsonic/ctrl.go | 10 ++- server/ctrlsubsonic/handlers_by_tags.go | 32 ++++++++ server/ctrlsubsonic/handlers_raw.go | 2 +- server/ctrlsubsonic/spec/spec.go | 49 ++++++----- 14 files changed, 268 insertions(+), 48 deletions(-) create mode 100644 infocache/albuminfocache/albuminfocache.go rename {artistinfocache => infocache/artistinfocache}/artistinfocache.go (97%) rename {artistinfocache => infocache/artistinfocache}/artistinfocache_test.go (100%) diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index b896f7d..8816789 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -30,9 +30,10 @@ import ( "golang.org/x/sync/errgroup" "go.senan.xyz/gonic" - "go.senan.xyz/gonic/artistinfocache" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/handlerutil" + "go.senan.xyz/gonic/infocache/albuminfocache" + "go.senan.xyz/gonic/infocache/artistinfocache" "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/lastfm" "go.senan.xyz/gonic/listenbrainz" @@ -240,6 +241,7 @@ func main() { sessDB.SessionOpts.SameSite = http.SameSiteLaxMode artistInfoCache := artistinfocache.New(dbc, lastfmClient) + albumInfoCache := albuminfocache.New(dbc, lastfmClient) scrobblers := []scrobble.Scrobbler{lastfmClient, listenbrainzClient} @@ -251,7 +253,7 @@ func main() { if err != nil { log.Panicf("error creating admin controller: %v\n", err) } - ctrlSubsonic, err := ctrlsubsonic.New(dbc, scannr, musicPaths, *confPodcastPath, cacheDirAudio, cacheDirCovers, jukebx, playlistStore, scrobblers, podcast, transcoder, lastfmClient, artistInfoCache, resolveProxyPath) + ctrlSubsonic, err := ctrlsubsonic.New(dbc, scannr, musicPaths, *confPodcastPath, cacheDirAudio, cacheDirCovers, jukebx, playlistStore, scrobblers, podcast, transcoder, lastfmClient, artistInfoCache, albumInfoCache, resolveProxyPath) if err != nil { log.Panicf("error creating subsonic controller: %v\n", err) } diff --git a/db/db.go b/db/db.go index 8debe2e..a0d57ac 100644 --- a/db/db.go +++ b/db/db.go @@ -574,6 +574,15 @@ func (p *ArtistInfo) SetSimilarArtists(items []string) { p.SimilarArtists = stri func (p *ArtistInfo) GetTopTracks() []string { return strings.Split(p.TopTracks, ";") } func (p *ArtistInfo) SetTopTracks(items []string) { p.TopTracks = strings.Join(items, ";") } +type AlbumInfo struct { + ID int `gorm:"primary_key" sql:"type:int REFERENCES albums(id) ON DELETE CASCADE"` + CreatedAt time.Time + UpdatedAt time.Time `gorm:"index"` + Notes string + MusicBrainzID string + LastFMURL string +} + func splitIDs(in, sep string) []specid.ID { if in == "" { return []specid.ID{} diff --git a/db/migrations.go b/db/migrations.go index 52b2002..0259998 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -69,6 +69,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202310252205", migrateAlbumTagArtistString), construct(ctx, "202310281803", migrateTrackArtists), construct(ctx, "202311062259", migrateArtistAppearances), + construct(ctx, "202311072309", migrateAlbumInfo), } return gormigrate. @@ -779,3 +780,10 @@ func migrateArtistAppearances(tx *gorm.DB, _ MigrationContext) error { return nil } + +func migrateAlbumInfo(tx *gorm.DB, _ MigrationContext) error { + return tx.AutoMigrate( + AlbumInfo{}, + ). + Error +} diff --git a/go.mod b/go.mod index 45e939b..63b600d 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,12 @@ require ( github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.7.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.3.1 - github.com/gorilla/securecookie v1.1.1 - github.com/gorilla/sessions v1.2.1 + github.com/google/uuid v1.4.0 + github.com/gorilla/securecookie v1.1.2 + github.com/gorilla/sessions v1.2.2 github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 github.com/josephburnett/jd v1.5.2 - github.com/mattn/go-sqlite3 v1.14.17 + github.com/mattn/go-sqlite3 v1.14.18 github.com/mitchellh/mapstructure v1.5.0 github.com/mmcdole/gofeed v1.2.1 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -28,7 +28,7 @@ require ( github.com/stretchr/testify v1.8.1 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.17.0 - golang.org/x/sync v0.4.0 + golang.org/x/sync v0.5.0 gopkg.in/gormigrate.v1 v1.6.0 jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 ) @@ -40,7 +40,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect - github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/context v1.1.2 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -61,8 +61,8 @@ require ( github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/image v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.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 diff --git a/go.sum b/go.sum index 7fc8120..2f42c22 100644 --- a/go.sum +++ b/go.sum @@ -46,16 +46,21 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= 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.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -97,8 +102,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ 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.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/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= @@ -180,8 +185,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -192,8 +197,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= @@ -204,8 +209,8 @@ 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/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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= diff --git a/infocache/albuminfocache/albuminfocache.go b/infocache/albuminfocache/albuminfocache.go new file mode 100644 index 0000000..f9c9677 --- /dev/null +++ b/infocache/albuminfocache/albuminfocache.go @@ -0,0 +1,81 @@ +//nolint:revive +package albuminfocache + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jinzhu/gorm" + "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/lastfm" +) + +const keepFor = 30 * time.Hour * 24 + +type AlbumInfoCache struct { + db *db.DB + lastfmClient *lastfm.Client +} + +func New(db *db.DB, lastfmClient *lastfm.Client) *AlbumInfoCache { + return &AlbumInfoCache{db: db, lastfmClient: lastfmClient} +} + +func (a *AlbumInfoCache) GetOrLookup(ctx context.Context, albumID int) (*db.AlbumInfo, error) { + var album db.Album + if err := a.db.Find(&album, "id=?", albumID).Error; err != nil { + return nil, fmt.Errorf("find album in db: %w", err) + } + if album.TagAlbumArtist == "" || album.TagTitle == "" { + return nil, fmt.Errorf("no metadata to look up") + } + + var albumInfo db.AlbumInfo + if err := a.db.Find(&albumInfo, "id=?", albumID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("find album info in db: %w", err) + } + + if albumInfo.ID == 0 || time.Since(albumInfo.UpdatedAt) > keepFor { + return a.Lookup(ctx, &album) + } + + return &albumInfo, nil +} + +func (a *AlbumInfoCache) Get(ctx context.Context, albumID int) (*db.AlbumInfo, error) { + var albumInfo db.AlbumInfo + if err := a.db.Find(&albumInfo, "id=?", albumID).Error; err != nil { + return nil, fmt.Errorf("find album info in db: %w", err) + } + return &albumInfo, nil +} + +func (a *AlbumInfoCache) Lookup(ctx context.Context, album *db.Album) (*db.AlbumInfo, error) { + var albumInfo db.AlbumInfo + albumInfo.ID = album.ID + + if err := a.db.FirstOrCreate(&albumInfo, "id=?", albumInfo.ID).Error; err != nil { + return nil, fmt.Errorf("first or create album info: %w", err) + } + if err := a.db.Save(&albumInfo).Error; err != nil { + return nil, fmt.Errorf("bump updated_at time: %w", err) + } + + info, err := a.lastfmClient.AlbumGetInfo(album.TagAlbumArtist, album.TagTitle) + if err != nil { + return nil, fmt.Errorf("get upstream info: %w", err) + } + + albumInfo.ID = album.ID + albumInfo.Notes = info.Wiki.Content + albumInfo.MusicBrainzID = info.MBID + albumInfo.LastFMURL = info.URL + + if err := a.db.Save(&albumInfo).Error; err != nil { + return nil, fmt.Errorf("save upstream info: %w", err) + } + + return &albumInfo, nil +} diff --git a/artistinfocache/artistinfocache.go b/infocache/artistinfocache/artistinfocache.go similarity index 97% rename from artistinfocache/artistinfocache.go rename to infocache/artistinfocache/artistinfocache.go index 3bb6b75..f30c1cb 100644 --- a/artistinfocache/artistinfocache.go +++ b/infocache/artistinfocache/artistinfocache.go @@ -29,6 +29,9 @@ func (a *ArtistInfoCache) GetOrLookup(ctx context.Context, artistID int) (*db.Ar if err := a.db.Find(&artist, "id=?", artistID).Error; err != nil { return nil, fmt.Errorf("find artist in db: %w", err) } + if artist.Name == "" { + return nil, fmt.Errorf("no metadata to look up") + } var artistInfo db.ArtistInfo if err := a.db.Find(&artistInfo, "id=?", artistID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/artistinfocache/artistinfocache_test.go b/infocache/artistinfocache/artistinfocache_test.go similarity index 100% rename from artistinfocache/artistinfocache_test.go rename to infocache/artistinfocache/artistinfocache_test.go diff --git a/lastfm/client.go b/lastfm/client.go index 8022e51..4e406e1 100644 --- a/lastfm/client.go +++ b/lastfm/client.go @@ -60,6 +60,26 @@ func (c *Client) ArtistGetInfo(artistName string) (Artist, error) { return resp.Artist, nil } +func (c *Client) AlbumGetInfo(artistName, albumName string) (Album, error) { + apiKey, _, err := c.keySecret() + if err != nil { + return Album{}, fmt.Errorf("get key and secret: %w", err) + } + + params := url.Values{} + params.Add("method", "album.getInfo") + params.Add("api_key", apiKey) + params.Add("artist", artistName) + params.Add("album", albumName) + + resp, err := c.makeRequest(http.MethodGet, params) + if err != nil { + return Album{}, fmt.Errorf("make request: %w", err) + } + + return resp.Album, nil +} + func (c *Client) ArtistGetTopTracks(artistName string) (TopTracks, error) { apiKey, _, err := c.keySecret() if err != nil { diff --git a/lastfm/model.go b/lastfm/model.go index d8eb1b7..ab49bcd 100644 --- a/lastfm/model.go +++ b/lastfm/model.go @@ -9,6 +9,7 @@ type ( Session Session `xml:"session"` Error Error `xml:"error"` Artist Artist `xml:"artist"` + Album Album `xml:"album"` TopTracks TopTracks `xml:"toptracks"` SimilarTracks SimilarTracks `xml:"similartracks"` SimilarArtists SimilarArtists `xml:"similarartists"` @@ -61,6 +62,54 @@ type ( Bio ArtistBio `xml:"bio"` } + Album struct { + XMLName xml.Name `xml:"album"` + Name string `xml:"name"` + Artist string `xml:"artist"` + MBID string `xml:"mbid"` + URL string `xml:"url"` + Image []struct { + Text string `xml:",chardata"` + Size string `xml:"size,attr"` + } `xml:"image"` + Listeners string `xml:"listeners"` + Playcount string `xml:"playcount"` + Tracks struct { + Text string `xml:",chardata"` + Track []struct { + Text string `xml:",chardata"` + Rank string `xml:"rank,attr"` + Name string `xml:"name"` + URL string `xml:"url"` + Duration string `xml:"duration"` + Streamable struct { + Text string `xml:",chardata"` + Fulltrack string `xml:"fulltrack,attr"` + } `xml:"streamable"` + Artist struct { + Text string `xml:",chardata"` + Name string `xml:"name"` + Mbid string `xml:"mbid"` + URL string `xml:"url"` + } `xml:"artist"` + } `xml:"track"` + } `xml:"tracks"` + Tags struct { + Text string `xml:",chardata"` + Tag []struct { + Text string `xml:",chardata"` + Name string `xml:"name"` + URL string `xml:"url"` + } `xml:"tag"` + } `xml:"tags"` + Wiki struct { + Text string `xml:",chardata"` + Published string `xml:"published"` + Summary string `xml:"summary"` + Content string `xml:"content"` + } `xml:"wiki"` + } + ArtistTag struct { Name string `xml:"name"` URL string `xml:"url"` diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index bf58184..fb8cefe 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -11,9 +11,10 @@ import ( "log" "net/http" - "go.senan.xyz/gonic/artistinfocache" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/handlerutil" + "go.senan.xyz/gonic/infocache/albuminfocache" + "go.senan.xyz/gonic/infocache/artistinfocache" "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/lastfm" "go.senan.xyz/gonic/playlist" @@ -63,10 +64,11 @@ type Controller struct { transcoder transcode.Transcoder lastFMClient *lastfm.Client artistInfoCache *artistinfocache.ArtistInfoCache + albumInfoCache *albuminfocache.AlbumInfoCache resolveProxyPath ProxyPathResolver } -func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPath string, cacheAudioPath string, cacheCoverPath string, jukebox *jukebox.Jukebox, playlistStore *playlist.Store, scrobblers []scrobble.Scrobbler, podcasts *podcast.Podcasts, transcoder transcode.Transcoder, lastFMClient *lastfm.Client, artistInfoCache *artistinfocache.ArtistInfoCache, resolveProxyPath ProxyPathResolver) (*Controller, error) { +func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPath string, cacheAudioPath string, cacheCoverPath string, jukebox *jukebox.Jukebox, playlistStore *playlist.Store, scrobblers []scrobble.Scrobbler, podcasts *podcast.Podcasts, transcoder transcode.Transcoder, lastFMClient *lastfm.Client, artistInfoCache *artistinfocache.ArtistInfoCache, albumInfoCache *albuminfocache.AlbumInfoCache, resolveProxyPath ProxyPathResolver) (*Controller, error) { c := Controller{ ServeMux: http.NewServeMux(), @@ -83,6 +85,7 @@ func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPa transcoder: transcoder, lastFMClient: lastFMClient, artistInfoCache: artistInfoCache, + albumInfoCache: albumInfoCache, resolveProxyPath: resolveProxyPath, } @@ -132,8 +135,9 @@ func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPa c.Handle("/getArtist", chain(resp(c.ServeGetArtist))) c.Handle("/getArtists", chain(resp(c.ServeGetArtists))) c.Handle("/search3", chain(resp(c.ServeSearchThree))) - c.Handle("/getArtistInfo2", chain(resp(c.ServeGetArtistInfoTwo))) c.Handle("/getStarred2", chain(resp(c.ServeGetStarredTwo))) + c.Handle("/getArtistInfo2", chain(resp(c.ServeGetArtistInfoTwo))) + c.Handle("/getAlbumInfo2", chain(resp(c.ServeGetAlbumInfoTwo))) // browse by folder c.Handle("/getIndexes", chain(resp(c.ServeGetIndexes))) diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 24c456d..f5b1950 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -380,6 +380,38 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { return sub } +func (c *Controller) ServeGetAlbumInfoTwo(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + id, err := params.GetID("id") + if err != nil { + return spec.NewError(10, "please provide an `id` parameter") + } + + var album db.Album + err = c.dbc. + Where("id=?", id.Value). + Find(&album). + Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return spec.NewError(70, "album with id %q not found", id) + } + + sub := spec.NewResponse() + sub.AlbumInfo = &spec.AlbumInfo{} + + info, err := c.albumInfoCache.GetOrLookup(r.Context(), album.ID) + if err != nil { + log.Printf("error fetching album info from lastfm: %v", err) + return sub + } + + sub.AlbumInfo.Notes = info.Notes + sub.AlbumInfo.MusicBrainzID = info.MusicBrainzID + sub.AlbumInfo.LastFMURL = info.LastFMURL + + return sub +} + func (c *Controller) ServeGetGenres(_ *http.Request) *spec.Response { var genres []*db.Genre c.dbc. diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 0f8de8d..4389e19 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -15,8 +15,8 @@ import ( "github.com/disintegration/imaging" "github.com/jinzhu/gorm" - "go.senan.xyz/gonic/artistinfocache" "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/infocache/artistinfocache" "go.senan.xyz/gonic/server/ctrlsubsonic/params" "go.senan.xyz/gonic/server/ctrlsubsonic/spec" "go.senan.xyz/gonic/server/ctrlsubsonic/specid" diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index a47c007..adec86d 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -45,27 +45,28 @@ type Response struct { MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"` ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"` Licence *Licence `xml:"license" json:"license,omitempty"` - SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"` - SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"` - User *User `xml:"user" json:"user,omitempty"` - Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"` - Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"` - ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"` - ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"` - Genres *Genres `xml:"genres" json:"genres,omitempty"` - PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"` - JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"` - JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"` - Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"` - NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"` - Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"` - Starred *Starred `xml:"starred" json:"starred,omitempty"` - StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"` - TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"` - SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"` - SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"` - InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"` - Lyrics *Lyrics `xml:"lyrics" json:"lyrics,omitempty"` + SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"` + SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"` + User *User `xml:"user" json:"user,omitempty"` + Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"` + Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"` + ArtistInfo *ArtistInfo `xml:"artistInfo" json:"artistInfo,omitempty"` + ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"` + AlbumInfo *AlbumInfo `xml:"albumInfo" json:"albumInfo,omitempty"` + Genres *Genres `xml:"genres" json:"genres,omitempty"` + PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"` + JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"` + JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"` + Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"` + NewestPodcasts *NewestPodcasts `xml:"newestPodcasts" json:"newestPodcasts,omitempty"` + Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"` + Starred *Starred `xml:"starred" json:"starred,omitempty"` + StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"` + TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"` + SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"` + SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"` + InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"` + Lyrics *Lyrics `xml:"lyrics" json:"lyrics,omitempty"` } func NewResponse() *Response { @@ -305,6 +306,12 @@ type ArtistInfo struct { Similar []*Artist `xml:"similarArtist,omitempty" json:"similarArtist,omitempty"` } +type AlbumInfo struct { + Notes string `xml:"notes" json:"notes"` + MusicBrainzID string `xml:"musicBrainzId" json:"musicBrainzId"` + LastFMURL string `xml:"lastFmUrl" json:"lastFmUrl"` +} + type Genres struct { List []*Genre `xml:"genre" json:"genre"` }