Skip to content

Add Twitch ad blocking, honest UI, and store-ready docs - #3

Merged
wpggLabs merged 14 commits into
mainfrom
feat/twitch-ad-blocker
Jul 3, 2026
Merged

wpggLabs merged 14 commits into
mainfrom
feat/twitch-ad-blocker

Conversation

@wpggLabs

@wpggLabs wpggLabs commented Jul 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Turns TwitchShield from a passive observer into a working, privacy-first Twitch ad blocker for Chrome and Firefox, with a professional engineering setup.

Ad blocking

  • Client-side engine hooks the Twitch player's HLS worker, detects server-side-inserted (SSAI) ad markers, and substitutes an ad-free playlist. No backend, talks only to Twitch's own APIs.
  • Centralized signatures — all Twitch-dependent values live in one patchable adblock-config.ts behind a signatureVersion, so updates are a one-file change.
  • Mute-through-ad resilience while the clean stream loads (user toggle in the dashboard).

UI

  • Popup reflects real settings state (fixes controls that ignored stored values), drops dead mode pills, replaces alert() with inline status, and shows a live blocked-ad counter + per-channel whitelist toggle.

Engineering

  • CI/CD — GitHub Actions runs type-check, ESLint, tests, build, and Firefox validation on every push/PR, plus a Chromium smoke job.
  • ESLint with typed no-floating-promises (already caught + fixed two real bugs).
  • Opt-in live ad-block test (RUN_LIVE=1) and extended build-integrity coverage.
  • engines: node >=20.

Docs & assets

  • Rewritten README (with CI badge), GitHub Pages landing site, Chrome Web Store / Firefox AMO submission guide, store-assets checklist + promo graphic. Removed superseded observation-phase docs.

Validation

  • npm run lint (type-check + ESLint) — clean
  • npm test (unit + build-integrity) — 7/7
  • npm run test:smoke:chromium — passes
  • npm run test:firefox:lint — 0 errors, 0 warnings

Honest limitations

No Twitch ad blocker is perfect: Twitch changes ad delivery frequently, so occasional ads may appear until adblock-config.ts is updated. Live-ad effectiveness is not asserted in CI (non-deterministic) — spot-check with RUN_LIVE=1 npm run test:live:adblock before a release.

wpggLabs added 12 commits June 18, 2026 19:20
Intercept the HLS media playlist inside the Twitch player worker, detect
server-side-inserted ad markers, and substitute an ad-free playlist obtained
with a non-mid-roll player type. Injected into the page MAIN world via a
web-accessible resource, gated by the per-channel whitelist and on/off state.
Reflect real settings state in the popup (fixes controls that ignored stored
values), remove dead mode pills, replace alert() with inline status messages,
show a live blocked-ad counter and current-channel whitelist toggle, and align
all copy with ad blocking rather than passive observation.
Document the ad-blocking architecture and honest limitations, add a GitHub
Pages landing page and Chrome Web Store / Firefox AMO submission guide, and
remove superseded observation-phase reports.
Move the client id, ad markers, player types, and access-token hash into
adblock-config.ts with a signature version, so Twitch-side changes are a
single-file update. No remote config fetch, preserving the local-only guarantee.
Wire the muteDuringAds setting to briefly mute the player while the ad-free
stream loads, with a user toggle in the dashboard.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 671fe5a5fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +43
if (isTwitchWorker && !url.includes("twitchShield") && blockingEnabled()) {
try {
super(buildWorkerBlob(url), options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor pause and whitelist in existing workers

The gate is evaluated only before wrapping a Worker. After a Twitch HLS worker has been constructed, syncSettings only changes data-twitchshield-block in the page; the injected fetch hook inside that already-running worker never reads that attribute or receives the setting. If the user clicks Pause or whitelists the current channel while watching, the existing worker keeps substituting playlists until reload, so ads are still removed even though the UI says they're allowed.

Useful? React with 👍 / 👎.

type: "ad-stripped",
channel
});
return new Response(text, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip ads before reporting the fallback as blocked

When the clean-playlist lookup fails, this path returns text unchanged even though the request got here only after containsAd(text) was true. That means the player still receives the original stitched ad playlist, while the worker posts ad-stripped and the UI can count it as blocked. Either actually remove the stitched segments here or report/pass through as a failure instead of a successful strip.

Useful? React with 👍 / 👎.

Comment on lines 116 to +118
const diagnostics = await TwitchShieldStorage.getDiagnostics();
const adsRemoved = diagnostics.filter((event) => event.eventType === "ad_ui_signal_detected").length;
blockedCount.textContent = String(adsRemoved);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep blocked counts independent of diagnostics

Diagnostics are disabled by default, and record() drops all ad events when diagnostics are off; deriving the popup counter only from stored diagnostics therefore leaves the Ads Blocked card at 0 for default users even when the worker removes ads. Store a privacy-safe aggregate counter separately from the opt-in diagnostic log, or update the UI copy so the count is only advertised when diagnostics are enabled.

Useful? React with 👍 / 👎.

Comment on lines +19 to +20
const result = await chrome.storage.local.get("settings");
return { ...DEFAULT_SETTINGS, ...(result.settings || {}) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore Firefox async API compatibility

This path now selects any environment with chrome.storage.local, including the Firefox build, but then uses the Promise form of chrome.storage.local.get. Firefox exposes the chrome.* compatibility namespace as callback-based unless you use browser.*/a polyfill, so on Firefox this call resolves to undefined and result.settings throws before the popup, content script, or background can load settings. Keep the old callback/browser fallback or route Firefox through a promise wrapper.

Useful? React with 👍 / 👎.

}
record("page_loaded", "Twitch page observation started.");
inspectPage();
new MutationObserver(inspectPage).observe(document.documentElement, { childList: true, subtree: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute channel on Twitch SPA navigations

Twitch changes channels with in-page navigation, but after the initial derivePageContext() this observer only calls inspectPage(), so channel and the whitelist decision remain tied to the first URL until a reload or settings sync. If a user navigates from one channel to another, ads can stay blocked on a newly whitelisted channel (or stay disabled after leaving a whitelisted one), and diagnostics are attributed to the stale channel.

Useful? React with 👍 / 👎.

Comment on lines +26 to +29
script.src = chrome.runtime.getURL("adblock.js");
script.type = "text/javascript";
script.dataset.twitchshield = "engine";
(document.head || document.documentElement).appendChild(script);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install the worker hook before page scripts can run

The main-world hook is now loaded via a dynamically inserted external script, which does not block the parser or wait before Twitch's own early scripts can create the HLS Worker. When the player constructs its worker before adblock.js has fetched and executed, the native Worker is used and no playlist interception is installed for that page load. This needs a synchronous bootstrap or another guarantee that the hook is in place before Twitch can start the player worker.

Useful? React with 👍 / 👎.

wpggLabs added 2 commits July 3, 2026 12:12
Capture the baseline count after the disabled setting propagates rather than
from a stale pre-disable snapshot, so late UI-change events do not flake CI.
@wpggLabs
wpggLabs merged commit 9b8cea7 into main Jul 3, 2026
2 checks passed
@wpggLabs
wpggLabs deleted the feat/twitch-ad-blocker branch July 3, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant