Wraps show/persona/schedule/trigger calls that were previously done by hand with raw docker exec curl, and documents the gotchas hit tonight (whole-object PUT/POST semantics, the frequency enum, the silent default-persona trap, and the port-not-published-to-host setup). Claude-Session: https://claude.ai/code/session_01HZQK6jHmdTpFjFZM8FUnqA
5.2 KiB
name, description
| name | description |
|---|---|
| subwave | 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 <command> ...
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 /showsneeds 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-topicdo exactly this. PUT /scheduletakes 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-randomdo this for you. The response'sdroppedcount should always be0; a nonzero value means a show id in the grid doesn't exist anymore (a bug elsewhere, not addressed here).POST /settingsfor a new persona is also whole-array append, not a single-persona-create endpoint — there is no such endpoint. Readpersonas, append one object, POST{"personas": [...]}.- Persona
frequencyis a strict enum:silent | quiet | moderate | chatty | aggressive. Passing"normal"(an easy guess) fails withmust be one of: silent, quiet, moderate, chatty, aggressive— the CLI defaults tomoderateto sidestep this. - A show with no explicit
personaIdinherits whateverp_default0is (one of the original 3 built-in personas) — and if that persona hasdjMode: 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 differentdayrows via repeatedschedule setcalls — 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 27MBvoices-v1.0.binfile — installing/using more costs nothing.PERSONA_LIMIT = 48is the only hard ceiling (/app/src/schemas/persona.tsinside 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/getScanStatusvia the Subsonic API) before concluding it's actually missing.