# EH Downloader · Wails 版 **E-Hentai 画廊批量下载工具** —— 原 Electron 版用 Go + Wails v2 重写。 > 私有项目,仅供自用,未授权分发。 ## 为什么重写 原作者在 macOS 上用 Electron 开发,项目打包要在 Mac 上跑。这台 Windows 机器上 Electron 起不来(`0x80000003`,V8 初始化崩溃,Electron 33/40 表现一致)。 于是整个业务逻辑用 Go 重写,前端界面和协议结论完全保留。 对比: | | Electron 版 | Wails 版 | |---|---|---| | 安装包 | 90 MB | **12 MB** | | 内存占用 | ~180 MB | **~26 MB** | | 冷启动 | ~2 s | **~0.2 s** | | 运行时 | Chromium 180MB | 系统 WebView2 | | 原生依赖 | 无(但要带 Chromium) | 无 | ## 环境要求 - **Go ≥ 1.21** - **Wails CLI**:`go install github.com/wailsapp/wails/v2/cmd/wails@latest` - **WebView2 Runtime**(Windows 10/11 自带或从微软官网装) ## 快速开始 ```bash # 国内建议先配镜像(否则 go mod 会卡在 proxy.golang.org) go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct go env -w GOSUMDB=off git clone cd eh-downloader-wails go mod download wails dev # 开发模式(带热重载) go test ./... # 跑测试(37 个用例) wails build # 打包 → build/bin/eh-downloader-wails.exe ``` macOS 打包:`wails build -platform darwin/universal` Linux:`wails build -platform linux/amd64` ## 功能 和原 Electron 版完全一致: - **批量下载** —— 一行一个链接,粘进去就走 - **断点续传** —— 以文件系统为准,中途断网/关窗口/删状态文件都不影响续传 - **油猴联动** —— 浏览时推送画廊,列表页勾选后批量推送 - **目录模板** —— 按作者 / 社团 / 分类 / 标签自动归档 - **任务清理** —— 只删列表项,本地文件一个都不动 - **代理支持** —— HTTP / SOCKS5,首次启动自动探测本机代理 - **失败重试 + 日志面板** —— 指数退避重试,失败清单可导出 ## 架构 ``` eh-downloader-wails/ ├── main.go Wails 启动配置(窗口尺寸、绑定注册) ├── app.go ★ 绑定层:Go 方法暴露给前端 ├── internal/ │ ├── eh/ ★ 协议核心(可独立测试,无 UI 依赖) │ │ ├── eh.go ①画廊页分页 → ②/s/ → ③hath直链 → 下载 │ │ ├── eh_test.go 单元测试 │ │ └── e2e_test.go 端到端链路测试(httptest 模拟 EH) │ └── core/ │ ├── engine.go 任务编排、并发池、退避重试、断点续传 │ ├── store.go JSON 原子写 + 任务状态机 │ ├── paths.go 目录模板引擎 + 路径安全清洗 │ ├── bridge.go 本地 HTTP 桥口(供油猴推送) │ └── *_test.go 单测 ├── frontend/ │ ├── index.html 界面(沿用原版) │ ├── js/api.js ★ IPC 适配层:window.eh.* → Wails bindings │ ├── js/renderer.js 渲染逻辑(沿用原版,未改) │ └── css/style.css 样式(沿用原版) └── userscript/ 油猴脚本(沿用原版,协议未变) ``` ### 与 Electron 版的对应关系 | Electron | Wails | |---|---| | `ipcMain.handle('settings:get')` | `func (a *App) GetSettings() Settings` | | `contextBridge` preload | `frontend/js/api.js` 适配层 | | `webContents.send('log')` | `runtime.EventsEmit(ctx, "log", ...)` | | `axios` | `net/http` | | `https-proxy-agent` | `net/http.Transport.Proxy` | | `socks-proxy-agent` | `golang.org/x/net/proxy` | | `fs.promises.rename` 原子写 | 同(`os.Rename`) | | `AbortController` | `context.WithCancel` | 前端业务代码(`renderer.js`)**一行没改** —— 原来调 `window.eh.*`, 现在由 `api.js` 把这些调用转发到 Wails 的 Go 绑定。 ## 协议逆向(2026 版,结论沿用原项目实测) 每个画廊需要 **2N+1 次请求**(N = 页数),无法绕过: | 阶段 | 请求 | 说明 | |---|---|---| | 1 | `GET /g/{gid}/{token}/` | 拿全部页的 `/s/` 链接 + 原始文件名 | | 2 | `GET /s/{key}/{gid}-{page}` | 拿该页 hath 全图 URL,**N 次** | | 3 | `GET ` | 下载图片本身,**N 次** | ### 三个必须知道的坑 **1. 画廊页每屏只列 20 张,必须翻 `?p=N` 分页** ``` /g/{gid}/{token}/ → 1-20 /g/{gid}/{token}/?p=1 → 21-40 ``` 不做这个就只会下到前 20 张(且不报错,静默截断)。 `FetchGalleryPage` 有分页循环,按 `Showing X - Y of N images` 自动翻页。 e2e 测试里用 45 张的 mock 专门验证了这个。 **2. `/s/` 的 key 必须和页号配对** `/s/bb06b5db77/4239250-1` 里的 `bb06b5db77` 是第 1 页专属的 key。 拿它去请求第 2 页会返回 200 但内容是错的。 **3. 图片 URL 每次都不一样** ``` https://oclzqqk.gyktyewfonyf.hath.network:39656/h/4e9af9f.../keystamp=1791448200-...;xres=800/NH.webp ↑随机子域 ↑随机端口 ↑带时间戳的签名 ``` 随机子域名 + 随机端口 + 时间戳签名 —— **不存在固定直链**, 只能实时从 `/s/` 页面解析。代码在 `ResolveImageURL`。 > 这就是网上老爬虫失效的真正原因。它不是"解码"问题, > 就是从 HTML 里正则提取一条 URL,没有签名计算。 ### 其他实测结论 - **Referer 不是必需的**,裸请求也能 200。代码仍带 UA 兜底。 - 图片实际格式是 **WebP**。文件名用画廊页 `title="Page N: 原名"` 里的原名。 - `xres` 参数:数字是**长边上限**,`xres=org` 请求原图。 - 并发 10 未触发限流,但建议 **2~4**。 ### 已失效的老接口 | 老写法 | 现状 | |---|---| | 页面内 `var gmetadata = {...}` | 已移除 | | `/img/{gid}/{gid}/{gid}-{page}.jpg` | 已改为 `/{2字符}/{哈希}/{gid}-{n}.webp` | | `?pkl=&pjt=` 懒加载 | 已移除 | | `api.e-hentai.org` 的 `method=gdata` | 对所有 gid 返回 `Gallery not found` | | 表单提交 | 已改为 JSON body,`gidlist` 必须是数组 | ## 目录结构模板 默认 `{title}`。可用占位符: | 占位符 | 含义 | 兜底值 | |---|---|---| | `{title}` | 画廊标题 | — | | `{gid}` | 画廊号 | — | | `{artist}` | 作者:`artist:` → 标题 `[X]` → 上传者 | `未知作者` | | `{group}` | 社团:`group:` → 标题 `(X)` | `未知社团` | | `{series}` | 系列(`parody:`) | `无系列` | | `{category}` | 分类 | `未分类` | | `{lang}` | 语言 | `nolang` | | `{uploader}` | 上传者 | `unknown` | | `{tag}` `{tag1}` `{tag2}` | 第 N 个普通标签 | 空 | | `{tags}` | 全部普通标签,`_` 连接 | 空 | | `{tags:3}` | 前 3 个普通标签 | 空 | 常用预设:`{artist}/{title}`、`{group}/{title}`、`{category}/{group}/{title}`。 **换模板不会重下已完成的图** —— 引擎下之前会先去老的「标题目录」里看一眼。 每段路径都会清洗:`/ \ : * ? " < > |` 和控制字符换成 `_`, `.` / `..` 直接作废(防目录穿越),超长截到 110 字符,末尾的点/空格去掉, **Windows 保留设备名(CON/PRN/NUL/COM1…)会自动加前缀**(Go 版比原版多这层)。 ## 设置 | 项 | 默认 | 说明 | |---|---|---| | 下载目录 | `~/Downloads/EH` | 每个画廊一个子目录 | | 代理模式 | 自动探测 | off / http / socks5 | | 代理地址 | 自动探测 | 常见端口 7890/7897/1087/1080/8080/10808/10809/8888 | | 并发数 | 3 | 建议 2~4 | | 超时 | 30s | | | 下载分辨率 | 站点默认 | `auto` / `org` / 数字 | | 目录模板 | `{title}` | | | 油猴推送端口 | 37777 | | 状态文件: - Windows:`%APPDATA%\EH Downloader\state.json` - macOS:`~/Library/Application Support/EH Downloader/state.json` - Linux:`~/.config/EH Downloader/state.json` ## 本地桥口(油猴推送) App 启动时在 `127.0.0.1:37777` 起 HTTP 服务(**只绑回环**): | 接口 | 说明 | |---|---| | `GET /ping` | 探活,返回 `{ok:true}` | | `POST /add` | 推送单个,需带 `X-EHDL-Token: ehdl-local-9f2a4c7e` | | `POST /add-batch` | 批量推送,`{items:[...]}`,一次最多 500 个 | | `GET /info` | 版本与端口 | 安全措施:**固定 token + Origin 白名单**(只放行 e-hentai.org / exhentai.org)。 > 油猴脚本用 `GM_xmlhttpRequest` 而不是 `fetch`,直接绕开 CORS。 ## 已知限制 - **只支持 E-Hentai**,不支持 ExHentai(需账号 cookie,本工具不含登录)。 受限画廊会报「画廊受限或不存在」。 - 每张图 2 次请求,大画廊慢。206 张约 413 次请求、并发 3 时约 8 分钟。 - `xres=org` 并非所有画廊都支持。 - 下载的是 WebP,不是上传者提交的原始 jpg/png。 ## 测试 ```bash go test ./... # 全部 go test ./internal/eh/ -v # 协议层(含端到端) go test -race ./... # 竞态检测 ``` 37 个用例覆盖: - 输入解析(多行/分隔符/去重/注释) - hath 直链正则(含不该匹配的负例) - xres 改写(auto/org/数字) - 画廊页分页(45 张 mock 验证 3 屏翻页) - key 与页码配对 - meta 解析 + 标题约定兜底 - 目录模板展开 + 路径穿越防护 + Windows 保留名 - 断点续传、取消清理 .part - 状态机(清除已完成/失败重排/崩溃归位/文件损坏回落) ## 免责声明 仅供学习与技术研究使用。使用者自行遵守 [E-Hentai 的站点条款](https://e-hentai.org/policy.php) 及相关法律法规, 尊重内容作者的著作权,不要将下载内容用于传播或商业用途。 **本项目为私有项目,保留所有权利。未经作者许可,请勿分发或二次发布。**