YouTube Ad Muter + Button Auto-Skip (VoidMuser)

不隐藏广告;在检测到广告时自动静音;当出现“跳过广告”按钮时自动点击,仅使用官方按钮,不再快进/重载视频。

当前为 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 Muter + Button Auto-Skip (VoidMuser)
// @namespace    
// @version      2.0.0
// @description  不隐藏广告;在检测到广告时自动静音;当出现“跳过广告”按钮时自动点击,仅使用官方按钮,不再快进/重载视频。
// @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 输出调试日志

  // ✅ 1. 不再隐藏广告:这里改为空数组,就不会再通过 CSS 隐藏任何广告元素
  const CSS_HIDE_SELECTORS = [];               // No ad hiding / 不隐藏广告

  // ✅ 2. 不再移除广告 DOM
  const REMOVE_PAIRS = [];                     // No DOM removal / 不移除广告 DOM

  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;        // 保留但不再使用重载逻辑
  const RELOAD_MAX_BACKOFF_MS = 30000;         // 同上
  const SEEK_EPSILON = 0.25;                   // 保留(已不再用来跳广告)

  // 连环多广告检测参数(保留,用于连续广告时多次检查按钮)
  const CHAIN_SKIP_MAX = 4;
  const CHAIN_SKIP_DELAY_MS = 800;

  /*********************
   * Internal State
   * 内部状态
   *********************/
  const state = {
    skipping: false,            // Re-entry lock / 防重入锁
    lastReloadAt: 0,            // 已不再使用重载,但先保留结构
    reloadAttempts: 0,
    lastReloadSignature: '',
    scheduled: false,           // Debounce schedule flag / 去抖调度标记

    // ✅ 新增:记录“广告静音”前的视频状态
    adMuted: false,
    prevMuted: null,
    prevVolume: null,
  };

  /*********************
   * 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);
  }

  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);
    }
  }

  // 查询跳过按钮(兼容不同形态)
  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;
    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;
  }

  // 广告上下文探测
  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 };
  }

  // 获取播放器引用
  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 (_) {}
      }
      if (!player && moviePlayerEl) player = moviePlayerEl;
    }
    return { moviePlayerEl, playerEl, player };
  }

  //(原重载相关函数保留但不再调用)
  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;
  }

  function restoreSubtitlesIfNeeded(moviePlayerEl, wantOn) {
    if (!moviePlayerEl) return;
    if (typeof moviePlayerEl.isSubtitlesOn !== 'function' ||
        typeof moviePlayerEl.toggleSubtitlesOn !== 'function') return;

    const start = now();
    const timer = setInterval(() => {
      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 {
          clearInterval(timer);
        }
      } catch (_) {}
    }, 250);
  }

  /*********************
   * ✅ 新增:只负责“广告时静音 / 非广告时还原”
   *********************/
  function ensureAdMute(ctx) {
    const video = document.querySelector('video.html5-main-video');
    if (!video) return;

    if (ctx.adLikely) {
      // 进入广告:记录当前静音与音量,并静音
      if (!state.adMuted) {
        state.adMuted = true;
        state.prevMuted = video.muted;
        state.prevVolume = video.volume;
        if (DEBUG) log('Ad detected, muting video / 检测到广告,开始静音');
      }
      video.muted = true;
      try { video.volume = 0; } catch (_) {}
    } else {
      // 离开广告:还原之前的声音状态
      if (state.adMuted) {
        if (DEBUG) log('Ad ended, restore volume / 广告结束,还原音量');
        if (state.prevMuted !== null) {
          video.muted = state.prevMuted;
        }
        if (typeof state.prevVolume === 'number') {
          try { video.volume = state.prevVolume; } catch (_) {}
        }
        state.adMuted = false;
        state.prevMuted = null;
        state.prevVolume = null;
      }
    }
  }

  /*********************
   * ✅ 改造后的“软跳过”:只点 YouTube 自带的“跳过广告”按钮
   * 不再做静音+跳尾;不再 seekTo;不再重载
   *********************/
  function trySoftSkip(players, ctx) {
    if (ctx.skipBtn) {
      try {
        ctx.skipBtn.click();
        log('Clicked Skip Button / 点击跳过按钮');
        return true;
      } catch (_) {
        // ignore
      }
    }
    return false;
  }

  // 重载兜底逻辑保留但不再调用(你如果以后想恢复“暴力穿广告”,可以再接回去)
  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;

    const signature = `${vid}:${Math.floor(start / 5)}`;
    const nowTs = now();

    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 {
      state.reloadAttempts = 0;
    }

    let wantSubsOn = false;
    if (players.moviePlayerEl &&
        typeof players.moviePlayerEl.isSubtitlesOn === 'function') {
      try { wantSubsOn = !!players.moviePlayerEl.isSubtitlesOn(); } catch (_) {}
    }

    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() });
      restoreSubtitlesIfNeeded(players.moviePlayerEl, wantSubsOn);
      return true;
    }
    return false;
  }

  // 连环检测:多条广告时多试几次
  function chainSkipIfNeeded() {
    let count = 0;

    const loop = () => {
      if (count >= CHAIN_SKIP_MAX) return;
      count++;

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

      if (!state.skipping) {
        skipAd(true);
      }

      if (count < CHAIN_SKIP_MAX) {
        setTimeout(loop, CHAIN_SKIP_DELAY_MS);
      }
    };

    setTimeout(loop, CHAIN_SKIP_DELAY_MS);
  }

  // 主流程
  function skipAd(fromChain) {
    if (isShorts()) return;             // Shorts 不动
    if (state.skipping) return;
    state.skipping = true;

    let acted = false;

    try {
      const ctx = detectAdContext();

      // ✅ 无论是否有广告,每次检查都先处理“静音/还原”
      ensureAdMute(ctx);

      // 没广告了就只需要上面那行的“还原音量”,直接结束
      if (!ctx.adLikely) return;

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

      // ✅ 只做“官方按钮跳过”
      const softOK = trySoftSkip(players, ctx);
      if (softOK) {
        acted = true;
      }

      // ❌ 不再调用重载兜底:保留广告,只是静音 + 能跳的时候自动跳
      // const heavyOK = tryHeavyReload(players);
      // if (heavyOK) {
      //   acted = true;
      // }
    } finally {
      state.skipping = false;

      // 如果刚刚真的点过“跳过广告”,为了连续广告的情况,跑一轮连环检测
      if (acted && !fromChain) {
        chainSkipIfNeeded();
      }
    }
  }

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

  // 观察器:DOM 有变化时触发检查(广告出现/结束都会动 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();                 // 现在 CSS 选择器是空的,不会隐藏内容
  removeAdElements();       // 现在数组为空,不会真的移除东西
  setupObserver();
  scheduleCheck(0);         // 刚加载时先检查一次(如果一进来就有广告)

  // 兜底定时器:即使 MutationObserver 漏掉了,也会定期检查
  setInterval(() => scheduleCheck(), INTERVAL_CHECK_MS);
  setInterval(() => removeAdElements(), INTERVAL_CLEAN_MS);
})();