feat: /api/* library reads, search, and media endpoints #15

Merged
bvandeusen merged 262 commits from dev into main 2026-04-21 23:02:29 -04:00
bvandeusen commented 2026-04-21 20:46:50 -04:00 (Migrated from git.fabledsword.com)

Ships Plans 2 and 3 of the web UI /api/* rollout. Plan 2 delivers the paginated library reads and search; Plan 3 delivers the cover + stream byte-serving endpoints that Plan 2's DTOs already reference.

Plan 3 — media endpoints

Spec: docs/superpowers/specs/2026-04-21-web-ui-media-endpoints-design.md
Plan: docs/superpowers/plans/2026-04-21-web-ui-media-endpoints.md

  • GET /api/albums/{id}/cover — cover art bytes; explicit cover_art_path takes precedence, else sidecar lookup (cover.{jpg,jpeg,png} then folder.{jpg,jpeg,png}) next to the first track's file. Content-Type from extension.
  • GET /api/tracks/{id}/stream — raw audio bytes with Range / If-Modified-Since / ETag via http.ServeContent. Content-Type from track.file_format, Accept-Ranges: bytes.

Both under RequireUser. Vanished-file races are 404, not 500. MIME tables duplicated locally rather than shared with internal/subsonic — deliberate per project direction (subsonic stays frozen). audioContentType intentionally diverges from subsonic (opus→audio/ogg, aac→audio/aac), documented inline.

Plan 2 — library reads + search

Spec: docs/superpowers/specs/2026-04-20-web-ui-library-reads-design.md
Plan: docs/superpowers/plans/2026-04-20-web-ui-library-reads.md

  • GET /api/artists — paginated list, sort=alpha|newest, default limit=50, clamped to [1,200]
  • GET /api/artists/{id} — artist detail with nested album refs
  • GET /api/albums/{id} — album detail with nested track refs (aggregates duration_sec)
  • GET /api/tracks/{id} — track detail with parent album + artist metadata
  • GET /api/search?q=… — three-facet paginated search; each facet carries its own total

Plan 2 highlights

  • Enveloped pagination: {items, total, limit, offset} uniform across list and every search facet
  • Ref/Detail DTO split mirroring internal/subsonic (packages stay independent — duplication is intentional)
  • Empty slices always render as [], never null (make([]T, 0, len(x)) and asserted in tests)
  • Out-of-range limit/offset silently clamp; only non-numeric values return 400
  • AlbumRef.duration_sec is 0 in nested browse contexts, populated on direct album GET (per spec data flow)

Test plan

  • internal/api/... test suite green (includes 20+ new tests for Plan 3 handlers + helpers)
  • TestRoutesRegisteredInMount verifies all 7 /api/* library/search/media routes wired under RequireUser
  • Build + vet clean across all packages
  • Two-stage review (spec compliance + code quality) per task, plus final end-to-end review
  • Manual browser smoke after merge: <img src="/api/albums/{id}/cover"> renders, <audio src="/api/tracks/{id}/stream"> plays and seeks
  • Manual curl: Range: bytes=0-99 → 206 with Content-Range, repeat with If-Modified-Since → 304

🤖 Generated with Claude Code

Ships Plans 2 and 3 of the web UI `/api/*` rollout. Plan 2 delivers the paginated library reads and search; Plan 3 delivers the cover + stream byte-serving endpoints that Plan 2's DTOs already reference. ## Plan 3 — media endpoints Spec: `docs/superpowers/specs/2026-04-21-web-ui-media-endpoints-design.md` Plan: `docs/superpowers/plans/2026-04-21-web-ui-media-endpoints.md` - `GET /api/albums/{id}/cover` — cover art bytes; explicit `cover_art_path` takes precedence, else sidecar lookup (`cover.{jpg,jpeg,png}` then `folder.{jpg,jpeg,png}`) next to the first track's file. `Content-Type` from extension. - `GET /api/tracks/{id}/stream` — raw audio bytes with Range / `If-Modified-Since` / ETag via `http.ServeContent`. `Content-Type` from `track.file_format`, `Accept-Ranges: bytes`. Both under `RequireUser`. Vanished-file races are 404, not 500. MIME tables duplicated locally rather than shared with `internal/subsonic` — deliberate per project direction (subsonic stays frozen). `audioContentType` intentionally diverges from subsonic (opus→`audio/ogg`, aac→`audio/aac`), documented inline. ## Plan 2 — library reads + search Spec: `docs/superpowers/specs/2026-04-20-web-ui-library-reads-design.md` Plan: `docs/superpowers/plans/2026-04-20-web-ui-library-reads.md` - `GET /api/artists` — paginated list, `sort=alpha|newest`, default `limit=50`, clamped to `[1,200]` - `GET /api/artists/{id}` — artist detail with nested album refs - `GET /api/albums/{id}` — album detail with nested track refs (aggregates `duration_sec`) - `GET /api/tracks/{id}` — track detail with parent album + artist metadata - `GET /api/search?q=…` — three-facet paginated search; each facet carries its own `total` ### Plan 2 highlights - Enveloped pagination: `{items, total, limit, offset}` uniform across list and every search facet - Ref/Detail DTO split mirroring `internal/subsonic` (packages stay independent — duplication is intentional) - Empty slices always render as `[]`, never `null` (`make([]T, 0, len(x))` and asserted in tests) - Out-of-range `limit`/`offset` silently clamp; only non-numeric values return 400 - `AlbumRef.duration_sec` is `0` in nested browse contexts, populated on direct album GET (per spec data flow) ## Test plan - [x] `internal/api/...` test suite green (includes 20+ new tests for Plan 3 handlers + helpers) - [x] `TestRoutesRegisteredInMount` verifies all 7 `/api/*` library/search/media routes wired under `RequireUser` - [x] Build + vet clean across all packages - [x] Two-stage review (spec compliance + code quality) per task, plus final end-to-end review - [ ] Manual browser smoke after merge: `<img src="/api/albums/{id}/cover">` renders, `<audio src="/api/tracks/{id}/stream">` plays and seeks - [ ] Manual curl: `Range: bytes=0-99` → 206 with `Content-Range`, repeat with `If-Modified-Since` → 304 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#15