原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。 按用户多年使用习惯(5 画廊 × 每画廊 8 张)改造为双层并发。 - 双层并发:画廊并发(默认5) × 页并发(默认8) = 最多 40 张同时下载, 两个值都在设置面板可调(1-20)。页级 worker 池 + mutex 保护计数。 - 全局限流:对齐参考实现的 nextRequestTime 算法,限所有请求 (含 /s/ 解析与图片本体),默认 0 秒关闭。 - 整轮扫尾:跑完后自动重扫失败页 1 轮(同样走完整 4 次退避重试), 仍失败则停下并标记 partial,换线路后手动点开始即可续传。 - 画廊目录清单 .ehdl.json:记录 gid/token/总页数/已完成页, 即使 state.json 丢失也能算出「应该多少张、还差多少张」。 顺带修复: - Start() 双重加锁导致死锁(引擎完全无法启动) - clampInt 越界时返回默认值而非边界值(设 30 会静默变 8) - 直链正则写死 hath.network,CDN 换域名会全军覆没 -> 加图片 URL 兜底 - ReconcileTask 会误报「有改动」 测试从 37 增至 62,其中并发/限流/扫尾用 httptest 模拟 EH 实测: - 页并发峰值 = 8、画廊并发峰值 = 5(完全生效) - 12 次请求限流 80ms 实测 966ms(期望 880ms) - 断点续传 4 张已存在时只发 5 次请求
241 lines
5.9 KiB
Go
241 lines
5.9 KiB
Go
package core
|
|
|
|
// 画廊清单(.ehdl.json)测试
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"eh-downloader-wails/internal/eh"
|
|
)
|
|
|
|
func TestManifestRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
m := Manifest{
|
|
GID: "4239274",
|
|
Token: "55e069b6b9",
|
|
Title: "[Noxe] Test",
|
|
TotalPages: 206,
|
|
DonePages: []int{3, 1, 2},
|
|
FailedPage: []int{10, 9},
|
|
}
|
|
|
|
if err := WriteManifest(dir, m); err != nil {
|
|
t.Fatalf("写清单失败: %v", err)
|
|
}
|
|
|
|
// 文件应该真的存在
|
|
p := filepath.Join(dir, ManifestFileName)
|
|
if _, err := os.Stat(p); err != nil {
|
|
t.Fatalf("清单文件没生成: %v", err)
|
|
}
|
|
// 不该残留 .tmp
|
|
if _, err := os.Stat(p + ".tmp"); err == nil {
|
|
t.Error("临时文件没清理")
|
|
}
|
|
|
|
got, err := ReadManifest(dir)
|
|
if err != nil {
|
|
t.Fatalf("读清单失败: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("读回来的清单是 nil")
|
|
}
|
|
|
|
if got.GID != m.GID || got.Token != m.Token {
|
|
t.Errorf("gid/token 不对: %s/%s", got.GID, got.Token)
|
|
}
|
|
if got.TotalPages != 206 {
|
|
t.Errorf("总页数不对: %d", got.TotalPages)
|
|
}
|
|
if got.Version != ManifestVersion {
|
|
t.Errorf("版本号不对: %d", got.Version)
|
|
}
|
|
|
|
// 页码应该被排序过
|
|
for i := 1; i < len(got.DonePages); i++ {
|
|
if got.DonePages[i] < got.DonePages[i-1] {
|
|
t.Errorf("donePages 没排序: %v", got.DonePages)
|
|
break
|
|
}
|
|
}
|
|
if len(got.DonePages) != 3 {
|
|
t.Errorf("donePages 数量不对: %v", got.DonePages)
|
|
}
|
|
}
|
|
|
|
func TestManifestNotExist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
m, err := ReadManifest(dir)
|
|
if err != nil {
|
|
t.Errorf("不存在时不该报错: %v", err)
|
|
}
|
|
if m != nil {
|
|
t.Error("不存在时该返回 nil")
|
|
}
|
|
}
|
|
|
|
func TestManifestCorrupted(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, ManifestFileName), []byte("{坏掉的 json"), 0o644)
|
|
|
|
if _, err := ReadManifest(dir); err == nil {
|
|
t.Error("坏 json 应该报错")
|
|
}
|
|
}
|
|
|
|
func TestScanManifests(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// 两个正常画廊
|
|
d1 := filepath.Join(root, "画廊A")
|
|
d2 := filepath.Join(root, "画廊B")
|
|
os.MkdirAll(d1, 0o755)
|
|
os.MkdirAll(d2, 0o755)
|
|
WriteManifest(d1, Manifest{GID: "111", Token: "a", TotalPages: 10, DonePages: []int{1, 2}})
|
|
WriteManifest(d2, Manifest{GID: "222", Token: "b", TotalPages: 5, DonePages: []int{1}})
|
|
|
|
// 一个非画廊目录(没清单)
|
|
os.MkdirAll(filepath.Join(root, "普通文件夹"), 0o755)
|
|
|
|
// 一个隐藏目录(该跳过)
|
|
hidden := filepath.Join(root, ".hidden")
|
|
os.MkdirAll(hidden, 0o755)
|
|
WriteManifest(hidden, Manifest{GID: "999", Token: "z"})
|
|
|
|
got := ScanManifests(root)
|
|
if len(got) != 2 {
|
|
t.Fatalf("应扫到 2 个清单,实际 %d: %+v", len(got), got)
|
|
}
|
|
|
|
found := map[string]bool{}
|
|
for _, m := range got {
|
|
found[m.GID] = true
|
|
}
|
|
if !found["111"] || !found["222"] {
|
|
t.Errorf("扫到的画廊不对: %v", found)
|
|
}
|
|
if found["999"] {
|
|
t.Error("隐藏目录不该被扫到")
|
|
}
|
|
}
|
|
|
|
// 用清单补全丢失的任务信息
|
|
func TestReconcileTask(t *testing.T) {
|
|
m := &Manifest{
|
|
GID: "111",
|
|
TotalPages: 206,
|
|
Title: "从清单恢复的标题",
|
|
DonePages: []int{1, 2, 3, 7},
|
|
}
|
|
|
|
// 模拟 state.json 丢了的任务
|
|
task := &Task{GID: "111", Token: "a", Pages: map[string]string{}}
|
|
|
|
if !ReconcileTask(task, m) {
|
|
t.Error("应该有修改")
|
|
}
|
|
if task.Title != "从清单恢复的标题" {
|
|
t.Errorf("标题没补上: %q", task.Title)
|
|
}
|
|
if task.TotalPages != 206 {
|
|
t.Errorf("总页数没补上: %d", task.TotalPages)
|
|
}
|
|
if len(task.Pages) != 4 {
|
|
t.Errorf("已完成页没补上: %v", task.Pages)
|
|
}
|
|
if task.Pages["7"] != "done" {
|
|
t.Errorf("第 7 页该是 done: %v", task.Pages)
|
|
}
|
|
|
|
// 已有信息不该被覆盖
|
|
task2 := &Task{
|
|
GID: "111", Title: "已有标题", TotalPages: 10,
|
|
Pages: map[string]string{"1": "done", "2": "done", "3": "done", "7": "done"},
|
|
}
|
|
if ReconcileTask(task2, m) {
|
|
t.Error("清单内容都已存在时不该报改动")
|
|
}
|
|
if task2.Title != "已有标题" {
|
|
t.Errorf("已有标题被覆盖了: %q", task2.Title)
|
|
}
|
|
if task2.TotalPages != 10 {
|
|
t.Errorf("已有总页数被覆盖了: %d", task2.TotalPages)
|
|
}
|
|
|
|
// 完全空白任务 + 空清单:不该有任何改动
|
|
empty := &Task{GID: "999", Pages: map[string]string{}}
|
|
if ReconcileTask(empty, &Manifest{GID: "999"}) {
|
|
t.Error("空清单不该产生改动")
|
|
}
|
|
}
|
|
|
|
// state.json 丢失的真实场景:完整走一遍
|
|
func TestManifestEnablesResumeWithoutState(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "某画廊")
|
|
os.MkdirAll(dir, 0o755)
|
|
|
|
// 模拟:之前下过 3 张,清单记录了
|
|
WriteManifest(dir, Manifest{
|
|
GID: "4239274",
|
|
Token: "abc",
|
|
Title: "测试",
|
|
TotalPages: 10,
|
|
DonePages: []int{1, 2, 3},
|
|
})
|
|
for i := 1; i <= 3; i++ {
|
|
os.WriteFile(filepath.Join(dir, "00"+string(rune('0'+i))+".png"), []byte("data"), 0o644)
|
|
}
|
|
|
|
// 现在 state.json 完全丢失,只能靠清单 + 文件系统
|
|
ms := ScanManifests(root)
|
|
if len(ms) != 1 {
|
|
t.Fatalf("应扫到 1 个,实际 %d", len(ms))
|
|
}
|
|
|
|
m := ms[0]
|
|
if m.TotalPages != 10 {
|
|
t.Errorf("总页数不该: %d", m.TotalPages)
|
|
}
|
|
|
|
// 靠文件系统知道哪几张有了
|
|
have := 0
|
|
for i := 1; i <= m.TotalPages; i++ {
|
|
name := "00" + string(rune('0'+i)) + ".png"
|
|
if st, err := os.Stat(filepath.Join(dir, name)); err == nil && st.Size() > 0 {
|
|
have++
|
|
}
|
|
}
|
|
if have != 3 {
|
|
t.Errorf("应有 3 张已下载,实际 %d", have)
|
|
}
|
|
// 还差 7 张 —— 这是清单带来的价值
|
|
if m.TotalPages-have != 7 {
|
|
t.Errorf("应还差 7 张,实际 %d", m.TotalPages-have)
|
|
}
|
|
}
|
|
|
|
func TestFindTaskByGID(t *testing.T) {
|
|
s, _ := newTestStore(t)
|
|
s.AddTasks([]eh.GalleryRef{{GID: "111", Token: "tokA"}})
|
|
|
|
// token 对不上也能按 gid 找到
|
|
task := s.FindTaskByGID("111")
|
|
if task == nil {
|
|
t.Fatal("应该找得到")
|
|
}
|
|
if task.Token != "tokA" {
|
|
t.Errorf("token 不对: %s", task.Token)
|
|
}
|
|
|
|
if s.FindTaskByGID("999") != nil {
|
|
t.Error("不存在的 gid 应返回 nil")
|
|
}
|
|
// token 精确匹配仍要正常工作
|
|
if s.FindTask("111", "tokA") == nil {
|
|
t.Error("精确查找应该能找到")
|
|
}
|
|
} |