// ==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 = `
`;
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(); }
})();