Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -50,6 +50,15 @@
"content.js"
],
"run_at": "document_idle"
},
{
"matches": [
"*://*.youtube.com/*"
],
"js": [
"youtube.js"
],
"run_at": "document_start"
}
]
}
36 changes: 36 additions & 0 deletions rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
]
130 changes: 130 additions & 0 deletions youtube.js
Original file line number Diff line number Diff line change
@@ -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();
});