Stealthy ad-skipper for YouTube: preserves ad DOM nodes, uses delayed/random clicks and temporary speed-up fallback to avoid YouTube ad-block detection overlay. Desktop only.
当前为
// ==UserScript==
// @name Bye Bye YouTube Ads - Stealth Mode
// @version 3.2
// @description Stealthy ad-skipper for YouTube: preserves ad DOM nodes, uses delayed/random clicks and temporary speed-up fallback to avoid YouTube ad-block detection overlay. Desktop only.
// @author DishantX (stealth patch)
// @match *://www.youtube.com/*
// @exclude *://www.youtube.com/*/music*
// @exclude *://music.youtube.com/*
// @exclude *://m.youtube.com/*
// @icon https://tenor.com/view/manifest-meditate-pepe-gif-12464108004541162266
// @license MIT
// @namespace https://greasyfork.org/users/1467023
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
'use strict';
// ---------- CONFIG ----------
const LOG = false; // toggle console debug logs
const MIN_CLICK_DELAY = 500; // ms (after skip button appears)
const MAX_CLICK_DELAY = 1300; // ms
const POLL_MIN = 700; // ms (randomized poll)
const POLL_MAX = 1500; // ms
const SPEED_FALLBACK = 8; // playbackRate used to fast-forward ads as fallback
const SPEED_DURATION_MIN = 800; // ms to keep sped up if we used speed-up (min)
// ----------------------------
const log = (...args) => { if (LOG) console.log('[ByeByeYT-Stealth]', ...args); };
// Inject non-destructive CSS: AD containers remain in DOM but made invisible to user (opacity), not display:none
const stealthCss = `
/* Make ad overlays visually invisible but keep them in DOM.
Important: avoid display:none or removing elements (YouTube checks DOM). */
.ytp-ad-overlay, .ytp-featured-product, .ytp-ad-player-overlay,
#player-ads, ytd-companion-ad-renderer, ytd-display-ad-renderer,
ytd-banner-promo-renderer, ytd-promoted-sparkles-text-renderer,
ytd-ad-slot-renderer {
opacity: 0 !important;
pointer-events: none !important;
/* keep layout and size so YouTube sees them */
transform: none !important;
}
/* For any visually intrusive promoted badges near thumbnails, keep them but reduce visual impact */
.ytd-promoted-sparkles-text-renderer, .ytp-ce-element {
opacity: 0.01 !important;
pointer-events: none !important;
}
/* Floating toggle style */
#bbye-stealth-toggle {
position: fixed;
right: 10px;
bottom: 80px;
z-index: 2147483647;
background: rgba(0,0,0,0.6);
color: white;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
user-select: none;
box-shadow: 0 6px 18px rgba(0,0,0,0.45);
backdrop-filter: blur(4px);
}
#bbye-stealth-toggle[data-enabled="false"] { opacity: 0.45; }
`;
const style = document.createElement('style');
style.setAttribute('data-bbye-stealth', '1');
style.textContent = stealthCss;
(document.head || document.documentElement).appendChild(style);
// ---- utilities ----
function randBetween(min, max) {
return Math.floor(min + Math.random() * (max - min));
}
function qsAll(selector) {
try { return Array.from(document.querySelectorAll(selector)); } catch (e) { return []; }
}
// Detect skip-like buttons robustly using aria-label / text / role
function isSkipButton(el) {
if (!el || el.nodeType !== 1) return false;
try {
const aria = (el.getAttribute && el.getAttribute('aria-label')) || '';
const txt = (el.textContent || '').trim();
const role = (el.getAttribute && el.getAttribute('role')) || '';
const cls = (el.className || '').toLowerCase();
// visible text or aria hint
if (/skip ad|skipads|skip ad(s)?|skip this ad|skip|close ad|close overlay|dismiss ad/i.test(aria + ' ' + txt)) return true;
// known role/class hints
if (role === 'button' && /skip|ad-skip|ad_skip|ytp-ad-skip/.test(cls)) return true;
// some buttons have exact label "Skip ad"
if (/^skip ad$/i.test(txt)) return true;
} catch (e) { /* ignore */ }
return false;
}
function findSkipButtons() {
// broad search: buttons and links near the player
const candidates = qsAll('button, a[role="button"], div[role="button"]');
return candidates.filter(isSkipButton);
}
// Click element with a small randomized human-like delay
function clickWithHumanDelay(el) {
if (!el) return false;
const delay = randBetween(MIN_CLICK_DELAY, MAX_CLICK_DELAY);
setTimeout(() => {
try {
el.click();
log('Clicked skip-like button after delay', delay, el);
} catch (e) {
log('Click failed', e, el);
}
}, delay);
return true;
}
// Speed-up fallback (stealthy): temporarily increase playbackRate (less suspicious than jumping currentTime)
let _speedState = { active: false, prevRate: 1, restoreTimeout: null };
function speedUpVideoTemporary(video) {
if (!video) video = document.querySelector('video');
if (!video) return false;
try {
if (_speedState.active) return true; // already active
const prev = (video.playbackRate && video.playbackRate > 0) ? video.playbackRate : 1;
_speedState.prevRate = prev;
// Choose a high speed but not astronomical — large speeds like 8 are effective and less suspicious than instant skip
video.playbackRate = Math.max(prev, SPEED_FALLBACK);
_speedState.active = true;
log('sped up video from', prev, 'to', video.playbackRate);
// keep minimum duration in case ad is short; will be restored when ad ends or after minimum
_speedState.restoreTimeout = setTimeout(() => {
restoreVideoSpeed(video);
}, SPEED_DURATION_MIN);
return true;
} catch (e) {
log('speedUpVideoTemporary failed', e);
return false;
}
}
function restoreVideoSpeed(video) {
if (!video) video = document.querySelector('video');
if (!video) return;
try {
if (_speedState.restoreTimeout) {
clearTimeout(_speedState.restoreTimeout);
_speedState.restoreTimeout = null;
}
if (_speedState.active) {
video.playbackRate = _speedState.prevRate || 1;
_speedState.active = false;
log('restored playbackRate to', video.playbackRate);
}
} catch (e) { log('restoreVideoSpeed failed', e); }
}
// Heuristic: is an ad playing or ad context present?
function isAdContext() {
try {
const player = document.getElementById('movie_player');
if (player && player.classList && player.classList.contains('ad-showing')) return true;
if (document.querySelector('.ytp-ad-player-overlay, .ytp-ad-overlay, ytd-display-ad-renderer, ytd-companion-ad-renderer')) return true;
// skip button presence indicates ad context
if (findSkipButtons().length > 0) return true;
// some promoted badge indicates ad context
if (document.querySelector('ytd-promoted-sparkles-text-renderer, .ytp-ce-element')) return true;
} catch (e) { /* ignore */ }
return false;
}
// Handle ad: try skip buttons (delayed), close overlays, then speed-up fallback
function handleAdOnce() {
if (!_enabled) return false;
if (!isAdContext()) {
// if we had sped up, restore
const vid = document.querySelector('video');
if (vid) restoreVideoSpeed(vid);
return false;
}
log('Ad context detected -> attempting stealth actions');
// 1) click skip-like buttons with small human delay
const skips = findSkipButtons();
if (skips.length) {
for (const b of skips) clickWithHumanDelay(b);
// give YouTube a moment — restore speed only if no skip appears after delay
setTimeout(() => {
// if still ad-playing and no skip used, fallback to speed
if (isAdContext()) {
const vid = document.querySelector('video');
if (vid) speedUpVideoTemporary(vid);
}
}, MAX_CLICK_DELAY + 150);
return true;
}
// 2) try to close overlay-ish elements via 'close' aria labels (with delay)
const closeCandidates = qsAll('button, a').filter(el => {
try {
const aria = (el.getAttribute && el.getAttribute('aria-label')) || '';
const txt = (el.textContent || '').trim();
return /close ad|close overlay|dismiss ad|close/i.test(aria + ' ' + txt);
} catch (e) { return false; }
});
if (closeCandidates.length) {
for (const c of closeCandidates) clickWithHumanDelay(c);
return true;
}
// 3) fallback: temporarily speed up playback (stealthy)
const vid = document.querySelector('video');
if (vid) {
speedUpVideoTemporary(vid);
return true;
}
return false;
}
// Randomized polling loop (self-scheduling)
let _pollHandle = null;
function startRandomPolling() {
if (_pollHandle) return;
function loop() {
try {
if (_enabled) handleAdOnce();
} catch (e) { log('poll loop err', e); }
const next = randBetween(POLL_MIN, POLL_MAX);
_pollHandle = setTimeout(loop, next);
}
loop();
log('started randomized polling');
}
function stopRandomPolling() {
if (_pollHandle) {
clearTimeout(_pollHandle);
_pollHandle = null;
}
}
// Observe movie_player class changes and DOM insertions to react faster
let _playerObserver = null;
function attachObservers() {
const player = document.getElementById('movie_player');
if (player && !_playerObserver) {
try {
_playerObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
// attribute change for 'class' -> ad-showing could be set
if (m.type === 'attributes' && m.attributeName === 'class') {
if (player.classList.contains('ad-showing')) {
setTimeout(() => { if (_enabled) handleAdOnce(); }, 60);
} else {
// ad ended: restore speed if needed
const vid = document.querySelector('video');
if (vid) restoreVideoSpeed(vid);
}
}
// node additions near player might include skip buttons
if (m.addedNodes && m.addedNodes.length) {
setTimeout(() => { if (_enabled) handleAdOnce(); }, 80);
}
}
});
_playerObserver.observe(player, { attributes: true, attributeFilter: ['class'], childList: true, subtree: true });
log('player observer attached');
} catch (e) { log('attachObservers failed', e); }
}
// global observer for new nodes (lightweight)
try {
const gobs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.addedNodes && m.addedNodes.length) {
setTimeout(() => { if (_enabled) handleAdOnce(); }, 120);
break;
}
}
});
gobs.observe(document.documentElement || document.body, { childList: true, subtree: true });
log('global observer attached');
} catch (e) { log('global observer failed', e); }
}
// Hook into YouTube navigation so script re-applies on page change
function hookNavigation() {
document.addEventListener('yt-navigate-finish', () => {
setTimeout(() => { if (_enabled) handleAdOnce(); }, 300);
}, { passive: true });
const push = history.pushState;
history.pushState = function () {
const res = push.apply(this, arguments);
setTimeout(() => { if (_enabled) handleAdOnce(); }, 300);
return res;
};
}
// ------------ UI toggle ----------------
let _enabled = true;
function createToggle() {
if (document.getElementById('bbye-stealth-toggle')) return;
const t = document.createElement('div');
t.id = 'bbye-stealth-toggle';
t.title = 'Toggle ByeByeYT Stealth Mode';
t.textContent = 'ByeByeYT: ON';
t.setAttribute('data-enabled', 'true');
t.addEventListener('click', () => {
_enabled = !_enabled;
t.textContent = _enabled ? 'ByeByeYT: ON' : 'ByeByeYT: OFF';
t.setAttribute('data-enabled', _enabled ? 'true' : 'false');
if (_enabled) {
startRandomPolling();
handleAdOnce();
} else {
stopRandomPolling();
const v = document.querySelector('video');
if (v) restoreVideoSpeed(v);
}
}, { passive: true });
document.documentElement.appendChild(t);
}
// --------------- init -----------------
function init() {
attachObservers();
hookNavigation();
createToggle();
startRandomPolling();
// small initial attempt
setTimeout(() => { if (_enabled) handleAdOnce(); }, 400);
}
if (document.readyState === 'complete' || document.readyState === 'interactive') init();
else window.addEventListener('DOMContentLoaded', init, { once: true });
})();