YouTube Ad Auto-Skipper

Prioritize "Soft Skip" (Mute+Seek/Button), fallback to "Reload" with cooldown; uses MutationObserver; reduces detection risk. / 优先“软跳过”(按钮/静音+跳尾),失败时执行带冷却的“重载”;使用 MutationObserver + 兜底定时器;降低被风控概率。

目前為 2025-11-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Ad Auto-Skipper
// @version      2025.11.24
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @match        https://music.youtube.com/*
// @exclude      https://studio.youtube.com/*
// @grant        none
// @license      MIT
// @noframes
// @run-at       document-idle
// @namespace 
// @description Prioritize "Soft Skip" (Mute+Seek/Button), fallback to "Reload" with cooldown; uses MutationObserver; reduces detection risk. / 优先“软跳过”(按钮/静音+跳尾),失败时执行带冷却的“重载”;使用 MutationObserver + 兜底定时器;降低被风控概率。
// ==/UserScript==

(function () {
  'use strict';

  const DEBUG = false;

  // CSS选择器:只隐藏明确的广告元素
  const CSS_HIDE_SELECTORS = [
    '#player-ads',
    '#masthead-ad',
    '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]',
    '.ytp-ad-overlay-container',
    '.ytp-ad-text-overlay',
    '.ytp-ad-image-overlay',
    '.ytp-paid-content-overlay',
    'ytd-merch-shelf-renderer',
    'ytmusic-mealbar-promo-renderer'
  ];

  const CHECK_DEBOUNCE_MS = 100;
  const INTERVAL_CHECK_MS = 1000; // 1秒检查一次保底
  const SEEK_EPSILON = 0.1;
  
  const state = {
    checkTimer: null
  };

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

  const isMobile = location.hostname === 'm.youtube.com';
  const isMusic = location.hostname === 'music.youtube.com';

  function addCss() {
    const sel = CSS_HIDE_SELECTORS.join(',');
    if (!sel) return;
    const style = document.createElement('style');
    // opacity:0 防检测,pointer-events:none 防止点击穿透拦截
    style.textContent = `${sel}{opacity:0 !important; pointer-events:none !important; z-index:-1 !important;}`;
    document.head ? document.head.appendChild(style) : document.documentElement.appendChild(style);
  }

  // 【关键修复】获取播放器容器,限制查找范围
  function getPlayerContainer() {
    return document.querySelector('#movie_player') || 
           document.querySelector('yt-music-player') || 
           document.body;
  }

  function querySkipButton(container) {
    // 1. 优先查找标准的广告跳过按钮 class
    const highPriorityBtn = container.querySelector('.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-ad-skip-button-slot, .ytp-ad-skip-button-container button');
    if (highPriorityBtn) return highPriorityBtn;

    // 2. 只有找不到标准 class 时,才遍历 button 文本
    // 【关键修复】限制只在 container (播放器) 内部查找,不扫描整个 document
    const buttons = container.querySelectorAll('button'); 
    
    // 转换为数组进行查找
    for (const b of buttons) {
      // 忽略不可见按钮,提高性能并减少误判
      if (b.offsetParent === null) continue;

      const t = (b.getAttribute('aria-label') || b.textContent || '').trim();
      // 【关键修复】移除了 'Skip', '繼續' 等太宽泛的词,只匹配明确的广告词
      if (/skip ad|skip ads|跳过广告|跳過廣告/i.test(t)) {
        return b;
      }
    }
    return null;
  }

  function detectAdContext() {
    const container = getPlayerContainer();
    
    // 只在播放器内部查找元素
    const adShowing = !!container.querySelector('.ad-showing');
    const pieCountdown = !!container.querySelector('.ytp-ad-timed-pie-countdown-container');
    const skipBtn = querySkipButton(container);
    
    const adLikely = adShowing || pieCountdown || !!skipBtn;
    return { adLikely, skipBtn, container };
  }

  function trySoftSkip(ctx) {
    // 1. 点击跳过按钮
    if (ctx.skipBtn) {
      const delay = 300 + Math.random() * 500;
      setTimeout(() => {
        try {
          if (ctx.skipBtn && ctx.skipBtn.click) {
            ctx.skipBtn.click();
            log('Clicked skip button');
            // 点击后立即触发下一次检查(应对连续广告)
            setTimeout(() => scheduleCheck(0), 50);
          }
        } catch (_) {}
      }, delay);
      return true;
    }

    // 2. 强制跳过(加速+静音)
    if (ctx.adLikely) {
      const adVideo = ctx.container.querySelector('video.html5-main-video');
      if (adVideo && !Number.isNaN(adVideo.duration) && adVideo.duration > 0) {
         // 只有当确实是广告时才操作
         // 简单的判断:通常广告视频时间很短,或者有 ad-showing 类
         // 为了安全,我们依赖 detectAdContext 的 adLikely 判断
        try {
          const targetTime = Math.max(0, adVideo.duration - SEEK_EPSILON);
          if (adVideo.currentTime < targetTime) {
              adVideo.muted = true;
              adVideo.playbackRate = 16; 
              adVideo.currentTime = targetTime;
              log('Accelerated ad');
              return true;
          }
        } catch (_) {}
      }
    }
    return false;
  }

  function skipAd() {
    const ctx = detectAdContext();
    if (ctx.adLikely) {
      log('Ad detected');
      const handled = trySoftSkip(ctx);
      if (handled) {
          scheduleCheck(1000);
      }
    }
  }

  function scheduleCheck(delayMs = CHECK_DEBOUNCE_MS) {
    if (state.checkTimer) clearTimeout(state.checkTimer);
    state.checkTimer = setTimeout(() => {
      skipAd();
      state.checkTimer = null;
    }, delayMs);
  }

  function setupObserver() {
    // 【关键修复】只监视播放器区域,而不是整个 Body
    // 这避免了点击菜单修改 Body 属性时触发脚本,也减少了性能消耗
    const targetNode = document.querySelector('#movie_player') || document.body;
    
    const observer = new MutationObserver((mutations) => {
      // 简单防抖
      scheduleCheck(50);
    });
    
    observer.observe(targetNode, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['class', 'style', 'src'] // 限制监控的属性
    });
  }

  // === 启动 ===
  addCss();
  
  // 尝试等待播放器加载后再启动 Observer
  const waitTimer = setInterval(() => {
      if (document.querySelector('#movie_player') || document.querySelector('video')) {
          clearInterval(waitTimer);
          setupObserver();
          log('Observer attached to player');
      }
  }, 500);

  // 定时保底
  setInterval(() => scheduleCheck(), INTERVAL_CHECK_MS);
  
  // 立即检查一次
  scheduleCheck(1000);
})();