FXVNPRo Script Manager - Automatically skips YouTube ads, hides ad elements, and restores audio after skipping
当前为
// ==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://greasyfork.org/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);
})();