# 4KHD Downloader `migmg.ssuu.uk`(4KHD 系采集站)画廊批量下载器。浏览器里用油猴脚本把画廊推给本地程序, 程序解析分页页、拿到全部图片直链、多线程下载到本地。 程序结构和交互沿用 [eh-downloader](../eh-downloader)(E-Hentai 版), 但站点协议层是照着 4KHD 的真实页面重写的。 --- ## 快速上手 ### 1. 构建 前置:**Go 1.24+**,以及 **Wails CLI v2** 和 **WebView2 运行时**(Win10/11 一般已自带)。 ```powershell # 装 Wails CLI go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0 # 构建 cd hd4k-downloader wails build ``` 产物在 `build\bin\4KHD-Downloader.exe`。 > 本机 Go 装在 `%USERPROFILE%\.local\go`,不在系统 PATH 里, > 调用前需要 `$env:PATH = "$env:USERPROFILE\.local\go\bin;$env:PATH"`。 ### 2. 装油猴脚本 把 [`userscript/hd4k-to-downloader.user.js`](userscript/hd4k-to-downloader.user.js) 拖进 Tampermonkey 即可,无需额外配置(端口和 token 已写死在内)。 - **画廊详情页**:标题下面出现「📤 发送画廊地址」按钮 - **列表页**:每条画廊前面出现复选框,右下角浮出批量工具条 ### 3. 下图 点推送 → 程序窗口自动弹到前台 → 「开始下载」。 也可在程序里「导入链接」,粘贴一批 URL(每行一条)手动加任务。 --- ## 它是怎么工作的 ``` 浏览器 (油猴脚本) │ GM_xmlhttpRequest → http://127.0.0.1:37778/add ▼ 本地桥口 (Go, 只监听 127.0.0.1) │ {url, title} → 解析成 GalleryRef,URL 域名归一化,入队 ▼ 下载引擎 │ 抓第 1 页 → 从 .page-link-box 读总页数 → 并发抓 2..N 页 │ 从 HTML 抽 i0.wp.com 直链 → 过滤推荐位 → 按页去重 │ 多线程下载 → .part 临时文件 → 原子改名 ▼ 下载目录/<模板>/xxx-001.webp … + .hd4k.json(清单,用于续传) ``` ### 和 eh-downloader 的关键差异 | | EH Downloader | 4KHD Downloader | |---|---|---| | 任务主键 | gid + token | id(**主域名 + 路径**) | | 图片地址 | hath 签名链接,**会过期** | 稳定 URL 哈希,**永久有效** | | 续传方式 | 清单存直链,重启 0 请求 | 同左(4KHD 更彻底) | | 抓取请求数 | 2N+1(列表 + 详情 ×2) | **N**(只有分页页) | | 分辨率 | 可选 `xres=org` 等 | **走源站直链**(不用 Photon 代理,否则免费额度打爆会被 429 封) | | 目录占位符 | `{title} {gid} {artist} {group} {series} {category} {lang} {tag1} {tags:3}` | `{title} {name} {slug} {host} {category} {id}` | | 桥口端口 | 37777 | **37778** | 4KHD 没有 EH 那套 artist/group/series/tags 标签体系,站点真实提供的元数据只有 标题、路径名、域名和分类数字,所以占位符和预设都相应变少了。 --- ## 站点适配(重要) 这类采集站**随时换域名**。实测 `www.4khd.com`、`m.4khd.com` 现在全部 404, 只有 `migmg.ssuu.uk` 活着。所以域名做成了设置项,不用改代码: - **设置 → 站点域名** → 加一行即可 - 保存后,油猴脚本下次加载会从桥口 `/info` 读到最新域名列表 ### 域名归一化 页面上散布的链接**未必指向主域名**。实测 `migmg.ssuu.uk` 首页里 90 个 `/content/` 链接**全部**指向已 404 的 `www.4khd.com`。 所以 `4khd.com`、`www.4khd.com`、`m.4khd.com` 都配成了 `migmg.ssuu.uk` 的 **别名**:不管用户从哪个域名复制链接,后端都会归一到主域名。 不归一的话,同一个画廊会因为「从哪个域名复制的链接」变成两个不同任务, 而且引擎会去请求一个死域名。 分页后缀(`…/x.html/2`)也会被剥掉,避免 `/2` 和 `/1` 变成两个任务。 --- ## 协议层是怎么定下来的 全部基于 2026-10-11 对真实页面的实测,不是照着 HTML 猜的。几个关键点: **图片地址**:页面给的是 Jetpack Photon 代理,但我们**不直接用** 页面里的图片地址长这样: ``` https://i0.wp.com/pic.4khd.com/{seg1}/{seg2}/{seg3}/w1300-rw/{name}.webp?w=1300 ``` 去掉 `https://i0.wp.com/` 前缀、丢掉 `?w=1300` 查询串,就是源站直链: ``` https://pic.4khd.com/{seg1}/{seg2}/{seg3}/w1300-rw/{name}.webp ``` 解析时一律转成源站直链(`OriginURL()`)。原因见下面的「为什么不用 Photon」。 - 路径里的 `w1300-rw` 是**原站自己的缩略图目录**,必须保留; Photon 的 `?w=1300` 是它的 resize 参数,源站不认,要去掉 - 实测去掉查询串后字节数与 Photon 正常时**完全一致**,画质没变 - 源站**零防盗链**:裸 GET(无 Referer / Cookie / UA)直接 200 ### 为什么不用 Photon(2026-10-11 实测教训) Photon 是 WordPress 的**免费图片 CDN**。批量下载会打爆它的免费额度, 被封后返回: ``` HTTP 429 Access has been disabled due to excessive usage. ``` 关键点:**这跟请求头无关**。实测对照同一张图、同一时刻: | 请求方式 | 结果 | |---|---| | 我们的头(Chrome UA + Referer) | 429 | | 完整浏览器头(+ Sec-Fetch-* / avif) | 429 | | 裸请求(一个头都不发) | **429** | | **源站直连**(`pic.4khd.com`) | **200,128056 字节** | 被封后连裸请求都 429,说明是**按 IP 的额度封禁**,不是指纹识别。 而浏览器还能正常加载,只是因为它那点量远没到限额。 源站压测:**20 并发全部成功,零限流**;连续下 50 张也全部成功。 ### 遇到限流怎么办 即便换了源站,如果将来源站也开始限流,引擎有全局冷却机制: - 认出 429/503/403 后,**所有 worker 一起停**(而不是各自退避重试) - 优先遵守服务器给的 `Retry-After`,没给就按 10s→20s→40s→80s 指数退避 - 冷却期满一起放行 > 为什么必须「一起停」:40 个 worker 各自独立退避会错峰重试, > 结果是持续往已限流的服务器打请求,永远缓不过来。 **每页必须去重**:每张图在 HTML 里出现**两次** —— 一次 ``、一次 ``, 内容完全相同。不去重的话 20 张会解析成 40 张(这个 bug 是被真实样本夹具测出来的)。 **推荐位图片怎么认**:正文图和推荐图走的是**同一个 CDN**,域名区分不了,只能靠特征: - `4KHD-beautifulGirls.webp`(推荐位统一封面) - `/w1090-h1500-p-k-no-rw/`(推荐位尺寸段) - `?resize=`(推荐位查询参数) > 路径第 3 段的时间戳**区分不了**两者 —— 实测两边形式完全一样,都是 base36 时间戳。 **分页**:`.page-link-box` 里每个页码是**独立的 `
  • `**, 取最大值即总页数。 **不能追 `rel=next`**:页面里的 `rel="next"` 和 `canonical` 都指向已 404 的 `www.4khd.com`。所以引擎只按总页数自己拼 URL,不跟这条链。 **标题**:优先 `h3.wp-block-post-title`。 > ⚠️ 别抓 `

    ` —— 那是站点名 "4KHD",不是画廊标题。 > h3 里是**双空格**(`Myu_a(뮤아)␣␣SWEETBOX`,分享链接里的 `%20%20` 证实双空格才是原文), > 而 `` / `og:title` 被 WordPress 规范化成了单空格。解析时会折叠空白。 --- ## 项目结构 ``` main.go Wails 入口 app.go 绑定层:把 Go 能力暴露给前端 wails.json Wails 配置 internal/hd4k/ ← 站点协议层(换站点只动这里) site.go 站点配置、URL 解析、域名归一化 hd4k.go 抓取、图片 URL 提取、下载 limiter.go 限流器 testdata/ 真实页面样本(p1/p2/p3) hd4k_test.go manual_real_test.go 连真实站点的手动验证(-tags manual) internal/core/ ← 与站点无关的通用引擎 store.go 任务表 + 设置(原子落盘) engine.go 调度、并发、重试、断点续传 manifest.go .hd4k.json 清单读写 paths.go 目录模板与文件名清洗 bridge.go 127.0.0.1 本地桥口(给油猴用) version.go 版本号、桥口 token core_test.go engine_test.go 端到端集成测试(本地假站点) frontend/ 静态界面(无框架、无打包) userscript/ 油猴脚本 ``` --- ## 测试 ```powershell $env:PATH = "$env:USERPROFILE\.local\go\bin;$env:PATH" go test ./internal/... -v ``` 56 个顶层测试 + 3 个参数化子测试,全部离线跑(本地 `httptest` 假站点)。 关键覆盖: - **真实页面夹具**:p1/p2/p3 三页真实 HTML,断言 20/20/13 张、共 53 张 - **解析器边界**:标题提取与实体解码、分页后缀剥离、别名域名归一 - **桥口**:token 校验(无 token → 403)、CORS 只放行已知站点、只绑回环地址 - **引擎端到端**:解析 → 下载 → 落盘 → 写清单 → 状态机整条链路 - **断点续传**:清单完整时重启 **0 网络请求**;清单信息不全时老实回退重抓 - **改目录模板**:复用老目录里已下好的文件,不重下 ### 连真实站点验证(需网络,默认不跑) ```powershell $env:HD4K_URL = "https://migmg.ssuu.uk/content/09/djawa-photo-vol0411-ddiddi-yummy-delivery.html" $env:HD4K_N = "6" go test -tags manual -run TestRealSiteDownload -v ./internal/hd4k/ ``` --- ## 应用图标 用的是站点自己的图标(`/wp-content/uploads/2023/02/4khd-1.png`,200×200)。 注意它在页面上挂的是已 404 的 `www.4khd.com`,所以从 `migmg.ssuu.uk` 取。 环境里没有 ImageMagick / ffmpeg / Python,所以 ICO 是自己生成的 (`tools/mkico`,10 个尺寸,小尺寸 BMP + 大尺寸 PNG-in-ICO): ```powershell go run ./tools/mkico -in build/icon-source.png -out build/windows/icon.ico go run ./tools/icocheck build/windows/icon.ico <导出目录> # 校验 + 导出各尺寸看效果 go run ./tools/alphaprobe <某张图> # 打印采样点的 RGBA 与 alpha 分布 ``` > 缩放必须用 box filter + premultiplied alpha。少做 premultiplied 那一步的话, > 缩放结果会**反色**(橙色变纯白)—— 这就是 `alphaprobe` 存在的理由, > 光看缩略图分不清是图错了还是预览器把透明当白底。 --- ## 清理过小的图片 「清除已完成」按钮除了清任务列表,还会顺带**删掉画廊目录里小于 10KB 的图片**。 门槛 `MinImageBytes = 10KB`(`internal/core/paths.go`)取自实测:4KHD 正文图 最小也在 **90KB 以上**,而占位图 / 损坏图 / CDN 错误页伪装的图通常只有几 KB。 实测 50 张真实下载的图,最小 90.2 KB —— 10KB 门槛不会误伤正常图。 清扫规则: - 只删**图片扩展名**的文件(`.webp .jpg .jpeg .png .gif .bmp .avif .jfif`) - 清单(`.hd4k.json`)**不动** —— 要留着续传 - `.part` 未完成的下载**不动** —— 它可能正好小于门槛但其实没下完 - 非图片文件(`.txt` 等)不动 - 单个文件出错不中断整个清扫 没有已完成任务时也能用 —— 这时按钮只做「清理小图」这一步。 --- ## 已知限制 - 解析器首选 `i\d+.wp.com` 精确匹配;一张都匹配不到时会退到宽松匹配 (任何图片扩展名 + 推荐位特征过滤),以防站点换 CDN 就全线失败 - 单个画廊最多抓 2000 页,防止异常页码把机器跑满 - 桥口 token 是明文常量,仅靠「只监听 127.0.0.1」防外部访问 —— 别把它暴露到公网 --- ## 声明 本工具仅供个人学习与备份自己有权保存的内容使用。 使用者需自行遵守目标站点的条款与所在地区的法律, 因使用本工具产生的一切后果由使用者自行承担。