music: per-user Favorites + Currently-playing (platform/Postgres)
User-level state for the music app, served by the platform from Postgres (not the sidecar, which is stateless about users) under the same /api/music prefix so the music-app account gate permits it: - music_favorites (userId, kind, key) — kind ∈ track|album|artist, opaque path key the server never interprets; unique per (user,kind,key), newest-first. GET /favorites (grouped), POST /favorites (idempotent), DELETE /favorites. - music_now_playing (one row/user) — current track + position snapshot for resume-across-launch/device, with a light title/artist/album cache so the resume card renders before the library index syncs. GET/PUT/DELETE /now-playing (upsert). Routes registered before the catch-all proxy. Tables created via direct DDL; query layer smoke-tested against the live DB. MUSIC_API.md documents the contract for the app. App-side wiring (heart toggles, Favorites view, player persistence) follows next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,49 @@ Result: a resync after adding one album = 1 manifest fetch + that one album's `m
|
||||
|
||||
---
|
||||
|
||||
## Per-user state — Favorites & Currently-playing
|
||||
|
||||
Unlike everything above (library data served by the sidecar), these are **per-user** and served by the
|
||||
platform straight from Postgres — same `/api/music` prefix and same auth. Keys are opaque paths the app
|
||||
supplies; the server never interprets them:
|
||||
|
||||
| kind | key |
|
||||
|---|---|
|
||||
| `track` | home-path — `Music/<rel>/<file>` (also the `/stream` path & queue id) |
|
||||
| `album` | music-rel — `Albums/AC-DC/[1980] Back in Black` |
|
||||
| `artist` | music-rel — `Albums/AC-DC` |
|
||||
|
||||
### Favorites
|
||||
|
||||
- **`GET /api/music/favorites`** → grouped keys, newest first:
|
||||
```json
|
||||
{ "tracks": ["Music/…/01 Hells Bells.mp3"], "albums": ["Albums/AC-DC/[1980] Back in Black"], "artists": ["Albums/AC-DC"] }
|
||||
```
|
||||
- **`POST /api/music/favorites`** `{ "kind": "track|album|artist", "key": "…" }` → `{ ok: true }`. Idempotent
|
||||
(a repeat add is a no-op).
|
||||
- **`DELETE /api/music/favorites?kind=<kind>&key=<key>`** → `{ ok: true }` (no-op if not set). Key passed as a
|
||||
query param (URL-encode it).
|
||||
- `400 { error: "kind and key required" }` on a bad/missing kind or empty key.
|
||||
|
||||
### Currently-playing (resume)
|
||||
|
||||
One snapshot per user — persist while playing (throttled) and on pause / track-change / close; read it on
|
||||
launch to offer "resume".
|
||||
|
||||
- **`GET /api/music/now-playing`** → the snapshot or `null`:
|
||||
```json
|
||||
{ "homePath": "Music/…/01 Hells Bells.mp3", "dir": "Music/Albums/AC-DC/[1980] Back in Black",
|
||||
"title": "Hells Bells", "artist": "AC/DC", "album": "Back in Black",
|
||||
"durationSec": 312.5, "positionSec": 140, "updatedAt": "2026-07-27T11:27:54.441Z" }
|
||||
```
|
||||
`dir` is the folder to rebuild the album queue from (empty for a cross-album queue → resume the single track).
|
||||
- **`PUT /api/music/now-playing`** `{ homePath (required), dir?, title?, artist?, album?, durationSec?, positionSec? }`
|
||||
→ `{ ok: true }` (upsert). Omitted fields default to `""`/`0`.
|
||||
- **`DELETE /api/music/now-playing`** → `{ ok: true }` (clear, e.g. on stop).
|
||||
- `400 { error: "homePath required" }` if `homePath` is missing/empty.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Covers are server-compressed** (≤600px / q5) — sync them as-is; no client-side resizing needed.
|
||||
|
||||
Reference in New Issue
Block a user