Skip to content

Commit

Permalink
Added support for getArtistInfo. Uses a munged version of the filenam…
Browse files Browse the repository at this point in the history
…e to help LastFM make a match.
  • Loading branch information
brian-doherty committed Oct 21, 2023
1 parent 95bc919 commit d966b18
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 36 deletions.
66 changes: 40 additions & 26 deletions artistinfocache/artistinfocache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"time"

"github.com/iancoleman/strcase"
"github.com/jinzhu/gorm"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/lastfm"
Expand All @@ -24,46 +25,58 @@ func New(db *db.DB, lastfmClient *lastfm.Client) *ArtistInfoCache {
return &ArtistInfoCache{db: db, lastfmClient: lastfmClient}
}

func (a *ArtistInfoCache) GetOrLookup(ctx context.Context, artistID int) (*db.ArtistInfo, error) {
func (a *ArtistInfoCache) getOrLookup(ctx context.Context, artistName string) (*db.ArtistInfo, error) {
var artistInfo db.ArtistInfo
if err := a.db.Find(&artistInfo, "name=?", strcase.ToDelimited(artistName, ' ')).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("find artist info in db: %w", err)
}

if artistInfo.Biography == "" /* prev not found maybe */ || time.Since(artistInfo.UpdatedAt) > keepFor {
return a.Lookup(ctx, artistName)
}

return &artistInfo, nil
}

func (a *ArtistInfoCache) GetOrLookupByArtist(ctx context.Context, artistID int) (*db.ArtistInfo, error) {
var artist db.Artist
if err := a.db.Find(&artist, "id=?", artistID).Error; err != nil {
return nil, fmt.Errorf("find artist in db: %w", err)
}

var artistInfo db.ArtistInfo
if err := a.db.Find(&artistInfo, "id=?", artistID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("find artist info in db: %w", err)
}
return a.getOrLookup(ctx, artist.Name)
}

if artistInfo.ID == 0 || artistInfo.Biography == "" /* prev not found maybe */ || time.Since(artistInfo.UpdatedAt) > keepFor {
return a.Lookup(ctx, &artist)
func (a *ArtistInfoCache) GetOrLookupByAlbum(ctx context.Context, albumID int) (*db.ArtistInfo, error) {
var album db.Album
if err := a.db.Find(&album, "id=?", albumID).Error; err != nil {
return nil, fmt.Errorf("find artist in db: %w", err)
}

return &artistInfo, nil
return a.getOrLookup(ctx, album.RightPath)
}

func (a *ArtistInfoCache) Get(ctx context.Context, artistID int) (*db.ArtistInfo, error) {
func (a *ArtistInfoCache) Get(ctx context.Context, artistName string) (*db.ArtistInfo, error) {
var artistInfo db.ArtistInfo
if err := a.db.Find(&artistInfo, "id=?", artistID).Error; err != nil {
if err := a.db.Find(&artistInfo, "name=?", strcase.ToDelimited(artistName, ' ')).Error; err != nil {
return nil, fmt.Errorf("find artist info in db: %w", err)
}
return &artistInfo, nil
}

func (a *ArtistInfoCache) Lookup(ctx context.Context, artist *db.Artist) (*db.ArtistInfo, error) {
func (a *ArtistInfoCache) Lookup(ctx context.Context, artistName string) (*db.ArtistInfo, error) {
var artistInfo db.ArtistInfo
artistInfo.ID = artist.ID
artistInfo.Name = strcase.ToDelimited(artistName, ' ')

if err := a.db.FirstOrCreate(&artistInfo, "id=?", artistInfo.ID).Error; err != nil {
if err := a.db.FirstOrCreate(&artistInfo, "name=?", artistInfo.Name).Error; err != nil {
return nil, fmt.Errorf("first or create artist info: %w", err)
}

info, err := a.lastfmClient.ArtistGetInfo(artist.Name)
info, err := a.lastfmClient.ArtistGetInfo(artistName)
if err != nil {
return nil, fmt.Errorf("get upstream info: %w", err)
}

artistInfo.ID = artist.ID
artistInfo.Biography = info.Bio.Summary
artistInfo.MusicBrainzID = info.MBID
artistInfo.LastFMURL = info.URL
Expand All @@ -77,7 +90,7 @@ func (a *ArtistInfoCache) Lookup(ctx context.Context, artist *db.Artist) (*db.Ar
url, _ := a.lastfmClient.StealArtistImage(info.URL)
artistInfo.ImageURL = url

topTracksResponse, err := a.lastfmClient.ArtistGetTopTracks(artist.Name)
topTracksResponse, err := a.lastfmClient.ArtistGetTopTracks(artistName)
if err != nil {
return nil, fmt.Errorf("get top tracks: %w", err)
}
Expand All @@ -96,22 +109,23 @@ func (a *ArtistInfoCache) Lookup(ctx context.Context, artist *db.Artist) (*db.Ar

func (a *ArtistInfoCache) Refresh() error {
q := a.db.
Where("artist_infos.id IS NULL OR artist_infos.updated_at<?", time.Now().Add(-keepFor)).
Joins("LEFT JOIN artist_infos ON artist_infos.id=artists.id")
Where("artist_infos.updated_at<?", time.Now().Add(-keepFor))

var artist db.Artist
if err := q.Find(&artist).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("finding non cached artist: %w", err)
}
if artist.ID == 0 {
var artistInfo db.ArtistInfo
err := q.Find(&artistInfo).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// No outdated records
return nil
}
if err != nil {
return fmt.Errorf("finding non cached artist: %w", err)
}

if _, err := a.Lookup(context.Background(), &artist); err != nil {
return fmt.Errorf("looking up non cached artist %s: %w", artist.Name, err)
if _, err := a.Lookup(context.Background(), artistInfo.Name); err != nil {
return fmt.Errorf("looking up non cached artist %s: %w", artistInfo.Name, err)
}

log.Printf("cached artist info for %q", artist.Name)
log.Printf("cached artist info for %q", artistInfo.Name)

return nil
}
4 changes: 2 additions & 2 deletions artistinfocache/artistinfocache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func TestInfoCache(t *testing.T) {
)

cache := New(m.DB(), lastfmClient)
_, err := cache.GetOrLookup(context.Background(), artist.ID)
_, err := cache.GetOrLookupByArtist(context.Background(), artist.ID)
require.NoError(t, err)
_, err = cache.GetOrLookup(context.Background(), artist.ID)
_, err = cache.GetOrLookupByArtist(context.Background(), artist.ID)
require.NoError(t, err)

require.Equal(t, int32(1), count.Load())
Expand Down
Binary file added cmd/gonic/gonic
Binary file not shown.
2 changes: 1 addition & 1 deletion db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ func (ir *InternetRadioStation) SID() *specid.ID {
}

type ArtistInfo struct {
ID int `gorm:"primary_key" sql:"type:int REFERENCES artists(id) ON DELETE CASCADE"`
Name string `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time `gorm:"index"`
Biography string
Expand Down
37 changes: 37 additions & 0 deletions db/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202309070009", migrateDeleteArtistCoverField),
construct(ctx, "202309131743", migrateArtistInfo),
construct(ctx, "202309161411", migratePlaylistsPaths),
construct(ctx, "202310211302", migrateArtistInfoIDtoName),
}

return gormigrate.
Expand Down Expand Up @@ -729,3 +730,39 @@ func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error {
}
return Dump(context.Background(), tx, fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix()))
}

func migrateArtistInfoIDtoName(tx *gorm.DB, ctx MigrationContext) error {

Check warning on line 734 in db/migrations.go

View workflow job for this annotation

GitHub Actions / Lint and test

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
// New primary key means new table. Dropping the old one completely since it's a cache.
step := tx.Exec(`
CREATE TABLE IF NOT EXISTS "new_artist_infos" ("name" varchar(255), "created_at" datetime,
"updated_at" datetime, "biography" varchar(255), "music_brainz_id" varchar(255),
"last_fm_url" varchar(255),"image_url" varchar(255),"similar_artists" varchar(255),
"top_tracks" varchar(255) , PRIMARY KEY ("name"));
`)
if err := step.Error; err != nil {
return fmt.Errorf("step make new artist info cache: %w", err)
}

step = tx.Exec(`
DROP TABLE artist_infos
`)
if err := step.Error; err != nil {
return fmt.Errorf("step drop old table: %w", err)
}

step = tx.Exec(`
ALTER TABLE new_artist_infos RENAME TO artist_infos
`)
if err := step.Error; err != nil {
return fmt.Errorf("rename new table: %w", err)
}

step = tx.Exec(`
CREATE INDEX idx_artist_infos_updated_at ON "artist_infos"(updated_at) ;
`)
if err := step.Error; err != nil {
return fmt.Errorf("create new index: %w", err)
}

return nil
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/google/uuid v1.3.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/iancoleman/strcase v0.3.0
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
Expand Down Expand Up @@ -42,6 +43,7 @@ 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/iancoleman/strcase v0.3.0 // 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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
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=
Expand Down
96 changes: 94 additions & 2 deletions server/ctrlsubsonic/handlers_by_folder.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package ctrlsubsonic

import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/iancoleman/strcase"
"github.com/jinzhu/gorm"

"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/handlerutil"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
)

// the subsonic spec mentions "artist" a lot when talking about the
Expand Down Expand Up @@ -281,8 +288,93 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
return sub
}

func (c *Controller) ServeGetArtistInfo(_ *http.Request) *spec.Response {
return spec.NewResponse()
func (c *Controller) genAlbumCoverURL(r *http.Request, album *db.Album, size int) string {
coverURL, _ := url.Parse(handlerutil.BaseURL(r))
coverURL.Path = c.resolveProxyPath("/rest/getCoverArt")

query := r.URL.Query()
query.Set("id", album.SID().String())
query.Set("size", strconv.Itoa(size))
coverURL.RawQuery = query.Encode()

return coverURL.String()
}

func (c *Controller) ServeGetArtistInfo(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, "artist with album id %q not found", id)
}

sub := spec.NewResponse()
sub.ArtistInfo = &spec.ArtistInfo{}

info, err := c.artistInfoCache.GetOrLookupByAlbum(r.Context(), album.ID)
if err != nil {
log.Printf("error fetching artist info from lastfm: %v", err)
return sub
}

sub.ArtistInfo.Biography = info.Biography
sub.ArtistInfo.MusicBrainzID = info.MusicBrainzID
sub.ArtistInfo.LastFMURL = info.LastFMURL

sub.ArtistInfo.SmallImageURL = c.genAlbumCoverURL(r, &album, 64)
sub.ArtistInfo.MediumImageURL = c.genAlbumCoverURL(r, &album, 126)
sub.ArtistInfo.LargeImageURL = c.genAlbumCoverURL(r, &album, 256)

if info.ImageURL != "" {
sub.ArtistInfo.SmallImageURL = info.ImageURL
sub.ArtistInfo.MediumImageURL = info.ImageURL
sub.ArtistInfo.LargeImageURL = info.ImageURL
sub.ArtistInfo.ArtistImageURL = info.ImageURL
}

count := params.GetOrInt("count", 20)
inclNotPresent := params.GetOrBool("includeNotPresent", false)

for i, similarName := range info.GetSimilarArtists() {
if i == count {
break
}
var album db.Album
err = c.dbc.
Where("right_path LIKE ?", strcase.ToDelimited(similarName, '%')).
Find(&album).
Error
if errors.Is(err, gorm.ErrRecordNotFound) && !inclNotPresent {
continue
}

if album.ID == 0 {
// add a very limited artist, since we don't have everything with `inclNotPresent`
sub.ArtistInfo.Similar = append(sub.ArtistInfo.Similar, &spec.Artist{
ID: &specid.ID{},
Name: similarName,
})
continue
}

// To do: Embed this into the query above
_ = c.dbc.
Model(&db.Album{}).
Where("parent_id=?", album.ID).
Count(&album.ChildCount)

sub.ArtistInfo.Similar = append(sub.ArtistInfo.Similar, spec.NewArtistByFolder(&album))
}

return sub
}

func (c *Controller) ServeGetStarred(r *http.Request) *spec.Response {
Expand Down
4 changes: 2 additions & 2 deletions server/ctrlsubsonic/handlers_by_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
sub := spec.NewResponse()
sub.ArtistInfoTwo = &spec.ArtistInfo{}

info, err := c.artistInfoCache.GetOrLookup(r.Context(), artist.ID)
info, err := c.artistInfoCache.GetOrLookupByArtist(r.Context(), artist.ID)
if err != nil {
log.Printf("error fetching artist info from lastfm: %v", err)
return sub
Expand Down Expand Up @@ -539,7 +539,7 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
return spec.NewError(0, "finding artist by name: %v", err)
}

info, err := c.artistInfoCache.GetOrLookup(r.Context(), artist.ID)
info, err := c.artistInfoCache.GetOrLookupByArtist(r.Context(), artist.ID)
if err != nil {
log.Printf("error fetching artist info from lastfm: %v", err)
return spec.NewResponse()
Expand Down
15 changes: 12 additions & 3 deletions server/ctrlsubsonic/handlers_raw.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func coverFor(dbc *db.DB, artistInfoCache *artistinfocache.ArtistInfoCache, id s
case specid.Album:
return coverForAlbum(dbc, id.Value)
case specid.Artist:
return coverForArtist(artistInfoCache, id.Value)
return coverForArtist(dbc, artistInfoCache, id.Value)
case specid.Podcast:
return coverForPodcast(dbc, id.Value)
case specid.PodcastEpisode:
Expand All @@ -106,8 +106,17 @@ func coverForAlbum(dbc *db.DB, id int) (*os.File, error) {
return os.Open(filepath.Join(folder.RootDir, folder.LeftPath, folder.RightPath, folder.Cover))
}

func coverForArtist(artistInfoCache *artistinfocache.ArtistInfoCache, id int) (io.ReadCloser, error) {
info, err := artistInfoCache.Get(context.Background(), id)
func coverForArtist(dbc *db.DB, artistInfoCache *artistinfocache.ArtistInfoCache, id int) (io.ReadCloser, error) {
var artist db.Artist
err := dbc.
Where("id=?", id).
First(&artist).
Error
if err != nil {
return nil, fmt.Errorf("artist not found: %w", err)
}

info, err := artistInfoCache.Get(context.Background(), artist.Name)
if err != nil {
return nil, fmt.Errorf("get artist info from cache: %w", err)
}
Expand Down

0 comments on commit d966b18

Please sign in to comment.