From 8ce611e9e1ba9c29b29b3e9b954fe09e00b6e963 Mon Sep 17 00:00:00 2001 From: AbrahamOP Date: Thu, 11 Jun 2026 23:56:36 +0200 Subject: [PATCH] feat(youtube): block YouTube ads (player-level skip + ad slots + tracking endpoints) - youtube.js content script: fast-forwards video ads, clicks skip buttons, closes overlays, hides in-feed/banner/masthead ad slots via injected CSS - Respects global toggle, pause mode and per-domain whitelist - 4 new DNR rules: youtube.com/api/stats/ads, /pagead/, /ptracking, googleads.g.doubleclick.net/pagead/id (googlevideo.com left untouched to avoid breaking playback) - CI: youtube.js added to syntax checks - Bump to 1.4.0 --- .github/workflows/ci.yml | 2 +- README.md | 1 + manifest.json | 11 +++- rules.json | 36 +++++++++++ youtube.js | 130 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 youtube.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a6b4b..1b925b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Check JS syntax run: | - for f in background.js content.js popup.js dashboard.js; do + for f in background.js content.js popup.js dashboard.js youtube.js; do node --check "$f" done diff --git a/README.md b/README.md index 26f03d4..c342a9f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ GoaBlockAD blocks ads at two levels: **network requests** are killed before they ## Features - **Network-level blocking** — Requests to Google Ads, Amazon, Criteo, Taboola, Outbrain, and 20+ ad networks are blocked before they reach your browser +- **YouTube ad blocking** — Video ads are auto-skipped at the player level (fast-forward + skip button), in-feed and banner ad slots are hidden, and YouTube ad-tracking endpoints are blocked - **Cosmetic filtering** — Hides empty ad containers, sticky banners, and overlay placeholders that survive network blocking - **Real-time stats** — Live counter of blocked ads in the extension badge and popup - **Dashboard** — View blocked domains, manage filters, customize behavior diff --git a/manifest.json b/manifest.json index 710ac07..b6ac7e5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "GoaBlockAD", - "version": "1.3.1", + "version": "1.4.0", "description": "Une extension simple et efficace pour bloquer les publicités.", "permissions": [ "declarativeNetRequest", @@ -50,6 +50,15 @@ "content.js" ], "run_at": "document_idle" + }, + { + "matches": [ + "*://*.youtube.com/*" + ], + "js": [ + "youtube.js" + ], + "run_at": "document_start" } ] } \ No newline at end of file diff --git a/rules.json b/rules.json index 8cd65b4..c23cbec 100644 --- a/rules.json +++ b/rules.json @@ -295,5 +295,41 @@ "urlFilter": "||smartadserver.com^", "resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame"] } + }, + { + "id": 34, + "priority": 1, + "action": { "type": "block" }, + "condition": { + "urlFilter": "||youtube.com/api/stats/ads", + "resourceTypes": ["image", "xmlhttprequest", "other"] + } + }, + { + "id": 35, + "priority": 1, + "action": { "type": "block" }, + "condition": { + "urlFilter": "||youtube.com/pagead/", + "resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame", "other"] + } + }, + { + "id": 36, + "priority": 1, + "action": { "type": "block" }, + "condition": { + "urlFilter": "||youtube.com/ptracking", + "resourceTypes": ["image", "xmlhttprequest", "other"] + } + }, + { + "id": 37, + "priority": 1, + "action": { "type": "block" }, + "condition": { + "urlFilter": "||googleads.g.doubleclick.net/pagead/id", + "resourceTypes": ["script", "xmlhttprequest", "other"] + } } ] diff --git a/youtube.js b/youtube.js new file mode 100644 index 0000000..ed88663 --- /dev/null +++ b/youtube.js @@ -0,0 +1,130 @@ +// GoaBlockAD - YouTube Ad Blocking +// YouTube serves video ads from the same domains as regular videos (googlevideo.com), +// so network-level blocking can't be used without breaking playback. Instead, this +// script works at the player level: it fast-forwards video ads, clicks skip buttons, +// and hides static ad slots with injected CSS. + +const YT_STYLE_ID = 'goablockad-youtube-style'; +const SKIP_SELECTORS = [ + '.ytp-ad-skip-button', + '.ytp-ad-skip-button-modern', + '.ytp-skip-ad-button', + 'button[class*="ytp-ad-skip"]' +]; +const YT_AD_CSS = ` + #masthead-ad, + ytd-ad-slot-renderer, + ytd-rich-item-renderer:has(ytd-ad-slot-renderer), + ytd-in-feed-ad-layout-renderer, + ytd-display-ad-renderer, + ytd-promoted-sparkles-web-renderer, + ytd-promoted-sparkles-text-search-renderer, + ytd-banner-promo-renderer, + ytd-statement-banner-renderer, + ytd-player-legacy-desktop-watch-ads-renderer, + #player-ads, + ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"], + ytd-merch-shelf-renderer, + ytm-companion-slot, + .ytp-ad-overlay-container, + .ytp-ad-text-overlay { + display: none !important; + } +`; + +let ytActive = false; +let pollTimer = null; +let adInProgress = false; + +function isWhitelisted(whitelist) { + const host = location.hostname.replace(/^www\./, ''); + return (whitelist || []).some(d => host === d || host.endsWith('.' + d)); +} + +function injectStyle() { + if (document.getElementById(YT_STYLE_ID)) return; + const style = document.createElement('style'); + style.id = YT_STYLE_ID; + style.textContent = YT_AD_CSS; + (document.head || document.documentElement).appendChild(style); +} + +function removeStyle() { + document.getElementById(YT_STYLE_ID)?.remove(); +} + +function incrementCount() { + chrome.storage.local.get(['count'], (result) => { + chrome.storage.local.set({ count: (result.count || 0) + 1 }); + }); +} + +function skipVideoAd() { + const player = document.querySelector('.html5-video-player.ad-showing, .html5-video-player.ad-interrupting'); + if (!player) { + adInProgress = false; + return; + } + + // Fast-forward the ad to its end — the player then resumes the real video + const video = player.querySelector('video'); + if (video && isFinite(video.duration) && video.duration > 0 && video.currentTime < video.duration) { + video.muted = true; + video.currentTime = video.duration; + } + + // Click the skip button as soon as it exists (covers non-seekable ads) + for (const sel of SKIP_SELECTORS) { + const btn = player.querySelector(sel); + if (btn) { btn.click(); break; } + } + + // Count each ad once, not every poll tick + if (!adInProgress) { + adInProgress = true; + incrementCount(); + } +} + +function closeOverlays() { + document.querySelector('.ytp-ad-overlay-close-button')?.click(); +} + +function startBlocking() { + if (ytActive) return; + ytActive = true; + injectStyle(); + // YouTube is a SPA and rebuilds the player across navigations; a light poll + // is more reliable here than scoping a MutationObserver to a transient node. + pollTimer = setInterval(() => { + skipVideoAd(); + closeOverlays(); + }, 300); +} + +function stopBlocking() { + if (!ytActive) return; + ytActive = false; + removeStyle(); + if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } + adInProgress = false; +} + +function syncState() { + chrome.storage.local.get(['enabled', 'cosmetic', 'whitelist', 'pausedUntil'], (result) => { + const enabled = result.enabled !== false; + const paused = result.pausedUntil && Date.now() < result.pausedUntil; + if (enabled && !paused && !isWhitelisted(result.whitelist)) { + startBlocking(); + } else { + stopBlocking(); + } + }); +} + +syncState(); + +chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace !== 'local') return; + if (changes.enabled || changes.whitelist || changes.pausedUntil) syncState(); +});