diff --git a/Adding-a-Radio-Source.md b/Adding-a-Radio-Source.md new file mode 100644 index 0000000..9d1a547 --- /dev/null +++ b/Adding-a-Radio-Source.md @@ -0,0 +1,175 @@ +# 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`: + +```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} listStations + * @property {(station: Station) => Promise} getNowPlaying + * @property {(station: Station) => Promise} 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` + +```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` + +```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` + +```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 + +```json +"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 `