feat: 双层并发 + 全局限流 + 整轮扫尾 + 画廊目录清单
原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。 按用户多年使用习惯(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 次请求
This commit is contained in:
+41
-5
@@ -172,6 +172,8 @@ var (
|
||||
|
||||
// hath 全图 URL:随机子域 + 随机端口 + keystamp 签名
|
||||
reHath = regexp.MustCompile(`(?i)https://[a-z0-9-]+(?:\.[a-z0-9-]+)*\.hath\.network(?::\d+)?/[^\s"'<>)\]]+`)
|
||||
// 兜底:CDN 换域名时,从页面里抓第一个看起来像图片的 URL
|
||||
reImgAny = regexp.MustCompile(`(?i)https?://[^\s"'<>)\]]+\.(?:png|jpe?g|webp|gif)(?:\?[^\s"'<>)\]]*)?`)
|
||||
)
|
||||
|
||||
// parseMeta 从画廊页 HTML 的 #taglist 表格解析 艺术家/社团/分类/标签
|
||||
@@ -275,6 +277,8 @@ type Client struct {
|
||||
proxy string
|
||||
proxyMode string // off | http | socks5
|
||||
xres string // auto | org | 数字(长边上限)
|
||||
|
||||
limiter *Limiter
|
||||
}
|
||||
|
||||
// NewClient 建客户端
|
||||
@@ -285,7 +289,26 @@ func NewClient(timeout time.Duration, proxyURL, proxyMode, xres string) *Client
|
||||
if xres == "" {
|
||||
xres = "auto"
|
||||
}
|
||||
return &Client{timeout: timeout, proxy: proxyURL, proxyMode: proxyMode, xres: xres}
|
||||
return &Client{
|
||||
timeout: timeout,
|
||||
proxy: proxyURL,
|
||||
proxyMode: proxyMode,
|
||||
xres: xres,
|
||||
limiter: NewLimiter(0),
|
||||
}
|
||||
}
|
||||
|
||||
// SetRequestInterval 设置全局请求间隔(0 = 不限流)
|
||||
func (c *Client) SetRequestInterval(d time.Duration) {
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
c.limiter.SetInterval(d)
|
||||
}
|
||||
|
||||
// RequestInterval 当前请求间隔
|
||||
func (c *Client) RequestInterval() time.Duration {
|
||||
return c.limiter.Interval()
|
||||
}
|
||||
|
||||
// SetProxy 改代理配置
|
||||
@@ -358,6 +381,11 @@ func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, u string, timeout time.Duration) (*http.Response, error) {
|
||||
// 全局限流:所有请求(含 /s/ 解析与画廊页)统一排队
|
||||
if err := c.limiter.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -506,11 +534,14 @@ func (c *Client) ResolveImageURL(ctx context.Context, sPath string) (string, err
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := reHath.FindString(html)
|
||||
if m == "" {
|
||||
return "", fmt.Errorf("未能从 /s/ 页面解析出图片地址: %s", sPath)
|
||||
if m := reHath.FindString(html); m != "" {
|
||||
return c.applyXres(DecodeEntities(m)), nil
|
||||
}
|
||||
return c.applyXres(DecodeEntities(m)), nil
|
||||
// CDN 换域名时的兜底:抓页面上第一个图片 URL
|
||||
if m := reImgAny.FindString(html); m != "" {
|
||||
return c.applyXres(DecodeEntities(m)), nil
|
||||
}
|
||||
return "", fmt.Errorf("未能从 /s/ 页面解析出图片地址: %s", sPath)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -542,6 +573,11 @@ func (c *Client) applyXres(u string) string {
|
||||
func (c *Client) DownloadImage(ctx context.Context, imgURL, destPath string, onProgress func(received, total int64)) (int64, error) {
|
||||
_, _, baseTimeout := c.snapshot()
|
||||
|
||||
// 图片本体也要限流
|
||||
if err := c.limiter.Wait(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imgURL, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
Reference in New Issue
Block a user