Clone
1
Adding a Radio Source
David Keane edited this page 2026-06-09 01:30:32 +01:00

Adding a Radio Source

RangerHQ Tuner is architected so adding a new radio network requires writing exactly one new file in src/sources/ and adding one line to the registry. The popup, the New Tab Page, the offscreen audio host, and the storage layer all stay untouched.

The contract — RadioSource interface

Every adapter must conform to the JSDoc interface in src/sources/base-source.js:

/**
 * @typedef {Object} Station
 * @property {string}   id           Namespaced identifier, e.g. "somafm:groovesalad"
 * @property {string}   sourceId     The network adapter id, e.g. "somafm"
 * @property {string}   name         Display name shown in the popup
 * @property {string}   description  Short blurb
 * @property {string}   genre        Comma-separated genres or single tag
 * @property {string}   artworkUrl   Square artwork URL (or empty string)
 * @property {string[]} streamUrls   Resolved direct stream URLs, ordered by preference
 */

/**
 * @typedef {Object} NowPlaying
 * @property {string}  title
 * @property {string}  artist
 * @property {string} [album]
 */

/**
 * @typedef {Object} RadioSource
 * @property {string} id           Adapter id, e.g. "somafm"
 * @property {string} displayName  Human-readable network name
 * @property {() => Promise<Station[]>}                     listStations
 * @property {(station: Station) => Promise<NowPlaying|null>} getNowPlaying
 * @property {(station: Station) => Promise<string>}        resolveStreamUrl
 */

The contract is locked as of v0.1.0. Adding fields is fine; renaming or removing is not — it would break every existing adapter.

Step-by-step: add a new network

Let's pretend we're adding Radio Garden (radio.garden) as a second source.

Step 1 — write src/sources/radio-garden.js

// src/sources/radio-garden.js
//
// Radio Garden — globe-rotating discovery of indie radio stations
// from anywhere on Earth. Public API, no auth required.

/** @type {import('./base-source.js').RadioSource} */
export const radioGarden = {
  id: 'radio-garden',
  displayName: 'Radio Garden',

  async listStations() {
    const res = await fetch('https://radio.garden/api/...');
    if (!res.ok) throw new Error(`Radio Garden HTTP ${res.status}`);
    const data = await res.json();

    return data.stations.map(s => ({
      id:          `radio-garden:${s.id}`,           // <-- namespaced id, important
      sourceId:    'radio-garden',
      name:        s.title,
      description: s.country || '',
      genre:       s.tags.join(', '),
      artworkUrl:  s.image || '',
      streamUrls:  [],                                // resolved lazily
    }));
  },

  async resolveStreamUrl(station) {
    const shortId = station.id.split(':')[1];
    const res = await fetch(`https://radio.garden/api/stations/${shortId}/stream`);
    if (!res.ok) throw new Error(`Radio Garden stream HTTP ${res.status}`);
    return res.text();   // direct .mp3 URL
  },

  async getNowPlaying(station) {
    // Radio Garden doesn't expose per-station now-playing metadata
    // through a public endpoint, so return null. The Tuner UI handles
    // this gracefully (just shows the station name + "Live").
    return null;
  },
};

Step 2 — register it in src/sources/index.js

import { somaFM }     from './somafm.js';
import { radioGarden } from './radio-garden.js';        // <-- new

const REGISTERED = [
  somaFM,
  radioGarden,                                          // <-- new
];

Step 3 — add the host permission to manifest.json

"host_permissions": [
  "https://somafm.com/*",
  "https://*.somafm.com/*",
  "https://radio.garden/*"
]

You'll also need to write a permission justification for the new host (the Chrome Web Store reviewer reads it).

Step 4 — bump the version + update the CHANGELOG

"version": "0.4.0"

In CHANGELOG.md add a new [0.4.0] entry describing the addition.

Step 5 — test it locally

  1. chrome://extensions → reload RangerHQ Tuner
  2. Open the popup or New Tab Page
  3. The station list should now include Radio Garden stations alongside SomaFM
  4. Pick one → it should play
  5. The History tab should log tracks if Radio Garden returns now-playing metadata

That's it. No UI changes, no message-type additions, no rewrites of the popup or the offscreen audio loop.

Namespacing rules

Station IDs must be prefixed with the source ID, separated by a colon: sourceId:stationShortId. Examples:

  • somafm:groovesalad
  • radio-garden:dublin-ireland-rte-1
  • internet-radio:bbc-radio-6

This is how the popup's "Now Playing" highlight, history entries, and chrome.storage's tuner.currentStationId all stay unambiguous across sources.

What "good first sources" look like

If you're contributing an adapter, the best candidates are radio networks that meet all of:

  • Public JSON catalogue (no API key required)
  • Direct stream URLs (icecast, shoutcast, .pls, .m3u — anything an <audio> element can play)
  • Permissive ToS for third-party tools/aggregators
  • HTTPS endpoints (Chrome blocks mixed-content audio)
  • ⚠️ Optional: per-station now-playing metadata endpoint (RangerHQ Tuner UI handles missing metadata gracefully, but history logging requires it)

Examples that fit: SomaFM (already done), Internet-Radio.com, Radio Garden, Shoutcast directory, Icecast directories.

Examples that don't fit and shouldn't be added as adapters: Spotify, YouTube, Apple Music, Tidal, Deezer (all require OAuth + Premium and don't fit the "play freely-licensed indie radio" purpose of the extension — those services are reachable via the four search-link buttons instead).

Smoke test before submitting an adapter PR

# load the extension locally
open chrome://extensions
# enable Dev mode, Load unpacked, pick the repo folder

# verify the adapter
# 1. Pick a station from your network
# 2. Audio should play within ~3 seconds
# 3. Close the popup → audio continues
# 4. Wait 30 seconds → switch to the History tab on the new tab
# 5. At least one entry should appear (assuming getNowPlaying returns data)
# 6. Click "YouTube" on the row → opens a YouTube search results page
# 7. Star the row → switches to Favourites tab → entry is there

# verify the registry didn't drift
grep -A3 "REGISTERED =" src/sources/index.js

If all those check out, open an issue or PR on the repo.