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.
If api/nowplaying.php is unreachable or returns invalid JSON, the player also
treats that as offline (rather than freezing on the initial state). To preview
the offline state without stopping your stream, append ?offline=1 to the URL
(e.g. https://radio.example.com/?offline=1) and press play.
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.