Skip to content

Artist photos: fetch from Apple Music, cache, serve locally

jedmund requested to merge jedmund/artist-photos into main

Adds an artist portrait to the /artist/[key] header. Photos come from Apple Music's artist search, get downloaded and stored under ${DATA_PATH}/artist-photos/, and are served from a local /api/artist-art/[key] endpoint that mirrors the existing /api/artwork/[id] pattern.

What changes

  • artists table (new) — keyed by artistKey, with name, photo_path (relative to DATA_PATH), photo_source, photo_fetched_at, photo_attempted_at. The attempt timestamp lets failed lookups back off for 7 days instead of retrying every scan.
  • searchAppleMusicArtist(name) added to the existing Apple Music client. Hits ?types=artists across the JP and US storefronts in parallel and picks the best name match. Returns the templated artwork URL ({w}x{h} intact) or null.
  • fetchAndCacheArtistPhoto(key, name) — orchestrator: check cache, check back-off, search Apple Music, download to ${DATA_PATH}/artist-photos/${slug}-${shortHash}.${ext} via .tmp + atomic rename, single upsert into artists. Slug for grep-ability, short-hash on the key to disambiguate slug collisions (matters more for romanized non-Western names).
  • Sorted scan hook — after scanSortedDirectory finishes its upsert + prune, walk distinct sorted artists, fetch missing photos via PQueue({concurrency: 5}). Steady-state scans pay one DB lookup per artist because cache + back-off short-circuit inside the service. Skips the Various Artists and Unknown Artist sentinels.
  • /api/artist-art/[key] — reads artists.photo_path, serves the file with MIME sniffed from extension, 1h cache.
  • /artist/[key] UI<img> in the header pulling from the new endpoint, falling back to the existing person-icon placeholder on 404.

Verification

  • pnpm run build passes.
  • Set Apple Music creds in env and restart. The startup sorted scan runs Fetching artist photos for N sorted artists in the logs, followed by a summary tally.
  • Inspect ${DATA_PATH}/artist-photos/. Files have slug-hash.jpg names.
  • Open /artist/<known-artist> — portrait renders in the header.
  • Open /artist/<obscure-or-no-match> — placeholder icon renders; artists row exists with photo_path NULL and photo_attempted_at set.
  • Trigger another scan; in the log, that obscure artist now shows backed-off and doesn't re-hit Apple Music.
  • Without Apple Music creds the service no-ops: photo_attempted_at advances but photo_path stays null, page falls back to placeholder.

Notes / future work

  • Photo fetch is tied to the sorted scan only. Unsorted-only artists (someone whose albums haven't been sorted yet) won't get a photo until they appear on the sorted side. If that matters, add a parallel hook in scanUnsortedDirectory.
  • No Discogs fallback yet — Apple Music coverage is fine for the common case; a Discogs source can layer in without schema changes by setting photo_source = 'discogs'.
  • No 'Re-fetch this artist' button on the UI. Easy to add by hitting a POST /api/artist-art/[key] action that calls fetchAndCacheArtistPhoto(key, name, {forceRefresh: true}).

Merge request reports

Loading