300 lines
12 KiB
Markdown
300 lines
12 KiB
Markdown
# 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」防外部访问 —— 别把它暴露到公网
|
||
|
||
---
|
||
|
||
## 声明
|
||
|
||
本工具仅供个人学习与备份自己有权保存的内容使用。
|
||
使用者需自行遵守目标站点的条款与所在地区的法律,
|
||
因使用本工具产生的一切后果由使用者自行承担。 |