Build your own radio player

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.

The premise

One stream. Every listener. Your interface.

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.

§ Full-featured

Now-playing with cover art and acoustic tags, live listener count, the AI DJ's booth feed, song requests, and the weekly schedule.

§ Forkable by design

The data layer (API client + hooks) is cleanly split from the UI, so you keep the plumbing and rebuild only the look.

§ Agent-ready

Ships an AGENTS.md and built-in skills, so Claude Code or Cursor already understand how it's wired. Describe the vibe and go.

Quick start · by hand

Clone, run, done.

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.

Quick start · with agents

Describe the look and let the agent build it.

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.

1 Hand it the repo

Clone it and open the folder in your agent (Claude Code, Cursor…), or paste the repo URL and ask it to clone.

2 Describe the look

Talk like a designer, not a programmer. The redesign-player skill keeps the audio and live data working while it restyles.

3 See it, ship it

Ask the agent to run the dev server, then to deploy it to Vercel, Netlify, or Docker. Keep iterating in plain English.

Example prompts

“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.

Guidelines

Building your own player

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.

1. One origin, three things

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.

2. Poll, don't subscribe

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.

3. Respect live-radio rules

Do

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.

Don't

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.

4. Split data from presentation

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).

Reference

Station API

The public surface a player consumes. Base is ${stationUrl}/api; the stream is at ${stationUrl}/stream.mp3.

EndpointReturns / powers
GET /now-playingCurrent track + cover art + acoustic tags (genre·BPM·key·mood·energy), on-air DJ persona + show + guests, listener count, stream bitrate, LLM token ticker.
GET /stateQueue (up next) + history (recently played) + DJ log.
GET /sessionThe Booth feed: the live DJ session's chat turns (what the AI DJ said/queued).
GET /scheduleThe weekly 7×24 grid + shows + personas, in station-local time.
POST /requestSubmit a plain-language song request → returns a requestId.
GET /request/:idPoll a request's outcome (pending / resolved / failed).
GET /cover/:idCover-art proxy (safe for lock-screen artwork).
GET /healthLiveness. Returns {"status":"on-air"}.
GET /stream.mp3The audio itself (Icecast; not under /api).

Optional mounts appear when the operator enables them: /stream.opus, /stream.flac, /stream.aac. MP3 is always served.

Ship it

Deploy

It's a static SPA, so it goes to any static host in one click. Or run it as a container.

Deploy with Vercel Deploy to Netlify Deploy to Cloudflare

# 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.