// ==UserScript==
// @name 豆包视频&图片下载助手
// @namespace https://www.doubao.com/
// @version 5.0.0
// @description 自动提取豆包(Doubao)网站中的视频与图片链接,支持视频/图片预览、一键复制、批量下载,带可拖拽悬浮按钮和红点提醒
// @author Doubao Downloader
// @match https://www.doubao.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @run-at document-end
// @noframes
// @supportURL https://github.com
// @homepageURL https://github.com
// @license MIT
//
// 免责声明:本脚本仅供学习和参考,切勿用于违法或商用,否则后果自负。
//
// ==/UserScript==
(function () {
'use strict';
const videoLinks = new Map();
const imageLinks = new Map();
const processedVideoSrcs = new Set();
const processedImageSrcs = new Set();
let activeTab = 'video';
GM_addStyle(`
#db-dl-btn, #db-dl-panel {
--bg: #ffffff;
--bg-sub: #f8fafc;
--bg-elevated: #f1f5f9;
--bg-hover: #f1f5f9;
--border: #e2e8f0;
--border-strong: #cbd5e1;
--text: #0f172a;
--text-sub: #475569;
--text-muted: #94a3b8;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-soft: #eff6ff;
--danger: #ef4444;
--danger-soft: #fef2f2;
--danger-hover: #dc2626;
--success: #10b981;
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-lg: -8px 0 32px rgba(15, 23, 42, 0.12);
}
#db-dl-panel, #db-dl-btn, #db-dl-toast {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#db-dl-btn {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483646;
width: 48px;
height: 48px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
cursor: move;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid var(--border);
outline: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
#db-dl-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25); }
#db-dl-btn:active { transform: translateY(0); cursor: grabbing; }
#db-dl-btn img { width: 32px; height: 32px; pointer-events: none; border-radius: 6px; }
#db-dl-btn .badge {
position: absolute;
top: -4px;
right: -4px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #ef4444;
box-shadow: 0 0 0 2px #ffffff, 0 2px 8px rgba(239, 68, 68, 0.5);
pointer-events: none;
transition: transform 0.2s ease;
z-index: 10;
}
#db-dl-btn .badge.hidden { display: none; }
#db-dl-btn .badge.pulse { animation: db-badge-pulse 1.6s ease-in-out infinite; }
@keyframes db-badge-pulse {
0%,100% { box-shadow: 0 0 0 2px #ffffff, 0 2px 6px rgba(239, 68, 68, 0.4); transform: scale(1); }
50% { box-shadow: 0 0 0 2px #ffffff, 0 0 0 8px rgba(239, 68, 68, 0.15), 0 2px 8px rgba(239, 68, 68, 0.5); transform: scale(1.05); }
}
#db-dl-overlay {
position: fixed;
inset: 0;
z-index: 2147483647;
background: rgba(15, 23, 42, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
#db-dl-overlay.show { opacity: 1; pointer-events: auto; }
#db-dl-panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 480px;
max-width: 92vw;
z-index: 2147483648;
background: var(--bg);
color: var(--text);
box-shadow: var(--shadow-lg);
transform: translateX(105%);
transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
display: flex;
flex-direction: column;
font-size: 14px;
}
#db-dl-panel.open { transform: translateX(0); }
.db-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 16px 0 20px;
flex-shrink: 0;
background: var(--bg);
}
.db-panel-header .title {
font-size: 16px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
letter-spacing: -0.01em;
flex: 1;
min-width: 0;
}
.db-panel-header .title svg { width: 22px; height: 22px; flex-shrink: 0; border-radius: 5px; }
.db-panel-header .title-text { flex-shrink: 0; }
.db-panel-header .count-tag {
background: var(--primary);
color: #ffffff;
font-size: 11px;
font-weight: 700;
padding: 3px 9px;
border-radius: 10px;
min-width: 24px;
text-align: center;
flex-shrink: 0;
}
.db-panel-header .close-btn {
width: 30px;
height: 30px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-sub);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
line-height: 1;
font-size: 18px;
flex-shrink: 0;
}
.db-panel-header .close-btn:hover { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
.db-panel-tabs {
display: flex;
gap: 4px;
padding: 12px 20px 0;
flex-shrink: 0;
background: var(--bg);
}
.db-panel-tabs button {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-sub);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
border-radius: 8px 8px 0 0;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-bottom: 2px solid transparent;
}
.db-panel-tabs button svg { width: 14px; height: 14px; fill: currentColor; }
.db-panel-tabs button:hover { color: var(--text); background: var(--bg-hover); }
.db-panel-tabs button.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.db-panel-tabs .tab-count {
background: var(--bg-elevated);
color: var(--text-sub);
font-size: 10px;
font-weight: 700;
padding: 1px 6px;
border-radius: 8px;
min-width: 18px;
text-align: center;
}
.db-panel-tabs button.active .tab-count {
background: var(--primary-soft);
color: var(--primary);
}
.db-panel-toolbar {
display: flex;
gap: 6px;
padding: 10px 16px;
flex-shrink: 0;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.db-panel-toolbar button {
flex: 1;
padding: 7px 0;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-sub);
font-size: 12px;
cursor: pointer;
transition: all 0.15s ease;
font-weight: 500;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.db-panel-toolbar button svg { width: 12px; height: 12px; fill: currentColor; }
.db-panel-toolbar button:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary); }
.db-panel-toolbar button.primary {
background: var(--primary);
color: #fff;
border-color: var(--primary);
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.25);
}
.db-panel-toolbar button.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: #fff; }
.db-panel-list {
flex: 1;
overflow-y: auto;
padding: 12px;
overscroll-behavior: contain;
background: var(--bg-sub);
}
.db-panel-list::-webkit-scrollbar { width: 6px; }
.db-panel-list::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
.db-panel-list::-webkit-scrollbar-track { background: transparent; }
.db-disclaimer {
flex-shrink: 0;
padding: 10px 16px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-muted);
text-align: center;
line-height: 1.5;
background: var(--bg);
}
.db-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
padding: 40px 20px;
gap: 6px;
}
.db-empty-state .icon-wrap {
width: 64px;
height: 64px;
border-radius: 16px;
background: var(--bg);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
}
.db-empty-state .icon-wrap svg { width: 28px; height: 28px; fill: var(--primary); opacity: 0.5; }
.db-empty-state .hint { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.db-video-card, .db-image-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin-bottom: 10px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
animation: db-card-in 0.3s ease;
position: relative;
}
@keyframes db-card-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes db-card-out {
to { opacity: 0; transform: translateX(40px); height: 0; padding: 0; margin: 0; border-width: 0; }
}
.db-video-card:hover, .db-image-card:hover { border-color: var(--primary); box-shadow: 0 2px 12px rgba(37, 99, 235, 0.08); }
.db-card-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.db-thumb {
width: 80px;
height: 60px;
border-radius: 6px;
background: var(--bg-elevated);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 1px solid var(--border);
}
.db-thumb.video { height: 45px; }
.db-thumb img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.db-thumb svg { width: 22px; height: 22px; fill: var(--primary); opacity: 0.5; }
.db-card-info { flex: 1; min-width: 0; }
.db-card-info .meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
}
.db-card-info .meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border-strong); }
.db-card-info .meta .badge-mini {
background: var(--primary-soft);
color: var(--primary);
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 4px;
}
.db-card-info .filename {
font-size: 12px;
color: var(--text);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
line-height: 1.4;
}
.db-delete-btn {
position: absolute;
top: 8px;
right: 8px;
width: 22px;
height: 22px;
border-radius: 5px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.15s ease;
}
.db-video-card:hover .db-delete-btn, .db-image-card:hover .db-delete-btn { opacity: 1; }
.db-delete-btn:hover { background: var(--danger-soft); color: var(--danger); }
.db-delete-btn svg { width: 12px; height: 12px; fill: currentColor; }
.db-card-actions { display: flex; gap: 6px; }
.db-card-actions button {
flex: 1;
padding: 7px 0;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-sub);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-family: inherit;
}
.db-card-actions button svg { width: 13px; height: 13px; fill: currentColor; }
.db-card-actions button.copy-btn:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary); }
.db-card-actions button.dl-btn {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
.db-card-actions button.dl-btn:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.db-card-actions button.copied {
background: var(--success) !important;
border-color: var(--success) !important;
color: #fff !important;
}
#db-dl-toast {
position: fixed;
bottom: 88px;
left: 50%;
transform: translateX(-50%) translateY(8px);
z-index: 2147483649;
background: var(--text);
color: var(--bg);
padding: 9px 18px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
white-space: nowrap;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.2);
}
#db-dl-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#db-preview-modal {
position: fixed;
inset: 0;
z-index: 2147483650;
background: rgba(0, 0, 0, 0.85);
display: none;
align-items: center;
justify-content: center;
padding: 32px;
backdrop-filter: blur(8px);
}
#db-preview-modal.show { display: flex; animation: db-modal-in 0.25s ease; }
@keyframes db-modal-in {
from { opacity: 0; }
to { opacity: 1; }
}
#db-preview-modal .preview-content {
position: relative;
max-width: 92vw;
max-height: 92vh;
display: flex;
align-items: center;
justify-content: center;
}
#db-preview-modal img,
#db-preview-modal video {
max-width: 92vw;
max-height: 88vh;
border-radius: 8px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
background: #000;
display: block;
}
#db-preview-modal .preview-close {
position: fixed;
top: 20px;
right: 24px;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.15);
color: #fff;
font-size: 22px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
z-index: 10;
line-height: 1;
}
#db-preview-modal .preview-close:hover { background: rgba(255, 255, 255, 0.3); }
#db-preview-modal .preview-info {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-size: 12px;
max-width: 80vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
}
#db-preview-modal video {
outline: none;
}
.db-thumb { cursor: pointer; position: relative; }
.db-thumb::after {
content: '预览';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0);
letter-spacing: 0.5px;
transition: all 0.15s ease;
pointer-events: none;
border-radius: 6px;
}
.db-video-card:hover .db-thumb::after,
.db-image-card:hover .db-thumb::after {
background: rgba(15, 23, 42, 0.55);
color: #fff;
}
`);
function showToast(msg, duration) {
let toast = document.getElementById('db-dl-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'db-dl-toast';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toast._timer);
toast._timer = setTimeout(() => toast.classList.remove('show'), duration || 1800);
}
function normalizeKey(url) {
try {
const u = new URL(url);
return u.origin + u.pathname;
} catch (e) {
return url.split('?')[0].split('#')[0];
}
}
function isVideoUrl(url) {
if (!url) return false;
if (url.includes('.mp4') || url.includes('.m3u8') || url.includes('.flv') || url.includes('.webm')) return true;
if (url.includes('mime_type=video')) return true;
if (url.includes('/video/') && url.includes('douyin')) return true;
return false;
}
function isImageUrl(url) {
if (!url) return false;
if (/\.(png|jpg|jpeg|webp|gif|bmp|svg)(\?|$|#)/i.test(url)) return true;
if (url.includes('mime_type=image')) return true;
if (url.includes('imagex') || url.includes('rc_gen_image')) return true;
if (url.includes('p1-sign') || url.includes('p3-flow-imagex') || url.includes('byteimg.com')) return true;
return false;
}
function extractVideoSrc(element) {
if (!element) return null;
if (element.tagName === 'VIDEO') {
if (element.src && element.src.startsWith('http')) return element.src;
if (element.currentSrc && element.currentSrc.startsWith('http')) return element.currentSrc;
const source = element.querySelector('source[src]');
if (source && source.src.startsWith('http')) return source.src;
}
if (element.querySelectorAll) {
const innerVid = element.querySelector('video[src]');
if (innerVid && innerVid.src && innerVid.src.startsWith('http')) return innerVid.src;
}
const dataUrl = element.getAttribute && element.getAttribute('data-url');
if (dataUrl && dataUrl.startsWith('http')) return dataUrl;
return null;
}
function extractImageSrc(element) {
if (!element) return null;
if (element.tagName === 'IMG') {
const src = element.src || element.currentSrc;
if (src && src.startsWith('http')) return src;
const dataSrc = element.getAttribute('data-src') || element.getAttribute('data-original');
if (dataSrc && dataSrc.startsWith('http')) return dataSrc;
}
if (element.tagName === 'SOURCE') {
if (element.src && element.src.startsWith('http')) return element.src;
const srcset = element.srcset;
if (srcset) {
const firstUrl = srcset.split(',')[0].trim().split(' ')[0];
if (firstUrl.startsWith('http')) return firstUrl;
}
}
if (element.querySelectorAll) {
const innerImg = element.querySelector('img[src]');
if (innerImg) {
const src = innerImg.src || innerImg.currentSrc;
if (src && src.startsWith('http')) return src;
}
}
const bgImg = element.style && element.style.backgroundImage;
if (bgImg && bgImg.includes('url(')) {
const match = bgImg.match(/url\(["']?(https?:\/\/[^"')]+)["']?\)/);
if (match) return match[1];
}
return null;
}
function getActiveMap() { return activeTab === 'video' ? videoLinks : imageLinks; }
function getActiveProcessed() { return activeTab === 'video' ? processedVideoSrcs : processedImageSrcs; }
function getActiveExtractor() { return activeTab === 'video' ? extractVideoSrc : extractImageSrc; }
function processVideo(el) {
const src = extractVideoSrc(el);
if (!src) return;
const cleanSrc = src.replace(/&/g, '&');
const key = normalizeKey(cleanSrc);
if (processedVideoSrcs.has(key)) return;
processedVideoSrcs.add(key);
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const filename = extractFilename(cleanSrc, '.mp4');
const resolution = extractResolution(cleanSrc);
videoLinks.set(key, { src: cleanSrc, type: 'video', title: filename, timestamp, resolution });
if (activeTab === 'video') addCard(key, 'video');
updateBadge();
}
function processImage(src, sizeKb) {
if (!src || !src.startsWith('http')) return;
if (!isImageUrl(src)) return;
const cleanSrc = src.replace(/&/g, '&');
const key = normalizeKey(cleanSrc);
if (processedImageSrcs.has(key)) return;
processedImageSrcs.add(key);
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const filename = extractFilename(cleanSrc, '.png');
imageLinks.set(key, { src: cleanSrc, type: 'image', title: filename, timestamp, sizeKb });
if (activeTab === 'image') addCard(key, 'image');
updateBadge();
}
function processElement(el) {
const url = extractImageSrc(el);
if (url && isImageUrl(url)) {
processImage(url);
return;
}
const vidUrl = extractVideoSrc(el);
if (vidUrl) processVideo(el);
}
function extractFilename(url, defaultExt) {
try {
const u = new URL(url);
const pathParts = u.pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
const last = pathParts[pathParts.length - 1];
if (last.includes('.')) return last;
return last + defaultExt;
}
} catch (e) {}
return 'file_' + Date.now() + defaultExt;
}
function extractResolution(url) {
const match = url.match(/br=(\d+)/);
if (match) {
const br = parseInt(match[1]);
return br > 1000 ? (br / 1000).toFixed(1) + ' Mbps' : br + ' kbps';
}
return '';
}
function formatSize(kb) {
if (!kb) return '';
if (kb > 1024) return (kb / 1024).toFixed(1) + ' MB';
return kb + ' KB';
}
function scanPage() {
document.querySelectorAll('div[class*="video-canvas-panel-player"]').forEach(processVideo);
document.querySelectorAll('.xgplayer').forEach(processVideo);
document.querySelectorAll('video').forEach(processVideo);
document.querySelectorAll('img[src]').forEach(el => {
const src = el.src || el.currentSrc;
if (src && src.startsWith('http')) processImage(src);
});
document.querySelectorAll('img[data-src]').forEach(el => {
const src = el.getAttribute('data-src');
if (src && src.startsWith('http')) processImage(src);
});
document.querySelectorAll('source[srcset]').forEach(el => {
const srcset = el.srcset;
if (srcset) {
const firstUrl = srcset.split(',')[0].trim().split(' ')[0];
if (firstUrl.startsWith('http')) processImage(firstUrl);
}
});
}
function setupPerformanceObserver() {
if (!window.PerformanceObserver) return;
try {
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const url = entry.name;
if (!url || !url.startsWith('http')) continue;
if (!isImageUrl(url)) continue;
const size = entry.transferSize ? Math.round(entry.transferSize / 1024) : 0;
processImage(url, size);
}
});
obs.observe({ type: 'resource', buffered: true });
} catch (e) {}
}
function setupObserver() {
const observer = new MutationObserver((mutations) => {
let shouldScan = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.querySelectorAll) {
const matches = node.querySelectorAll('video[src], img[src], img[data-src], .xgplayer, div[class*="video-canvas-panel-player"]');
if (matches.length > 0) { shouldScan = true; break; }
}
if (node.tagName === 'VIDEO' || node.tagName === 'IMG' || node.tagName === 'SOURCE') {
shouldScan = true; break;
}
}
}
}
if (mutation.type === 'attributes') {
if (mutation.target.tagName === 'VIDEO' && mutation.attributeName === 'src') {
if (mutation.target.src && mutation.target.src.startsWith('http')) processVideo(mutation.target);
}
if (mutation.target.tagName === 'IMG' && (mutation.attributeName === 'src' || mutation.attributeName === 'data-src')) {
const src = mutation.target.src || mutation.target.currentSrc || mutation.target.getAttribute('data-src');
if (src && src.startsWith('http')) processImage(src);
}
}
}
if (shouldScan) {
clearTimeout(observer._debounce);
observer._debounce = setTimeout(scanPage, 500);
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'data-src'] });
return observer;
}
function buildUI() {
const btn = document.createElement('button');
btn.id = 'db-dl-btn';
btn.title = '豆包视频&图片下载助手';
btn.innerHTML = `
`;
btn.addEventListener('click', togglePanel);
setupDrag(btn);
restorePosition(btn);
document.body.appendChild(btn);
const overlay = document.createElement('div');
overlay.id = 'db-dl-overlay';
overlay.addEventListener('click', closePanel);
document.body.appendChild(overlay);
const panel = document.createElement('div');
panel.id = 'db-dl-panel';
panel.innerHTML = `