Greasy Fork

Greasy Fork is available in English.

HLS SSAI Ad Cleaner (Mobile Preview)

针对移动端 Edge 优化的 HLS 净化预览版。修正了过滤逻辑并增强了标签保留,解决了移动端内核加载失败的问题。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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)已就绪`);
})();