// ==UserScript== // @name 4KHD Downloader 助手 // @namespace http://127.0.0.1:37778 // @version 1.0.0 // @description ① 画廊页:把当前画廊推送到本地 4KHD Downloader ② 列表页:勾选后批量推送 // @author - // @match *://migmg.ssuu.uk/* // @match *://www.4khd.com/* // @match *://4khd.com/* // @match *://m.4khd.com/* // @grant GM_xmlhttpRequest // @connect 127.0.0.1 // @connect localhost // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const PORT = 37778; const TOKEN = 'hd4k-local-4b91e3d7'; 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-HD4K-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 = ` #hd4k-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; } #hd4k-btn:hover{background:#62acff} #hd4k-btn:active{transform:translateY(1px)} #hd4k-btn[disabled]{opacity:.45;cursor:default} #hd4k-btn.is-off{background:#6b7280;border-color:#6b7280} #hd4k-btn.is-done{background:#3fb950;border-color:#3fb950} #hd4k-status{font-size:12px;color:#8b93a3} #hd4k-status.err{color:#f85149} #hd4k-status.ok{color:#3fb950} /* 列表页勾选框 */ .hd4k-cb { display:inline-block; vertical-align:middle; margin-right:6px; width:15px; height:15px; cursor:pointer; accent-color:#4a9eff; position:relative; top:-1px; } .hd4k-cb-row.off a{ opacity:.5 } /* 悬浮批量工具条 */ #hd4k-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; } #hd4k-bar .n{ font-variant-numeric:tabular-nums; color:#8b93a3 } #hd4k-bar .n b{ color:#4a9eff; font-size:14px } #hd4k-bar button { background:#23272f; color:#e6e8ec; border:1px solid #2e333d; border-radius:7px; padding:5px 11px; font-size:12px; cursor:pointer; } #hd4k-bar button:hover:not([disabled]){ background:#2c313a; border-color:#3d4450 } #hd4k-bar button[disabled]{ opacity:.4; cursor:default } #hd4k-bar button.primary{ background:#4a9eff; border-color:#4a9eff; color:#04121f; font-weight:600 } #hd4k-bar .dot{ width:8px; height:8px; border-radius:50%; background:#6b7280; flex:none; } #hd4k-bar .dot.on{ background:#3fb950; box-shadow:0 0 7px #3fb950 } #hd4k-bar .msg{ font-size:12px; color:#8b93a3 } #hd4k-bar .msg.ok{ color:#3fb950 } #hd4k-bar .msg.err{ color:#f85149 } `; // ============================================================ // 公共:URL 解析 // ============================================================ // 画廊链接形态:/content/{分类数字}/{名字}.html // 分页是路径后缀(…/x.html/2),归一时必须剥掉。 const RE_GALLERY = /^\/content\/(\d+)\/([^/]+\.html)$/; function isGalleryPath(pathname) { return RE_GALLERY.test(pathname); } /** * 把页面上的链接规范化成「当前站点」的绝对 URL。 * * ⚠️ 这一步不能省:实测 migmg.ssuu.uk 首页里 90 个 /content/ 链接 * **全部**指向 www.4khd.com,而那个域名已经 404。 * 原样把 href 推给后端,后端就会去请求一个死域名。 */ function canonicalize(href) { try { const u = new URL(href, location.origin); if (!RE_GALLERY.test(u.pathname)) return null; return `${location.origin}${u.pathname}`; } catch (_) { return null; } } /** 取当前页面画廊标题(优先 h3 —— 那是页面可见标题且没有站点后缀) */ function readPageTitle() { const h3 = document.querySelector('h3.wp-block-post-title'); if (h3) { const t = h3.textContent.replace(/\s+/g, ' ').trim(); if (t) return t.slice(0, 300); } const og = document.querySelector('meta[property="og:title"]'); if (og && og.content) { return og.content.replace(/\s+/g, ' ').trim().slice(0, 300); } return document.title.replace(/\s*-\s*4KHD\s*$/, '').trim().slice(0, 300); } // ============================================================ // 模式 A:画廊详情页 —— 顶部加发送按钮 // ============================================================ function runGalleryPage() { if (!isGalleryPath(location.pathname)) return false; const galleryUrl = `${location.origin}${location.pathname}`; const title = readPageTitle(); addStyle(CSS); // 挂在正文标题附近;找不到就退到页首 const anchor = document.querySelector('h3.wp-block-post-title') || document.querySelector('h1') || document.body.firstElementChild; if (!anchor) return false; const box = document.createElement('div'); box.style.cssText = 'display:flex;align-items:center;gap:10px;margin:14px 0;flex-wrap:wrap'; const btn = document.createElement('button'); btn.id = 'hd4k-btn'; btn.type = 'button'; btn.innerHTML = '📤发送画廊地址'; const status = document.createElement('span'); status.id = 'hd4k-status'; box.append(btn, status); anchor.insertAdjacentElement('afterend', box); let online = false; const setStatus = (t, k) => { status.textContent = t; status.className = k || ''; }; const markOffline = () => { btn.classList.add('is-off'); btn.title = '4KHD Downloader 没在运行,请先打开'; }; const markDone = () => { btn.classList.add('is-done'); btn.innerHTML = '✓已发送'; }; btn.addEventListener('click', () => { if (!online) { markOffline(); setStatus('4KHD Downloader 没在运行,请先打开它', 'err'); return; } btn.disabled = true; setStatus('发送中…', ''); post('/add', { url: galleryUrl, title }, (ok, j) => { btn.disabled = false; if (!ok) { online = false; markOffline(); btn.innerHTML = '📤发送画廊地址'; setStatus( j.error === 'timeout' ? '✗ 连接超时,请先打开工具' : '✗ 连不上,请先打开 4KHD Downloader', 'err' ); return; } markDone(); setStatus(j.existed ? '已在列表中(跳过重复)' : '✓ 已推送,等待下载', 'ok'); }); }); ping((ok) => { online = ok; if (ok) setStatus('4KHD Downloader 运行中', 'ok'); else { markOffline(); setStatus('4KHD Downloader 未运行 —— 点击会提示', ''); } }); return true; } // ============================================================ // 模式 B:列表页 —— 勾选批量推送 // ============================================================ function runListPage() { const links = [...document.querySelectorAll('a[href*="/content/"]')]; if (!links.length) return false; const seen = new Set(); const items = []; for (const a of links) { const href = a.getAttribute('href') || ''; const url = canonicalize(href); if (!url) continue; // 同一路径在页面上常出现多次(缩略图 + 标题两处) if (seen.has(url)) continue; seen.add(url); // 标题优先取 h3(列表页卡片里的标题),退回链接文本 let title = ''; const t = a.querySelector('h3'); if (t) title = t.textContent; else if (a.querySelector('img') && a.parentElement?.querySelector('h3')) { title = a.parentElement.querySelector('h3').textContent; } else { title = a.textContent; } title = (title || '').replace(/\s+/g, ' ').trim().slice(0, 300); items.push({ url, title, anchor: a, cb: null }); } if (!items.length) return false; addStyle(CSS); // 每行前面插复选框 for (const it of items) { const a = it.anchor; if (!a || !a.parentNode) continue; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'hd4k-cb'; cb.title = `勾选 ${it.url}`; a.parentNode.insertBefore(cb, a); cb.addEventListener('change', refresh); it.cb = cb; } // ---- 悬浮工具条 ---- const bar = document.createElement('div'); bar.id = 'hd4k-bar'; bar.innerHTML = ` 已选 0 / ${items.length} `; document.body.appendChild(bar); const $ = (id) => bar.querySelector('#' + id); const dot = $('hd4k-dot'); const numEl = $('hd4k-n'); const sendBtn = $('hd4k-send'); const msg = $('hd4k-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 ? '推送到工具' : '工具未运行'; sendBtn.classList.toggle('primary', online); } function say(text, kind) { msg.textContent = text; msg.className = 'msg' + (kind ? ' ' + kind : ''); } $('hd4k-all').onclick = () => { boxes.forEach((c) => (c.checked = true)); refresh(); }; $('hd4k-inv').onclick = () => { boxes.forEach((c) => (c.checked = !c.checked)); refresh(); }; $('hd4k-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) => ({ url: i.url, title: i.title })), }, (ok, j) => { sendBtn.disabled = false; if (!ok) { online = false; dot.classList.remove('on'); say( j.error === 'timeout' ? '✗ 连接超时,请先打开工具' : '✗ 连不上,请先打开 4KHD Downloader', 'err' ); refresh(); return; } const bits = []; if (j.added) bits.push(`新增 ${j.added}`); if (j.existed) bits.push(`已存在 ${j.existed}`); if (j.invalid) bits.push(`无效 ${j.invalid}`); 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 ? '工具运行中' : '4KHD Downloader 未运行', ok ? 'ok' : 'err'); refresh(); }); refresh(); return true; } // ============================================================ // 入口 // ============================================================ if (runGalleryPage()) return; runListPage(); })();