Greasy Fork

Greasy Fork is available in English.

Bye Bye YouTube Ads - Improved (October Update)

Skip YouTube ads automatically, and block ads more effectively (desktop only). Updated for more robust detection.

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

// ==UserScript==
// @name        Bye Bye YouTube Ads - Improved (October Update)
// @version     3.1
// @description Skip YouTube ads automatically, and block ads more effectively (desktop only). Updated for more robust detection.
// @author      DishantX 
// @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';

  const LOG = false; // set true for debugging in console

  const log = (...args) => { if (LOG) console.log('[ByeByeYTAds]', ...args); };

  // --- 1) CSS: hide common ad overlay elements (non-destructive selectors) ---
  const css = `
    /* in-player overlays/cards/promos */
    .ytp-ad-overlay, .ytp-ad-player-overlay, .ytp-featured-product, .ytp-ad-image-overlay,
    #player-ads, ytd-companion-ad-renderer, ytd-display-ad-renderer, ytd-banner-promo-renderer,
    ytd-promoted-sparkles-text-renderer, ytd-ad-slot-renderer { display: none !important; pointer-events: none !important; }

    /* promoted badges that sometimes overlay thumbnails */
    .ytd-promoted-sparkles-text-renderer, .ytp-ce-element { display: none !important; }

    /* don't hide site-critical elements — be conservative */
  `;
  const styleTag = document.createElement('style');
  styleTag.setAttribute('data-bbye-ads', '1');
  styleTag.textContent = css;
  (document.head || document.documentElement).appendChild(styleTag);

  // --- utilities ---
  function queryAllButtons() {
    return Array.from(document.querySelectorAll('button, a'));
  }

  function isSkipishButton(el) {
    if (!el || el.nodeType !== 1) return false;
    try {
      const aria = (el.getAttribute && el.getAttribute('aria-label')) || '';
      const txt = (el.textContent || '').trim();
      const classes = (el.className || '').toLowerCase();

      // aria label or visible text that indicates a skip/close action
      if (/skip ad|skipads|skip ad(s)?|skip|close ad|close overlay|dismiss ad/i.test(aria + ' ' + txt)) {
        return true;
      }
      // known class fragments
      if (classes.includes('ytp-ad-skip') || classes.includes('overlay-close') || classes.includes('ad-overlay-close') || classes.includes('ad-close')) {
        return true;
      }

    } catch (e) {
      // ignore
    }
    return false;
  }

  function clickSkipButtons() {
    const buttons = queryAllButtons().filter(isSkipishButton);
    if (buttons.length === 0) return false;
    for (const b of buttons) {
      try {
        b.click();
        log('Clicked skipish button', b);
      } catch (e) {
        log('Click failed', e, b);
      }
    }
    return true;
  }

  function closeAdOverlays() {
    const sel = [
      '.ytp-ad-overlay-close-button',
      'button[aria-label*="Close ad"]',
      'button[aria-label*="close ad"]'
    ];
    for (const s of sel) {
      const el = document.querySelector(s);
      if (el) {
        try { el.click(); log('Closed overlay with', s); } catch(e){/*ignore*/ }
        return true;
      }
    }
    return false;
  }

  // Fast-forward / jump strategies
  function jumpAdToEnd(video) {
    if (!video) video = document.querySelector('video');
    if (!video) return false;
    const dur = Number(video.duration);
    if (!isFinite(dur) || dur <= 0) return false;
    // only jump when there's a meaningful distance to skip
    if (video.currentTime >= dur - 0.5) return false;
    try {
      // attempt to jump to end
      video.currentTime = Math.max(0, dur - 0.05);
      // try to play (some players may pause on assignment)
      video.play().catch(()=>{});
      log('jumped to end', video.currentTime, dur);
      return true;
    } catch (e) {
      log('jumpAdToEnd failed', e);
      return false;
    }
  }

  function speedUpAd(video) {
    if (!video) video = document.querySelector('video');
    if (!video) return false;
    try {
      const prev = video.playbackRate || 1;
      // Try a big speed to finish the ad quickly.
      // Some players restrict this — that's why it's a fallback.
      video.playbackRate = Math.max(prev, 16);
      setTimeout(() => {
        try { video.playbackRate = prev; } catch (e) {}
      }, 1200);
      log('temporarily sped playbackRate to skip ad');
      return true;
    } catch (e) {
      log('speedUpAd failed', e);
      return false;
    }
  }

  // Heuristic: detect presence of ad using several signals
  function isAdPlaying() {
    try {
      const player = document.getElementById('movie_player');
      if (player && player.classList && player.classList.contains('ad-showing')) return true;

      // look for known ad elements
      if (document.querySelector('.ytp-ad-player-overlay, .ytp-ad-overlay, ytd-display-ad-renderer, ytd-companion-ad-renderer')) return true;

      // skip button presence implies ad context
      const foundSkip = queryAllButtons().some(isSkipishButton);
      if (foundSkip) return true;

      // If video is present and has a "ad" text overlays or elements near the player
      const adBadge = document.querySelector('ytd-promoted-sparkles-text-renderer, .ytp-ce-element');
      if (adBadge) return true;

    } catch (e) {
      // ignore detection errors
    }
    return false;
  }

  // Main monitor function: try strategies in order
  function handleAdEvent() {
    if (!isAdPlaying()) return false;
    log('Ad detected -> acting');

    // 1) Click any skip-like buttons
    if (clickSkipButtons()) return true;

    // 2) Close overlays
    if (closeAdOverlays()) return true;

    // 3) Jump video to end (most reliable for unskippable ads)
    const vid = document.querySelector('video');
    if (jumpAdToEnd(vid)) return true;

    // 4) Speed up as last resort
    if (speedUpAd(vid)) return true;

    return false;
  }

  // Observe player class changes (movie_player) and DOM additions
  function setupObservers() {
    const player = document.getElementById('movie_player');
    if (player) {
      try {
        const mo = new MutationObserver(muts => {
          for (const m of muts) {
            if (m.type === 'attributes' && m.attributeName === 'class') {
              if (player.classList.contains('ad-showing')) {
                log('player class ad-showing observed');
                setTimeout(handleAdEvent, 50);
              }
            }
            if (m.addedNodes && m.addedNodes.length) {
              // small delay allows YouTube to add skip buttons
              setTimeout(handleAdEvent, 60);
            }
          }
        });
        mo.observe(player, { attributes: true, attributeFilter: ['class'], childList: true, subtree: true });
        log('Attached observer to movie_player');
      } catch (e) {
        log('Failed to observe movie_player', e);
      }
    }

    // Observe document for dynamic ad nodes
    try {
      const bodyObserver = new MutationObserver((muts) => {
        for (const m of muts) {
          if (m.addedNodes && m.addedNodes.length) {
            setTimeout(handleAdEvent, 80);
            break;
          }
        }
      });
      bodyObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
      log('Attached global DOM observer');
    } catch (e) {
      log('Failed to attach global observer', e);
    }
  }

  // Periodic poll as fallback (lightweight)
  const POLL_MS = 900;
  let pollHandle = null;
  function startPolling() {
    if (pollHandle) clearInterval(pollHandle);
    pollHandle = setInterval(() => {
      try {
        if (isAdPlaying()) {
          handleAdEvent();
        }
      } catch (e) { /* swallow */ }
    }, POLL_MS);
    log('Polling started', POLL_MS);
  }

  // Hook into navigation events in YouTube single-page navigation
  function setupNavigationHooks() {
    document.addEventListener('yt-navigate-finish', () => {
      setTimeout(() => {
        handleAdEvent();
      }, 300);
    }, { passive: true });

    // some sites/older clients use pushState
    const pushStateOrig = history.pushState;
    history.pushState = function () {
      try { pushStateOrig.apply(this, arguments); } catch (e) { /* ignore */ }
      setTimeout(handleAdEvent, 300);
    };
  }

  // init
  function init() {
    log('ByeByeYTAds init');
    setupObservers();
    setupNavigationHooks();
    startPolling();

    // One-off immediate attempt (in case the ad is already present)
    setTimeout(handleAdEvent, 400);
  }

  // If DOM not ready wait a bit
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    init();
  } else {
    window.addEventListener('DOMContentLoaded', init, { once: true });
    setTimeout(() => { if (!pollHandle) init(); }, 1500); // fallback
  }

})();