Files
Lyra/docs/superpowers/specs/2026-07-12-lyra-discovery-preview-design.md
Jonathan 4dbb6d9d83 docs: discovery preview design — artist preview page (disco + tracklists + own-state)
Follow-up to slice 3: a read-only artist preview page keyed by MBID, reachable from
suggested artists, suggested albums, and Find-similar results. Shows discography,
lazy per-album tracklists, own-state badges; Follow (reuse /api/artists) + Want
(new /api/discover/want upsert). Web-side MB reads only, no worker changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 01:26:16 +02:00

10 KiB
Raw Permalink Blame History

Lyra — Discovery Preview (Design)

Date: 2026-07-12 Status: Approved design, ready for implementation planning Slice: 3 follow-up (Discovery preview / depth + actionability)

Overview

Slice 3 shipped a discovery feed that is view-only: you can Follow / Want / Dismiss a suggestion, but you cannot inspect it first (no discography, no tracklist), and the "Find similar" seed-search results are a dead end (no actions at all). This follow-up adds depth and actionability via a single read-only artist preview page keyed by MusicBrainz MBID, reachable from every discovery entry point.

From a suggested artist, a suggested album, or a Find-similar result you can now open a preview page that shows the artist's discography, expands any album to its tracklist, marks albums you already have or monitor, and lets you Follow the artist or Want a specific album — all before committing. All MusicBrainz reads happen web-side (the pattern slices 23 established for interactive lookups); there are no worker changes and no new config.

Goals

  • Let the user inspect a recommendation in depth before acting: artist → albums → tracklist, without following first.
  • Make Find-similar results actionable (currently plain text): each links to the preview page and has a direct Follow.
  • Show own-state on the preview so the user doesn't re-request music they already have or monitor.
  • Reuse existing primitives: Follow = the existing POST /api/artists; the MB discography route already exists; Want reuses the discovery want upsert.
  • Keep it web-side and credential-free; fully testable with a fake/mocked MB.

Non-goals (this slice)

  • No worker changes, no new Config keys, no changes to the discovery sweep.
  • No tracklist caching/persistence — fetched live from MB on expand.
  • No audio preview/playback.
  • No separate album page — album entry points reuse the artist preview anchored to the album.
  • No monitor-toggling on the preview (that stays on /artists/[id] after follow).

Scope

In scope: a browseReleaseGroupTracks MB lib function; a GET /api/discover/preview/[mbid] endpoint (annotated discography + own-state + followed flag); a GET /api/mb/release-groups/[rgMbid]/tracks endpoint; a POST /api/discover/want endpoint (want a specific release-group by metadata); the /discover/artist/[mbid] preview page; and wiring the three discovery entry points (suggested artist, suggested album, Find-similar result) to the preview page + a Follow action on seed-search results.

Out of scope: everything under Non-goals.

Architecture

  /discover feed (discover-client.tsx)
    suggested artist name  ─► /discover/artist/[artistMbid]?name=…
    suggested album        ─► /discover/artist/[artistMbid]?name=…#rg-<rgMbid>
    Find-similar result    ─► name link to /discover/artist/[mbid]?name=…
                              + Follow button ─► POST /api/artists {mbid,name}

  /discover/artist/[mbid] (preview page, read-only)
    GET /api/discover/preview/[mbid]   ─► artistName, followed, releases[
                                            {rgMbid, album, types, date, monitored, have} ]
      (MB browse release-groups  +  join MonitoredRelease/LibraryItem  +  WatchedArtist)
    per-album expand:
      GET /api/mb/release-groups/[rgMbid]/tracks ─► { tracks[] }   (lazy, MB)
    actions:
      Follow  ─► POST /api/artists {mbid,name}            (existing)
      Want    ─► POST /api/discover/want {rgMbid, …}      (upsert monitored MonitoredRelease)

Everything the page needs is one MB-backed endpoint plus lazy per-album track fetches; actions reuse the existing follow primitive and a small want-by-metadata upsert.

Components

MB lib — web/src/lib/musicbrainz.ts

Add two functions:

getArtistName(mbid: string): Promise<string> — MB /artist/<mbid>?fmt=json, returns the name (or ""). Used only as the fallback when the caller didn't pass ?name=.

browseReleaseGroupTracks(rgMbid: string): Promise<Track[]> where Track = { position: number; title: string; lengthMs: number | null }.

  • Query /release?release-group=<rgMbid>&inc=recordings&fmt=json&limit=25.
  • Choose one representative release: prefer status === "Official", then the earliest date; fall back to the first release. If none, return [].
  • Flatten its media[].tracks[] to { position, title, lengthMs: track.length ?? null }, ordered by media then track position.
  • Reuses the existing mbGet helper + USER_AGENT.

GET /api/discover/preview/[mbid]

Returns the data for the whole preview page:

{
  artistName: string,
  followed: boolean,           // an existing WatchedArtist with this mbid?
  releases: Array<{
    rgMbid: string,
    album: string,
    primaryType: string | null,
    secondaryTypes: string[],
    firstReleaseDate: string | null,
    monitored: boolean,        // a monitored MonitoredRelease for this rgMbid
    have: boolean,             // MonitoredRelease.currentQualityClass set, OR a LibraryItem match
  }>
}

Implementation:

  1. browseReleaseGroups(mbid) for the discography. Default the response to core releases (isCoreRelease); accept ?all=true to include every type (the preview page's "show all types" toggle passes this).
  2. Resolve artistName: use the ?name= query param if present (all in-app callers pass it); otherwise getArtistName(mbid). The ?name= shortcut avoids an extra MB call on the common path; direct navigation still resolves correctly.
  3. followed: WatchedArtist.findUnique({ where: { mbid } }) != null.
  4. Own-state: one MonitoredRelease.findMany({ where: { rgMbid: { in: rgMbids } } }) → map monitored and have = currentQualityClass != null; plus a LibraryItem match by (artistName, album) contributes have. Annotate each release-group.
  5. 502 if MB is unavailable.

GET /api/mb/release-groups/[rgMbid]/tracks

Thin wrapper over browseReleaseGroupTracks: { tracks } on success, 502 on MB failure. Lazy — the preview page calls it only when an album is expanded.

POST /api/discover/want

Body { rgMbid, artistMbid, artistName, album, primaryType?, secondaryTypes?, firstReleaseDate? }. Validates rgMbid/artistMbid/artistName/album are non-empty strings, then prisma.monitoredRelease.upsert({ where: { rgMbid }, create: { …, monitored: true }, update: { monitored: true } }) — identical to the discovery action route's want branch, but keyed off caller-supplied metadata (the preview page already has it from the discography). Returns { id, album, monitored }. 400 on a bad body.

Preview page — /discover/artist/[mbid]/page.tsx + preview-client.tsx

  • Server page reads params.mbid and searchParams.name, renders the client with both + a nav line.
  • Client:
    • On mount, GET /api/discover/preview/[mbid]?name=…(&all=…).
    • Header: artist name; Follow button → POST /api/artists (hidden or disabled + labelled "Following" when followed); a "MusicBrainz ↗" external link to https://musicbrainz.org/artist/<mbid>.
    • "Show all types" checkbox → refetch with all=true.
    • Discography list: each row keyed/anchored id="rg-<rgMbid>", shows album + type + year, an own-state badge (In library / Monitored / none), a Want button (disabled when have || monitored) → POST /api/discover/want with the row's metadata, and an expand toggle that lazy-loads GET /api/mb/release-groups/[rgMbid]/tracks and lists ## Title (m:ss).
    • On load, if the URL has a #rg-<rgMbid> hash, scroll it into view and highlight.

Feed wiring — web/src/app/discover/discover-client.tsx

  • Suggested-artist artistName<a href="/discover/artist/{artistMbid}?name=…">.
  • Suggested-album album<a href="/discover/artist/{artistMbid}?name=…#rg-{rgMbid}">.
  • Find-similar result rows → name linked to /discover/artist/{mbid}?name=…, plus a Follow button calling POST /api/artists { mbid, name } (optimistically removes/greys the row on success).

Error handling

Failure Behavior
MB unavailable (preview browse) preview endpoint returns 502; page shows an error with a retry.
MB unavailable (tracklist) tracks endpoint 502; the expanded album shows "couldn't load tracks", other rows unaffected.
Release-group has no releases/tracks browseReleaseGroupTracks returns []; UI shows "no track data".
Want a release already monitored upsert flips monitored=true idempotently; button was already disabled if own-state known.
Follow an already-followed artist POST /api/artists returns 409; UI treats it as already-followed (no error surfaced).
Direct navigation without ?name= endpoint resolves the name from MB; slightly slower, still correct.

Testing (web, against lyra_test)

  • browseReleaseGroupTracks — mocked fetch: parses tracks, prefers Official then earliest release, orders by media/position, [] when no releases.
  • /api/discover/preview/[mbid] — mock browseReleaseGroups; seed WatchedArtist / MonitoredRelease (monitored + have) / LibraryItem; assert own-state annotation, followed, core-only default vs ?all=true, 502 on MB throw, ?name= shortcut.
  • /api/mb/release-groups/[rgMbid]/tracks — mock lib: tracks shape + 502.
  • /api/discover/want — DB-backed: creates a monitored MonitoredRelease from the body; re-want idempotent; 400 on bad body.
  • Preview page + client and feed wiring — verified by tsc --noEmit + npm run build (UI wiring, consistent with how the slice-3 pages were verified).

Notes / deferred

  • Tracklists are fetched live per expand; if MB rate-limiting bites, a short-lived cache or persisting tracklists on Want is a later option.
  • The preview shows a single representative release's tracklist; multi-disc/edition differences aren't surfaced.
  • have via LibraryItem matches on (artistName, album) (name-based, like the existing wanted-add path); MBID-based library matching is a broader future change.