// ==UserScript== // @name 全网视频增强助手 - 多平台去广告·倍速·净化 // @namespace https://github.com/video-enhancer // @version 2.6.0 // @description 支持 iflix / iQIYI海外版(iq.com) / 优酷海外版(youku.tv) / 优酷国内(youku.com) / 1905电影网 / PPTV(PP视频) / 土豆 / 哔哩哔哩 / 西瓜视频 去广告、倍速突破、界面净化、播放增强 // @author VideoEnhancer // @icon https://cdn-icons-png.flaticon.com/512/1384/1384060.png // @match *://www.iflix.com/* // @match *://*.iflix.com/* // @match *://www.iq.com/* // @match *://*.iq.com/* // @match *://www.youku.tv/* // @match *://*.youku.tv/* // @match *://www.youku.com/* // @match *://*.youku.com/* // @match *://www.1905.com/* // @match *://*.1905.com/* // @match *://www.pptv.com/* // @match *://*.pptv.com/* // @match *://v.pptv.com/* // @match *://www.tudou.com/* // @match *://*.tudou.com/* // @match *://www.bilibili.com/* // @match *://*.bilibili.com/* // @match *://www.ixigua.com/* // @match *://*.ixigua.com/* // @match *://www.ixigua.net/* // @match *://*.ixigua.net/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_setClipboard // @grant GM_download // @grant GM_xmlhttpRequest // @connect * // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; // ============================================================ // 全局配置 // ============================================================ const CONFIG = { version: '2.6.0', debug: GM_getValue('debug', false), // 广告屏蔽规则 blockSelectors: { // 通用广告选择器 global: [ '[id*="ad-"]', '[id*="advert"]', '[id*="guanggao"]', '[class*="ad-wrap"]', '[class*="ad-banner"]', '[class*="ad-slot"]', '[class*="advert"]', '[class*="guanggao"]', '[class*="sidebar-ad"]', '[class*="popup-ad"]', '[class*="float-ad"]', '[class*="overlay-ad"]', '[class*="banner-ad"]', '[class*="video-ad"]', 'iframe[src*="ad"]', 'iframe[src*="doubleclick"]', 'div[data-ad]', 'div[aria-label="广告"]', '.ad-container', '.ad-player', '.ad-layer', '.iqp-player-videoad', '.iqp-ad', '.skippable-after', ], // 爱奇艺海外版 (iq.com) iq: [ '.iqp-banner', '.iqp-side-ad', '.m-iqp-ad', '[class*="iqp"][class*="ad"]', '.twelve-adv', '.mod-ad', '.m-slider-ad', '.iqp-pause-ad', ], // iflix iflix: [ '.ad-overlay', '[class*="preroll"]', '[class*="midroll"]', '[class*="postroll"]', '[class*="sponsor"]', '[data-testid*="ad"]', ], // 优酷 youku: [ '.youku-layer-logo', // 优酷logo水印 '#module-basic-advert', '.adv-area', '.pause-adv', '.control-advert', '.corner-mark', '.yk-ad', '[class*="advert-"]', '.recommend-ad', ], // 1905电影网 moive1905: [ '.adWrap', '.ad-box', '#ad_', '.side-ad', '.top-ad', '.banner-ad', ], // PPTV / PP视频 pptv: [ '.pp-ad', '.ad-wrap', '.ad-box', '.ad-layer', '.pp-player-ad', '.suspend-ad', '[id*="ad"]', '[class*="ad-"]', ], // 土豆 tudou: [ '.td-ad', '.advert', '.ad-module', '.sidebar-promo', ], // 哔哩哔哩 bili: [ '.ad-report', '.bili-ad', '#slide_ad', '.video-page-game-card', '.rec-section', '.pop-live', '[class*="ad-"]', '.bili-dyn-ads', '.video-card-ad', '.activity-m', '.vcd', // 视频卡片广告 '.bangumi-pgc-video-card', // 大会员推广 ], // 西瓜视频 xigua: [ '.ad-container', '[class*="xigua-ad"]', '.video-feed-ad', '.inline-ad', ], }, // 倍速控制 speedOptions: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0], defaultSpeed: GM_getValue('defaultSpeed', 1.0), // 自动跳过片头片尾 (秒) skipIntro: GM_getValue('skipIntro', 0), skipOutro: GM_getValue('skipOutro', 0), // 界面净化 removePopup: GM_getValue('removePopup', true), // 去除弹窗 removeFloat: GM_getValue('removeFloat', true), // 去除悬浮元素 removeSidebar: GM_getValue('removeSidebar', false), // 去除侧边栏 autoFullscreen: GM_getValue('autoFullscreen', false), autoPlay: GM_getValue('autoPlay', true), }; // ============================================================ // 工具函数 // ============================================================ const Utils = { log(...args) { if (CONFIG.debug) { console.log(`[视频增强 v${CONFIG.version}]`, ...args); } }, info(...args) { console.log(`%c[视频增强 v${CONFIG.version}]%c ${args.join(' ')}`, 'background:#f60;color:#fff;border-radius:3px;padding:0 4px', 'color:#f60'); }, currentSite() { const h = location.hostname; const map = { 'iflix': /iflix\.com/, 'iq': /iq\.com/, 'youku': /youku\.(com|tv)/, 'movie1905': /1905\.com/, 'pptv': /pptv\.com/, 'tudou': /tudou\.com/, 'bili': /bilibili\.com/, 'xigua': /ixigua\.(com|net)/, }; for (const [site, re] of Object.entries(map)) { if (re.test(h)) return site; } return 'unknown'; }, // 安全移除元素 remove(el) { if (!el) return; try { el.remove(); } catch { try { el.parentNode.removeChild(el); } catch {} } }, // 隐藏元素 hide(el) { if (!el) return; el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('height', '0', 'important'); el.style.setProperty('overflow', 'hidden', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); }, // 观察DOM变化 observe(target, callback, opts = {}) { if (!target) return; const observer = new MutationObserver(callback); observer.observe(target, { childList: true, subtree: true, ...opts, }); return observer; }, // 等待元素出现 waitFor(selector, timeout = 10000, parent = document) { return new Promise((resolve, reject) => { const el = parent.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = parent.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(parent, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`超时: ${selector}`)); }, timeout); }); }, // 防抖 debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }, }; // ============================================================ // 模块一:广告拦截 // ============================================================ const AdBlocker = { removedCount: 0, init() { const site = Utils.currentSite(); this.removeStaticAds(site); this.observeDynamicAds(site); this.interceptRequests(site); Utils.info(`广告拦截已启用 [${site}]`); }, // 移除静态广告 removeStaticAds(site) { const selectors = [ ...CONFIG.blockSelectors.global, ...(CONFIG.blockSelectors[site] || []), ]; selectors.forEach(sel => { try { document.querySelectorAll(sel).forEach(el => { // 安全检查:避免误删播放器本身 if (this.isPlayerElement(el)) return; Utils.hide(el); this.removedCount++; }); } catch {} }); }, // 动态广告监控 observeDynamicAds(site) { const debouncedClean = Utils.debounce(() => { const selectors = [ ...CONFIG.blockSelectors.global, ...(CONFIG.blockSelectors[site] || []), ]; let count = 0; selectors.forEach(sel => { try { document.querySelectorAll(sel).forEach(el => { if (this.isPlayerElement(el)) return; if (el.offsetParent !== null || el.style.display !== 'none') { Utils.hide(el); count++; } }); } catch {} }); if (count > 0) { this.removedCount += count; Utils.log(`动态清理 ${count} 个广告元素`); } }, 500); Utils.observe(document.body, debouncedClean); }, // 拦截广告请求 interceptRequests(site) { // 拦截广告相关的脚本和资源加载 const adPatterns = [ /doubleclick\.net/, /googlesyndication\.com/, /ad\.iqiyi\.com/, /ad\.youku\.com/, /ad\.bilibili\.com/, /cm\.bilibili\.com/, /static\.ad\.pptv\.com/, /tanx\.com/, /mm\.taobao\.com/, /adnxs\.com/, /adsrvr\.org/, /analytics\./, /tracker\./, /collect\./, /log\./, /beacon\./, ]; const origOpen = XMLHttpRequest.prototype.open; const origFetch = window.fetch; // 拦截 XHR XMLHttpRequest.prototype.open = function (method, url, ...args) { if (adPatterns.some(p => p.test(url))) { Utils.log(`拦截广告请求: ${url}`); return; // 不执行请求 } return origOpen.call(this, method, url, ...args); }; // 拦截 Fetch window.fetch = function (url, options) { const urlStr = typeof url === 'string' ? url : (url && url.url) || ''; if (adPatterns.some(p => p.test(urlStr))) { Utils.log(`拦截广告Fetch: ${urlStr}`); return Promise.resolve(new Response('', { status: 200 })); } return origFetch.call(this, url, options); }; // 注入 CSS 隐藏策略 const hideCSS = adPatterns.map(p => { try { return p.source; } catch { return ''; } }).join(','); if (hideCSS) { GM_addStyle(` /* 全局广告屏蔽 */ [id*="ad-"], [id*="advert"], [id*="guanggao"], [class*="ad-wrap"], [class*="ad-banner"], [class*="ad-slot"], [class*="advert"], [class*="guanggao"], [class*="sidebar-ad"], [class*="popup-ad"], [class*="float-ad"], [class*="overlay-ad"], .ad-container, .ad-player, .ad-layer, .iqp-player-videoad, .iqp-ad, .skippable-after, .twelve-adv, .mod-ad, .pause-adv, .ad-overlay, [class*="preroll"], [class*="midroll"], #module-basic-advert, .adv-area, .pause-adv, .control-advert, .yk-ad, .recommend-ad, .adWrap, .ad-box, .side-ad, .top-ad, .pp-ad, .ad-wrap, .ad-box, .ad-layer, .pp-player-ad, .td-ad, .advert, .ad-module, .ad-report, .bili-ad, #slide_ad, .video-page-game-card, .rec-section, .pop-live, .vcd, .bili-dyn-ads, .video-card-ad, .activity-m, .bangumi-pgc-video-card, .video-feed-ad, .inline-ad, [data-ad], div[aria-label="广告"], [data-testid*="ad"] { display: none !important; visibility: hidden !important; height: 0 !important; overflow: hidden !important; pointer-events: none !important; position: absolute !important; z-index: -9999 !important; } /* 去除弹窗遮罩 */ [class*="modal"][class*="ad"], [class*="popup"][class*="ad"], [class*="dialog"][class*="ad"], [class*="layer"][class*="ad"] { display: none !important; } `); } }, // 判断是否为播放器核心元素 (避免误删) isPlayerElement(el) { const tag = el.tagName; const cls = el.className || ''; const id = el.id || ''; // video/audio 标签绝对不能删 if (tag === 'VIDEO' || tag === 'AUDIO' || tag === 'SOURCE') return true; // 播放器容器不能删 if (/player|video-box|player-container|xgplayer|bilibili-player/i.test(cls + id)) { // 但如果class同时含有ad,则可能是广告层 if (/ad/i.test(cls + id)) return false; return true; } return false; }, }; // ============================================================ // 模块二:视频播放增强 // ============================================================ const VideoEnhancer = { currentVideo: null, init() { this.findVideo(); this.observeVideo(); Utils.info('视频增强已启用'); }, findVideo() { const video = document.querySelector('video'); if (video && video !== this.currentVideo) { this.currentVideo = video; this.applyEnhancements(video); } }, observeVideo() { // 持续寻找 video 元素 const findVideo = Utils.debounce(() => this.findVideo(), 1000); Utils.observe(document.body, findVideo); // 定时检查 (某些SPA页面) setInterval(findVideo, 3000); }, applyEnhancements(video) { if (!video) return; Utils.log('找到视频播放器,正在应用增强...'); // 倍速设置 if (CONFIG.defaultSpeed > 1.0) { video.playbackRate = CONFIG.defaultSpeed; } // 自动播放 if (CONFIG.autoPlay) { video.play().catch(() => {}); } // 去除播放限制 this.removePlaybackRestrictions(video); // 创建控制面板 this.createControlPanel(video); // 自动跳过片头 this.setupSkipIntro(video); // 监听广告插入 this.monitorAdInsertion(video); }, removePlaybackRestrictions(video) { // 去除禁止快进 const origPlay = HTMLMediaElement.prototype.play; video.play = function () { return origPlay.call(this).catch(() => {}); }; // 允许任意倍速 try { Object.defineProperty(video, 'playbackRate', { get() { return this._playbackRate || 1; }, set(v) { this._playbackRate = v; // 绕过某些平台的倍速检查 try { this._playbackRate = v; } catch {} }, configurable: true, }); } catch {} // 禁止暂停广告自动恢复播放 video.addEventListener('pause', (e) => { setTimeout(() => { // 如果是因为广告导致的暂停,3秒后尝试恢复 if (video.paused && !document.hidden) { const overlay = document.querySelector( '[class*="ad"][class*="playing"], [class*="ad"][class*="pause"]' ); if (!overlay) { // video.play().catch(() => {}); } } }, 3000); }); }, // 监控广告插入并自动跳过 monitorAdInsertion(video) { let adDetected = false; // 检测广告:通常广告视频时长很短(<30秒)且不可跳过 const checkAd = () => { if (!video.duration || video.duration > 60) { if (adDetected) { adDetected = false; Utils.log('广告结束,恢复正常播放'); } return; } // 检测可能的广告 const indicators = [ document.querySelector('[class*="ad-skip"]'), document.querySelector('[class*="skip-btn"]'), document.querySelector('[class*="countdown"]'), document.querySelector('[class*="ad-time"]'), ]; const adOverlay = document.querySelector( '[class*="ad-layer"], [class*="video-ad"], [class*="player-ad"]' ); if (adOverlay || (video.duration < 30 && video.currentTime < video.duration)) { if (!adDetected) { adDetected = true; Utils.log('检测到广告,尝试跳过...'); this.trySkipAd(video); } } }; video.addEventListener('timeupdate', Utils.debounce(checkAd, 1000)); video.addEventListener('loadedmetadata', checkAd); }, trySkipAd(video) { // 方法1:寻找跳过按钮并点击 const skipSelectors = [ '[class*="skip"]', '[class*="Skip"]', 'button[class*="close"]', '[class*="ad-close"]', '[class*="ad-skip-btn"]', '[class*="skipAd"]', '.iqp-player-videoad-close', '.skippable-after', '[class*="countdown"]', // 倒计时结束后的跳过 ]; const clickSkip = () => { skipSelectors.forEach(sel => { document.querySelectorAll(sel).forEach(btn => { if (btn.offsetParent !== null) { btn.click(); Utils.log('点击跳过按钮:', sel); } }); }); }; // 立即尝试 clickSkip(); // 持续尝试 const interval = setInterval(clickSkip, 500); setTimeout(() => clearInterval(interval), 15000); // 最多尝试15秒 }, setupSkipIntro(video) { if (CONFIG.skipIntro <= 0) return; video.addEventListener('timeupdate', () => { if (video.currentTime >= CONFIG.skipIntro && video.currentTime < CONFIG.skipIntro + 1) { video.currentTime = CONFIG.skipIntro; Utils.info(`已跳过片头 ${CONFIG.skipIntro}s`); } }); }, createControlPanel(video) { if (document.getElementById('ve-control-panel')) return; const panel = document.createElement('div'); panel.id = 've-control-panel'; panel.innerHTML = `
🎬 视频增强 v${CONFIG.version} [${Utils.currentSite()}]
${CONFIG.defaultSpeed}x
100%
`; document.body.appendChild(panel); this.injectPanelCSS(); this.bindPanelEvents(panel, video); this.makePanelDraggable(panel); }, makePanelDraggable(panel) { const header = panel.querySelector('.ve-panel-header'); let isDragging = false, startX, startY, origX, origY; header.addEventListener('mousedown', (e) => { if (e.target.closest('.ve-toggle')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); origX = rect.left; origY = rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = (origX + e.clientX - startX) + 'px'; panel.style.top = (origY + e.clientY - startY) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); // 收起/展开 const toggle = panel.querySelector('.ve-toggle'); toggle.addEventListener('click', () => { const body = panel.querySelector('.ve-panel-body'); const isHidden = body.style.display === 'none'; body.style.display = isHidden ? 'block' : 'none'; toggle.textContent = isHidden ? '−' : '+'; }); }, injectPanelCSS() { GM_addStyle(` #ve-control-panel { position: fixed; top: 20px; right: 20px; z-index: 2147483647; background: rgba(20, 20, 30, 0.92); color: #e0e0e0; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.5); font-family: -apple-system, "Microsoft YaHei", sans-serif; font-size: 13px; min-width: 280px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); user-select: none; transition: opacity 0.3s; } #ve-control-panel:hover { opacity: 1 !important; } .ve-panel-header { display: flex; align-items: center; padding: 8px 12px; cursor: move; border-bottom: 1px solid rgba(255,255,255,0.1); } .ve-title { font-weight: 600; font-size: 14px; color: #fff; } .ve-site { margin-left: 8px; color: #888; font-size: 11px; } .ve-toggle { margin-left: auto; background: none; border: none; color: #aaa; font-size: 18px; cursor: pointer; padding: 0 4px; } .ve-toggle:hover { color: #fff; } .ve-panel-body { padding: 8px 12px; } .ve-section { display: flex; align-items: center; margin: 6px 0; gap: 6px; } .ve-section-compact { flex-wrap: wrap; } .ve-section label { color: #bbb; font-size: 12px; white-space: nowrap; } .ve-section select, .ve-section input[type="number"] { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 2px 6px; font-size: 12px; outline: none; } .ve-section select:focus, .ve-section input:focus { border-color: #4fc3f7; } .ve-speed-slider { flex: 1; accent-color: #4fc3f7; height: 3px; } .ve-speed-display, .ve-volume-display { font-size: 11px; color: #4fc3f7; min-width: 36px; text-align: right; } .ve-volume-boost { flex: 1; accent-color: #ff9800; height: 3px; } .ve-pip { background: rgba(79, 195, 247, 0.2); border: 1px solid #4fc3f7; color: #4fc3f7; border-radius: 4px; padding: 2px 10px; font-size: 12px; cursor: pointer; outline: none; } .ve-pip:hover { background: rgba(79, 195, 247, 0.3); } .ve-screenshot, .ve-download, .ve-copy-url { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: #ccc; border-radius: 4px; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; } .ve-screenshot:hover, .ve-download:hover, .ve-copy-url:hover { background: rgba(255,255,255,0.15); color: #fff; } .ve-section input[type="checkbox"] { accent-color: #4fc3f7; } /* 通知Toast */ .ve-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.85); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; z-index: 2147483647; animation: ve-fadeIn 0.3s, ve-fadeOut 0.3s 1.7s forwards; pointer-events: none; } @keyframes ve-fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes ve-fadeOut { from { opacity: 1; } to { opacity: 0; } } /* 视频下载列表 */ .ve-download-area { margin-top: 8px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; max-height: 300px; overflow-y: auto; } .ve-download-area::-webkit-scrollbar { width: 4px; } .ve-download-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; } .ve-dl-title { color: #4fc3f7; font-weight: 600; font-size: 12px; } .ve-url-list { display: flex; flex-direction: column; gap: 6px; } .ve-url-row { background: rgba(255,255,255,0.05); border-radius: 6px; padding: 6px 8px; display: flex; flex-direction: column; gap: 4px; } .ve-url-info { display: flex; align-items: baseline; gap: 6px; } .ve-url-label { color: #fff; font-size: 12px; font-weight: 500; } .ve-url-meta { color: #888; font-size: 10px; } .ve-url-actions { display: flex; gap: 4px; flex-wrap: wrap; } .ve-url-actions button { background: rgba(79,195,247,0.15); border: 1px solid rgba(79,195,247,0.3); color: #4fc3f7; border-radius: 3px; padding: 2px 6px; font-size: 11px; cursor: pointer; outline: none; } .ve-url-actions button:hover { background: rgba(79,195,247,0.3); } .ve-url-actions .ve-btn-dl-m3u8 { background: rgba(255,152,0,0.15); border-color: rgba(255,152,0,0.3); color: #ff9800; } .ve-url-actions .ve-btn-dl-m3u8:hover { background: rgba(255,152,0,0.3); } `); }, bindPanelEvents(panel, video) { // 倍速控制 const speedSelect = panel.querySelector('.ve-speed'); const speedSlider = panel.querySelector('.ve-speed-slider'); const speedDisplay = panel.querySelector('.ve-speed-display'); const setSpeed = (val) => { const speed = parseFloat(val); video.playbackRate = speed; speedDisplay.textContent = speed + 'x'; GM_setValue('defaultSpeed', speed); }; speedSelect.addEventListener('change', (e) => { setSpeed(e.target.value); speedSlider.value = e.target.value; }); speedSlider.addEventListener('input', (e) => { const val = parseFloat(e.target.value); video.playbackRate = val; speedDisplay.textContent = val + 'x'; }); speedSlider.addEventListener('change', (e) => { GM_setValue('defaultSpeed', parseFloat(e.target.value)); }); // 画中画 panel.querySelector('.ve-pip').addEventListener('click', () => { if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); panel.querySelector('.ve-pip').textContent = '开启'; } else if (video.requestPictureInPicture) { video.requestPictureInPicture().then(() => { panel.querySelector('.ve-pip').textContent = '关闭'; }).catch(() => { this.showToast('画中画不可用'); }); } }); // 音量增益 const volumeSlider = panel.querySelector('.ve-volume-boost'); const volumeDisplay = panel.querySelector('.ve-volume-display'); volumeSlider.addEventListener('input', (e) => { const vol = parseInt(e.target.value); video.volume = Math.min(vol / 100, 1); volumeDisplay.textContent = vol + '%'; // 使用Web Audio API实现音量增益 this.applyVolumeBoost(video, vol / 100); }); // 自动播放 panel.querySelector('.ve-auto-play').addEventListener('change', (e) => { GM_setValue('autoPlay', e.target.checked); CONFIG.autoPlay = e.target.checked; }); // 自动全屏 panel.querySelector('.ve-auto-fs').addEventListener('change', (e) => { GM_setValue('autoFullscreen', e.target.checked); CONFIG.autoFullscreen = e.target.checked; if (e.target.checked) { this.enterFullscreen(video); } }); // 跳过片头片尾 panel.querySelector('.ve-skip-intro').addEventListener('change', (e) => { CONFIG.skipIntro = parseInt(e.target.value) || 0; GM_setValue('skipIntro', CONFIG.skipIntro); }); panel.querySelector('.ve-skip-outro').addEventListener('change', (e) => { CONFIG.skipOutro = parseInt(e.target.value) || 0; GM_setValue('skipOutro', CONFIG.skipOutro); if (CONFIG.skipOutro > 0) { this.setupSkipOutro(video); } }); // 截图 panel.querySelector('.ve-screenshot').addEventListener('click', () => { this.takeScreenshot(video); }); // 检测视频源 & 真正下载 panel.querySelector('.ve-detect-urls').addEventListener('click', () => { VideoDownloader.detect(video, panel); }); // 复制地址 panel.querySelector('.ve-copy-url').addEventListener('click', () => { GM_setClipboard(location.href, 'text'); this.showToast('已复制当前页面地址'); }); // 双击视频全屏 video.addEventListener('dblclick', () => { this.enterFullscreen(video); }); }, applyVolumeBoost(video, boost) { if (boost <= 1) return; if (!this._audioCtx) { this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); this._source = this._audioCtx.createMediaElementSource(video); this._gain = this._audioCtx.createGain(); this._source.connect(this._gain); this._gain.connect(this._audioCtx.destination); } this._gain.gain.value = boost; }, enterFullscreen(video) { const container = video.closest('[class*="player"]') || video.closest('div'); const el = container || video; try { (el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen).call(el); } catch {} }, setupSkipOutro(video) { video.addEventListener('timeupdate', () => { if (CONFIG.skipOutro > 0 && video.duration) { const remaining = video.duration - video.currentTime; if (remaining <= CONFIG.skipOutro && remaining > CONFIG.skipOutro - 1) { Utils.info(`已跳过片尾 ${CONFIG.skipOutro}s`); video.currentTime = video.duration; video.pause(); } } }); }, takeScreenshot(video) { try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0); const link = document.createElement('a'); link.download = `screenshot_${Date.now()}.png`; link.href = canvas.toDataURL('image/png'); link.click(); this.showToast('截图已保存'); } catch (e) { this.showToast('截图失败(可能有跨域限制)'); } }, showVideoInfo(video) { const info = [ `当前地址: ${location.href}`, `视频时长: ${this.formatTime(video.duration)}`, `当前时间: ${this.formatTime(video.currentTime)}`, `播放速率: ${video.playbackRate}x`, `分辨率: ${video.videoWidth}x${video.videoHeight}`, ]; GM_setClipboard(info.join('\n'), 'text'); this.showToast('视频信息已复制到剪贴板'); }, formatTime(sec) { if (!sec || isNaN(sec)) return '00:00'; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; }, showToast(msg) { const toast = document.createElement('div'); toast.className = 've-toast'; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); }, }; // ============================================================ // 模块二B:视频源检测与真实下载 // ============================================================ const VideoDownloader = { capturedURLs: [], init() { this.hookMediaRequests(); Utils.info('视频源拦截已启用'); }, // 在 document-start 阶段 hook,拦截所有视频/音频请求 hookMediaRequests() { const self = this; const videoExts = /\.(mp4|m3u8|m4s|ts|flv|webm|mkv|avi|mov|mp3|aac|m4a|ogg|wav)(\?|$)/i; const videoContentTypes = /(video|audio)\/(mp4|mpeg|ogg|webm|x-flv|mp2t|apple-mpegurl|x-mpegURL|octet-stream|quicktime)/i; const cdnPatterns = [ // B站 /bilivideo\.com/, /cn-[a-z]+-[a-z]\d*\.bilivideo\.com/, /upos-sz-[a-z]+\.bilivideo\.com/, /akamaihd\.net/, // 优酷 /youku\.com\/.*\.(mp4|m3u8)/, /kugou\.com/, /vali\.cpm\.youku\.com/, /a1\.ykimg\.com/, // 爱奇艺 /iqiyi\.com\/.*\.(mp4|m3u8)/, /iq\.com\/.*\.(mp4|m3u8)/, /qiyi\.com/, /71\.am\.com/, // PPTV /pptv\.com\/.*\.(mp4|m3u8)/, /ppimg\.com/, // 1905 /1905\.com\/.*\.(mp4|m3u8)/, // 西瓜 /ixigua\.com\/.*\.(mp4|m3u8)/, /pstatp\.com/, /snssdk\.com/, /byteimg\.com/, // iflix /iflix\.com\/.*\.(mp4|m3u8)/, // 土豆 /tudou\.com\/.*\.(mp4|m3u8)/, // 通用CDN /vod\.|video\.|media\.|stream\.|cdn\./, /cloudfront\.net/, /akamaized\.net/, /llnwi\.net/, /vod-cdn\./, /m3u8/, /\.mp4/, /\.flv/, ]; const isVideoURL = (url) => { if (!url || typeof url !== 'string') return false; return videoExts.test(url) || cdnPatterns.some(p => p.test(url)); }; // Hook XMLHttpRequest const origXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...args) { if (isVideoURL(url)) { self.captureURL(url, 'XHR'); } return origXHROpen.call(this, method, url, ...args); }; // Hook Fetch const origFetch = window.fetch; window.fetch = function (urlOrReq, options) { const url = typeof urlOrReq === 'string' ? urlOrReq : (urlOrReq && urlOrReq.url) || ''; if (isVideoURL(url)) { self.captureURL(url, 'Fetch'); } return origFetch.call(this, urlOrReq, options); }; // Hook