原 Electron 版在这台 Windows 机器上无法启动(0x80000003 崩在 V8 初始化), 整个业务逻辑用 Go 重写,前端界面与协议结论完全保留。 - internal/eh: 协议核心,画廊页分页 -> /s/ -> hath 直链 -> 下载 - internal/core: 并发引擎 / 状态存储 / 目录模板 / 油猴桥口 - app.go: Wails 绑定层,替代 Electron IPC - frontend/js/api.js: 适配层,renderer.js 一行未改 - 37 个测试,含 httptest 模拟 EH 的端到端链路验证 相比 Electron 版:90MB -> 12.4MB,180MB 内存 -> 27MB,构建 90s -> 2s。 顺带修掉原版代理探测的 bug(永远返回第一个端口,从不等连接结果)。
237 lines
6.9 KiB
Go
237 lines
6.9 KiB
Go
package eh
|
|
|
|
import "testing"
|
|
|
|
// ---- 输入解析 ----
|
|
|
|
func TestParseGalleryInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want []GalleryRef
|
|
}{
|
|
{
|
|
name: "标准完整链接",
|
|
in: "https://e-hentai.org/g/4239272/55e069b6b9",
|
|
want: []GalleryRef{{GID: "4239272", Token: "55e069b6b9"}},
|
|
},
|
|
{
|
|
name: "多行多链接",
|
|
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa\n" +
|
|
"https://e-hentai.org/g/2222222/bbbbbbbbbb",
|
|
want: []GalleryRef{
|
|
{GID: "1111111", Token: "aaaaaaaaaa"},
|
|
{GID: "2222222", Token: "bbbbbbbbbb"},
|
|
},
|
|
},
|
|
{
|
|
name: "竖线分隔一行多个",
|
|
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa|https://e-hentai.org/g/2222222/bbbbbbbbbb",
|
|
want: []GalleryRef{
|
|
{GID: "1111111", Token: "aaaaaaaaaa"},
|
|
{GID: "2222222", Token: "bbbbbbbbbb"},
|
|
},
|
|
},
|
|
{
|
|
name: "去重",
|
|
in: "https://e-hentai.org/g/1111111/aaaaaaaaaa\nhttps://e-hentai.org/g/1111111/aaaaaaaaaa",
|
|
want: []GalleryRef{{GID: "1111111", Token: "aaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "注释行忽略",
|
|
in: "# 这是注释\nhttps://e-hentai.org/g/1111111/aaaaaaaaaa",
|
|
want: []GalleryRef{{GID: "1111111", Token: "aaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "裸 gid 不匹配完整链接",
|
|
in: "4239274",
|
|
want: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ParseGalleryInput(tt.in)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("数量不对: got %d want %d (%+v)", len(got), len(tt.want), got)
|
|
}
|
|
for i := range got {
|
|
if got[i].GID != tt.want[i].GID || got[i].Token != tt.want[i].Token {
|
|
t.Errorf("第 %d 项不对: got %s/%s want %s/%s",
|
|
i, got[i].GID, got[i].Token, tt.want[i].GID, tt.want[i].Token)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractGIDs(t *testing.T) {
|
|
got := ExtractGIDs("看这个 4239274 还有 4239274 重复 和 1234567")
|
|
if len(got) != 2 {
|
|
t.Fatalf("应去重成 2 个,实际 %d 个: %v", len(got), got)
|
|
}
|
|
if got[0] != "4239274" || got[1] != "1234567" {
|
|
t.Errorf("结果不对: %v", got)
|
|
}
|
|
}
|
|
|
|
// ---- hath 直链正则(核心) ----
|
|
|
|
func TestReHath(t *testing.T) {
|
|
// 从 README 里抄的真实样本
|
|
samples := []string{
|
|
`https://oclzqqk.gyktyewfonyf.hath.network:39656/h/4e9af9f...-44168-707-754-wbp/keystamp=1791448200-8b38ea8f6d;fileindex=270107377;xres=800/NH.webp`,
|
|
`<img src="https://abc.hath.network:8080/h/xyz/gid-1.jpg">`,
|
|
`"https://foo.bar.hath.network/h/a/b/c.png"`,
|
|
}
|
|
for _, s := range samples {
|
|
m := reHath.FindString(s)
|
|
if m == "" {
|
|
t.Errorf("没匹配到: %s", s)
|
|
continue
|
|
}
|
|
if m[len(m)-1] == '"' || m[len(m)-1] == '>' {
|
|
t.Errorf("匹配多了尾部字符: %q", m)
|
|
}
|
|
}
|
|
|
|
// 不该匹配的
|
|
for _, s := range []string{
|
|
`https://e-hentai.org/s/abc/123-1`,
|
|
`https://example.com/h/foo`,
|
|
} {
|
|
if m := reHath.FindString(s); m != "" {
|
|
t.Errorf("不该匹配却匹配了: %q", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplyXres(t *testing.T) {
|
|
const base = "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=800/NH.webp"
|
|
|
|
cases := []struct {
|
|
xres string
|
|
want string
|
|
}{
|
|
{"auto", base},
|
|
{"", base},
|
|
{"org", "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=org/NH.webp"},
|
|
{"1920", "https://a.hath.network:39656/h/x/keystamp=1;fileindex=2;xres=1920/NH.webp"},
|
|
}
|
|
for _, c := range cases {
|
|
cl := NewClient(0, "", "off", c.xres)
|
|
if got := cl.applyXres(base); got != c.want {
|
|
t.Errorf("xres=%s: got %s\n want %s", c.xres, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- 缩略图链接解析(画廊页分页的关键) ----
|
|
|
|
func TestReThumbLink(t *testing.T) {
|
|
// 模拟 EH 画廊页里的一行
|
|
html := `<a href="/s/bb06b5db77/4239250-1"><div title="Page 1: 001.jpg" class="lazy"></div></a>
|
|
<a href="https://e-hentai.org/s/cc11ee22ff/4239250-2"><div title="Page 2: 002.png"></div></a>`
|
|
|
|
ms := reThumbLink.FindAllStringSubmatch(html, -1)
|
|
if len(ms) != 2 {
|
|
t.Fatalf("应匹配 2 个,实际 %d", len(ms))
|
|
}
|
|
if ms[0][1] != "/s/bb06b5db77/4239250-1" {
|
|
t.Errorf("第 1 个路径不对: %q", ms[0][1])
|
|
}
|
|
if ms[0][2] != "1" || ms[0][3] != "001.jpg" {
|
|
t.Errorf("第 1 个页码/文件名不对: page=%s name=%q", ms[0][2], ms[0][3])
|
|
}
|
|
if ms[1][1] != "https://e-hentai.org/s/cc11ee22ff/4239250-2" {
|
|
t.Errorf("第 2 个路径不对: %q", ms[1][1])
|
|
}
|
|
}
|
|
|
|
func TestReTotal(t *testing.T) {
|
|
for _, s := range []string{
|
|
"Showing 1 - 20 of 206 images",
|
|
"Showing 21 - 40 of 206 images",
|
|
} {
|
|
m := reTotal.FindStringSubmatch(s)
|
|
if m == nil {
|
|
t.Fatalf("没匹配: %s", s)
|
|
}
|
|
if m[1] != "206" {
|
|
t.Errorf("总数不对: %s", m[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- meta 解析 ----
|
|
|
|
func TestParseMeta(t *testing.T) {
|
|
html := `<meta name="description" content="Free Hentai Doujinshi Gallery: Something" />
|
|
<div id="taglist">
|
|
<table>
|
|
<tr><td class="tc">artist:</td><td><div class="gtl"><a href="/tag/artist:noxe">Noxe</a><a href="/tag/artist:other">Other</a></div></td></tr>
|
|
<tr><td class="tc">group:</td><td><div class="gtl"><a href="/tag/group:criticalhai">CriticalHAI</a></div></td></tr>
|
|
<tr><td class="tc">parody:</td><td><div class="gtl"><a href="/tag:parody:wuthering">wuthering waves</a></div></td></tr>
|
|
<tr><td class="tc">language:</td><td><div class="gtl"><a href="/tag:language:chinese">chinese</a></div></td></tr>
|
|
<tr><td class="tc">other:</td><td><div class="gtl"><a href="/tag:qingxiao">qingxiao</a><a href="/tag:rover">rover</a><a href="/tag:more">more</a></div></td></tr>
|
|
</table>
|
|
</div>
|
|
<a href="/uploader/velius11">uploader</a>`
|
|
|
|
meta := parseMeta(html, "[Noxe] Title (Circle)")
|
|
if meta.Category != "doujinshi" {
|
|
t.Errorf("分类不对: %q", meta.Category)
|
|
}
|
|
if meta.Artist != "Noxe" {
|
|
t.Errorf("作者不对: %q", meta.Artist)
|
|
}
|
|
if meta.Group != "CriticalHAI" {
|
|
t.Errorf("社团不对: %q", meta.Group)
|
|
}
|
|
if meta.Series != "wuthering waves" {
|
|
t.Errorf("系列不对: %q", meta.Series)
|
|
}
|
|
if meta.Lang != "chinese" {
|
|
t.Errorf("语言不对: %q", meta.Lang)
|
|
}
|
|
// more 按钮要过滤掉
|
|
if len(meta.Tags) != 2 || meta.Tags[0] != "qingxiao" || meta.Tags[1] != "rover" {
|
|
t.Errorf("标签不对(more 未过滤): %v", meta.Tags)
|
|
}
|
|
if meta.Uploader != "velius11" {
|
|
t.Errorf("上传者不对: %q", meta.Uploader)
|
|
}
|
|
}
|
|
|
|
// 标题约定兜底:[作者] 标题 (社团)
|
|
func TestParseMetaTitleFallback(t *testing.T) {
|
|
html := `<div id="taglist"></div>`
|
|
meta := parseMeta(html, "[Noxe] 0820 Qingxiao [AI Generated] (CriticalHAI)")
|
|
if meta.Artist != "Noxe" {
|
|
t.Errorf("作者兜底失败: %q", meta.Artist)
|
|
}
|
|
if meta.Group != "CriticalHAI" {
|
|
t.Errorf("社团兜底失败: %q", meta.Group)
|
|
}
|
|
}
|
|
|
|
// ---- HTML 实体 ----
|
|
|
|
func TestDecodeEntities(t *testing.T) {
|
|
cases := map[string]string{
|
|
"a & b": "a & b",
|
|
"<tag>": "<tag>",
|
|
""x"": `"x"`,
|
|
"AB": "AB",
|
|
"A": "A",
|
|
"plain": "plain",
|
|
"😀": "\U0001F600",
|
|
"no entities!": "no entities!",
|
|
}
|
|
for in, want := range cases {
|
|
if got := DecodeEntities(in); got != want {
|
|
t.Errorf("DecodeEntities(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|