// ==UserScript== // @name 下载路由 | Download Router // @namespace https://github.com/mks155 // @homepageURL https://github.com/mks155/DownloadRouter // @icon https://mks155.github.io/assets/svg/downloadrouter.svg // @version 1.0.3 // @description 接管浏览器任意下载链接,快速调起分发给迅雷 / 比特彗星等客户端。识别漏网就按住 Alt+右键。 | Take over any download link in the browser, quickly launch and distribute it to clients such as Thunderbolt / BitComet. To identify any missed links, hold down Alt and right-click. // @author mks155 // @license MIT // @match *://*/* // @run-at document-idle // @sandbox raw // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @noframes // ==/UserScript== (() => { 'use strict'; const LS_KEY = 'download_router_config'; // 顶层只取引用不解析;解析推迟到打开设置面板时 const SELF_SRC = (document.currentScript && document.currentScript.textContent) || ''; const META = (typeof GM_info !== 'undefined' && GM_info.script) || {}; /** 元数据块是版本号与图标的唯一来源。TM 免申请即给 GM_info,其余引擎走自身源码 */ function meta(key) { if (key in META) return META[key] || ''; const m = SELF_SRC && new RegExp('^[ \\t]*//[ \\t]*@' + key + '[ \\t]+(.+?)[ \\t]*$', 'm').exec(SELF_SRC); return m ? m[1] : ''; } const DEFAULT_CONFIG = { launchMethod: 'href', // href | anchor forceOnAlt: true, theme: 'auto', // auto | light | dark }; const LAUNCH_METHODS = { href: '地址栏', anchor: '链接触发' }; const THEMES = { auto: { name: '跟随系统', desc: '按浏览器的深浅色设置自动切换' }, light: { name: '浅色', desc: '始终用浅色菜单' }, dark: { name: '深色', desc: '始终用深色菜单' }, }; const CONFIG = { ...DEFAULT_CONFIG, ...loadConfig() }; if (!LAUNCH_METHODS[CONFIG.launchMethod]) CONFIG.launchMethod = 'href'; if (!THEMES[CONFIG.theme]) CONFIG.theme = 'auto'; function loadConfig() { try { return GM_getValue(LS_KEY, {}) || {}; } catch { return {}; } } function saveConfig() { try { GM_setValue(LS_KEY, CONFIG); return true; } catch { return false; } } // ── Base64 ──────────────────────────────────────────── /** UTF-8 而非 btoa 直吃:URL 常含中文文件名 */ function utf8ToBase64(str) { let bin = ''; for (const b of new TextEncoder().encode(str)) bin += String.fromCharCode(b); return btoa(bin); } function base64ToUtf8(b64) { let s = b64.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/'); const pad = s.length % 4; if (pad === 2) s += '=='; else if (pad === 3) s += '='; else if (pad === 1) throw new Error('非法 base64'); return new TextDecoder().decode(Uint8Array.from(atob(s), (c) => c.charCodeAt(0))); } // ── 下载器 ──────────────────────────────────────────── const COMMON = /^(https?|ftp):/i; const NATIVE = /^(magnet|ed2k|thunder|thunderbolt|flashget|qqdl|ftp):/i; const HANDLERS = { thunder: { name: '迅雷', icon: 'data:image/png;base64,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', scheme: 'thunder://', passthrough: NATIVE, accept: COMMON, build: (url) => 'thunder://' + utf8ToBase64(`AA${url}ZZ`), parse: (link) => unwrap(base64ToUtf8(link.replace(/^thunder:\/\//i, '')), 'AA', 'ZZ'), hint: '安装迅雷 PC 客户端后自动可用', }, bitcomet: { name: '比特彗星', icon: 'data:image/png;base64,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', scheme: 'bc://', passthrough: /^magnet:/i, accept: COMMON, /** &exec= 不能省,缺了客户端会弹「无法解析」 */ build(url, filename) { const n = sanitizeName(filename); return ( 'bc://http/' + utf8ToBase64( `AA/${encodeURIComponent(n)}/?url=${encodeURIComponent(url)}&exec=${encodeURIComponent(n)}ZZ` ) ); }, parse(link) { const m = link.match(/^bc:\/\/(?:http|bt)\/(.+)$/i); if (!m) return null; const text = base64ToUtf8(m[1]); const i = text.indexOf('/?url='); if (i === -1) return unwrap(text, 'AA', 'ZZ'); const raw = text.slice(i + 6).replace(/&exec=[\s\S]*$/, '').replace(/ZZ$/, ''); try { return decodeURIComponent(raw); } catch { return raw; } }, hint: '安装比特彗星客户端后自动可用', }, }; const HANDLER_IDS = Object.keys(HANDLERS); function unwrap(text, head, tail) { if (text.startsWith(head) && text.endsWith(tail)) text = text.slice(head.length, -tail.length); text = text.trim(); if (NATIVE.test(text)) return text; if (COMMON.test(text)) { try { new URL(text); return text; } catch { return null; } } return null; } const sanitizeName = (s) => String(s || '').replace(/[!?&|`"'*\/:<>\\]/g, '_'); function buildLink(id, rawUrl, name = '') { const h = HANDLERS[id]; const url = String(rawUrl).trim(); if (!h || !url) return null; if (h.passthrough.test(url)) return url; if (!h.accept.test(url)) return null; return h.build(url, name || guessName(url)); } function parseLink(id, link) { const h = HANDLERS[id]; if (!h) return null; try { return h.parse(String(link)); } catch { return null; } } /** 只判协议,不做编码,用于快速筛选 */ function anyHandlerAccepts(url) { return HANDLER_IDS.some((id) => { const h = HANDLERS[id]; return h.passthrough.test(url) || h.accept.test(url); }); } // ── 链接识别 ────────────────────────────────────────── const RE_ARCHIVE = /\.(?:zip|rar|7z|tar|gz|tgz|bz2|xz|zst|apk|ipa|exe|msi|msix|msu|appx|dmg|pkg|deb|rpm|iso|img|bin|vhd|vhdx|ova|ovf|cab|jar|war|torrent|magnet|ed2k)(?:$|[?#])/i; const RE_DOC = /\.(?:pdf|epub|mobi|djvu|docx?|xlsx?|pptx?|csv|txt|rtf|chm)(?:$|[?#])/i; const RE_HINT = /[?&](?:download|dl|file|attachment|attach|filename)=|\/(?:download|downloads|down|dl|attachment|attachments|release[s]?\/download|files?)(?:[/?#]|$)/i; const RE_SCHEME = /^(?:https?|ftp|magnet|ed2k|thunder):/i; /** force=true 时跳过启发式判断,只看有没有下载器收得了 */ function isDownloadLink(rawUrl, force = false) { if (!rawUrl) return false; const url = rawUrl.trim(); if (!RE_SCHEME.test(url)) return false; if (force) return anyHandlerAccepts(url); if (/^(magnet|ed2k|thunder):/i.test(url)) return true; return RE_ARCHIVE.test(url) || RE_DOC.test(url) || RE_HINT.test(url); } function guessName(rawUrl) { try { const u = new URL(rawUrl, location.href); const seg = u.pathname.split('/').filter(Boolean).pop() || ''; let name; try { name = decodeURIComponent(seg); } catch { name = seg; } if (name && /\.[a-z0-9]{1,8}$/i.test(name)) return name; for (const k of ['filename', 'file', 'name', 'title', 'fn']) { const v = u.searchParams.get(k); if (v) return v; } return (u.hostname + u.pathname).replace(/\/$/, '') || String(rawUrl); } catch { return String(rawUrl); } } // ── 唤起 ────────────────────────────────────────────── /** 唤起须在 click 同步栈内完成,否则 Chrome 判定为非用户手势而不放行 */ function launch(rawUrl, id, name = '') { const link = buildLink(id, rawUrl, name); if (!link) return false; if (CONFIG.launchMethod === 'anchor') { const a = document.createElement('a'); a.href = link; a.style.display = 'none'; a.click(); a.remove(); } else { location.href = link; } return true; } async function copyText(text) { try { await navigator.clipboard.writeText(text); notify('已复制链接地址'); } catch { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;opacity:0'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); ta.remove(); notify(ok ? '已复制链接地址' : '复制失败,请手动复制', ok ? '' : 'err'); } } // ── 配色(菜单与弹窗共用) ────────────────────────────── const THEME_VARS = ` :host { --bg:#fff; --fg:#1f2328; --border:#c8ced8; --sep:#e6e9ee; --tag:#7b8492; --dim:#9aa1ad; --dim-bg:#eef0f3; --dim-fg:#6b7280; --accent:#2f6fed; --btn-bg:#f7f8fa; --btn-fg:#1f2328; --shadow:0 8px 28px rgba(15,23,42,.18); } @media (prefers-color-scheme: dark) { :host:not([data-theme="light"]) { --bg:#1c1f26; --fg:#e6e9ef; --border:#454b5a; --sep:#333845; --tag:#98a0af; --dim:#6b7385; --dim-bg:#3a3f4d; --dim-fg:#a8b0c0; --accent:#4d8bff; --btn-bg:#272b35; --btn-fg:#e6e9ef; --shadow:0 8px 28px rgba(0,0,0,.5); } } :host([data-theme="dark"]) { --bg:#1c1f26; --fg:#e6e9ef; --border:#454b5a; --sep:#333845; --tag:#98a0af; --dim:#6b7385; --dim-bg:#3a3f4d; --dim-fg:#a8b0c0; --accent:#4d8bff; --btn-bg:#272b35; --btn-fg:#e6e9ef; --shadow:0 8px 28px rgba(0,0,0,.5); } :host([data-theme="light"]) { --bg:#fff; --fg:#1f2328; --border:#c8ced8; --sep:#e6e9ee; --tag:#7b8492; --dim:#9aa1ad; --dim-bg:#eef0f3; --dim-fg:#6b7280; --accent:#2f6fed; --btn-bg:#f7f8fa; --btn-fg:#1f2328; --shadow:0 8px 28px rgba(15,23,42,.18); } `; // ── 页面内轻提示 ────────────────────────────────────── const TOAST_CSS = ` :host { all: initial } * { box-sizing:border-box; font-family:-apple-system, "Segoe UI", "Microsoft YaHei", sans-serif } ${THEME_VARS} .box { position:fixed; right:16px; bottom:16px; z-index:2147483647; display:flex; flex-direction:column; align-items:flex-end; gap:8px; pointer-events:none; max-width:min(340px, calc(100vw - 32px)); } .t { display:flex; align-items:flex-start; gap:8px; padding:9px 13px; border-radius:9px; border:1px solid var(--border); background:var(--bg); box-shadow:var(--shadow); color:var(--fg); font-size:12.5px; line-height:1.5; word-break:break-word; animation:tin .16s ease-out; } .t .dot { flex:none; width:7px; height:7px; margin-top:5px; border-radius:50%; background:var(--accent) } .t.ok { border-color:#2f9e68 } .t.ok .dot { background:#2f9e68 } .t.err { border-color:#d3453b } .t.err .dot { background:#d3453b } .t.out { animation:tout .16s ease-in forwards } @keyframes tin { from { opacity:0; transform:translateY(6px) } } @keyframes tout { to { opacity:0; transform:translateY(4px) } } @media (prefers-reduced-motion: reduce) { .t, .t.out { animation:none } } `; let toastHost = null; let toastBox = null; function notify(text, kind = '') { console.info(`[下载路由] ${text}`); if (!document.body) return; if (!toastHost) { toastHost = document.createElement('div'); toastHost.style.cssText = 'all:initial;position:fixed;z-index:2147483647;'; const shadow = toastHost.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = TOAST_CSS; toastBox = document.createElement('div'); toastBox.className = 'box'; shadow.append(style, toastBox); document.body.appendChild(toastHost); } if (CONFIG.theme !== 'auto') toastHost.setAttribute('data-theme', CONFIG.theme); const el = document.createElement('div'); el.className = kind ? 't ' + kind : 't'; const dot = document.createElement('span'); dot.className = 'dot'; const tx = document.createElement('span'); tx.textContent = text; el.append(dot, tx); toastBox.appendChild(el); while (toastBox.childElementCount > 3) toastBox.firstElementChild.remove(); setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 320); }, kind === 'err' ? 5000 : 2600); } // ── 右键菜单 ────────────────────────────────────────── const CTX_CSS = ` :host { all: initial; } * { box-sizing: border-box; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; } ${THEME_VARS} .ctx { position:fixed; display:none; z-index:2147483647; min-width:184px; padding:4px; background:var(--bg); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); color-scheme:light dark; } .ctx.show { display:block } .ctx .mi { display:flex; align-items:center; gap:7px; padding:7px 10px; font-size:12px; color:var(--fg); cursor:pointer; border-radius:5px; white-space:nowrap; } .ctx .mi .ic { flex:none; width:15px; height:15px; border-radius:2px; object-fit:contain } .ctx .mi.dim .ic { opacity:.42; filter:grayscale(1) } .ctx .mi:hover { background:var(--accent); color:#fff } .ctx .mi.dim { color:var(--dim) } .ctx .mi.dim:hover { background:var(--dim-bg); color:var(--dim-fg) } .ctx .sep { height:1px; background:var(--sep); margin:4px 2px } .ctx .tag { padding:6px 10px 4px; font-size:10px; color:var(--tag); letter-spacing:.06em; text-transform:uppercase; user-select:none; } `; let ctxHost = null; let ctxUrl = ''; let ctxName = ''; function buildCtxItems(url, name, force) { const isBt = /^(magnet|ed2k|thunder):/i.test(url); const rows = []; let usable = false; for (const id of HANDLER_IDS) { const h = HANDLERS[id]; const ok = buildLink(id, url, name) !== null; if (ok) usable = true; if (!ok && !force) continue; const title = ok ? isBt ? '透传原始协议,由系统默认的 BT/磁力处理器接手' : `${h.scheme} · ${h.hint}` : `${h.name}不收这个协议,点了会提示失败`; rows.push( `