Table of Contents
- Architecture
- High-level component map
- The five load-bearing decisions
- 1. Per-user state in wp_usermeta
- 2. HTML5 <audio> element + one vanilla-JS controller
- 3. Stations as a static PHP registry
- 4. Pop-out window for continuous background play
- 5. The 4-button search-link affordance
- File-by-file walkthrough
- Permission profile (deliberately narrow)
Architecture
RangerHQ Radio is hand-rolled PHP + a single vanilla-JS controller file. No Composer, no npm, no build step. The whole plugin is ~7 PHP files plus one stylesheet and one JS file.
High-level component map
wp-admin
┌────────────────────────────────────────────────────────────────┐
│ WP Dashboard │
│ ┌──────────────────────────────────┐ │
│ │ Radio Dashboard Widget │ inc/dashboard-widget.php│
│ │ - station picker (44 channels) │ │
│ │ - Play / Pause / Volume │ │
│ │ - now-playing metadata │ │
│ └──────────────────────────────────┘ │
│ │
│ Sidebar: 📻 Radio ▾ │
│ ├─ Player inc/admin-page.php (full-page player) │
│ ├─ History inc/history.php (tracks + 4-button search) │
│ ├─ Settings inc/settings.php │
│ └─ About inc/about.php │
│ │
│ + optional Pop-out window │
│ admin-post.php?action=radio_popout (chromeless) │
└────────────────────────────────────────────────────────────────┘
│
│ HTML5 <audio> + JS controller (radio.js)
▼
┌────────────────────────────────────────────────────────────────┐
│ SomaFM stream URL (e.g. ice1.somafm.com/groovesalad-128-mp3) │
│ │
│ AND: songs/<id>.json polled every 25s for track metadata │
└────────────────────────────────────────────────────────────────┘
│
│ on metadata change → AJAX log
▼
┌────────────────────────────────────────────────────────────────┐
│ wp-admin/admin-ajax.php?action=radio_log_track │
│ → inc/history.php → radio_log_track() │
│ → wp_usermeta key 'radio_history' (FIFO, capped at 500) │
└────────────────────────────────────────────────────────────────┘
Per-user storage in wp_usermeta:
┌────────────────────────────────────────────────────────────────┐
│ radio_state — current station + volume │
│ radio_history — FIFO 500 cap, dedup against last entry │
│ radio_favourites — uncapped, user-starred tracks │
└────────────────────────────────────────────────────────────────┘
The five load-bearing decisions
1. Per-user state in wp_usermeta
Each WP admin user gets their own player state under three separate wp_usermeta keys:
radio_state— current station ID + volume preferenceradio_history— FIFO array of heard tracks, capped at 500radio_favourites— uncapped array of starred tracks
Three keys (not one big blob) because track logging is frequent — it would be wasteful to rewrite the whole state every 25 seconds when only history changes. Separating high-write data from rarely-updated state is a small but real performance win.
2. HTML5 <audio> element + one vanilla-JS controller
assets/js/radio.js is the single source of truth for the player. It owns the <audio> element, listens to play / pause / error / timeupdate events, polls SomaFM's metadata endpoint every 25 seconds while playing, and broadcasts state changes back to the UI.
No JavaScript framework. No React, no Vue, no jQuery, no Alpine. ~600 lines of plain ES5 (for max WordPress browser compatibility — WP supports old browsers).
3. Stations as a static PHP registry
inc/stations.php contains a flat array of 44 stations, each with: id, name, genre, stream_url, description, art_url, songs_url (the SomaFM metadata endpoint). See Stations for the full catalogue.
Why static (not dynamically fetched from SomaFM's channels.json)? Three reasons:
- Curation — David picked the best 44, not all ~80 SomaFM channels
- Reliability — no runtime dependency on SomaFM's catalogue endpoint being reachable
- Performance — zero outbound API calls just to render the station list
The trade-off: when SomaFM adds a new channel or retires one, the plugin needs a manual update. That's a deliberate maintenance choice — see Stations for the "how to add a station" walkthrough.
4. Pop-out window for continuous background play
The pop-out is a chromeless WordPress admin page rendered at admin-post.php?action=radio_popout. It uses the SAME radio.js controller and the SAME audio element, but in a separate browser window so the player keeps going when you navigate the main admin to a different page.
The pop-out body has the class radio-popout so the CSS can apply popup-specific overrides (smaller header, no sidebar, compressed layout).
5. The 4-button search-link affordance
The headline interactivity feature. See The 4 Buttons for the full explanation. The URL builder lives in inc/history.php as radio_search_urls( $artist, $title ):
return array(
'spotify' => 'https://open.spotify.com/search/' . $enc,
'youtube' => 'https://www.youtube.com/results?search_query=' . $enc,
'apple' => 'https://music.apple.com/search?term=' . $enc,
'bandcamp' => 'https://bandcamp.com/search?q=' . $enc,
);
This pattern is mirrored byte-for-byte in RangerHQ Tuner's src/lib/history.js — see Family.
File-by-file walkthrough
If you've cloned the repo and want to understand the flow, read these files in order:
radio.php— the entry point WordPress sees. Plugin header, admin-menu, asset enqueue, AJAX endpoint registration, pop-out renderer.inc/stations.php— the 44-station catalogue + genre groupings + lookup helpers.inc/state.php—radio_get_state(),radio_update_state(), defaults.inc/history.php—radio_log_track(),radio_get_history(),radio_get_favourites(),radio_toggle_favourite(),radio_clear_history_all(),radio_search_urls(). The data layer for history + favourites + 4-button URLs.assets/js/radio.js— the audio controller, the metadata poller, the AJAX track-log dispatcher.inc/dashboard-widget.php— registers the WP Dashboard widget.inc/admin-page.php— the full Radio page.inc/history.php(page render) — the History admin page with the 4-button search row per track.
Total reading time end-to-end: about 20 minutes.
Permission profile (deliberately narrow)
Radio is a wp-admin-only plugin. It:
- ❌ Does NOT register front-end shortcodes
- ❌ Does NOT enqueue assets on the public site
- ❌ Does NOT create custom post types, taxonomies, or REST API endpoints (only one
admin-ajaxendpoint for track logging) - ❌ Does NOT contact any service other than SomaFM (no telemetry, no analytics)
- ✅ Only loads its CSS + JS on the radio admin pages + the WP Dashboard
- ✅ Stores everything in
wp_usermetaon the user's own WP site