--- name: subwave description: Manage SUB/WAVE (self-hosted AI radio station) shows, DJ personas, the weekly schedule grid, and live overrides. Use for any SUB/WAVE admin-API work — creating a new anchor show, minting a DJ persona, scheduling a slot, or triggering a show live. --- # SUB/WAVE admin API A CLI wrapper (`subwave_api.py`, in this skill's directory) around the `sub-wave-controller` admin REST API, distilled from a long session of building ~30 anchor-playlist shows and DJ personas by hand with raw `docker exec curl` calls. Use it instead of re-deriving the request shapes each time. ## Setup Auth comes from `subwave/.env` (`ADMIN_USER`/`ADMIN_PASS`) at the repo root — no flags needed. **The admin API (port 7701) is not published to the host** — `docker ps` shows `sub-wave-controller` with only `7701/tcp` (internal), not `0.0.0.0:7701->7701`. Every call must go through `docker exec sub-wave-controller curl ...`, which is exactly what this script does. Don't waste a round trip re-checking this — it's a deliberate container-internal-only setup, not a misconfiguration. Run it as: `python3 .claude/skills/subwave/subwave_api.py ...` ## Commands ``` persona list persona create --name N --voice V --tagline T --soul S [--frequency silent|quiet|moderate|chatty|aggressive] [--humour 0-10] [--warmth 0-10] [--local-colour 0-10] show list show get --id ID show create --name N --topic T --playlist-id ID [--genres a,b,c] [--moods a,b] [--energies a,b] [--eras 1990-2000,2005-2010] [--persona-id ID] show set-persona --id ID --persona-id ID show set-name --id ID --name N show set-topic --id ID --topic T schedule grid # full 7x24 weekly grid, show names schedule open-slots # every null (day, hour) pair schedule set --show-id ID --day 0-6 --hour 0-23 # day 0 = Sunday schedule set-random --show-id ID # pick a random open slot trigger --show-id ID [--minutes 60] # live override, starts immediately ``` Playlists themselves (search/curate/create anchor playlists in Navidrome) are a separate concern — use the **navidrome** skill for that, then pass the resulting playlist id to `show create --playlist-id`. ## Gotchas learned the hard way - **`POST /shows` needs the show wrapped in `{"show": {...}}`** — posting the show object bare fails with a schema error (`expected object, received undefined`). The CLI handles this. - **New shows need a full show object, not a partial patch** — there's no PATCH endpoint. To change one field (persona, name, topic), read the full current show from `schedule.json`, mutate the one field, and re-POST the whole thing. `show set-persona`/`set-name`/`set-topic` do exactly this. - **`PUT /schedule` takes the *entire* weekly grid**, not a single slot — read the current grid, mutate one `[day][hour]` cell, PUT the whole thing back. `schedule set`/`set-random` do this for you. The response's `dropped` count should always be `0`; a nonzero value means a show id in the grid doesn't exist anymore (a bug elsewhere, not addressed here). - **`POST /settings` for a new persona is also whole-array append**, not a single-persona-create endpoint — there is no such endpoint. Read `personas`, append one object, POST `{"personas": [...]}`. - **Persona `frequency` is a strict enum**: `silent | quiet | moderate | chatty | aggressive`. Passing `"normal"` (an easy guess) fails with `must be one of: silent, quiet, moderate, chatty, aggressive` — the CLI defaults to `moderate` to sidestep this. - **A show with no explicit `personaId` inherits whatever `p_default0` is** (one of the original 3 built-in personas) — and if that persona has `djMode: false`, the show runs with **zero DJ talk breaks** and sounds silent/DJ-less. Always set a real persona explicitly once you know what voice/character you want; don't assume a blank field means "no host," it means "the accidental default host." - **`/schedule/override` (the live-trigger endpoint) starts immediately** — `startedAt: Date.now()`. There's no way to schedule a *future* start time through this endpoint; "play this at 9pm" only works if it's already ~9pm, or by placing the show in the recurring weekly grid at that hour instead. - **Two shows can't cleanly share one weekly slot** — if you want two moods to alternate in the same hour across the week (e.g. show A on Sun/Tue/Thu/Sat, show B on Mon/Wed/Fri), that's just setting the same `[hour]` column to different show ids on different `day` rows via repeated `schedule set` calls — there's no "alternate" primitive, just per-day-row assignment. - **Kokoro voice pool has ~28 usable English voices** (`af_*`/`am_*` American, `bf_*`/`bm_*` British) sharing one 27MB `voices-v1.0.bin` file — installing/using more costs nothing. `PERSONA_LIMIT = 48` is the only hard ceiling (`/app/src/schemas/persona.ts` inside the controller container). - **A library rescan can surface artists that "don't exist" on a first check** — Navidrome's index lags real disk state. If a user swears an artist should be there, `navidrome_api.py`-equivalent rescan (`startScan`/`getScanStatus` via the Subsonic API) before concluding it's actually missing.