// ==UserScript== // @name EH Downloader 助手 // @namespace http://127.0.0.1:37777 // @version 2.0.0 // @description ① 画廊页:把当前画廊推送到本地 EH Downloader ② 列表页:勾选后批量推送 // @author jack // @match https://e-hentai.org/* // @match https://exhentai.org/* // @grant GM_xmlhttpRequest // @connect 127.0.0.1 // @connect localhost // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const PORT = 37777; const TOKEN = 'ehdl-local-9f2a4c7e'; const API = `http://127.0.0.1:${PORT}`; // ============================================================ // 公共:通讯 // ============================================================ function ping(cb) { GM_xmlhttpRequest({ method: 'GET', url: `${API}/ping`, timeout: 1500, onload: (r) => cb(r.status === 200), onerror: () => cb(false), ontimeout: () => cb(false), }); } function post(path, data, cb) { GM_xmlhttpRequest({ method: 'POST', url: `${API}${path}`, headers: { 'Content-Type': 'application/json', 'X-EHDL-Token': TOKEN }, data: JSON.stringify(data), timeout: 8000, onload: (r) => { let j = {}; try { j = JSON.parse(r.responseText); } catch (_) {} cb(r.status === 200, j); }, // 连不上 = 工具没开 onerror: () => cb(false, { error: 'offline' }), ontimeout: () => cb(false, { error: 'timeout' }), }); } function addStyle(css) { const s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); } const CSS = ` #ehdl-btn { display:inline-flex; align-items:center; gap:6px; padding:5px 13px; border-radius:20px; cursor:pointer; font:13px/1.4 -apple-system,"PingFang SC",sans-serif; color:#fff; background:#4a9eff; border:1px solid #3b8ae6; transition:background .12s,opacity .12s; } #ehdl-btn:hover{background:#62acff} #ehdl-btn:active{transform:translateY(1px)} #ehdl-btn[disabled]{opacity:.45;cursor:default} #ehdl-btn.is-off{background:#6b7280;border-color:#6b7280} #ehdl-btn.is-done{background:#3fb950;border-color:#3fb950} #ehdl-status{font-size:12px;color:#8b93a3} #ehdl-status.err{color:#f85149} #ehdl-status.ok{color:#3fb950} /* 列表页勾选框 */ .ehdl-cb { display:inline-block; vertical-align:middle; margin-right:6px; width:15px; height:15px; cursor:pointer; accent-color:#4a9eff; position:relative; top:-1px; } .ehdl-cb-row.off a{ opacity:.5 } /* 悬浮批量工具条 */ #ehdl-bar { position:fixed; right:18px; bottom:18px; z-index:2147483000; display:flex; align-items:center; gap:9px; padding:9px 13px; border-radius:11px; background:rgba(28,31,38,.96); border:1px solid #2e333d; box-shadow:0 10px 34px rgba(0,0,0,.5); font:12px/1.4 -apple-system,"PingFang SC",sans-serif; color:#e6e8ec; backdrop-filter:blur(8px); max-width:calc(100vw - 36px); flex-wrap:wrap; } #ehdl-bar .n{ font-variant-numeric:tabular-nums; color:#8b93a3 } #ehdl-bar .n b{ color:#4a9eff; font-size:14px } #ehdl-bar button { background:#23272f; color:#e6e8ec; border:1px solid #2e333d; border-radius:7px; padding:5px 11px; font-size:12px; cursor:pointer; } #ehdl-bar button:hover:not([disabled]){ background:#2c313a; border-color:#3d4450 } #ehdl-bar button[disabled]{ opacity:.4; cursor:default } #ehdl-bar button.primary{ background:#4a9eff; border-color:#4a9eff; color:#04121f; font-weight:600 } #ehdl-bar button.primary:hover:not([disabled]){ background:#62acff } #ehdl-bar .dot{ width:8px; height:8px; border-radius:50%; background:#6b7280 } #ehdl-bar .dot.on{ background:#3fb950 } #ehdl-bar .msg{ max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap } #ehdl-bar .msg.err{ color:#f85149 } #ehdl-bar .msg.ok{ color:#3fb950 } `; addStyle(CSS); // ============================================================ // 模式 A:画廊详情页 —— 发送当前画廊 // ============================================================ function runGalleryPage() { const m = location.pathname.match(/^\/g\/(\d+)\/([0-9a-f]+)/i); if (!m) return false; const gid = m[1]; const token = m[2]; const galleryUrl = `${location.origin}/g/${gid}/${token}/`; const titleEl = document.querySelector('h1#gn'); const title = (titleEl?.textContent || document.title || '') .replace(/\s*-\s*E-Hentai Galleries\s*$/i, '').trim(); // 动作区 #gd5(Report Gallery / Archive Download 那一栏) let host = document.getElementById('gd5') || document.getElementById('gd1'); if (!host) { host = document.createElement('div'); host.id = 'gd5'; const anchor = document.querySelector('#gdt') || document.body; anchor.parentNode.insertBefore(host, anchor); } const box = document.createElement('div'); box.style.cssText = 'margin:6px 0 10px;display:flex;align-items:center;gap:8px'; host.appendChild(box); const btn = document.createElement('button'); btn.id = 'ehdl-btn'; btn.type = 'button'; btn.innerHTML = '📤发送画廊地址'; const status = document.createElement('span'); status.id = 'ehdl-status'; box.append(btn, status); let online = false; const setStatus = (t, k) => { status.textContent = t; status.className = k || ''; }; const markOffline = () => { btn.classList.add('is-off'); btn.title = 'EH Downloader 没在运行,请先打开'; }; const markDone = () => { btn.classList.add('is-done'); btn.innerHTML = '✓已发送'; }; btn.addEventListener('click', () => { if (!online) { markOffline(); setStatus('EH Downloader 没在运行,请先打开它', 'err'); return; } btn.disabled = true; setStatus('发送中…', ''); post('/add', { gid, token, url: galleryUrl, title }, (ok, j) => { btn.disabled = false; if (!ok) { online = false; markOffline(); btn.innerHTML = '📤发送画廊地址'; setStatus(j.error === 'timeout' ? '✗ 连接超时,请先打开工具' : '✗ 连不上,请先打开 EH Downloader', 'err'); return; } markDone(); setStatus(j.existed ? '已在列表中(跳过重复)' : '✓ 已推送,等待下载', 'ok'); }); }); ping((ok) => { online = ok; if (ok) setStatus('EH Downloader 运行中', 'ok'); else { markOffline(); setStatus('EH Downloader 未运行 —— 点击会提示', ''); } }); return true; } // ============================================================ // 模式 B:列表页 —— 勾选批量推送 // ============================================================ function runListPage() { // 收集本页所有画廊链接(按 gid 去重) const links = [...document.querySelectorAll('a[href*="/g/"]')]; const seen = new Set(); const items = []; // 链接里的标题在 .glink 里;直接 textContent 会把嵌套 div 的标签文字也吸进来 const readTitle = (a) => { const g = a.querySelector('.glink'); return (g?.textContent || a.textContent || '').trim().slice(0, 200); }; for (const a of links) { const href = a.getAttribute('href') || ''; const m = href.match(/\/g\/(\d+)\/([0-9a-f]+)/i); if (!m) continue; const gid = m[1]; const token = m[2]; const key = `${gid}/${token}`; if (seen.has(key)) continue; seen.add(key); items.push({ gid, token, url: `${location.origin}/g/${gid}/${token}/`, title: readTitle(a), anchor: a, cb: null, }); } if (!items.length) return false; // 每行前面插复选框 for (const it of items) { const a = it.anchor; if (!a || !a.parentNode) continue; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'ehdl-cb'; cb.title = `勾选 ${it.gid}`; a.parentNode.insertBefore(cb, a); cb.addEventListener('change', refresh); it.cb = cb; } // ---- 悬浮工具条 ---- const bar = document.createElement('div'); bar.id = 'ehdl-bar'; bar.innerHTML = ` 已选 0 / ${items.length} `; document.body.appendChild(bar); const $ = (id) => bar.querySelector('#' + id); const dot = $('ehdl-dot'); const numEl = $('ehdl-n'); const sendBtn = $('ehdl-send'); const msg = $('ehdl-msg'); let online = false; const boxes = items.map((i) => i.cb).filter(Boolean); function selected() { return boxes.filter((c) => c.checked).length; } function refresh() { const n = selected(); numEl.textContent = n; sendBtn.disabled = n === 0 || !online; sendBtn.textContent = online ? '推送到工具' : '工具未运行'; if (!online) { sendBtn.classList.remove('primary'); } else { sendBtn.classList.add('primary'); } } function say(text, kind) { msg.textContent = text; msg.className = 'msg' + (kind ? ' ' + kind : ''); } $('ehdl-all').onclick = () => { boxes.forEach((c) => (c.checked = true)); refresh(); }; $('ehdl-inv').onclick = () => { boxes.forEach((c) => (c.checked = !c.checked)); refresh(); }; $('ehdl-none').onclick = () => { boxes.forEach((c) => (c.checked = false)); refresh(); }; sendBtn.onclick = () => { const picked = items.filter((i) => i.cb && i.cb.checked); if (!picked.length) return; sendBtn.disabled = true; say(`推送 ${picked.length} 个…`, ''); post('/add-batch', { items: picked.map((i) => ({ gid: i.gid, token: i.token, url: i.url, title: i.title })), }, (ok, j) => { sendBtn.disabled = false; if (!ok) { online = false; dot.classList.remove('on'); say(j.error === 'timeout' ? '✗ 连接超时,请先打开工具' : '✗ 连不上,请先打开 EH Downloader', 'err'); refresh(); return; } const bits = []; if (j.added) bits.push(`新增 ${j.added}`); if (j.existed) bits.push(`已存在 ${j.existed}`); say(`✓ ${bits.join(' · ') || '无变化'}`, 'ok'); picked.forEach((i) => { if (i.cb) i.cb.checked = false; }); refresh(); }); }; ping((ok) => { online = ok; dot.classList.toggle('on', ok); say(ok ? '工具运行中' : 'EH Downloader 未运行', ok ? 'ok' : 'err'); refresh(); }); refresh(); return true; } // ============================================================ // 入口 // ============================================================ if (runGalleryPage()) return; runListPage(); })();