原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。 按用户多年使用习惯(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 次请求
87 lines
1.9 KiB
Go
87 lines
1.9 KiB
Go
package eh
|
|
|
|
// 全局限流器
|
|
//
|
|
// 算法对齐参考实现(E-Hentai Downloader 的「请求间隔」设置):
|
|
// 用一个全局的 nextRequestTime,每次请求都把自己排到它后面,再把它往后推。
|
|
//
|
|
// next = max(next, now) + interval
|
|
// 等待到 next 时刻再真正发出
|
|
//
|
|
// 效果是「所有请求之间至少间隔 interval 毫秒」,与并发数无关 ——
|
|
// 哪怕 40 个 worker 同时来,也会被串成每 interval 放行一个。
|
|
//
|
|
// interval = 0 时完全旁路,不产生任何额外开销。
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Limiter 全局请求节流器
|
|
type Limiter struct {
|
|
mu sync.Mutex
|
|
interval time.Duration
|
|
next time.Time // 下一个请求允许发出的时刻
|
|
}
|
|
|
|
// NewLimiter 建限流器(interval <= 0 表示不限流)
|
|
func NewLimiter(interval time.Duration) *Limiter {
|
|
if interval < 0 {
|
|
interval = 0
|
|
}
|
|
return &Limiter{interval: interval}
|
|
}
|
|
|
|
// SetInterval 运行时改间隔
|
|
func (l *Limiter) SetInterval(d time.Duration) {
|
|
if d < 0 {
|
|
d = 0
|
|
}
|
|
l.mu.Lock()
|
|
l.interval = d
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
// Interval 当前间隔
|
|
func (l *Limiter) Interval() time.Duration {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.interval
|
|
}
|
|
|
|
// Wait 占用一个请求名额,必要时阻塞到放行时刻。
|
|
// 返回 error 是为了能响应取消(点「停止」时不该继续排队)。
|
|
func (l *Limiter) Wait(ctx context.Context) error {
|
|
l.mu.Lock()
|
|
interval := l.interval
|
|
if interval <= 0 {
|
|
l.mu.Unlock()
|
|
return ctx.Err()
|
|
}
|
|
|
|
now := time.Now()
|
|
// 关键:如果过去的排队已经堆积,next 可能远在未来,
|
|
// 这里取 max 让第一个请求立刻发出去,避免冷启动时集体等一大轮。
|
|
if l.next.Before(now) {
|
|
l.next = now
|
|
}
|
|
at := l.next
|
|
l.next = at.Add(interval)
|
|
l.mu.Unlock()
|
|
|
|
delay := time.Until(at)
|
|
if delay <= 0 {
|
|
return ctx.Err()
|
|
}
|
|
|
|
t := time.NewTimer(delay)
|
|
defer t.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-t.C:
|
|
return nil
|
|
}
|
|
} |