wiki: replace placeholder with 5 substantive pages

- Home: overview + quick links + source layout + install + zip recipe
- Architecture: MV3 + offscreen pattern + source-adapter pattern +
  cross-surface storage sync, plus an ASCII component diagram
- Adding a Radio Source: full walkthrough of writing a new adapter,
  with code template, namespacing rules, smoke test checklist
- Privacy: TL;DR + storage key inventory + outbound endpoints +
  permission justifications (mirrors PRIVACY.md)
- FAQ: 12 common questions + family pointer
- Family: positions Tuner against RangerHQ Radio (WP sibling) +
  RangerHQ Buddy + future Logbook
2026-06-09 01:30:32 +01:00
parent 9f4a1d2d56
commit 0ffe12efb0
7 changed files with 596 additions and 1 deletions
+175
@@ -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<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`
```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 `<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
```bash
# 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.
+120
@@ -0,0 +1,120 @@
# Architecture
RangerHQ Tuner is a Chrome Manifest V3 extension built with vanilla JavaScript ES modules — no React, no Vue, no Vite, no build step. The whole thing is ~36 source files and 133 KB zipped.
## Why no framework / no build step
The extension's UI surface is small (a 360 px popup + a Settings page + a New Tab Page). React or Svelte would add Vite/Rollup tooling, source maps, CSP headaches, and a `dist/` build step for negligible developer-experience gain at this scale. The WordPress family (`rangerhq-radio`, `rangerhq-buddy`) is hand-rolled PHP for the same reason — pick the right tool for the size of the surface.
The exception trigger is documented at the top of the [project plan](https://git.davidtkeane.com/ranger/rangerhq-tuner/src/branch/main/CHANGELOG.md): if the popup ever grows beyond ~5 distinct views, switch to Lit or Preact (both work without a build step via ESM CDN).
## The Manifest V3 audio problem
MV3 service workers can be killed by Chrome at any time when they go idle (typically after 30 seconds of no events). An `<audio>` element instantiated in a service worker would die with it, causing playback to stop randomly. The official MV3 mechanism for persistent audio is the **`chrome.offscreen` API**, which lets the extension spawn a hidden DOM document specifically for tasks the service worker can't do.
RangerHQ Tuner creates exactly one offscreen document, lazily on the first user-initiated play, with `reasons: ['AUDIO_PLAYBACK']`. The document holds a single `<audio>` element and the 25-second metadata polling loop.
## High-level component map
```
┌─────────────────┐ ┌──────────────────┐
│ Toolbar popup │ │ New Tab Page │
│ (popup/*) │ │ (newtab/*) │
└──────┬──────────┘ └─────────┬────────┘
│ │
│ chrome.runtime.sendMessage │
│ target: 'sw' │
▼ ▼
┌──────────────────────────────────────────────┐
│ Service Worker (background/service-worker) │
│ - opens offscreen doc on demand │
│ - routes PLAY/PAUSE/SET_VOLUME messages │
│ - storage gateway for offscreen writes │
└──────┬───────────────────────────────────────┘
│ chrome.runtime.sendMessage
│ target: 'offscreen'
┌──────────────────────────────────────────────┐
│ Offscreen document (offscreen/*) │
│ - owns the <audio> element │
│ - 25-second metadata polling loop │
│ - asks SW to write history entries │
└──────┬───────────────────────────────────────┘
│ chrome.runtime.sendMessage
│ target: 'popup' (UI bucket)
┌──────────────────────────────────────────────┐
│ UI surfaces (popup + newtab + options) │
│ re-render on STATE_CHANGED / METADATA_ │
│ UPDATED / TRACK_LOGGED │
└──────────────────────────────────────────────┘
chrome.storage.local
┌───────────────────────────────────────────────────────┐
│ tuner.history tuner.favourites │
│ tuner.stationsCache tuner.cachedAt │
│ tuner.currentStationId tuner.volume tuner.isPlaying │
│ tuner.historyCap │
└───────────────────────────────────────────────────────┘
▲ ▲
│ popup/newtab read/write │ Options page
└───── chrome.storage.onChanged ──────────────┘
(cross-surface sync)
```
## Three architectural decisions, ranked by importance
### 1. The source-adapter pattern (`src/sources/`)
Every radio network is a single module that conforms to the `RadioSource` JSDoc interface in `src/sources/base-source.js`. The popup, the new tab, and the offscreen document never know which network is which — they all query through `src/sources/index.js`'s registry. Adding a new network = drop a new file + add one line to the registry.
See [[Adding a Radio Source]] for the practical walkthrough.
### 2. Audio in an offscreen document, not the service worker
Already explained above. The cost: one extra message hop (popup → SW → offscreen). The benefit: audio that doesn't randomly cut out.
### 3. Storage gateway through the service worker
The offscreen document doesn't reliably have `chrome.storage` access in some Chrome versions, so the offscreen polling loop sends a `LOG_TRACK_REQUEST` message to the service worker, which has unrestricted storage access. The SW writes to `tuner.history` and broadcasts `TRACK_LOGGED` back to the UI surfaces.
The library file `src/lib/history.js` also defensively guards every storage call with a `storage()` helper that returns `null` if `chrome.storage` is absent — so even if the module is imported into an edge context, it returns empty defaults instead of throwing.
## Cross-surface state sync
Both the popup and the New Tab Page subscribe to `chrome.storage.onChanged`. When the user picks a station in one surface, the other surface re-renders without a manual reload:
- `tuner.currentStationId` change → re-resolve `currentStation` from the local catalogue, re-render Now Playing + quick-station chips + active-row highlight
- `tuner.isPlaying` change → re-render the play/pause button state
- `tuner.history` change → re-render the History tab
- `tuner.favourites` change → re-render the Favourites tab AND the star indicators in History
Single source of truth = `chrome.storage.local`. No drift between surfaces.
## Permission profile (deliberately narrow)
```json
"permissions": ["offscreen", "storage"],
"host_permissions": ["https://somafm.com/*", "https://*.somafm.com/*"]
```
- **No `tabs` permission.** The popup's "Open in tab" / "History" / "Settings" buttons use `chrome.tabs.create()` and `chrome.runtime.openOptionsPage()`, both of which work on the extension's own URLs without needing `tabs`.
- **No `<all_urls>` permission.** Never reads or writes to user-visited pages.
- **No `webRequest` permission.** No traffic interception.
- **No `cookies` permission.** No cookie access.
- **No remote code.** Every script that runs ships inside the package — no `eval`, no dynamically loaded JS.
## Code paths to read first
If you've cloned the repo and want to understand the flow, read these files in order:
1. **`manifest.json`** — the entry point Chrome sees.
2. **`src/popup/popup.js`** — UI flow from a station click to a `PLAY` message.
3. **`src/background/service-worker.js`** — message router + offscreen creator + storage gateway.
4. **`src/offscreen/offscreen.js`** — `<audio>` host + metadata polling loop.
5. **`src/sources/somafm.js`** — first concrete adapter, see the contract in action.
6. **`src/lib/history.js`** — history/favourites/search-URL helpers, mirrors the WordPress sibling's `inc/history.php`.
Total reading time end-to-end: about 20 minutes.
+65
@@ -0,0 +1,65 @@
# FAQ
## Why SomaFM?
SomaFM is a listener-supported independent radio network with ~30 always-on channels across electronic, ambient, indie, jazz, lounge, classic film noir soundtracks, and more. Their content is publicly streamable, their API (`channels.json`, per-station `.pls` files, per-station `songs/{id}.json`) is public and well-documented, and their community guidelines explicitly welcome third-party tools that play their streams. They are the cleanest fit for a "first network" adapter on a brand-new MV3 extension.
## Will you add other radio networks?
Yes — that's the whole point of the source-adapter pattern in `src/sources/`. Likely candidates: Internet-Radio.com, Radio Garden, Shoutcast / Icecast directories. See [[Adding a Radio Source]] for the implementation walkthrough; PRs welcome.
## Will you add Spotify / YouTube / Apple Music / Tidal playback?
**No, and that's a deliberate scope choice.** Embedding paid streaming services would require OAuth flows, premium-account gating, broad host permissions, third-party SDKs, and a fundamentally different privacy story — none of which fits a lightweight free indie-radio extension.
Instead, RangerHQ Tuner gives you **four search-link buttons** next to every history entry. Click "Spotify" → opens Spotify's public search results page in a new tab. Click "YouTube" → opens YouTube search. Same for Apple Music and Bandcamp. The destination service plays the track; the extension just helps you find it. No auth, no API keys, no SDK.
## Does the extension play YouTube / Spotify audio inside itself?
No. The four service buttons are plain HTML `<a href>` links that open a new browser tab pointed at the service's public search URL. RangerHQ Tuner only plays SomaFM audio (and any future radio adapters added the same way).
## Why does the audio keep playing after I close the popup?
Because the audio is hosted in a Chrome **offscreen document**, not in the popup itself. The popup is just a view onto the playback state — closing the popup destroys the view but the offscreen document (and the `<audio>` element it contains) keeps running. The same applies to the New Tab Page.
## Why does the New Tab Page show the player?
The extension declares `chrome_url_overrides.newtab` in its manifest, which is the standard Chrome way to replace the default new tab page. You opted in by installing the extension — Chrome will show you the override consent the first time. If you ever want Chrome's default new tab back, disable or remove the extension.
## Can I export my history?
Not yet. The data lives in `chrome.storage.local` under the `tuner.history` key — you can pull it out by opening the DevTools console on the New Tab Page and running:
```js
chrome.storage.local.get('tuner.history').then(o => console.log(JSON.stringify(o['tuner.history'], null, 2)));
```
A proper Export button is on the future roadmap.
## How big can the history grow?
Configurable on the Options page: 50500 entries. Default is 500. At ~200 bytes per entry that's ~100 KB — well inside Chrome's `chrome.storage.local` quota (~10 MB).
## Does it work offline?
Audio playback requires a network connection to SomaFM. The popup will still open, and your history and favourites are still readable, but nothing will play.
## Does it work on Firefox / Edge / Safari?
Right now: Chrome only. Edge should work as-is (it uses the same Manifest V3 / Chromium engine). Firefox would need a `web-ext` adaptation — on the future roadmap. Safari extensions are a separate beast and not planned.
## How do I uninstall and wipe everything?
`chrome://extensions` → RangerHQ Tuner → **Remove**. That removes the extension and wipes its local storage. Nothing else needs cleaning up — no servers held your data, no accounts to delete.
## Is this related to RangerHQ Radio (the WordPress plugin)?
Yes — they're family. **RangerHQ Radio** is the WordPress-admin sibling (lives in your WP admin sidebar, plays SomaFM there). **RangerHQ Tuner** is the browser-resident sibling for when you're outside WP. Same brand, same SomaFM source, same 4-button-search affordance — different surface. See [[Family]].
## Where do I file a bug or feature request?
Open an issue at <https://git.davidtkeane.com/ranger/rangerhq-tuner/issues> or email <david@davidtkeane.com>.
## Who made this?
David Keane, Dublin, Ireland. <david@davidtkeane.com> · <https://davidtkeane.com>
+57
@@ -0,0 +1,57 @@
# Family
RangerHQ Tuner is one of several **RangerHQ** projects by David Keane. The family share a name, a visual identity (the helmet logo), a GPL v2+ licence, and a "lightweight, no-telemetry, hand-rolled" ethos. They live on different platforms because the audience for each is genuinely different.
## Current family members
### RangerHQ Tuner — this project
- **Surface:** Chrome browser (toolbar + New Tab Page)
- **Function:** Play SomaFM internet radio, log heard tracks, link out to Spotify / YouTube / Apple Music / Bandcamp
- **Repo:** [git.davidtkeane.com/ranger/rangerhq-tuner](https://git.davidtkeane.com/ranger/rangerhq-tuner)
- **Distribution:** Chrome Web Store (in review at time of writing)
### RangerHQ Radio — WordPress sibling
- **Surface:** WordPress admin
- **Function:** Same idea as Tuner but for WordPress admins — play SomaFM from your wp-admin sidebar with the same 4-button search-link affordance
- **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 Radio is the older sibling — it shipped first, and its `inc/history.php` is the file that Tuner's `src/lib/history.js` mirrors exactly. The 4-button search URLs (Spotify, YouTube, Apple Music, Bandcamp) are the same URL templates in both.
### RangerHQ Buddy — WordPress dashboard pet
- **Surface:** WordPress admin
- **Function:** A small virtual pet that lives in your WP dashboard. Mood will eventually reflect your site's health (post counts, outdated plugins, spam queue).
- **Repo:** [git.davidtkeane.com/ranger/rangerhq-buddy](https://git.davidtkeane.com/ranger/rangerhq-buddy)
- **Distribution:** WordPress.org Plugin Directory (in review)
Unrelated to radio — different product, same family branding.
## Coming next
### RangerHQ Logbook
- **Surface:** WordPress admin
- **Function:** A site-changes audit log for WordPress administrators. Captures who did what when across plugins, themes, options, users — searchable + exportable.
- **Status:** In design
### Browser ports
- **Edge:** Manifest V3 / Chromium-based — RangerHQ Tuner should work as-is, just needs separate Microsoft Edge Add-ons store submission.
- **Firefox:** Needs a `web-ext` adaptation — different background-script model. On the future roadmap.
## Why a "family" of small things instead of one big thing
Each project has a sharp, single purpose and ships on the surface where it actually belongs. The RangerHQ Tuner UI lives in the browser because that's where people listen to music; the WordPress family lives in wp-admin because that's where the WordPress workflow lives.
Sharing a brand across surfaces — same helmet, same colour palette, same "hand-rolled, no-telemetry" promise — means a user who likes one is more likely to try another. Sharing **code patterns** across surfaces (the 4-button search URLs, the FIFO-history dedup rule, the Keep-a-Changelog format) means each new sibling ships faster than the last.
## Identity
- **Logo:** the RangerHQ helmet (`src/assets/img/ranger.png`)
- **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
+101
@@ -0,0 +1,101 @@
# RangerHQ Tuner Wiki
**A lightweight Chrome extension for SomaFM internet radio with track history and one-click search to the big-four music services.**
| | |
|---|---|
| **Status** | v0.3.0 — Chrome Web Store submission in review |
| **Latest tag** | [v0.3.0](https://git.davidtkeane.com/ranger/rangerhq-tuner/src/tag/v0.3.0) |
| **Licence** | GPL v2 or later |
| **Landing page** | https://davidtkeane.com/rangerhq-tuner |
| **Privacy policy** | https://davidtkeane.com/rangerhq-tuner/privacy |
| **Family** | Sibling to [RangerHQ Radio](https://wordpress.org/plugins/rangerhq-radio/) (WordPress) |
---
## What it does
RangerHQ Tuner lives in your Chrome toolbar. Click the helmet icon, pick a SomaFM station, music starts. Audio keeps playing after the popup closes because it's hosted in a Manifest V3 offscreen document.
Every track that comes through gets logged to a capped FIFO history. Each entry in the history has four buttons next to it — Spotify, YouTube, Apple Music, Bandcamp — that open the corresponding service's public search results page in a new browser tab. No accounts, no API keys, no third-party SDK; just a way to find the track on whichever music service you actually use.
Optionally, RangerHQ Tuner replaces Chrome's default New Tab Page with a RangerHQ-branded landing that has the player + live clock + station picker + history + favourites.
## Quick links
- [[Architecture]] — how Manifest V3 + the offscreen audio pattern + the source-adapter pattern all fit together
- [[Adding a Radio Source]] — drop a single file in `src/sources/`, no other changes needed
- [[Privacy]] — what's stored (locally only), what's not, what the four search buttons actually do
- [[FAQ]] — common questions
- [[Family]] — sibling projects (RangerHQ Radio, RangerHQ Buddy)
## Install
### From Chrome Web Store (when live)
Search for **RangerHQ Tuner** in the [Chrome Web Store](https://chrome.google.com/webstore/category/extensions) → Add to Chrome.
### Developer mode (right now)
```bash
git clone https://git.davidtkeane.com/ranger/rangerhq-tuner.git
```
Then in Chrome:
1. Open `chrome://extensions`
2. Toggle **Developer mode** on
3. Click **Load unpacked**
4. Select the cloned folder
5. Pin the helmet icon to your toolbar
## Source layout
```
rangerhq-tuner/
├── manifest.json # Manifest V3, narrow permissions
├── LICENSE # GPL v2 or later
├── README.md
├── CHANGELOG.md # Keep a Changelog format
├── PRIVACY.md # canonical privacy policy
├── store/ # Chrome Web Store assets (not in zip)
│ ├── store-icon-128.png
│ ├── promo-tile-440x280.png
│ ├── marquee-1400x560.png
│ └── screenshots/
└── src/
├── background/
│ └── service-worker.js # message router + storage gateway
├── offscreen/
│ ├── offscreen.html # hidden audio host
│ └── offscreen.js # owns <audio> + 25s metadata polling
├── popup/ # toolbar popup UI
├── newtab/ # New Tab Page override UI
├── options/ # Settings page
├── lib/
│ ├── messages.js # message type + target constants
│ ├── history.js # history + favourites + search URLs
│ └── playlist-parser.js # .pls parser
├── sources/ # extensibility seam — see "Adding a Radio Source"
│ ├── base-source.js # RadioSource interface contract
│ ├── somafm.js # first concrete adapter
│ └── index.js # registry
└── assets/
├── icons/ # 16/32/48/128 toolbar PNGs
└── img/ranger.png # master helmet logo
```
## Building the Chrome Web Store package
```bash
cd rangerhq-tuner
zip -r ../rangerhq-tuner-v0.3.0.zip \
manifest.json src/ LICENSE \
-x "*.DS_Store" -x "*/.git/*" -x "*/node_modules/*"
```
Final size is ~133 KB.
## Reporting issues / suggestions
Open an [issue](https://git.davidtkeane.com/ranger/rangerhq-tuner/issues) on this repo, or email <david@davidtkeane.com>.
+78
@@ -0,0 +1,78 @@
# Privacy (summary)
> **Full canonical policy:** [`PRIVACY.md` in the repo](https://git.davidtkeane.com/ranger/rangerhq-tuner/src/branch/main/PRIVACY.md) — and the public version at <https://davidtkeane.com/rangerhq-tuner/privacy>
## TL;DR
**RangerHQ Tuner collects no personal data. Nothing leaves your device.**
Everything the extension remembers — your last station, your volume, your history, your favourites, the cached SomaFM catalogue — lives in `chrome.storage.local` on your own computer. The extension author never sees any of it. Neither does Google. Neither does anyone else.
## What we don't collect
Using Chrome Web Store's standard data category vocabulary, RangerHQ Tuner does **not** collect any of:
- Personally identifiable information
- Health information
- Financial / payment information
- Authentication information
- Personal communications
- Location
- Web history
- User activity
- Website content
No accounts, no login, no telemetry, no analytics, no third-party tracking scripts, no advertising code.
## What we store locally (and only locally)
| `chrome.storage.local` key | What it holds | Why |
|---|---|---|
| `tuner.stationsCache` | A cached SomaFM channel list | Fast popup open |
| `tuner.cachedAt` | Cache timestamp | Expires after 6 hours |
| `tuner.currentStationId` | Your last-picked station ID | Resume where you left off |
| `tuner.volume` | Your last volume (0.01.0) | Persistent across sessions |
| `tuner.isPlaying` | Whether playback was active | UI state sync |
| `tuner.history` | Capped FIFO of `{artist, title, station, stationId, at}` | History tab |
| `tuner.favourites` | Uncapped same-shape list | Favourites tab |
| `tuner.historyCap` | Your chosen history cap | Configurable 50500 |
Wipe any of it from the extension's Options page (**Clear history / Clear favourites / Clear EVERYTHING**) or by removing the extension from `chrome://extensions`.
## Outbound network requests
RangerHQ Tuner contacts **only** the following endpoints, all on `somafm.com`:
| Endpoint | When |
|---|---|
| `https://somafm.com/channels.json` | At most once every 6 hours |
| `https://somafm.com/{...}.pls` | Once per station change |
| Direct stream URLs (e.g. `https://ice1.somafm.com/groovesalad-128-mp3`) | Continuous while playing |
| `https://somafm.com/songs/{id}.json` | Every 25 seconds while playing |
These are public-API calls — no authentication, no user identifiers, no tracking parameters added. SomaFM sees your IP exactly as if you visited <https://somafm.com> in a browser tab.
## "Search this track" link-outs
The four buttons next to each history entry — **Spotify**, **YouTube**, **Apple Music**, **Bandcamp** — are plain HTML anchor tags (`<a href>` with `target="_blank" rel="noopener noreferrer"`) that open the corresponding service's public search results page in a new browser tab.
**RangerHQ Tuner 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.
## Permission justifications
| Permission | Why |
|---|---|
| `offscreen` | Manifest V3 service workers can't host an `<audio>` element; the `chrome.offscreen` API provides a hidden DOM document for persistent audio playback. |
| `storage` | Persist your settings, last station, volume, history, and favourites in `chrome.storage.local` on your own device. |
| `https://somafm.com/*` | Fetch the public station catalogue, playlist files, audio streams, and now-playing metadata from SomaFM. No auth, no user data sent. |
We do not request `tabs`, `activeTab`, `<all_urls>`, `webRequest`, `cookies`, or any other broad permission.
## Children's privacy
RangerHQ Tuner is a general-purpose audio player and does not knowingly process data of children under 13. It collects nothing about anyone of any age.
## Contact
- Email: <david@davidtkeane.com>
- Issues / source: <https://git.davidtkeane.com/ranger/rangerhq-tuner>
-1
@@ -1 +0,0 @@
Welcome to the Wiki.