## Daveo Radio
Just a litle page on the intrawebs where I can broadcast and chat with friends.
> **Note:** there is no authentication. The page, the chat socket and the Icecast
> stream are all reachable by anyone who has the URL. See "Auth" below before
> putting this anywhere public.
---
#### Built With:
- Icecast - https://icecast.org
- Node - https://nodejs.org
- Express - https://expressjs.com
- Socket.io - https://socket.io
- Redis - https://redis.io (optional)
- Amplitude.js - https://github.com/serversideup/amplitudejs
- Last.fm API - https://www.last.fm/api
---
#### Running it
Needs Node 20+.
```sh
npm install
cp .env.example .env # then edit
npm run build # src/ -> app/
npm start
```
`npm run dev` rebuilds on change and reloads the server.
Config is all environment variables (see `.env.example`) — there are no
`config.js` / `config-dev.js` files anymore.
Nothing renders until `npm run build` has run: Express serves `app/`, which is
generated from `src/` and is not in git.
#### Serving under davidawindham.com/radio
Set `BASE_PATH=/radio` and `TRUST_PROXY=1`, then proxy to it from Apache — the
same shape as the existing `/ask` proxy in the daw vhost:
```apache
# radio: proxy /radio to the node app (mirrors /ask)
ProxyPreserveHost On
ProxyPass /radio http://127.0.0.1:3000/radio upgrade=websocket
ProxyPassReverse /radio http://127.0.0.1:3000/radio
```
Both sides keep the `/radio` prefix on purpose — the app is mounted at it and
expects to see it, and that keeps its own `/radio` → `/radio/` redirect correct
through `ProxyPassReverse`.
`upgrade=websocket` is what carries socket.io. It needs **Apache 2.4.47+**, where
`mod_proxy_http` handles protocol upgrades itself — `mod_proxy_wstunnel` is *not*
required (on older Apache it would be). Without the upgrade, socket.io still
works but silently falls back to HTTP long-polling.
ProxyPass is matched before the request is mapped to the filesystem, so
WordPress and its `.htaccess` never see `/radio`.
> **If `/radio` redirects you to `/online-radio`:** that's a **cached 301**. The
> WordPress page that used to live at `/radio` moved, and WP issues a permanent
> old-slug redirect, which browsers cache hard. Once the proxy is in place
> Apache answers `/radio` before WP ever sees it — but a browser that cached the
> 301 beforehand will keep redirecting itself. Hard-reload, or clear that entry.
Leave `BASE_PATH` empty to serve at a domain root instead; the client works out
where it's mounted at runtime, so the same build covers both.
#### Site chrome
The header and footer come from the main site's shared web components — the same
ones `/rtc` uses:
```html
```
`chrome.js` renders into a shadow root, so the site's styles stay sealed off from
this page's Bootstrap 3 and the two can't collide. Nothing is copied into this
repo — the chrome is maintained in the main site and can't drift out of sync here.
That script tag is root-relative on purpose: in production Apache serves `/embed`
from the docroot alongside `/radio`. Locally there's no Apache in front, so set
`DAW_ORIGIN=http://daw.stu` and the app proxies `/embed` to it. **Leave
`DAW_ORIGIN` unset in production.**
#### Routes
| Route | Purpose |
|---|---|
| `/` | the page |
| `/health` | ok, room count, socket count |
| `/api/status` | now-playing + listeners, proxied from Icecast |
| `/api/lastfm` | sidebar data, proxied so the API key stays server-side |
| `/api/broadcast` | POST `{msg}` — sends to every room |
All are relative to `BASE_PATH`.
#### Icecast
The player streams from `$STREAM_URL` and gets now-playing from `$STREAM_STATUS_URL`.
Both must be **https** — the page is served over TLS, so an `http://` stream is
blocked as mixed content.
Status is read from Icecast's stock `status-json.xsl` and proxied through
`/api/status` rather than being fetched from the browser. The old code called a
hand-customized `status2.xsl` over JSONP; Icecast upgrades overwrite those XSL
files (see the 2021 note below — it 404s today). The stock endpoint survives
upgrades, and proxying keeps the page same-origin.
#### Redis
Optional. Unset `REDIS_URL` and it runs single-process, keeping per-connection
state in memory. Set it and socket.io uses the Redis adapter so rooms and
presence work across multiple processes.
#### Chat
One room. The join/leave UI is gone and the server has no
`subscribe`/`unsubscribe` handlers, so the Lobby is the only room there is —
removing the buttons alone wouldn't have stopped anyone emitting `subscribe` by
hand.
Nicknames are opt-in: the person button beside the message box opens the nickname
dialog. Nothing is demanded on load; unnamed users chat as `anonymous`.
#### Auth
There isn't any, and there's no longer anything pretending otherwise — the old
password prompt was only ever a client-side modal gate, so it was removed rather
than left to imply protection it never gave.
`/api/broadcast` is unauthenticated, the socket handshake is unauthenticated, and
the Icecast stream can be opened directly. Doing this properly means a
server-verified session covering the socket handshake and the stream, not just
the page.
---
#### Notes:
- Feb 2016 - built and published @ http://radio.davidawindham.com
- stream running from http://stream.davidawindham.com (8008)
- 2021 - Icecast updated overwrote Icecast XML files - see: https://davidawindham.com/til/docs/host/Icecast
- May 2022 - Migrate to new server, reinstall Icecast, add SSL to stream, updated packages, and secure Redis.
- Jul 2026 - Modernized. Server rewritten on Express 5 / socket.io 4; gulp (dead on
Node 12+) replaced with esbuild; config moved to env vars; stream status proxied
server-side over https; mountable under a subpath.
- Jul 2026 - Trimmed for the davidawindham.com/radio rebuild. Dropped the Facebook
comments tab (third-party JS for a widget on the long-dead Graph v2.5) and the
Call/Video tabs (WebRTC sample code with no signaling — it could only call
itself). Last.fm moved behind a server-side proxy so the key no longer ships to
the browser.