// ==UserScript== // @name M3U8视频链接检测助手 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 自动检测页面中的M3U8视频链接,智能验证可用性,支持一键复制 // @author MissChina // @license 仅限个人非商业用途,禁止商业使用 // @match *://*/* // @run-at document-start // @grant GM_setClipboard // @grant GM_notification // @icon data:image/svg+xml,🎬 // @downloadURL https://update.greasyfork.icu/scripts/557172/M3U8%E8%A7%86%E9%A2%91%E9%93%BE%E6%8E%A5%E6%A3%80%E6%B5%8B%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/557172/M3U8%E8%A7%86%E9%A2%91%E9%93%BE%E6%8E%A5%E6%A3%80%E6%B5%8B%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; // -------------------- 配置与全局状态 -------------------- const validLinks = new Set(); // 已确认有效的 M3U8 链接 const pendingLinks = new Map(); // 待确认的 M3U8 链接 const logs = []; // 日志记录 let panel = null; // 面板根节点 let activeTab = 'links'; // 当前激活的 Tab let tsDetectedCount = 0; // .ts 检测计数(限制次数) // ======================================================== // 日志系统 // ======================================================== function log(msg, type = 'info') { const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }); logs.push({ time: timestamp, msg, type }); if (logs.length > 100) logs.shift(); updatePanel(); } // ======================================================== // 工具函数 // ======================================================== // 判断是否为 M3U8 链接 function isM3U8(url) { if (!url || typeof url !== 'string') return false; return /\.m3u8(?:\?|#|$)/.test(url) && !url.includes('.ts'); } // 获取 URL 的目录路径 function getPathOnly(url) { try { const u = new URL(url); return u.pathname.substring(0, u.pathname.lastIndexOf('/')); } catch(e) { return ''; } } // 获取 URL 的 origin function getOrigin(url) { try { return new URL(url).origin; } catch(e) { return ''; } } // ======================================================== // 面板刷新 // ======================================================== function updatePanel() { if (!panel) return; const cntEl = document.getElementById('cnt'); const lcntEl = document.getElementById('lcnt'); const cont = document.getElementById('cont'); if (cntEl) cntEl.textContent = validLinks.size; if (lcntEl) lcntEl.textContent = logs.length; if (!cont) return; if (activeTab === 'links') { // 链接列表 if (validLinks.size === 0) { cont.innerHTML = '
当前页面暂无 M3U8 链接
'; } else { cont.innerHTML = Array.from(validLinks).map(url => `
${url}
`).join(''); cont.querySelectorAll('.m3u8-btn-pri').forEach(btn => { btn.onclick = () => window.m3u8Copy(btn.dataset.url); }); } } else if (activeTab === 'logs') { // 日志列表 if (logs.length === 0) { cont.innerHTML = '
暂无日志
'; } else { const typeColors = { info: '#4b5563', success: '#16a34a', warning: '#d97706', error: '#dc2626' }; cont.innerHTML = `
` + logs.slice().reverse().map(l => `
${l.time} ${l.msg}
`).join(''); const clearBtn = document.getElementById('clear-logs'); if (clearBtn) { clearBtn.onclick = () => { logs.length = 0; updatePanel(); }; } } } } // ======================================================== // 复制 M3U8 链接 // ======================================================== window.m3u8Copy = function(url) { try { GM_setClipboard(url); GM_notification({ title: '✅ 复制成功', text: '链接已复制到剪贴板', timeout: 2000 }); log('📋 已复制链接: ' + url, 'success'); } catch(e) { log('❌ 复制失败: ' + e.toString(), 'error'); } }; // ======================================================== // 链接状态管理 // ======================================================== function addValid(url) { if (!validLinks.has(url)) { log('✅ 发现 M3U8: ' + url, 'success'); validLinks.add(url); pendingLinks.delete(url); if (validLinks.size === 1) { setTimeout(showPanel, 120); } else { updatePanel(); } } } function addPending(url) { if (!validLinks.has(url) && !pendingLinks.has(url)) { pendingLinks.set(url, { tsCount: 0 }); } } // 处理 .ts 资源侦测,用来辅助确认真实 m3u8 地址 function onTS(tsUrl) { if (tsDetectedCount >= 3) return; tsDetectedCount++; const tsPath = getPathOnly(tsUrl); const tsOrigin = getOrigin(tsUrl); for (const [m3u8Url, info] of pendingLinks) { const m3u8Path = getPathOnly(m3u8Url); const m3u8Origin = getOrigin(m3u8Url); if (m3u8Path === tsPath) { info.tsCount++; if (info.tsCount >= 1) { if (tsOrigin !== m3u8Origin) { try { const m3u8UrlObj = new URL(m3u8Url); const tsUrlObj = new URL(tsUrl); const realUrl = tsUrlObj.origin + m3u8UrlObj.pathname + m3u8UrlObj.search; log('🔧 检测到重定向,尝试构造真实 M3U8 URL', 'warning'); pendingLinks.delete(m3u8Url); addValid(realUrl); } catch(e) { addValid(m3u8Url); } } else { addValid(m3u8Url); } } } } } // ======================================================== // Hook fetch / XHR / PerformanceObserver // ======================================================== // 重写 fetch const _fetch = window.fetch; window.fetch = function(...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url; if (url && isM3U8(url)) { addPending(url); return _fetch.apply(this, args).then(res => { if (res.status === 200) { res.clone().text().then(txt => { if (txt && txt.includes('#EXTM3U')) addValid(url); }).catch(() => {}); } return res; }); } else if (url && url.includes('.ts')) { onTS(url); } return _fetch.apply(this, args); }; // 重写 XMLHttpRequest const _open = XMLHttpRequest.prototype.open; const _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...rest) { this.__url = url; return _open.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function(...args) { const url = this.__url; if (url && isM3U8(url)) { addPending(url); this.addEventListener('load', function() { if (this.status === 200) { try { const txt = this.responseText; if (txt && txt.includes('#EXTM3U')) addValid(url); } catch(e) {} } }); } else if (url && url.includes('.ts')) { onTS(url); } return _send.apply(this, args); }; // PerformanceObserver 监听资源加载 try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { const url = entry.name; if (isM3U8(url)) { addPending(url); } else if (url.includes('.ts')) { onTS(url); } } }); observer.observe({ entryTypes: ['resource'] }); } catch(e) {} // ======================================================== // 面板创建 & 拖动 // ======================================================== function showPanel() { if (!panel) { if (document.body) { createPanel(); } else { setTimeout(showPanel, 100); } } else { panel.style.display = 'block'; } } function createPanel() { if (panel) return; panel = document.createElement('div'); panel.setAttribute('data-m3u8-panel', 'true'); panel.innerHTML = `
M
M3U8 检测助手
`; document.body.appendChild(panel); const root = document.getElementById('m3u8-root'); const bodyEl = document.getElementById('m3u8-body'); const minBtn = document.getElementById('m3u8-min'); const closeBtn = document.getElementById('m3u8-close'); // 折叠 if (minBtn) { minBtn.onclick = () => { const hidden = bodyEl.style.display === 'none'; bodyEl.style.display = hidden ? 'block' : 'none'; minBtn.textContent = hidden ? '−' : '+'; }; } // 关闭 if (closeBtn) { closeBtn.onclick = () => { panel.style.display = 'none'; }; } // Tab 切换 panel.querySelectorAll('.m3u8-tab').forEach(tab => { tab.onclick = () => { panel.querySelectorAll('.m3u8-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); activeTab = tab.dataset.tab; updatePanel(); }; }); // 拖动:拖动根容器 #m3u8-root const dragEl = document.getElementById('m3u8-drag'); let dragging = false; let startX = 0, startY = 0; let startLeft = 0, startTop = 0; const onMouseMove = (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; root.style.left = startLeft + dx + 'px'; root.style.top = startTop + dy + 'px'; root.style.right = 'auto'; root.style.bottom = 'auto'; }; const endDrag = () => { if (!dragging) return; dragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', endDrag); }; if (dragEl) { dragEl.addEventListener('mousedown', (e) => { if (e.target.closest('.m3u8-btn-hdr')) return; e.preventDefault(); const rect = root.getBoundingClientRect(); dragging = true; startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', endDrag); }); } updatePanel(); } })();