Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DeetsRadio

Status: live at radio-api.deets.solutions · Platform: Cloudflare Worker + Durable Objects + D1

Cloudflare Worker behind the DeetsRadio tab on deets.solutions — sibling to DeetsSolutions (the site), DeetsLeague (the League tab's worker), and DeetsMusic (the desktop app the radio UX is ported from). Design doc: DeetsSolutions/docs/radio.md.

One Durable Object per listening room: hibernatable WebSockets carry the room protocol (communal queue, synced play/pause/skip/back, shared history), a storage alarm advances tracks so an empty room keeps "playing", and GET /room/{code}/peek powers the site's create-confirm gate. The room owns a clock, never audio — playback happens per-listener in the browser through Apple Music (or the 30 s preview tier). Each room has a background owner (creator, then join-order succession; connection-scoped) whose one power is close — sign the station off and free the code — and the same alarm slot doubles as a 1-hour idle+empty expiry fuse, so abandoned rooms return their names to the pool.

No dependencies, no build step, no D1, no KV. Free tier covers it: WS messages bill 20:1 under hibernation, presence broadcasts persist nothing, and each mutation is one batched storage write (room history rides the DO's private SQLite).

Wire protocol

JSON messages over GET /room/{code}/ws. Queue verbs need the queue capability at e, player verbs need player at e (owner always has both); the rest are open to any joined listener except the owner-only trio and close. A literal ping is auto-answered pong by the platform without waking the object.

Client → server:

Type Payload Does
join name, token, create? Enter (or, with create, mint) the room; replies with snapshot
rename name Change display name (unique per room)
play / pause / skip / back Shared transport; starts count down LEAD ms
add entry, at?, source? Queue a sanitized entry (cap 200); first add while idle starts play
remove entryId Drop a queued entry
reorder entryId, to Move a queued entry
resolve entryId, youtube, source? Backfill a missing YouTube block (never overwrites)
setVideo entryId, youtube Overwrite an entry's video (manual pick)
setSong entryId, song Re-pin an entry's Apple identity
setCap t, cap, level Owner only: grant queue/player at r|e to handle t
kick t Owner only: disconnect a listener (no ban)
setMode mode Owner only: open | restricted (capability defaults)
close Owner only: sign the station off, free the code

Server → client:

Type Carries When
snapshot v, serverNow, room, transport, current, queue, history, listeners, owner, you Reply to a successful join
state v, serverNow, plus only the changed fields Broadcast after every mutation
presence listeners, owner, you Join / leave / rename / cap or mode change
error code: rate, name-taken, full, no-room, perm Refused message (join refusals also close the socket)
kicked serverNow You were kicked (socket closes)
closed serverNow Owner closed the room (socket closes)

Local dev

npx wrangler dev --port 8789
# then open http://localhost:8787/radio/?api=http://127.0.0.1:8789
# (the site itself runs from DeetsSolutions via python -m http.server 8787)
curl "http://127.0.0.1:8789/room/test-room/peek"

Deploy

npx wrangler login      # one-time
npx wrangler deploy     # custom domain radio-api.deets.solutions rides wrangler.jsonc

No secrets in v0.9 (Apple search runs in the browser on the origin-locked developer token; Spotify — which would put a client-credentials secret here — is a v1.0 flag away).

License

MIT — see LICENSE.

About

Cloudflare Worker behind the DeetsRadio tab on deets.solutions — one Durable Object per listening room, hibernatable WebSockets for synced playback, queue, and chat.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages