// ==UserScript== // @name 流媒体聚合搜索 & 下载器快捷启动(增强版 v3.6) // @namespace https://workbuddy.cn // @version 3.6 // @description 聚合搜索主流正版流媒体(可自定义平台 + 分组 + 单独开关),支持更多下载器,收藏搜索历史(可导出),深色模式。仅用于合法内容。 // @author WorkBuddy // @license MIT // @match *://*/* // @grant GM_openInTab // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; /* ============================ 默认数据 ============================ */ // 全部为合法授权平台:国际流媒体 + 国内电视台官方平台 + 国内垂直/综合正版平台 const DEFAULT_PLATFORMS = [ // —— 国际正版流媒体 —— { name: 'Netflix', url: 'https://www.netflix.com/search?q={q}', enabled: true, group: '国际正版流媒体' }, { name: 'Disney+', url: 'https://www.disneyplus.com/search?q={q}', enabled: true, group: '国际正版流媒体' }, { name: 'Prime Video', url: 'https://www.amazon.com/s?k={q}&i=instant-video', enabled: true, group: '国际正版流媒体' }, { name: 'U-NEXT', url: 'https://video.unext.jp/search?query={q}', enabled: true, group: '国际正版流媒体' }, { name: 'Freevee', url: 'https://www.amazon.com/s?k={q}&i=instant-video&search-type=freevee', enabled: true, group: '国际正版流媒体' }, { name: 'Crackle', url: 'https://www.crackle.com/search?q={q}', enabled: true, group: '国际正版流媒体' }, { name: 'Movist', url: 'https://movist.com/search?q={q}', enabled: true, group: '国际正版流媒体' }, { name: 'Vudu', url: 'https://www.vudu.com/content/browse/search/{q}', enabled: true, group: '国际正版流媒体' }, { name: 'Tubi', url: 'https://tubitv.com/search/{q}', enabled: true, group: '国际正版流媒体' }, { name: 'Pluto TV', url: 'https://pluto.tv/search/{q}', enabled: true, group: '国际正版流媒体' }, // —— 国内电视台旗下官方平台 + 国内垂直/综合正版平台 —— { name: '央视频', url: 'https://www.yangshipin.cn/search/result?query={q}', enabled: true, group: '国内正版平台' }, { name: '芒果TV', url: 'https://so.mgtv.com/so/?k={q}', enabled: true, group: '国内正版平台' }, { name: '百视通', url: 'https://www.bestv.com.cn/', enabled: true, group: '国内正版平台' }, // 大屏为主,Web 搜索端点未确认,指向官网首页 { name: 'bilibili', url: 'https://search.bilibili.com/all?keyword={q}', enabled: true, group: '国内正版平台' }, { name: '1905电影网', url: 'https://www.1905.com/search/?q={q}', enabled: true, group: '国内正版平台' }, // 老电影/经典剧,官方授权 { name: '腾讯视频', url: 'https://v.qq.com/x/search/?q={q}', enabled: true, group: '国内正版平台' }, { name: '爱奇艺', url: 'https://so.iqiyi.com/so/q_{q}', enabled: true, group: '国内正版平台' }, { name: '优酷', url: 'https://so.youku.com/search_video/q_{q}', enabled: true, group: '国内正版平台' }, { name: '搜狐视频', url: 'https://so.tv.sohu.com/mts?wd={q}', enabled: true, group: '国内正版平台' }, // 搜索端点已确认 { name: 'PP视频', url: 'https://search.pptv.com/s_video?kw={q}', enabled: true, group: '国内正版平台' }, // 原 PPTV 聚力,搜索端点已确认 { name: '华数TV', url: 'https://www.wasu.cn/search/{q}', enabled: true, group: '国内正版平台' }, // 华数TV 搜索为路径式路由 /search/{关键词},由用户浏览器实测确认(WebFetch 不跑 JS 故只见到 SSR 壳) // —— 国际版(面向海外,与国内会员不通用)—— { name: '爱奇艺国际版', url: 'https://www.iq.com/search?keyword={q}', enabled: true, group: '国际版(海外)' }, // iq.com { name: '腾讯国际版WeTV', url: 'https://wetv.vip/search?q={q}', enabled: true, group: '国际版(海外)' }, // WeTV 搜索端点已确认 // —— 广电总局验收的OTT牌照方(参考条目:电视/大屏为主,无网页搜索端点,点击仅打开官网)—— { name: '未来电视(CNTV)', url: 'https://www.chinaott.com/', enabled: true, group: '广电OTT牌照方(参考)' }, { name: 'CIBN(国广)', url: 'https://www.cibn.cc/', enabled: true, group: '广电OTT牌照方(参考)' }, { name: '银河奇异果(央广)', url: 'https://app.iqiyi.com/tv/player/', enabled: true, group: '广电OTT牌照方(参考)' }, { name: '南方传媒云视听', url: 'https://www.snm.gd.cn/', enabled: true, group: '广电OTT牌照方(参考)' }, // 注:芒果TV(湖南广电)已在「国内正版平台」可搜索;百视通已在「国内正版平台」作参考。七家牌照方全部覆盖。 ]; // 分组顺序;默认折叠的分组 const GROUP_ORDER = ['国际正版流媒体', '国内正版平台', '广电OTT牌照方(参考)', '国际版(海外)', '其他']; const GROUP_COLLAPSED = { '国际版(海外)': true }; // type: qb | tr | aria | gopeed | open | copy const DEFAULT_DOWNLOADERS = [ { id: 'qb', name: 'qBittorrent', type: 'qb', webui: 'http://localhost:8080', user: 'admin', pass: 'adminadmin', enabled: true }, { id: 'tr', name: 'Transmission', type: 'tr', rpc: 'http://localhost:9091/transmission/rpc', user: '', pass: '', enabled: true }, { id: 'motrix', name: 'Motrix', type: 'aria', rpc: 'http://localhost:16800/jsonrpc', token: '', enabled: true }, { id: 'gopeed', name: 'gopeed', type: 'gopeed', api: 'http://localhost:9999', token: '', enabled: true }, { id: 'wugong', name: '迅雷蜈蚣', type: 'xunlei', webui: 'http://localhost:2345', enabled: true }, // cnk3x/xunlei Docker 版迅雷,标准端口 2345 { id: 'utw', name: 'uTorrent Web', type: 'open', webui: 'https://web.utorrent.com', enabled: true }, { id: 'thunder',name: '迅雷', type: 'copy', note: '复制后迅雷桌面端自动捕获剪贴板磁力链接', enabled: true }, { id: 'idm', name: 'IDM', type: 'copy', note: '复制下载地址,IDM 接管', enabled: false }, { id: 'fdm', name: 'FDM', type: 'copy', note: '复制后 FDM 接管', enabled: false }, { id: 'bitcomet',name:'BitComet', type: 'copy', note: '复制后 BitComet 接管', enabled: false }, { id: 'qdown', name: 'QDown', type: 'copy', note: '复制后 QDown 接管', enabled: false }, { id: 'xdown', name: 'XDown', type: 'copy', note: '复制后 XDown 接管', enabled: false }, { id: 'tixati', name: 'Tixati', type: 'copy', note: '复制后 Tixati 接管', enabled: false }, { id: 'biglybt',name: 'BiglyBT', type: 'copy', note: '复制后 BiglyBT 接管', enabled: false }, { id: 'frost', name: 'FrostWire', type: 'copy', note: '复制后 FrostWire 接管', enabled: false }, { id: 'folx', name: 'Folx', type: 'copy', note: '复制后 Folx 接管(macOS)', enabled: false }, ]; const HISTORY_KEY = 'wb_history'; const PLAT_KEY = 'wb_platforms'; const DL_KEY = 'wb_downloaders'; const THEME_KEY = 'wb_theme'; const HISTORY_MAX = 50; /* ============================ 存储 ============================ */ const enc = (s) => encodeURIComponent(s); function getArr(key, def) { const v = GM_getValue(key, null); if (v) { try { return JSON.parse(v); } catch (e) {} } return JSON.parse(JSON.stringify(def)); } function setArr(key, val) { GM_setValue(key, JSON.stringify(val)); } let PLATFORMS = getArr(PLAT_KEY, DEFAULT_PLATFORMS).map((p) => ({ enabled: true, ...p })); let DOWNLOADERS = getArr(DL_KEY, DEFAULT_DOWNLOADERS); let HISTORY = getArr(HISTORY_KEY, []); let THEME = GM_getValue(THEME_KEY, 'system'); // system | light | dark let GROUP_STATE = GM_getValue('wb_group_state', {}) || {}; // 分组折叠状态:group -> true 折叠 /* ============================ 下载器发送 ============================ */ function toast(msg) { let t = $('#wb-toast'); if (!t) { t = document.createElement('div'); t.id = 'wb-toast'; t.className = 'toast'; document.body.appendChild(t); } t.textContent = msg; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 1800); } function sendTo(d, url) { if (!url) { toast('请先粘贴下载链接'); return; } if (!d.enabled) { toast(d.name + ' 未启用'); return; } switch (d.type) { case 'qb': return qbAdd(d, url); case 'tr': return trAdd(d, url); case 'aria': return ariaAdd(d, url); case 'gopeed': return gopeedAdd(d, url); case 'xunlei': return xunleiAdd(d, url); case 'open': return GM_openInTab(d.webui, true); case 'copy': return (GM_setClipboard(url), toast('已复制,' + (d.note || d.name + ' 接管'))); default: toast('未知下载器类型'); } } function qbAdd(d, url) { const base = d.webui.replace(/\/$/, ''); GM_xmlhttpRequest({ method: 'POST', url: base + '/api/v2/auth/login', data: `username=${enc(d.user)}&password=${enc(d.pass)}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, onload: (r) => { const sid = (r.responseHeaders.match(/SID=([^;]+)/) || [])[1]; if (!sid) return toast('qBittorrent 登录失败'); GM_xmlhttpRequest({ method: 'POST', url: base + '/api/v2/torrents/add', data: new URLSearchParams({ urls: url }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': 'SID=' + sid }, onload: () => toast('已发送到 qBittorrent'), onerror: () => toast('qBittorrent 发送失败'), }); }, onerror: () => toast('qBittorrent 连接失败'), }); } function trAdd(d, url) { const req = { method: 'torrent-add', arguments: { filename: url } }; const post = (sid) => GM_xmlhttpRequest({ method: 'POST', url: d.rpc, data: JSON.stringify(req), headers: { 'Content-Type': 'application/json', ...(d.user ? { 'Authorization': 'Basic ' + btoa(d.user + ':' + d.pass) } : {}), ...(sid ? { 'X-Transmission-Session-Id': sid } : {}), }, onload: (r) => { if (r.status === 409) { const nsid = (r.responseHeaders.match(/X-Transmission-Session-Id:\s*([^\r\n]+)/i) || [])[1]; if (nsid) return post(nsid.trim()); return toast('Transmission 需要会话 ID'); } toast('已发送到 Transmission'); }, onerror: () => toast('Transmission 连接失败'), }); post(null); } function ariaAdd(d, url) { const payload = { jsonrpc: '2.0', id: 'wb', method: 'aria2.addUri', params: d.token ? [`token:${d.token}`, [url]] : [[url]] }; GM_xmlhttpRequest({ method: 'POST', url: d.rpc, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, onload: () => toast('已发送到 ' + d.name), onerror: () => toast(d.name + ' 连接失败'), }); } function gopeedAdd(d, url) { const base = d.api.replace(/\/$/, ''); GM_xmlhttpRequest({ method: 'POST', url: base + '/api/v1/task', data: JSON.stringify({ type: 'uri', uri: url }), headers: { 'Content-Type': 'application/json', ...(d.token ? { 'Authorization': 'Bearer ' + d.token } : {}), }, onload: () => toast('已发送到 gopeed'), onerror: () => toast('gopeed 连接失败'), }); } // 迅雷蜈蚣 = cnk3x/xunlei Docker 版迅雷,API 走 Synology 风格 pan-xunlei-com CGI // 要求:Docker 迅雷 Web 端(:2345)已登录;pan-auth 是从首页 uiauth() 提取的静态 token function xunleiAdd(d, url) { const base = d.webui.replace(/\/$/, ''); const H0 = { 'User-Agent': 'Mozilla/5.0' }; GM_xmlhttpRequest({ method: 'GET', url: base + '/webman/3rdparty/pan-xunlei-com/index.cgi/', headers: H0, onload: (r) => { const m = (r.responseText || '').match(/uiauth\(value\)\{\s*return\s*"([^"]+)"/); if (!m) return toast('迅雷蜈蚣:未找到 pan-auth(请确认已登录 :2345)'); const panAuth = m[1]; const cookie = (r.responseHeaders.match(/set-cookie:\s*([^\r\n]+)/i) || [])[1] || ''; const H = { 'pan-auth': panAuth, 'device-space': '', 'content-type': 'application/json', 'Accept': '*/*' }; if (cookie) H['Cookie'] = cookie.split(';')[0]; // 取设备 device_id GM_xmlhttpRequest({ method: 'GET', url: base + '/webman/3rdparty/pan-xunlei-com/index.cgi/drive/v1/tasks?type=user%23runner&device_space=', headers: H, onload: (r2) => { let deviceId; try { deviceId = JSON.parse(r2.responseText).tasks[0].params.target; } catch (e) { return toast('迅雷蜈蚣:获取设备失败(未登录?)'); } // 取根目录 parent_folder_id GM_xmlhttpRequest({ method: 'GET', url: base + '/webman/3rdparty/pan-xunlei-com/index.cgi/drive/v1/files?space=' + encodeURIComponent(deviceId) + '&limit=200&parent_id=&filters=%7B%22kind%22%3A%7B%22eq%22%3A%22drive%23folder%22%7D%7D&page_token=&device_space=', headers: H, onload: (r3) => { let parentId; try { const f = JSON.parse(r3.responseText).files[0]; parentId = f.parent_id || f.id; } catch (e) { return toast('迅雷蜈蚣:获取目录失败'); } // 取任务名(resource/list) GM_xmlhttpRequest({ method: 'POST', url: base + '/webman/3rdparty/pan-xunlei-com/index.cgi/drive/v1/resource/list?device_space=', headers: H, data: JSON.stringify({ urls: url }), onload: (r4) => { let name; try { name = JSON.parse(r4.responseText).list.resources[0].name; } catch (e) { name = url; } const body = { type: 'user#download-url', name: name, file_name: name, file_size: '0', space: deviceId, params: { target: deviceId, url: url, total_file_count: '0', parent_folder_id: parentId, sub_file_index: '', file_id: '' } }; GM_xmlhttpRequest({ method: 'POST', url: base + '/webman/3rdparty/pan-xunlei-com/index.cgi/drive/v1/task?device_space=', headers: H, data: JSON.stringify(body), onload: (r5) => { const ok = (() => { try { return JSON.parse(r5.responseText).HttpStatus === 0; } catch (e) { return false; } })(); toast(ok ? '已发送到 迅雷蜈蚣' : '迅雷蜈蚣:提交失败'); }, onerror: () => toast('迅雷蜈蚣:连接失败'), }); }, onerror: () => toast('迅雷蜈蚣:连接失败'), }); }, onerror: () => toast('迅雷蜈蚣:连接失败'), }); }, onerror: () => toast('迅雷蜈蚣:连接失败'), }); }, onerror: () => toast('迅雷蜈蚣:连接失败'), }); } /* ============================ 历史 ============================ */ function addHistory(q) { q = q.trim(); if (!q) return; HISTORY = HISTORY.filter((h) => h.q !== q); HISTORY.unshift({ q, time: Date.now(), fav: false }); if (HISTORY.length > HISTORY_MAX) HISTORY = HISTORY.slice(0, HISTORY_MAX); setArr(HISTORY_KEY, HISTORY); renderHistory(); } function toggleFav(q) { const h = HISTORY.find((x) => x.q === q); if (!h) return; h.fav = !h.fav; setArr(HISTORY_KEY, HISTORY); renderHistory(); } function delHistory(q) { HISTORY = HISTORY.filter((x) => x.q !== q); setArr(HISTORY_KEY, HISTORY); renderHistory(); } function exportHistory() { if (!HISTORY.length) return toast('暂无历史可导出'); const data = HISTORY.map((h) => ({ query: h.q, time: new Date(h.time).toISOString(), favorite: h.fav })); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'wb-search-history-' + new Date().toISOString().slice(0, 10) + '.json'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); toast('历史已导出为 JSON'); } function renderHistory() { const box = $('#wb-history'); if (!box) return; const fav = HISTORY.filter((h) => h.fav); const rest = HISTORY.filter((h) => !h.fav); const items = [...fav, ...rest]; box.innerHTML = ''; if (!items.length) { box.innerHTML = '
暂无记录
'; return; } items.forEach((h) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:3px 0;'; const star = document.createElement('span'); star.textContent = h.fav ? '★' : '☆'; star.style.cssText = 'cursor:pointer;color:#e6a700;'; star.onclick = (e) => { e.stopPropagation(); toggleFav(h.q); }; const txt = document.createElement('span'); txt.textContent = h.q; txt.style.cssText = 'flex:1;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; txt.title = '点击重新搜索'; txt.onclick = () => { $('#wb-q').value = h.q; doSearch(); }; const del = document.createElement('span'); del.textContent = '✕'; del.style.cssText = 'cursor:pointer;color:#bbb;'; del.onclick = (e) => { e.stopPropagation(); delHistory(h.q); }; row.append(star, txt, del); box.appendChild(row); }); } /* ============================ 搜索 ============================ */ function doSearch() { const q = $('#wb-q').value.trim(); if (!q) return toast('请输入关键词'); const active = PLATFORMS.filter((p) => p.enabled); if (!active.length) return toast('没有启用的平台'); const targets = active.filter((p) => p.url.includes('{q}')); // 仅真正可网页搜索的平台 if (!targets.length) return toast('没有可网页搜索的启用平台(参考条目仅能打开官网)'); targets.forEach((p, i) => setTimeout(() => GM_openInTab(p.url.replace('{q}', enc(q)), true), i * 120)); addHistory(q); toast('已打开 ' + targets.length + ' 个可搜索平台'); } /* ============================ 主题 ============================ */ function resolveTheme() { if (THEME === 'dark') return true; if (THEME === 'light') return false; return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } function applyTheme() { const panel = $('#wb-panel'); if (!panel) return; panel.setAttribute('data-theme', resolveTheme() ? 'dark' : 'light'); const btn = $('#wb-theme'); if (btn) btn.textContent = resolveTheme() ? '☀' : '🌙'; } function cycleTheme() { THEME = THEME === 'system' ? 'light' : THEME === 'light' ? 'dark' : 'system'; GM_setValue(THEME_KEY, THEME); applyTheme(); toast('主题:' + ({ system: '跟随系统', light: '浅色', dark: '深色' })[THEME]); } /* ============================ UI ============================ */ const STYLE = ` #wb-panel{position:fixed;right:16px;bottom:16px;z-index:999999;width:340px;background:#fff; border:1px solid #e3e3e3;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.15); font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;color:#222;overflow:hidden; transition:background .2s,color .2s;} #wb-panel header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px; background:#2b2f3a;color:#fff;cursor:move;user-select:none;} #wb-panel header .hbtns{display:flex;gap:10px;align-items:center;} #wb-panel header span{cursor:pointer;} #wb-panel .body{padding:12px;max-height:72vh;overflow:auto;} #wb-panel input[type=text]{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d0d0d0; border-radius:8px;outline:none;font-size:13px;background:#fff;color:#222;} #wb-panel input[type=text]:focus{border-color:#4a90e2;} #wb-panel .row{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;} #wb-panel button{background:#4a90e2;color:#fff;border:none;border-radius:8px;padding:6px 10px; cursor:pointer;font-size:12px;flex:1;min-width:60px;} #wb-panel button.gray{background:#e6e6e6;color:#333;} #wb-panel button:hover{filter:brightness(.95);} #wb-panel .sec{margin-top:14px;border-top:1px solid #eee;padding-top:10px;} #wb-panel .sec h4{margin:0 0 6px;font-size:12px;color:#888;font-weight:600; display:flex;justify-content:space-between;align-items:center;} #wb-panel .chips{display:flex;flex-wrap:wrap;gap:6px;} #wb-panel .pgroup{margin-bottom:8px;} #wb-panel .pgroup-h{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none; padding:2px 0;font-size:12px;color:#666;font-weight:600;} #wb-panel .pgroup-h:hover{color:#333;} #wb-panel .pg-arrow{width:12px;display:inline-block;color:#999;font-size:10px;} #wb-panel .pg-name{flex:1;} #wb-panel .pg-count{font-weight:400;color:#bbb;font-size:11px;} #wb-panel .pg-chips.collapsed{display:none;} #wb-panel .chip{background:#f0f4ff;color:#2b5fd6;border:1px solid #d4e0ff;border-radius:20px; padding:4px 10px;cursor:pointer;font-size:12px;} #wb-panel .chip:hover{background:#e0ebff;} #wb-panel .cfg{display:none;margin-top:8px;} #wb-panel .cfg.open{display:block;} #wb-panel .cfg label{display:block;font-size:11px;color:#777;margin:6px 0 2px;} #wb-panel .cfg input[type=text]{width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #d0d0d0; border-radius:6px;font-size:12px;background:#fff;color:#222;} #wb-panel .toast{position:fixed;right:16px;bottom:360px;background:#333;color:#fff;padding:8px 14px; border-radius:8px;font-size:12px;z-index:1000000;opacity:0;transition:opacity .3s;pointer-events:none;} #wb-panel .toast.show{opacity:1;} #wb-fab{position:fixed;right:16px;bottom:16px;z-index:999998;width:52px;height:52px;border-radius:50%; background:#2b2f3a;color:#fff;border:none;cursor:pointer;font-size:22px;box-shadow:0 6px 20px rgba(0,0,0,.25);} #wb-mini{font-size:11px;color:#4a90e2;cursor:pointer;user-select:none;} /* 深色模式 */ #wb-panel[data-theme="dark"]{background:#1e2128;color:#e6e6e6;border-color:#333;} #wb-panel[data-theme="dark"] header{background:#111317;} #wb-panel[data-theme="dark"] .body{color:#e6e6e6;} #wb-panel[data-theme="dark"] input[type=text]{background:#2a2e37;color:#e6e6e6;border-color:#3a3f4a;} #wb-panel[data-theme="dark"] .sec{border-top-color:#333;} #wb-panel[data-theme="dark"] .sec h4{color:#9aa0aa;} #wb-panel[data-theme="dark"] .chip{background:#2a3550;color:#8ab4ff;border-color:#34456b;} #wb-panel[data-theme="dark"] .chip:hover{background:#324063;} #wb-panel[data-theme="dark"] .pgroup-h{color:#9aa0aa;} #wb-panel[data-theme="dark"] .pgroup-h:hover{color:#cfd4dc;} #wb-panel[data-theme="dark"] .pg-arrow{color:#7a8190;} #wb-panel[data-theme="dark"] .pg-count{color:#6b7280;} #wb-panel[data-theme="dark"] button.gray{background:#3a3f4a;color:#ddd;} #wb-panel[data-theme="dark"] .cfg input[type=text]{background:#2a2e37;color:#e6e6e6;border-color:#3a3f4a;} #wb-panel[data-theme="dark"] .cfg label{color:#9aa0aa;} #wb-panel[data-theme="dark"] .toast{background:#000;color:#fff;} #wb-panel[data-theme="dark"] #wb-history{color:#e6e6e6;} `; let toastTimer; function toast(msg) { let t = $('#wb-toast'); if (!t) { t = document.createElement('div'); t.id = 'wb-toast'; t.className = 'toast'; document.body.appendChild(t); } t.textContent = msg; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove('show'), 1800); } function buildUI() { if ($('#wb-fab')) return; const style = document.createElement('style'); style.textContent = STYLE; document.head.appendChild(style); const fab = document.createElement('button'); fab.id = 'wb-fab'; fab.textContent = '▶'; fab.title = '流媒体搜索 / 下载器'; document.body.appendChild(fab); const panel = document.createElement('div'); panel.id = 'wb-panel'; panel.style.display = 'none'; panel.innerHTML = `
流媒体聚合 & 下载器 🌙

流媒体平台 管理

下载器(合法来源文件)

搜索历史 导出清空

仅用于检索正版流媒体与发送合法来源下载任务,请遵守所在地区法律法规与平台服务条款。
`; document.body.appendChild(panel); // 平台 chips(按分组渲染,仅显示启用的) renderPlatChips(); // 下载器 chips const dlChips = $('#wb-dl-chips', panel); DOWNLOADERS.forEach((d) => { const c = document.createElement('span'); c.className = 'chip'; c.textContent = d.name; c.style.opacity = d.enabled ? '1' : '.5'; c.onclick = () => { if (d.webui) GM_openInTab(d.webui, true); else if (d.rpc) GM_openInTab(d.rpc.replace(/\/jsonrpc|\/rpc$/, ''), true); else if (d.api) GM_openInTab(d.api, true); else toast(d.note || (d.name + ':请配置')); }; dlChips.appendChild(c); }); // 下载器发送按钮 const dlBtns = $('#wb-dl-btns', panel); DOWNLOADERS.filter((d) => d.enabled).forEach((d) => { const b = document.createElement('button'); b.className = 'gray'; b.textContent = '→ ' + d.name; b.onclick = () => sendTo(d, $('#wb-magnet').value.trim()); dlBtns.appendChild(b); }); const copyBtn = document.createElement('button'); copyBtn.className = 'gray'; copyBtn.textContent = '复制'; copyBtn.onclick = () => { const v = $('#wb-magnet').value.trim(); if (v) { GM_setClipboard(v); toast('已复制'); } }; dlBtns.appendChild(copyBtn); // 交互 $('#wb-close', panel).onclick = () => { panel.style.display = 'none'; fab.style.display = 'block'; }; fab.onclick = () => { panel.style.display = 'block'; fab.style.display = 'none'; }; $('#wb-theme', panel).onclick = cycleTheme; $('#wb-q', panel).addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); }); $('#wb-search', panel).onclick = doSearch; $('#wb-copy', panel).onclick = () => { const q = $('#wb-q').value.trim(); const links = PLATFORMS.filter((p) => p.enabled && p.url.includes('{q}')).map((p) => p.url.replace('{q}', enc(q))); if (!links.length) return toast('没有可搜索的启用平台'); GM_setClipboard(links.join('\n')); toast('已复制 ' + links.length + ' 个搜索链接'); }; $('#wb-plat-edit', panel).onclick = () => { const cfg = $('#wb-plat-cfg', panel); cfg.classList.toggle('open'); renderPlatCfg(cfg); }; $('#wb-dl-cfg-toggle', panel).onclick = () => { const cfg = $('#wb-dl-cfg', panel); cfg.classList.toggle('open'); renderDlCfg(cfg); }; $('#wb-hist-clear', panel).onclick = () => { HISTORY = []; setArr(HISTORY_KEY, HISTORY); renderHistory(); }; $('#wb-hist-export', panel).onclick = exportHistory; renderHistory(); applyTheme(); drag(panel, $('header', panel)); setTimeout(scanMagnets, 1500); } function renderPlatCfg(cfg) { cfg.innerHTML = ''; PLATFORMS.forEach((p, idx) => { const wrap = document.createElement('div'); wrap.style.marginTop = '6px'; wrap.innerHTML = `
`; cfg.appendChild(wrap); }); const addWrap = document.createElement('div'); addWrap.style.marginTop = '8px'; addWrap.innerHTML = ``; cfg.appendChild(addWrap); const impWrap = document.createElement('div'); impWrap.style.marginTop = '6px'; impWrap.innerHTML = ``; cfg.appendChild(impWrap); $('#wb-plat-import', cfg).onclick = () => { let added = 0; DEFAULT_PLATFORMS.forEach((dp) => { if (!PLATFORMS.some((p) => p.name === dp.name)) { PLATFORMS.push({ ...dp }); added++; } }); if (added) { setArr(PLAT_KEY, PLATFORMS); renderPlatCfg(cfg); renderPlatChips(); toast('已导入 ' + added + ' 个预设平台'); } else toast('预设平台已齐全'); }; cfg.querySelectorAll('input').forEach((inp) => { inp.onchange = () => { const i = +inp.dataset.i, f = inp.dataset.f; if (f === 'enabled') PLATFORMS[i].enabled = inp.checked; else PLATFORMS[i][f] = inp.value; setArr(PLAT_KEY, PLATFORMS); toast('已保存'); if (f === 'enabled' || f === 'name' || f === 'url' || f === 'group') renderPlatChips(); }; }); cfg.querySelectorAll('[data-del]').forEach((b) => { b.onclick = () => { PLATFORMS.splice(+b.dataset.del, 1); setArr(PLAT_KEY, PLATFORMS); renderPlatCfg(cfg); renderPlatChips(); }; }); $('#wb-plat-add', cfg).onclick = () => { PLATFORMS.push({ name: '新平台', url: 'https://example.com/search?q={q}', enabled: true, group: '其他' }); setArr(PLAT_KEY, PLATFORMS); renderPlatCfg(cfg); renderPlatChips(); }; } function groupCollapsed(g) { if (g in GROUP_STATE) return GROUP_STATE[g]; return GROUP_COLLAPSED[g] || false; } function setGroupCollapsed(g, v) { GROUP_STATE[g] = v; GM_setValue('wb_group_state', GROUP_STATE); } function platformGroups() { const map = {}; PLATFORMS.forEach((p) => { const g = p.group || '其他'; (map[g] = map[g] || []).push(p); }); const groups = []; GROUP_ORDER.forEach((g) => { if (map[g]) { groups.push([g, map[g]]); delete map[g]; } }); Object.keys(map).forEach((g) => groups.push([g, map[g]])); // 其余分组排在后面 return groups; } function renderPlatChips() { const box = $('#wb-plat-chips'); if (!box) return; box.innerHTML = ''; platformGroups().forEach(([g, list]) => { const enabled = list.filter((p) => p.enabled); const collapsed = groupCollapsed(g); const wrap = document.createElement('div'); wrap.className = 'pgroup'; const head = document.createElement('div'); head.className = 'pgroup-h'; head.innerHTML = `${collapsed ? '▸' : '▾'}` + `${g}` + `${enabled.length}`; const chips = document.createElement('div'); chips.className = 'chips pg-chips' + (collapsed ? ' collapsed' : ''); if (enabled.length) { enabled.forEach((p) => { const c = document.createElement('span'); c.className = 'chip'; c.textContent = p.name; if (!p.url.includes('{q}')) c.title = '(无网页搜索,点击打开官网)'; c.onclick = () => GM_openInTab(p.url.replace('{q}', enc($('#wb-q').value || '')), true); chips.appendChild(c); }); } else { chips.innerHTML = '(无启用的平台)'; } head.onclick = () => { setGroupCollapsed(g, !groupCollapsed(g)); renderPlatChips(); }; wrap.append(head, chips); box.appendChild(wrap); }); } function renderDlCfg(cfg) { cfg.innerHTML = ''; DOWNLOADERS.forEach((d, idx) => { const wrap = document.createElement('div'); wrap.style.marginTop = '8px'; let html = ``; ['webui', 'rpc', 'api', 'user', 'pass', 'token'].forEach((f) => { if (d[f] !== undefined) html += ``; }); if (d.note) html += ``; wrap.innerHTML = html; cfg.appendChild(wrap); }); cfg.querySelectorAll('input').forEach((inp) => { inp.onchange = () => { const i = +inp.dataset.i, f = inp.dataset.f; DOWNLOADERS[i][f] = inp.type === 'checkbox' ? inp.checked : inp.value; setArr(DL_KEY, DOWNLOADERS); toast('已保存'); }; }); } function drag(el, handle) { let sx, sy, ox, oy, on = false; handle.onmousedown = (e) => { on = true; sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); ox = r.left; oy = r.top; e.preventDefault(); }; document.onmousemove = (e) => { if (!on) return; el.style.left = (ox + e.clientX - sx) + 'px'; el.style.top = (oy + e.clientY - sy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }; document.onmouseup = () => { on = false; }; } function scanMagnets() { const inp = $('#wb-magnet'); if (!inp || inp.value) return; const links = [...new Set([...document.querySelectorAll('a[href^="magnet:"]')].map((a) => a.href))]; if (links.length) inp.value = links[0]; } function init() { buildUI(); if (window.matchMedia) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (THEME === 'system') applyTheme(); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();