A small, forkable reference player for a SUB/WAVE internet radio station. Clone it, restyle it by hand or by describing the vibe to a coding agent, then deploy it. There's also a guide here for building your own player from scratch against the SUB/WAVE public API.
SUB/WAVE is a personal internet radio station: one shared Icecast stream, an AI DJ picking tracks and talking between them. Everyone hears the same broadcast at the same time. It's radio, not a playlist. This repo is a reference web player for it, built to be gutted and made your own.
Now-playing with cover art and acoustic tags, live listener count, the AI DJ's booth feed, song requests, and the weekly schedule.
The data layer (API client + hooks) is cleanly split from the UI, so you keep the plumbing and rebuild only the look.
Ships an AGENTS.md and built-in skills, so Claude Code or Cursor already understand how it's wired. Describe the vibe and go.
React + Vite + TypeScript + Tailwind. It comes pointed at the live public station, so it plays real radio the moment you run it. No config to fill in first.
# clone and run git clone https://github.com/getsubwave/web-player.git cd web-player npm install npm run dev # → http://localhost:5173
npm run build produces a static dist/ you can host anywhere.
Point it at your own station by setting VITE_STATION_URL in a .env
file, and everything else derives from that one origin.
Would rather just say what you want and let a coding agent write it? The repo is set up for that, and you don't need to know how to code.
Clone it and open the folder in your agent (Claude Code, Cursor…), or paste the repo URL and ask it to clone.
Talk like a designer, not a programmer. The redesign-player skill keeps the audio and live data working while it restyles.
Ask the agent to run the dev server, then to deploy it to Vercel, Netlify, or Docker. Keep iterating in plain English.
“Restyle this like a warm 70s vinyl setup: cream, burnt orange and brown, chunky rounded panels, a serif logo.”
“Make it minimal black-and-white brutalist. Monospace font, hard edges, no shadows.”
“Point this at my station at https://radio.mysite.com, then deploy it to Vercel.”
Then keep going: “make the accent pinker”, “bigger cover art”, “move the request box to the top”. You never have to open the code.
Building a player from scratch, maybe for the web, a TV, or an e-ink dashboard? Here are the ideas that carry the weight. They hold whether you fork this repo or start fresh in another framework.
Every SUB/WAVE deployment serves the same routes on one hostname. From a single station URL you derive everything:
const API = `${stationUrl}/api` // controller JSON
const AUDIO = `${stationUrl}/stream.mp3` // Icecast MP3 (universal floor)
const COVER = `${API}/cover/${subsonicId}` // artwork proxy
Everything is unauthenticated and CORS-open (Access-Control-Allow-Origin: *), and the Icecast mount sends the same, so a browser client can fetch and stream cross-origin from anywhere.
There's no websocket. Poll /now-playing, /state and /session on a timer (5s is plenty) and render the latest snapshot. Pause polling when the tab is hidden; keep the last good snapshot on a failed fetch so the UI never blanks.
tune-in on tap browsers block autoplay, so start audio from a user gesture.
an off-air state the track can be null even when online.
optional fields a fresh track has no BPM/mood; a jingle has no cover.
no skip / next one shared stream, so skipping would skip for everyone. no seek bar the playhead is client-side and approximate. no per-listener shuffle it's a broadcast, not a queue you own.
The thing that makes a player easy to restyle: keep all fetching and audio in one thin layer (a client + a few hooks), and let the UI be a pure function of that data. Then a redesign never touches the plumbing. In this repo that's src/lib/ + src/hooks/ (keep) vs src/components/ (rewrite).
The public surface a player consumes. Base is ${stationUrl}/api; the stream is at ${stationUrl}/stream.mp3.
| Endpoint | Returns / powers |
|---|---|
GET /now-playing | Current track + cover art + acoustic tags (genre·BPM·key·mood·energy), on-air DJ persona + show + guests, listener count, stream bitrate, LLM token ticker. |
GET /state | Queue (up next) + history (recently played) + DJ log. |
GET /session | The Booth feed: the live DJ session's chat turns (what the AI DJ said/queued). |
GET /schedule | The weekly 7×24 grid + shows + personas, in station-local time. |
POST /request | Submit a plain-language song request → returns a requestId. |
GET /request/:id | Poll a request's outcome (pending / resolved / failed). |
GET /cover/:id | Cover-art proxy (safe for lock-screen artwork). |
GET /health | Liveness. Returns {"status":"on-air"}. |
GET /stream.mp3 | The audio itself (Icecast; not under /api). |
Optional mounts appear when the operator enables them: /stream.opus, /stream.flac, /stream.aac. MP3 is always served.
It's a static SPA, so it goes to any static host in one click. Or run it as a container.
# or with Docker docker compose up -d --build # → http://localhost:8080
VITE_STATION_URL is baked in at build time, so set it as a build arg or env var to lock a deployment to your own station, then rebuild.