first commit
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
// ==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 = '<span>📤</span><span>发送画廊地址</span>';
|
||||
|
||||
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 = '<span>✓</span><span>已发送</span>';
|
||||
};
|
||||
|
||||
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 = '<span>📤</span><span>发送画廊地址</span>';
|
||||
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 = `
|
||||
<span class="dot" id="hd4k-dot"></span>
|
||||
<span class="n">已选 <b id="hd4k-n">0</b> / ${items.length}</span>
|
||||
<button id="hd4k-all">全选</button>
|
||||
<button id="hd4k-inv">反选</button>
|
||||
<button id="hd4k-none">清空</button>
|
||||
<button id="hd4k-send" class="primary" disabled>推送到工具</button>
|
||||
<span class="msg" id="hd4k-msg"></span>
|
||||
`;
|
||||
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();
|
||||
})();
|
||||
Reference in New Issue
Block a user