// ==UserScript== // @name 视频提取器(一键下载版) // @namespace http://tampermonkey.net/ // @version 1.2 // @description 提取网页视频链接,支持直链下载,m3u8/blob给予提示 // @match *://*/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; const STYLE = ` #vh-float-btn { position: fixed; bottom: 20px; right: 20px; width: 48px; height: 48px; background-color: #ff5722; border-radius: 50%; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: pointer; z-index: 999999; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; user-select: none; } #vh-panel { position: fixed; bottom: 80px; right: 20px; width: 380px; max-height: 70vh; background: rgba(30,30,40,0.95); backdrop-filter: blur(8px); border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.4); z-index: 999998; display: none; flex-direction: column; font-family: system-ui, sans-serif; border: 1px solid rgba(255,255,255,0.2); color: #f0f0f0; overflow: hidden; } #vh-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: rgba(0,0,0,0.5); cursor: move; border-bottom: 1px solid rgba(255,255,255,0.1); } #vh-content { flex: 1; overflow-y: auto; padding: 12px; font-size: 13px; } .vh-video-item { background: rgba(0,0,0,0.4); border-radius: 8px; padding: 10px; margin-bottom: 10px; border-left: 3px solid #ff5722; word-break: break-all; } .vh-video-url { font-family: monospace; font-size: 12px; background: #1e1e2a; padding: 6px; border-radius: 6px; margin: 6px 0; color: #9cdcfe; } .vh-buttons { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; } .vh-btn { background: #3a3a4a; border: none; color: white; padding: 4px 12px; border-radius: 20px; cursor: pointer; font-size: 12px; } .vh-btn-copy { background: #2c7da0; } .vh-btn-open { background: #2e6b3e; } .vh-btn-download { background: #9b59b6; } .vh-empty { text-align: center; padding: 30px 10px; color: #aaa; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ff5722; border-radius: 4px; } `; let floatBtn, panel, isPanelVisible = false; let isDragging = false, dragX, dragY; let refreshTimer = null; let observer = null; function toAbsoluteUrl(url) { if (!url) return ''; if (/^https?:\/\/|blob:|data:/i.test(url)) return url; try { return new URL(url, location.href).href; } catch(e) { return url; } } function extractSrcFromVideo(video) { const urls = new Set(); if (video.src) urls.add(toAbsoluteUrl(video.src)); if (video.currentSrc && video.currentSrc !== video.src) urls.add(toAbsoluteUrl(video.currentSrc)); video.querySelectorAll('source').forEach(src => { if (src.src) urls.add(toAbsoluteUrl(src.src)); }); ['data-src', 'data-video-url', 'data-url'].forEach(attr => { let val = video.getAttribute(attr); if (val) urls.add(toAbsoluteUrl(val)); }); return Array.from(urls); } function getAllVideoUrls() { const videos = document.querySelectorAll('video'); const urlMap = new Map(); for (let video of videos) { let urls = extractSrcFromVideo(video); for (let url of urls) { if (url && !urlMap.has(url)) { urlMap.set(url, video); } if (urlMap.size > 50) break; } } if (urlMap.size < 30) { document.querySelectorAll('a[href$=".mp4"], a[href$=".webm"], a[href$=".m3u8"], a[href$=".mov"]').forEach(link => { let href = link.href; if (href && !urlMap.has(href)) urlMap.set(href, link); }); } return Array.from(urlMap.keys()).map(url => ({ url, element: urlMap.get(url) })); } // 判断链接类型 function getUrlType(url) { if (url.startsWith('blob:')) return 'blob'; if (url.includes('.m3u8')) return 'm3u8'; if (/\.(mp4|webm|mov|mkv|avi|flv)$/i.test(url)) return 'direct'; return 'unknown'; } // 下载直链 function downloadDirect(url) { const a = document.createElement('a'); a.href = url; a.download = ''; // 触发下载,文件名从URL提取 a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); toast('⬇️ 开始下载'); } function handleDownload(url) { const type = getUrlType(url); if (type === 'direct') { downloadDirect(url); } else if (type === 'm3u8') { toast('⚠️ 这是m3u8流媒体,无法直接下载。请使用 N_m3u8DL-CLI 或 VLC 等工具', 5000); } else if (type === 'blob') { toast('❌ Blob链接无法直接下载,请按F12 → 网络 → 筛选Media → 找到真实地址', 6000); } else { toast('❓ 未知类型,请尝试复制链接后手动处理', 4000); } } let rendering = false; function renderVideoList() { if (!isPanelVisible || rendering) return; rendering = true; try { const contentDiv = document.getElementById('vh-content'); if (!contentDiv) return; const videos = getAllVideoUrls(); if (videos.length === 0) { contentDiv.innerHTML = '
🎬 未检测到视频源
试试播放视频后点刷新
'; return; } let html = ''; videos.forEach((item, idx) => { let url = item.url; let typeLabel = ''; const t = getUrlType(url); if (t === 'direct') typeLabel = '📹 直链'; else if (t === 'm3u8') typeLabel = '🎬 HLS流'; else if (t === 'blob') typeLabel = '🧩 Blob对象'; else typeLabel = '🔗 未知'; let short = url.length > 70 ? url.slice(0,67)+'...' : url; html += `
${typeLabel} ${idx+1}
${escapeHtml(short)}
`; }); contentDiv.innerHTML = html; contentDiv.querySelectorAll('.vh-btn-copy').forEach(btn => { btn.onclick = () => copyToClipboard(btn.getAttribute('data-url')); }); contentDiv.querySelectorAll('.vh-btn-open').forEach(btn => { btn.onclick = () => window.open(btn.getAttribute('data-url'), '_blank'); }); contentDiv.querySelectorAll('.vh-btn-download').forEach(btn => { btn.onclick = () => handleDownload(btn.getAttribute('data-url')); }); } finally { rendering = false; } } function escapeHtml(str) { return str.replace(/[&<>]/g, m => ({'&':'&','<':'<','>':'>'}[m])); } function escapeAttr(str) { return str.replace(/&/g, '&').replace(/"/g, '"'); } function copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => toast('✅ 已复制')).catch(() => fallbackCopy(text)); } else fallbackCopy(text); } function fallbackCopy(text) { let ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); toast('📋 已复制'); } let toastEl, toastTimer; function toast(msg, duration = 2000) { if (!toastEl) { toastEl = document.createElement('div'); toastEl.style.cssText = 'position:fixed;bottom:80px;right:80px;background:#333;color:#fff;padding:6px 12px;border-radius:20px;z-index:1000000;font-size:13px;opacity:0;transition:0.2s;max-width:300px;text-align:center'; document.body.appendChild(toastEl); } toastEl.textContent = msg; toastEl.style.opacity = '1'; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.style.opacity = '0', duration); } function scheduleRefresh() { if (!isPanelVisible) return; if (refreshTimer) clearTimeout(refreshTimer); refreshTimer = setTimeout(() => { renderVideoList(); refreshTimer = null; }, 300); } function createUI() { if (document.getElementById('vh-float-btn')) return; const style = document.createElement('style'); style.textContent = STYLE; document.head.appendChild(style); floatBtn = document.createElement('div'); floatBtn.id = 'vh-float-btn'; floatBtn.textContent = '🎬'; document.body.appendChild(floatBtn); panel = document.createElement('div'); panel.id = 'vh-panel'; panel.innerHTML = `
🎥 视频提取·下载 ${location.hostname}
✨ 点击刷新或播放视频
`; document.body.appendChild(panel); document.getElementById('vh-refresh')?.addEventListener('click', () => { renderVideoList(); toast('🔄 已刷新'); }); document.getElementById('vh-close')?.addEventListener('click', () => togglePanel(false)); const header = document.getElementById('vh-header'); header.addEventListener('mousedown', (e) => { if (e.target.closest('button')) return; isDragging = true; const rect = panel.getBoundingClientRect(); dragX = e.clientX - rect.left; dragY = e.clientY - rect.top; panel.style.transition = 'none'; }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; let left = e.clientX - dragX, top = e.clientY - dragY; left = Math.min(window.innerWidth - panel.offsetWidth - 10, Math.max(10, left)); top = Math.min(window.innerHeight - panel.offsetHeight - 10, Math.max(10, top)); panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); window.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = ''; }); floatBtn.onclick = () => togglePanel(!isPanelVisible); } function togglePanel(show) { if (!panel) return; if (show) { panel.style.display = 'flex'; isPanelVisible = true; if (!panel.style.left && !panel.style.right) { panel.style.right = '20px'; panel.style.bottom = '80px'; panel.style.left = 'auto'; } renderVideoList(); } else { panel.style.display = 'none'; isPanelVisible = false; if (refreshTimer) clearTimeout(refreshTimer); } } function startSafeObserver() { if (observer) observer.disconnect(); observer = new MutationObserver((mutations) => { let shouldSchedule = false; for (let mut of mutations) { if (panel && (mut.target === panel || panel.contains(mut.target))) continue; if (mut.type === 'attributes' && (mut.attributeName === 'src' || mut.attributeName === 'data-src')) { shouldSchedule = true; break; } if (mut.type === 'childList') { for (let node of mut.addedNodes) { if (node.nodeType === 1 && (node.matches?.('video') || node.querySelector?.('video'))) { shouldSchedule = true; break; } } } if (shouldSchedule) break; } if (shouldSchedule) scheduleRefresh(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'data-src'] }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => { createUI(); startSafeObserver(); }); else { createUI(); startSafeObserver(); } })();