// ==UserScript== // @name M3U8 Filter Ad Script // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description 自用,拦截和过滤 m3u8(解析/采集资源) 的广告切片,同时打印被过滤的行信息。 // @author ltxlong // @match *://*/* // @grant unsafeWindow // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; initHook(); function initHook() { hookXHR(); } let ts_name_len = 0; // ts前缀长度 let ts_name_len_extend = 1; // 容错 let first_extinf_row = ''; let the_extinf_judge_row_n = 0; let the_same_extinf_name_n = 0; let the_extinf_benchmark_n = 5; // 基准 let prev_ts_name_index = -1; // ts序列号 let ts_type = 0; // 0-xxxx000数字递增.ts模式 ;1-xxxxxxxxxx.ts模式 let the_ext_x_mode = 0; // 0-ext_x_discontinuity判断模式0;1-ext_x_discontinuity判断模式1 function isM3U8File(url) { return /\.m3u8($|\?)/.test(url); } async function safelyProcessM3U8(url, content) { try { const lines = content.split('\n'); const newLines = filterLines(lines); return newLines.join('\n'); } catch (e) { console.error(`处理 m3u8 文件时出错: ${url}`, e); return content; } } function extractNumberBeforeTS(str) { // 匹配 .ts 前面的数字 const match = str.match(/(\d+)\.ts/); if (match) { // 使用 parseInt 去掉前导 0 return parseInt(match[1], 10); } return null; // 如果不匹配,返回 null } function filterLines(lines) { let result = []; // 先根据第一、二个ts名称来初始化参数 for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (the_extinf_judge_row_n === 0 && line.startsWith('#EXTINF')) { first_extinf_row = line; the_extinf_judge_row_n++; } else if (the_extinf_judge_row_n === 1 && line.startsWith('#EXTINF')) { if (line !== first_extinf_row) { first_extinf_row = ''; } the_extinf_judge_row_n++; } let the_ts_name_len = line.indexOf('.ts'); // ts前缀长度 if (the_ts_name_len > 0) { ts_name_len = the_ts_name_len; let ts_name_index = extractNumberBeforeTS(line); if (ts_name_index === null) { ts_type = 1; // ts命名模式 console.log('----------------------------识别ts模式1---------------------------'); break; } else { if (the_extinf_judge_row_n === 1) { prev_ts_name_index = ts_name_index; // ts序列号 } else if (the_extinf_judge_row_n === 2) { if (ts_name_index !== prev_ts_name_index + 1) { ts_type = 1; // ts命名模式 console.log('----------------------------识别ts模式1---------------------------'); break; } else { prev_ts_name_index--; } } } if (the_extinf_judge_row_n === 2) { console.log('----------------------------识别ts模式0---------------------------') break; } } } // 开始遍历过滤 for (let i = 0; i < lines.length; i++) { let ts_index_check = false; const line = lines[i]; if (ts_type === 0) { if (line.startsWith('#EXT-X-DISCONTINUITY') && lines[i + 1] && lines[i + 2]) { // 检查当前行是否跟 #EXT-X-PLAYLIST-TYPE相关 if (i > 0 && lines[i - 1].startsWith('#EXT-X-PLAYLIST-TYPE')) { result.push(line); continue; } else { let the_ts_name_len = lines[i + 2].indexOf('.ts'); // ts前缀长度 if (the_ts_name_len > 0) { // 根据ts名字长度过滤 if (the_ts_name_len - ts_name_len > ts_name_len_extend) { // 广告过滤 if (lines[i + 3] && lines[i + 3].startsWith('#EXT-X-DISCONTINUITY')) { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-ts文件名长度-'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2], "\n", lines[i + 3]); console.log('------------------------------------------------------------------'); i += 3; } else { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-ts文件名长度'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2]); console.log('------------------------------------------------------------------'); i += 2; } continue; } else { ts_name_len = the_ts_name_len; } // 根据ts序列号过滤 let the_ts_name_index = extractNumberBeforeTS(lines[i + 2]); if (the_ts_name_index !== prev_ts_name_index + 1) { // 广告过滤 if (lines[i + 3] && lines[i + 3].startsWith('#EXT-X-DISCONTINUITY')) { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-ts序列号-'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2], "\n", lines[i + 3]); console.log('------------------------------------------------------------------'); i += 3; } else { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-ts序列号'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2]); console.log('------------------------------------------------------------------'); i += 2; } continue; } } } } if (line.startsWith('#EXTINF') && lines[i + 1]) { let the_ts_name_len = lines[i + 1].indexOf('.ts'); // ts前缀长度 if (the_ts_name_len > 0) { // 根据ts名字长度过滤 if (the_ts_name_len - ts_name_len > ts_name_len_extend) { // 广告过滤 if (lines[i + 2] && lines[i + 2].startsWith('#EXT-X-DISCONTINUITY')) { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXTINF-ts文件名长度-'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2]); console.log('------------------------------------------------------------------'); i += 2; } else { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXTINF-ts文件名长度'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1]); console.log('------------------------------------------------------------------'); i += 1; } continue; } else { ts_name_len = the_ts_name_len; } // 根据ts序列号过滤 let the_ts_name_index = extractNumberBeforeTS(lines[i + 1]); if (the_ts_name_index === prev_ts_name_index + 1) { prev_ts_name_index++; } else { // 广告过滤 if (lines[i + 2].startsWith('#EXT-X-DISCONTINUITY')) { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXTINF-ts序列号-'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2]); console.log('------------------------------------------------------------------'); i += 2; } else { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXTINF-ts序列号'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1]); console.log('------------------------------------------------------------------'); i += 1; } continue; } } } } else { if (line.startsWith('#EXTINF')) { if (line === first_extinf_row && the_same_extinf_name_n <= the_extinf_benchmark_n && the_ext_x_mode === 0) { the_same_extinf_name_n++; } else { the_ext_x_mode = 1; } if (the_same_extinf_name_n > the_extinf_benchmark_n) { the_ext_x_mode = 1; } } if (line.startsWith('#EXT-X-DISCONTINUITY')) { // 检查当前行是否跟 #EXT-X-PLAYLIST-TYPE相关 if (i > 0 && lines[i - 1].startsWith('#EXT-X-PLAYLIST-TYPE')) { result.push(line); continue; } else { // 如果第 i+2 行是 .ts 文件,跳过当前行和接下来的两行 if (lines[i + 1] && lines[i + 1].startsWith('#EXTINF') && lines[i + 2] && lines[i + 2].indexOf('.ts') > 0) { let the_ext_x_discontinuity_condition_flag = false; if (the_ext_x_mode === 1) { the_ext_x_discontinuity_condition_flag = lines[i + 1] !== first_extinf_row && the_same_extinf_name_n > the_extinf_benchmark_n; } // 进一步检测第 i+3 行是否也是 #EXT-X-DISCONTINUITY if (lines[i + 3] && lines[i + 3].startsWith('#EXT-X-DISCONTINUITY') && the_ext_x_discontinuity_condition_flag) { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-广告-#EXT-X-DISCONTINUITY过滤'); console.log('过滤的行:', "\n", line, "\n", lines[i + 1], "\n", lines[i + 2], "\n", lines[i + 3]); console.log('------------------------------------------------------------------'); i += 3; // 跳过当前行和接下来的三行 } else { // 打印即将过滤的行 console.log('------------------------------------------------------------------'); console.log('过滤规则: #EXT-X-DISCONTINUITY-单个标识过滤'); console.log('过滤的行:', "\n", line); console.log('------------------------------------------------------------------'); } continue; } } } } // 保留不需要过滤的行 result.push(line); } return result; } function hookXHR() { const OriginalXHR = unsafeWindow.XMLHttpRequest; unsafeWindow.XMLHttpRequest = class extends OriginalXHR { constructor() { super(); this.addEventListener('readystatechange', async function () { if (this.readyState === 4 && this.status === 200 && isM3U8File(this.responseURL)) { const modifiedResponse = await safelyProcessM3U8(this.responseURL, this.responseText); Object.defineProperty(this, 'responseText', { value: modifiedResponse }); Object.defineProperty(this, 'response', { value: modifiedResponse }); } }, false); } }; } })();