Greasy Fork

Greasy Fork is available in English.

M3U8/bilibili 视频嗅探下载 + MediaGo 投喂器

强力视频嗅探与投喂:自动抓取 M3U8 及 B 站资源,一键投喂至 MediaGo(支持 Docker 与本地版)全画质彩色标注,支持域名自动归类文件夹、智能路径去重及防重名命名系统。保留原始长链接,提供动态投喂状态反馈,让视频资源整理从此整整齐齐。

当前为 2025-12-25 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         M3U8/bilibili 视频嗅探下载 + MediaGo 投喂器
// @version      2.5.6
// @description  强力视频嗅探与投喂:自动抓取 M3U8 及 B 站资源,一键投喂至 MediaGo(支持 Docker 与本地版)全画质彩色标注,支持域名自动归类文件夹、智能路径去重及防重名命名系统。保留原始长链接,提供动态投喂状态反馈,让视频资源整理从此整整齐齐。
// @author       zhecydn
// @match        *://*/*
// @allframes    true
// @run-at       document-start
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @namespace https://blog.zhecydn.asia/
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. 变量与内存金库 (地基 2.4.6) ---
    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 isMinimized = GM_getValue('is_minimized', false);
    let savedPos = GM_getValue('panel_pos', { top: '20px', left: 'auto', right: '20px' });

    let detectedUrls = new Set();
    let memoryVault = [];
    let panel = null;
    let gearIcon = null;

    const isBiliPage = location.hostname.includes('bilibili.com');

    // --- 2. 增强后台逻辑:过滤、去重与勋章 ---
    function getResTag(u) {
        u=u.toLowerCase();
        if(u.includes('8k') || u.includes('4320')) return '<span style="color:#ffa502;font-weight:bold;">[👑 8K]</span> ';
        if(u.includes('4k') || u.includes('2160')) return '<span style="color:#ff4757;font-weight:bold;">[💎 4K]</span> ';
        if(u.includes('1080') || u.includes('1920')) return '<span style="color:#e67e22;font-weight:bold;">[🔥 1080P]</span> ';
        if(u.includes('720')) return '<span style="color:#2ed573;font-weight:bold;">[🎬 720P]</span> ';
        if(u.includes('480')) return '<span style="color:#2980b9;font-weight:bold;">[📺 480P]</span> ';
        return '';
    }

    function addUrl(url, customTitle = null, isBiliBatch = false) {
        if (typeof url !== 'string') return;

        // 【智能引擎增强】:路径指纹去重(忽略随机参数)
        const fingerprint = url.split('?')[0];
        if (detectedUrls.has(fingerprint)) return;

        if (!isBiliBatch && !/\.m3u8(\?|$)/i.test(url)) return;

        // 【智能引擎增强】:过滤无效碎片(通常分片链接长且含 fragment 字样)
        if (!isBiliBatch && url.includes('fragment') && (url.includes('.ts') || url.includes('seg-'))) return;

        if (window.self !== window.top) {
            window.top.postMessage({ type: 'VIDEO_MSG_V256', url, customTitle, isBiliBatch }, '*');
            return;
        }

        detectedUrls.add(fingerprint);
        memoryVault.push({ url, customTitle, isBiliBatch });

        if (!panel && !isMinimized) createPanel();
        if (panel) renderSingleItem({ url, customTitle, isBiliBatch });
    }

    function renderSingleItem(item) {
        const list = document.getElementById('m3u8-list');
        if (!list) return;
        const li = document.createElement('li');
        li.className = 'm3u8-item';
        let tag = item.isBiliBatch ? '<span style="color:#fb7299;font-weight:bold;">[🎬 选集]</span> ' : getResTag(item.url);

        // 【核心地基】长链接显示逻辑
        const name = item.customTitle ? `${tag}${item.customTitle}` : `${tag}${item.url.split('?')[0].substring(0, 55)}...`;

        li.innerHTML = `<input type="checkbox" class="checkbox" data-url="${item.url}" data-title="${item.customTitle || ''}"><div class="url-content"><div class="url-text" title="${item.url}">${name}</div><button class="single-send">${target==='nas'?'投喂docker':'投喂本地'}</button></div>`;
        li.onclick = (e) => { if (e.target.tagName !== 'BUTTON') { const cb = li.querySelector('.checkbox'); cb.checked = !cb.checked; li.classList.toggle('selected', cb.checked); isBiliPage ? updateBiliBtnText() : updateBatchBtnText(); } };
        list.prepend(li);
        li.querySelector('.single-send').onclick = (e) => { e.stopPropagation(); sendTask(item.url, e.target, item.customTitle, item.isBiliBatch); };
    }

    // --- 3. UI 物理隔离切换 (100% 2.4.6 原始 CSS 与布局) ---
    function createPanel() {
        if (document.getElementById('mediago-panel')) return;
        if (gearIcon) { gearIcon.remove(); gearIcon = null; }

        panel = document.createElement('div');
        panel.id = 'mediago-panel';
        panel.className = theme;
        applyPos(panel);
        panel.innerHTML = `
            <div id="p-header"><span id="min-btn" style="cursor:pointer;margin-right:8px;">➖</span>🔍 m3u8资源嗅探器 <span id="theme-toggle" style="float:right;cursor:pointer;margin-left:12px;">🌓</span><span id="set-btn" style="float:right;cursor:pointer;">⚙️</span></div>
            <div class="top-bar">
                <button id="sel-all">全选</button>
                ${isBiliPage ? '<button id="scan-bili" style="background:#e67e22 !important;">🔍 扫描可见选集</button><button id="bili-main-btn" style="background:#fb7299 !important;">🚀 投喂b站直链</button>' : '<button id="batch-btn" style="background:#fd7e14 !important;">🚀 一键投喂</button>'}
            </div>
            <ul id="m3u8-list"></ul>
            <div id="p-footer">
                <div class="ctrl-row">目标: <label><input type="radio" name="target" value="nas" ${target==='nas'?'checked':''}> docker</label> <label><input type="radio" name="target" value="local" ${target==='local'?'checked':''}> 本地</label> <span style="margin:0 5px;opacity:0.3">|</span> 模式: <label><input type="radio" name="mode" value="api" ${mode==='api'?'checked':''}> API</label> <label><input type="radio" name="mode" value="url" ${mode==='url'?'checked':''}> URL</label></div>
                <div class="ctrl-row">归类: <label><input type="radio" name="folder" value="domain" ${folderType==='domain'?'checked':''}> 域名文件夹</label> <label><input type="radio" name="folder" value="default" ${folderType==='default'?'checked':''}> 默认根目录</label></div>
                <div class="tutorial-box"><a href="https://blog.zhecydn.asia/archives/1962" target="_blank" class="mg-blog-link" style="font-size:12px !important;">📖 脚本使用教程</a></div>
            </div>`;
        (document.body || document.documentElement).appendChild(panel);
        memoryVault.forEach(item => renderSingleItem(item));
        setupEvents(panel);
    }

    function createGear() {
        if (document.getElementById('mediago-gear')) return;
        if (panel) { panel.remove(); panel = null; }
        gearIcon = document.createElement('div');
        gearIcon.id = 'mediago-gear';
        gearIcon.innerHTML = '⚙️';
        applyPos(gearIcon);
        (document.body || document.documentElement).appendChild(gearIcon);
        setupEvents(gearIcon);
    }

    function toggleMin(toMin) {
        isMinimized = toMin;
        GM_setValue('is_minimized', isMinimized);
        if (isMinimized) createGear(); else createPanel();
    }

    // --- 4. 投喂逻辑 (稳健算法增强 + 动态反馈) ---
    function sendTask(url, btn, customName = null, forceBili = false) {
        const isBili = forceBili || url.includes('bilibili.com');
        const finalType = isBili ? 'bilibili' : 'm3u8';

        // 【内功增强】:稳健命名算法,彻底杜绝 NAS 覆盖
        let base = (customName || document.title).replace(/[\\/:\*\?"<>\|]/g, "_").trim();
        if(!counter[base]) counter[base] = 0; counter[base]++;
        GM_setValue('counter', counter);
        const finalName = `${base.substring(0,30)}_${counter[base]}_${new Date().getTime().toString().slice(-4)}`;

        const folder = folderType === 'domain' ? location.hostname.split('.')[0] : '';
        const encodedName = encodeURIComponent(finalName), encodedUrl = encodeURIComponent(url);
        const folderParam = folder ? `&folder=${encodeURIComponent(folder)}` : '';

        // 【反馈增强】:三态动态显示
        if (btn) {
            btn.innerText = "⏳ 投喂中...";
            btn.style.background = "#f1c40f"; // 黄色反馈
            btn.style.pointerEvents = "none";
        }

        const successAction = () => {
            if (btn) {
                btn.innerText = "✅ 已成功投喂";
                btn.style.background = "#27ae60"; // 绿色反馈
                btn.style.opacity = "0.7";
                setTimeout(() => {
                    btn.style.pointerEvents = "auto";
                    btn.style.background = "";
                    if(btn.id === 'bili-main-btn') updateBiliBtnText();
                    else if(btn.className.includes('single-send')) btn.innerText = target==='nas'?'投喂docker':'投喂本地';
                    else if(btn.id === 'batch-btn') updateBatchBtnText();
                }, 2000);
            }
        };

        if (target === 'local') {
            window.open(`mediago://index.html/?n=true&name=${encodedName}&url=${encodedUrl}&type=${finalType}&silent=true${folderParam}`, '_blank');
            successAction();
        } 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: finalType, folder: folder }),
                    onload: () => successAction(),
                    onerror: () => { if(btn) { btn.innerText = "❌ 失败"; btn.style.background = "#e74c3c"; btn.style.pointerEvents="auto"; } }
                });
            } else {
                window.open(`${MEDIAGO_URL}/?n=true&name=${encodedName}&url=${encodedUrl}&type=${finalType}&silent=true${folderParam}`, '_blank');
                successAction();
            }
        }
    }

    // --- 5. 其余辅助逻辑 ---
    function scanBili() {
        let count = 0;
        document.querySelectorAll('.imageListItem_wrap__o28QW, .video-pod__item').forEach(el => {
            const bv = el.getAttribute('data-key');
            const a = el.querySelector('a');
            if (bv) { addUrl(`https://www.bilibili.com/video/${bv}`, el.querySelector('.title')?.innerText.trim(), true); count++; }
            else if (a) { addUrl(new URL(a.getAttribute('href'), location.href).href, el.querySelector('.imageListItem_titleWrap__YTlLH')?.getAttribute('title'), true); count++; }
        });
        if (count > 0) updateBiliBtnText();
        else alert("雷达空空如也...这好像是个单集视频哦,直接投喂即可!🧐");
    }

    function applyPos(el) { el.style.top = savedPos.top; el.style.left = savedPos.left; el.style.right = savedPos.right; }
    function updateBiliBtnText() { const btn=document.getElementById('bili-main-btn'); if(btn){ const n=panel.querySelectorAll('.checkbox:checked').length; btn.innerText=n>0?`🚀 投喂 ${n} 个` : `🚀 投喂b站直链`; } }
    function updateBatchBtnText() { const btn=document.getElementById('batch-btn'); if(btn){ const n=panel.querySelectorAll('.checkbox:checked').length; btn.innerText=n>0?`🚀 投喂 ${n} 个` : `🚀 一键投喂`; } }

    function setupEvents(el) {
        if (el.id === 'mediago-panel') {
            document.getElementById('min-btn').onclick = () => toggleMin(true);
            document.getElementById('theme-toggle').onclick = () => { theme=(theme==='dark'?'light':'dark'); GM_setValue('theme', theme); panel.className=theme; };
            document.getElementById('set-btn').onclick = () => { let u=prompt('mediago docker地址:', MEDIAGO_URL); if(u){ MEDIAGO_URL=u.trim().replace(/\/+$/, ''); GM_setValue('mediago_url', MEDIAGO_URL); } };
            if(isBiliPage) {
                document.getElementById('scan-bili').onclick = scanBili;
                document.getElementById('bili-main-btn').onclick = function() {
                    const checked = panel.querySelectorAll('.checkbox:checked');
                    if(checked.length) checked.forEach((cb, i) => setTimeout(() => sendTask(cb.dataset.url, this, cb.dataset.title, true), i*1000));
                    else sendTask(location.href.split('?')[0], this, document.title.split('_')[0]);
                };
            } else {
                document.getElementById('batch-btn').onclick = function() {
                    const checked = panel.querySelectorAll('.checkbox:checked');
                    if(checked.length) {
                        const p = prompt(`🚀 一键投喂:`, document.title);
                        if(p) checked.forEach((cb, i) => setTimeout(() => sendTask(cb.dataset.url, this, `${p}_${i+1}`), i*800));
                    }
                };
            }
            document.getElementById('sel-all').onclick = () => { const cbs=panel.querySelectorAll('.checkbox'), all=Array.from(cbs).every(c=>c.checked); cbs.forEach(c=>{ c.checked=!all; c.closest('.m3u8-item').classList.toggle('selected', !all); }); isBiliPage?updateBiliBtnText():updateBatchBtnText(); };
            panel.querySelectorAll('input[name="target"]').forEach(r => r.onchange = e => { target=e.target.value; GM_setValue('target', target); updateBtnLabels(); });
            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); });
        } else { el.onclick = () => { if(el.dataset.dragged!=='true') toggleMin(false); }; }

        let isDrag = false, ox, oy;
        const dragHeader = el.id==='mediago-panel'?document.getElementById('p-header'):el;
        dragHeader.onmousedown = e => { if(e.target.tagName==='SPAN') return; isDrag=true; el.dataset.dragged='false'; ox=e.clientX-el.offsetLeft; oy=e.clientY-el.offsetTop; };
        document.onmousemove = e => { if(isDrag){ el.dataset.dragged='true'; let nx=(e.clientX-ox)+'px', ny=(e.clientY-oy)+'px'; el.style.left=nx; el.style.top=ny; el.style.right='auto'; savedPos={top:ny, left:nx, right:'auto'}; }};
        document.onmouseup = () => { if(isDrag){ isDrag=false; GM_setValue('panel_pos', savedPos); }};
    }

    function updateBtnLabels() { document.querySelectorAll('.single-send').forEach(b => b.innerText = target==='nas'?'投喂docker':'投喂本地'); }

    GM_addStyle(`
        #mediago-panel { position: fixed !important; width: 380px !important; z-index: 2147483647 !important; border-radius: 12px !important; box-shadow: 0 10px 40px rgba(0,0,0,0.5) !important; display: flex !important; flex-direction: column !important; padding: 10px !important; font-family: sans-serif !important; border: 1px solid rgba(128,128,128,0.3) !important; font-size: 13px !important; }
        #mediago-panel.dark { background: rgba(30,30,30,0.95) !important; color: #fff !important; }
        #mediago-panel.light { background: rgba(255,255,255,0.98) !important; color: #111 !important; }
        #mediago-gear { position: fixed !important; width: 42px !important; height: 42px !important; background: rgba(30,30,30,0.9) !important; color: #fb7299 !important; border-radius: 50% !important; z-index: 2147483647 !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; font-size: 24px !important; box-shadow: 0 4px 15px rgba(0,0,0,0.4) !important; border: 1px solid rgba(251,114,153,0.4) !important; }
        #p-header { cursor: move !important; padding: 8px !important; background: rgba(128,128,128,0.2) !important; border-radius: 8px !important; font-weight: bold !important; font-size: 13px !important; margin-bottom: 6px !important; }
        .top-bar { display: flex !important; gap: 4px !important; margin-bottom: 8px !important; }
        .top-bar button { flex: 1 !important; padding: 6px 2px !important; border: none !important; border-radius: 6px !important; cursor: pointer !important; font-size: 11px !important; font-weight: bold !important; color: #fff !important; background: #555 !important; }
        #m3u8-list { list-style: none !important; padding: 0 !important; margin: 0 !important; overflow-y: auto !important; flex: 1 !important; max-height: 350px !important; }
        .m3u8-item { display: flex !important; align-items: center !important; padding: 8px !important; background: rgba(128,128,128,0.1) !important; margin-bottom: 4px !important; border-radius: 8px !important; cursor: pointer !important; border-left: 4px solid #a55eea !important; transition: 0.2s !important; }
        .m3u8-item.selected { background: rgba(165, 94, 234, 0.15) !important; border-left-color: #00aeec !important; }
        .checkbox { margin-right: 8px !important; width: 15px !important; height: 15px !important; }
        .url-text { font-size: 12px !important; word-break: break-all !important; line-height: 1.3 !important; }
        .single-send { width: 100% !important; background: #27ae60 !important; border: none !important; color: #fff !important; padding: 4px !important; border-radius: 5px !important; cursor: pointer !important; font-size: 11px !important; font-weight: bold !important; margin-top: 4px !important; transition: 0.2s !important; }
        #p-footer { border-top: 1px solid rgba(128,128,128,0.2) !important; padding-top: 8px !important; }
        .ctrl-row { display: flex !important; justify-content: center !important; align-items: center !important; gap: 6px !important; margin-bottom: 4px !important; font-size: 11px !important; }
        .tutorial-box { text-align: center !important; margin-top: 6px !important; padding-top: 4px !important; border-top: 1px dashed rgba(128,128,128,0.3) !important; }
        .mg-blog-link { color: #a55eea !important; text-decoration: none !important; font-size: 12px !important; font-weight: bold !important; }
    `);

    const oX = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(m, u) { try { addUrl(new URL(u, location.href).href); } catch(e){} return oX.apply(this, arguments); };
    const oF = window.fetch; window.fetch = function(r) { let u = typeof r === 'string' ? r : (r && r.url); if(u){ try { addUrl(new URL(u, location.href).href); } catch(e){} } return oF.apply(this, arguments); };
    if (isMinimized) createGear(); else createPanel();
    window.addEventListener('message', e => { if (e.data && e.data.type === 'VIDEO_MSG_V256') addUrl(e.data.url, e.data.customTitle, e.data.isBiliBatch); });
})();