Files
2026-10-11 12:56:38 +08:00

300 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 里出现**两次** —— 一次 `<a href>`、一次 `<img src>`,
内容完全相同。不去重的话 20 张会解析成 40 张(这个 bug 是被真实样本夹具测出来的)。
**推荐位图片怎么认**:正文图和推荐图走的是**同一个 CDN**,域名区分不了,只能靠特征:
- `4KHD-beautifulGirls.webp`(推荐位统一封面)
- `/w1090-h1500-p-k-no-rw/`(推荐位尺寸段)
- `?resize=`(推荐位查询参数)
> 路径第 3 段的时间戳**区分不了**两者 —— 实测两边形式完全一样,都是 base36 时间戳。
**分页**:`.page-link-box` 里每个页码是**独立的 `<li class="numpages">`**,
取最大值即总页数。
**不能追 `rel=next`**:页面里的 `rel="next"` 和 `canonical` 都指向已 404 的
`www.4khd.com`。所以引擎只按总页数自己拼 URL,不跟这条链。
**标题**:优先 `h3.wp-block-post-title`。
> ⚠️ 别抓 `<h1>` —— 那是站点名 "4KHD",不是画廊标题。
> h3 里是**双空格**(`Myu_a(뮤아)␣␣SWEETBOX`,分享链接里的 `%20%20` 证实双空格才是原文),
> 而 `<title>` / `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」防外部访问 —— 别把它暴露到公网
---
## 声明
本工具仅供个人学习与备份自己有权保存的内容使用。
使用者需自行遵守目标站点的条款与所在地区的法律,
因使用本工具产生的一切后果由使用者自行承担。