// ==UserScript== // @name HLS SSAI Ad Cleaner (Mobile Preview) // @name:zh-CN HLS SSAI 广告过滤工具 (移动端预览版) // @namespace hls-ssai-ad-cleaner-preview // @version 3.0-preview // @description 针对移动端 Edge 优化的 HLS 净化预览版。修正了过滤逻辑并增强了标签保留,解决了移动端内核加载失败的问题。 // @description:zh-CN 针对移动端 Edge 优化的 HLS 净化预览版。修正了过滤逻辑并增强了标签保留,解决了移动端内核加载失败的问题。 // @author Gavin Newsom // @match *://*/* // @grant none // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/576352/HLS%20SSAI%20Ad%20Cleaner%20%28Mobile%20Preview%29.user.js // @updateURL https://update.greasyfork.icu/scripts/576352/HLS%20SSAI%20Ad%20Cleaner%20%28Mobile%20Preview%29.meta.js // ==/UserScript== (function () { 'use strict'; const LOG_PREFIX = '[AdCleaner-Preview]'; const oldXhrOpen = XMLHttpRequest.prototype.open; const oldFetch = window.fetch; /** * 深度净化核心逻辑 (移动端稳定版) */ function getPurifiedUrlSync(url) { if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:')) return url; if (!url.includes('m3u8')) return url; try { const xhr = new XMLHttpRequest(); xhr.isInternalRequest = true; // 使用同步请求获取内容 oldXhrOpen.apply(xhr, ['GET', url, false]); xhr.send(); if (xhr.status === 200) { let content = xhr.responseText; const lines = content.split('\n'); const newLines = []; const isMaster = content.includes('#EXT-X-STREAM-INF'); const hasAds = content.includes('#EXT-X-DISCONTINUITY'); if (isMaster) { for (let line of lines) { let t = line.trim(); if (t && !t.startsWith('#')) { const absUrl = new URL(t, url).href; newLines.push(getPurifiedUrlSync(absUrl)); } else { newLines.push(line); } } } else if (hasAds) { let count = 0, keep = true; for (let line of lines) { let t = line.trim(); if (!t) continue; if (t.startsWith('#EXT-X-DISCONTINUITY')) { count++; // 修正后的逻辑:偶数区间(0, 2, 4...)通常是正片 keep = (count % 2 === 0); newLines.push(t); continue; } // 关键:保留所有非片段配置标签,确保索引文件格式完整 if (t.startsWith('#EXT-X-') && !t.startsWith('#EXTINF')) { newLines.push(t); continue; } // 处理视频片段信息 if (keep || t.startsWith('#EXTM3U') || t.startsWith('#EXT-X-ENDLIST')) { if (!t.startsWith('#') && !t.startsWith('http')) { t = new URL(t, url).href; } newLines.push(t); } } } else { // 无广告时,仅执行绝对路径转换 for (let line of lines) { let t = line.trim(); if (t && !t.startsWith('#') && !t.startsWith('http')) { newLines.push(new URL(t, url).href); } else { newLines.push(line); } } } const finalContent = newLines.join('\n'); const finalBlob = URL.createObjectURL(new Blob([finalContent], { type: 'application/vnd.apple.mpegurl' })); // 打印净化对比日志 if (content.length !== finalContent.length) { console.log(`${LOG_PREFIX} 净化成功: ${url.split('/').pop()} (${content.length} -> ${finalContent.length} bytes)`); } return finalBlob; } } catch (e) { console.error(`${LOG_PREFIX} 净化过程中发生异常:`, e); } return url; } // 劫持 XHR Open XMLHttpRequest.prototype.open = function (m, url, ...args) { if (!this.isInternalRequest && typeof url === 'string' && url.includes('m3u8')) { url = getPurifiedUrlSync(url); } return oldXhrOpen.apply(this, [m, url, ...args]); }; // 劫持 Fetch API window.fetch = function (input, init) { let url = (input instanceof Request) ? input.url : String(input); if (!init?.isInternal && url.includes('m3u8') && !url.startsWith('blob:')) { url = getPurifiedUrlSync(url); input = (input instanceof Request) ? new Request(url, input) : url; } return oldFetch(input, init); }; // 劫持 HTMLMediaElement.src 及其子元素 const hijackProperty = (proto, prop) => { const desc = Object.getOwnPropertyDescriptor(proto, prop); if (!desc) return; Object.defineProperty(proto, prop, { get: function () { return desc.get.call(this); }, set: function (val) { if (val && typeof val === 'string' && val.includes('m3u8') && !val.startsWith('blob:')) { val = getPurifiedUrlSync(val); } return desc.set.call(this, val); } }); }; hijackProperty(HTMLMediaElement.prototype, 'src'); if (window.HTMLSourceElement) hijackProperty(HTMLSourceElement.prototype, 'src'); console.log(`${LOG_PREFIX} 移动端预览版(3.0)已就绪`); })();