YouTube Ad Muter + Auto Skip Button (VoidMuser Mod)

不隐藏广告:播放广告时自动静音;出现“跳过广告”按钮时自动点击;广告结束自动恢复静音状态。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Ad Muter + Auto Skip Button (VoidMuser Mod)
// @namespace    
// @version      2.1.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';

  /*********************
   * Quick Switches(开关配置,改 true/false 即可)
   *********************/
  const MASTER_ENABLE = true;              // ✅ 总开关:false 时脚本不做任何事
  const ENABLE_AD_MUTE = true;             // ✅ 广告自动静音 + 结束后自动恢复
  const ENABLE_AUTO_SKIP_BUTTON = true;    // ✅ 自动点击“跳过广告”按钮
  const ENABLE_CHAIN_SKIP = true;          // ✅ 连环多广告时多次检测跳过

  /*********************
   * Adjustable Parameters(一般不用动)
   *********************/
  const DEBUG = false;                     // 调试日志
  const CSS_HIDE_SELECTORS = [];           // 不隐藏广告
  const REMOVE_PAIRS = [];                 // 不移除广告 DOM
  const CHECK_DEBOUNCE_MS = 150;           // 去抖延时
  const INTERVAL_CHECK_MS = 2000;          // 兜底检测间隔
  const INTERVAL_CLEAN_MS = 4000;          // 兜底清理间隔(现在几乎无作用)
  const SEEK_EPSILON = 0.25;               // 占位(不再用于跳广告)

  // 连环多广告检测参数
  const CHAIN_SKIP_MAX = 4;
  const CHAIN_SKIP_DELAY_MS = 800;

  /*********************
   * Internal State
   *********************/
  const state = {
    skipping: false,        // 防重入
    scheduled: false,       // 去抖标记

    // 广告静音相关状态
    adMuted: false,
    prevMuted: null,
    prevVolume: null
  };

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

  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.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;

    // 兜底:用 aria-label 或 文本匹配
    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 };
  }

  /*********************
   * 广告静音逻辑
   * - ENABLE_AD_MUTE = true 时:
   *   · 广告出现:记录原静音 & 音量 -> 强制静音
   *   · 广告结束:恢复到原来的静音 & 音量
   *********************/
  function ensureAdMute(ctx) {
    if (!ENABLE_AD_MUTE) return;

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

  /*********************
   * “软跳过”:只点官方“跳过广告”按钮
   * - 这里完全符合你说的:
   *   · 不再做 currentTime = duration - SEEK_EPSILON 的快进穿广告
   *   · 不再用 player.seekTo 绕过广告
   *   · 只要按钮出现就帮你点一下
   *********************/
  function trySoftSkip(ctx) {
    if (!ENABLE_AUTO_SKIP_BUTTON) return false;
    if (ctx.skipBtn) {
      try {
        ctx.skipBtn.click();
        log('Clicked Skip Button / 点击跳过按钮');
        return true;
      } catch (_) {
        // ignore
      }
    }
    return false;
  }

  /*********************
   * 连环多广告检测:成功跳过后,再检测几轮
   * - 继续保留你原脚本的“连环多广告”思路
   *********************/
  function chainSkipIfNeeded() {
    if (!ENABLE_CHAIN_SKIP) return;

    let count = 0;

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

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

      if (!state.skipping) {
        skipAd(true);  // fromChain = true,避免再次开启连环
      }

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

    setTimeout(loop, CHAIN_SKIP_DELAY_MS);
  }

  /*********************
   * 主流程
   * - 广告一出现:skipAd() → 静音 + 有按钮就点
   * - 广告结束:下一轮 skipAd() 检测到“非广告” → 自动恢复声音
   *********************/
  function skipAd(fromChain) {
    if (!MASTER_ENABLE) return;  // 总开关
    if (isShorts()) return;      // 不处理 Shorts,避免误伤
    if (state.skipping) return;
    state.skipping = true;

    let acted = false;

    try {
      const ctx = detectAdContext();

      // 1. 先处理广告静音 / 非广告时恢复声音
      ensureAdMute(ctx);

      // 2. 不是广告就结束(只负责静音恢复)
      if (!ctx.adLikely) return;

      // 3. 是广告:如果开启了自动跳过,就只点官方“跳过广告”按钮
      const softOK = trySoftSkip(ctx);
      if (softOK) {
        acted = true;
      }

      // 4. 不再做:
      //    - currentTime = duration - SEEK_EPSILON 的快进穿广告
      //    - player.seekTo 的时间线绕过
      //    - tryHeavyReload 的重载穿广告
      //    所以不可跳过广告会“正常播完”
      //    且在 ENABLE_AD_MUTE = 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
    });
  }

  /*********************
   * 启动
   *********************/
  addCss();                 // 现在不会隐藏任何广告元素(数组为空)
  removeAdElements();       // 现在也不会移除广告 DOM(数组为空)

  setupObserver();
  scheduleCheck(0);         // 载入页面时先检测一次

  // 兜底定时器:防止 MutationObserver 漏掉某些情况
  setInterval(() => scheduleCheck(), INTERVAL_CHECK_MS);
  setInterval(() => removeAdElements(), INTERVAL_CLEAN_MS);
})();