// ==UserScript== // @name 媒体资源深度嗅探 (M3U8/MP4 & Master多码率解析) // @namespace https://viayoo.com/ // @version 2.5.0 // @description 深度嗅探网页 M3U8/MP4 链接。自动识别并展开 Master M3U8 (假链接) 的多码率列表,支持下载 M3U8 索引文件及复制专业下载命令。 // @author Qwen // @match *://*/* // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @connect * // @license Apache-2.0 // ==/UserScript== (function () { 'use strict'; const foundUrls = new Set(); let panel = null; let isPanelVisible = false; let listContainer = null; // ================= 核心:网络请求拦截 ================= const originalFetch = window.fetch; window.fetch = function (...args) { checkUrl(args); return originalFetch.apply(this, args); }; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { checkUrl(url); return originalOpen.apply(this, arguments); }; // 定时扫描 DOM 中的 video/audio 标签 (防漏) setInterval(() => { document.querySelectorAll('video, audio, source').forEach(el => { if (el.src) checkUrl(el.src); if (el.currentSrc) checkUrl(el.currentSrc); }); }, 3000); function checkUrl(url) { if (!url) return; const rawUrl = typeof url === 'object' ? (url.href || url.url || url.toString()) : url.toString(); if (!rawUrl || rawUrl.startsWith('blob:') || rawUrl.startsWith('data:')) return; // 匹配 m3u8 和 mp4 if (rawUrl.match(/\.m3u8(\?|$)/i) || rawUrl.match(/\.mp4(\?|$)/i)) { addResource(rawUrl); } } // ================= 核心:M3U8 解析与处理 ================= function addResource(url) { if (foundUrls.has(url)) return; foundUrls.add(url); if (url.includes('.m3u8')) { // 获取 M3U8 内容判断是否为 Master (多码率索引) fetchContent(url, (content) => { if (content.includes('#EXT-X-STREAM-INF')) { const streams = parseMasterM3U8(content, url); renderMasterResource(url, streams); } else { renderSingleResource(url, 'M3U8 (媒体流)'); } }, () => { renderSingleResource(url, 'M3U8 (跨域受限)'); }); } else { renderSingleResource(url, 'MP4'); } } function fetchContent(url, onSuccess, onError) { GM_xmlhttpRequest({ method: 'GET', url: url, onload: (res) => onSuccess(res.responseText), onerror: onError, ontimeout: onError }); } function parseMasterM3U8(content, baseUrl) { const lines = content.split('\n'); const streams = []; let currentInfo = {}; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXT-X-STREAM-INF:')) { const resMatch = line.match(/RESOLUTION=(\d+x\d+)/); const bwMatch = line.match(/BANDWIDTH=(\d+)/); const nameMatch = line.match(/NAME="([^"]+)"/); currentInfo = { resolution: resMatch ? resMatch : '未知', bandwidth: bwMatch ? Math.round(bwMatch / 1000) + ' kbps' : '', name: nameMatch ? nameMatch : '' }; } else if (line && !line.startsWith('#') && (currentInfo.resolution || currentInfo.name)) { let subUrl = line; if (!subUrl.startsWith('http')) { try { const base = new URL(baseUrl); const basePath = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1); subUrl = new URL(subUrl, base.origin + basePath).href; } catch (e) { /* 忽略解析错误 */ } } streams.push({ ...currentInfo, url: subUrl }); currentInfo = {}; } } return streams; }[[source_group_web_1]] // ================= UI 渲染逻辑 ================= function createPanel() { if (panel) return; panel = document.createElement('div'); panel.id = 'media-sniffer-panel'; panel.style.cssText = ` position: fixed; bottom: 80px; right: 15px; z-index: 2147483647; background: rgba(24, 24, 27, 0.95); color: #e4e4e7; padding: 15px; border-radius: 12px; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; width: 380px; max-height: 60vh; overflow-y: auto; box-shadow: 0 10px 25px rgba(0,0,0,0.5); display: none; border: 1px solid #3f3f46; backdrop-filter: blur(10px); `; const header = document.createElement('div'); header.style.cssText = 'display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; color:#f