YouTube Ad Auto-Skipper

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

当前为 2025-11-23 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
})();