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

8.6 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.

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.