// ==UserScript== // @name YouTube 优化大师 // @namespace https://t.me/ // @version 0.7.8 // @description YouTube 视频体验增强:v0.7.8 跳过按钮+seekTo+10s冷却,无hooks无循环。含画质/布局/年龄/倒赞等功能。 // @author ARIES // @icon https://github.com/yuanwangokk-1/subscribe/blob/main/picture/Chrome.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* // @run-at document-start // @grant none // @license MIT // @noframes // ==/UserScript== (function () { 'use strict'; // ===== 调试日志(带颜色前缀,方便在 F12 Console 里一眼识别) ===== const 日志标签 = '%c[YT优化大师]'; const 样式信息 = 'color:#339af0;font-weight:bold;'; const 样式成功 = 'color:#51cf66;font-weight:bold;'; const 样式警告 = 'color:#ffa94d;font-weight:bold;'; const 样式错误 = 'color:#ff6b6b;font-weight:bold;'; let 调试模式启用 = false; function logInfo() { if (!调试模式启用) return; console.log(日志标签, 样式信息, ...arguments); } function logOk() { console.log(日志标签, 样式成功, ...arguments); } function logWarn() { console.warn(日志标签, 样式警告, ...arguments); } function logError() { console.error(日志标签, 样式错误, ...arguments); } logOk('v0.7.8 脚本已注入 →', location.href); // ===== Trusted Types 兼容 ===== // YouTube 启用了 CSP「require-trusted-types-for 'script'」,所有 innerHTML 赋值 // 必须是 TrustedHTML 类型,否则浏览器直接抛错拦截。这里建一个放行 policy 来包装。 let 创建HTML = (s) => s; // 不支持 Trusted Types 的环境直接透传字符串 if (window.trustedTypes && window.trustedTypes.createPolicy) { try { const policy = window.trustedTypes.createPolicy('nextshield-adb', { createHTML: (s) => s }); 创建HTML = (s) => policy.createHTML(s); logOk('Trusted Types policy「nextshield-adb」已创建'); } catch (e) { logWarn('创建 Trusted Types policy 失败(可能名称冲突):', e.message); } } // ===== 广告拦截核心(跳过按钮 + seekTo + 冷却防循环) ===== // 策略(核心原则:零运行时函数钩子,不修改 fetch/XHR/JSON.parse 等原生函数): // 1. 播放器级广告处理 —— MutationObserver 检测播放器 class 变化,检测到广告状态后处理。 // 2. 广告静音 + 跳过按钮强制显示 —— 配合使用,模拟真人操作模式。 // 3. 定时器加入随机 jitter —— 打破固定间隔的可预测模式。 logOk('广告拦截核心 v0.7.8 初始化完成(策略:跳过按钮 + 广告静音)'); // ===== 功能开关(持久化到 localStorage) ===== // 气泡执行提示 / 自动切换画质 / 统计信息汉化 / 网速单位转换 / 预测倒赞数据:默认开启。 // 广告过滤细粒度开关(5 项),每项对应一种独立的拦截手段,检测特征各异, // 可单独启用/禁用,便于用控制变量法逐项二分定位被油管检测的元凶。 const 设置键 = 'nextshield_adb_options_v2'; const 选项 = { 气泡执行提示: true, 自动画质模式: 'best', 统计信息汉化: true, 网速单位转换: true, 倒赞比例条: true, 预测倒赞数据: true, // ---- 广告过滤细粒度开关(5 项,默认全开) ---- 强显跳过按钮: true, // 🖱️ 强制显示隐藏的跳过按钮(让可跳过广告不被倒计时遮挡) 广告时段静音: true, // 🔇 广告播放时自动静音 广告横幅移除: true, // 🧹 移除页面上的广告横幅 / 商品推荐 / 信息流广告(DOM 删除) 年龄限制绕过: true, // 🔞 绕过年龄验证弹窗 自动恢复播放: true, // ▶️ 广告暂停视频后自动恢复播放 调试模式: false }; function 加载选项() { try { const 存档 = JSON.parse(localStorage.getItem(设置键) || '{}'); if (typeof 存档.气泡执行提示 === 'boolean') 选项.气泡执行提示 = 存档.气泡执行提示; if (typeof 存档.自动画质模式 === 'string') { if (['best', '1080p', 'off'].includes(存档.自动画质模式)) { 选项.自动画质模式 = 存档.自动画质模式; } } else if (typeof 存档.自动最优画质 === 'boolean') { 选项.自动画质模式 = 存档.自动最优画质 ? 'best' : 'off'; } if (typeof 存档.统计信息汉化 === 'boolean') 选项.统计信息汉化 = 存档.统计信息汉化; if (typeof 存档.网速单位转换 === 'boolean') 选项.网速单位转换 = 存档.网速单位转换; if (typeof 存档.倒赞比例条 === 'boolean') 选项.倒赞比例条 = 存档.倒赞比例条; if (typeof 存档.预测倒赞数据 === 'boolean') 选项.预测倒赞数据 = 存档.预测倒赞数据; if (typeof 存档.调试模式 === 'boolean') 选项.调试模式 = 存档.调试模式; // ---- 广告过滤细粒度开关:直接读取 5 项 boolean ---- // 5 项 key 集中在此数组,方便与「迁移」分支统一处理 const 广告开关键表 = [ '强显跳过按钮', '广告时段静音', '广告横幅移除', '年龄限制绕过', '自动恢复播放' ]; const 已迁移到细粒度 = 广告开关键表.some(k => typeof 存档[k] === 'boolean'); if (已迁移到细粒度) { // 新版存档:逐项读取 广告开关键表.forEach(k => { if (typeof 存档[k] === 'boolean') 选项[k] = 存档[k]; }); } else if (typeof 存档.快速处理 === 'boolean') { // 向后兼容:旧版「快速处理」开关已废弃,忽略 logInfo('检测到旧版广告过滤开关「快速处理」,已忽略'); } // 其余 3 项(广告横幅移除/年龄限制绕过/自动恢复播放)在旧版没有独立开关, // 迁移后保持默认开启,与旧行为一致。 调试模式启用 = !!选项.调试模式; if (!['best', '1080p', 'off'].includes(选项.自动画质模式)) 选项.自动画质模式 = 'best'; logInfo('已加载功能开关:', { ...选项 }); } catch (e) { logWarn('功能开关存档损坏,使用默认值:', e); } } function 保存选项() { try { localStorage.setItem(设置键, JSON.stringify(选项)); } catch (e) { } } // 广告时段静音 / 跳过按钮处理为被动特性,当对应开关开启时需监听播放器 class 变化。 // 各过滤函数内部已自行检查对应开关,无需在主循环层面统一门控。 // 所有 5 项全关时才停止播放器观察器。 function 视频广告处理启用() { return 选项.强显跳过按钮 || 选项.广告时段静音 || 选项.广告横幅移除 || 选项.年龄限制绕过 || 选项.自动恢复播放; } // 反检测:不隐藏广告 slot。YouTube 会注入广告元素并用 getComputedStyle / // getBoundingClientRect 校验其是否真实渲染,display:none 或零尺寸即判定为被拦截。 // 正确做法是「不隐藏、靠秒跳让广告瞬间播完」——slot 照常渲染(校验通过)。 function 同步广告过滤状态() { if (!视频广告处理启用()) { if (播放器观察器) { 播放器观察器.disconnect(); 播放器观察器 = null; } 已绑定播放器 = null; 恢复音量(); // 关闭广告处理时恢复音量 } } // ===== 全局状态 ===== let 画质已提升 = false; // 当前视频是否已提升画质(切视频时重置) const 最近通知记录 = new Map(); // 通知冷却记录:键=类型+消息,值=上次弹出时间戳 const 播放器广告类名 = ['ad-showing', 'ad-interrupting', 'ytp-ad-player-overlay', 'ytp-ad-display-override']; let 播放器观察器 = null; let 已绑定播放器 = null; // ===== 广告时段静音状态 ===== let 广告静音已应用 = false; // 当前广告是否已应用静音 let 广告静音前音量 = null; // 广告前的音量(0~1) let 广告静音前是否静音 = null; // 广告前的静音状态 let 广告静音恢复定时器 = null; // 延迟确认恢复音量的定时器(防止广告中间 class 抖动误恢复) let 音量恢复保护定时器 = null; // 恢复音量后的短期保护定时器(防止 YouTube 播放器覆盖恢复值) // ===== 拦截统计(持久化到 localStorage,刷新页面也不丢) ===== // 5 项广告过滤策略(每项限 6 字,策略名即代码标识,保持一致方便 UI 对齐) // 计数会显示在该开关左侧。总数 = 各策略计数之和(动态求和,不单独存)。 const 策略键表 = [ '强显跳过按钮', '广告时段静音', '广告横幅移除', '年龄限制绕过', '自动恢复播放' ]; const 统计 = {}; 策略键表.forEach(k => { 统计[k] = 0; }); const 存档键 = 'nextshield_adb_stats_v1'; function 统计总数() { return 策略键表.reduce((n, k) => n + (统计[k] || 0), 0); } function 加载统计() { try { const 存档 = JSON.parse(localStorage.getItem(存档键) || '{}'); 策略键表.forEach(k => { 统计[k] = Number.isFinite(存档[k]) ? 存档[k] : 0; }); logInfo('已加载历史统计:', { ...统计 }); } catch (e) { logWarn('统计存档损坏,从 0 重新开始:', e); } } function 保存统计() { try { localStorage.setItem(存档键, JSON.stringify(统计)); } catch (e) { } } // 注:以下名字保持英文,因为它们是 DOM / YouTube API / CSS 类名,改成中文会失效: // - CSS 类名(nextshield-stats、nextshield-notification 等):在