WWW.YOUINFO.SITE
标签聚合 namespace

/tag/namespace

linux.do · 2026-04-29 10:13:59+08:00 · tech

// ==UserScript== // @name Gemini Always Pro // @namespace https://github.com/lzcmian/gemini-always-pro // @version 0.5.1 // @description Keep Gemini locked to the Pro model via structural selectors. // @author lzcmian // @match https://gemini.google.com/* // @match https://bard.google.com/* // @run-at document-idle // @grant GM_registerMenuCommand // ==/UserScript== (() => { "use strict"; const STORAGE_KEY = "gemini-always-pro.lock-enabled"; const TOGGLE_ID = "gemini-always-pro-toggle"; const STYLE_ID = "gemini-always-pro-style"; const WARN_ATTR = "data-gap-warn"; const MODE_BUTTON = '[data-test-id="bard-mode-menu-button"]'; const PRO_OPTION = '[data-test-id="bard-mode-option-pro"]'; const DEBOUNCE_MS = 120; const RUN_GAP_MS = 1500; const MENU_SETTLE_MS = 300; const USER_QUIET_MS = 1200; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let enabled = localStorage.getItem(STORAGE_KEY) !== "0"; let busyUntil = 0; let pending = 0; let lastUserInputAt = 0; let syntheticDepth = 0; const modeButton = () => document.querySelector(MODE_BUTTON); const isProNow = () => modeButton()?.innerText.trim().toLowerCase() === "pro"; const userBusy = () => { const a = document.activeElement; if (!a?.matches?.('input, textarea, [contenteditable="true"], [contenteditable="plaintext-only"]')) return false; if (Date.now() - lastUserInputAt < USER_QUIET_MS) return true; return Boolean(a.value?.trim() || a.textContent?.replace(/​/g, "").trim()); }; const renderToggle = () => { let btn = document.getElementById(TOGGLE_ID); if (!btn) { btn = document.createElement("button"); btn.id = TOGGLE_ID; btn.type = "button"; btn.style.cssText = "all:initial;position:fixed;right:14px;bottom:14px;z-index:2147483647;" + "min-width:108px;padding:9px 13px;border:0;border-radius:999px;color:#fff;" + 'font:700 13px/1.2 system-ui,-apple-system,"Segoe UI",sans-serif;cursor:pointer;' + "box-shadow:0 6px 22px rgba(0,0,0,.28);user-select:none"; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); enabled = !enabled; localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0"); renderToggle(); if (enabled) schedule(); }); (document.body || document.documentElement).append(btn); } btn.textContent = enabled ? "Pro 锁定:开" : "Pro 锁定:关"; btn.style.background = enabled ? "#0b57d0" : "#5f6368"; btn.setAttribute("aria-pressed", String(enabled)); }; const ensureWarningStyle = () => { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` [data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] { background-color: #d93025 !important; color: #fff !important; box-shadow: 0 0 0 2px rgba(217,48,37,.55), 0 0 12px rgba(217,48,37,.45) !important; border-radius: 999px !important; transition: background-color .15s ease, box-shadow .15s ease; } [data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] *, [data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] mat-icon { color: #fff !important; fill: #fff !important; } `; (document.head || document.documentElement).append(style); }; const updateWarning = () => { const btn = modeButton(); if (!btn) return; if (isProNow()) btn.removeAttribute(WARN_ATTR); else btn.setAttribute(WARN_ATTR, "true"); }; async function ensurePro() { if (!enabled || Date.now() < busyUntil) return; if (isProNow()) return; if (userBusy()) { schedule(USER_QUIET_MS); return; } const btn = modeButton(); if (!btn) return; busyUntil = Date.now() + RUN_GAP_MS; syntheticDepth += 1; try { btn.click(); await sleep(MENU_SETTLE_MS); const pro = document.querySelector(PRO_OPTION); if (pro) pro.click(); else document.body.click(); } finally { syntheticDepth -= 1; } } const schedule = (delay = DEBOUNCE_MS) => { clearTimeout(pending); pending = setTimeout(ensurePro, delay); }; const markUserInput = (e) => { if (syntheticDepth > 0 || e.target?.id === TOGGLE_ID) return; lastUserInputAt = Date.now(); }; for (const ev of ["pointerdown", "keydown", "input", "compositionstart", "paste"]) { document.addEventListener(ev, markUserInput, { capture: true, passive: true }); } new MutationObserver(() => { updateWarning(); if (enabled) schedule(); }).observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["class", "aria-expanded", "data-test-id"], }); if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("切换 Gemini Pro 锁定", () => { enabled = !enabled; localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0"); renderToggle(); if (enabled) schedule(); }); } ensureWarningStyle(); updateWarning(); renderToggle(); schedule(); })(); 1 个帖子 - 1 位参与者 阅读完整话题

linux.do · 2026-04-26 17:03:26+08:00 · tech

// ==UserScript== // @name Codedex 自动翻译(Chrome Translator API / Google 降级) // @namespace https://github.com/yourname/codedex-translator // @version 5.0.0 // @description 检测到 .challenge-container 加载完毕后自动翻译,无需按钮 // @author You // @match https://www.codedex.io/* // @grant GM_xmlhttpRequest // @connect translate.googleapis.com // @run-at document-idle // ==/UserScript== (async function () { 'use strict'; const CONFIG = { sourceLang: 'en', targetLang: 'zh', targetLangGoogle: 'zh-CN', }; const SELECTORS = [ '.challenge-container h2', '.challenge-container h3', '.challenge-container p', '.challenge-container li', '.challenge-container .clone p', ]; // ─── Chrome Translator API ─────────────────────────────────────────────────── let chromeTranslator = null; async function initChromeTranslator() { if ('Translator' in self) { try { const avail = await Translator.availability({ sourceLanguage: CONFIG.sourceLang, targetLanguage: CONFIG.targetLang, }); if (avail === 'no') return false; chromeTranslator = await Translator.create({ sourceLanguage: CONFIG.sourceLang, targetLanguage: CONFIG.targetLang, }); console.log('[Codedex Translator] Chrome Translator 就绪:', avail); return true; } catch (e) { console.warn('[Codedex Translator] Chrome Translator 失败:', e); return false; } } // 旧版兼容 Chrome 138-140 if (window.ai?.translator) { try { const canDo = await window.ai.translator.canTranslate({ sourceLanguage: CONFIG.sourceLang, targetLanguage: CONFIG.targetLang, }); if (canDo === 'no') return false; chromeTranslator = await window.ai.translator.createTranslator({ sourceLanguage: CONFIG.sourceLang, targetLanguage: CONFIG.targetLang, }); if (chromeTranslator?.ready) await chromeTranslator.ready; return true; } catch (e) { console.warn('[Codedex Translator] window.ai.translator 失败:', e); return false; } } return false; } // ─── Chrome LanguageDetector ───────────────────────────────────────────────── let langDetector = null; async function initLangDetector() { if ('LanguageDetector' in self) { try { const avail = await LanguageDetector.availability(); if (avail === 'no') return; langDetector = await LanguageDetector.create(); console.log('[Codedex Translator] LanguageDetector 就绪:', avail); } catch (e) { console.warn('[Codedex Translator] LanguageDetector 初始化失败:', e); } } } // 取容器内一段代表性文本做语言检测,返回 true 表示需要翻译 async function shouldTranslate(container) { if (!langDetector) return true; // 没有检测器就默认翻译 const sample = container.innerText.trim().slice(0, 200); if (!sample) return false; try { const results = await langDetector.detect(sample); const top = results?.[0]?.detectedLanguage; console.log('[Codedex Translator] 检测语言:', top); return top === 'en'; // 只翻译英文内容 } catch { return true; } } // ─── Google Translate 降级 ─────────────────────────────────────────────────── function translateWithGoogle(text) { return new Promise((resolve) => { if (!text.trim()) return resolve(text); const url = `https://translate.googleapis.com/translate_a/single` + `?client=gtx&sl=${CONFIG.sourceLang}&tl=${CONFIG.targetLangGoogle}` + `&dt=t&q=${encodeURIComponent(text)}`; GM_xmlhttpRequest({ method: 'GET', url, onload(res) { try { const data = JSON.parse(res.responseText); resolve(data[0].filter(Boolean).map((s) => s[0]).join('') || text); } catch { resolve(text); } }, onerror() { resolve(text); }, ontimeout() { resolve(text); }, timeout: 8000, }); }); } // ─── 统一翻译 ───────────────────────────────────────────────────────────────── let mode = 'none'; async function translateText(text) { if (!text.trim()) return text; if (mode === 'chrome') { try { const res = await chromeTranslator.translate(text); if (res) return res; } catch (e) { console.warn('[Codedex Translator] Chrome 翻译单条失败,降级:', e); } } return translateWithGoogle(text); } // ─── DOM 翻译 ───────────────────────────────────────────────────────────────── const DONE = 'data-cdx-done'; const ORIG = 'data-cdx-orig'; function collectNodes() { const nodes = []; SELECTORS.forEach((sel) => document.querySelectorAll(sel).forEach((el) => { if (!el.hasAttribute(DONE) && el.innerText.trim()) nodes.push(el); }) ); return nodes; } // 收集叶子文本节点,跳过代码块 const SKIP_TAGS = new Set(['CODE', 'PRE', 'KBD', 'VAR', 'SAMP']); function collectLeafTextNodes(el) { const result = []; function walk(node) { if (node.nodeType === Node.ELEMENT_NODE && SKIP_TAGS.has(node.tagName)) return; if (node.nodeType === Node.TEXT_NODE) { if (node.textContent.trim()) result.push(node); return; } node.childNodes.forEach(walk); } walk(el); return result; } async function translateContainer(container) { const els = []; SELECTORS.forEach((sel) => container.querySelectorAll(sel).forEach((el) => { if (!el.hasAttribute(DONE) && el.innerText.trim()) els.push(el); }) ); SELECTORS.forEach((sel) => { if (container.matches?.(sel) && !container.hasAttribute(DONE) && container.innerText.trim()) els.push(container); }); if (!els.length) return; for (const el of els) { el.setAttribute(DONE, '1'); const leafNodes = collectLeafTextNodes(el); for (const textNode of leafNodes) { const orig = textNode.textContent.trim(); if (!orig) continue; const result = await translateText(orig); if (result && result !== orig) { textNode.textContent = textNode.textContent.replace(orig, result); } } } } // ─── 等待 .challenge-container 出现/内容变化后翻译 ────────────────────────── let translateTimer = null; async function maybeTranslate(container) { if (await shouldTranslate(container)) { await translateContainer(container); } } function scheduleTranslate(container) { clearTimeout(translateTimer); translateTimer = setTimeout(() => maybeTranslate(container), 300); } function waitAndTranslate() { const existing = document.querySelector('.challenge-container'); if (existing) maybeTranslate(existing); // 监听 .challenge-container 的新增 和 内部文本内容变化 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // 新节点插入:检查是否是或包含 challenge-container for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (node.classList?.contains('challenge-container')) { scheduleTranslate(node); return; } const inner = node.querySelector?.('.challenge-container'); if (inner) { scheduleTranslate(inner); return; } } // 文本内容变化:如果变化发生在 challenge-container 内部 if (mutation.type === 'characterData' || mutation.type === 'childList') { const container = mutation.target.closest?.('.challenge-container') ?? (mutation.target.nodeType === Node.TEXT_NODE ? mutation.target.parentElement?.closest('.challenge-container') : null); if (container) { scheduleTranslate(container); return; } } } }); observer.observe(document.body, { childList: true, subtree: true, characterData: true, }); } // ─── 启动 ───────────────────────────────────────────────────────────────────── const ok = await initChromeTranslator(); mode = ok ? 'chrome' : 'google'; await initLangDetector(); console.log('[Codedex Translator] 模式:', mode); waitAndTranslate(); })(); 效果: 优先使用Chrome Translate API(Chrome 本地翻译)服务 其次才是Google API,所以 支持Translator API 的情况下速度更快 Enjoy it! 1 个帖子 - 1 位参与者 阅读完整话题

linux.do · 2026-04-20 11:45:58+08:00 · tech

最近有下载B站视频的需求,做了一个油猴脚本,用起来还是很不错的,分享给大家。 // ==UserScript== // @name B站视频下载助手 // @namespace https://example.com/ // @version 0.4.0 // @description bilibili 下载助手:支持清晰度选择、批量分P/剧集下载、复制 ffmpeg 命令 // @author Taoao.wei // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_addStyle // @grant unsafeWindow // @connect bilibili.com // @connect *.bilibili.com // @connect *.bilivideo.com // @connect bilivideo.com // @run-at document-idle // ==/UserScript== (function () { "use strict"; const PANEL_ID = "tm-bili-download-panel-plus-fixed"; const state = { href: location.href, meta: null, formats: [], currentPlayInfo: null, logs: [], isQueueRunning: false, queueMode: "idle", queueItems: [], queueIndex: 0, queueTotal: 0, queueDone: 0, queueFailed: 0, currentTaskLabel: "", queueRunId: 0, }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function $(selector, root = document) { return root.querySelector(selector); } function sanitizeFileName(name) { return String(name || "bilibili_video") .replace(/[\\/:*?"<>|]+/g, "_") .replace(/\s+/g, " ") .replace(/\.+$/g, "") .trim() .slice(0, 150); } function pad2(n) { return String(n).padStart(2, "0"); } function uniqBy(arr, keyFn) { const map = new Map(); for (const item of arr) map.set(keyFn(item), item); return [...map.values()]; } function addLog(text) { const line = `[${new Date().toLocaleTimeString()}] ${text}`; state.logs.push(line); state.logs = state.logs.slice(-12); const logEl = $(`#${PANEL_ID} .tm-log`); if (logEl) logEl.textContent = state.logs.join("\n"); } function setStatus(text) { const statusEl = $(`#${PANEL_ID} .tm-status`); if (statusEl) statusEl.textContent = text; } function renderQueueState() { const progressTextEl = $(`#${PANEL_ID} .tm-progress-text`); const progressFillEl = $(`#${PANEL_ID} .tm-progress-fill`); const currentTaskEl = $(`#${PANEL_ID} .tm-current-task`); const addBtn = $(`#${PANEL_ID} button[data-action="queue-current"]`); const addAllBtn = $(`#${PANEL_ID} button[data-action="queue-all"]`); const stopBtn = $(`#${PANEL_ID} button[data-action="stop-all"]`); const total = state.queueTotal || 0; const done = Math.min(state.queueDone || 0, total); const percent = total ? Math.max(0, Math.min(100, (done / total) * 100)) : 0; let suffix = "(空闲)"; if (state.queueMode === "running") { suffix = state.queueFailed ? `(运行中,失败 ${state.queueFailed})` : "(运行中)"; } else if (state.queueMode === "stopping") { suffix = "(停止中)"; } else if (total) { suffix = state.queueFailed ? `(完成,失败 ${state.queueFailed})` : "(完成)"; } if (progressTextEl) progressTextEl.textContent = `进度:${done}/${total}${suffix}`; if (progressFillEl) progressFillEl.style.width = `${percent}%`; let currentText = "当前:空闲"; if (state.currentTaskLabel) { currentText = `当前:${state.currentTaskLabel}`; } else if (state.queueMode === "stopping") { currentText = "当前:等待当前条目收尾"; } else if (!state.isQueueRunning && total && done >= total) { currentText = "当前:已完成"; } if (currentTaskEl) currentTaskEl.textContent = currentText; if (addBtn) addBtn.disabled = state.queueMode === "stopping"; if (addAllBtn) addAllBtn.disabled = state.queueMode === "stopping"; if (stopBtn) stopBtn.disabled = !state.isQueueRunning; } function setProgress(current, total) { state.queueDone = current; state.queueTotal = total; renderQueueState(); } function setCurrentTask(label) { state.currentTaskLabel = label; renderQueueState(); } function clearQueueTracking(resetProgress = false) { state.queueItems = []; state.queueIndex = 0; state.currentTaskLabel = ""; if (resetProgress) { state.queueTotal = 0; state.queueDone = 0; state.queueFailed = 0; } renderQueueState(); } function formatQueueTaskLabel(item) { if (!item) return ""; const page = item.page || item; const pageLabel = item.scope === "current" ? `当前P${pad2(page.page || 1)}` : state.queueTotal > 1 ? `P${pad2(page.page || 1)}` : `P${pad2(page.page || 1)}`; const qualityLabel = item.requestedLabel || item.qualityLabel || "当前清晰度"; return `${pageLabel} ${page.part} - ${qualityLabel}`; } function readVarFromScripts(varName) { const scripts = Array.from(document.scripts); for (const script of scripts) { const text = script.textContent || ""; if (!text.includes(varName)) continue; const patterns = [ new RegExp(`window\\.${varName}\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*;`), new RegExp(`${varName}\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*;`), ]; for (const pattern of patterns) { const match = text.match(pattern); if (!match) continue; try { return JSON.parse(match[1]); } catch {} } } return null; } function getInitialState() { return ( unsafeWindow.__INITIAL_STATE__ || readVarFromScripts("__INITIAL_STATE__") ); } function getPlayInfo() { return unsafeWindow.__playinfo__ || readVarFromScripts("__playinfo__"); } async function waitForContext(timeout = 12000) { const start = Date.now(); while (Date.now() - start < timeout) { const meta = buildMeta(); const playInfo = getPlayInfo(); if (meta && playInfo?.data) return { meta, playInfo }; await sleep(400); } return { meta: buildMeta(), playInfo: getPlayInfo() }; } function currentVideoPageIndex(pages) { const url = new URL(location.href); const p = Number(url.searchParams.get("p") || "1"); if (!Number.isFinite(p) || p < 1) return 1; return Math.min(p, Math.max(1, pages.length || 1)); } function buildVideoMetaFromState(s) { const videoData = s?.videoData; if (!videoData?.bvid || !Array.isArray(videoData.pages)) return null; const title = sanitizeFileName( videoData.title || document.title.replace(/_哔哩哔哩_bilibili$/, "").trim(), ); const pages = videoData.pages.map((item, idx) => ({ page: idx + 1, cid: item.cid, part: sanitizeFileName(item.part || `P${idx + 1}`), bvid: videoData.bvid, })); const currentPage = currentVideoPageIndex(pages); return { type: "video", title, bvid: videoData.bvid, pages, currentPage, current: pages[currentPage - 1], }; } function buildBangumiMetaFromState(s) { const epInfo = s?.epInfo; if (!epInfo?.cid) return null; const seriesTitle = sanitizeFileName( s?.h1Title || s?.mediaInfo?.title || document.title.replace(/_哔哩哔哩_bilibili$/, "").trim(), ); const epList = Array.isArray(s?.epList) && s.epList.length ? s.epList : [epInfo]; const pages = epList.map((ep, idx) => ({ page: idx + 1, cid: ep.cid, bvid: ep.bvid || epInfo.bvid, part: sanitizeFileName( [ep.titleFormat, ep.long_title].filter(Boolean).join(" ") || `EP${idx + 1}`, ), })); const currentCid = epInfo.cid; const currentIndex = Math.max( 0, pages.findIndex((item) => item.cid === currentCid), ); return { type: "bangumi", title: seriesTitle, bvid: epInfo.bvid, pages, currentPage: currentIndex + 1, current: pages[currentIndex], }; } function buildMeta() { const s = getInitialState(); return buildVideoMetaFromState(s) || buildBangumiMetaFromState(s); } function getFormatsFromPlayInfo(playInfo) { return uniqBy( (playInfo?.data?.support_formats || []).map((item) => ({ qn: item.quality, label: item.new_description || item.display_desc || item.format || String(item.quality), })), (item) => item.qn, ).sort((a, b) => b.qn - a.qn); } function pickBestAudio(data) { const candidates = []; if (Array.isArray(data?.dash?.audio)) candidates.push(...data.dash.audio); if (data?.dash?.flac?.audio) candidates.push(data.dash.flac.audio); if (Array.isArray(data?.dash?.dolby?.audio)) candidates.push(...data.dash.dolby.audio); if (!candidates.length) return null; return [...candidates].sort((a, b) => { const aFlac = /flac/i.test(a?.codecs || "") ? 1 : 0; const bFlac = /flac/i.test(b?.codecs || "") ? 1 : 0; if (bFlac !== aFlac) return bFlac - aFlac; return (b.bandwidth || 0) - (a.bandwidth || 0); })[0]; } function pickBestVideo(data, quality) { const videos = Array.isArray(data?.dash?.video) ? data.dash.video : []; if (!videos.length) return null; const exact = videos.filter((item) => item.id === quality); const list = exact.length ? exact : videos; return [...list].sort((a, b) => { const h = (b.height || 0) - (a.height || 0); if (h !== 0) return h; return (b.bandwidth || 0) - (a.bandwidth || 0); })[0]; } function detectSingleFileExt(url) { if (/\.flv(\?|$)/i.test(url)) return "flv"; if (/\.mp4(\?|$)/i.test(url)) return "mp4"; return "mp4"; } function audioSaveExt(stream) { return /flac/i.test(stream?.codecs || "") ? "flac" : "m4a"; } function videoSaveExt() { return "mp4"; } function buildBaseName(meta, page, qualityLabel) { const prefix = meta.type === "bangumi" ? `[${page.bvid || meta.bvid || "BILI"}] ${meta.title}` : `[${meta.bvid || "BILI"}] ${meta.title}`; const pageSuffix = meta.pages.length > 1 ? ` - P${pad2(page.page)} ${page.part}` : page.part && page.part !== meta.title ? ` - ${page.part}` : ""; return sanitizeFileName(`${prefix}${pageSuffix} - ${qualityLabel}`); } function buildDownloadInfo(meta, page, playData, requestedQn, qnLabelMap) { if (playData?.dash) { const actualQn = playData.quality || requestedQn; const qualityLabel = qnLabelMap.get(actualQn) || qnLabelMap.get(requestedQn) || `${actualQn}P`; const video = pickBestVideo(playData, actualQn); const audio = pickBestAudio(playData); if (!video) throw new Error("没有找到对应的视频流"); const baseName = buildBaseName(meta, page, qualityLabel); return { type: "dash", actualQn, qualityLabel, videoUrl: video.baseUrl || video.base_url, audioUrl: audio ? audio.baseUrl || audio.base_url : "", videoName: `${baseName}.video.${videoSaveExt()}`, audioName: audio ? `${baseName}.audio.${audioSaveExt(audio)}` : "", outputName: `${baseName}.merged.mp4`, }; } if (Array.isArray(playData?.durl) && playData.durl.length > 0) { const actualQn = playData.quality || requestedQn; const qualityLabel = qnLabelMap.get(actualQn) || qnLabelMap.get(requestedQn) || `${actualQn}P`; const baseName = buildBaseName(meta, page, qualityLabel); const ext = detectSingleFileExt(playData.durl[0].url); return { type: "single", actualQn, qualityLabel, singleUrl: playData.durl[0].url, fileName: `${baseName}.${ext}`, }; } throw new Error("当前页面没有可识别的下载流"); } async function fetchPlayData(page, qn) { const url = new URL("https://api.bilibili.com/x/player/playurl"); url.searchParams.set("bvid", page.bvid); url.searchParams.set("cid", String(page.cid)); url.searchParams.set("qn", String(qn)); url.searchParams.set("fnval", "16"); url.searchParams.set("fnver", "0"); url.searchParams.set("fourk", "1"); const res = await fetch(url.toString(), { credentials: "include" }); if (!res.ok) throw new Error(`请求失败:HTTP ${res.status}`); const json = await res.json(); if (json.code !== 0 || !json.data) { throw new Error(json.message || "接口返回异常"); } return json.data; } function ffmpegCommand(info) { if (info.type === "single") { return `# 单文件无需合并:${info.fileName}`; } if (!info.audioName) { return `ffmpeg -i "${info.videoName}" -c copy "${info.outputName}"`; } return `ffmpeg -i "${info.videoName}" -i "${info.audioName}" -c copy "${info.outputName}"`; } function getSelectedQn() { return Number($(`#${PANEL_ID} .tm-quality`)?.value || 0); } function getSelectedLabel() { return ( $( `#${PANEL_ID} .tm-quality`, )?.selectedOptions?.[0]?.textContent?.trim() || "" ); } function getQnLabelMap() { return new Map(state.formats.map((item) => [item.qn, item.label])); } function populateQualityOptions() { const select = $(`#${PANEL_ID} .tm-quality`); if (!select) return; const currentQn = state.currentPlayInfo?.data?.quality; const formats = state.formats.length ? state.formats : [{ qn: currentQn || 0, label: `当前清晰度 ${currentQn || ""}` }]; const oldValue = Number(select.value || 0); select.innerHTML = formats .map((item) => { const selected = oldValue ? oldValue === item.qn : currentQn === item.qn; return `<option value="${item.qn}" ${selected ? "selected" : ""}>${item.label}</option>`; }) .join(""); } async function gmDownloadSafe(url, name) { return new Promise((resolve) => { GM_download({ url, name, saveAs: false, onload: () => resolve({ ok: true }), onerror: (err) => resolve({ ok: false, err }), }); }); } async function gmXhrBlobDownload(url, name) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", headers: { Referer: location.href, }, onload: (response) => { if ( response.status >= 200 && response.status < 300 && response.response ) { triggerBlobDownload(response.response, name); resolve(); return; } reject(new Error(`HTTP ${response.status}`)); }, onerror: (error) => { reject(new Error(error?.error || "gm_xhr_failed")); }, ontimeout: () => { reject(new Error("gm_xhr_timeout")); }, }); }); } async function fetchBlobWithPageContext(url) { const res = await fetch(url, { credentials: "include", referrer: location.href, referrerPolicy: "strict-origin-when-cross-origin", }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return await res.blob(); } function triggerBlobDownload(blob, name) { const objectUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = objectUrl; a.download = name; a.rel = "noopener noreferrer"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(objectUrl), 30000); } async function downloadViaBlob(url, name) { const blob = await fetchBlobWithPageContext(url); triggerBlobDownload(blob, name); } function formatBlockedMessage(name, detail) { return `下载受阻:${name}\n${detail}\n\n当前脚本无法直接替你绕过站点的访问控制。你可以改用页面内观看、官方缓存/离线能力,或在你自己的环境里手动处理已授权内容。`; } async function fallbackDownload(url, name, reason) { try { addLog(`${reason},改用 GM_xmlhttpRequest 下载:${name}`); await gmXhrBlobDownload(url, name); return; } catch (gmXhrError) { const gmXhrDetail = gmXhrError?.message || String(gmXhrError); addLog(`GM_xmlhttpRequest 下载失败:${gmXhrDetail}`); } try { addLog(`继续改用页面上下文下载:${name}`); await downloadViaBlob(url, name); } catch (blobError) { const detail = blobError?.message || String(blobError); addLog(`页面上下文下载失败:${detail}`); throw new Error( formatBlockedMessage( name, `${reason};GM_xmlhttpRequest 与页面上下文请求均失败:${detail}`, ), ); } } async function startDownload(url, name) { const result = await gmDownloadSafe(url, name); if (result.ok) return; const error = result.err?.error || result.err?.message || String(result.err || "unknown"); if (error === "not_whitelisted") { await fallbackDownload(url, name, "Tampermonkey 拒绝扩展名"); return; } if (/forbidden/i.test(error)) { await fallbackDownload(url, name, "直链下载被拒绝"); return; } if (/xhr_failed/i.test(error)) { await fallbackDownload(url, name, "Tampermonkey XHR 下载失败");https://linux.do/t/topic/28272 return; } await fallbackDownload(url, name, `GM_download 失败:${error}`); } function createQueueItem(meta, page, qn, requestedLabel, scope = "batch") { return { meta: { ...meta, pages: Array.isArray(meta?.pages) ? meta.pages.map((item) => ({ ...item })) : [], current: meta?.current ? { ...meta.current } : null, }, page: { ...page }, qn, requestedLabel, scope, }; } function enqueueItems(items) { if (!items.length) return; if ( !state.isQueueRunning && state.queueMode === "idle" && !state.queueItems.length ) { state.queueIndex = 0; state.queueTotal = 0; state.queueDone = 0; state.queueFailed = 0; state.currentTaskLabel = ""; } state.queueItems.push(...items); state.queueTotal += items.length; renderQueueState(); } function buildTaskQueue(meta, qn, requestedLabel) { return (meta?.pages || []).map((page) => createQueueItem(meta, page, qn, requestedLabel, "batch"), ); } function getSelectedQueueConfig() { const qn = getSelectedQn(); if (!qn) throw new Error("请选择清晰度"); return { qn, requestedLabel: getSelectedLabel() || String(qn), }; } async function resolveTask(item) { const playData = await fetchPlayData(item.page, item.qn); return buildDownloadInfo( item.meta, item.page, playData, item.qn, getQnLabelMap(), ); } async function triggerDownloadInfo(info) { if (info.type === "single") { addLog(`开始下载:${info.fileName}`); await startDownload(info.singleUrl, info.fileName); return; } addLog(`开始下载:${info.videoName}`); await startDownload(info.videoUrl, info.videoName); if (info.audioUrl) { await sleep(250); addLog(`开始下载:${info.audioName}`); await startDownload(info.audioUrl, info.audioName); } } function ensureQueueRunning() { if (state.queueMode === "stopping") { addLog("队列停止中,请稍后再加入新任务"); return; } if (state.isQueueRunning || !state.queueItems.length) { renderQueueState(); return; } state.queueRunId += 1; state.isQueueRunning = true; state.queueMode = "running"; setStatus(`队列下载中 ${state.queueDone}/${state.queueTotal}`); renderQueueState(); runQueue(state.queueRunId); } function queueCurrent() { if (!state.meta?.current) throw new Error("未识别当前视频信息"); if (state.queueMode === "stopping") throw new Error("队列停止中,请稍后再试"); const { qn, requestedLabel } = getSelectedQueueConfig(); const item = createQueueItem( state.meta, state.meta.current, qn, requestedLabel, "current", ); enqueueItems([item]); addLog(`已加入队列:${formatQueueTaskLabel(item)}`); setStatus(`已加入队列 ${state.queueTotal}/${state.queueTotal}`); ensureQueueRunning(); } function queueAll() { if (!state.meta?.pages?.length) throw new Error("没有可下载的分P/剧集"); if (state.queueMode === "stopping") throw new Error("队列停止中,请稍后再试"); const { qn, requestedLabel } = getSelectedQueueConfig(); const items = buildTaskQueue(state.meta, qn, requestedLabel); if (!items.length) throw new Error("没有可下载的分P/剧集"); enqueueItems(items); addLog(`已加入 ${items.length} 个任务:${requestedLabel}`); setStatus(`已加入队列,共 ${state.queueTotal} 个任务`); ensureQueueRunning(); } function stopBackgroundQueue(reason = "用户手动停止", options = {}) { const { hard = false, clearProgress = false } = options; if (!state.isQueueRunning && state.queueMode !== "stopping") { if (hard && clearProgress) clearQueueTracking(true); return; } if (hard) { state.queueRunId += 1; state.isQueueRunning = false; state.queueMode = "idle"; clearQueueTracking(clearProgress); setStatus("队列已停止"); addLog(`队列已停止:${reason}`); return; } if (state.queueMode === "stopping") return; state.isQueueRunning = false; state.queueMode = "stopping"; setStatus("队列停止中..."); addLog(`队列将在当前任务后停止:${reason}`); renderQueueState(); } async function runQueue(runId) { try { while ( runId === state.queueRunId && state.queueMode === "running" && state.queueIndex < state.queueItems.length ) { const item = state.queueItems[state.queueIndex]; const currentNumber = state.queueIndex + 1; const taskLabel = formatQueueTaskLabel(item); setCurrentTask(taskLabel); setStatus(`队列下载中 ${currentNumber}/${state.queueTotal}`); addLog(`开始任务 ${currentNumber}/${state.queueTotal}:${taskLabel}`); try { const info = await resolveTask(item); if (runId !== state.queueRunId) return; if (info.actualQn !== item.qn) { addLog( `请求 ${item.requestedLabel},实际返回 ${info.qualityLabel}`, ); } await triggerDownloadInfo(info); if (runId !== state.queueRunId) return; state.queueDone += 1; } catch (err) { if (runId !== state.queueRunId) return; state.queueFailed += 1; state.queueDone += 1; addLog(`任务失败:${taskLabel} - ${err.message || String(err)}`); } if (runId !== state.queueRunId) return; state.queueIndex += 1; setProgress(state.queueDone, state.queueTotal); if (state.queueMode === "stopping" || !state.isQueueRunning) break; await sleep(600); } if (runId !== state.queueRunId) return; const wasStopped = state.queueMode === "stopping"; state.isQueueRunning = false; state.queueMode = "idle"; clearQueueTracking(false); if (wasStopped) { setStatus("队列已停止"); addLog(`队列已停止,已完成 ${state.queueDone}/${state.queueTotal}`); return; } setStatus("队列已完成"); addLog( `队列完成:成功 ${state.queueTotal - state.queueFailed},失败 ${state.queueFailed}`, ); } catch (err) { if (runId !== state.queueRunId) return; state.isQueueRunning = false; state.queueMode = "idle"; clearQueueTracking(false); setStatus("队列异常结束"); addLog(err.message || String(err)); } } async function refresh() { setStatus("正在读取页面信息..."); const result = await waitForContext(); state.meta = result.meta; state.currentPlayInfo = result.playInfo; if (!state.meta) { setStatus("未识别到视频信息"); addLog("当前页面暂不支持"); renderQueueState(); return; } state.formats = getFormatsFromPlayInfo(state.currentPlayInfo); populateQualityOptions(); const partText = state.meta.pages.length > 1 ? `P${pad2(state.meta.currentPage)} / 共 ${state.meta.pages.length} 个` : "单P"; setStatus(`${state.meta.title} | ${partText}`); addLog(`已识别:${state.meta.title}`); if (state.formats.length) { addLog( `可选清晰度:${state.formats.map((item) => item.label).join(" / ")}`, ); } renderQueueState(); } async function downloadCurrent() { try { queueCurrent(); } catch (err) { setStatus("加入队列失败"); addLog(err.message || String(err)); alert(err.message || String(err)); } } async function downloadAll() { try { queueAll(); } catch (err) { setStatus("加入队列失败"); addLog(err.message || String(err)); alert(err.message || String(err)); } } async function copyCurrentFfmpeg() { try { const info = await getCurrentPageInfo(); GM_setClipboard(ffmpegCommand(info)); setStatus("已复制当前 ffmpeg 命令"); addLog("当前 ffmpeg 命令已复制"); } catch (err) { setStatus("复制失败"); addLog(err.message || String(err)); alert(err.message || String(err)); } } async function copyAllFfmpeg() { try { if (!state.meta?.pages?.length) throw new Error("没有可处理的分P/剧集"); const qn = getSelectedQn(); if (!qn) throw new Error("请选择清晰度"); const lines = []; setStatus("正在生成批量 ffmpeg 命令..."); for (let i = 0; i < state.meta.pages.length; i++) { const page = state.meta.pages[i]; const playData = await fetchPlayData(page, qn); const info = buildDownloadInfo( state.meta, page, playData, qn, getQnLabelMap(), ); lines.push(ffmpegCommand(info)); await sleep(120); } GM_setClipboard(lines.join("\n")); setStatus("已复制批量 ffmpeg 命令"); addLog(`已复制 ${lines.length} 条 ffmpeg 命令`); } catch (err) { setStatus("复制失败"); addLog(err.message || String(err)); alert(err.message || String(err)); } } function ensurePanel() { if (document.getElementById(PANEL_ID)) return; GM_addStyle(` #${PANEL_ID} { position: fixed; top: 110px; right: 20px; z-index: 999999; width: 340px; background: rgba(24,24,28,.96); color: #fff; border: 1px solid rgba(255,255,255,.12); border-radius: 14px; box-shadow: 0 12px 28px rgba(0,0,0,.28); backdrop-filter: blur(8px); padding: 12px; font-size: 13px; } #${PANEL_ID} * { box-sizing: border-box; } #${PANEL_ID} .tm-title { font-weight: 700; font-size: 14px; margin-bottom: 8px; } #${PANEL_ID} .tm-status { font-size: 12px; color: rgba(255,255,255,.86); margin-bottom: 10px; word-break: break-all; } #${PANEL_ID} .tm-progress-wrap { margin-bottom: 10px; } #${PANEL_ID} .tm-progress-text, #${PANEL_ID} .tm-current-task { font-size: 12px; color: rgba(255,255,255,.82); word-break: break-word; } #${PANEL_ID} .tm-progress-bar { margin: 6px 0; height: 8px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,.12); } #${PANEL_ID} .tm-progress-fill { width: 0; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #00aeec 0%, #50d7ff 100%); transition: width .2s ease; } #${PANEL_ID} .tm-row { display: flex; gap: 8px; margin-bottom: 8px; } #${PANEL_ID} .tm-quality { width: 100%; border: none; border-radius: 8px; padding: 8px 10px; background: #2f3136; color: #fff; outline: none; } #${PANEL_ID} button { width: 100%; border: none; border-radius: 8px; padding: 8px 10px; background: #00aeec; color: #fff; cursor: pointer; font-size: 13px; } #${PANEL_ID} button:hover { opacity: .92; } #${PANEL_ID} button:disabled, #${PANEL_ID} .tm-quality:disabled { opacity: .55; cursor: not-allowed; } #${PANEL_ID} .tm-log { margin-top: 8px; min-height: 108px; max-height: 220px; overflow: auto; white-space: pre-wrap; word-break: break-word; background: rgba(255,255,255,.06); border-radius: 8px; padding: 8px; font-size: 12px; line-height: 1.5; } `); const panel = document.createElement("div"); panel.id = PANEL_ID; panel.innerHTML = ` <div class="tm-title">B站下载助手 增强版(修正版)</div> <div class="tm-status">初始化中...</div> <div class="tm-progress-wrap"> <div class="tm-progress-text">进度:0/0(空闲)</div> <div class="tm-progress-bar"><div class="tm-progress-fill"></div></div> <div class="tm-current-task">当前:空闲</div> </div> <div class="tm-row"> <select class="tm-quality"></select> </div> <div class="tm-row"> <button data-action="refresh">刷新信息</button> <button data-action="current">加入当前</button> </div> <div class="tm-row"> <button data-action="all">加入全部</button> <button data-action="stop-all">停止队列</button> </div> <div class="tm-row"> <button data-action="ffmpeg-current">复制当前ffmpeg</button> <button data-action="ffmpeg-all">复制批量ffmpeg</button> </div> <div class="tm-log"></div> `; panel.addEventListener("click", async (event) => { const btn = event.target.closest("button"); if (!btn) return; const action = btn.dataset.action; if (action === "refresh") return refresh(); if (action === "current") return downloadCurrent(); if (action === "all") return downloadAll(); if (action === "stop-all") return stopBackgroundQueue(); if (action === "ffmpeg-current") return copyCurrentFfmpeg(); if (action === "ffmpeg-all") return copyAllFfmpeg(); }); document.body.appendChild(panel); renderQueueState(); } async function init() { ensurePanel(); await refresh(); setInterval(async () => { if (location.href !== state.href) { state.href = location.href; if (state.isQueueRunning || state.queueMode === "stopping") { stopBackgroundQueue("页面地址变化", { hard: true, clearProgress: true, }); } state.meta = null; state.formats = []; state.currentPlayInfo = null; addLog("页面地址变化,重新读取..."); await refresh(); } }, 1200); } init(); })(); 源码github地址 1 个帖子 - 1 位参与者 阅读完整话题