Greasy Fork is available in English.
针对移动端 Edge 优化的 HLS 净化预览版。修正了过滤逻辑并增强了标签保留,解决了移动端内核加载失败的问题。
// ==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
// ==/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)已就绪`);
})();