// ==UserScript== // @name Switch520下载优化 // @namespace Switch520_Download_Optimize // @version 1.8 // @description Switch520点击下载按钮后,直接弹出下载地址,无需反复跳转、输入密码 // @author 一身惆怅 // @match https://www.gamer520.com/* // @match https://gamer520.com/* // @match https://*.gamers520.com/* // @connect gamer520.com // @connect www.gamer520.com // @connect gamers520.com // @connect 520.gamers520.com // @connect * // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_openInTab // @grant GM_setClipboard // @grant GM_addStyle // @run-at document-end // @noframes // ==/UserScript== (() => { 'use strict'; const IS_SOURCE = /(^|\.)gamer520\.com$/.test(location.hostname); const IS_TARGET = /(^|\.)gamers520\.com$/.test(location.hostname); const PENDING_KEY = 'g520_pending'; const CACHE_TTL = 6 * 3600e3; const HOP_TIMEOUT = 8000; const PREFETCH = true; const BLOCK_SEL = 'div.bdp-container'; const GO_SEL = 'a[href*="/go?post_id="], a[href*="/go/?post_id="]'; const PLATFORMS = [['baidu', /pan\.baidu\.com/i], ['quark', /pan\.quark\.cn/i], ['xunlei', /pan\.xunlei\.com/i]]; const toUrl = (u, base = location.href) => { try { return new URL(u, base); } catch { return null; } }; const platformOf = (url) => PLATFORMS.find(([, re]) => re.test(url))?.[0]; const hasAny = (r) => Boolean(r?.baidu || r?.quark || r?.xunlei); const extractLinks = (doc) => { const scope = doc.querySelector('.bdp-container, .bdp-grid') ?? doc; const out = { baidu: null, quark: null, xunlei: null, unzip: '' }; for (const img of scope.querySelectorAll('img')) { const qr = img.getAttribute('src') || img.getAttribute('data-src') || ''; const url = toUrl(qr)?.searchParams.get('data')?.trim(); const key = url && platformOf(url); if (key) out[key] ??= { url, qr }; } for (const a of scope.querySelectorAll('a[href]')) { const url = a.getAttribute('href'); const key = url.includes('/s/') && platformOf(url); if (key) out[key] ??= { url }; } for (const [key] of PLATFORMS) { if (out[key]) out[key].pwd = toUrl(out[key].url)?.searchParams.get('pwd') ?? ''; } if (out.baidu && !out.baidu.pwd) { out.baidu.pwd = scope.querySelector('.bdp-baidu .bdp-pwd-box strong')?.textContent.trim() ?? ''; } const head = scope.querySelector('.bdp-header') ?? doc.querySelector('.bdp-header'); out.unzip = head?.textContent.match(/解压密码\s*[::]\s*([^\s||]+)/)?.[1] ?? ''; return out; }; if (IS_TARGET) { const pending = GM_getValue(PENDING_KEY, null); if (pending && Date.now() - pending.ts < 90e3) { const attempt = (left) => { const data = extractLinks(document); if (!hasAny(data) && left > 0) return setTimeout(attempt, 500, left - 1); GM_setValue(`g520_result_${pending.id}`, { ts: Date.now(), data, page: location.href }); GM_deleteValue(PENDING_KEY); setTimeout(() => window.close(), 300); }; attempt(10); } return; } if (!IS_SOURCE) return; const gmGet = (url) => new Promise((resolve, reject) => GM_xmlhttpRequest({ method: 'GET', url, timeout: HOP_TIMEOUT, headers: { Referer: location.href }, onload: (r) => resolve({ text: r.responseText ?? '', finalUrl: r.finalUrl || url }), onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), })); const JS_JUMPS = [ /location\.(?:replace|assign)\(\s*['"]([^'"]+)['"]/i, /(?:window\.|document\.|top\.|self\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i, /window\.open\(\s*['"]([^'"]+)['"]/i, ]; const findNext = (text, doc, base) => { const abs = (u) => toUrl(u.replaceAll('&', '&'), base)?.href ?? null; const refresh = doc.querySelector('meta[http-equiv="refresh" i]')?.getAttribute('content')?.match(/url\s*=\s*['"]?([^'"]+)/i); if (refresh) return abs(refresh[1]); for (const re of JS_JUMPS) { const m = text.match(re); if (m && !/^javascript:/i.test(m[1])) return abs(m[1]); } const a = doc.querySelector('a[href*="gamers520.com/"], a[href*="/go?"], a#go, a.go-link'); return a ? abs(a.getAttribute('href')) : null; }; const parser = new DOMParser(); const resolveByRequest = async (url) => { for (let hop = 0, cur = url; hop < 6; hop++) { const { text, finalUrl } = await gmGet(cur); if (text.length < 12000 && /Just a moment|cf-mitigated|challenge-platform/i.test(text)) { throw new Error('Cloudflare 验证拦截'); } const doc = parser.parseFromString(text, 'text/html'); const data = extractLinks(doc); if (hasAny(data)) return { data, page: finalUrl }; const next = findNext(text, doc, finalUrl); if (!next || next === cur) break; cur = next; } throw new Error('未在跳转链中找到网盘链接'); }; const resolveByTab = (url, id) => { const key = `g520_result_${id}`; const { promise, resolve, reject } = Promise.withResolvers(); GM_deleteValue(key); GM_setValue(PENDING_KEY, { id, ts: Date.now() }); const tab = GM_openInTab(url, { active: false, insert: true, setParent: true }); const finish = () => { clearTimeout(timer); GM_removeValueChangeListener(listener); try { tab?.close?.(); } catch { } }; const listener = GM_addValueChangeListener(key, (_k, _o, val) => { if (!val) return; finish(); hasAny(val.data) ? resolve(val) : reject(new Error('目标页未找到网盘链接')); }); const timer = setTimeout(() => { finish(); GM_deleteValue(PENDING_KEY); reject(new Error('后台标签页超时')); }, 45e3); return promise; }; const resolveLinks = async ({ goUrl, id }, allowTab) => { try { return await resolveByRequest(goUrl); } catch (e) { if (!allowTab) throw e; console.warn('[G520] 直连失败,切换后台标签页:', e.message); setStatus('直连受限,正在通过后台标签页解析'); return resolveByTab(goUrl, id); } }; const cacheKey = (id) => `g520_cache_${id}`; const readCache = (id) => { const c = GM_getValue(cacheKey(id), null); return hasAny(c?.data) && Date.now() - c.ts < CACHE_TTL ? c : null; }; const inflight = new Map(); const fetchLinks = (ctx, allowTab) => { const running = inflight.get(ctx.id); if (running) { return !allowTab || running.allowTab ? running.promise : running.promise.catch(() => fetchLinks(ctx, true)); } const promise = resolveLinks(ctx, allowTab) .then(({ data, page }) => { const entry = { ts: Date.now(), data, page }; GM_setValue(cacheKey(ctx.id), entry); return entry; }) .finally(() => inflight.delete(ctx.id)); inflight.set(ctx.id, { promise, allowTab }); return promise; }; const qrCache = new Map(); const qrSource = ({ qr, url }) => qr?.replace(/size=\d+x\d+/, 'size=320x320') ?? `https://api.qrserver.com/v1/create-qr-code/?size=320x320&margin=0&data=${encodeURIComponent(url)}`; const loadQr = (item) => { const url = qrSource(item); if (!qrCache.has(url)) { const { promise, resolve } = Promise.withResolvers(); const fallback = () => resolve(url); GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', timeout: 15e3, anonymous: true, onload: ({ status, response }) => { if (status !== 200 || !response) return fallback(); const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = fallback; reader.readAsDataURL(response); }, onerror: fallback, ontimeout: fallback, }); qrCache.set(url, promise); } return qrCache.get(url); }; const ICON = { close: '', qr: '', link: '', copy: '', open: '', refresh: '', stack: '', check: '', alert: '', }; const CSS = ` :host{all:initial} *,*::before,*::after{box-sizing:border-box} .ld{ --bg:#0B0D12;--surface:#11151D;--card:#10141C;--sunken:#0A0D12; --text:#E8EDF5;--text-2:#929BAA;--text-3:#687386; --line:rgba(255,255,255,.08);--line-2:rgba(255,255,255,.12); --accent:#4B8DFF;--r-lg:14px;--r-md:10px;--r-sm:8px; --sans:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif; --mono:ui-monospace,SFMono-Regular,"Cascadia Mono",Consolas,Menlo,monospace; font-family:var(--sans);font-size:14px;line-height:1.5;color:var(--text); -webkit-font-smoothing:antialiased; } button{font:inherit;color:inherit;background:none;border:0;margin:0;padding:0;cursor:pointer} a{color:inherit;text-decoration:none} svg{display:block;flex:none} :focus-visible{outline:2px solid rgba(75,141,255,.7);outline-offset:2px} .ld-mask{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:16px; background:rgba(4,5,8,.72);backdrop-filter:blur(2px);opacity:0;transition:opacity .18s ease} .ld-mask.show{opacity:1} .ld-panel{position:relative;display:flex;flex-direction:column;width:min(920px,100%);max-height:calc(100vh - 32px); background:var(--bg);border:1px solid var(--line);border-radius:var(--r-lg); box-shadow:0 1px 0 rgba(255,255,255,.04) inset,0 24px 64px -12px rgba(0,0,0,.7),0 8px 24px rgba(0,0,0,.35); transform:translateY(8px);transition:transform .18s ease} .ld-mask.show .ld-panel{transform:none} .ld-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 24px 0} .ld-title{font-size:16px;font-weight:600;letter-spacing:.02em;color:var(--text)} .ld-close{display:grid;place-items:center;width:32px;height:32px;border-radius:var(--r-sm);color:var(--text-2); border:1px solid transparent;transition:background .15s,color .15s,border-color .15s} .ld-close:hover{background:var(--surface);border-color:var(--line);color:var(--text)} .ld-close:active{background:var(--sunken)} .ld-game{margin:14px 24px 0;padding-bottom:18px;border-bottom:1px solid var(--line);font-size:16px;font-weight:600;line-height:1.45;color:var(--text); display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word} .ld-body{flex:1 1 auto;min-height:0;overflow:auto;padding:20px 24px} .ld-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:20px} .ld-secret{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;padding:12px 14px;text-align:left; background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);transition:border-color .15s,background .15s} .ld-secret:hover{border-color:var(--line-2);background:#141922} .ld-secret:active{background:var(--sunken)} .ld-secret-k{display:block;font-size:12px;color:var(--text-3)} .ld-secret-v{display:block;margin-top:2px;font-family:var(--mono);font-size:15px;letter-spacing:.04em;color:var(--text);word-break:break-all} .ld-secret-i{color:var(--text-3);transition:color .15s} .ld-secret:hover .ld-secret-i{color:var(--text-2)} .ld-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} @media (max-width:880px){.ld-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} @media (max-width:600px){.ld-grid{grid-template-columns:minmax(0,1fr)}.ld-head,.ld-body{padding-left:16px;padding-right:16px}.ld-game{margin-left:16px;margin-right:16px}.ld-foot{padding-left:16px;padding-right:16px}} .ld-card{--brand:#4B8DFF;display:flex;flex-direction:column;gap:12px;min-width:0;padding:16px; background:var(--card);border:1px solid var(--line);border-radius:var(--r-md);transition:border-color .15s} .ld-card:hover{border-color:var(--line-2)} .ld-card.baidu{--brand:#4B8DFF}.ld-card.quark{--brand:#9067F9}.ld-card.xunlei{--brand:#28B8E8} .ld-card-top{display:flex;align-items:center;gap:10px;min-width:0} .ld-ico{display:grid;place-items:center;width:36px;height:36px;flex:none;border-radius:var(--r-sm);background:var(--sunken);border:1px solid var(--line)} .ld-ico svg{width:24px;height:24px} .ld-name{min-width:0;flex:1} .ld-name b{display:block;font-size:14px;font-weight:600;color:var(--text)} .ld-name small{display:block;font-size:11px;letter-spacing:.08em;color:var(--text-3)} .ld-qrbtn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:var(--r-sm);font-size:12px;color:var(--text-2); border:1px solid var(--line);background:var(--sunken);transition:color .15s,border-color .15s} .ld-qrbtn:hover,.ld-qrbtn[aria-expanded="true"]{color:var(--text);border-color:var(--line-2)} .ld-url{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;min-height:62px;background:var(--sunken);border:1px solid var(--line);border-radius:var(--r-sm); font-family:var(--mono);font-size:12px;line-height:1.55;color:#C7CEDA;overflow-wrap:anywhere;word-break:break-all} .ld-url svg{margin-top:3px;color:var(--text-3)} .ld-url span{min-width:0} .ld-code{display:flex;align-items:center;gap:8px;height:24px;font-size:12px;color:var(--text-3)} .ld-code b{display:inline-flex;align-items:center;height:24px;padding:0 8px;border-radius:6px;font-family:var(--mono);font-size:13px;font-weight:600;letter-spacing:.08em; color:var(--text);background:color-mix(in srgb,var(--brand) 12%,transparent);border:1px solid color-mix(in srgb,var(--brand) 26%,transparent)} .ld-code i{font-style:normal;color:var(--text-3)} .ld-btns{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-top:auto} .ld-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;border-radius:var(--r-sm);font-size:13px;font-weight:600; transition:filter .15s,background .15s,border-color .15s,transform .12s} .ld-btn:active{transform:translateY(1px)} .ld-btn.primary{color:#fff;background:color-mix(in srgb,var(--brand) 82%,#0B0D12);border:1px solid color-mix(in srgb,var(--brand) 90%,#fff 10%)} .ld-btn.primary:hover{filter:brightness(1.1)} .ld-btn.secondary{color:var(--text);background:var(--surface);border:1px solid var(--line-2)} .ld-btn.secondary:hover{background:#171C26;border-color:rgba(255,255,255,.18)} .ld-btn[disabled],.ld-btn[aria-disabled="true"]{opacity:.4;pointer-events:none} .ld-card.off .ld-ico{filter:grayscale(1);opacity:.55} .ld-card.off .ld-url{color:var(--text-3);font-family:var(--sans)} .ld-foot{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 16px;padding:12px 24px 16px;border-top:1px solid var(--line);font-size:12px;color:var(--text-3)} .ld-actions{display:flex;flex-wrap:wrap;gap:4px} .ld-act{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:var(--r-sm);color:var(--text-2);transition:background .15s,color .15s} .ld-act:hover{background:var(--surface);color:var(--text)} .ld-act:active{background:var(--sunken)} .ld-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;min-height:240px;text-align:center;color:var(--text-2)} .ld-spin{width:22px;height:22px;border-radius:50%;border:2px solid var(--line-2);border-top-color:var(--accent);animation:ld-spin .8s linear infinite} @keyframes ld-spin{to{transform:rotate(360deg)}} .ld-state.err{color:#E58A9B} .ld-state.err p{margin:0;max-width:420px;color:var(--text-2)} .ld-state .ld-btns{width:min(320px,100%);margin-top:4px} .ld-pop{position:fixed;z-index:2147483646;padding:12px 12px 10px;background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-md); box-shadow:0 16px 40px rgba(0,0,0,.55);opacity:0;transform:translateY(4px);pointer-events:none;transition:opacity .15s,transform .15s;text-align:center} .ld-pop.show{opacity:1;transform:none} .ld-pop-img{display:grid;place-items:center;width:176px;height:176px;padding:10px;background:#fff;border-radius:var(--r-sm)} .ld-pop-img img{display:block;width:156px;height:156px;image-rendering:pixelated} .ld-pop-img .ld-spin{border-color:#E3E6EB;border-top-color:#4B8DFF} .ld-pop-err{font-size:12px;color:var(--text-3)} .ld-pop-cap{margin-top:8px;font-size:12px;color:var(--text-2)} .ld-toast{position:fixed;left:50%;bottom:40px;z-index:2147483647;display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:var(--r-sm); font-size:13px;color:var(--text);background:#151A23;border:1px solid var(--line-2);box-shadow:0 10px 30px rgba(0,0,0,.45); transform:translate(-50%,6px);opacity:0;transition:opacity .15s,transform .15s;pointer-events:none} .ld-toast svg{color:#5FD08A} .ld-toast.show{opacity:1;transform:translate(-50%,0)} .ld-trigger{display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 16px;margin:0 0 12px;border-radius:10px;font-size:13px;font-weight:600;letter-spacing:.04em; color:#E8EDF5;background:#11151D;border:1px solid rgba(255,255,255,.12);box-shadow:0 4px 14px rgba(0,0,0,.25);transition:background .15s,border-color .15s} .ld-trigger:hover{background:#171C26;border-color:rgba(255,255,255,.2)} .ld-trigger i{width:6px;height:6px;border-radius:50%;background:#4B8DFF} .ld-hint{margin:0 0 12px;font-size:12px;color:var(--text-2)} .ld-hint:empty{display:none} .ld-sk{display:block;height:10px;border-radius:4px;background:linear-gradient(90deg,#151a23 0%,#1b212c 50%,#151a23 100%);background-size:200% 100%;animation:ld-shimmer 1.4s ease-in-out infinite} @keyframes ld-shimmer{from{background-position:100% 0}to{background-position:-100% 0}} .ld-card.sk .ld-url{flex-direction:column;gap:8px;justify-content:center} .ld-card.sk .ld-btn{pointer-events:none;opacity:.35} @media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation-duration:1.5s!important}} `; const LOGO = { baidu: '', quark: '', xunlei: '', }; const DISKS = [ { key: 'baidu', name: '百度网盘', en: 'BAIDU PAN' }, { key: 'quark', name: '夸克网盘', en: 'QUARK' }, { key: 'xunlei', name: '迅雷云盘', en: 'THUNDER' }, ]; const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => `${c.charCodeAt(0)};`); const el = (className, attrs = {}) => { const node = document.createElement('div'); node.className = className; for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v); return node; }; const makeHost = (id) => { const host = document.createElement('div'); host.id = id; const root = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = CSS; const wrap = el('ld'); root.append(style, wrap); return { host, wrap }; }; const UI = makeHost('loot-drop-root'); document.documentElement.append(UI.host); let toastEl, toastTimer; const toast = (msg) => { toastEl ??= UI.wrap.appendChild(el('ld-toast', { role: 'status' })); toastEl.innerHTML = `${ICON.check}${esc(msg)}`; toastEl.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600); }; const copy = (text, label) => { try { GM_setClipboard(text, 'text'); } catch { navigator.clipboard?.writeText(text); } toast(label ? `已复制 · ${label}` : '已复制'); }; let mask = null, body = null, lastFocus = null, view = null, runToken = 0; let pop = null, popOwner = null; const setStatus = (text) => { const hint = mask?.querySelector('.ld-hint'); if (hint) hint.textContent = text; }; const dropFoot = () => mask?.querySelector('.ld-foot')?.remove(); const placePop = (btn) => { const { top, right, bottom } = btn.getBoundingClientRect(); const w = 202, h = 230, gap = 8; pop.style.left = `${Math.min(Math.max(8, right - w), innerWidth - w - 8)}px`; pop.style.top = `${top - h - gap < 8 ? bottom + gap : top - h - gap}px`; }; const hideQr = () => { popOwner?.setAttribute('aria-expanded', 'false'); popOwner = null; pop?.classList.remove('show'); }; const showQr = (btn) => { const { key, name } = DISKS.find((d) => d.key === btn.dataset.qr); const item = view?.data?.[key]; if (!item) return; pop ??= UI.wrap.appendChild(el('ld-pop', { role: 'tooltip' })); popOwner?.setAttribute('aria-expanded', 'false'); popOwner = btn; btn.setAttribute('aria-expanded', 'true'); pop.innerHTML = `
解析失败:${esc(err.message ?? err)}