// ==UserScript==
// @name 短视频去水印下载 Pro + 全站嗅探下载
// @name:zh-CN 短视频去水印下载 Pro + 全站嗅探下载
// @version 3.3.3
// @license MIT
// @description 🎯 支持抖音、小红书网页版无水印高清下载。抖音:自动识别当前播放视频,一键下载无水印 MP4、封面图,支持图文帖多图批量保存,自动提取作者/标题/发布时间;小红书:支持视频笔记、图文笔记,自动解析无水印原图和视频链接,批量下载图片同时嗅探所有网页中的 M3U8、MP4 视频链接,可直接复制或下载。
// @tag 视频下载 视频嗅探 去水印 全平台下载 抖音 小红书
// @author LightDownload
// @match *://*.douyin.com/*
// @match *://*.xiaohongshu.com/*
// @match *://*.bilibili.com/*
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant unsafeWindow
// @connect *
// @run-at document-start
// @namespace https://greasyfork.org/
// ==/UserScript==
(function () {
'use strict';
/* ══════════════════════════════════════════════════════════
STATE
══════════════════════════════════════════════════════════ */
var dirHandle = null;
var videoInfo = null;
var isDownloading = false;
var lastNavUrl = '';
var imageDownloading = false;
var capturedCovers = [];
var xhsNoteData = null;
var detectedUrls = new Set();
var sniffMemoryVault = [];
var isSniffSectionVisible = GM_getValue('sniff_section_visible', false);
/* ══════════════════════════════════════════════════════════
PLATFORM DETECTION
══════════════════════════════════════════════════════════ */
function isDouyin() { return location.hostname.includes('douyin.com'); }
function isXiaohongshu() { return location.hostname.includes('xiaohongshu.com'); }
function isBili() { return location.hostname.includes('bilibili.com'); }
function isDyOrXhs() { return isDouyin() || isXiaohongshu(); }
/* ══════════════════════════════════════════════════════════
UTILS
══════════════════════════════════════════════════════════ */
function q(id) { return document.getElementById(id); }
function esc(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>'); }
function avPh() { var d=document.createElement('div'); d.className='svd-avph'; d.textContent='👤'; return d; }
function setStatus(m, c) {
var e = q('svd-stattxt');
if (!e) return;
e.textContent = m;
e.className = c || '';
}
function setProgress(r) {
var b = q('svd-prog'), f = q('svd-progfill');
if (!b || !f) return;
if (r <= 0 || r >= 1) { b.style.display = 'none'; f.style.width = '0%'; }
else { b.style.display = 'block'; f.style.width = (r * 100).toFixed(1) + '%'; }
}
function setBtnsEnabled(on) {
var vb = q('svd-btn-v'), cb = q('svd-btn-c');
if (!on) { if (vb) vb.disabled = true; if (cb) cb.disabled = true; return; }
if (vb) vb.disabled = !!(videoInfo && videoInfo.isImagePost);
if (cb) cb.disabled = !(videoInfo && videoInfo.coverUrl);
}
function sanitize(s) { return (s||'').replace(/[\\/:*?"<>|\r\n]/g,'_').trim().substring(0,80); }
/** 从 URL 提取自然文件名,用于嗅探下载 */
function getUrlFilename(url) {
try {
var pathname = new URL(url).pathname;
var parts = pathname.split('/').filter(Boolean);
var last = parts[parts.length - 1] || '';
if (last) return decodeURIComponent(last);
} catch(e) {}
var m = url.match(/\/([^/?#]+)(?:\?|#|$)/);
return m ? decodeURIComponent(m[1]) : 'video.mp4';
}
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
return new Promise(function(resolve, reject) {
var ta = document.createElement('textarea');
ta.value = text; ta.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); resolve(); } catch(e) { reject(e); }
document.body.removeChild(ta);
});
}
/* ══════════════════════════════════════════════════════════
INDEXEDDB — 目录句柄持久化
══════════════════════════════════════════════════════════ */
function _idbOp(mode, fn) {
return new Promise(function(resolve) {
var req = indexedDB.open('svd_fs_db', 1);
req.onupgradeneeded = function(e) { e.target.result.createObjectStore('handles'); };
req.onsuccess = function(e) { fn(e.target.result, resolve); };
req.onerror = function() { resolve(null); };
});
}
function _idbSaveHandle(handle) {
return _idbOp('readwrite', function(db, resolve) {
var tx = db.transaction('handles', 'readwrite');
tx.objectStore('handles').put(handle, 'dir');
tx.oncomplete = function() { db.close(); resolve(true); };
tx.onerror = function() { db.close(); resolve(false); };
});
}
function _idbLoadHandle() {
return _idbOp('readonly', function(db, resolve) {
var tx = db.transaction('handles', 'readonly');
var gr = tx.objectStore('handles').get('dir');
gr.onsuccess = function() { db.close(); resolve(gr.result || null); };
gr.onerror = function() { db.close(); resolve(null); };
});
}
async function _restoreDirHandle() {
try {
var handle = await _idbLoadHandle();
if (!handle) return;
var perm = await handle.queryPermission({ mode: 'readwrite' });
if (perm === 'granted') {
dirHandle = handle;
_applyDirHandleUI(handle.name, true);
} else {
GM_setValue('svd_last_dir_name', handle.name);
_applyDirHandleUI(handle.name, false);
}
} catch(e) {}
}
function _applyDirHandleUI(name, authorized) {
var d = q('svd-path-txt');
if (!d) return;
if (authorized) {
d.textContent = '📁 ' + name + ' (已授权)';
d.title = '目录:' + name;
} else {
d.textContent = '📁 ' + name + '(请点击按钮重新授权)';
}
d.classList.add('set');
}
/* ══════════════════════════════════════════════════════════
SNIFFER — 视频链接嗅探
══════════════════════════════════════════════════════════ */
function getResTag(u) {
u = u.toLowerCase();
if (u.includes('8k') || u.includes('4320')) return '[👑 8K] ';
if (u.includes('4k') || u.includes('2160')) return '[💎 4K] ';
if (u.includes('1080') || u.includes('1920')) return '[🔥 1080P] ';
if (u.includes('720')) return '[🎬 720P] ';
if (u.includes('480')) return '[📺 480P] ';
return '';
}
function tryRemoveWatermark(rawUrl) {
try {
var u = new URL(rawUrl, location.href);
var wmParams = ['watermark','wm','x-watermark','x-wm','logo'];
var changed = false;
wmParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } });
var newPath = u.pathname.replace(/\/watermark\//i, '/');
if (newPath !== u.pathname) { u.pathname = newPath; changed = true; }
return changed ? u.href : null;
} catch(e) {}
return null;
}
function addUrl(url, customTitle, isBiliBatch) {
if (typeof url !== 'string') return;
var fingerprint = url.split('?')[0];
if (detectedUrls.has(fingerprint)) return;
if (!isBiliBatch &&
!/\.(m3u8|mp4|ts|m4s|mpd|m4v|mkv|webm|mov|avi|flv)(\?|$)/i.test(url) &&
!/\/video\/|\/vod\/|\/stream\/|\/playlist\/|m3u8/i.test(url)) return;
if (url.includes('fragment') && (url.includes('.ts') || url.includes('seg-'))) return;
if (window.self !== window.top) {
window.top.postMessage({ type: 'VIDEO_SNIFF_ADD', url: url, customTitle: customTitle, isBiliBatch: isBiliBatch }, '*');
return;
}
detectedUrls.add(fingerprint);
sniffMemoryVault.push({ url: url, customTitle: customTitle, isBiliBatch: isBiliBatch });
renderSingleSniffItem({ url: url, customTitle: customTitle, isBiliBatch: isBiliBatch });
updateSniffStatus();
if (customTitle && !isBiliBatch) {
var cleanUrl = tryRemoveWatermark(url);
if (cleanUrl) {
var cleanFP = cleanUrl.split('?')[0];
if (!detectedUrls.has(cleanFP)) addUrl(cleanUrl, customTitle + '(去水印尝试)', false);
}
}
}
function renderSingleSniffItem(item) {
var list = q('svd-m3u8-list');
if (!list) return;
var li = document.createElement('li');
li.className = 'svd-m3u8-item';
var tag = item.isBiliBatch ? '[🎬 选集] ' : getResTag(item.url);
var name = item.customTitle
? (tag + item.customTitle)
: (tag + item.url.split('?')[0].substring(0, 60) + '...');
li.innerHTML = ''
+ '
'
+ '
' + name + '
'
+ '
'
+ ''
+ ''
+ '
';
li.querySelector('.svd-sniff-copy').onclick = function(e) {
e.stopPropagation();
copyToClipboard(item.url).then(function() { setStatus('链接已复制', 'ok'); }).catch(function() { setStatus('复制失败', 'err'); });
};
li.querySelector('.svd-sniff-dl').onclick = function(e) {
e.stopPropagation();
// 使用 URL 中的自然文件名,而非写死的通用名
downloadBestQuality(item.url, getUrlFilename(item.url), '嗅探视频');
};
li.querySelector('input').addEventListener('click', function(e) { e.stopPropagation(); });
list.prepend(li);
}
function captureVideoElementSrcs() {
document.querySelectorAll('video').forEach(function(v) {
var src = v.currentSrc || v.src;
if (src && !/^(blob:|data:)/i.test(src)) addUrl(src);
v.querySelectorAll('source').forEach(function(s) {
var ss = s.getAttribute('src');
if (ss && !/^(blob:|data:)/i.test(ss)) addUrl(ss);
});
});
}
function updateSniffStatus() {
if (isDyOrXhs()) return;
if (isSniffSectionVisible) captureVideoElementSrcs();
if (isSniffSectionVisible) {
var count = detectedUrls.size;
setStatus(count > 0 ? ('✅ 已嗅探到 ' + count + ' 个链接,可下载或复制') : '📡 嗅探面板已开启,播放视频后自动捕获链接', count > 0 ? 'ok' : 'info');
} else {
setStatus('📡 请打开嗅探面板以捕获视频链接', 'info');
}
}
/* ══════════════════════════════════════════════════════════
NETWORK INTERCEPTORS — 封面捕获 + 嗅探 + 小红书数据
══════════════════════════════════════════════════════════ */
function isCoverUrl(url) {
return /byteimg\.com\/img\/|douyinstatic\.com|pstatp\.com|xhscdn\.com|xiaohongshu\.com/.test(url) &&
/\.(jpe?g|webp|png)/.test(url) && !/avatar/.test(url);
}
function captureCoverUrl(rawUrl) {
if (!rawUrl || typeof rawUrl !== 'string' || /^(blob:|data:)/.test(rawUrl)) return;
var url; try { url = decodeURIComponent(rawUrl); } catch(e) { url = rawUrl; }
if (isCoverUrl(url)) pushCoverCapture(url);
}
function pushCoverCapture(url) {
var base = url.split('?')[0];
for (var i = 0; i < capturedCovers.length; i++) {
if (capturedCovers[i].url.split('?')[0] === base) { capturedCovers[i].ts = Date.now(); return; }
}
capturedCovers.push({ url: url, ts: Date.now() });
capturedCovers.sort(function(a, b) { return b.ts - a.ts; });
if (capturedCovers.length > 20) capturedCovers.pop();
}
function bestCover() {
if (!capturedCovers.length) return null;
var now = Date.now();
var fresh = capturedCovers.filter(function(c) { return now - c.ts < 90000; });
return (fresh.length ? fresh[0] : capturedCovers[0]).url;
}
function extractXiaohongshuFromResponse(responseText) {
try {
var data = JSON.parse(responseText);
var note = null;
if (data && data.data) {
note = data.data.note_card || data.data.note ||
(data.data.items && data.data.items.length && data.data.items[0].note_card) || null;
}
if (!note) return;
_buildXhsNoteData(note, null);
} catch(e) { console.warn('[XHS] 解析笔记数据失败:', e); }
}
/** 统一构建 XHS 笔记对象,避免重复代码 */
function _buildXhsNoteData(note, noteIdOverride) {
var images = [];
if (note.image_list) {
images = note.image_list.map(function(img) {
var infoList = img.info_list || [];
var url = img.url_default || (infoList.length && infoList[infoList.length-1].url) || (infoList[0] && infoList[0].url) || '';
var liveUrl = (img.live_photo && img.stream && img.stream.h264 && img.stream.h264[0] && img.stream.h264[0].master_url) || '';
return { url: url, liveUrl: liveUrl };
});
}
var videoUrl = '';
if (note.video) {
var stream = (note.video.media && note.video.media.stream) || note.video.stream || null;
videoUrl = (stream && stream.h265 && stream.h265[0] && stream.h265[0].master_url) ||
(stream && stream.h264 && stream.h264[0] && stream.h264[0].master_url) || '';
}
xhsNoteData = {
platform: '小红书',
author: (note.user && (note.user.nickname || note.user.nick_name)) || '未知作者',
authorId: (note.user && note.user.user_id) || '',
avatarUrl: (note.user && note.user.avatar) || '',
desc: note.title || note.desc || '无文案',
createTime: note.time ? new Date(note.time).toLocaleString('zh-CN', {hour12:false}) : '未知',
coverUrl: (note.image_list && note.image_list[0] && note.image_list[0].url_default) ||
(note.video && note.video.image && note.video.image.thumbnail) || '',
videoUrl: videoUrl,
isImagePost: !videoUrl && images.length > 0,
images: images,
noteId: note.note_id || noteIdOverride || '',
ipLocation: note.ip_location || '',
topics: []
};
renderInfo(xhsNoteData);
}
(function setupInterceptors() {
// 劫持 fetch
try {
var _origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function() {
var a = Array.prototype.slice.call(arguments);
var url = typeof a[0] === 'string' ? a[0] : (a[0] && a[0].url) || '';
captureCoverUrl(url);
if (url) {
try {
var abs = new URL(url, location.href).href;
if (/\.(m3u8|mp4)(\?|$)/i.test(abs)) addUrl(abs);
} catch(e) {}
}
return _origFetch.apply(this, a);
};
} catch(e) {}
// 劫持 XHR
var _origOpen = unsafeWindow.XMLHttpRequest.prototype.open;
var _origSend = unsafeWindow.XMLHttpRequest.prototype.send;
unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
this._url = url;
captureCoverUrl(typeof url === 'string' ? url : '');
return _origOpen.apply(this, arguments);
};
unsafeWindow.XMLHttpRequest.prototype.send = function() {
var self = this, url = self._url;
if (url) {
try {
var abs = new URL(url, location.href).href;
if (/\.(m3u8|mp4)(\?|$)/i.test(abs)) addUrl(abs);
} catch(e) {}
}
if (isXiaohongshu() && url && (
url.includes('/api/sns/web/v1/feed') ||
url.includes('/api/sns/web/v2/note/detail') ||
url.includes('/api/sns/web/v1/note/user_posted') ||
url.includes('/api/sns/web/v1/note/detail')
)) {
self.addEventListener('load', function() {
if (self.status === 200) extractXiaohongshuFromResponse(self.responseText);
});
}
return _origSend.apply(this, arguments);
};
// 小红书详情页备用:__INITIAL_STATE__
if (isXiaohongshu() && location.pathname.startsWith('/explore/')) {
function tryExtractInitialState() {
try {
var state = unsafeWindow.__INITIAL_STATE__;
if (!state || !state.note) return false;
var noteId = location.pathname.split('/').pop();
var noteMap = state.note.noteDetailMap;
var noteEntry = noteMap && noteMap[noteId] && noteMap[noteId].note;
if (!noteEntry) return false;
_buildXhsNoteData(noteEntry, noteId);
return true;
} catch(e) {}
return false;
}
setTimeout(function() {
if (!xhsNoteData) {
if (!tryExtractInitialState()) setTimeout(function() { if (!xhsNoteData) tryExtractInitialState(); }, 2000);
}
}, 2500);
}
// B站批量扫描
if (isBili()) {
window.scanBili = function() {
var count = 0;
document.querySelectorAll('.imageListItem_wrap__o28QW, .video-pod__item').forEach(function(el) {
var bv = el.getAttribute('data-key');
var a = el.querySelector('a');
if (bv) { addUrl('https://www.bilibili.com/video/' + bv, el.querySelector('.title') && el.querySelector('.title').innerText.trim(), true); count++; }
else if (a) { addUrl(new URL(a.getAttribute('href'), location.href).href, el.querySelector('.imageListItem_titleWrap__YTlLH') && el.querySelector('.imageListItem_titleWrap__YTlLH').getAttribute('title'), true); count++; }
});
setStatus(count > 0 ? ('已扫描到 ' + count + ' 个选集') : '未找到可扫描的选集', count > 0 ? 'ok' : 'info');
};
}
// 跨 iframe 消息接收
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'VIDEO_SNIFF_ADD') {
addUrl(e.data.url, e.data.customTitle, e.data.isBiliBatch);
}
});
})();
/* ══════════════════════════════════════════════════════════
DOUYIN OBSERVER
══════════════════════════════════════════════════════════ */
var _itemObserver = null, _currentItemKey = null;
function getItemKey(el) {
return el.getAttribute('data-aweme-id') || el.getAttribute('data-id') ||
el.dataset.awemeid || el.dataset.id ||
(el.parentElement ? Array.prototype.indexOf.call(el.parentElement.children, el) : 0) + '_i';
}
function setupItemObserver() {
if (!isDouyin()) return;
if (_itemObserver) { _itemObserver.disconnect(); _itemObserver = null; }
var items = document.querySelectorAll('.page-recommend-container,.dySwiperSlide[data-e2e="feed-item"]');
if (!items.length) return;
_itemObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.intersectionRatio >= 0.65) {
var key = getItemKey(entry.target);
if (key && key !== _currentItemKey) {
_currentItemKey = key;
capturedCovers = [];
xhsNoteData = null;
if (!isDownloading) setTimeout(doRefresh, 900);
}
}
});
}, { threshold: [0.65] });
Array.prototype.forEach.call(items, function(item) { _itemObserver.observe(item); });
}
var _domWatcher = null;
function watchForNewItems() {
if (!isDouyin() || _domWatcher) return;
_domWatcher = new MutationObserver(function(m) {
for (var i = 0; i < m.length; i++) { if (m[i].addedNodes.length) { setTimeout(setupItemObserver, 200); break; } }
});
_domWatcher.observe(document.body, { childList: true, subtree: true });
}
/* ══════════════════════════════════════════════════════════
VIDEO EXTRACTION — 抖音
══════════════════════════════════════════════════════════ */
function findCurrentFeedItem() {
if (!isDouyin()) return null;
var vids = Array.prototype.slice.call(document.querySelectorAll('video'));
var playing = null;
for (var i = 0; i < vids.length; i++) {
if (!vids[i].paused && vids[i].readyState >= 2 && vids[i].currentTime > 0) { playing = vids[i]; break; }
}
if (!playing && vids.length) {
var bestV = -1;
for (var j = 0; j < vids.length; j++) {
var r = vids[j].getBoundingClientRect();
var v = Math.min(r.bottom, window.innerHeight) - Math.max(r.top, 0);
if (v > bestV) { bestV = v; playing = vids[j]; }
}
}
if (playing) {
var el = playing.parentElement;
while (el && el !== document.body) {
if (el.classList.contains('page-recommend-container') || el.dataset.e2e === 'feed-item' ||
el.id === 'slideMode' || el.classList.contains('playerControlHeight')) return el;
el = el.parentElement;
}
}
var items = Array.prototype.slice.call(document.querySelectorAll(
'.page-recommend-container,.dySwiperSlide[data-e2e="feed-item"],#slideMode,.playerControlHeight'
));
var best = null, bestVis = -1;
for (var k = 0; k < items.length; k++) {
var rr = items[k].getBoundingClientRect();
var vv = Math.min(rr.bottom, window.innerHeight) - Math.max(rr.top, 0);
if (vv > bestVis) { bestVis = vv; best = items[k]; }
}
return best;
}
function hasImageTextBadge(container) {
if (!container) return false;
var all = container.querySelectorAll('span,div,a,em,i,button');
for (var i = 0; i < all.length; i++) { if (all[i].textContent.trim() === '图文') return true; }
return !!(
container.querySelector('.xgplayer-progress-outer.picture') ||
container.querySelector('[class*="pictureMode"],[class*="PicTextTag"],[class*="picTextTag"]') ||
container.querySelector('[class*="imageTextTag"],[class*="ImageTextTag"],[class*="img-text-tag"]') ||
container.getAttribute('data-type') === 'image'
);
}
function isContentImg(img) {
if (!img) return false;
var src = img.src || img.getAttribute('data-src') || '';
if (!src || /^(blob:|data:)/.test(src) || src.length < 20) return false;
if (/avatar|emoji|icon|logo|dot|indicator/i.test(src)) return false;
if (/avatar|emoji|icon|logo|dot|indicator/i.test(img.className || '')) return false;
var w = img.naturalWidth || img.width || 0;
var h = img.naturalHeight || img.height || 0;
if (w > 0 && w < 50) return false;
if (h > 0 && h < 50) return false;
var par = img.parentElement;
for (var i = 0; i < 3 && par; i++) {
if (/avatar|Avatar|userPhoto/i.test(par.className || '')) return false;
par = par.parentElement;
}
return true;
}
function extractSlideImages(container) {
var images = [], seen = {};
var slideSelectors = '[class*="swiper-slide"],[class*="SwiperSlide"],[class*="slide-item"],[class*="ImageItem"],[class*="imageItem"],[class*="pic-item"]';
var slides = container.querySelectorAll(slideSelectors);
if (slides.length >= 2) {
for (var i = 0; i < slides.length; i++) {
var img = (function(sl) {
var imgs = sl.querySelectorAll('img');
for (var j = 0; j < imgs.length; j++) {
if (isContentImg(imgs[j])) return imgs[j].src || imgs[j].getAttribute('data-src') || '';
}
return null;
})(slides[i]);
if (img && !seen[img]) { seen[img] = true; images.push(img); }
}
if (images.length >= 2) return images;
}
var wrapper = container.querySelector('[class*="swiper-wrapper"],[class*="SwiperWrapper"],[class*="carousel"],[class*="Carousel"]');
if (wrapper) {
var imgs = wrapper.querySelectorAll('img');
for (var j = 0; j < imgs.length; j++) {
if (isContentImg(imgs[j])) {
var src = imgs[j].src || imgs[j].getAttribute('data-src') || '';
if (src && !seen[src]) { seen[src] = true; images.push(src); }
}
}
}
return images;
}
function detectImagePost(container) {
if (!container || !hasImageTextBadge(container)) return { isImage: false, images: [] };
return { isImage: true, images: extractSlideImages(container) };
}
function extractCover(container) {
if (!container) return bestCover() || '';
var xgPoster = container.querySelector('.xgplayer-poster,.xg-poster,[class*="xgplayer-poster"],[class*="xg-poster"]');
if (xgPoster) {
var bgStyle = xgPoster.getAttribute('style') || '';
var bgMatch = bgStyle.match(/background(?:-image)?\s*:\s*url\(["']?(https?:[^"')]+)["']?\)/) ||
window.getComputedStyle(xgPoster).backgroundImage.match(/url\(["']?(https?:[^"')]+)["']?\)/);
if (bgMatch) return bgMatch[1];
var xgImg = xgPoster.querySelector('img');
if (xgImg && xgImg.src && !/^(blob:|data:)/.test(xgImg.src)) return xgImg.src;
}
var coverSels = ['img[class*="cover"]','img[class*="Cover"]','img[class*="poster"]','img[class*="Poster"]','img[class*="thumb"]','img[class*="Thumb"]'];
for (var s = 0; s < coverSels.length; s++) {
var ci = container.querySelector(coverSels[s]);
if (ci && isContentImg(ci)) return ci.src;
}
var imgs = container.querySelectorAll('img');
for (var i = 0; i < imgs.length; i++) {
var src = imgs[i].src || imgs[i].getAttribute('data-src') || '';
if (isContentImg(imgs[i]) && src) return src;
}
var bgEls = container.querySelectorAll('[class*="cover"],[class*="poster"],[class*="mask"]');
for (var b = 0; b < bgEls.length; b++) {
var bg = window.getComputedStyle(bgEls[b]).backgroundImage;
var m = bg.match(/url\(["']?(https?:[^"')]+)["']?\)/);
if (m) return m[1];
}
return bestCover() || '';
}
/** 解析 RENDER_DATA,同时提取视频信息和最高清视频URL */
function extractFromRenderData() {
if (!isDouyin()) return null;
try {
var el = document.getElementById('RENDER_DATA');
if (!el) return null;
var raw = JSON.parse(decodeURIComponent(el.textContent));
var detail = null;
for (var key in raw) {
if (key === '_location' || key === 'app' || key === '11') continue;
if (raw[key] && raw[key].videoDetail) { detail = raw[key].videoDetail; break; }
}
if (!detail) return null;
var author = detail.authorInfo || detail.author || {};
var video = detail.video || {};
var textExtra = detail.textExtra || [];
// 提取最高清视频 URL(合并原 getHighQualityVideoUrlFromRenderData)
var videoUrl = null;
var bitrates = video.bitRate || [];
if (bitrates.length > 0) {
bitrates.sort(function(a, b) { return (b.bit_rate || 0) - (a.bit_rate || 0); });
var best = bitrates[0];
if (best && best.play_addr && best.play_addr.url_list && best.play_addr.url_list.length) {
videoUrl = best.play_addr.url_list[0].replace(/^\/\//, 'https://');
}
}
if (!videoUrl) {
var pa = video.play_addr;
if (pa && pa.url_list && pa.url_list.length) videoUrl = pa.url_list[0].replace(/^\/\//, 'https://');
}
if (!videoUrl) {
var h264 = video.h264_packed_addr;
if (h264 && h264.url_list && h264.url_list.length) videoUrl = h264.url_list[0].replace(/^\/\//, 'https://');
}
var coverList = ((video.cover || {}).urlList) || ((video.dynamicCover || {}).urlList) || ((video.originCover || {}).urlList) || [];
var coverUrl = coverList[0] || bestCover() || '';
var avList = ((author.avatarThumb || {}).urlList) || ((author.avatarMedium || {}).urlList) || [];
var topics = textExtra.filter(function(t) { return t.hashtagName; }).map(function(t) { return '#' + t.hashtagName; });
var rawTime = detail.create_time || detail.createTime;
var hasImages = !!(detail.imageAlbumMusicInfo || (detail.images && detail.images.length));
var container = findCurrentFeedItem();
var isImagePost = hasImages || (container && hasImageTextBadge(container));
var images = isImagePost ? (detail.images || []).map(function(img) {
var list = img.urlList || [];
return list[list.length - 1] || list[0] || '';
}).filter(Boolean) : [];
return {
platform: '抖音',
author: author.nickname || author.uniqueId || '未知作者',
authorId: author.uniqueId || '',
avatarUrl: avList[0] || '',
createTime: rawTime ? new Date(rawTime * 1000).toLocaleString('zh-CN', {hour12:false}) : '未知',
desc: detail.desc || detail.title || '无文案',
topics: topics,
coverUrl: coverUrl,
videoUrl: videoUrl,
isImagePost: isImagePost,
images: images
};
} catch(e) { console.warn('[SVD] RENDER_DATA:', e); return null; }
}
function getHeaders() {
var origin = location.origin, referer = location.href;
if (isDouyin()) { origin = 'https://www.douyin.com'; referer = 'https://www.douyin.com/'; }
else if (isXiaohongshu()) { origin = 'https://www.xiaohongshu.com'; referer = 'https://www.xiaohongshu.com/'; }
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Origin': origin, 'Referer': referer
};
}
function getCurrentVideoElement() {
var container = findCurrentFeedItem() || document.querySelector('.playerControlHeight') || document.getElementById('slideMode');
return container ? container.querySelector('video') : null;
}
function getVideoUrlFromDom(videoEl) {
if (!videoEl) return null;
var src = videoEl.currentSrc || videoEl.getAttribute('src') || '';
if (src && !/^(blob:|data:)/.test(src) && /^https?:/.test(src)) {
try { return decodeURI(src); } catch(e) { return src; }
}
var sources = videoEl.querySelectorAll('source');
for (var i = 0; i < sources.length; i++) {
var s = sources[i].getAttribute('src') || '';
if (s && !/^(blob:|data:)/.test(s) && /^https?:/.test(s)) {
try { return decodeURI(s); } catch(e) { return s; }
}
}
return null;
}
function extractVideoInfo() {
if (isDouyin()) {
var rd = extractFromRenderData();
if (rd) {
// RENDER_DATA 已包含 videoUrl;若仍为 null 则从 DOM 补充
if (!rd.videoUrl) rd.videoUrl = getVideoUrlFromDom(getCurrentVideoElement());
return rd;
}
var sm = document.getElementById('slideMode');
if (sm) return extractFromContainer(sm, false);
var pc = document.querySelector('.playerControlHeight');
if (pc) return extractFromContainer(pc, true);
var fi = findCurrentFeedItem();
if (fi) return extractFromFeedItem(fi);
return null;
} else if (isXiaohongshu()) {
return xhsNoteData;
}
return null;
}
function extractFromFeedItem(item) {
var author = ((item.querySelector('.account-name') || {textContent:''}).textContent || '').trim() || '未知作者';
var avEl = item.querySelector('[class*="avatar"] img,.avatar img,img[class*="Avatar"]');
var desc = '无文案';
var te = item.querySelector('.title');
if (te) { var sp = te.querySelector('span'); desc = ((sp && sp.textContent) || te.textContent || '').replace('展开', '').trim(); }
var topics = [];
var tags = item.querySelectorAll('a[href*="hashtag"],[class*="Tag"],[class*="tag"]');
for (var i = 0; i < tags.length; i++) { var tt = tags[i].textContent.trim(); if (tt && tt[0] === '#' && topics.indexOf(tt) === -1) topics.push(tt); }
var imgPost = detectImagePost(item);
var timeEl = item.querySelector('[class*="create-time"],[class*="createTime"],[data-e2e="video-time"],[class*="publish-time"]');
return {
platform: '抖音', author: author, authorId: '', avatarUrl: avEl ? avEl.src : '',
createTime: timeEl ? timeEl.textContent.trim() : '未知',
desc: desc, topics: topics,
coverUrl: extractCover(item),
videoUrl: imgPost.isImage ? null : getVideoUrlFromDom(item.querySelector('video')),
isImagePost: imgPost.isImage, images: imgPost.images
};
}
function extractFromContainer(container, isDetail) {
var an = container.querySelector('.account-name,[class*="nickname"] span,[class*="accountName"] span');
var desc = '无文案';
if (isDetail) { var h1 = container.querySelector('h1'); if (h1) desc = h1.textContent.trim(); }
else { var tn = container.querySelector('.title'); if (tn) desc = tn.textContent.replace('展开', '').trim(); }
var imgPost = detectImagePost(container);
var timeEl = container.querySelector('[class*="create-time"],[class*="createTime"],[data-e2e="video-time"],[class*="publish-time"]');
return {
platform: '抖音', author: (an && an.textContent.trim()) || '未知作者', authorId: '',
avatarUrl: container.querySelector('[class*="avatar"] img,.avatar img') ? container.querySelector('[class*="avatar"] img,.avatar img').src : '',
createTime: timeEl ? timeEl.textContent.trim() : '未知',
desc: desc, topics: [],
coverUrl: extractCover(container),
videoUrl: imgPost.isImage ? null : getVideoUrlFromDom(container.querySelector('video')),
isImagePost: imgPost.isImage, images: imgPost.images
};
}
/* ══════════════════════════════════════════════════════════
DOWNLOAD CORE
══════════════════════════════════════════════════════════ */
function getFilename(info, type, idx) {
var customName = '';
var inputEl = q('svd-filename-input');
if (inputEl) customName = inputEl.value.trim();
if (customName) {
var sc = sanitize(customName);
if (type === 'video') return sc + '.mp4';
if (type === 'cover') return sc + '_封面.jpg';
if (type === 'image') return sc + '_图文第' + (idx + 1) + '张.jpg';
return sc + '.bin';
}
var a = sanitize(info.author), d = sanitize((info.desc || '').substring(0, 25));
if (type === 'video') return a + '-' + d + '.mp4';
if (type === 'cover') return a + '-封面.jpg';
if (type === 'image') return a + '-图文第' + (idx + 1) + '张.jpg';
return a + '-文件.bin';
}
function resolveVideoUrl(rawUrl, callback) {
if (!rawUrl) { callback(null); return; }
var method = navigator.userAgent.indexOf('Firefox') !== -1 ? 'GET' : 'HEAD';
GM_xmlhttpRequest({
method: method, url: rawUrl, headers: getHeaders(), cookie: document.cookie, timeout: 20000,
onload: function(res) {
if (res.status === 200) callback(rawUrl);
else if (res.status === 302) {
var m = (res.responseHeaders || '').match(/[Ll]ocation:\s*(.+)/);
callback(m ? m[1].trim() : rawUrl);
} else if (res.status === 401 || res.status === 403) {
finishDL(false, '链接已失效,请重新加载视频后下载');
} else callback(rawUrl);
},
onerror: function() { callback(rawUrl); }
});
}
function startDownloadWithUrl(rawUrl) {
if (!videoInfo) videoInfo = extractVideoInfo() || { author: '未知', desc: '视频' };
videoInfo.videoUrl = rawUrl;
isDownloading = true;
setBtnsEnabled(false);
setStatus('正在解析视频地址...', 'info');
setProgress(0.02);
var lockedFilename = getFilename(videoInfo, 'video');
var lockedAuthor = videoInfo.author || '未知';
var ue = q('svd-f-url');
if (ue && rawUrl) {
ue.textContent = rawUrl.substring(0, 90) + (rawUrl.length > 90 ? '…' : '');
ue.className = 'svd-url found';
}
resolveVideoUrl(rawUrl, function(finalUrl) {
if (!finalUrl) { finishDL(false, '无法解析视频地址'); return; }
fetchBlob(finalUrl, lockedFilename, '视频 · ' + lockedAuthor);
});
}
function doDownloadVideo() {
if (isDownloading) { setStatus('下载进行中...', 'info'); return; }
if (isDouyin()) {
var rd = extractFromRenderData();
if (rd && rd.videoUrl) { startDownloadWithUrl(rd.videoUrl); return; }
var domUrl = getVideoUrlFromDom(getCurrentVideoElement());
if (domUrl) { startDownloadWithUrl(domUrl); return; }
// 动态捕获
isDownloading = true; setBtnsEnabled(false);
setStatus('🔍 正在捕获视频链接...', 'info');
var captureDone = false, captureTimer = null;
var origFetch = unsafeWindow.fetch;
var origXHROpen = unsafeWindow.XMLHttpRequest.prototype.open;
var origXHRSend = unsafeWindow.XMLHttpRequest.prototype.send;
function captureAndRestore(url) {
if (captureDone) return;
captureDone = true;
unsafeWindow.fetch = origFetch;
unsafeWindow.XMLHttpRequest.prototype.open = origXHROpen;
unsafeWindow.XMLHttpRequest.prototype.send = origXHRSend;
if (captureTimer) clearTimeout(captureTimer);
var decoded; try { decoded = decodeURI(url); } catch(e) { decoded = url; }
startDownloadWithUrl(decoded);
}
unsafeWindow.fetch = function() {
var args = Array.prototype.slice.call(arguments);
var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
if (url && (url.indexOf('.mp4') > -1 || url.indexOf('mime_type=video') > -1 || url.indexOf('douyinvod') > -1)) captureAndRestore(url);
if (url) { try { var a = new URL(url, location.href).href; if (/\.(m3u8|mp4)(\?|$)/i.test(a)) addUrl(a); } catch(e) {} }
return origFetch.apply(this, args);
};
unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
if (typeof url === 'string' && (url.indexOf('.mp4') > -1 || url.indexOf('mime_type=video') > -1 || url.indexOf('douyinvod') > -1)) captureAndRestore(url);
return origXHROpen.apply(this, arguments);
};
var videoEl = getCurrentVideoElement();
if (videoEl) videoEl.load();
captureTimer = setTimeout(function() {
if (!captureDone) {
unsafeWindow.fetch = origFetch;
unsafeWindow.XMLHttpRequest.prototype.open = origXHROpen;
unsafeWindow.XMLHttpRequest.prototype.send = origXHRSend;
setStatus('❌ 捕获超时,请重试或刷新页面', 'err');
isDownloading = false; setBtnsEnabled(true); setProgress(0);
}
}, 8000);
} else if (isXiaohongshu()) {
if (!videoInfo || !videoInfo.videoUrl) { setStatus('❌ 未找到视频链接', 'err'); return; }
startDownloadWithUrl(videoInfo.videoUrl);
} else {
setStatus('请使用嗅探面板下载', 'info');
}
}
function doDownloadCover() {
var url = (videoInfo && videoInfo.coverUrl) || bestCover();
if (!url) { setStatus('未找到封面', 'err'); return; }
doDownload(url, getFilename(videoInfo || {author:'未知',desc:'封面'}, 'cover'), '封面');
}
function doDownload(url, filename, label) {
if (isDownloading) { setStatus('下载进行中...', 'info'); return; }
isDownloading = true; setBtnsEnabled(false);
setStatus('正在验证 ' + label + ' ...', 'info'); setProgress(0.02);
GM_xmlhttpRequest({
method: navigator.userAgent.indexOf('Firefox') !== -1 ? 'GET' : 'HEAD',
url: url, headers: getHeaders(), cookie: document.cookie, timeout: 20000,
onload: function(r) {
var fu = url;
if (r.status === 302) { var m = (r.responseHeaders||'').match(/[Ll]ocation:\s*(.+)/); if (m) fu = m[1].trim(); }
else if (r.status === 403 || r.status === 401) { finishDL(false, '链接已失效,请等视频重新加载后下载'); return; }
fetchBlob(fu, filename, label);
},
onerror: function() { fetchBlob(url, filename, label); }
});
}
function doDownloadImage(url, filename, onComplete) {
setStatus('正在下载图片: ' + filename, 'info');
GM_xmlhttpRequest({
method: 'GET', url: url, headers: getHeaders(), cookie: document.cookie,
responseType: 'blob', timeout: 60000,
onload: function(res) {
if (res.status === 200) {
saveFile(res.response, filename).then(function(r) {
setStatus((r.ok ? '✅ 已保存: ' : '❌ ') + filename, r.ok ? 'ok' : 'err');
if (onComplete) onComplete(r.ok);
});
} else {
setStatus('❌ 图片下载失败 HTTP ' + res.status, 'err');
if (onComplete) onComplete(false);
}
},
onerror: function() { setStatus('❌ 图片下载出错', 'err'); if (onComplete) onComplete(false); }
});
}
function resolveBestM3u8Stream(m3u8Url, content) {
var lines = content.split('\n'), bestBw = -1, bestUri = null, curBw = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line.startsWith('#EXT-X-STREAM-INF')) {
var bwMatch = line.match(/BANDWIDTH=(\d+)/);
curBw = bwMatch ? parseInt(bwMatch[1], 10) : 0;
} else if (line && !line.startsWith('#') && curBw !== null) {
if (curBw > bestBw) { bestBw = curBw; bestUri = line; }
curBw = null;
}
}
if (!bestUri) return null;
try { return new URL(bestUri, m3u8Url).href; } catch(e) { return bestUri; }
}
function downloadBestQuality(rawUrl, filename, label) {
if (!rawUrl) { setStatus('❌ 无效链接', 'err'); return; }
if (/\.mp4(\?|$)/i.test(rawUrl) && !/\.m3u8/i.test(rawUrl)) { fetchBlob(rawUrl, filename, label); return; }
setStatus('正在解析视频清晰度...', 'info');
GM_xmlhttpRequest({
method: 'GET', url: rawUrl, headers: getHeaders(), cookie: document.cookie, timeout: 30000,
onload: function(res) {
if (res.status === 200) {
var best = resolveBestM3u8Stream(rawUrl, res.responseText);
fetchBlob(best || rawUrl, filename, label);
} else {
fetchBlob(rawUrl, filename, label);
}
},
onerror: function() { fetchBlob(rawUrl, filename, label); }
});
}
function fetchBlob(url, filename, label) {
setStatus('正在下载 ' + label + '...', 'info');
GM_xmlhttpRequest({
method: 'GET', url: url, headers: getHeaders(), cookie: document.cookie,
responseType: 'blob', timeout: 600000,
onprogress: function(e) {
if (e.lengthComputable) {
setProgress(e.loaded / e.total);
setStatus('⬇ ' + (e.loaded/1048576).toFixed(1) + ' / ' + (e.total/1048576).toFixed(1) + ' MB', 'info');
}
},
onload: function(res) {
if (res.status === 200) {
setStatus('正在保存...', 'info');
saveFile(res.response, filename).then(function(r) {
finishDL(r.ok, r.ok ? (r.method === 'fs' ? '已存入目录: ' + filename : '已触发下载: ' + filename) : r.error);
});
} else finishDL(false, 'HTTP ' + res.status);
},
onerror: function(e) { finishDL(false, e.error || '网络错误'); }
});
}
function finishDL(ok, msg) {
isDownloading = false; setProgress(0); setBtnsEnabled(true);
setStatus((ok ? '✅ ' : '❌ ') + msg, ok ? 'ok' : 'err');
}
async function saveFile(blob, filename) {
if (!dirHandle) return saveViaGM(blob, filename);
try {
var parts = filename.split('/'), finalName = parts.pop();
var cur = dirHandle;
for (var i = 0; i < parts.length; i++) {
if (parts[i]) cur = await cur.getDirectoryHandle(parts[i], { create: true });
}
var fileHandle = await cur.getFileHandle(finalName, { create: true });
var writable = await fileHandle.createWritable();
await writable.write(blob); await writable.close();
return { ok: true, method: 'fs' };
} catch(e) {
console.warn('[SVD] FS写入失败,回退GM_download:', e);
return saveViaGM(blob, filename);
}
}
function saveViaGM(blob, filename) {
return new Promise(function(res) {
var bu = URL.createObjectURL(blob);
GM_download({
url: bu, name: filename,
onload: function() { URL.revokeObjectURL(bu); res({ ok: true, method: 'gm' }); },
onerror: function(e) { URL.revokeObjectURL(bu); res({ ok: false, error: e.error || '失败' }); }
});
});
}
/* ══════════════════════════════════════════════════════════
UI — STYLES
══════════════════════════════════════════════════════════ */
var T = {
bg: '#ffffff', bg2: '#f7f8fa', bg3: '#f0f2f5',
border: '#e4e6ea', border2: '#ced0d4',
text: '#1c1e21', text2: '#65676b', text3: '#a8aaad',
accent: '#fe2c55', accentBg: 'rgba(254,44,85,0.08)',
purple: '#6366f1', green: '#16a34a', red: '#dc2626', blue: '#2563eb',
shadow: '0 4px 24px rgba(0,0,0,0.10), 0 1px 4px rgba(0,0,0,0.06)'
};
function injectStyles() {
if (q('svd-styles')) return;
var s = document.createElement('style');
s.id = 'svd-styles';
s.textContent = [
'#svd-win{position:fixed;top:58px;right:18px;width:410px;max-height:96vh;overflow-y:auto;overflow-x:hidden;resize:both;min-width:300px;max-width:680px;min-height:180px;background:'+T.bg+';border:1px solid '+T.border+';border-radius:16px;box-shadow:'+T.shadow+';z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;font-size:13px;color:'+T.text+';scrollbar-width:thin;scrollbar-color:'+T.border+' transparent}',
'#svd-win::-webkit-scrollbar{width:5px}#svd-win::-webkit-scrollbar-thumb{background:'+T.border2+';border-radius:3px}',
'#svd-win.svd-mini{max-height:52px;overflow:hidden}',
'#svd-hdr{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:10px;padding:14px 16px;background:'+T.bg+';border-bottom:1px solid '+T.border+';border-radius:16px 16px 0 0;cursor:move}',
'.svd-badge{width:28px;height:28px;border-radius:9px;background:'+T.accent+';display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 6px rgba(254,44,85,.35)}',
'.svd-badge svg{width:15px;height:15px;fill:#fff}',
'.svd-title{flex:1;font-weight:700;font-size:14px;color:'+T.text+';letter-spacing:-.2px}.svd-title em{color:'+T.accent+';font-style:normal}',
'.svd-wbtns{display:flex;gap:6px}',
'.svd-wbtn{width:22px;height:22px;border-radius:50%;border:none;cursor:pointer;font-size:11px;font-weight:700;line-height:1;display:flex;align-items:center;justify-content:center;transition:transform .15s}.svd-wbtn:hover{transform:scale(1.15)}',
'.svd-wbtn-min{background:#fdbc40;color:#5a3900}.svd-wbtn-x{background:#fd5f57;color:#5a0000}',
'.svd-sec{padding:12px 16px;border-bottom:1px solid '+T.border+';background:'+T.bg+'}',
'.svd-sec-lbl{font-size:10px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:'+T.text3+';margin-bottom:8px}',
'#svd-path-row{display:flex;align-items:center;gap:8px}',
'#svd-path-txt{flex:1;padding:7px 10px;background:'+T.bg2+';border:1px solid '+T.border+';border-radius:8px;color:'+T.text3+';font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'#svd-path-txt.set{color:'+T.green+';border-color:rgba(22,163,74,.3);background:rgba(22,163,74,.06)}',
'#svd-path-btn{padding:7px 13px;background:'+T.purple+';border:none;border-radius:8px;color:#fff;font-size:11.5px;font-weight:600;cursor:pointer;white-space:nowrap;box-shadow:0 2px 8px rgba(99,102,241,.3);transition:opacity .15s,transform .15s}',
'#svd-path-btn:hover{opacity:.88;transform:translateY(-1px)}',
'#svd-capbar{display:flex;align-items:center;gap:7px;padding:7px 16px;background:'+T.bg2+';border-bottom:1px solid '+T.border+';font-size:11px}',
'.svd-dot{width:7px;height:7px;border-radius:50%;background:'+T.text3+';flex-shrink:0;transition:background .3s}',
'.svd-dot.on{background:#16a34a;box-shadow:0 0 5px rgba(22,163,74,.6);animation:svd-pulse 1.5s infinite}',
'@keyframes svd-pulse{0%,100%{opacity:1}50%{opacity:.45}}',
'#svd-captxt{color:'+T.text3+'}#svd-captxt.on{color:'+T.green+'}',
'#svd-refresh{width:100%;padding:8px;background:'+T.bg2+';border:1px solid '+T.border+';border-radius:8px;color:'+T.text2+';font-size:12px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;transition:background .15s,border-color .15s}',
'#svd-refresh:hover{background:'+T.bg3+';border-color:'+T.border2+'}',
'#svd-tbl{padding:4px 16px 12px;background:'+T.bg+';border-bottom:1px solid '+T.border+'}',
'.svd-row{display:grid;grid-template-columns:64px 1fr;gap:10px;padding:8px 0;border-bottom:1px solid '+T.bg3+';align-items:start}.svd-row:last-child{border-bottom:none}',
'.svd-lbl{font-size:11.5px;font-weight:600;color:'+T.text3+';padding-top:1px}.svd-val{font-size:12.5px;color:'+T.text+';line-height:1.5}',
'.svd-ac{display:flex;align-items:center;gap:9px}',
'.svd-av{width:34px;height:34px;border-radius:50%;object-fit:cover;flex-shrink:0;border:2px solid '+T.accentBg+'}',
'.svd-avph{width:34px;height:34px;border-radius:50%;background:'+T.bg3+';display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}',
'.svd-aname{font-size:13px;font-weight:600;color:'+T.text+'}.svd-aid{font-size:11px;color:'+T.text3+';margin-top:1px}',
'.svd-desc{max-height:60px;overflow-y:auto;font-size:12.5px;color:'+T.text2+';line-height:1.6;word-break:break-word;scrollbar-width:thin}',
'.svd-tags{display:flex;flex-wrap:wrap;gap:4px}',
'.svd-tag{padding:2px 8px;background:'+T.accentBg+';border:1px solid rgba(254,44,85,.2);border-radius:100px;color:'+T.accent+';font-size:11px;font-weight:500}',
'.svd-cimg{width:100%;max-height:130px;object-fit:cover;border-radius:10px;display:block;cursor:pointer;transition:opacity .2s}.svd-cimg:hover{opacity:.85}',
'.svd-cph{width:100%;height:70px;background:'+T.bg2+';border:1.5px dashed '+T.border+';border-radius:10px;display:flex;align-items:center;justify-content:center;color:'+T.text3+';font-size:12px}',
'.svd-vid{width:100%;max-height:180px;border-radius:10px;background:#000;display:block;object-fit:contain}',
'.svd-imgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}',
'.svd-ig-item{position:relative;border-radius:8px;overflow:hidden;cursor:pointer;background:'+T.bg2+'}',
'.svd-igthumb{width:100%;aspect-ratio:1;object-fit:cover;display:block;transition:transform .2s}.svd-ig-item:hover .svd-igthumb{transform:scale(1.04)}',
'.svd-ig-dl{position:absolute;bottom:5px;right:5px;background:rgba(255,255,255,.9);backdrop-filter:blur(4px);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:13px;opacity:0;transform:translateY(4px);transition:opacity .18s,transform .18s;box-shadow:0 1px 4px rgba(0,0,0,.15)}',
'.svd-ig-item:hover .svd-ig-dl{opacity:1;transform:translateY(0)}',
'.svd-ig-idx{position:absolute;top:5px;left:6px;background:rgba(0,0,0,.45);color:#fff;font-size:10px;font-weight:600;padding:1px 5px;border-radius:4px}',
'.svd-imgdl-all{margin-top:7px;width:100%;padding:7px;background:'+T.bg2+';border:1px solid '+T.border+';border-radius:8px;color:'+T.text2+';font-size:11.5px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;transition:background .15s}',
'.svd-imgdl-all:hover{background:'+T.bg3+'}.svd-imgdl-all:disabled{opacity:.4;cursor:not-allowed}',
'.svd-url{font-size:10.5px;color:'+T.text3+';word-break:break-all;background:'+T.bg2+';border-radius:6px;padding:5px 8px;max-height:36px;overflow:hidden}',
'.svd-url.found{color:'+T.green+';background:rgba(22,163,74,.07)}',
'.svd-na{color:'+T.text3+';font-style:italic;font-size:11.5px}',
'#svd-actions{display:flex;gap:8px;padding:12px 16px;background:'+T.bg+';border-bottom:1px solid '+T.border+'}',
'.svd-abtn{flex:1;padding:10px 8px;border:none;border-radius:10px;font-size:12.5px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;transition:transform .15s,box-shadow .15s,opacity .15s}',
'.svd-abtn:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(0,0,0,.12)}.svd-abtn:disabled{opacity:.38;cursor:not-allowed;transform:none;box-shadow:none}',
'.svd-dlv{background:'+T.accent+';color:#fff}.svd-dlc{background:'+T.bg2+';color:'+T.text2+';border:1.5px solid '+T.border+'}',
'#svd-statarea{padding:9px 16px 13px;background:'+T.bg+'}',
'#svd-prog{height:3px;background:'+T.bg3+';border-radius:2px;margin-bottom:7px;overflow:hidden;display:none}',
'#svd-progfill{height:100%;width:0%;background:'+T.accent+';border-radius:2px;transition:width .2s}',
'#svd-stattxt{font-size:11px;color:'+T.text3+';text-align:center;min-height:16px}',
'#svd-stattxt.ok{color:'+T.green+'}#svd-stattxt.err{color:'+T.red+'}#svd-stattxt.info{color:'+T.blue+'}',
// 触发按钮:默认显示(flex),打开小窗后隐藏
'#svd-trig{position:fixed;bottom:108px;right:18px;width:46px;height:46px;border-radius:50%;background:'+T.accent+';border:none;cursor:pointer;z-index:2147483646;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px rgba(254,44,85,.4);transition:transform .2s}',
'#svd-trig:hover{transform:scale(1.1)}#svd-trig svg{width:22px;height:22px;fill:#fff}',
'#svd-opacity-slider{-webkit-appearance:none;appearance:none;width:80px;height:4px;background:'+T.border2+';border-radius:10px;outline:none;margin:0 8px;cursor:pointer}',
'#svd-opacity-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;background:'+T.accent+';border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,0.2);border:2px solid white}',
'#svd-opacity-slider::-moz-range-thumb{width:14px;height:14px;background:'+T.accent+';border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,0.2);border:2px solid white}',
'#svd-author-bar{padding:8px 16px 12px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid '+T.border+';background:'+T.bg+';font-size:11px}',
'.svd-author{color:'+T.text3+'}.svd-author a{color:'+T.accent+';text-decoration:none;font-weight:600}',
'.svd-donate{color:'+T.purple+';cursor:pointer;font-weight:500;background:'+T.accentBg+';padding:3px 8px;border-radius:12px;transition:background .15s}',
'.svd-donate:hover{background:rgba(254,44,85,0.15)}',
'#svd-donate-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:2147483648;display:none;align-items:center;justify-content:center}',
'#svd-donate-content{background:white;padding:24px 30px;border-radius:20px;text-align:center;max-width:360px;box-shadow:0 10px 40px rgba(0,0,0,0.2)}',
'#svd-donate-content img{width:280px;height:280px;margin:15px 0;border-radius:12px}',
'#svd-donate-close{margin-top:10px;padding:6px 20px;background:'+T.accent+';color:white;border:none;border-radius:20px;cursor:pointer}',
// 嗅探面板样式
'#svd-sniff-toggle-btn{width:calc(100% - 32px);margin:0 16px 10px;padding:8px;background:'+T.bg2+';border:1px solid '+T.border+';border-radius:8px;color:'+T.text2+';font-size:12px;font-weight:500;cursor:pointer;text-align:center}',
'#svd-sniff-section{margin:0 16px 10px;border:1px solid '+T.border+';border-radius:10px;padding:8px;background:'+T.bg2+';display:none;max-height:300px;overflow-y:auto}',
'#svd-sniff-section.visible{display:block}',
'#svd-m3u8-list{list-style:none;padding:0;margin:0}',
'.svd-m3u8-item{display:flex;align-items:center;padding:6px;background:rgba(128,128,128,0.08);margin-bottom:4px;border-radius:6px;border-left:3px solid '+T.accent+'}',
'.svd-sniff-checkbox{margin-right:6px;width:14px;height:14px}',
'.svd-sniff-url-content{flex:1}.svd-sniff-url-text{font-size:11px;word-break:break-all;line-height:1.3}',
'.svd-sniff-actions{display:flex;gap:4px;margin-top:4px}',
'.svd-sniff-actions button{padding:2px 8px;font-size:10px;border:none;border-radius:4px;cursor:pointer}',
'.svd-sniff-copy{background:#3498db;color:#fff}.svd-sniff-dl{background:#27ae60;color:#fff}',
'#svd-sniff-toolbar{display:flex;gap:4px;margin-bottom:6px}',
'#svd-sniff-toolbar button{flex:1;padding:4px 2px;border:none;border-radius:5px;cursor:pointer;font-size:10px;font-weight:bold;color:#fff;background:#555}',
'#svd-sniff-clear-btn{background:#e74c3c !important}'
].join('');
document.head.appendChild(s);
}
/* ══════════════════════════════════════════════════════════
UI — WINDOW BUILD
══════════════════════════════════════════════════════════ */
function buildWindow() {
if (q('svd-win')) return;
var w = document.createElement('div');
w.id = 'svd-win';
// 默认隐藏,点击触发按钮后打开
w.style.display = 'none';
var isDouyinOrXhs = isDyOrXhs();
var platformTitle = isDouyin() ? '短视频下载 Pro' : (isXiaohongshu() ? '小红书下载 Pro' : '视频嗅探 Pro');
var sniffHtml = (!isDouyinOrXhs) ? [
'',
'',
'
',
' ',
' ',
' ',
' ',
(isBili() ? ' ' : ''),
'
',
'
',
'
'
].join('') : '';
var dyXhsHtml = isDouyinOrXhs ? [
'',
'',
'',
'
平台—
',
'
',
'
发布时间—
',
'
',
'
',
'
',
'
',
'
',
'
',
'',
' ',
' ',
'
'
].join('') : '';
w.innerHTML = [
'',
'
',
'
' + platformTitle + '',
'
',
'
',
'
',
'',
'',
'
✏️ 文件命名 (留空则自动生成)
',
'
',
'
',
sniffHtml,
dyXhsHtml,
'',
'',
'
By 花海🌸',
'
❤️ 赞赏支持',
'
'
].join('');
document.body.appendChild(w);
bindEvents(w);
makeDraggable(w, w.querySelector('#svd-hdr'));
createDonateModal();
// 恢复嗅探面板状态(仅非抖音/XHS)
if (!isDouyinOrXhs && isSniffSectionVisible) {
var sec = q('svd-sniff-section');
if (sec) sec.classList.add('visible');
var btn = q('svd-sniff-toggle-btn');
if (btn) btn.textContent = '📡 关闭嗅探工具';
}
sniffMemoryVault.forEach(function(item) { renderSingleSniffItem(item); });
}
function buildTrigger() {
if (q('svd-trig')) return;
var btn = document.createElement('button');
btn.id = 'svd-trig';
btn.title = '打开下载助手';
btn.innerHTML = '';
btn.addEventListener('click', function() {
var w = q('svd-win');
if (w) { w.style.display = ''; btn.style.display = 'none'; }
});
document.body.appendChild(btn);
}
function createDonateModal() {
if (q('svd-donate-modal')) return;
var modal = document.createElement('div');
modal.id = 'svd-donate-modal';
modal.innerHTML = [
'',
'
赞赏支持
',
'
如果觉得好用,请作者喝杯咖啡☕
',
'

',
'
微信
',
'
',
'
'
].join('');
document.body.appendChild(modal);
q('svd-donate-btn').addEventListener('click', function() { modal.style.display = 'flex'; });
q('svd-donate-close').addEventListener('click', function() { modal.style.display = 'none'; });
modal.addEventListener('click', function(e) { if (e.target === modal) modal.style.display = 'none'; });
}
function bindEvents(w) {
w.querySelector('.svd-wbtn-min').addEventListener('click', function() { w.classList.toggle('svd-mini'); });
w.querySelector('.svd-wbtn-x').addEventListener('click', function() {
w.style.display = 'none';
var t = q('svd-trig'); if (t) t.style.display = 'flex';
});
var pathBtn = w.querySelector('#svd-path-btn');
if (pathBtn) pathBtn.addEventListener('click', async function() {
if (!window.showDirectoryPicker) { setStatus('浏览器不支持目录选择', 'err'); return; }
try {
var dh = await window.showDirectoryPicker({ mode: 'readwrite' });
dirHandle = dh;
await _idbSaveHandle(dh);
GM_setValue('svd_last_dir_name', dh.name);
_applyDirHandleUI(dh.name, true);
setStatus('下载目录: ' + dh.name + ' (授权成功)', 'ok');
} catch(e) { if (e.name !== 'AbortError') setStatus('目录选择失败: ' + e.message, 'err'); }
});
var refreshBtn = w.querySelector('#svd-refresh');
if (refreshBtn) refreshBtn.addEventListener('click', doRefresh);
var btnV = w.querySelector('#svd-btn-v');
if (btnV) btnV.addEventListener('click', doDownloadVideo);
var btnC = w.querySelector('#svd-btn-c');
if (btnC) btnC.addEventListener('click', doDownloadCover);
var slider = w.querySelector('#svd-opacity-slider');
if (slider) {
var savedOpacity = GM_getValue('svd_win_opacity', 1.0);
slider.value = savedOpacity; w.style.opacity = savedOpacity;
slider.addEventListener('input', function(e) { var v = parseFloat(e.target.value); w.style.opacity = v; GM_setValue('svd_win_opacity', v); });
slider.addEventListener('mousedown', function(e) { e.stopPropagation(); });
}
// 嗅探区域事件(仅非抖音/XHS 时存在相关元素)
var sniffToggle = w.querySelector('#svd-sniff-toggle-btn');
if (sniffToggle) {
sniffToggle.addEventListener('click', function() {
var sec = q('svd-sniff-section');
if (!sec) return;
isSniffSectionVisible = !sec.classList.contains('visible');
sec.classList.toggle('visible', isSniffSectionVisible);
this.textContent = isSniffSectionVisible ? '📡 关闭嗅探工具' : '📡 打开视频嗅探工具';
GM_setValue('sniff_section_visible', isSniffSectionVisible);
updateSniffStatus();
});
}
var selAll = w.querySelector('#svd-sel-all-sniff');
if (selAll) selAll.addEventListener('click', function() {
var cbs = w.querySelectorAll('.svd-sniff-checkbox');
var allChecked = Array.prototype.every.call(cbs, function(c) { return c.checked; });
Array.prototype.forEach.call(cbs, function(c) { c.checked = !allChecked; });
});
var copySelected = w.querySelector('#svd-sniff-copy-selected');
if (copySelected) copySelected.addEventListener('click', function() {
var urls = [];
Array.prototype.forEach.call(w.querySelectorAll('.svd-sniff-checkbox:checked'), function(c) { urls.push(c.dataset.url); });
if (urls.length) copyToClipboard(urls.join('\n')).then(function() { setStatus('已复制 ' + urls.length + ' 个链接', 'ok'); });
});
var dlSelected = w.querySelector('#svd-sniff-dl-selected');
if (dlSelected) dlSelected.addEventListener('click', function() {
var checked = w.querySelectorAll('.svd-sniff-checkbox:checked');
Array.prototype.forEach.call(checked, function(cb, i) {
setTimeout(function() {
// 使用 URL 自然文件名
downloadBestQuality(cb.dataset.url, getUrlFilename(cb.dataset.url), '嗅探视频 ' + (i + 1));
}, i * 800);
});
});
var clearBtn = w.querySelector('#svd-sniff-clear-btn');
if (clearBtn) clearBtn.addEventListener('click', function() {
detectedUrls.clear(); sniffMemoryVault = [];
var list = q('svd-m3u8-list'); if (list) list.innerHTML = '';
setStatus('已清空嗅探记录', 'info');
});
if (isBili()) {
var scanBiliBtn = w.querySelector('#svd-scan-bili-btn');
if (scanBiliBtn) scanBiliBtn.addEventListener('click', function() { window.scanBili && window.scanBili(); });
}
}
function makeDraggable(el, handle) {
var d = false, sx, sy, ox, oy;
handle.addEventListener('mousedown', function(e) {
if (e.target.classList.contains('svd-wbtn')) return;
d = true; sx = e.clientX; sy = e.clientY;
var r = el.getBoundingClientRect(); ox = r.left; oy = r.top;
document.addEventListener('mousemove', mv); document.addEventListener('mouseup', mu);
e.preventDefault();
});
function mv(e) {
if (!d) return;
el.style.left = Math.max(0, Math.min(ox + e.clientX - sx, window.innerWidth - el.offsetWidth)) + 'px';
el.style.top = Math.max(0, Math.min(oy + e.clientY - sy, window.innerHeight - el.offsetHeight)) + 'px';
el.style.right = 'auto';
}
function mu() { d = false; document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', mu); }
}
/* ══════════════════════════════════════════════════════════
UI — RENDER INFO
══════════════════════════════════════════════════════════ */
function tickCapture() {
var dot = q('svd-capdot'), txt = q('svd-captxt');
if (!dot || !txt) return;
var cc = capturedCovers.length, hasInfoCover = !!(videoInfo && videoInfo.coverUrl);
if (cc || hasInfoCover) {
dot.className = 'svd-dot on'; txt.className = 'on';
txt.textContent = hasInfoCover ? '封面已就绪' : ('已捕获 ' + cc + ' 张封面图');
} else {
dot.className = 'svd-dot'; txt.className = '';
txt.textContent = '等待封面加载...';
}
if (videoInfo && !videoInfo.coverUrl && cc) {
videoInfo.coverUrl = bestCover();
patchCoverUI(videoInfo.coverUrl);
}
}
function patchCoverUI(url) {
var el = q('svd-f-cov'); if (!el || !url) return;
if (!el.querySelector('img')) {
var ci = new Image(); ci.className = 'svd-cimg'; ci.src = url;
ci.addEventListener('click', function() { window.open(url, '_blank'); });
el.innerHTML = ''; el.appendChild(ci);
}
var cb = q('svd-btn-c'); if (cb) cb.disabled = false;
}
function renderInfo(info) {
if (!info) { setStatus('❌ 未能解析视频信息,请等视频播放后刷新', 'err'); return; }
if (!isDownloading) videoInfo = info;
q('svd-f-plt').textContent = info.platform;
var ae = q('svd-f-aut'); ae.innerHTML = '';
var ac = document.createElement('div'); ac.className = 'svd-ac';
if (info.avatarUrl) {
var ai = new Image(); ai.className = 'svd-av'; ai.src = info.avatarUrl;
ai.onerror = function() { ai.replaceWith(avPh()); };
ac.appendChild(ai);
} else ac.appendChild(avPh());
var nw = document.createElement('div');
nw.innerHTML = '' + esc(info.author) + '
' + (info.authorId ? '@' + esc(info.authorId) + '
' : '');
ac.appendChild(nw); ae.appendChild(ac);
q('svd-f-time').textContent = info.createTime;
q('svd-f-desc').textContent = info.desc || '无文案';
var te = q('svd-f-tags'); te.innerHTML = '';
if (info.topics && info.topics.length) {
var tw = document.createElement('div'); tw.className = 'svd-tags';
info.topics.forEach(function(t) { var sp = document.createElement('span'); sp.className = 'svd-tag'; sp.textContent = t; tw.appendChild(sp); });
te.appendChild(tw);
} else te.innerHTML = '无话题标签';
var ce = q('svd-f-cov'); ce.innerHTML = '';
if (info.coverUrl) {
var ci = new Image(); ci.className = 'svd-cimg'; ci.src = info.coverUrl; ci.title = '点击查看原图';
ci.addEventListener('click', function() { window.open(info.coverUrl, '_blank'); });
ci.onerror = function() { ce.innerHTML = '封面加载失败
'; };
ce.appendChild(ci);
} else ce.innerHTML = '封面加载中...
';
var lbl = q('svd-content-lbl'); if (lbl) lbl.textContent = info.isImagePost ? '图片' : '视频';
var ve = q('svd-f-vid'); ve.innerHTML = '';
if (info.isImagePost) {
if (info.images && info.images.length) buildImageGrid(ve, info);
else ve.innerHTML = '图文笔记(含背景音乐,无独立视频)';
var vbtn = q('svd-btn-v'); if (vbtn) vbtn.disabled = true;
} else {
if (info.videoUrl) {
var vd = document.createElement('video'); vd.className = 'svd-vid'; vd.controls = true; vd.preload = 'metadata';
if (info.coverUrl) vd.poster = info.coverUrl;
vd.src = info.videoUrl; ve.appendChild(vd);
} else ve.innerHTML = '点击下载按钮自动捕获高清链接';
}
var ue = q('svd-f-url');
if (ue) {
if (info.videoUrl) { ue.textContent = info.videoUrl.substring(0, 90) + (info.videoUrl.length > 90 ? '…' : ''); ue.className = 'svd-url found'; }
else { ue.textContent = '点击下载按钮实时捕获'; ue.className = 'svd-url'; }
}
q('svd-btn-v').disabled = !!(info.isImagePost);
q('svd-btn-c').disabled = !info.coverUrl;
setStatus('✅ 视频信息加载完成', 'ok');
}
function buildImageGrid(container, info) {
var grid = document.createElement('div'); grid.className = 'svd-imgrid';
info.images.forEach(function(item, idx) {
var url = typeof item === 'string' ? item : item.url;
var liveUrl = (typeof item === 'object' && item.liveUrl) ? item.liveUrl : null;
var itemDiv = document.createElement('div'); itemDiv.className = 'svd-ig-item';
var numLabel = document.createElement('span'); numLabel.className = 'svd-ig-idx'; numLabel.textContent = idx + 1;
var img = new Image(); img.className = 'svd-igthumb'; img.src = url; img.loading = 'lazy';
img.onerror = function() { itemDiv.style.background = '#f0f0f0'; img.style.opacity = '.3'; };
var dlBtn = document.createElement('button'); dlBtn.className = 'svd-ig-dl'; dlBtn.title = '下载第' + (idx+1) + '张'; dlBtn.textContent = '⬇';
(function(u, live, i) {
dlBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (live) doDownload(live, getFilename(info, 'image', i).replace(/\.(jpg|jpeg|png|webp)$/i, '.mp4'), '实况视频');
else doDownloadImage(u, getFilename(info, 'image', i));
});
})(url, liveUrl, idx);
itemDiv.appendChild(img); itemDiv.appendChild(numLabel); itemDiv.appendChild(dlBtn);
grid.appendChild(itemDiv);
});
container.appendChild(grid);
var allBtn = document.createElement('button'); allBtn.className = 'svd-imgdl-all';
allBtn.innerHTML = '⬇ 全部下载(' + info.images.length + '张)';
allBtn.addEventListener('click', function() {
if (imageDownloading) { setStatus('图片下载中,请稍候...', 'info'); return; }
imageDownloading = true; allBtn.disabled = true;
var total = info.images.length, done = 0;
function scheduleNext(idx) {
if (idx >= total) return;
var item = info.images[idx];
var url = typeof item === 'string' ? item : item.url;
var liveUrl = (typeof item === 'object' && item.liveUrl) ? item.liveUrl : null;
setTimeout(function() {
var finish = function() { done++; if (done >= total) { imageDownloading = false; allBtn.disabled = false; } };
if (liveUrl) { doDownload(liveUrl, getFilename(info, 'image', idx).replace(/\.(jpg|jpeg|png|webp)$/i, '.mp4'), '实况视频'); finish(); }
else doDownloadImage(url, getFilename(info, 'image', idx), finish);
scheduleNext(idx + 1);
}, idx === 0 ? 0 : 300);
}
scheduleNext(0);
});
container.appendChild(allBtn);
}
/* ══════════════════════════════════════════════════════════
REFRESH & BOOTSTRAP
══════════════════════════════════════════════════════════ */
function doRefresh() {
setStatus('正在读取视频信息...', 'info');
setTimeout(function() {
if (!isDyOrXhs()) { updateSniffStatus(); return; }
var info = extractVideoInfo();
if (info) renderInfo(info);
else setStatus('❌ 未能解析视频信息,请等视频播放后刷新', 'err');
}, 350);
}
function bootstrap() {
injectStyles();
buildWindow();
buildTrigger();
_restoreDirHandle();
if (isDouyin()) {
watchForNewItems();
setTimeout(function() { setupItemObserver(); doRefresh(); }, 1800);
} else {
setTimeout(doRefresh, 1500);
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function() { setTimeout(bootstrap, 500); });
else setTimeout(bootstrap, 500);
setInterval(function() {
if (!q('svd-win')) buildWindow();
if (!q('svd-trig')) buildTrigger();
if (!q('svd-styles')) injectStyles();
tickCapture();
if (isSniffSectionVisible && !isDyOrXhs()) captureVideoElementSrcs();
var cur = location.href;
if (cur !== lastNavUrl) {
lastNavUrl = cur;
capturedCovers = [];
xhsNoteData = null;
imageDownloading = false;
if (!isDownloading) setTimeout(doRefresh, isDouyin() ? 1800 : 1000);
}
}, 800);
})();