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
40 changes: 39 additions & 1 deletion content.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
/* GoaBlockAD - Content CSS */
/* We inject styles via JS to allow toggling, but this file is reserved for static overrides if needed. */
/* We inject styles via JS to allow toggling, but this file is reserved for static overrides if needed. */

/* YouTube: hide the video and overlays while an ad is playing so the user
doesn't see the brief flash before our auto-skip kicks in. The video
element keeps playing underneath so fast-forward still works. */
.html5-video-player.ad-showing video,
.html5-video-player.ad-interrupting video {
opacity: 0 !important;
}

.html5-video-player.ad-showing .ytp-ad-player-overlay,
.html5-video-player.ad-showing .ytp-ad-player-overlay-layout,
.html5-video-player.ad-showing .ytp-ad-image-overlay,
.html5-video-player.ad-showing .ytp-ad-overlay-container,
.html5-video-player.ad-showing .ytp-ad-text-overlay,
.html5-video-player.ad-showing .ytp-ad-preview-container,
.html5-video-player.ad-interrupting .ytp-ad-player-overlay,
.html5-video-player.ad-interrupting .ytp-ad-player-overlay-layout {
display: none !important;
}

/* Hide every overlay/UI piece YouTube layers on top of an ad: the entire ad
module, the "promoted/sponsored" cards, action companions, surveys, etc.
The skip button is preserved in case the click programmatique is ignored
and the user wants to click it manually. */
.video-ads,
.ytp-ad-module,
ytd-player[hidden-by-goablockad],
.ytp-ad-action-interstitial,
.ytp-ad-action-interstitial-background-container,
.ytp-ad-action-interstitial-card,
.ytp-ad-action-interstitial-image,
.ytp-ad-action-interstitial-slot,
.ytp-flyout-cta,
.ytp-ad-survey,
.ytp-ad-survey-questions,
ytmusic-ad-slot-renderer {
display: none !important;
}
178 changes: 177 additions & 1 deletion content.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,27 @@ const adSelectors = [
'.ad-container',
'.adsbox',
'#ad-sidebar',
'.sponsored-content'
'.sponsored-content',
// YouTube
'ytd-display-ad-renderer',
'ytd-promoted-video-renderer',
'ytd-promoted-sparkles-web-renderer',
'ytd-in-feed-ad-layout-renderer',
'ytd-ad-slot-renderer',
'ytd-banner-promo-renderer',
'ytd-statement-banner-renderer',
'ytd-companion-slot-renderer',
'ytd-action-companion-ad-renderer',
'ytd-merch-shelf-renderer',
'ytd-rich-section-renderer:has(ytd-statement-banner-renderer)',
'#masthead-ad',
'#player-ads',
'.ytp-ad-overlay-slot',
'.ytp-ad-overlay-container',
'.ytp-featured-product',
'.ytp-suggested-action',
'tp-yt-paper-dialog:has(yt-mealbar-promo-renderer)',
'ytmusic-statement-banner-renderer'
];

function applyCosmeticFiltering() {
Expand Down Expand Up @@ -63,3 +83,159 @@ chrome.storage.onChanged.addListener((changes, namespace) => {
applyCosmeticFiltering();
}
});

// ─────────────────────────────────────────────
// YouTube video ad auto-skip
// Runs only on YouTube. Clicks the skip button when available, and fast-forwards
// non-skippable ads by jumping the player to the end of the ad clip.
// Independent of the cosmetic toggle — gated only by the master `enabled` flag.
// ─────────────────────────────────────────────
(() => {
const host = location.hostname;
if (!/(^|\.)youtube\.com$/.test(host) && !/(^|\.)youtube-nocookie\.com$/.test(host)) return;

const LOG = '[GoaBlockAD/yt]';
const SKIP_SELECTORS = [
'.ytp-ad-skip-button',
'.ytp-ad-skip-button-modern',
'.ytp-skip-ad-button',
'.ytp-ad-skip-button-container button',
'.ytp-skip-ad button',
'button[class*="ytp-ad-skip"]',
'button[class*="ytp-skip-ad"]'
];

let enabled = true;
chrome.storage.local.get(['enabled'], (r) => {
enabled = r.enabled !== false;
console.log(LOG, 'init, enabled =', enabled);
});
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && changes.enabled) {
enabled = changes.enabled.newValue !== false;
}
});

function isAdPlaying(player) {
return !!player && (
player.classList.contains('ad-showing') ||
player.classList.contains('ad-interrupting')
);
}

function clickSkipButtons() {
for (const sel of SKIP_SELECTORS) {
const btns = document.querySelectorAll(sel);
for (const btn of btns) {
try { btn.click(); } catch (_) { /* ignore */ }
}
}
}

// Per-video state. Reset whenever the underlying <video> swaps src.
const videoState = new WeakMap();

function getState(video) {
let s = videoState.get(video);
if (!s) {
s = { skippedSrc: null, suspendUntil: 0 };
videoState.set(video, s);
}
return s;
}

function fastForwardAd(video) {
if (!video) return;
const state = getState(video);
// Guard: after a src swap (loadstart), YouTube may not have cleared
// the `ad-showing` class yet — wait it out so we don't fast-forward
// the real video by mistake.
if (Date.now() < state.suspendUntil) return;
// Only skip once per ad src to avoid bleeding into the real video.
if (state.skippedSrc === video.src) return;
if (!isFinite(video.duration) || video.duration <= 0) return;
try {
state.skippedSrc = video.src;
video.muted = true;
video.currentTime = video.duration;
chrome.storage.local.get(['count'], (r) => {
chrome.storage.local.set({ count: (r.count || 0) + 1 });
});
} catch (_) { /* ignore */ }
}

function muteIfAd(video) {
if (video && !video.muted) {
try { video.muted = true; } catch (_) { /* ignore */ }
}
}

function tick() {
if (!enabled) return;
const player = document.querySelector('.html5-video-player, #movie_player');
if (!player) return;
if (isAdPlaying(player)) {
const video = player.querySelector('video');
muteIfAd(video);
fastForwardAd(video);
clickSkipButtons();
}
}

// Hook the video element's own timeupdate event — fires while the ad is
// playing, even when our MutationObserver misses the class transition.
const videoListeners = new WeakSet();
function attachVideoListener(video) {
if (!video || videoListeners.has(video)) return;
videoListeners.add(video);

// When the underlying media changes (next ad or real video), restore
// playback state so previous mutations don't bleed across, and arm a
// short suspension window before allowing another skip.
video.addEventListener('loadstart', () => {
const s = getState(video);
s.skippedSrc = null;
s.suspendUntil = Date.now() + 150;
try {
video.playbackRate = 1;
// Only unmute if we're transitioning OUT of an ad. While the
// player is still in ad mode (chained ads), keep audio muted.
const player = video.closest('.html5-video-player');
if (!isAdPlaying(player)) {
video.muted = false;
}
} catch (_) { /* ignore */ }
});

video.addEventListener('timeupdate', () => {
if (!enabled) return;
const player = video.closest('.html5-video-player');
if (isAdPlaying(player)) {
muteIfAd(video);
fastForwardAd(video);
clickSkipButtons();
}
});
console.log(LOG, 'attached timeupdate listener to <video>');
}

function findVideos() {
document.querySelectorAll('video').forEach(attachVideoListener);
}

const observer = new MutationObserver(() => {
findVideos();
tick();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'src']
});

// Initial pass + backup interval
findVideos();
tick();
setInterval(() => { findVideos(); tick(); }, 500);
})();
12 changes: 12 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
"content.js"
],
"run_at": "document_idle"
},
{
"matches": [
"*://*.youtube.com/*",
"*://*.youtube-nocookie.com/*"
],
"js": [
"youtube-inject.js"
],
"run_at": "document_start",
"world": "MAIN",
"all_frames": true
}
]
}
45 changes: 45 additions & 0 deletions rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,50 @@
"urlFilter": "||smartadserver.com^",
"resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame"]
}
},
{
"id": 34,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||youtube.com/api/stats/ads",
"resourceTypes": ["xmlhttprequest", "image", "ping"]
}
},
{
"id": 35,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||youtube.com/pagead/",
"resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame", "ping"]
}
},
{
"id": 36,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||youtube.com/ptracking",
"resourceTypes": ["xmlhttprequest", "image", "ping"]
}
},
{
"id": 37,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||googleads.g.doubleclick.net^",
"resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame"]
}
},
{
"id": 38,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||static.doubleclick.net^",
"resourceTypes": ["script", "image", "xmlhttprequest", "sub_frame"]
}
}
]
Loading