wiki: initial 6 pages for the oldest RangerHQ family sibling
Mirrors the rangerhq-tuner + rangerhq-buddy wiki pattern. Radio is the project where the four-button-search affordance and the per-user wp_usermeta storage pattern were both pioneered, so the wiki explicitly traces the lineage to the rest of the family. - Home: 44 SomaFM stations, v0.7.6, status table, source layout - Architecture: per-user wp_usermeta (3 keys), HTML5 audio + vanilla JS controller, hand-curated stations registry, pop-out window via admin-post.php?action=radio_popout - Stations: why curated not dynamic, 10 genres, station entry shape, how to add a station, how to retire one - The 4 Buttons: dedicated page explaining the Spotify/YouTube/Apple/ Bandcamp public-URL pattern (this is where it was born), why link-out beats embed - Privacy: per-user storage inventory, 'only somafm.com' outbound audit, GDPR notes, three-option wipe runbook - Family: positions Radio as the OLDEST sibling, traces the lineage byte-for-byte to Tuner's history.js, names Buddy as a different product that inherits the same architectural ethos
+126
@@ -0,0 +1,126 @@
|
||||
# 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).
|
||||
|
||||
### 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 )`:
|
||||
|
||||
```php
|
||||
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](https://git.davidtkeane.com/ranger/rangerhq-tuner) `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.php`** — `radio_get_state()`, `radio_update_state()`, defaults.
|
||||
4. **`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.
|
||||
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
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# FAQ
|
||||
|
||||
## Why SomaFM specifically?
|
||||
|
||||
SomaFM is a listener-supported independent radio network with ~80 always-on channels across nearly every genre — ambient, electronic, lounge, jazz, indie, metal, world, holiday, seasonal specials. Their content is publicly streamable, their endpoints (`channels.json`, per-station `songs/{id}.json`) are public and well-documented, and their community guidelines explicitly welcome third-party tools that play their streams. No other free indie radio network ticks all of those boxes.
|
||||
|
||||
## Why only 44 of the ~80 SomaFM channels?
|
||||
|
||||
Quality filter. David picked the 44 he actually listens to. The rest are either too niche, too seasonal (some of the holiday channels overlap), or not a great fit for a wp-admin background-listening context. See [[Stations]] for the full curated list and the "how to add a station" walkthrough if you want to extend it.
|
||||
|
||||
## Does the plugin send any data off my WordPress site?
|
||||
|
||||
No, beyond fetching the SomaFM audio stream and the public now-playing track metadata. No telemetry, no analytics, no error reporting, no font/image CDNs. The plugin makes zero `wp_remote_*` calls. The four search-link buttons (Spotify / YouTube / Apple / Bandcamp) are plain HTML `<a href>` links that don't fire until you click them. See [[Privacy]] for the full audit.
|
||||
|
||||
## Will it work if my WordPress server is behind a firewall?
|
||||
|
||||
The plugin itself runs entirely server-side and client-side on the admin's browser. The audio + metadata fetches happen in the admin's browser, not the server. As long as the admin's browser can reach `somafm.com`, the plugin works. Your WordPress server doesn't need any outbound access for RangerHQ Radio.
|
||||
|
||||
## Why does the plugin play in the WordPress admin and not on the public site?
|
||||
|
||||
Because it's for admin use. Front-end audio players are a different category of plugin (and usually require shortcodes, theme integration, premium features, etc.). RangerHQ Radio is for the WordPress admin who wants background music while they work in their admin panel — a niche that wasn't well served.
|
||||
|
||||
## What's the pop-out window for?
|
||||
|
||||
When you navigate to a different admin page (e.g. Posts → All Posts → Edit), the dashboard widget's audio context is destroyed because the page reloads. The pop-out opens a separate chromeless WP admin window that ONLY contains the player — that window stays open and keeps the audio going while you navigate the main admin freely.
|
||||
|
||||
Open it from the Player page → "Pop out" button. The URL is `wp-admin/admin-post.php?action=radio_popout`.
|
||||
|
||||
## Can I have more than one user listening on the same site?
|
||||
|
||||
Yes — RangerHQ Radio is fully per-user. Each admin has their own `radio_state`, `radio_history`, `radio_favourites`. Multi-admin sites work perfectly.
|
||||
|
||||
## Why isn't the station list bigger? Other plugins have thousands of stations.
|
||||
|
||||
By design. See [[Stations]] — curation is a feature, not a bug. If you want a directory-style "every radio station on the internet" experience, look at plugins that wrap Internet-Radio.com or Shoutcast's directory. RangerHQ Radio is "44 stations David trusts" — narrower, more reliable, lighter weight.
|
||||
|
||||
If you want a 30,000-station discovery experience, the [[Family|RangerHQ Tuner]] Chrome extension is the better fit for that — its architecture has an extensibility seam designed for multiple radio-network adapters.
|
||||
|
||||
## What about Spotify / Apple Music / YouTube? Why aren't those built-in?
|
||||
|
||||
They're built-in as **search-link buttons** on every history track, not as embedded players. Embedding the actual playback would require OAuth / Premium-account gating / public API keys / privacy reset — see [[The 4 Buttons]] for the full explanation. Link-out + search is the right architecture for "find this track on another service."
|
||||
|
||||
## How big can the history grow?
|
||||
|
||||
Capped at 500 entries per user. The cap is a constant (`RADIO_HISTORY_CAP`) in `inc/history.php` — change it there if you really want more, but 500 is plenty for most listening patterns and keeps the `wp_usermeta` row reasonably small.
|
||||
|
||||
## Does it work on multisite?
|
||||
|
||||
Yes. Each user's `radio_state` / `radio_history` / `radio_favourites` are per-user, not per-blog. If you have multiple sites, your radio state is the same across all of them.
|
||||
|
||||
## Is the source code open?
|
||||
|
||||
Yes. GPL v2 or later. Full source at <https://git.davidtkeane.com/ranger/rangerhq-radio>. Inspect every line.
|
||||
|
||||
## Where do I file a bug or feature request?
|
||||
|
||||
Open an issue at <https://git.davidtkeane.com/ranger/rangerhq-radio/issues> or email <david@davidtkeane.com>.
|
||||
|
||||
## Who made this?
|
||||
|
||||
David Keane, Dublin, Ireland. <david@davidtkeane.com> · <https://davidtkeane.com>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# Family
|
||||
|
||||
RangerHQ Radio is the **oldest sibling** in the RangerHQ family. The four-button-search affordance and the per-user-`wp_usermeta` storage pattern were both pioneered here, then ported to other family members.
|
||||
|
||||
## Current family members
|
||||
|
||||
### RangerHQ Radio — this project
|
||||
|
||||
- **Surface:** WordPress admin
|
||||
- **Function:** SomaFM player with track history + favourites + four-button search to Spotify / YouTube / Apple / Bandcamp + optional pop-out window
|
||||
- **Repo:** [git.davidtkeane.com/ranger/rangerhq-radio](https://git.davidtkeane.com/ranger/rangerhq-radio)
|
||||
- **Distribution:** [WordPress.org Plugin Directory](https://wordpress.org/plugins/rangerhq-radio/) (LIVE)
|
||||
|
||||
### RangerHQ Buddy — WordPress sibling
|
||||
|
||||
- **Surface:** WordPress admin
|
||||
- **Function:** A small virtual pet for every WP admin user; Phase E will tie its mood to your site's actual health
|
||||
- **Repo:** [git.davidtkeane.com/ranger/rangerhq-buddy](https://git.davidtkeane.com/ranger/rangerhq-buddy)
|
||||
- **Distribution:** WordPress.org Plugin Directory (in round-2 review at time of writing)
|
||||
|
||||
Buddy inherits Radio's `wp_usermeta` storage pattern and Tier-1 admin discipline. Different product (virtual pet, not radio) — same architectural ethos.
|
||||
|
||||
### RangerHQ Tuner — Chrome sibling
|
||||
|
||||
- **Surface:** Chrome browser (toolbar popup + optional New Tab Page override)
|
||||
- **Function:** Same idea as Radio, but in the browser instead of wp-admin. Plays SomaFM, logs tracks, four-button search out to the major services
|
||||
- **Repo:** [git.davidtkeane.com/ranger/rangerhq-tuner](https://git.davidtkeane.com/ranger/rangerhq-tuner)
|
||||
- **Distribution:** Chrome Web Store (in review at time of writing)
|
||||
|
||||
Tuner is **directly ported from Radio**:
|
||||
|
||||
- The four-URL templates in `src/lib/history.js` are byte-for-byte identical to `radio_search_urls()` in `inc/history.php` here.
|
||||
- The FIFO-500-with-dedup-against-last-entry rule is the same.
|
||||
- The `(unknown)` artist skip is the same.
|
||||
- The Stations / History / Favourites tab structure on Tuner's New Tab Page mirrors the History tab here.
|
||||
|
||||
The architectural translation from PHP `wp_usermeta` to JavaScript `chrome.storage.local` happened cleanly because Radio's data layer was small and self-contained.
|
||||
|
||||
## Coming next
|
||||
|
||||
### RangerHQ Logbook
|
||||
|
||||
- **Surface:** WordPress admin
|
||||
- **Function:** Site-changes audit log for WordPress administrators. Captures who did what when across plugins, themes, options, users — searchable + exportable
|
||||
- **Status:** In design
|
||||
|
||||
## Family-wide promises (the "RangerHQ promise")
|
||||
|
||||
Every plugin or extension in the RangerHQ family commits to:
|
||||
|
||||
- ✅ **GPL v2 or later** — open source, audit-able, fork-able
|
||||
- ✅ **No telemetry, ever** — nothing leaves your site / browser / device
|
||||
- ✅ **No third-party JavaScript SDKs** — the code you install is the code that runs
|
||||
- ✅ **No CDN-fetched fonts or images** — assets are bundled, period
|
||||
- ✅ **Narrow permissions** — request only what's needed, justify the rest
|
||||
- ✅ **Per-user / per-device privacy** — no cross-account leakage, no shared site-wide state where per-user makes more sense
|
||||
- ✅ **Hand-rolled, no build step where possible** — vanilla PHP on WordPress, vanilla JS in the browser
|
||||
- ✅ **Tier-1 admin discipline** — single h1 per admin page, escaped output everywhere, accessibility-respecting, no inline `<style>` blocks
|
||||
- ✅ **Keep-a-Changelog format** — every release documented at the file-level detail
|
||||
|
||||
If a plugin or extension can't meet all of these, it doesn't ship under the RangerHQ name.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Logo:** the RangerHQ helmet (the gladiator / Spartan-style silhouette)
|
||||
- **Palette:** dark base `#0f1411`, accent green `#6dbf7a` (RangerHQ green), cream `#f4e9b7` (highlights)
|
||||
- **Typography:** system fonts, no web fonts
|
||||
- **Voice:** matter-of-fact, "what it does today" + "what it doesn't do yet"
|
||||
- **Maker:** [David Keane](https://davidtkeane.com), Dublin, Ireland
|
||||
|
||||
## The lineage in one paragraph
|
||||
|
||||
RangerHQ Radio was the first plugin in the family. The four-button-search pattern emerged because David wanted a way to save tracks he heard on SomaFM, without embedding the auth complexity of Spotify or YouTube. Once the pattern proved itself on wp.org (Radio went live 2026-06-04), it was ported byte-for-byte to **RangerHQ Tuner** (Chrome) so the same idea would work in the browser. **RangerHQ Buddy** is a different product (virtual pet) but inherits the per-user-`wp_usermeta` storage pattern and the wp.org submission discipline from Radio's experience.
|
||||
|
||||
Each new family member ships faster than the last, because the architectural patterns are already proven and the wp.org / Chrome Web Store submission playbook is already written.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# RangerHQ Radio Wiki
|
||||
|
||||
**A small, focused internet radio player for your WordPress admin. 44 hand-curated SomaFM stations, per-user history + favourites, four-click search to Spotify / YouTube / Apple Music / Bandcamp, optional pop-out window for continuous background play.**
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Latest tag** | [v0.7.6](https://git.davidtkeane.com/ranger/rangerhq-radio/src/tag/v0.7.6) |
|
||||
| **wp.org status** | LIVE — [wordpress.org/plugins/rangerhq-radio](https://wordpress.org/plugins/rangerhq-radio/) |
|
||||
| **Licence** | GPL v2 or later |
|
||||
| **Tested up to** | WordPress 7.0 |
|
||||
| **Requires PHP** | 7.4 |
|
||||
| **Landing page** | https://davidtkeane.com/rangerhq-radio |
|
||||
| **Family** | Older sibling to [RangerHQ Buddy](https://git.davidtkeane.com/ranger/rangerhq-buddy) (WP) and [RangerHQ Tuner](https://git.davidtkeane.com/ranger/rangerhq-tuner) (Chrome) |
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
RangerHQ Radio gives every WordPress admin user a small audio player that lives in the WP admin sidebar. Pick a SomaFM station from the [[Stations]] catalogue (44 curated channels across ambient, electronic, lounge, rock, metal, jazz, world, reggae, holiday, specials), hit Play, music streams via HTML5 audio. Each user gets their own station memory, volume preference, track history, and favourites — all per-user in `wp_usermeta`, nothing shared between accounts.
|
||||
|
||||
The 4-button search affordance — **Spotify / YouTube / Apple Music / Bandcamp** — was born here. Every track in your history has the four service buttons next to it; clicking opens that service's public search results in a new tab so you can find the track on whichever music service you actually use. No accounts, no API keys, no SDK embedded in the plugin.
|
||||
|
||||
There's also a **pop-out window** mode for continuous background play — opens a chromeless WP-admin-rendered window so the player keeps going even when you navigate away from admin pages.
|
||||
|
||||
## Quick links
|
||||
|
||||
- [[Architecture]] — how the per-user `radio_state`, HTML5 audio, station registry, history layer, and pop-out window all fit together
|
||||
- [[Stations]] — the 44 curated SomaFM channels and how the genre grouping works
|
||||
- [[The 4 Buttons]] — the Spotify / YouTube / Apple / Bandcamp pattern explained (and why it's the right architecture for "find this track elsewhere")
|
||||
- [[Privacy]] — what's stored (locally on your site, per-user), what's not
|
||||
- [[FAQ]] — common questions
|
||||
- [[Family]] — sibling projects in the RangerHQ family
|
||||
|
||||
## Install
|
||||
|
||||
### From WordPress.org
|
||||
|
||||
Search for **RangerHQ Radio** in `Plugins → Add New` → Install → Activate. Or visit [wordpress.org/plugins/rangerhq-radio](https://wordpress.org/plugins/rangerhq-radio/).
|
||||
|
||||
### From source (developer mode)
|
||||
|
||||
```bash
|
||||
git clone https://git.davidtkeane.com/ranger/rangerhq-radio.git
|
||||
```
|
||||
|
||||
Drop the folder into `wp-content/plugins/` and activate via the WordPress admin.
|
||||
|
||||
## Source layout
|
||||
|
||||
```
|
||||
rangerhq-radio/
|
||||
├── radio.php # plugin header + menu + asset enqueue + pop-out renderer
|
||||
├── LICENSE # GPL v2 or later
|
||||
├── README.md
|
||||
├── CHANGELOG.md # Keep a Changelog format
|
||||
├── readme.txt # wp.org-format readme
|
||||
├── screenshot-1.png … 5.png # wp.org listing screenshots
|
||||
├── inc/
|
||||
│ ├── state.php # per-user state CRUD (wp_usermeta)
|
||||
│ ├── stations.php # 44 hand-curated SomaFM stations + genre groupings
|
||||
│ ├── history.php # track logging, favourites, 4-button search URLs
|
||||
│ ├── dashboard-widget.php # the player on WP Dashboard
|
||||
│ ├── admin-page.php # dedicated "Radio" page
|
||||
│ ├── settings.php # Settings page
|
||||
│ └── about.php # About page
|
||||
└── assets/
|
||||
├── css/radio.css # all visual styling
|
||||
└── js/radio.js # HTML5 audio controller + history logger + UI
|
||||
```
|
||||
|
||||
## Reporting issues / suggestions
|
||||
|
||||
Open an [issue](https://git.davidtkeane.com/ranger/rangerhq-radio/issues) or email <david@davidtkeane.com>.
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# Privacy
|
||||
|
||||
## TL;DR
|
||||
|
||||
**RangerHQ Radio stores everything per-user on your own WordPress site. The only external service contacted is SomaFM, for the audio stream + the public now-playing metadata. Nothing else, nowhere else.**
|
||||
|
||||
## What is stored on your WordPress site
|
||||
|
||||
In `wp_usermeta`, **one row per admin user** under three meta keys:
|
||||
|
||||
| Key | Shape | Notes |
|
||||
|---|---|---|
|
||||
| `radio_state` | `{ station_id, volume }` | Last picked station + last volume |
|
||||
| `radio_history` | Array of `{ artist, title, station, station_id, at }` | FIFO, capped at 500 entries |
|
||||
| `radio_favourites` | Array of `{ artist, title, station, station_id, at }` | Uncapped, user-starred tracks |
|
||||
|
||||
**Per-user, multi-admin-friendly:** Alice's history is independent of Bob's. WordPress's user-meta API enforces this.
|
||||
|
||||
## What is NOT stored
|
||||
|
||||
- No IP addresses
|
||||
- No browser fingerprints
|
||||
- No session IDs beyond what WordPress already manages
|
||||
- No page-view counters
|
||||
- No interaction logs (every play / pause / skip)
|
||||
- No tracking cookies set by the plugin
|
||||
- No analytics of any kind
|
||||
|
||||
## Outbound network requests
|
||||
|
||||
The plugin contacts only **`somafm.com`** and SomaFM's stream subdomains:
|
||||
|
||||
| Endpoint | When | Why |
|
||||
|---|---|---|
|
||||
| Direct stream URLs (e.g. `https://ice1.somafm.com/groovesalad-128-mp3`) | Continuous while playing | The actual audio |
|
||||
| `https://somafm.com/songs/<channel-id>.json` | Every 25 seconds while playing | Public now-playing track metadata |
|
||||
|
||||
That's it. No other server is contacted by RangerHQ Radio. No telemetry, no analytics, no error reporting, no font CDN, no image CDN (station artwork is served by SomaFM directly when you view their site, but the player itself uses static URLs).
|
||||
|
||||
Your WordPress server's IP is naturally observable by SomaFM as part of routine HTTP serving — exactly the same as if you visited <https://somafm.com> in a browser tab. Nothing identifying the WordPress installation is sent.
|
||||
|
||||
Verify the audit yourself:
|
||||
|
||||
```bash
|
||||
grep -rn "wp_remote_\|file_get_contents.*http\|curl_init" inc/ radio.php
|
||||
grep -rn "fetch(\|XMLHttpRequest" assets/js/
|
||||
```
|
||||
|
||||
You'll find `wp_remote_*` is not used at all (the audio + metadata is fetched directly by the browser's HTML5 audio element + a JS `fetch()` in `radio.js`, both client-side). The JS makes two kinds of requests: SomaFM's metadata endpoint, and your own WordPress's `admin-ajax.php` for track logging.
|
||||
|
||||
## The 4-button search links
|
||||
|
||||
Every track in your History tab has four buttons: **Spotify**, **YouTube**, **Apple Music**, **Bandcamp**. Each is a plain HTML `<a href>` link that opens that service's public search results page in a new browser tab. See [[The 4 Buttons]] for the full explanation.
|
||||
|
||||
**RangerHQ Radio does not embed any third-party SDK, player, tracker, or analytics code.** The extension does not communicate with Spotify, YouTube, Apple Music, or Bandcamp servers in any way. When you click one of the buttons, you are simply navigating to a public search URL. Anything that happens after that is between you, your browser, and the destination site.
|
||||
|
||||
## Multi-admin sites
|
||||
|
||||
Each WordPress user gets their own three `wp_usermeta` rows (`radio_state` / `radio_history` / `radio_favourites`). Alice's history is private to her; Bob can't see it; the WP-options-level "everyone shares one player" anti-pattern is avoided.
|
||||
|
||||
## How to wipe everything
|
||||
|
||||
Three options:
|
||||
|
||||
1. **The plugin's own "Clear history" button** on the History admin page — clears the user's `radio_history` row. Favourites preserved.
|
||||
|
||||
2. **Deactivate + delete the plugin** through WP admin. WordPress will leave the `wp_usermeta` rows in place by design (so reinstalling restores your data). To wipe them manually:
|
||||
|
||||
```sql
|
||||
DELETE FROM wp_usermeta
|
||||
WHERE meta_key IN ('radio_state', 'radio_history', 'radio_favourites');
|
||||
```
|
||||
|
||||
3. **Delete the user.** `wp_delete_user()` removes all `wp_usermeta` for that user. Your radio data goes with them.
|
||||
|
||||
## Children's privacy
|
||||
|
||||
RangerHQ Radio is a WordPress administrator-only feature. Visitors of the public site cannot access it. Admin access requires a WordPress account, which means the user is authenticated. No data is collected about anyone of any age.
|
||||
|
||||
## Compliance notes
|
||||
|
||||
- **GDPR:** No personal data is processed beyond what WordPress already stores. WordPress's existing GDPR tools (`wp_export_personal_data`, `wp_erase_personal_data`) pick up the three `radio_*` `usermeta` rows because they're standard usermeta.
|
||||
- **CCPA, LGPD, PIPEDA, etc.:** Same logic. No collection, no sharing, no third-party processors.
|
||||
|
||||
## Contact
|
||||
|
||||
- Email: <david@davidtkeane.com>
|
||||
- Issues / source: <https://git.davidtkeane.com/ranger/rangerhq-radio>
|
||||
- Author: David Keane, Dublin, Ireland
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Stations
|
||||
|
||||
RangerHQ Radio ships **44 hand-curated SomaFM stations** organised across **10 genres**. They are NOT fetched dynamically from SomaFM's `channels.json` — they are stored as a static PHP array in `inc/stations.php` and updated by hand when SomaFM adds or retires channels.
|
||||
|
||||
## Why hand-curated, not dynamic?
|
||||
|
||||
| Benefit | What it gets you |
|
||||
|---|---|
|
||||
| **Quality filter** | The catalogue is what David actually listens to. Not every SomaFM channel needs to be on the menu — some are seasonal, some are niche, some get retired. |
|
||||
| **Reliability** | The plugin works even if `channels.json` is unreachable. No runtime dependency on a third-party JSON endpoint just to render the station list. |
|
||||
| **Performance** | Zero outbound HTTP requests just to show the picker. Catalogue is in PHP memory, instantly available. |
|
||||
| **Predictability** | The reviewer (wp.org or Chrome Web Store) can audit the exact set of stations the plugin ships. No surprises from a dynamic catalogue that could change after review. |
|
||||
|
||||
The trade-off: the catalogue needs manual maintenance. SomaFM doesn't change much (they're a stable indie network), so this hasn't been a burden in practice.
|
||||
|
||||
## The 10 genres
|
||||
|
||||
| Genre | Examples |
|
||||
|---|---|
|
||||
| **Ambient** | Drone Zone, Deep Space One, Mission Control, Earwaves |
|
||||
| **Electronic** | Groove Salad, Space Station Soma, cliqhop idm, DEF CON Radio, Beat Blender |
|
||||
| **Lounge** | Secret Agent, Lush, Folk Forward |
|
||||
| **Rock** | Indie Pop Rocks!, BAGeL Radio, U80s |
|
||||
| **Metal** | Metal Detector, Doomed |
|
||||
| **Jazz** | Sonic Universe, Vaporwaves |
|
||||
| **World** | Suburbs of Goa, Illinois Street Lounge |
|
||||
| **Reggae** | Reggaedelica |
|
||||
| **Holiday** | Christmas Lounge, Christmas Rocks!, Jolly Ol' Soul, Xmas in Frisko |
|
||||
| **Specials** | Black Rock FM, Boot Liquor, Covers, Seven Inch Soul, Underground 80s, ThistleRadio |
|
||||
|
||||
(Examples — see `inc/stations.php` for the authoritative list of all 44.)
|
||||
|
||||
## The station entry shape
|
||||
|
||||
```php
|
||||
array(
|
||||
'id' => 'soma-groovesalad', // namespaced unique ID
|
||||
'name' => 'Groove Salad',
|
||||
'genre' => 'Electronic',
|
||||
'stream_url' => 'https://ice1.somafm.com/groovesalad-128-mp3',
|
||||
'description' => 'A nicely chilled plate of ambient/downtempo beats and grooves.',
|
||||
'art_url' => 'https://api.somafm.com/img/groovesalad400.jpg',
|
||||
'songs_url' => 'https://somafm.com/songs/groovesalad.json',
|
||||
),
|
||||
```
|
||||
|
||||
Every field is required. The `songs_url` is what `assets/js/radio.js` polls every 25 seconds for the "now playing" track metadata.
|
||||
|
||||
## Lookup helpers
|
||||
|
||||
`inc/stations.php` exposes three helpers:
|
||||
|
||||
| Function | What it returns |
|
||||
|---|---|
|
||||
| `radio_get_stations_flat()` | A flat array of all 44 station entries |
|
||||
| `radio_get_stations_grouped()` | A map of `genre => array of stations` for the genre-grouped UI |
|
||||
| `radio_find_station( $station_id )` | A single station entry by ID, or `null` |
|
||||
| `radio_default_station_id()` | The default station for new users (Groove Salad) |
|
||||
|
||||
## How to add a station
|
||||
|
||||
The station array is a static PHP array. Adding one is a five-line patch.
|
||||
|
||||
### Step 1 — verify the station on SomaFM
|
||||
|
||||
Check that the channel exists at the URL you'll use:
|
||||
|
||||
```
|
||||
https://somafm.com/<channel-shortname>/
|
||||
```
|
||||
|
||||
For example: <https://somafm.com/groovesalad/>. Confirm the `songs/<shortname>.json` endpoint also returns track metadata.
|
||||
|
||||
### Step 2 — add the entry to `inc/stations.php`
|
||||
|
||||
```php
|
||||
array(
|
||||
'id' => 'soma-<your-shortname>',
|
||||
'name' => 'Your Station Name',
|
||||
'genre' => 'Electronic', // pick from the 10 existing genres or add a new one
|
||||
'stream_url' => 'https://ice1.somafm.com/<shortname>-128-mp3',
|
||||
'description' => 'One-sentence blurb.',
|
||||
'art_url' => 'https://api.somafm.com/img/<shortname>400.jpg',
|
||||
'songs_url' => 'https://somafm.com/songs/<shortname>.json',
|
||||
),
|
||||
```
|
||||
|
||||
### Step 3 — bump the plugin version
|
||||
|
||||
`radio.php` header `Version:` + the `Stable tag:` in `readme.txt` + a `CHANGELOG.md` entry.
|
||||
|
||||
### Step 4 — test it
|
||||
|
||||
1. Reload the plugin in WP admin
|
||||
2. Visit the Radio page — your new station should appear under its genre
|
||||
3. Pick it → audio should play
|
||||
4. Wait 30 seconds → the History tab should log a track if the songs endpoint returned metadata
|
||||
5. The 4-button search row should appear next to the logged track
|
||||
|
||||
### Step 5 — tag the release on Gitea
|
||||
|
||||
```bash
|
||||
git tag -a v0.7.x -m "v0.7.x — add <station name>"
|
||||
git push origin v0.7.x
|
||||
```
|
||||
|
||||
Then upload the updated zip to wp.org SVN (same workflow as any other version bump).
|
||||
|
||||
## How to retire a station
|
||||
|
||||
If SomaFM kills a channel:
|
||||
|
||||
1. Remove the entry from `inc/stations.php`
|
||||
2. Bump the version
|
||||
3. Note in the CHANGELOG that the station was retired upstream
|
||||
4. Users with that station in their `radio_state` will fall back to the default on next load (`radio_get_state()` handles the missing-station case gracefully)
|
||||
|
||||
## What about non-SomaFM stations?
|
||||
|
||||
Today the plugin is **SomaFM-only by design**. The IDs are prefixed `soma-` and the architecture assumes the SomaFM metadata-endpoint shape (`songs/<id>.json`).
|
||||
|
||||
If you wanted to add another network (Internet-Radio.com, RadioGarden, Shoutcast directory), the cleanest path is to copy the [Tuner's adapter pattern](https://git.davidtkeane.com/ranger/rangerhq-tuner/wiki/Adding-a-Radio-Source) — split `inc/stations.php` into per-source files plus a registry. That's an architectural change, not a one-line addition.
|
||||
|
||||
## Want to suggest a station?
|
||||
|
||||
Open an issue at <https://git.davidtkeane.com/ranger/rangerhq-radio/issues>. Curation is intentional, so not every suggestion will land, but good additions are welcome.
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# The 4 Buttons (Spotify / YouTube / Apple Music / Bandcamp)
|
||||
|
||||
The four small buttons that appear next to every track in your **History** tab are the headline interactivity feature of RangerHQ Radio. **This page explains the pattern in detail because it's the pattern that the entire RangerHQ family inherits.**
|
||||
|
||||
## What they do
|
||||
|
||||
When SomaFM is playing and a song you like comes on, you usually want to:
|
||||
|
||||
- Save it to a Spotify playlist
|
||||
- Watch the music video on YouTube
|
||||
- Add it to your Apple Music library
|
||||
- Buy it on Bandcamp to actually support the artist
|
||||
|
||||
Each of those services has a **public search results URL** that takes an arbitrary query string. RangerHQ Radio puts four small buttons next to every history track that link to the corresponding service's public search URL with the artist + title pre-filled. Click one, the service opens in a new browser tab, results are right there.
|
||||
|
||||
That's it. No deeper integration. No accounts. No SDK.
|
||||
|
||||
## The exact URLs
|
||||
|
||||
```php
|
||||
function radio_search_urls( $artist, $title ) {
|
||||
$enc = rawurlencode( trim( $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,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
These are byte-for-byte the same URLs that [RangerHQ Tuner](https://git.davidtkeane.com/ranger/rangerhq-tuner) uses in `src/lib/history.js`. The pattern was born here, in the WordPress version, and ported to Chrome.
|
||||
|
||||
## Why "link out" rather than "play in-place"?
|
||||
|
||||
This is the architectural decision that earns every other property of the plugin. Embedding Spotify / YouTube / Apple Music players inside the plugin would require:
|
||||
|
||||
| Service | What integration would cost |
|
||||
|---|---|
|
||||
| **Spotify** | OAuth 2.0 + premium-account-gated Web Playback SDK + your-IP and listening-history flowing to Spotify even when you're not actively using them |
|
||||
| **YouTube** | Either the YouTube IFrame Player (which shows video, not just audio — different product) or the YouTube Data API (10,000 quota units/day free tier + an API key that becomes public in a WordPress plugin) |
|
||||
| **Apple Music** | MusicKit JS + a public API key + Apple-account auth + the user must have Apple Music to play anything |
|
||||
| **Bandcamp** | No public playback API exists. Their "embed player" widget per-album is the only path, requires per-track embeds, doesn't work for search results |
|
||||
|
||||
**Cost summary:** OAuth + premium gating + API keys leaking through the WordPress source + privacy reset (now we're touching your accounts on four services) + Plugin Check / Web Store review storms.
|
||||
|
||||
**The 4-button link-out pattern costs nothing.** Public search URLs are universally accessible, no auth, no API key, no quota, no SDK, no third-party JavaScript loaded into your wp-admin. The plugin stays under 50 KB. The reviewer can verify the URLs in five seconds.
|
||||
|
||||
## The trade-off you accept
|
||||
|
||||
You don't get one-click play. You get one-click *search*, and then YOU pick the result. That's a slightly worse UX for the case where the service's #1 result is exactly the track you wanted (which is most of the time).
|
||||
|
||||
But you get a **dramatically better** product on every other axis:
|
||||
- ✅ Works on a free Spotify / Apple Music / YouTube account
|
||||
- ✅ Works if you have NO account on those services
|
||||
- ✅ Doesn't ship your listening history to four music platforms
|
||||
- ✅ Doesn't break when Spotify deprecates a SDK version
|
||||
- ✅ Doesn't require WordPress admin to maintain API keys
|
||||
- ✅ Passes wp.org / Chrome Web Store review cleanly
|
||||
|
||||
For a free open-source plugin where the alternative is "don't ship it at all because of all the auth complexity," the 4-button pattern is the right call.
|
||||
|
||||
## Discoverability
|
||||
|
||||
When you read the plugin description on wp.org or the davidtkeane.com landing page, the 4-button affordance is what makes people install. Most WP plugins for "audio in admin" just play radio. RangerHQ Radio plays radio AND helps you find your favourites elsewhere — that's the differentiator.
|
||||
|
||||
## Where this pattern came from
|
||||
|
||||
The pattern was already common in dedicated music-discovery sites (last.fm, hypem.com, etc.) that link out to streaming services. RangerHQ Radio adapted it to "you heard this on the radio I'm playing in your wp-admin — here's where to find it later." Same idea, different surface.
|
||||
|
||||
## Where this pattern went next
|
||||
|
||||
**RangerHQ Tuner** (the Chrome browser sibling) ported the exact same URL builder + the exact same 4-button UI row to a JavaScript module. Look at `src/lib/history.js` in the tuner repo — `searchUrls()` is the same function. The two implementations evolve together.
|
||||
|
||||
When future RangerHQ family members need this affordance (e.g. a hypothetical RangerHQ Discord bot that posts "Now playing on the WP admin" and provides search links), they should mirror the same URL templates from this page. **One canonical source of truth, ported as needed.**
|
||||
|
||||
## Customising the buttons
|
||||
|
||||
For now, the four services are hard-coded in `radio_search_urls()`. There's no admin UI to add/remove them.
|
||||
|
||||
If you'd like to:
|
||||
- **Remove a service** (e.g. you don't want Apple Music): Edit `inc/history.php` and the rendering loop in `radio_render_history_page()` to drop that key.
|
||||
- **Add a service** (e.g. Tidal, Deezer, Discogs, Genius): Add a new URL template to `radio_search_urls()` and a new link in the rendering loop. Pull requests welcome — see [Reporting issues](https://git.davidtkeane.com/ranger/rangerhq-radio/issues).
|
||||
|
||||
## See also
|
||||
|
||||
- [[Architecture]] — where this fits in the rest of the plugin
|
||||
- [[Privacy]] — what the buttons do and don't send
|
||||
- [[Family]] — the same pattern in RangerHQ Tuner
|
||||
Reference in New Issue
Block a user