需求两条:
1) 列表页的复选框改成按钮,点一下直接推送到工具
2) 推送过但没点进去的画廊,标题也要变灰(像浏览过的),
这样刷新后还认得出哪些推过
一、行内推送按钮
复选框 + 底部「批量推送」两步走太啰嗦,列表页每行改成「📤 推送」
胶囊按钮,点一下直接调 /add。按钮三态:
📤 推送 -> ⏳ 推送中(禁用) -> ✓ 已推送(变绿)
底部悬浮条保留,改为「全部推送」+「重置状态」+ 本页计数。
二、标灰::visited 伪造不了,只能自己记
E-Hentai 的灰色来自自带样式
a:visited .glink, a:active .glink { color:#8F6063 }
但 :visited 是浏览器**受保护**伪类:脚本查 matches(':visited')
永远返回 false,加 class 也触发不了站点的原生规则。
所以改成自己记一份「推送过」的清单,再照抄站点的色值手动染色,
观感与原生 :visited 一致(hover 恢复站点的 #8F4701)。
三、清单存哪:工具端为主,浏览器端为辅
新增 internal/core/pushed.go —— PushedStore 独立于 Store,
单独存 %APPDATA%\EH Downloader\pushed.json。
为什么单独一个文件:state.json 里的 tasks 是「当前要下载的东西」,
用户点「清除已完成」就删掉了;但「这个画廊我推送过」是历史事实,
删了任务不该忘掉。所以推送历史与任务列表解耦。
新增桥口 GET /pushed 返回推送历史(与 GET /tasks 语义不同:
后者是当前任务列表,会被清除)。
油猴脚本侧:
- localStorage(ehdl_pushed_gids)先渲染,刷新后立刻可见,
工具不在线也能用
- 工具在线时用 /pushed 校正一次(localStorage 可能被清,
工具才是权威),以工具为准整体覆盖
- GET /tasks 保留给需要看当前任务列表的场合
四、附带:state.json 事故的恢复工具
tools/recoverstate —— 修复被写坏的 state.json:
1) token 先从下载目录的 .ehdl.json 清单里找
2) 找不到的用 E-Hentai 的 ?next=<gid> 定位法去站点抓
(该 URL 会返回以 gid 开头的 25 条列表,正则取 token,
offset 0..5 轮着试)
3) 抓不到的说明画廊在站点上已被删除
-dry 预演不写盘,写盘前先备份原文件。
配套回归测试:
- internal/core/pushed_test.go 推送历史读写、任务删除后仍保留
- internal/core/recover_e2e_test.go 恢复出的任务能真下载(71/71 页)
- internal/eh/recover_verify_test.go 恢复出的 token 能解析画廊
- app_test.go 点「清除已完成」后灰标不消失
- internal/core/bridge_test.go /tasks 与 /pushed 的 token 校验
后两者需要能连 e-hentai.org,设 EH_TEST_PROXY 才跑。
99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
package core
|
||
|
||
// 端到端验证:恢复出来的任务能真的下载
|
||
//
|
||
// 2026-10-24 事故修复的回归测试。
|
||
// state.json 被清空过,token/title/status 全丢,靠「?next=<gid> 定位法」抓回来。
|
||
// 这个测试确认抓回来的 token + title 能让引擎真的把图下下来。
|
||
//
|
||
// 需要能连上 e-hentai.org,跑法:
|
||
//
|
||
// EH_TEST_PROXY=http://127.0.0.1:7890 go test ./internal/core/ -run TestRecoveredTaskDownloads -v
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
|
||
"eh-downloader-wails/internal/eh"
|
||
)
|
||
|
||
func TestRecoveredTaskDownloads(t *testing.T) {
|
||
proxy := os.Getenv("EH_TEST_PROXY")
|
||
if proxy == "" {
|
||
t.Skip("设 EH_TEST_PROXY 才跑(需要能连 e-hentai.org)")
|
||
}
|
||
|
||
dir := t.TempDir()
|
||
dlRoot := filepath.Join(dir, "dl")
|
||
store := NewStore(filepath.Join(dir, "state.json"))
|
||
store.UpdateSettings(map[string]interface{}{
|
||
"downloadRoot": dlRoot,
|
||
"proxy": proxy,
|
||
"proxyMode": "http",
|
||
"timeout": 30000,
|
||
"xres": "auto",
|
||
"galleryConcurrency": 1,
|
||
"pageConcurrency": 2,
|
||
})
|
||
|
||
// 一个事故恢复时抓回来的任务(gid/token/title)
|
||
const gid = "4278109"
|
||
const token = "007bf09196"
|
||
const title = "Minamoto-no-Raikou [Fate/Grand Order]"
|
||
|
||
store.AddTasks([]eh.GalleryRef{{
|
||
GID: gid,
|
||
Token: token,
|
||
Raw: "https://e-hentai.org/g/" + gid + "/" + token + "/",
|
||
Title: title,
|
||
}})
|
||
|
||
sink := &testSink{}
|
||
e := NewEngine(store, sink, store.Settings())
|
||
|
||
// 只跑这一个任务
|
||
task0 := store.FindTask(gid, token)
|
||
if task0 == nil {
|
||
t.Fatal("任务不见了")
|
||
}
|
||
e.Enqueue([]Task{*task0})
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||
defer cancel()
|
||
e.Start(ctx)
|
||
|
||
task := store.FindTask(gid, token)
|
||
if task == nil {
|
||
t.Fatal("任务不见了")
|
||
}
|
||
if task.Status != "done" && task.Status != "partial" {
|
||
t.Fatalf("状态应为 done/partial,实际 %q(错误: %s)", task.Status, task.Error)
|
||
}
|
||
if task.Done == 0 {
|
||
t.Fatal("一张图都没下下来")
|
||
}
|
||
t.Logf("✓ 下载完成: %s %d/%d 页 (状态 %s)", task.Title, task.Done, task.TotalPages, task.Status)
|
||
|
||
// 文件系统里应该有东西
|
||
var files int
|
||
filepath.Walk(dlRoot, func(p string, info os.FileInfo, err error) error {
|
||
if err == nil && !info.IsDir() && filepath.Ext(p) != "" {
|
||
files++
|
||
}
|
||
return nil
|
||
})
|
||
if files == 0 {
|
||
t.Fatal("下载目录里没有文件")
|
||
}
|
||
t.Logf("✓ 磁盘上有 %d 个文件", files)
|
||
}
|
||
|
||
type testSink struct{}
|
||
|
||
func (s *testSink) TaskUpdate(t Task) {}
|
||
func (s *testSink) Log(level, msg string) {}
|
||
func (s *testSink) Done() {}
|