Clone
1
Architecture
David Keane edited this page 2026-06-09 02:32:38 +01:00

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 preference
  • radio_history — FIFO array of heard tracks, capped at 500
  • radio_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).

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:

  1. radio.php — the entry point WordPress sees. Plugin header, admin-menu, asset enqueue, AJAX endpoint registration, pop-out renderer.
  2. inc/stations.php — the 44-station catalogue + genre groupings + lookup helpers.
  3. inc/state.phpradio_get_state(), radio_update_state(), defaults.
  4. inc/history.phpradio_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.
  5. assets/js/radio.js — the audio controller, the metadata poller, the AJAX track-log dispatcher.
  6. inc/dashboard-widget.php — registers the WP Dashboard widget.
  7. inc/admin-page.php — the full Radio page.
  8. 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-ajax endpoint 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_usermeta on the user's own WP site