// ==UserScript== // @name 短视频去水印下载 Pro + 全站嗅探下载 // @name:zh-CN 短视频去水印下载 Pro + 全站嗅探下载 // @version 3.4.0 // @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 GM_notification // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect * // @run-at document-start // @namespace https://greasyfork.org/ // ==/UserScript== (function () { 'use strict'; /* ══════════════════════════════════════════════════════════ SETTINGS — 统一配置中心 ══════════════════════════════════════════════════════════ */ var SETTINGS = { // 外观 themeMode: GM_getValue('cfg_themeMode', 'auto'), // light / dark / auto rememberTriggerPos: GM_getValue('cfg_rememberTriggerPos', true), rememberLastTab: GM_getValue('cfg_rememberLastTab', true), windowOpacity: GM_getValue('svd_win_opacity', 1.0), // 下载 autoSaveCover: GM_getValue('cfg_autoSaveCover', false), filenameTemplate: GM_getValue('cfg_filenameTemplate', '{author}-{desc}'), notifyOnComplete: GM_getValue('cfg_notifyOnComplete', true), duplicateStrategy: GM_getValue('cfg_duplicateStrategy', 'rename'), // overwrite / rename / ask // 嗅探 minResolution: GM_getValue('cfg_minResolution', '0'), // 0 / 480 / 720 / 1080 linkTypeFilter: GM_getValue('cfg_linkTypeFilter', 'all'), // all / mp4 / m3u8 autoCopyFirst: GM_getValue('cfg_autoCopyFirst', false), showSniffPreview: GM_getValue('cfg_showSniffPreview', true), // 记忆数据 lastTab: GM_getValue('cfg_lastTab', 'main'), triggerPos: GM_getValue('cfg_triggerPos', null), // {left, top} }; function saveSetting(key, value) { SETTINGS[key] = value; // 维持 windowOpacity 的旧 key 兼容 var storeKey = key === 'windowOpacity' ? 'svd_win_opacity' : ('cfg_' + key); GM_setValue(storeKey, value); } /* ══════════════════════════════════════════════════════════ 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); var sniffFirstCopied = 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); }); } /** 通知(带回退)*/ function notify(title, text) { if (!SETTINGS.notifyOnComplete) return; try { if (typeof GM_notification === 'function') { GM_notification({ title: title, text: text, timeout: 4000, silent: false }); return; } } catch(e) {} // 回退到浏览器原生 try { if ('Notification' in window && Notification.permission === 'granted') { new Notification(title, { body: text }); } } catch(e) {} } /* ══════════════════════════════════════════════════════════ 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 ''; } /** 从 URL 推断分辨率(粗略)*/ function getUrlResolution(u) { u = u.toLowerCase(); if (u.includes('8k') || u.includes('4320')) return 4320; if (u.includes('4k') || u.includes('2160')) return 2160; if (u.includes('1080') || u.includes('1920')) return 1080; if (u.includes('720')) return 720; if (u.includes('480')) return 480; if (u.includes('360')) return 360; return 0; // 未知 } /** 判断链接类型 */ function getLinkType(u) { if (/\.m3u8(\?|$)/i.test(u)) return 'm3u8'; if (/\.mp4(\?|$)/i.test(u)) return 'mp4'; return 'other'; } 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 (!isBiliBatch) { // 最低分辨率过滤 var minRes = parseInt(SETTINGS.minResolution, 10) || 0; if (minRes > 0) { var res = getUrlResolution(url); if (res > 0 && res < minRes) return; // 已知分辨率且低于阈值才过滤 } // 链接类型过滤 if (SETTINGS.linkTypeFilter !== 'all') { var lt = getLinkType(url); if (lt !== SETTINGS.linkTypeFilter) 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 (SETTINGS.autoCopyFirst && !sniffFirstCopied && !isBiliBatch) { sniffFirstCopied = true; copyToClipboard(url).then(function() { setStatus('🔗 已自动复制首条嗅探链接', 'ok'); }).catch(function() {}); } 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 rawTag = getResTag(item.url); var tagText = item.isBiliBatch ? '🎬 选集' : (rawTag ? rawTag.replace(/<[^>]+>/g, '').trim() : ''); var tagClass = item.isBiliBatch ? 'svd-tag-bili' : 'svd-tag-res'; var title = item.customTitle || item.url.split('?')[0].split('/').filter(Boolean).pop() || item.url.split('?')[0].substring(0, 50); var header = document.createElement('div'); header.className = 'svd-sniff-header'; header.innerHTML = (tagText ? '' + esc(tagText) + '' : '') + '' + esc(title) + '' + ''; header.addEventListener('click', function() { li.classList.toggle('collapsed'); }); var body = document.createElement('div'); body.className = 'svd-sniff-body'; // 视频预览(受设置控制) if (SETTINGS.showSniffPreview) { var vid = document.createElement('video'); vid.className = 'svd-sniff-video'; vid.preload = 'metadata'; vid.title = '点击播放 / 暂停'; vid.style.cursor = 'pointer'; vid.addEventListener('click', function() { if (vid.paused) vid.play().catch(function() {}); else vid.pause(); }); if ('IntersectionObserver' in window) { var obs = new IntersectionObserver(function(entries, o) { if (entries[0].isIntersecting) { vid.src = item.url; o.disconnect(); } }, { threshold: 0.05 }); obs.observe(vid); } else { vid.src = item.url; } body.appendChild(vid); } var footer = document.createElement('div'); footer.className = 'svd-sniff-footer'; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'svd-sniff-checkbox'; cb.dataset.url = item.url; cb.dataset.bili = item.isBiliBatch ? '1' : ''; cb.addEventListener('click', function(e) { e.stopPropagation(); }); var urlSnip = document.createElement('div'); urlSnip.className = 'svd-sniff-urlsnip'; try { urlSnip.textContent = new URL(item.url).hostname; } catch(e) { urlSnip.textContent = item.url.substring(0, 30); } var actions = document.createElement('div'); actions.className = 'svd-sniff-actions'; var copyBtn = document.createElement('button'); copyBtn.className = 'svd-sniff-copy'; copyBtn.textContent = '复制'; copyBtn.title = '复制链接'; copyBtn.onclick = function(e) { e.stopPropagation(); copyToClipboard(item.url) .then(function() { setStatus('链接已复制', 'ok'); }) .catch(function() { setStatus('复制失败', 'err'); }); }; var dlBtn = document.createElement('button'); dlBtn.className = 'svd-sniff-dl'; dlBtn.textContent = '下载'; dlBtn.title = '下载视频'; dlBtn.onclick = function(e) { e.stopPropagation(); downloadBestQuality(item.url, getUrlFilename(item.url), '嗅探视频'); }; actions.appendChild(copyBtn); actions.appendChild(dlBtn); footer.appendChild(cb); footer.appendChild(urlSnip); footer.appendChild(actions); body.appendChild(footer); li.appendChild(header); li.appendChild(body); list.prepend(li); updateSniffGrid(); } function updateSniffGrid() { var list = q('svd-m3u8-list'); if (!list) return; var count = list.children.length; var cols = Math.min(Math.max(count, 1), 4); list.className = 'svd-cols-' + cols; var empty = q('svd-sniff-empty'); if (empty) empty.style.display = count === 0 ? 'block' : 'none'; if (typeof updateMainTabBadge === 'function') updateMainTabBadge(); } 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); } } 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() { 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) {} 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); }; 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); } 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'); }; } 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() || ''; } 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 || []; 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) { 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 applyFilenameTemplate(template, info, type, idx) { var now = new Date(); var date = now.getFullYear() + ('0'+(now.getMonth()+1)).slice(-2) + ('0'+now.getDate()).slice(-2); var time = ('0'+now.getHours()).slice(-2) + ('0'+now.getMinutes()).slice(-2) + ('0'+now.getSeconds()).slice(-2); var map = { '{author}': sanitize(info.author || '未知'), '{desc}': sanitize((info.desc || '').substring(0, 30)), '{platform}': sanitize(info.platform || ''), '{date}': date, '{time}': time, '{datetime}': date + '_' + time, '{index}': (idx != null ? (idx + 1) : '') }; var out = template || '{author}-{desc}'; Object.keys(map).forEach(function(k) { out = out.split(k).join(map[k]); }); // 多余分隔符清理 out = out.replace(/[-_]+$/g, '').replace(/^[-_]+/g, '').replace(/[-_]{2,}/g, '-'); return out || '未命名'; } function getFilename(info, type, idx) { // 优先级: 用户输入框 > 模板 var customName = ''; var inputEl = q('svd-filename-input'); if (inputEl) customName = inputEl.value.trim(); var baseName; if (customName) { baseName = sanitize(customName); } else { baseName = applyFilenameTemplate(SETTINGS.filenameTemplate, info || {author:'未知',desc:'视频'}, type, idx); } if (type === 'video') return baseName + '.mp4'; if (type === 'cover') return baseName + '_封面.jpg'; if (type === 'image') return baseName + '_图文第' + (idx + 1) + '张.jpg'; return baseName + '.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(success) { // 视频下载完成,如开启则自动下载封面 if (success && SETTINGS.autoSaveCover && videoInfo && videoInfo.coverUrl) { setTimeout(function() { doDownload(videoInfo.coverUrl, getFilename(videoInfo, 'cover'), '附带封面'); }, 500); } }); }); } 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 quickDownloadCurrent() { if (isDownloading) { setStatus('下载进行中...', 'info'); return; } if (isDyOrXhs()) { // 确保信息已读取 if (!videoInfo) { videoInfo = extractVideoInfo(); if (videoInfo) renderInfo(videoInfo); } if (!videoInfo) { setStatus('❌ 未读取到视频信息,请刷新一次', 'err'); return; } if (videoInfo.isImagePost) { setStatus('当前为图文,请在主面板下载图片', 'info'); return; } doDownloadVideo(); } 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 (r.ok) notify('图片已保存', filename); 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, onAfter) { 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) { var msg = r.ok ? (r.method === 'fs' ? '已存入目录: ' + (r.finalName || filename) : '已触发下载: ' + (r.finalName || filename)) : r.error; finishDL(r.ok, msg); if (r.ok) notify('下载完成', (r.finalName || filename)); if (onAfter) onAfter(r.ok); }); } else { finishDL(false, 'HTTP ' + res.status); if (onAfter) onAfter(false); } }, onerror: function(e) { finishDL(false, e.error || '网络错误'); if (onAfter) onAfter(false); } }); } function finishDL(ok, msg) { isDownloading = false; setProgress(0); setBtnsEnabled(true); setStatus((ok ? '✅ ' : '❌ ') + msg, ok ? 'ok' : 'err'); } /** 检查目录中是否已存在同名文件,返回最终可用文件名 */ async function resolveDuplicateName(targetDir, finalName) { var strategy = SETTINGS.duplicateStrategy; // 先看是否存在 var exists = false; try { await targetDir.getFileHandle(finalName); exists = true; } catch(e) { exists = false; } if (!exists) return { name: finalName, proceed: true }; if (strategy === 'overwrite') return { name: finalName, proceed: true }; if (strategy === 'ask') { var keep = window.confirm('文件已存在:\n' + finalName + '\n\n确定 = 覆盖 取消 = 自动加序号'); if (keep) return { name: finalName, proceed: true }; // 取消则回退到自动重命名 } // 自动加序号 (1), (2)... var dot = finalName.lastIndexOf('.'); var base = dot > 0 ? finalName.substring(0, dot) : finalName; var ext = dot > 0 ? finalName.substring(dot) : ''; for (var i = 1; i < 200; i++) { var candidate = base + ' (' + i + ')' + ext; try { await targetDir.getFileHandle(candidate); } catch(e) { return { name: candidate, proceed: true }; } } return { name: finalName, proceed: true }; } 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 resolved = await resolveDuplicateName(cur, finalName); if (!resolved.proceed) return { ok: false, error: '用户取消' }; finalName = resolved.name; 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', finalName: finalName }; } 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', finalName: filename }); }, onerror: function(e) { URL.revokeObjectURL(bu); res({ ok: false, error: e.error || '失败' }); } }); }); } /* ══════════════════════════════════════════════════════════ UI — THEMING (深色模式 + CSS 变量) ══════════════════════════════════════════════════════════ */ /** 亮色主题色板 */ var THEMES = { light: { bg: '#ffffff', bg2: '#f7f8fa', bg3: '#eef0f3', 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 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)' }, dark: { bg: '#1c1d20', bg2: '#26272b', bg3: '#33343a', border: '#3a3b40', border2: '#4a4b52', text: '#e8eaed', text2: '#a8aab0', text3: '#6e7077', accent: '#ff4b6e', accentBg: 'rgba(255,75,110,0.14)', purple: '#7e87ff', green: '#34d399', red: '#f87171', blue: '#60a5fa', shadow: '0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3)' } }; function currentThemeName() { if (SETTINGS.themeMode === 'light') return 'light'; if (SETTINGS.themeMode === 'dark') return 'dark'; // auto try { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } catch(e) { return 'light'; } } function applyTheme() { var name = currentThemeName(); var t = THEMES[name]; var root = document.documentElement; Object.keys(t).forEach(function(k) { root.style.setProperty('--svd-' + k, t[k]); }); // 给 window 加 class 以便部分元素针对深色覆写 var win = q('svd-win'); if (win) { win.classList.toggle('svd-dark', name === 'dark'); win.classList.toggle('svd-light', name === 'light'); } } // 监听系统主题变化 try { if (window.matchMedia) { var mql = window.matchMedia('(prefers-color-scheme: dark)'); if (mql.addEventListener) mql.addEventListener('change', function() { if (SETTINGS.themeMode === 'auto') applyTheme(); }); else if (mql.addListener) mql.addListener(function() { if (SETTINGS.themeMode === 'auto') applyTheme(); }); } } catch(e) {} /* ══════════════════════════════════════════════════════════ UI — STYLES ══════════════════════════════════════════════════════════ */ function injectStyles() { if (q('svd-styles')) return; var s = document.createElement('style'); s.id = 'svd-styles'; s.textContent = [ // CSS 变量初始默认值(applyTheme 会覆盖) ':root{--svd-bg:#ffffff;--svd-bg2:#f7f8fa;--svd-bg3:#eef0f3;--svd-border:#e4e6ea;--svd-border2:#ced0d4;--svd-text:#1c1e21;--svd-text2:#65676b;--svd-text3:#a8aaad;--svd-accent:#fe2c55;--svd-accentBg:rgba(254,44,85,0.08);--svd-purple:#6366f1;--svd-green:#16a34a;--svd-red:#dc2626;--svd-blue:#2563eb;--svd-shadow:0 8px 32px rgba(0,0,0,0.12),0 2px 8px rgba(0,0,0,0.06)}', // 窗口容器 '#svd-win{position:fixed;top:58px;right:18px;width:420px;height:640px;max-height:92vh;display:flex;flex-direction:column;resize:both;overflow:hidden;min-width:340px;max-width:720px;min-height:380px;background:var(--svd-bg);border:1px solid var(--svd-border);border-radius:14px;box-shadow:var(--svd-shadow);z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;font-size:13px;color:var(--svd-text)}', // 顶栏 '#svd-hdr{flex-shrink:0;display:flex;align-items:center;gap:10px;padding:10px 14px;background:linear-gradient(180deg,var(--svd-bg) 0%,var(--svd-bg2) 100%);border-bottom:1px solid var(--svd-border);border-radius:14px 14px 0 0;cursor:move;user-select:none}', '.svd-badge{width:26px;height:26px;border-radius:8px;background:linear-gradient(135deg,var(--svd-accent) 0%,#ff6b8b 100%);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 8px rgba(254,44,85,.35)}', '.svd-badge svg{width:14px;height:14px;fill:#fff}', '.svd-title{flex:1;font-weight:700;font-size:13.5px;color:var(--svd-text);letter-spacing:-.2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svd-title em{color:var(--svd-accent);font-style:normal;font-weight:800}', '.svd-wbtns{display:flex;gap:5px;align-items:center}', '.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,filter .15s}.svd-wbtn:hover{transform:scale(1.12);filter:brightness(1.05)}', '.svd-wbtn-x{background:#fd5f57;color:#5a0000}', // Tab 导航 '#svd-tabs{flex-shrink:0;display:flex;padding:0 8px;background:var(--svd-bg);border-bottom:1px solid var(--svd-border);gap:2px;position:relative}', '.svd-tab{flex:1;padding:10px 4px;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;color:var(--svd-text3);display:flex;align-items:center;justify-content:center;gap:5px;position:relative;transition:color .18s;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap}', '.svd-tab:hover{color:var(--svd-text2)}', '.svd-tab.active{color:var(--svd-accent);border-bottom-color:var(--svd-accent)}', '.svd-tab-ico{font-size:13px;line-height:1}', '.svd-tab-badge{position:absolute;top:6px;right:4px;min-width:14px;height:14px;padding:0 4px;border-radius:7px;background:var(--svd-accent);color:#fff;font-size:9px;font-weight:700;display:none;align-items:center;justify-content:center;line-height:1}', '.svd-tab-badge.show{display:flex}', // Tab 内容区 '#svd-body{flex:1;overflow-y:auto;overflow-x:hidden;background:var(--svd-bg);scrollbar-width:thin;scrollbar-color:var(--svd-border2) transparent}', '#svd-body::-webkit-scrollbar{width:5px}#svd-body::-webkit-scrollbar-thumb{background:var(--svd-border2);border-radius:3px}#svd-body::-webkit-scrollbar-thumb:hover{background:var(--svd-text3)}', '.svd-pane{display:none;animation:svd-fadeIn .2s ease-out}', '.svd-pane.active{display:block}', '@keyframes svd-fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}', // section '.svd-sec{padding:12px 14px;background:var(--svd-bg)}', '.svd-sec+.svd-sec{border-top:1px solid var(--svd-bg3)}', '.svd-sec-lbl{font-size:10px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--svd-text3);margin-bottom:8px;display:flex;align-items:center;gap:5px}', // 保存路径 '#svd-path-row{display:flex;align-items:center;gap:8px}', '#svd-path-txt{flex:1;padding:8px 11px;background:var(--svd-bg2);border:1px solid var(--svd-border);border-radius:8px;color:var(--svd-text3);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}', '#svd-path-txt.set{color:var(--svd-green);border-color:rgba(22,163,74,.3);background:rgba(22,163,74,.06)}', '#svd-path-btn{padding:8px 13px;background:linear-gradient(135deg,var(--svd-purple) 0%,#818cf8 100%);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:transform .15s,box-shadow .15s;flex-shrink:0}', '#svd-path-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(99,102,241,.4)}', '#svd-filename-input{width:100%;padding:9px 11px;border:1px solid var(--svd-border);border-radius:8px;background:var(--svd-bg2);color:var(--svd-text);font-size:12px;box-sizing:border-box;transition:border-color .15s,background .15s;outline:none}', '#svd-filename-input:focus{border-color:var(--svd-accent);background:var(--svd-bg)}', '#svd-filename-input::placeholder{color:var(--svd-text3)}', // 设置项行 '.svd-set-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 0;flex-wrap:wrap}', '.svd-set-row-lbl{font-size:12px;color:var(--svd-text2);font-weight:500;flex:1;min-width:120px}', '.svd-set-row-lbl small{display:block;margin-top:2px;color:var(--svd-text3);font-size:10.5px;font-weight:400}', '#svd-opacity-slider{-webkit-appearance:none;appearance:none;flex:1;max-width:160px;height:4px;background:var(--svd-border2);border-radius:10px;outline:none;cursor:pointer}', '#svd-opacity-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;background:var(--svd-accent);border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.2);border:2px solid #fff}', '#svd-opacity-slider::-moz-range-thumb{width:14px;height:14px;background:var(--svd-accent);border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.2);border:2px solid #fff}', // 通用 select / 单选段 '.svd-select{padding:5px 8px;border:1px solid var(--svd-border);border-radius:6px;background:var(--svd-bg);color:var(--svd-text);font-size:11.5px;cursor:pointer;outline:none;min-width:90px}', '.svd-select:focus{border-color:var(--svd-accent)}', '.svd-seg{display:inline-flex;border:1px solid var(--svd-border);border-radius:7px;overflow:hidden;background:var(--svd-bg2)}', '.svd-seg button{padding:5px 10px;border:none;background:transparent;color:var(--svd-text2);font-size:11px;cursor:pointer;font-weight:500;transition:background .15s,color .15s;border-right:1px solid var(--svd-border)}', '.svd-seg button:last-child{border-right:none}', '.svd-seg button:hover{background:var(--svd-bg3)}', '.svd-seg button.active{background:var(--svd-accent);color:#fff;font-weight:600}', // 开关 switch '.svd-switch{position:relative;width:36px;height:20px;background:var(--svd-border2);border-radius:20px;cursor:pointer;transition:background .2s;flex-shrink:0}', '.svd-switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.2)}', '.svd-switch.on{background:var(--svd-accent)}', '.svd-switch.on::after{transform:translateX(16px)}', // 危险按钮 '.svd-danger-btn{padding:7px 14px;background:var(--svd-bg2);border:1px solid var(--svd-red);color:var(--svd-red);border-radius:7px;font-size:11.5px;font-weight:600;cursor:pointer;transition:background .15s}', '.svd-danger-btn:hover{background:rgba(220,38,38,.08)}', // 模板示例 '.svd-template-hint{margin-top:6px;font-size:10.5px;color:var(--svd-text3);line-height:1.6}', '.svd-template-hint code{padding:1px 4px;background:var(--svd-bg2);border:1px solid var(--svd-border);border-radius:3px;font-size:10px;color:var(--svd-accent);cursor:pointer}', '.svd-template-hint code:hover{background:var(--svd-bg3)}', // 抖音/XHS:封面状态条 '#svd-capbar{display:flex;align-items:center;gap:8px;padding:9px 14px;background:var(--svd-bg2);border-bottom:1px solid var(--svd-bg3);font-size:11px}', '.svd-dot{width:7px;height:7px;border-radius:50%;background:var(--svd-text3);flex-shrink:0;transition:background .3s}', '.svd-dot.on{background:var(--svd-green);box-shadow:0 0 6px rgba(22,163,74,.6);animation:svd-pulse 1.5s infinite}', '@keyframes svd-pulse{0%,100%{opacity:1}50%{opacity:.45}}', '#svd-captxt{flex:1;color:var(--svd-text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}#svd-captxt.on{color:var(--svd-green);font-weight:500}', '#svd-refresh{flex-shrink:0;padding:5px 11px;background:var(--svd-bg);border:1px solid var(--svd-border);border-radius:7px;color:var(--svd-text2);font-size:11px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:background .15s,border-color .15s,color .15s,transform .15s}', '#svd-refresh:hover{background:var(--svd-accentBg);border-color:rgba(254,44,85,.3);color:var(--svd-accent);transform:translateY(-1px)}', // 顶部配置区 '.svd-top-config{padding:12px 14px;background:var(--svd-bg);border-bottom:1px solid var(--svd-bg3);display:flex;flex-direction:column;gap:8px}', // 信息卡 '.svd-info-card{margin:8px 14px 0;padding:10px 11px;background:linear-gradient(135deg,var(--svd-bg2) 0%,var(--svd-bg) 100%);border:1px solid var(--svd-bg3);border-radius:10px;display:flex;flex-direction:column;gap:7px}', '.svd-info-meta{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--svd-text2);flex-wrap:wrap}', '.svd-meta-item{display:inline-flex;align-items:center;gap:3px}', '.svd-meta-ico{font-size:10px;opacity:.7}', '.svd-meta-sep{color:var(--svd-border2);font-weight:700}', '.svd-info-desc-block{padding-top:7px;border-top:1px dashed rgba(128,128,128,.18);cursor:pointer;user-select:none}', '.svd-info-desc-block:hover .svd-desc-chevron{color:var(--svd-accent)}', '.svd-desc-lbl{display:flex;align-items:center;justify-content:space-between;font-size:9.5px;font-weight:700;letter-spacing:.6px;color:var(--svd-text3);margin-bottom:4px;text-transform:uppercase}', '.svd-desc-chevron{font-size:9px;color:var(--svd-text3);transition:transform .2s,color .15s;font-weight:400}', '.svd-info-desc-block.expanded .svd-desc-chevron{transform:rotate(180deg);color:var(--svd-accent)}', // 媒体行 '.svd-media-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:10px 14px 0;min-width:0}', '.svd-media-col{display:flex;flex-direction:column;gap:6px;min-width:0;overflow:hidden}', '.svd-media-lbl{font-size:10.5px;font-weight:700;letter-spacing:.8px;color:var(--svd-text3);text-transform:uppercase;display:flex;align-items:center;gap:4px}', '.svd-media-box{position:relative;background:var(--svd-bg2);border-radius:10px;overflow:hidden;min-height:100px;display:flex;align-items:center;justify-content:center}', // 链接行 '.svd-link-row{display:flex;align-items:flex-start;gap:6px;padding:10px 14px 0;font-size:10.5px}', '.svd-link-ico{flex-shrink:0;padding-top:5px;font-size:11px;opacity:.7}', // 作者 '.svd-ac{display:flex;align-items:center;gap:8px}', '.svd-av{width:30px;height:30px;border-radius:50%;object-fit:cover;flex-shrink:0;border:1.5px solid var(--svd-accentBg)}', '.svd-avph{width:30px;height:30px;border-radius:50%;background:var(--svd-bg3);display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}', '.svd-aname{font-size:12px;font-weight:600;color:var(--svd-text);line-height:1.3}.svd-aid{font-size:10.5px;color:var(--svd-text3);margin-top:1px}', // 文案 '.svd-desc{font-size:11.5px;color:var(--svd-text);line-height:1.5;word-break:break-word;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:max-height .2s ease}', '.svd-info-desc-block.expanded .svd-desc{white-space:normal;overflow-y:auto;max-height:88px;scrollbar-width:thin}', '.svd-info-desc-block.expanded .svd-desc::-webkit-scrollbar{width:3px}.svd-info-desc-block.expanded .svd-desc::-webkit-scrollbar-thumb{background:var(--svd-border2);border-radius:3px}', // 话题 '.svd-tags{display:flex;flex-wrap:wrap;gap:4px}', '.svd-tag{padding:2px 8px;background:var(--svd-accentBg);border:1px solid rgba(254,44,85,.2);border-radius:100px;color:var(--svd-accent);font-size:11px;font-weight:500}', // 封面/视频 '.svd-cimg{width:100%;max-height:140px;object-fit:cover;display:block;cursor:pointer;transition:opacity .2s}.svd-cimg:hover{opacity:.85}', '.svd-cph{width:100%;padding:24px 8px;color:var(--svd-text3);font-size:11px;text-align:center}', '.svd-vid{width:100%;max-height:200px;background:#000;display:block;object-fit:contain}', '.svd-imgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;padding:6px}', '.svd-ig-item{position:relative;border-radius:6px;overflow:hidden;cursor:pointer;background:var(--svd-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:4px;right:4px;background:rgba(255,255,255,.92);backdrop-filter:blur(4px);border:none;border-radius:5px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:11px;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:4px;left:5px;background:rgba(0,0,0,.5);color:#fff;font-size:9px;font-weight:600;padding:1px 4px;border-radius:3px}', '.svd-imgdl-all{margin:6px;width:calc(100% - 12px);padding:7px;background:var(--svd-bg);border:1px solid var(--svd-border);border-radius:7px;color:var(--svd-text2);font-size:11px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;transition:background .15s}', '.svd-imgdl-all:hover{background:var(--svd-bg3)}.svd-imgdl-all:disabled{opacity:.4;cursor:not-allowed}', // 链接 '.svd-url{flex:1;font-size:10.5px;color:var(--svd-text3);word-break:break-all;background:var(--svd-bg2);border-radius:6px;padding:6px 9px;max-height:36px;overflow:hidden;min-width:0}', '.svd-url.found{color:var(--svd-green);background:rgba(22,163,74,.07)}', '.svd-na{color:var(--svd-text3);font-style:italic;font-size:11.5px}', // 主下载按钮 '#svd-actions{display:flex;gap:8px;padding:10px 14px 14px;background:var(--svd-bg)}', '.svd-abtn{flex:1;padding:11px 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,filter .15s}', '.svd-abtn:hover{transform:translateY(-1px);filter:brightness(1.05)}.svd-abtn:disabled{opacity:.38;cursor:not-allowed;transform:none;filter:none}', '.svd-dlv{background:linear-gradient(135deg,var(--svd-accent) 0%,#ff5878 100%);color:#fff;box-shadow:0 2px 8px rgba(254,44,85,.3)}.svd-dlv:hover:not(:disabled){box-shadow:0 4px 14px rgba(254,44,85,.4)}', '.svd-dlc{background:var(--svd-bg2);color:var(--svd-text2);border:1.5px solid var(--svd-border)}.svd-dlc:hover:not(:disabled){background:var(--svd-bg3);border-color:var(--svd-border2)}', // 底部状态栏 '#svd-statarea{flex-shrink:0;padding:9px 14px 10px;background:var(--svd-bg2);border-top:1px solid var(--svd-border)}', '#svd-prog{height:3px;background:var(--svd-bg3);border-radius:2px;margin-bottom:6px;overflow:hidden;display:none}', '#svd-progfill{height:100%;width:0%;background:linear-gradient(90deg,var(--svd-accent),#ff6b8b);border-radius:2px;transition:width .2s}', '#svd-stattxt{font-size:11px;color:var(--svd-text3);text-align:center;min-height:14px;line-height:1.4}', '#svd-stattxt.ok{color:var(--svd-green)}#svd-stattxt.err{color:var(--svd-red)}#svd-stattxt.info{color:var(--svd-blue)}', // 触发球 '#svd-trig{position:fixed;bottom:108px;right:18px;width:46px;height:46px;border-radius:50%;background:linear-gradient(135deg,var(--svd-accent) 0%,#ff6b8b 100%);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-author-bar{padding:14px;display:flex;align-items:center;justify-content:space-between;font-size:11.5px;border-bottom:1px solid var(--svd-bg3)}', '.svd-author{color:var(--svd-text2)}.svd-author a{color:var(--svd-accent);text-decoration:none;font-weight:600}', '.svd-donate{color:#fff;cursor:pointer;font-weight:600;background:linear-gradient(135deg,var(--svd-accent) 0%,#ff6b8b 100%);padding:5px 12px;border-radius:14px;transition:transform .15s;box-shadow:0 2px 6px rgba(254,44,85,.3)}', '.svd-donate:hover{transform:translateY(-1px)}', '.svd-about-feature{padding:6px 14px 14px;font-size:11.5px;color:var(--svd-text2);line-height:1.7}', '.svd-about-feature strong{color:var(--svd-text);font-weight:600;display:block;margin:8px 0 4px;font-size:12px}', '.svd-about-feature strong:first-child{margin-top:0}', '.svd-kbd{display:inline-block;padding:1px 6px;background:var(--svd-bg2);border:1px solid var(--svd-border);border-bottom-width:2px;border-radius:4px;font-family:monospace;font-size:10.5px;color:var(--svd-text);margin:0 2px}', '#svd-donate-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:2147483648;display:none;align-items:center;justify-content:center}', '#svd-donate-content{background:var(--svd-bg);color:var(--svd-text);padding:24px 30px;border-radius:20px;text-align:center;max-width:360px;box-shadow:0 10px 40px rgba(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:var(--svd-accent);color:#fff;border:none;border-radius:20px;cursor:pointer}', // 嗅探 '#svd-sniff-section{padding:12px 14px;display:block}', '#svd-sniff-section.visible{display:block}', '#svd-sniff-toolbar{display:flex;gap:5px;margin-bottom:10px}', '#svd-sniff-toolbar button{flex:1;padding:7px 4px;border:none;border-radius:7px;cursor:pointer;font-size:10.5px;font-weight:700;color:#fff;background:#6b7280;transition:opacity .15s,transform .15s}', '#svd-sniff-toolbar button:hover{opacity:.88;transform:translateY(-1px)}', '#svd-sniff-clear-btn{background:#e74c3c !important}', '#svd-sniff-empty{text-align:center;padding:30px 16px;color:var(--svd-text3);font-size:12px;line-height:1.7}', '#svd-sniff-empty .svd-empty-ico{font-size:32px;margin-bottom:8px;display:block;opacity:.5}', '#svd-m3u8-list{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:1fr;gap:8px;align-items:start}', '#svd-m3u8-list.svd-cols-2{grid-template-columns:repeat(2,1fr)}', '#svd-m3u8-list.svd-cols-3{grid-template-columns:repeat(3,1fr)}', '#svd-m3u8-list.svd-cols-4{grid-template-columns:repeat(4,1fr)}', '#svd-m3u8-list.svd-cols-3 .svd-sniff-urlsnip,#svd-m3u8-list.svd-cols-4 .svd-sniff-urlsnip{display:none}', '#svd-m3u8-list.svd-cols-3 .svd-sniff-header,#svd-m3u8-list.svd-cols-4 .svd-sniff-header{padding:5px 6px;gap:3px}', '#svd-m3u8-list.svd-cols-3 .svd-sniff-footer,#svd-m3u8-list.svd-cols-4 .svd-sniff-footer{padding:4px 5px;gap:3px}', '#svd-m3u8-list.svd-cols-3 .svd-sniff-tag,#svd-m3u8-list.svd-cols-4 .svd-sniff-tag{display:none}', '#svd-m3u8-list.svd-cols-4 .svd-sniff-chevron{display:none}', '#svd-m3u8-list.svd-cols-2 .svd-sniff-actions button{font-size:11px;padding:5px 8px}', '#svd-m3u8-list.svd-cols-3 .svd-sniff-actions button{font-size:10px;padding:4px 6px}', '#svd-m3u8-list.svd-cols-4 .svd-sniff-actions button{font-size:9px;padding:3px 4px}', '.svd-m3u8-item{background:var(--svd-bg);border:1px solid var(--svd-border);border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.07)}', '.svd-sniff-header{display:flex;align-items:center;gap:6px;padding:8px 10px;background:var(--svd-bg2);cursor:pointer;user-select:none;border-bottom:1px solid var(--svd-border)}', '.svd-sniff-header:hover{background:var(--svd-bg3)}', '.svd-sniff-tag{display:inline-flex;align-items:center;padding:2px 6px;border-radius:20px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0}', '.svd-tag-bili{background:rgba(251,114,153,.12);color:#fb7299;border:1px solid rgba(251,114,153,.3)}', '.svd-tag-res{background:rgba(99,102,241,.10);color:var(--svd-purple);border:1px solid rgba(99,102,241,.25)}', '.svd-sniff-title{flex:1;font-size:11px;font-weight:600;color:var(--svd-text);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}', '.svd-sniff-chevron{font-size:10px;color:var(--svd-text3);flex-shrink:0;transition:transform .2s;line-height:1}', '.svd-m3u8-item.collapsed .svd-sniff-chevron{transform:rotate(-90deg)}', '.svd-m3u8-item.collapsed .svd-sniff-body{display:none}', '.svd-sniff-video{width:100%;height:auto;display:block;background:#111;object-fit:contain}', '.svd-sniff-footer{display:flex;align-items:center;gap:6px;padding:7px 10px}', '.svd-sniff-checkbox{flex-shrink:0;width:14px;height:14px;cursor:pointer;accent-color:var(--svd-accent)}', '.svd-sniff-urlsnip{flex:1;font-size:10.5px;color:var(--svd-text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}', '.svd-sniff-actions{display:flex;gap:4px;flex-shrink:0}', '.svd-sniff-actions button{padding:5px 10px;font-size:11px;font-weight:600;border:none;border-radius:6px;cursor:pointer;white-space:nowrap;transition:opacity .15s;display:flex;align-items:center;gap:3px}', '.svd-sniff-actions button:hover{opacity:.82}', '.svd-sniff-copy{background:#3498db;color:#fff}.svd-sniff-dl{background:#27ae60;color:#fff}', ].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 topConfigHtml = [ '
', '
', '
未选择目录,将使用浏览器默认下载
', ' ', '
', ' ', '
' ].join(''); var mainPaneHtml; if (isDouyinOrXhs) { mainPaneHtml = [ '
', topConfigHtml, '
', '
', ' 等待封面加载...', ' ', '
', '
', '
点击「刷新」读取视频信息
', '
', ' 📱', ' ·', ' 🕐', '
', '
', '
📝 发布文案▾ 展开
', '
', '
', ' ', '
', '
', '
', '
🖼 封面
', '
等待封面...
', '
', '
', '
视频
', '
', '
', '
', ' ', '
', ' ', ' ', '
', '
' ].join(''); } else { mainPaneHtml = [ '
', topConfigHtml, '
', '
', ' ', ' ', ' ', ' ', (isBili() ? ' ' : ''), '
', ' ', '
📡暂未捕获到视频链接
播放页面视频后将自动嗅探
', '
', '
' ].join(''); } /* ══════ 设置 Tab(大幅增强)══════ */ var settingsPaneHtml = [ '
', // —— 外观 —— '
', '
🎨 外观
', '
', ' 主题模式跟随系统会自动响应深色切换', '
', ' ', ' ', ' ', '
', '
', '
', ' 窗口透明度', ' ', '
', '
', ' 记住触发球位置关闭后下次仍保持在你拖到的地方', '
', '
', '
', ' 记住最后打开的 Tab', '
', '
', '
', // —— 下载 —— '
', '
📥 下载
', '
', ' 下载视频时同时保存封面', '
', '
', '
', ' 下载完成通知', '
', '
', '
', ' 同名文件处理', ' ', '
', '
', ' 文件名模板主页输入框留空时启用', ' ', '
', ' 可用占位符(点击插入):', ' {author}', ' {desc}', ' {platform}', ' {date}', ' {time}', ' {datetime}', '
', '
', '
', // —— 嗅探 —— '
', '
📡 嗅探
', '
', ' 最低分辨率过滤已知分辨率且低于阈值的链接被忽略', ' ', '
', '
', ' 链接类型筛选', '
', ' ', ' ', ' ', '
', '
', '
', ' 嗅探到首条自动复制本次会话仅触发一次', '
', '
', '
', ' 嗅探卡片显示视频预览关闭可省流量内存', '
', '
', '
', // —— 重置 —— '
', '
🧹 维护
', '
', ' 清空当前嗅探列表', ' ', '
', '
', ' 重置所有设置为默认', ' ', '
', '
', '
' ].join(''); var aboutPaneHtml = [ '
', '
', ' By 花海🌸 · v3.4.0', ' ❤️ 赞赏支持', '
', '
', ' 🎯 支持平台', ' 抖音、小红书网页版无水印下载;全站 M3U8 / MP4 视频嗅探。', ' 📥 主要功能', ' 一键下载无水印视频、封面图;图文笔记多图批量保存;自动提取作者 / 标题 / 发布时间;B站合集批量扫描。', ' ⌨️ 快捷键', ' Alt + D 唤起 / 隐藏小窗', '
Alt + S 快速下载当前视频', '
双击触发球 = 快速下载', ' 💡 使用提示', ' 设置中选择本地目录后,文件会直接保存至所选位置;否则走浏览器默认下载。', '
', '
' ].join(''); var mainTabLabel = isDouyinOrXhs ? '视频' : '嗅探'; var mainTabIco = isDouyinOrXhs ? '📥' : '📡'; w.innerHTML = [ '
', '
', ' ' + platformTitle + '', '
', '
', '
', ' ', ' ', ' ', '
', '
', mainPaneHtml, settingsPaneHtml, aboutPaneHtml, '
', '
', '
', '
' + (isDouyinOrXhs ? '点击「刷新」读取当前视频信息' : '📡 嗅探面板已开启,播放视频后自动捕获链接') + '
', '
' ].join(''); document.body.appendChild(w); bindEvents(w); bindTabs(w); applySettingsToUI(w); makeDraggable(w, w.querySelector('#svd-hdr')); createDonateModal(); applyTheme(); if (!isDouyinOrXhs) { isSniffSectionVisible = true; GM_setValue('sniff_section_visible', true); } sniffMemoryVault.slice().reverse().forEach(function(item) { renderSingleSniffItem(item); }); // 恢复上次 Tab if (SETTINGS.rememberLastTab && SETTINGS.lastTab && SETTINGS.lastTab !== 'main') { var tabBtn = w.querySelector('.svd-tab[data-tab="' + SETTINGS.lastTab + '"]'); if (tabBtn) tabBtn.click(); } } /** 把当前 SETTINGS 应用到刚建好的 UI 控件上 */ function applySettingsToUI(w) { // 主题分段 var segTheme = w.querySelectorAll('#svd-seg-theme button'); Array.prototype.forEach.call(segTheme, function(b) { b.classList.toggle('active', b.dataset.val === SETTINGS.themeMode); }); // 链接类型分段 var segLink = w.querySelectorAll('#svd-seg-linktype button'); Array.prototype.forEach.call(segLink, function(b) { b.classList.toggle('active', b.dataset.val === SETTINGS.linkTypeFilter); }); // 开关 var swMap = { 'svd-sw-trigpos': 'rememberTriggerPos', 'svd-sw-lasttab': 'rememberLastTab', 'svd-sw-autocover':'autoSaveCover', 'svd-sw-notify': 'notifyOnComplete', 'svd-sw-autocopy': 'autoCopyFirst', 'svd-sw-preview': 'showSniffPreview' }; Object.keys(swMap).forEach(function(id) { var el = w.querySelector('#' + id); if (el) el.classList.toggle('on', !!SETTINGS[swMap[id]]); }); // 下拉 var selDup = w.querySelector('#svd-sel-dup'); if (selDup) selDup.value = SETTINGS.duplicateStrategy; var selMin = w.querySelector('#svd-sel-minres'); if (selMin) selMin.value = SETTINGS.minResolution; // 模板 var tplIn = w.querySelector('#svd-tpl-input'); if (tplIn) tplIn.value = SETTINGS.filenameTemplate; } function bindTabs(w) { var tabs = w.querySelectorAll('.svd-tab'); var panes = w.querySelectorAll('.svd-pane'); Array.prototype.forEach.call(tabs, function(tab) { tab.addEventListener('click', function() { var target = tab.getAttribute('data-tab'); Array.prototype.forEach.call(tabs, function(t) { t.classList.toggle('active', t === tab); }); Array.prototype.forEach.call(panes, function(p) { p.classList.toggle('active', p.getAttribute('data-pane') === target); }); if (SETTINGS.rememberLastTab) saveSetting('lastTab', target); }); }); } function updateMainTabBadge() { var badge = q('svd-tab-badge-main'); if (!badge) return; if (isDyOrXhs()) { badge.classList.remove('show'); return; } var n = detectedUrls.size; if (n > 0) { badge.textContent = n > 99 ? '99+' : n; badge.classList.add('show'); } else badge.classList.remove('show'); } function buildTrigger() { if (q('svd-trig')) return; var btn = document.createElement('button'); btn.id = 'svd-trig'; btn.title = '打开下载助手(可拖拽 / 双击快下)'; btn.innerHTML = ''; // 恢复位置(若开启了记忆) if (SETTINGS.rememberTriggerPos && SETTINGS.triggerPos) { try { var p = SETTINGS.triggerPos; if (typeof p === 'string') p = JSON.parse(p); if (p && typeof p.left === 'number' && typeof p.top === 'number') { btn.style.left = Math.max(0, Math.min(p.left, window.innerWidth - 46)) + 'px'; btn.style.top = Math.max(0, Math.min(p.top, window.innerHeight - 46)) + 'px'; btn.style.right = 'auto'; btn.style.bottom = 'auto'; } } catch(e) {} } var _dragging = false, _dragMoved = false, _sx, _sy, _ox, _oy; var _lastClickTs = 0; function _trigDragStart(clientX, clientY) { _dragging = true; _dragMoved = false; _sx = clientX; _sy = clientY; var r = btn.getBoundingClientRect(); _ox = r.left; _oy = r.top; } function _trigDragMove(clientX, clientY) { if (!_dragging) return; var dx = clientX - _sx, dy = clientY - _sy; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) _dragMoved = true; if (!_dragMoved) return; btn.style.left = Math.max(0, Math.min(_ox + dx, window.innerWidth - btn.offsetWidth)) + 'px'; btn.style.top = Math.max(0, Math.min(_oy + dy, window.innerHeight - btn.offsetHeight)) + 'px'; btn.style.right = 'auto'; btn.style.bottom = 'auto'; } function _trigDragEnd() { _dragging = false; // 拖完保存位置 if (_dragMoved && SETTINGS.rememberTriggerPos) { var r = btn.getBoundingClientRect(); saveSetting('triggerPos', { left: r.left, top: r.top }); } } btn.addEventListener('mousedown', function(e) { _trigDragStart(e.clientX, e.clientY); document.addEventListener('mousemove', _onMouseMove); document.addEventListener('mouseup', _onMouseUp); e.preventDefault(); }); function _onMouseMove(e) { _trigDragMove(e.clientX, e.clientY); } function _onMouseUp() { _trigDragEnd(); document.removeEventListener('mousemove', _onMouseMove); document.removeEventListener('mouseup', _onMouseUp); } btn.addEventListener('touchstart', function(e) { var t = e.touches[0]; _trigDragStart(t.clientX, t.clientY); e.preventDefault(); }, { passive: false }); btn.addEventListener('touchmove', function(e) { var t = e.touches[0]; _trigDragMove(t.clientX, t.clientY); if (_dragMoved) e.preventDefault(); }, { passive: false }); btn.addEventListener('touchend', function() { _trigDragEnd(); }); // 单击 = 开窗;双击(<350ms)= 快速下载 btn.addEventListener('click', function() { if (_dragMoved) return; var now = Date.now(); if (now - _lastClickTs < 350) { _lastClickTs = 0; quickDownloadCurrent(); return; } _lastClickTs = now; // 延迟一点开窗,避免双击时也开窗 setTimeout(function() { if (Date.now() - _lastClickTs >= 350) { var w = q('svd-win'); if (w) { w.style.display = ''; btn.style.display = 'none'; } } }, 360); }); 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-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) { slider.value = SETTINGS.windowOpacity; w.style.opacity = SETTINGS.windowOpacity; slider.addEventListener('input', function(e) { var v = parseFloat(e.target.value); w.style.opacity = v; saveSetting('windowOpacity', v); }); slider.addEventListener('mousedown', function(e) { e.stopPropagation(); }); } // ── 主题分段 ──────────────────────────────────── var segTheme = w.querySelector('#svd-seg-theme'); if (segTheme) segTheme.addEventListener('click', function(e) { var t = e.target.closest('button'); if (!t) return; saveSetting('themeMode', t.dataset.val); Array.prototype.forEach.call(segTheme.querySelectorAll('button'), function(b) { b.classList.toggle('active', b === t); }); applyTheme(); }); // ── 链接类型分段 ──────────────────────────────── var segLink = w.querySelector('#svd-seg-linktype'); if (segLink) segLink.addEventListener('click', function(e) { var t = e.target.closest('button'); if (!t) return; saveSetting('linkTypeFilter', t.dataset.val); Array.prototype.forEach.call(segLink.querySelectorAll('button'), function(b) { b.classList.toggle('active', b === t); }); setStatus('链接类型筛选已更新(对后续嗅探生效)', 'info'); }); // ── 通用 switch 绑定 ──────────────────────────── Array.prototype.forEach.call(w.querySelectorAll('.svd-switch[data-key]'), function(sw) { sw.addEventListener('click', function() { var key = sw.dataset.key; var newVal = !SETTINGS[key]; sw.classList.toggle('on', newVal); saveSetting(key, newVal); // 立即生效的副作用 if (key === 'rememberTriggerPos' && !newVal) saveSetting('triggerPos', null); if (key === 'showSniffPreview') { setStatus('预览设置已更新(对后续嗅探条目生效)', 'info'); } }); }); // ── 下拉:同名策略 ───────────────────────────── var selDup = w.querySelector('#svd-sel-dup'); if (selDup) selDup.addEventListener('change', function() { saveSetting('duplicateStrategy', selDup.value); }); // ── 下拉:最低分辨率 ────────────────────────── var selMin = w.querySelector('#svd-sel-minres'); if (selMin) selMin.addEventListener('change', function() { saveSetting('minResolution', selMin.value); setStatus('分辨率过滤已更新(对后续嗅探生效)', 'info'); }); // ── 文件名模板 ────────────────────────────────── var tplIn = w.querySelector('#svd-tpl-input'); if (tplIn) { tplIn.addEventListener('input', function() { saveSetting('filenameTemplate', tplIn.value || '{author}-{desc}'); }); // 点击占位符插入 var hint = tplIn.parentElement.querySelector('.svd-template-hint'); if (hint) hint.addEventListener('click', function(e) { var c = e.target.closest('code[data-ins]'); if (!c) return; var ins = c.dataset.ins; var s = tplIn.selectionStart || tplIn.value.length; var e2 = tplIn.selectionEnd || tplIn.value.length; tplIn.value = tplIn.value.substring(0, s) + ins + tplIn.value.substring(e2); tplIn.focus(); tplIn.setSelectionRange(s + ins.length, s + ins.length); saveSetting('filenameTemplate', tplIn.value); }); } // ── 清空嗅探 ──────────────────────────────────── var clearSniffBtn = w.querySelector('#svd-btn-clear-sniff'); if (clearSniffBtn) clearSniffBtn.addEventListener('click', function() { detectedUrls.clear(); sniffMemoryVault = []; sniffFirstCopied = false; var list = q('svd-m3u8-list'); if (list) { list.innerHTML = ''; list.className = 'svd-cols-1'; } updateSniffGrid(); setStatus('已清空嗅探列表', 'info'); }); // ── 重置所有设置 ──────────────────────────────── var resetBtn = w.querySelector('#svd-btn-reset-all'); if (resetBtn) resetBtn.addEventListener('click', function() { if (!window.confirm('确定要将所有设置还原为默认吗?\n(目录授权和嗅探历史不受影响)')) return; var defaults = { themeMode: 'auto', rememberTriggerPos: true, rememberLastTab: true, windowOpacity: 1.0, autoSaveCover: false, filenameTemplate: '{author}-{desc}', notifyOnComplete: true, duplicateStrategy: 'rename', minResolution: '0', linkTypeFilter: 'all', autoCopyFirst: false, showSniffPreview: true, lastTab: 'main', triggerPos: null }; Object.keys(defaults).forEach(function(k) { saveSetting(k, defaults[k]); }); applySettingsToUI(w); applyTheme(); w.style.opacity = SETTINGS.windowOpacity; // 触发球归位 var trig = q('svd-trig'); if (trig) { trig.style.left = ''; trig.style.top = ''; trig.style.right = '18px'; trig.style.bottom = '108px'; } setStatus('✅ 所有设置已还原为默认', 'ok'); }); // ── 文案展开/折叠 ────────────────────────────── var descBlock = w.querySelector('.svd-info-desc-block'); if (descBlock) { descBlock.addEventListener('click', function() { var expanded = descBlock.classList.toggle('expanded'); var chev = descBlock.querySelector('.svd-desc-chevron'); if (chev) chev.textContent = expanded ? '▴ 收起' : '▾ 展开'; }); } // ── 嗅探工具栏 ────────────────────────────────── 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() { 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 = []; sniffFirstCopied = false; var list = q('svd-m3u8-list'); if (list) { list.innerHTML = ''; list.className = 'svd-cols-1'; } updateSniffGrid(); 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; function startDrag(cx, cy) { d = true; sx = cx; sy = cy; var r = el.getBoundingClientRect(); ox = r.left; oy = r.top; } function applyDrag(cx, cy) { if (!d) return; el.style.left = Math.max(0, Math.min(ox + cx - sx, window.innerWidth - el.offsetWidth)) + 'px'; el.style.top = Math.max(0, Math.min(oy + cy - sy, window.innerHeight - el.offsetHeight)) + 'px'; el.style.right = 'auto'; } handle.addEventListener('mousedown', function(e) { if (e.target.classList.contains('svd-wbtn')) return; startDrag(e.clientX, e.clientY); document.addEventListener('mousemove', mv); document.addEventListener('mouseup', mu); e.preventDefault(); }); function mv(e) { applyDrag(e.clientX, e.clientY); } function mu() { d = false; document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', mu); } handle.addEventListener('touchstart', function(e) { if (e.target.classList.contains('svd-wbtn')) return; var t = e.touches[0]; startDrag(t.clientX, t.clientY); e.preventDefault(); }, { passive: false }); handle.addEventListener('touchmove', function(e) { var t = e.touches[0]; applyDrag(t.clientX, t.clientY); e.preventDefault(); }, { passive: false }); handle.addEventListener('touchend', function() { d = false; }); } /* ══════════════════════════════════════════════════════════ 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; notify('批量完成', '共保存 ' + total + ' 个文件'); } }; 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); } /* ══════════════════════════════════════════════════════════ GLOBAL HOTKEYS + 菜单 ══════════════════════════════════════════════════════════ */ function setupHotkeys() { document.addEventListener('keydown', function(e) { // Alt+D = 切换小窗显示 if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 'd' || e.key === 'D')) { e.preventDefault(); toggleWindow(); } // Alt+S = 快速下载 if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 's' || e.key === 'S')) { e.preventDefault(); quickDownloadCurrent(); } }); } function toggleWindow() { var w = q('svd-win'), t = q('svd-trig'); if (!w) return; if (w.style.display === 'none' || w.style.display === '') { w.style.display = 'flex'; if (t) t.style.display = 'none'; } else { w.style.display = 'none'; if (t) t.style.display = 'flex'; } } function setupMenuCommands() { try { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('🪟 显示/隐藏小窗 (Alt+D)', toggleWindow); GM_registerMenuCommand('⚡ 快速下载当前视频 (Alt+S)', quickDownloadCurrent); GM_registerMenuCommand('🔄 刷新视频信息', doRefresh); } catch(e) {} } function bootstrap() { injectStyles(); buildWindow(); buildTrigger(); applyTheme(); setupHotkeys(); setupMenuCommands(); _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; sniffFirstCopied = false; if (!isDownloading) setTimeout(doRefresh, isDouyin() ? 1800 : 1000); } }, 800); })();