YouTube Ad Auto-Skipper (VoidMuser) / YouTube 广告自动跳过

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 (VoidMuser) / YouTube 广告自动跳过
// @namespace    
// @version      1.0.0
// @description  Prioritize "Soft Skip" (Mute+Seek/Button), fallback to "Reload" with cooldown; uses MutationObserver; reduces detection risk. / 优先“软跳过”(按钮/静音+跳尾),失败时执行带冷却的“重载”;使用 MutationObserver + 兜底定时器;降低被风控概率。
// @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
// ==/UserScript==

(function () {
  'use strict';

  /*********************
   * Adjustable Parameters (Modify as needed)
   * 可调参数(按需修改)
   *********************/
  const DEBUG = false;                         // Debug switch: true to output logs / 调试开关:true 输出调试日志
  const CSS_HIDE_SELECTORS = [                 // Safe hiding of ads/promos (CSS only, no DOM removal) / 安全隐藏的广告/促销区域(仅样式隐藏,不删 DOM)
    '#player-ads',
    '#masthead-ad',
    '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]',
    '.ytp-featured-product',
    '.ytp-ad-overlay-container',
    '.ytp-ad-text-overlay',
    '.ytp-ad-image-overlay',
    '.ytp-paid-content-overlay',
    '.yt-mealbar-promo-renderer',
    'ytd-merch-shelf-renderer',
    'ytmusic-mealbar-promo-renderer',
    'ytmusic-statement-banner-renderer'
  ];
  const REMOVE_PAIRS = [                       // Ad blocks to try removing only on "Non-Shorts" pages (avoid removing core containers) / 仅在“非 Shorts”页面尝试移除的广告块
    // Format: ['Outer Selector', 'Inner Selector required to confirm it is an ad']
    // 格式:['外层选择器', '其内必须存在的子元素选择器(用于确认确实是广告块)']
    // Example: ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer']
  ];
  const CHECK_DEBOUNCE_MS = 150;               // Debounce delay for triggering checks / 触发检测的去抖延时
  const INTERVAL_CHECK_MS = 2000;              // Fallback timer: Ad detection / 兜底定时:广告检测
  const INTERVAL_CLEAN_MS = 4000;              // Fallback timer: DOM cleanup / 兜底定时:清理广告 DOM
  const RELOAD_BASE_COOLDOWN_MS = 2000;        // Reload cooldown base value (base for exponential backoff) / 重载冷却基础值(指数回退的底数)
  const RELOAD_MAX_BACKOFF_MS = 30000;         // Max reload backoff time / 重载冷却最大回退时间
  const SEEK_EPSILON = 0.25;                   // Seconds to keep when seeking to end (prevents player freeze) / 跳到尾部时保留的秒数,避免部分播放器卡死

  /*********************
   * Internal State
   * 内部状态
   *********************/
  const state = {
    skipping: false,            // Re-entry lock / 防重入锁
    lastReloadAt: 0,            // Last reload timestamp / 上次重载时间戳
    reloadAttempts: 0,          // Consecutive reload attempts for same signature / 同一签名的连续重载尝试次数
    lastReloadSignature: '',    // Last reload signature (videoId + progress bucket) / 上次重载签名(videoId + 进度桶)
    scheduled: false,           // Debounce schedule flag / 去抖调度标记
  };

  /*********************
   * Helper Methods
   * 便捷方法
   *********************/
  const log = (...args) => { if (DEBUG) console.log('[ASYA]', ...args); };
  const now = () => Date.now();
  const timeStr = () => new Date().toTimeString().split(' ', 1)[0];

  const isMobile = location.hostname === 'm.youtube.com';
  const isMusic  = location.hostname === 'music.youtube.com';
  const isShorts = () => location.pathname.startsWith('/shorts/');

  function addCss() {
    const sel = CSS_HIDE_SELECTORS.join(',');
    if (!sel) return;
    const style = document.createElement('style');
    style.textContent = `${sel}{display:none !important;}`;
    document.head ? document.head.appendChild(style) : document.documentElement.appendChild(style);
  }

  // Safe removal only on non-Shorts pages to prevent scrolling issues
  // 仅在非 Shorts 页面做“安全移除”,避免误删导致滑动异常
  function removeAdElements() {
    if (isShorts()) return;
    for (const [outerSel, innerSel] of REMOVE_PAIRS) {
      const outer = document.querySelector(outerSel);
      if (!outer) continue;
      const inner = outer.querySelector(innerSel);
      if (!inner) continue;
      outer.remove();
      log('Removed ad block / 移除广告块:', outerSel, 'contains / 包含', innerSel);
    }
  }

  // Query Skip Button (Compatible with various forms)
  // 查询跳过按钮(兼容不同形态)
  function querySkipButton() {
    const byClass = document.querySelector(
      '.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-ad-skip-button-container button'
    );
    if (byClass) return byClass;
    // Fallback: check aria-label or text content / aria 或 文本兜底
    const btn = [...document.querySelectorAll('button')].find(b => {
      const t = (b.getAttribute('aria-label') || b.textContent || '').trim();
      return /skip ad|skip ads|跳过广告/i.test(t);
    });
    return btn || null;
  }

  // Detect Ad Context
  // 广告上下文探测
  function detectAdContext() {
    const adShowing    = !!document.querySelector('.ad-showing');
    const pieCountdown = !!document.querySelector('.ytp-ad-timed-pie-countdown-container');
    const survey       = !!document.querySelector('.ytp-ad-survey-questions');
    const skipBtn      = querySkipButton();
    const adLikely     = adShowing || pieCountdown || survey || !!skipBtn;
    return { adShowing, pieCountdown, survey, skipBtn, adLikely };
  }

  // Get Player References (Try to return player object)
  // 获取播放器相关引用(尽量返回 player 对象)
  function getPlayers() {
    const moviePlayerEl = document.querySelector('#movie_player') || null;
    let playerEl = null;
    let player = null;

    if (isMobile || isMusic) {
      playerEl = moviePlayerEl;
      player   = moviePlayerEl;
    } else {
      const ytd = document.querySelector('#ytd-player');
      playerEl = ytd || moviePlayerEl || null;
      if (ytd && typeof ytd.getPlayer === 'function') {
        try { player = ytd.getPlayer(); } catch (_) { /* Ignore / 忽略 */ }
      }
      if (!player && moviePlayerEl) player = moviePlayerEl;
    }
    return { moviePlayerEl, playerEl, player };
  }

  // Attempt reload API on candidates; fallback to seekTo
  // 统一在若干候选对象上尝试重载 API;不行则退化为 seekTo
  function safeLoadByVars(players, videoId, start) {
    const list = [players.player, players.playerEl, players.moviePlayerEl].filter(Boolean);
    for (const p of list) {
      if (typeof p.loadVideoWithPlayerVars === 'function') {
        p.loadVideoWithPlayerVars({ videoId, start });
        return true;
      }
      if (typeof p.loadVideoByPlayerVars === 'function') {
        p.loadVideoByPlayerVars({ videoId, start });
        return true;
      }
    }
    if (players.player && typeof players.player.seekTo === 'function') {
      players.player.seekTo(start, true);
      return true;
    }
    return false;
  }

  // Restore subtitles if they were previously enabled
  // 尝试恢复字幕(仅当此前为开启状态)
  function restoreSubtitlesIfNeeded(moviePlayerEl, wantOn) {
    if (!moviePlayerEl) return;
    if (typeof moviePlayerEl.isSubtitlesOn !== 'function' ||
        typeof moviePlayerEl.toggleSubtitlesOn !== 'function') return;

    const start = now();
    const timer = setInterval(() => {
      // Timeout and give up / 超时放弃
      if (now() - start > 5000) { clearInterval(timer); return; }
      try {
        const cur = !!moviePlayerEl.isSubtitlesOn();
        if (wantOn && !cur) {
          moviePlayerEl.toggleSubtitlesOn();
          clearInterval(timer);
        } else if (!wantOn && cur) {
          moviePlayerEl.toggleSubtitlesOn();
          clearInterval(timer);
        } else {
          // State matches, finish / 状态一致,结束
          clearInterval(timer);
        }
      } catch (_) { /* Ignore, wait for next tick / 忽略,等待下次 */ }
    }, 250);
  }

  // Soft Skip Strategy: Button -> Mute+SeekEnd (Only if definitely ad) -> Player seek
  // 软跳过策略:按钮 → 静音+跳尾(仅在确实处于广告态) → 播放器 seek
  function trySoftSkip(players, ctx) {
    // 1) Click "Skip Ad" button / 点击“跳过广告”按钮
    if (ctx.skipBtn) {
      try { ctx.skipBtn.click(); log('Clicked Skip Button / 点击跳过按钮'); return true; } catch (_) { /* Ignore / 忽略 */ }
    }

    // 2) In Ad-mode: Mute + Seek to End / Small Speedup
    // 广告态下尝试静音+跳尾 / 小幅提速
    if (ctx.adLikely) {
      const adVideo = document.querySelector('video.html5-main-video');
      if (adVideo && isFinite(adVideo.duration) && adVideo.duration > 0) {
        try {
          adVideo.muted = true;
          adVideo.currentTime = Math.max(0, adVideo.duration - SEEK_EPSILON);
          log('Muted and sought to end / 静音并跳到广告尾部');
          return true;
        } catch (_) { /* Some ads block seeking, ignore / 某些广告禁止 seek,忽略 */ }

        try {
          adVideo.playbackRate = 16;
          adVideo.muted = true;
          log('Temp speedup (Not success yet, fallback follows) / 临时小幅提速(不可视为成功,交给后续兜底)');
        } catch (_) { /* Ignore / 忽略 */ }
      }
    }

    // 3) Player seek to current time (Lighter than reload)
    // 播放器 seek 到当前进度(比重载更轻)
    if (players.player &&
        typeof players.player.seekTo === 'function' &&
        typeof players.player.getCurrentTime === 'function') {
      try {
        const start = Math.floor(players.player.getCurrentTime());
        players.player.seekTo(start, true);
        log('Used player.seekTo soft skip / 使用 player.seekTo 软跳过');
        return true;
      } catch (_) { /* Ignore / 忽略 */ }
    }

    return false;
  }

  // Heavy Reload Fallback with Cooldown & Exponential Backoff
  // 带冷却与指数回退的重载兜底
  function tryHeavyReload(players) {
    if (!players.player ||
        typeof players.player.getVideoData !== 'function' ||
        typeof players.player.getCurrentTime !== 'function') {
      return false;
    }

    const data  = players.player.getVideoData();
    const vid   = data && data.video_id;
    const start = Math.floor(players.player.getCurrentTime());
    if (!vid || !Number.isFinite(start)) return false;

    // Generate Signature (Bucket start time to reduce unnecessary "new scene" checks)
    // 生成签名(将 start 粗略分桶,减少不必要的“新场景”判定)
    const signature = `${vid}:${Math.floor(start / 5)}`;
    const nowTs = now();

    // Check Cooldown / Backoff
    // 判断冷却/回退
    if (signature === state.lastReloadSignature) {
      const backoff = Math.min(RELOAD_MAX_BACKOFF_MS, RELOAD_BASE_COOLDOWN_MS * Math.pow(2, state.reloadAttempts));
      if (nowTs - state.lastReloadAt < backoff) {
        log('Reload Cooldown, skipping: / 重载冷却中,跳过本次:', backoff - (nowTs - state.lastReloadAt), 'ms');
        return false;
      }
    } else {
      // New signature, reset attempts
      // 新签名,重置计数
      state.reloadAttempts = 0;
    }

    // Record subtitle state to restore after reload
    // 记录字幕状态,重载后尽量恢复
    let wantSubsOn = false;
    if (players.moviePlayerEl &&
        typeof players.moviePlayerEl.isSubtitlesOn === 'function') {
      try { wantSubsOn = !!players.moviePlayerEl.isSubtitlesOn(); } catch (_) { /* Ignore / 忽略 */ }
    }

    // Execute Reload (Unified via player / movie_player)
    // 真正的重载调用(统一走 player / movie_player)
    const ok = safeLoadByVars(players, vid, start);
    if (ok) {
      state.lastReloadSignature = signature;
      state.lastReloadAt = nowTs;
      state.reloadAttempts += 1;
      log('Executed Heavy Reload thru Ad / 执行重载穿过广告:', { vid, start, attempts: state.reloadAttempts, t: timeStr() });

      // Restore Subtitles Async
      // 异步恢复字幕
      restoreSubtitlesIfNeeded(players.moviePlayerEl, wantSubsOn);
      return true;
    }
    return false;
  }

  // Main Logic
  // 主流程
  function skipAd() {
    if (isShorts()) return;             // Do not intervene in Shorts to avoid accidental breaks / Shorts 先不介入以免误伤
    if (state.skipping) return;         // Prevent Re-entry / 防重入
    state.skipping = true;

    try {
      const ctx = detectAdContext();
      if (!ctx.adLikely) return;

      const players = getPlayers();
      if (!players.player && !players.playerEl && !players.moviePlayerEl) return;

      // Try "Soft Skip" first
      // 先尝试“软跳过”
      const softOK = trySoftSkip(players, ctx);
      if (softOK) return;

      // Soft skip failed -> Try Heavy Reload with Cooldown
      // 软跳过失败 → 带冷却的重载兜底
      tryHeavyReload(players);
    } finally {
      state.skipping = false;
    }
  }

  // Debounced Schedule
  // 去抖调度
  function scheduleCheck(delay = CHECK_DEBOUNCE_MS) {
    if (state.scheduled) return;
    state.scheduled = true;
    setTimeout(() => {
      state.scheduled = false;
      skipAd();
    }, delay);
  }

  // Observer: Monitor DOM changes that might trigger ads
  // 观察器:监控可能引发广告状态变化的 DOM 改动
  function setupObserver() {
    const target = document.body || document.documentElement;
    if (!target) return;

    const mo = new MutationObserver(() => {
      scheduleCheck(50);
    });
    mo.observe(target, {
      attributes: true,
      childList: true,
      subtree: true
    });
  }

  /*********************
   * Start / 启动
   *********************/
  addCss();
  removeAdElements();        // Initial Cleanup (Safe) / 初次清理(安全)
  setupObserver();           // Observe DOM / 观察 DOM 变化
  scheduleCheck(0);          // Try immediately / 立即尝试一次

  // Fallback Interval (Low Frequency): Ensure check/cleanup in edge cases
  // 兜底定时器(低频):确保在异常情况下仍能周期性检查/清理
  setInterval(() => scheduleCheck(), INTERVAL_CHECK_MS);
  setInterval(() => removeAdElements(), INTERVAL_CLEAN_MS);
})();