Greasy Fork

来自缓存

Greasy Fork is available in English.

HLS SSAI 广告过滤工具 (奇数不连续识别)

专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         HLS SSAI Ad Cleaner (Odd Discontinuity)
// @name:zh-CN   HLS SSAI 广告过滤工具 (奇数不连续识别)
// @namespace    hls-ssai-ad-cleaner-cleaner
// @version      2.0
// @description      专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。
// @description:zh-CN 专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。
// @description:en  Advanced SSAI ad cleaner for HLS (m3u8) streams. Uses "Recursive Blob Proxying" to purify master and variant playlists, effectively bypassing native HLS playback limitations in Chromium 147+ to ensure ad-free playback.
// @author       Gavin Newsom
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const LOG_PREFIX = '[AdCleaner]';
    const oldXhrOpen = XMLHttpRequest.prototype.open;
    const oldFetch = window.fetch;

    /**
     * 深度净化核心逻辑
     * 原理:拦截主索引,同步递归净化所有子资源并转换为 Blob URL,从而绕过内核原生请求限制。
     */
    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);
                        }
                    }
                    content = newLines.join('\n');
                } else if (hasAds) {
                    let count = 0, keep = true;
                    for (let line of lines) {
                        let t = line.trim();
                        if (t.startsWith('#EXT-X-DISCONTINUITY')) {
                            count++;
                            keep = (count % 2 !== 0);
                            continue;
                        }
                        if (keep || t.startsWith('#EXT-X-ENDLIST') || t.startsWith('#EXTM3U')) {
                            if (t && !t.startsWith('#') && !t.startsWith('http')) {
                                t = new URL(t, url).href;
                            }
                            newLines.push(t);
                        }
                    }
                    content = newLines.join('\n');
                } 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);
                        }
                    }
                    content = newLines.join('\n');
                }

                const finalBlob = URL.createObjectURL(new Blob([content], { type: 'application/vnd.apple.mpegurl' }));
                console.log(`${LOG_PREFIX} 资源净化成功: ${url.split('?')[0]} -> ${finalBlob}`);
                return finalBlob;
            }
        } catch (e) {
            console.error(`${LOG_PREFIX} 净化失败:`, e);
        }
        return url;
    }

    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]);
    };

    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);
    };

    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');

    setInterval(() => {
        document.querySelectorAll('video, source').forEach(el => {
            const currentSrc = el.src || el.getAttribute('src');
            if (currentSrc && currentSrc.includes('m3u8') && !currentSrc.startsWith('blob:') && !el.dataset.cleaned) {
                el.dataset.cleaned = "true";
                const newUrl = getPurifiedUrlSync(currentSrc);
                el.src = newUrl;
                if (el.tagName === 'VIDEO') {
                    el.load();
                    el.play().catch(() => { });
                } else if (el.parentElement && el.parentElement.tagName === 'VIDEO') {
                    el.parentElement.load();
                    el.parentElement.play().catch(() => { });
                }
            }
        });
    }, 1500);

    console.log(`${LOG_PREFIX} 系统就绪`);
})();