feat(artists): profile v2 — per-catalog songs with plays, ISRC, artwork and $ estimates

Merged
SW
2mo ago
+649-2

Row 3 of recoupable/chat#1968, implementing the contract in docs#304. Approved design: Artist Profile V2 canvas.

What this adds

GET /api/artists/{id}/profile grows additively: each catalog carries songs[] (isrc, name, album, artwork_url, plays, est_value_usd), sorted by plays descending and capped at the top 50, plus a top-level nullable valuation {low, mid, high}.

FileRole
lib/supabase/song_measurements/selectLatestSongPlays.tsLatest Spotify platform_displayed_play_count per ISRC (newest-first dedupe, chunked, degrade-on-error)
lib/supabase/catalog_songs/selectCatalogSongIsrcs.ts(catalog, song) grouping rows, chunked
lib/supabase/songs/updateSongArtworkUrl.tsThe write-through target on songs.artwork_url (database#58)
lib/artist/resolveSongArtwork.tsFetch-on-miss: one batched Apple ISRC lookup for songs without stored artwork, write-through, every failure degrades to null artwork
lib/artist/buildProfileSongs.tsPure composition: grouping, sort, cap — and every dollar delegates to computeValuationBand, zero copied constants. Per-song rows carry the mid for that song's plays; the artist band runs the model over all plays with the earliest release date across catalogs; null when nothing is measured
lib/artist/getArtistPublicProfile.tsOrchestration; allowlist construction and v1 fields unchanged

Type note: songs.artwork_url ships in database#58, so until the generated Supabase types regenerate, the column rides as an optional extra on the row type (commented at the site).

Verification

TDD, red before green, twice over: the three new suites (selectLatestSongPlays, resolveSongArtwork, buildProfileSongs — 11 tests) were RED (Cannot find module ×3) before implementation; the profile-composition extension (4 new cases incl. asks-Apple-only-for-missing-artwork and valuation-null-when-unmeasured) was RED (4 failures) before wiring. buildProfileSongs's money assertions compare against computeValuationBand called with the same inputs in the test — the implementation cannot drift from the model without failing.

CheckResult
Full affected domains (lib/artist lib/supabase lib/catalog)102 files / 528 tests passed
eslint on all new/changed filesclean
tsc --noEmit202 errors, identical to the pre-existing baseline, zero in touched files

Not yet done: live preview verification per the issue's Done-when (field-for-field vs docs#304 on a real artist, plays cross-checked against song_measurements, hand-calculated valuation reproduction, simulated Apple outage → nulls with a 200). The preview also needs database#58 applied before artwork_url persists. Flagging rather than implying it.

Merge order

docs#304 → database#58 → this PR → the chat PRs (rows 4–5).


Summary by cubic

Adds per-catalog songs and a nullable valuation to GET /api/artists/{id}/profile. Previously catalogs had no songs or valuation; now each catalog returns up to 50 songs sorted by plays, and Apple artwork URLs resolve to concrete sizes so images load.

  • Per-song fields: isrc, name, album, artwork_url, plays, est_value_usd. Per-song value uses computeValuationBand mid for that song’s plays; the artist valuation runs the same model across total plays using the earliest release date across catalogs; null when unmeasured.
  • Data flow: latest Spotify plays via lib/songs/selectLatestSongPlays (chunked; newest-first dedupe), catalog grouping via lib/songs/getCatalogSongs over lib/supabase/catalog_songs/selectCatalogSongs, artwork via lib/artist/resolveSongArtwork (Apple batch ISRC lookup with lib/apple/resolveAppleArtworkUrl to 296x296) and write-through to songs.artwork_url via lib/supabase/songs/updateSong. Orchestration in lib/artist/getArtistPublicProfile; shaping in lib/artist/buildProfileSongs.
  • Behavior: songs sort by plays desc and cap at 50 per catalog; no pagination. Apple or measurement failures degrade to missing artwork or empty objects and still return 200. v1 fields are unchanged.
  • Review renames/moves: chunking moved to lib/songs/getCatalogSongs; selectCatalogSongs is now a plain .in() query; generic typed updateSong (TablesUpdate<"songs">); selectLatestSongPlays lives in lib/songs/; profile reads songs.artwork_url directly; new lib/apple/resolveAppleArtworkUrl resolves {w}x{h} templates.

Rollout

  • Ensure the database has songs.artwork_url (types updated here). Merge order: docs → database schema → this PR → chat rows 4–5.

Summary by CodeRabbit

  • New Features
    • Artist public profiles now display detailed songs for each catalog, including artwork and play counts.
    • Profiles may include an estimated valuation range based on catalog activity and release history.
    • Missing song artwork can now be automatically retrieved and displayed.
    • Apple Music artwork is consistently formatted as square images with configurable sizing.
    • Large catalogs and song collections are processed more reliably.