| config | ||
| frontend | ||
| scripts | ||
| streamer | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| README.md | ||
denpa-radio
a tiny independent multi-station internet radio. cassette-deck aesthetic, real audio, no algorithms.
live at https://denpa.femboy.page.
what's playing
| station | description | mp3 | opus |
|---|---|---|---|
| minecraft | C418, Lena Raine, Aaron Cherof — vol α+β | https://denpa.femboy.page/minecraft.mp3 (192k) | https://denpa.femboy.page/minecraft.opus (96k) |
drop a stream URL into VLC, mpv, foobar, or any internet-radio app.
endpoints
/<station>.mp3— MP3 192 kbps stream/<station>.opus— Opus 96 kbps stream/now-playing/<station>.json— currently playing (artist, title, album, duration, started_at)/now-playing/<station>.history.json— last 50 tracks for the station/api/stations.json— list of all stations with metadata/api/stations/<station>/cover— cover art (when present)/status-json.xsl— raw Icecast status (listener counts, mount info)
CORS is open on all of the above; build your own scrobbler / dashboard / overlay.
architecture
Caddy (host) ─┬─ /<station>.{mp3,opus}, /status-json.xsl → icecast (libretime/icecast)
├─ /now-playing/*.json → caddy file_server
├─ /api/* → frontend (astro 5 + react)
└─ / → frontend
▲
└─ liquidsoap (savonet/liquidsoap)
│ reads station folders
│ writes now-playing JSON
▼
Hetzner Storage Box (CIFS, mounted RO)
three docker containers (icecast, liquidsoap, frontend) and a Caddy host service. each station is a folder on the storage box with a _meta.yml, a tracks/ subdir, and an optional cover.{jpg,png,webp}. the frontend reads _meta.yml files at request time, so adding a station does not require a rebuild.
adding a station
on the storage box (mounted at /mnt/trashbox/denpa-radio/library/ on summer):
mkdir -p <station-id>/{tracks,jingles}
cat > <station-id>/_meta.yml <<EOF
name: My Station
description: short description
color: '#5cae34'
tags: [tag1, tag2]
EOF
# drop audio files into <station-id>/tracks/
# subdirs supported: tracks/Volume Alpha/, tracks/Singles/, etc — Liquidsoap recurses
# (optional) drop cover.jpg into <station-id>/ (NOT inside tracks/ — would fail to decode)
then on summer:
# duplicate the existing minecraft station block in /srv/denpa-radio/config/liquidsoap.liq
# rename references to your new id, then:
docker compose -f /srv/denpa-radio/docker-compose.yml restart liquidsoap
the frontend picks up the new station within ~30s. no Caddy reload, no Icecast restart.
station ids must match [a-z0-9-]+.
tags
- v0.1.0 — radio backend live (Minecraft station, MP3 + Opus).
- v0.2.0 — frontend live (cassette deck UI, real WebAudio spectrum, recently-played).
working on this repo
see CLAUDE.md for the full operator's manual: SSH conventions, deploy quirks, frontend stack notes, Liquidsoap gotchas, and the things we've already learned the hard way.