// ==UserScript== // @name ehentai-go pre-download helper // @namespace https://localhost/ehentai-go // @version 5.1.0 // @description e-hentai 画廊页一键「Send & Push」: 解析画廊元信息 + 单页 URL 列表 → POST 给本机 ehentai-go 服务, 然后并发把缺失图片二进制推到后端。所有抓图都在浏览器完成, 绕开 Go 端被 e-hentai TLS fingerprint 拦截的问题。支持刷新/跳转前的放弃确认、按 title 续传、固定状态面板、失败自动 retry。 // @match https://e-hentai.org/g/* // @match https://exhentai.org/g/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const API_BASE = 'http://localhost:8990'; const PANEL_ID = 'ehg-panel'; const BTN_ID = 'ehg-btn'; const CONCURRENCY = 5; const PAGE_URL_TIMEOUT_MS = 10000; const IMAGE_FETCH_TIMEOUT_MS = 60000; GM_addStyle(` #${PANEL_ID} { position: fixed; left: 16px; bottom: 16px; z-index: 99999; width: 440px; max-width: calc(100vw - 32px); background: #1f2937; color: #e5e7eb; border: 1px solid #374151; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.35); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; overflow: hidden; user-select: none; } #${PANEL_ID} .ehg-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: #111827; border-bottom: 1px solid #374151; cursor: move; } #${PANEL_ID} .ehg-title { font-weight: 600; font-size: 13px; letter-spacing: .04em; text-transform: uppercase; color: #9ca3af; } #${PANEL_ID} .ehg-stage { font-size: 12px; padding: 3px 10px; border-radius: 999px; background: #374151; color: #d1d5db; } #${PANEL_ID} .ehg-stage.idle { background: #374151; color: #d1d5db; } #${PANEL_ID} .ehg-stage.run { background: #1d4ed8; color: #fff; } #${PANEL_ID} .ehg-stage.ok { background: #15803d; color: #fff; } #${PANEL_ID} .ehg-stage.warn { background: #b45309; color: #fff; } #${PANEL_ID} .ehg-stage.err { background: #b91c1c; color: #fff; } #${PANEL_ID} .ehg-body { padding: 12px 14px 14px; } #${PANEL_ID} .ehg-status { font-size: 13px; color: #e5e7eb; margin-bottom: 8px; word-break: break-all; } #${PANEL_ID} .ehg-progress { height: 7px; background: #374151; border-radius: 4px; overflow: hidden; margin-bottom: 10px; } #${PANEL_ID} .ehg-progress > span { display: block; height: 100%; width: 0; background: linear-gradient(90deg, #4f46e5, #22c55e); transition: width .25s ease; } #${PANEL_ID} .ehg-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; font-size: 12px; color: #9ca3af; margin-bottom: 12px; } #${PANEL_ID} .ehg-stats b { display: block; color: #f9fafb; font-size: 16px; font-variant-numeric: tabular-nums; } #${BTN_ID} { width: 100%; padding: 11px 14px; color: #fff; border: 0; border-radius: 6px; font: 600 14px/1 inherit; cursor: pointer; background: #4f46e5; transition: background .15s ease; } #${BTN_ID}:hover { background: #4338ca; } #${BTN_ID}:disabled { background: #4b5563; cursor: wait; opacity: .8; } #${BTN_ID}.ok { background: #16a34a; } #${BTN_ID}.err { background: #dc2626; } #${BTN_ID}.warn { background: #d97706; } #${PANEL_ID}.ehg-collapsed .ehg-body { display: none; } #${PANEL_ID} .ehg-toggle { background: transparent; border: 0; color: #9ca3af; cursor: pointer; font-size: 14px; padding: 0 4px; margin-left: 8px; } `); // ---- 工具 ---- function sanitize(name) { let s = name.replace(/[:"*?|<>/\\\n]/g, '-'); s = s.replace(/-+/g, '-'); s = s.trim().replace(/^\.+|\.+$/g, '').trim(); return s || 'untitled'; } function extractSinglePageURLs(html, baseURL) { const startIdx = html.indexOf('