// ==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();
})();