Greasy Fork

Greasy Fork is available in English.

Bye Bye YouTube Ads - Stealth Mode

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.

当前为 2025-10-02 提交的版本,查看 最新版本

// ==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   http://greasyfork.icu/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 });

})();