8.6 KiB
Radio
A small, self-hostable website for an internet radio station. Point it at your
stream, edit one .env file, and you get a live player with album art, a
"now playing" title, a recently-played history, listener counts, and optional
lyrics — no code changes required.
Originally built for radio.dou.bet; this version is generic so anyone can drop in their own configuration.
Features
- Custom player — play/pause, seek + volume, elapsed time, and a LIVE badge for live streams (or a plain native player with
PLAYER_STYLE=simple). - Now playing from Icecast, AzuraCast, Shoutcast, or any JSON feed, normalized server-side.
- Album art and lyrics via Genius, with an optional Last.fm art fallback.
- Page background that recolors to the current album art (auto light/dark text).
- MediaSession integration so track info appears in OS/lock-screen media controls.
- Configurable title cleanup (strip
(Remastered),[Explicit],.mp3, …). - Recently-played history, live listener count, and optional Matomo analytics.
- Fully themeable from
.env(CSS variables) — no CSS editing required.
How it works
browser ──▶ index.php / assets/* (the page + player)
browser ──▶ api/nowplaying.php ──▶ Icecast / AzuraCast / Shoutcast / custom
browser ──▶ api/artwork.php ──▶ Genius API (optional)
browser ──▶ api/lyrics.php ──▶ Genius song page (optional)
The browser only ever talks to this site. PHP fetches your stream backend
server-side and returns one normalized JSON shape, which means no CORS headaches
and your internal backend address (e.g. 127.0.0.1:8000 behind nginx) is never
exposed to visitors. A typical deployment is nginx → PHP-FPM on the web box,
with nginx also proxying the audio stream from Icecast.
Requirements
- PHP 7.4+ (8.x recommended) with cURL, served by any web server (nginx + PHP-FPM, Apache, Caddy…)
- A running stream with a now-playing endpoint — Icecast, AzuraCast, Shoutcast, or any JSON feed
- (optional) a free Genius API token for album art and lyrics
Quick start
git clone https://git.dou.bet/iamdoubz/radio.git
cd radio
cp .env.example .env
# edit .env — at minimum set SITE_NAME, STREAM_URL, and NOWPLAYING_URL
php -S localhost:8000 # for a quick local test
Then open http://localhost:8000. For production, serve the folder with nginx/Apache and PHP-FPM (see the example config near the bottom).
Configuration
Everything lives in .env — see .env.example for the fully
documented list. The main groups are:
- Branding & metadata —
SITE_NAME,SITE_DESCRIPTION,SITE_KEYWORDS,SITE_URL,OG_IMAGE. - Stream —
STREAM_URL(what the browser plays) andSTREAM_TYPE. - Now playing —
NOWPLAYING_PROVIDER,NOWPLAYING_URL, and provider-specific keys (below). - Artwork & lyrics —
ARTWORK_SOURCE,LYRICS_ENABLED,GENIUS_TOKEN. - Theme & colors —
THEME_BG,THEME_ACCENT,THEME_TEXT,THEME_FONT, … (drive CSS variables directly).
Now-playing providers
Set NOWPLAYING_PROVIDER and NOWPLAYING_URL:
| Provider | NOWPLAYING_URL example |
Notes |
|---|---|---|
icecast |
http://127.0.0.1:8000/status-json.xsl |
Set ICECAST_MOUNT if you run several mounts. Splits the stream title on TITLE_DELIMITER. |
azuracast |
https://azura.example.com/api/nowplaying/your_station |
Richest option — provides artist, title, album, art, and listeners natively (no Genius needed). |
shoutcast |
http://127.0.0.1:8000/stats?json=1 |
Splits the stream title on TITLE_DELIMITER. |
custom |
any URL returning JSON | Map fields with CUSTOM_ARTIST_PATH, CUSTOM_TITLE_PATH, CUSTOM_ART_PATH, etc. (dot notation, e.g. data.track.0.artist). |
TITLE_DELIMITER is how a combined Artist – Title string is split. The
original dou.bet station uses " <-|-> "; most setups use " - ".
Use TITLE_CLEANUP to strip junk from titles (e.g. .mp3, [Explicit],
(Remastered)). Separate entries with ||; set CLEANUP_IS_REGEX=true to
treat each entry as a case-insensitive regex.
Artwork & lyrics
ARTWORK_SOURCE controls where cover art comes from:
auto— use the art your provider supplies (e.g. AzuraCast); otherwise look it up on Genius; otherwise show the placeholder.provider— only ever use provider art.genius— always look art up on Genius.none— always showFALLBACK_ART.
If Genius has no cover, set LASTFM_API_KEY to fall back to Last.fm artwork.
(ARTWORK_SOURCE=lastfm uses Last.fm only; auto tries Genius then Last.fm.)
Lyrics require LYRICS_ENABLED=true and a Genius token. Lyrics are scraped
from the public Genius song page and returned as plain text; if Genius changes
their page layout, api/lyrics.php may need a tweak. Set
LYRICS_PREFER_ENGLISH=true to prefer the first English-language match when a
song has several Genius entries.
Player & appearance
PLAYER_STYLE—rich(custom controls, default) orsimple(native<audio controls>).THEME_DYNAMIC_BG— recolor the page background to the album art's average color, flipping text light/dark for contrast. Needs CORS-readable art to sample; it fails gracefully to your theme colors otherwise.MEDIASESSION— expose track title/artist/album/art to OS and lock-screen media controls.PLAYER_AUTOPLAY— attempt autoplay (most browsers block audio autoplay until the user interacts).
For a live stream the seek bar is replaced by a LIVE badge and an elapsed counter, since seeking a live stream isn't meaningful.
Offline fallback
When the now-playing provider reports the stream is offline, the player shows a
distinct placeholder (OFFLINE_ART, default assets/img/offline.svg) and an
"off air" label (OFFLINE_NAME / OFFLINE_MESSAGE), and the green LIVE dot
disappears. If you set OFFLINE_STREAM_URL to a local/hosted audio file, the
player switches to it (looped by default via OFFLINE_LOOP) so pressing play
gives listeners a "we'll be right back" bed instead of silence. When the live
stream returns, the player switches back automatically — without interrupting a
listener who is already playing.
You can hard-code artwork for specific tracks by creating config/overrides.json:
{ "artwork": [ { "artist": "Steve Reich", "art": "https://example.com/reich.jpg" } ] }
Theming
Colors and fonts are exposed as CSS variables generated from your .env, so you
can re-skin the site without editing CSS. For deeper layout changes edit
assets/css/radio.css, which uses those variables
(var(--accent), var(--bg), …).
Project structure
config.php # loads .env, applies defaults, exposes cfg()/config()
index.php # the page
partials/head.php # <head>: meta, SEO, theme variables, analytics
partials/noscript.php # no-JavaScript fallback
api/_bootstrap.php # shared helpers (config, HTTP, JSON)
api/nowplaying.php # normalizes Icecast/AzuraCast/Shoutcast/custom
api/artwork.php # optional Genius album-art lookup
api/lyrics.php # optional Genius lyrics scraper
assets/css/radio.css # themeable styles
assets/js/player.js # vanilla-JS player (no jQuery)
assets/img/ # placeholder art
.env.example # every setting, documented
nginx + PHP-FPM example
server {
server_name radio.example.com;
root /var/www/radio;
index index.php;
# Proxy the audio stream from Icecast (keeps Icecast off the public net).
location = /stream {
proxy_pass http://127.0.0.1:8000/your_mount;
proxy_set_header Host $http_host;
}
location / { try_files $uri $uri/ /index.php$is_args$args; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
With the server-side api/nowplaying.php proxy you no longer need the old
status-json.xsl / peers.xsl nginx location blocks — PHP fetches the backend
directly via NOWPLAYING_URL.
Security notes
.envis gitignored; keep your Genius token there, never in committed code.- Set
SHOW_ERRORS=falseandDEBUG=falsein production. - The lyrics/artwork endpoints echo back only sanitized data (plain-text lyrics, provider URLs).
Credits
Broadcast with Icecast and Mixxx; artwork and lyrics via the Genius API. Licensed under the terms in LICENSE.