music: document the full /api/music/* contract at the source of truth

The proxy is an opaque catch-all, so the endpoint surface wasn't perceivable from
the platform side. Add a contract header (all routes + params + SSE/response
shapes) atop the sidecar fetch handler where the routes are defined, and point
the proxy router at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 03:23:09 +00:00
co-authored by Claude Opus 4.8
parent 1192aa23fc
commit fe6f1fc095
2 changed files with 28 additions and 2 deletions
+6 -2
View File
@@ -3,8 +3,12 @@ import { getMusicServerUrl } from './sidecar-server';
// Thin reverse-proxy for /api/music/*. Auth is handled upstream by userMiddleware (this router mounts
// under the protected /api tree, so the media `?token=` path works). Everything else — path resolution,
// byte-range streaming, ffprobe duration — is done by the officer-music sidecar's audio server. We only
// forward the subpath + query + Range and stream the response back.
// byte-range streaming, ffprobe duration, indexing — is done by the officer-music sidecar's audio
// server. We only forward the subpath + query + Range and stream the response back.
//
// This is a catch-all, so it lists no routes: the full /api/music/* HTTP contract (stream, manifest,
// meta, cover, reindex, reindex/stream + their SSE/response shapes) is documented at the top of the
// sidecar's fetch handler — src/servers/sidecar/music/index.ts.
export const musicRouter = createRouter();
+22
View File
@@ -21,6 +21,28 @@ const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
// resolution + streaming + ffprobe duration happens here); the platform API is just a thin proxy that
// authenticates and forwards to us. The server listens on a random loopback port, reported to the API
// on connect so it can route `/api/music/*` here.
//
// ─────────────────────────────────────────────────────────────────────────────────────────────────
// HTTP CONTRACT — the full `/api/music/*` surface (this fetch handler is the source of truth; the
// platform side is an opaque catch-all proxy). All routes are reached as `/api/music/<name>`, authed
// upstream by userMiddleware (Bearer header or `?token=` for media). Data shapes are the exported
// `IndexStatus` / `IndexReport` / `IndexMeta` types in indexer.ts.
//
// GET /stream?path=<home-relative> audio with Range→206 (Content-Range/Length/Accept-Ranges)
// + `X-Audio-Duration` (seconds, ffprobe). 400/404/416.
// GET /manifest { version, generatedAt, albums: { "<rel>": { v, cover, tracks } } }
// GET /meta?path=<rel> album meta.json (IndexMeta). ETag: <v>; If-None-Match → 304.
// GET /cover?path=<rel> compressed cover.jpg. ETag: <v>; If-None-Match → 304.
// POST /reindex start an async build; returns IndexStatus (running: true).
// GET /reindex/status IndexStatus snapshot.
// GET /reindex/stream SSE. Triggers a build if idle (`?trigger=0` = watch-only).
// `event: progress` (IndexStatus) throttled ~200ms, then one
// `event: done` (IndexReport) and the stream closes.
// GET /health "ok".
//
// `<rel>` = album folder path relative to the Music root (e.g. "Albums/AC-DC/[1980] Back in Black").
// `v` = per-album version stamp; unchanged `v` ⇒ nothing changed ⇒ the phone can skip re-downloading.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;