Web Store submission target. Mirrors rangerhq-radio's track-history pattern (inc/history.php) so the family stays coherent across surfaces. Highlights - New Tab Page override (Tier 2.5) — Chrome's default new tab replaced with a RangerHQ-branded landing showing the player, current track, quick chips, searchable browse list, and now history + favourites tabs. - Track history + favourites — capped FIFO 500, dedup against last entry, skip "(unknown)" artist (SomaFM dead-air). Stored in chrome.storage.local under tuner.history + tuner.favourites. - 4-button search per entry — Spotify / YouTube / Apple Music / Bandcamp. Pure public-search-URL link-outs in a new tab, NO auth, NO API keys, NO quota, NO third-party SDK embedded. - Options page (chrome://extensions → details → options) — live stats, history cap slider (50-500), Clear history / Clear favourites / Clear EVERYTHING buttons, About panel with Gitea + davidtkeane.com links. - Popup nav row — Open in tab / History (#hash deep link) / Settings, using chrome.tabs.create + chrome.runtime.openOptionsPage. No new perms. - Cross-surface sync — popup ↔ newtab listen on chrome.storage.onChanged for tuner.currentStationId / tuner.isPlaying / history / favourites. - Storage gateway — offscreen doc can't reliably reach chrome.storage in some Chrome versions, so it sends LOG_TRACK_REQUEST to the SW which does the write. history.js also defensively guards every storage call. - Metadata latency fix — polling now starts immediately on PLAY, in parallel with audio buffer fill. First track display drops from ~10-15s to ~1-2s. Permissions unchanged - Still ["offscreen", "storage"] + somafm.com host only. - chrome.tabs.create works on our own extension URLs without "tabs" perm. - No webRequest, no <all_urls>, no third-party SDK. Bumped from 0.1.0 (last tag on Gitea) directly to 0.3.0. v0.2.0 (newtab + clock) was a working local build but never tagged; its features ship together with v0.3.0 in this single commit.
14 KiB
Changelog
All notable changes to RangerHQ Tuner are documented here. Format: Keep a Changelog 1.1.0 — versioning: SemVer.
[Unreleased]
Planned — Tier 3 (Chrome Web Store submission)
- Listing assets: 1280x800 screenshots, 440x280 promo tile
- Privacy policy URL published at
davidtkeane.com/rangerhq-tuner/privacy - 2-Step Verification on Google account
- $5 dev fee + submission
[0.3.0] — 2026-06-08
Added — Track history + 4-button search + Options page (Web Store target)
This is the version that goes to the Chrome Web Store. Mirrors the rangerhq-radio WordPress plugin's history feature so the family stays coherent across surfaces.
Track history
src/lib/history.js— helpers for history/favourites/search-URL/dedup/cap/clear, mirroringinc/history.phpin the WP plugin.- Storage:
tuner.history(FIFO, capped at 500 by default) andtuner.favourites(uncapped) inchrome.storage.local. Cap configurable 50–500 on the Options page. - Entry shape (mirror WP plugin exactly):
{ artist, title, station, stationId, at }whereat = Date.now(). - Dedup against the LAST entry only so consecutive identical now-playing reads don't pile up.
- Skip
(unknown)artist — SomaFM's dead-air/promo placeholder doesn't pollute the log.
Metadata polling
src/offscreen/offscreen.js— the offscreen document (where audio lives) now polls SomaFM's per-channel song endpoint every 25 s while playing. OnPLAY, polling starts (immediate first fetch + interval). OnPAUSEorERROR, polling stops. Within-session dedup prevents redundant logs when the same song stays current across polls.- Why the offscreen doc, not the SW: the SW gets killed when idle; the offscreen doc is alive while audio plays. This is the right home for the loop.
- New message types in
src/lib/messages.js:TRACK_LOGGED,FAVOURITE_TOGGLED,STORAGE_WIPED.
4-button search (the headline feature)
-
Every track row in History and Favourites gets four search-link buttons that open the major services' public search pages in a new tab:
Service URL pattern Spotify https://open.spotify.com/search/{enc}YouTube https://www.youtube.com/results?search_query={enc}Apple Music https://music.apple.com/search?term={enc}Bandcamp https://bandcamp.com/search?q={enc}Where
enc = encodeURIComponent(artist + " " + title). Zero auth, zero API keys, zero quota, zero ToS gray area. RangerHQ Tuner doesn't play these services — it opens the search results page and the user picks. Same approach as the rangerhq-radio WP plugin. -
Each link is brand-coloured (muted on the dark palette) — Spotify green, YouTube red, Apple pink, Bandcamp cyan.
target="_blank"+rel="noopener noreferrer".
Tabbed New Tab Page
- The New Tab Page's bottom section is now a 3-tab control: Stations · History · Favourites, plus a ⚙ icon that opens the Options page.
- History tab: newest first, with track title, artist (accent green),
station · 2m agometa line, star toggle, and the 4 search links. - Favourites tab: same shape, all your starred tracks.
- Each tab has its own "Clear …" button in the toolbar (confirm dialog).
Options page (NEW)
- Registered via
"options_page": "src/options/options.html"in the manifest. Opens viachrome://extensions→ details → Extension options OR via the ⚙ on the New Tab Page. - Local data card: live stats (History size, Favourites size, Stations cached, Total
tuner.*bytes), history-cap slider (50–500 step 50), and three buttons:Clear history— dropstuner.historyonly.Clear favourites— dropstuner.favouritesonly.Clear EVERYTHING(danger styling) — wipes everytuner.*key inchrome.storage.local.
- Playback card: current volume %, last-played station name (read-only).
- About card: version, Gitea repo link, davidtkeane.com link, SomaFM link, licence reminder.
- A small toast message confirms each destructive action.
- The page listens for
chrome.storage.onChangedso stats stay live if you wipe from somewhere else.
Manifest
- Version:
0.2.0→0.3.0. - Description updated to mention the 4-button search affordance (helps Web Store reviewers see the single-purpose statement matches the listing).
- Added
"options_page": "src/options/options.html". - No new permissions, no new host permissions. Still
["offscreen", "storage"]+somafm.comonly. Search-link clicks open in a new browser tab — that's the user's navigation, not an API call from the extension.
Cross-surface state sync
The popup and the New Tab Page now stay in lockstep without manual reloads. Both surfaces subscribe to chrome.storage.onChanged and re-render when:
tuner.currentStationIdchanges (someone in the other surface picked a station)tuner.isPlayingchanges (play/pause from anywhere)tuner.historyortuner.favouritesmutates (re-render the relevant tab)
Single source of truth = chrome.storage.local. No more drift between popup and New Tab.
Storage gateway architecture (MV3 safety net)
Offscreen documents don't reliably have access to chrome.storage across Chrome versions, so all storage writes from the offscreen audio host are routed through the service worker via a new LOG_TRACK_REQUEST message type. The SW (which always has chrome.storage) does the actual tuner.history write and broadcasts TRACK_LOGGED back to the UI surfaces.
As belt-and-braces, every function in src/lib/history.js now checks for chrome.storage availability before using it and returns sensible defaults if absent — so the module is safe to import from any extension context.
Metadata latency fix (parallelised audio + polling)
When PLAY arrives at the offscreen doc, metadata polling now starts immediately in parallel with the audio HTTP buffer fill, rather than waiting for audio.play() to resolve. First-track display time drops from ~10-15 seconds (audio buffer + then poll) to ~1-2 seconds (poll racing in parallel with buffer fill).
Popup quick-link nav
The toolbar popup now has a three-button nav row above the footer:
- ⎘ Open in tab —
chrome.tabs.create({url: chrome.runtime.getURL('src/newtab/newtab.html')})opens the full Tuner UI as a regular pinnable tab. - ♪ History — same as Open in tab but with
#historyhash so the History pane is pre-selected.newtab.jsreadslocation.hashon init and respects it. - ⚙ Settings —
chrome.runtime.openOptionsPage()opens the Options page directly.
No new permissions: chrome.tabs.create() works on our own extension URLs without the tabs permission. The Web Store permission profile stays ["offscreen", "storage"] + somafm.com host only.
Why this is the Web Store version
- Clear single-purpose statement now obvious: "Plays SomaFM internet radio, logs heard tracks, and provides search-link shortcuts to Spotify/YouTube/Apple Music/Bandcamp."
- Privacy story stays clean: "collects nothing, stores everything locally, no remote storage" — every Options-page checkbox stays "does not collect" on the Dashboard.
- The four service buttons are pure
<a href>link-outs — Web Store reviewers can verify they don't embed third-party SDKs. - All inline styles are gone (lessons from
rangerhq-buddy v0.1.5). - Accessibility: every button has an
aria-label, every interactive element is reachable by keyboard, focus rings visible, contrast ≥ 4.5:1. - Storage operations are SW-gated, so even Chrome versions with the offscreen-doc storage quirk run cleanly.
[0.2.0] — 2026-06-08
Added — New Tab Page override (Tier 2.5)
Replace Chrome's default New Tab Page with a RangerHQ-branded landing that surfaces the player, current track, a quick-station chip row, and a searchable browse list. Same chrome.offscreen audio pipeline as the popup — the New Tab Page is just a second view onto the same playback state. Live ticking clock (HH:MM in bold + small dim seconds) updates every second; date renders in the user's locale.
Note: v0.2.0 was a working local build but was never tagged on Gitea — its features ship to the world together with v0.3.0 in a single commit.
manifest.json: addedDescription string updated to mention the optional New Tab Page replacement (Web Store reviewers prefer overrides to be disclosed in the description)."chrome_url_overrides": { "newtab": "src/newtab/newtab.html" }src/newtab/newtab.html— full-viewport landing with header (helmet + clock), large now-playing block, generous Play/Volume controls, quick-station chips, searchable browse list, footer linking todavidtkeane.com+ Gitea.src/newtab/newtab.css— same earthy palette as the popup but laid out for a full screen. Subtle helmet watermark (2.5% opacity) in the background. Responsive shrink-to-fit atmax-height: 700px.src/newtab/newtab.js— reusessrc/lib/messages.js,src/sources/index.js, and the samechrome.storage.localkeys as the popup. State stays consistent: pick a station in the popup, refresh new tab → same station selected, same play state. Adds a clock that updates every 30 s.
Quick-pick stations: the chip row surfaces the 8 most-loved SomaFM channels (Groove Salad, Drone Zone, Indie Pop Rocks!, Secret Agent, Space Station Soma, Lush, Deep Space One, Fluid). One click → playing.
Why this feature exists: validates user_browser_resident_tools — David has the browser open all day, every new tab is a touchpoint. Every Cmd+T now opens a RangerHQ-branded landing that's one click from playing radio. The Web Store reviewer is also more likely to grant a clear single-purpose override than a vague one — having a useful New Tab Page is good signal alongside the toolbar player.
Permissions unchanged: no new permissions or hosts added. The override is a pure UI choice; no expanded API surface.
[0.1.0] — 2026-06-08
Added — Tier 1 MVP (Buddy is alive — er, Tuner is alive)
Buddy's browser cousin lives. First release of RangerHQ Tuner — a Chrome Manifest V3 extension that plays SomaFM internet radio from your toolbar. Sibling to rangerhq-radio (the WordPress version live on wp.org since 2026-06-04). Same brand idea, different surface: WP version lives in admin pages, Chrome version lives one toolbar-click away no matter what you're doing.
Architecture
- Manifest V3 — uses the
chrome.offscreenAPI to host the<audio>element in a hidden document. Service workers can't host audio in MV3 (they get killed when idle), so a one-off offscreen document is the only supported pattern. Working code insrc/offscreen/offscreen.js+src/background/service-worker.js. - Source-adapter pattern at
src/sources/— every radio network is a single file conforming to theRadioSourceinterface inbase-source.js. Adding a new network = drop a new file + register one line insrc/sources/index.js. Popup, SW, and offscreen never know which network is which. - Vanilla JS, no build step — pure ES modules loaded directly. No webpack, no Vite, no
npm install. Same ethos as the RangerHQ WP family (hand-rolled PHP). - No telemetry, no third-party JS — everything is local. Only outbound network calls are to SomaFM's public endpoints (catalogue + stream metadata).
Features
- Toolbar popup showing a searchable SomaFM station list (~150 channels) with artwork, name, and genre.
- Play / Pause / Volume controls. Volume persists across browser restarts.
- Last station picked is remembered — Tuner remembers where you left off.
- Now-playing metadata displayed from SomaFM's per-channel song history endpoint.
- Catalogue cached in
chrome.storage.localfor 6 hours to make popup re-opens instant. - Audio continues after popup closes — the offscreen document owns the playback, popup is just a view.
- RangerHQ helmet icon in the Chrome toolbar (resized from
src/assets/img/ranger.pngto 16/32/48/128 PNGs with a dark#1a221cpadded background that matches the popup palette).
Files (22 created)
rangerhq-tuner/
├── manifest.json
├── README.md
├── CHANGELOG.md
├── .gitignore
└── src/
├── assets/
│ ├── icons/icon-{16,32,48,128}.png (toolbar icons)
│ └── img/ranger.png (master helmet logo, 257×275)
├── background/service-worker.js (message router only)
├── offscreen/offscreen.{html,js} (audio host — the MV3 gotcha solved)
├── popup/popup.{html,css,js} (the toolbar UI)
├── sources/
│ ├── base-source.js (RadioSource interface contract)
│ ├── somafm.js (first concrete adapter)
│ └── index.js (registry)
└── lib/
├── messages.js (type + target constants)
└── playlist-parser.js (.pls parser)
Manifest permissions (narrow on purpose)
"permissions": ["offscreen", "storage"],
"host_permissions": ["https://somafm.com/*", "https://*.somafm.com/*"]
No tabs, no <all_urls>, no webRequest. Smallest permission ask possible = easiest Web Store review.
Architecture / status
- Tier 1 MVP — shipped this commit.
- Tier 2 (planned) — full UX polish, favourites via
chrome.storage.sync, now-playing polling loop,.m3uparser,.crxpackaging, second source adapter stub. - Tier 3 (planned) — Chrome Web Store submission. See
~/.ranger-memory/docs/for the per-family submission checklist; Chrome Web Store specifics tracked in the user's memory underreference_chrome_web_store_rules.
Why this exists
David has Chrome open essentially all day. The WP version of RangerHQ Radio requires going to admin pages to use; the toolbar version is one click away regardless of context. Same code-level effort, dramatically higher daily-use payoff. The "best thing we done so far" verdict on first sound (2026-06-08 evening) confirmed the bet.