From c374577328c17b6c3c7b6edbf901585e1f6644ee Mon Sep 17 00:00:00 2001 From: sentriz Date: Wed, 13 Sep 2023 20:35:38 +0100 Subject: [PATCH] feat(subsonic): cache and use lastfm responses for covers, bios, top songs --- artistinfocache/artistinfocache.go | 122 ++++++++++++++++++++++++ cmd/gonic/gonic.go | 24 +++-- db/migrations.go | 8 ++ db/model.go | 23 ++++- server/ctrlsubsonic/ctrl.go | 20 ++-- server/ctrlsubsonic/handlers_by_tags.go | 39 ++++---- server/ctrlsubsonic/handlers_raw.go | 89 ++++++++--------- 7 files changed, 236 insertions(+), 89 deletions(-) create mode 100644 artistinfocache/artistinfocache.go diff --git a/artistinfocache/artistinfocache.go b/artistinfocache/artistinfocache.go new file mode 100644 index 0000000..a2630ea --- /dev/null +++ b/artistinfocache/artistinfocache.go @@ -0,0 +1,122 @@ +//nolint:revive +package artistinfocache + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/jinzhu/gorm" + "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/scrobble/lastfm" +) + +const keepFor = 30 * time.Hour * 24 + +type ArtistInfoCache struct { + db *db.DB + lastfmClient *lastfm.Client +} + +func New(db *db.DB, lastfmClient *lastfm.Client) *ArtistInfoCache { + return &ArtistInfoCache{db: db, lastfmClient: lastfmClient} +} + +func (a *ArtistInfoCache) GetOrLookup(ctx context.Context, apiKey string, 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) + } + + if artistInfo.ID == 0 || artistInfo.Biography == "" /* prev not found maybe */ || time.Since(artistInfo.UpdatedAt) > keepFor { + return a.Lookup(ctx, apiKey, &artist) + } + + return &artistInfo, nil +} + +func (a *ArtistInfoCache) Get(ctx context.Context, artistID int) (*db.ArtistInfo, error) { + var artistInfo db.ArtistInfo + if err := a.db.Find(&artistInfo, "id=?", artistID).Error; err != nil { + return nil, fmt.Errorf("find artist info in db: %w", err) + } + return &artistInfo, nil +} + +func (a *ArtistInfoCache) Lookup(ctx context.Context, apiKey string, artist *db.Artist) (*db.ArtistInfo, error) { + var artistInfo db.ArtistInfo + artistInfo.ID = artist.ID + + if err := a.db.FirstOrCreate(&artistInfo, "id=?", artistInfo.ID).Error; err != nil { + return nil, fmt.Errorf("first or create artist info: %w", err) + } + + info, err := a.lastfmClient.ArtistGetInfo(apiKey, artist.Name) + 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 + + var similar []string + for _, sim := range info.Similar.Artists { + similar = append(similar, sim.Name) + } + artistInfo.SetSimilarArtists(similar) + + url, _ := a.lastfmClient.StealArtistImage(info.URL) + artistInfo.ImageURL = url + + topTracksResponse, err := a.lastfmClient.ArtistGetTopTracks(apiKey, artist.Name) + if err != nil { + return nil, fmt.Errorf("get top tracks: %w", err) + } + var topTracks []string + for _, tr := range topTracksResponse.Tracks { + topTracks = append(topTracks, tr.Name) + } + artistInfo.SetTopTracks(topTracks) + + if err := a.db.Save(&artistInfo).Error; err != nil { + return nil, fmt.Errorf("save upstream info: %w", err) + } + + return &artistInfo, nil +} + +func (a *ArtistInfoCache) Refresh(apiKey string, interval time.Duration) error { + ticker := time.NewTicker(interval) + for range ticker.C { + q := a.db. + Where("artist_infos.id IS NULL OR artist_infos.updated_at src.Bounds().Dx() { // don't upscale images width = src.Bounds().Dx() } - err = imaging.Save(imaging.Resize(src, width, 0, imaging.Lanczos), cachePath) - if err != nil { + if err = imaging.Save(imaging.Resize(src, width, 0, imaging.Lanczos), cachePath); err != nil { return fmt.Errorf("caching `%s`: %w", cachePath, err) } return nil @@ -235,11 +224,13 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s _, err = os.Stat(cachePath) switch { case os.IsNotExist(err): - coverPath, err := coverGetPath(c.DB, c.PodcastsPath, id) + reader, err := coverFor(c.DB, c.ArtistInfoCache, c.PodcastsPath, id) if err != nil { return spec.NewError(10, "couldn't find cover `%s`: %v", id, err) } - if err := coverScaleAndSave(coverPath, cachePath, size); err != nil { + defer reader.Close() + + if err := coverScaleAndSave(reader, cachePath, size); err != nil { log.Printf("error scaling cover: %v", err) return nil }