Files
radio/README.md
T
2026-07-24 12:18:20 -05:00

8.9 KiB
Raw Blame History

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) and STREAM_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 show FALLBACK_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) or simple (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

  • .env is gitignored; keep your Genius token there, never in committed code.
  • Set SHOW_ERRORS=false and DEBUG=false in 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.