feat(artists): profile v2 — per-catalog songs with plays, ISRC, artwork and $ estimates
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}.
| File | Role |
|---|---|
lib/supabase/song_measurements/selectLatestSongPlays.ts | Latest 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.ts | The write-through target on songs.artwork_url (database#58) |
lib/artist/resolveSongArtwork.ts | Fetch-on-miss: one batched Apple ISRC lookup for songs without stored artwork, write-through, every failure degrades to null artwork |
lib/artist/buildProfileSongs.ts | Pure 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.ts | Orchestration; 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.
| Check | Result |
|---|---|
Full affected domains (lib/artist lib/supabase lib/catalog) | 102 files / 528 tests passed |
eslint on all new/changed files | clean |
tsc --noEmit | 202 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
computeValuationBandmid 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 vialib/songs/getCatalogSongsoverlib/supabase/catalog_songs/selectCatalogSongs, artwork vialib/artist/resolveSongArtwork(Apple batch ISRC lookup withlib/apple/resolveAppleArtworkUrlto 296x296) and write-through tosongs.artwork_urlvialib/supabase/songs/updateSong. Orchestration inlib/artist/getArtistPublicProfile; shaping inlib/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;selectCatalogSongsis now a plain.in()query; generic typedupdateSong(TablesUpdate<"songs">);selectLatestSongPlayslives inlib/songs/; profile readssongs.artwork_urldirectly; newlib/apple/resolveAppleArtworkUrlresolves{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.