From 566e6edbbdddcc92ddaa7242975513a9b50bbd31 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Mon, 8 Jun 2026 03:07:09 +0200 Subject: [PATCH 1/2] feat(albums,artists): browse endpoints over per-library album + artist surface (phase 4.d.0.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 4 read-only endpoints that close the loop on the album / artist surface materialised by `apply::track` (phase 4.d.0.2) + the schema shipped in `20260608120000_album_artist.sql` (phase 4.d.0.1): GET /api/v1/profiles/{p}/libraries/{l}/albums GET /api/v1/profiles/{p}/libraries/{l}/albums/{id}/tracks GET /api/v1/profiles/{p}/libraries/{l}/artists GET /api/v1/profiles/{p}/libraries/{l}/artists/{id}/tracks Pattern lifts the 2-query ownership-check + fetch idiom from `db::playlist_track::fetch_for_owner` so 404 (library / album / artist missing or foreign-owned) stays distinct from 200 [] (owned but empty). The race window is benign — every parent cascades on DELETE so a row vanishing between the two queries collapses to the empty-list answer. Album list joins `artist` once to surface `album_artist_name` (avoids N artist lookups client-side). Compilation rows project null + is_compilation=true. Drill-down ORDER BY rides the indexes the schema migration planted: album → (disc_number, track_number, id), artist → (disc_number, track_number, id) via track_artist join. Writes deliberately not exposed — album / artist rows come from the apply pipeline only. TrackResponse gains `album_id` so the artist drill-down can deep-link contributed tracks to their album page without N extra round-trips. Existing `/tracks` collection still NULL-projects the column (its SELECT lives in waveflow-core); follow-up PR bumps core. Test coverage: empty → 200 [], ordering by updated_at DESC, tied updated_at → id ASC tiebreak (guards the apply-pipeline-batch invariant), compilation NULL projection, multi-artist tracks surface under every contributor, full cross-tenant battery (foreign user, foreign profile, wrong library_id pivot). OpenAPI snapshot guards the 4 new paths. Doc bullet added to server CLAUDE.md. Signed-off-by: InstaZDLL --- CLAUDE.md | 1 + src/api/albums.rs | 173 ++++++++++++++++ src/api/artists.rs | 145 ++++++++++++++ src/api/mod.rs | 14 +- src/api/tracks.rs | 12 ++ src/db.rs | 307 +++++++++++++++++++++++++++++ tests/albums.rs | 479 +++++++++++++++++++++++++++++++++++++++++++++ tests/artists.rs | 413 ++++++++++++++++++++++++++++++++++++++ tests/openapi.rs | 23 +++ 9 files changed, 1564 insertions(+), 3 deletions(-) create mode 100644 src/api/albums.rs create mode 100644 src/api/artists.rs create mode 100644 tests/albums.rs create mode 100644 tests/artists.rs diff --git a/CLAUDE.md b/CLAUDE.md index 90213c5..0d9e37c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,7 @@ CI runs the full suite only on Linux (service container); the Windows leg is a c - **Auth: JWT-only (Phase 1.d.2).** `middleware::authenticate` requires a Bearer JWT signed by the upstream Better Auth issuer. The middleware verifies the token via [`AppState::jwt_verifier`], then `db::users::find_or_provision_by_external_id(state.db, &sub, now_ms)` lazy-onboards the user on first request — a valid signature is the authoritative onboarding signal, so no separate `POST /api/v1/users` exists. Boot requires the full `WAVEFLOW_JWT_*` triple (`_JWKS_URL` / `_ISSUER` / `_AUDIENCE`); the legacy `X-User-Id` dev shim retired alongside `WAVEFLOW_DEV_AUTH`. - **Streaming (Phase 1.e).** Two endpoints: `POST /api/v1/profiles/{p}/libraries/{l}/tracks/{t}/stream-url` (JWT-authed) verifies tenant ownership and signs a short-lived (≤ 60 s) URL via [`stream_token::mint`]; `GET /api/v1/stream/{token}` is mounted OUTSIDE the JWT layer because browsers can't attach a Bearer to `