Files
rangerhq-tuner/CHANGELOG.md
T
ranger ad43df87c0 feat: v0.3.0 — history, 4-button search, options page, newtab override
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.
2026-06-09 00:23:36 +01:00

14 KiB
Raw Permalink Blame History

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, mirroring inc/history.php in the WP plugin.
  • Storage: tuner.history (FIFO, capped at 500 by default) and tuner.favourites (uncapped) in chrome.storage.local. Cap configurable 50500 on the Options page.
  • Entry shape (mirror WP plugin exactly): { artist, title, station, stationId, at } where at = 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. On PLAY, polling starts (immediate first fetch + interval). On PAUSE or ERROR, 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 ago meta 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 via chrome://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 (50500 step 50), and three buttons:
    • Clear history — drops tuner.history only.
    • Clear favourites — drops tuner.favourites only.
    • Clear EVERYTHING (danger styling) — wipes every tuner.* key in chrome.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.onChanged so stats stay live if you wipe from somewhere else.

Manifest

  • Version: 0.2.00.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.com only. 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.currentStationId changes (someone in the other surface picked a station)
  • tuner.isPlaying changes (play/pause from anywhere)
  • tuner.history or tuner.favourites mutates (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).

The toolbar popup now has a three-button nav row above the footer:

  • ⎘ Open in tabchrome.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 #history hash so the History pane is pre-selected. newtab.js reads location.hash on init and respects it.
  • ⚙ Settingschrome.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: added
    "chrome_url_overrides": { "newtab": "src/newtab/newtab.html" }
    
    Description string updated to mention the optional New Tab Page replacement (Web Store reviewers prefer overrides to be disclosed in the description).
  • 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 to davidtkeane.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 at max-height: 700px.
  • src/newtab/newtab.js — reuses src/lib/messages.js, src/sources/index.js, and the same chrome.storage.local keys 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.offscreen API 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 in src/offscreen/offscreen.js + src/background/service-worker.js.
  • Source-adapter pattern at src/sources/ — every radio network is a single file conforming to the RadioSource interface in base-source.js. Adding a new network = drop a new file + register one line in src/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.local for 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.png to 16/32/48/128 PNGs with a dark #1a221c padded 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, .m3u parser, .crx packaging, 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 under reference_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.

Rangers lead the way 🪖🎵