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