// ==UserScript== // @name M3U8 嗅探 + MediaGo 投喂器 // @namespace https://blog.zhecydn.asia/ // @version 2.0 // @description 一键投喂M3U8视频资源到 MediaGo(支持 docker 与本地版),具备自动防重名命名、4K/1080P 🔥 标注及文件夹自动整理功能 // @author zhecydn // @match *://*/* // @allframes true // @run-at document-start // @license MIT // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== (function() { 'use strict'; let MEDIAGO_URL = GM_getValue('mediago_url', ''); let theme = GM_getValue('theme', 'dark'); let mode = GM_getValue('mode', 'api'); let target = GM_getValue('target', 'nas'); let folderType = GM_getValue('folder_type', 'domain'); let counter = GM_getValue('counter', {}); let detectedUrls = new Set(); let panel = null; // --- 1. 跨页面通信 --- if (window.self !== window.top) { window.notifyTop = url => window.top.postMessage({ type: 'VIDEO_MSG_V20', url: url }, '*'); } else { window.addEventListener('message', e => { if (e.data && e.data.type === 'VIDEO_MSG_V20') addUrl(e.data.url); }); } // --- 2. 辅助逻辑 --- const getResTag = (u) => { u = u.toLowerCase(); // 8K 档 if (u.includes('8k') || u.includes('4320')) return '[👑 8K] '; // 4K 档 if (u.includes('4k') || u.includes('2160')) return '[💎 4K] '; // 2K / 1440P 档 if (u.includes('2k') || u.includes('1440')) return '[🚀 2K] '; // 1080P 档 if (u.includes('1080') || u.includes('1920') || u.includes('3000k')) return '[🔥 1080P] '; // 720P 档 if (u.includes('720') || u.includes('1280')) return '[🌟 720P] '; // 480P 档 if (u.includes('480') || u.includes('848') || u.includes('800k')) return '[🍃 480P] '; return ''; }; const getFolder = () => folderType === 'domain' ? location.hostname.split('.')[0] : ''; const getSmartName = (base) => { if (!counter[base]) counter[base] = 0; counter[base]++; GM_setValue('counter', counter); const now = new Date(); const ts = `${now.getHours()}${now.getMinutes()}${now.getSeconds()}`; return `${base}_${counter[base]}_${ts}`; }; // --- 3. 嗅探核心 --- function addUrl(url) { if (typeof url !== 'string' || !/\.m3u8(\?|$)/i.test(url) || detectedUrls.has(url)) return; if (url.startsWith('blob:')) return; if (window.self !== window.top) { window.notifyTop(url); return; } detectedUrls.add(url); if (!panel) createPanel(); const li = document.createElement('li'); li.innerHTML = `
${getResTag(url)}${url.split('?')[0].substring(0, 60)}...
`; document.getElementById('m3u8-list').prepend(li); const btn = li.querySelector('.single-send'); btn.onclick = () => sendTask(url, btn); } // A. 拦截 XHR/Fetch const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(m, u) { try { addUrl(new URL(u, location.href).href); } catch(e) {} return origOpen.apply(this, arguments); }; const origFetch = window.fetch; window.fetch = function(res) { let u = typeof res === 'string' ? res : (res && res.url); if (u) { try { addUrl(new URL(u, location.href).href); } catch(e) {} } return origFetch.apply(this, arguments); }; // B. 定时扫描 DOM (补回遗漏的静态扫描) setInterval(() => { document.querySelectorAll('video, source, a').forEach(el => { const src = el.src || el.getAttribute('src') || el.href; if (src && src.includes('.m3u8')) { try { addUrl(new URL(src, location.href).href); } catch(e) {} } }); }, 3000); // --- 4. 投喂逻辑 --- function sendTask(url, btn, customName = null) { const baseTitle = document.title || '视频任务'; let finalName = ""; if (customName === null) { const n = prompt('确认任务名称:', baseTitle); if (n === null) return; finalName = getSmartName(n.trim() || baseTitle); } else { finalName = getSmartName(customName); } const folder = getFolder(); const encodedName = encodeURIComponent(finalName); const encodedUrl = encodeURIComponent(url); const folderParam = folder ? `&folder=${encodeURIComponent(folder)}` : ''; if (target === 'local') { const jump = `mediago://index.html/?n=true&name=${encodedName}&url=${encodedUrl}&headers=Referer%3A*${folderParam}&type=m3u8&silent=true`; window.open(jump, '_blank'); } else { if (!MEDIAGO_URL) return alert('请先⚙️设置 mediago docker 地址'); if (mode === 'api') { GM_xmlhttpRequest({ method: 'POST', url: `${MEDIAGO_URL}/api/download-now`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ name: finalName, url: url, type: 'm3u8', folder: folder }), onload: () => console.log('API发送成功') }); } else { const jump = `${MEDIAGO_URL}/?n=true&name=${encodedName}&url=${encodedUrl}&headers=Referer%3A*${folderParam}&type=m3u8&silent=true`; window.open(jump, '_blank'); } } if (btn) { btn.innerText = "✅ 已投喂 (可重投)"; btn.style.opacity = "0.5"; } } // --- 5. UI 界面 --- function createPanel() { if (window.self !== window.top || document.getElementById('mediago-panel')) return; panel = document.createElement('div'); panel.id = 'mediago-panel'; panel.className = theme; panel.innerHTML = `
🔍 m3u8资源嗅探器 (MediaGo) 🌓 ⚙️
`; GM_addStyle(` #mediago-panel { position: fixed; top: 20px; right: 20px; width: 380px; max-height: 80vh; padding: 12px; border-radius: 12px; z-index: 2147483647; font-family: sans-serif; box-shadow: 0 10px 40px rgba(0,0,0,0.5); display: flex; flex-direction: column; border: 1px solid rgba(128,128,128,0.3); } #mediago-panel.dark { background: rgba(30,30,30,0.95); color: #fff; } #mediago-panel.light { background: rgba(255,255,255,0.98); color: #111; } #p-header { cursor: move; padding: 10px; background: rgba(128,128,128,0.2); border-radius: 8px; font-weight: bold; text-align: center; font-size: 14px; } .top-bar { display:flex; gap:10px; padding:10px; justify-content:center; border-bottom:1px solid rgba(128,128,128,0.2); } #sel-all { background:#666; } #batch-btn { background:#e67e22; } #m3u8-list { list-style: none; padding: 0; margin: 10px 0; overflow-y: auto; flex: 1; } #m3u8-list li { margin: 8px 0; padding: 10px; background: rgba(128,128,128,0.1); border-radius: 8px; position: relative; border-left: 4px solid #27ae60; } #mediago-panel button { color: white; border: none; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold; } .single-send { background: #27ae60; margin-top: 8px; width: 100%; } .url-text { font-size: 11px; word-break: break-all; opacity: 0.9; margin-left: 25px; line-height: 1.4; } .checkbox { position: absolute; top: 12px; left: 8px; transform: scale(1.1); } #p-footer { font-size: 11px; padding-top: 8px; border-top: 1px solid rgba(128,128,128,0.2); } .ctrl-row { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 4px 0; } .sub-row { margin-top: 5px; border-top: 1px dashed rgba(128,128,128,0.3); padding-top: 8px; } #p-footer label { cursor: pointer; display: flex; align-items: center; gap: 3px; } `); document.body.appendChild(panel); const header = document.getElementById('p-header'); let isDrag = false, ox, oy; header.onmousedown = e => { if(e.target.tagName==='SPAN') return; isDrag=true; ox=e.clientX-panel.offsetLeft; oy=e.clientY-panel.offsetTop; }; document.onmousemove = e => { if(isDrag){ panel.style.left=(e.clientX-ox)+'px'; panel.style.top=(e.clientY-oy)+'px'; panel.style.right='auto'; } }; document.onmouseup = () => isDrag=false; document.getElementById('set-btn').onclick = () => { let u = prompt('NAS 地址:', MEDIAGO_URL); if(u){ MEDIAGO_URL = u.trim().replace(/\/+$/, ''); GM_setValue('mediago_url', MEDIAGO_URL); } }; document.getElementById('theme-toggle').onclick = () => { theme = (theme === 'dark' ? 'light' : 'dark'); GM_setValue('theme', theme); panel.className = theme; }; panel.querySelectorAll('input[name="target"]').forEach(r => { r.onchange = e => { target = e.target.value; GM_setValue('target', target); document.querySelectorAll('.single-send').forEach(b => b.innerText = (target==='nas'?'投喂 docker':'投喂本地')); }; }); panel.querySelectorAll('input[name="mode"]').forEach(r => { r.onchange = e => { mode = e.target.value; GM_setValue('mode', mode); }; }); panel.querySelectorAll('input[name="folder"]').forEach(r => { r.onchange = e => { folderType = e.target.value; GM_setValue('folder_type', folderType); }; }); document.getElementById('sel-all').onclick = () => { const cbs = panel.querySelectorAll('.checkbox'); const all = Array.from(cbs).every(c => c.checked); cbs.forEach(c => c.checked = !all); }; document.getElementById('batch-btn').onclick = () => { let urls = Array.from(panel.querySelectorAll('.checkbox:checked')).map(c => c.dataset.url); if(urls.length) { const prefix = prompt(`批量投喂 ${urls.length} 个任务,前缀:`, document.title); if(prefix !== null) { urls.forEach((u, i) => setTimeout(() => sendTask(u, null, `${prefix}_批量${i+1}`), i * 500)); } } }; } })();