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 }