YouTube Ads Auto-Skipper

FXVNPRo Script Manager - Automatically skips YouTube ads, hides ad elements, and restores audio after skipping

当前为 2025-08-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         YouTube Ads Auto-Skipper
// @version      2025.08.26.2
// @description  FXVNPRo Script Manager - Automatically skips YouTube ads, hides ad elements, and restores audio after skipping
// @author       130195
// @license MIT
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @namespace    https://gf.qytechs.cn/en/users/1491267
// @icon         https://www.youtube.com/favicon.ico
// @unwrap
// @run-at       document-idle
// @grant        none
// ==/UserScript==
(() => {
  let popupState = 0;
  let popupElement = null;

  const rate = 1;
  const Promise = (async () => { })().constructor;

  const PromiseExternal = ((resolve_, reject_) => {
    const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
    return class PromiseExternal extends Promise {
      constructor(cb = h) {
        super(cb);
        if (cb === h) {
          this.resolve = resolve_;
          this.reject = reject_;
        }
      }
    };
  })();

  const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);

  let vload = null;

  const fastSeekFn = HTMLVideoElement.prototype.fastSeek || null;
  const addEventListenerFn = HTMLElement.prototype.addEventListener;
  if (!addEventListenerFn) return;
  const removeEventListenerFn = HTMLElement.prototype.removeEventListener;
  if (!removeEventListenerFn) return;

  const ytPremiumPopupSelector = 'yt-mealbar-promo-renderer.style-scope.ytd-popup-container:not([hidden])';
  const DEBUG = 0;
  const rand = (a, b) => a + Math.random() * (b - a);
  const log = DEBUG ? console.log.bind(console) : () => 0;

  const ytPremiumPopupClose = function () {
    const popup = document.querySelector(ytPremiumPopupSelector);
    if (popup instanceof HTMLElement) {
      if (HTMLElement.prototype.closest.call(popup, '[hidden]')) return;
      const cnt = insp(popup);
      const btn = cnt.$ ? cnt.$['dismiss-button'] : 0;
      if (btn instanceof HTMLElement && HTMLElement.prototype.closest.call(btn, '[hidden]')) return;
      btn && btn.click();
    }
  }

  const clickSkip = function () {
    const isAdsContainerContainsButton = document.querySelector('.video-ads.ytp-ad-module button');
    if (isAdsContainerContainsButton) {
      const btnFilter = e => HTMLElement.prototype.matches.call(e, ".ytp-ad-overlay-close-button, .ytp-ad-skip-button-modern, .ytp-ad-skip-button") && !HTMLElement.prototype.closest.call(e, '[hidden]');
      const btns = [...document.querySelectorAll('.video-ads.ytp-ad-module button[class*="ytp-ad-"]')].filter(btnFilter);
      if (btns.length !== 1) return;
      const btn = btns[0];
      if (btn instanceof HTMLElement) btn.click();
    }
  };

  const adsEndHandlerHolder = function (evt) {
    adsEndHandler && adsEndHandler(evt);
  }

  let adsEndHandler = null;

  
  const audioState = new WeakMap(); // video -> { prevMuted, prevVolume, restoreTimer, mo }
  const getMoviePlayer = (video) => video && video.closest && video.closest('#movie_player') || document.getElementById('movie_player');

  function rememberAudio(video) {
    if (!audioState.has(video)) {
      audioState.set(video, {
        prevMuted: video.muted,
        prevVolume: typeof video.volume === 'number' ? video.volume : 1,
        restoreTimer: null,
        mo: null
      });
    }
  }

  function tempMuteForAd(video) {
    try {
      rememberAudio(video);
      
      const st = audioState.get(video);
      if (st && st.prevMuted === true) return; 
      video.muted = true;
    } catch (e) {}
  }

  function restoreAudioNow(video) {
    const st = audioState.get(video);
    if (!st) return;
    try {
      
      if (st.prevMuted === false) {
        video.muted = false;
        if (typeof st.prevVolume === 'number') {
          
          if (st.prevVolume > 0) video.volume = st.prevVolume;
        }
        
        const ytplayer = video.closest('ytd-player, ytmusic-player');
        const cnt = insp(ytplayer || {});
        const api = (cnt && (cnt.player_ || cnt.playerApi || cnt.getPlayer && cnt.getPlayer())) || null;
        if (api && typeof api.setMuted === 'function') api.setMuted(false);
      }
    } catch (e) {}
  }

  function setupAdEndRestore(video) {
    const st = audioState.get(video) || {};
    
    if (st.restoreTimer) {
      clearTimeout(st.restoreTimer);
      st.restoreTimer = null;
    }
    if (st.mo) {
      try { st.mo.disconnect(); } catch (e) {}
      st.mo = null;
    }

    const moviePlayer = getMoviePlayer(video);

    
    if (moviePlayer) {
      const mo = new MutationObserver(() => {
        if (!moviePlayer.classList.contains('ad-showing')) {
          restoreAudioNow(video);
          try { mo.disconnect(); } catch (e) {}
          const s = audioState.get(video);
          if (s) s.mo = null;
        }
      });
      mo.observe(moviePlayer, { attributes: true, attributeFilter: ['class'] });
      st.mo = mo;
      audioState.set(video, st);
    }

    
    st.restoreTimer = setTimeout(() => {
      restoreAudioNow(video);
      const s = audioState.get(video);
      if (s) s.restoreTimer = null;
    }, 2000);
    audioState.set(video, st);
  }
  

  const videoPlayingHandler = async function (evt) {
    try {
      if (!evt || !evt.target || !evt.isTrusted || !(evt instanceof Event)) return;
      const video = evt.target;

      const checkPopup = popupState === 1;
      popupState = 0;

      const popupElementValue = popupElement;
      popupElement = null;

      if (video.duration < 0.8) return;

      await vload.then();
      if (!video.isConnected) return;

      const ytplayer = HTMLElement.prototype.closest.call(video, 'ytd-player, ytmusic-player');
      if (!ytplayer || !ytplayer.is) return;

      const ytplayerCnt = insp(ytplayer);
      const player_ = await (ytplayerCnt.player_ || ytplayer.player_ || ytplayerCnt.playerApi || ytplayer.playerApi || 0);
      if (!player_) return;

      if (typeof ytplayerCnt.getPlayer === 'function' && !ytplayerCnt.getPlayer()) {
        await new Promise(r => setTimeout(r, 40));
      }
      const playerController = await ytplayerCnt.getPlayer() || player_;
      if (!video.isConnected) return;

      if ('getPresentingPlayerType' in playerController && 'getDuration' in playerController) {
        const ppType = await playerController.getPresentingPlayerType();
        // ppType === 2 => ads; ppType === 1 => content
        if (ppType === 1 || typeof ppType !== 'number') return;

        const q = video.duration;
        const ytDuration = await playerController.getDuration();

        if (q > 0.8 && ytDuration > 2.5 && Math.abs(ytDuration - q) > 1.4) {
          try {
            
            tempMuteForAd(video);

            const w = Math.round(rand(582, 637) * rate);
            const sq = q - w / 1000;

            adsEndHandler = null;

            const expired = Date.now() + 968;

            removeEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
            removeEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
            removeEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);
            addEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
            addEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
            addEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);

            adsEndHandler = async function () {
              adsEndHandler = null;

              removeEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
              removeEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
              removeEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);

              if (Date.now() < expired) {
                const delay = Math.round(rand(92, 117));
                await new Promise(r => setTimeout(r, delay));

                Promise.resolve().then(() => {
                  clickSkip();
                }).catch(console.warn);

                
                Promise.resolve().then(() => {
                  setupAdEndRestore(video);
                }).catch(console.warn);

                
                checkPopup && Promise.resolve().then(() => {
                  const currentPopup = document.querySelector(ytPremiumPopupSelector);
                  if (popupElementValue ? currentPopup === popupElementValue : currentPopup) {
                    ytPremiumPopupClose();
                  }
                }).catch(console.warn);
              }
            };

            if (fastSeekFn) fastSeekFn.call(video, sq);
            else video.currentTime = sq;

          } catch (e) {
            console.warn(e);
            
            setupAdEndRestore(video);
          }
        }
      }
    } catch (e) {
      console.warn(e);
      
      if (evt && evt.target && evt.target.nodeName === 'VIDEO') {
        setupAdEndRestore(evt.target);
      }
    }
  };

  document.addEventListener('loadedmetadata', async function (evt) {
    try {
      if (!evt || !evt.target || !evt.isTrusted || !(evt instanceof Event)) return;

      const video = evt.target;
      if (video.nodeName !== "VIDEO") return;
      if (video.duration < 0.8) return;
      if (!video.matches('.video-stream.html5-main-video')) return;

      popupState = 0;

      vload = new PromiseExternal();

      popupElement = document.querySelector(ytPremiumPopupSelector);

      removeEventListenerFn.call(video, 'playing', videoPlayingHandler, { passive: true, capture: false });
      addEventListenerFn.call(video, 'playing', videoPlayingHandler, { passive: true, capture: false });

      
      rememberAudio(video);

      popupState = 1;

      let trial = 6;

      await new Promise(resolve => {
        let io = new IntersectionObserver(entries => {
          if (trial-- <= 0 || (entries && entries.length >= 1 && video.matches('ytd-player video, ytmusic-player video'))) {
            resolve();
            io.disconnect();
            io = null;
          }
        });
        io.observe(video);
      });

      vload.resolve();
    } catch (e) {
      console.warn(e);
    }
  }, true);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址