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

204 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```ts
{
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.