原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。 按用户多年使用习惯(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 次请求
219 lines
6.0 KiB
TypeScript
219 lines
6.0 KiB
TypeScript
export namespace core {
|
|
|
|
export class FailedItem {
|
|
page: number;
|
|
file: string;
|
|
error: string;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new FailedItem(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.page = source["page"];
|
|
this.file = source["file"];
|
|
this.error = source["error"];
|
|
}
|
|
}
|
|
export class Settings {
|
|
downloadRoot: string;
|
|
proxy: string;
|
|
proxyMode: string;
|
|
galleryConcurrency: number;
|
|
pageConcurrency: number;
|
|
requestIntervalMs: number;
|
|
timeout: number;
|
|
xres: string;
|
|
bridgePort: number;
|
|
dirTemplate: string;
|
|
_probed: boolean;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new Settings(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.downloadRoot = source["downloadRoot"];
|
|
this.proxy = source["proxy"];
|
|
this.proxyMode = source["proxyMode"];
|
|
this.galleryConcurrency = source["galleryConcurrency"];
|
|
this.pageConcurrency = source["pageConcurrency"];
|
|
this.requestIntervalMs = source["requestIntervalMs"];
|
|
this.timeout = source["timeout"];
|
|
this.xres = source["xres"];
|
|
this.bridgePort = source["bridgePort"];
|
|
this.dirTemplate = source["dirTemplate"];
|
|
this._probed = source["_probed"];
|
|
}
|
|
}
|
|
export class Task {
|
|
gid: string;
|
|
token: string;
|
|
raw: string;
|
|
title: string;
|
|
status: string;
|
|
done: number;
|
|
failed: number;
|
|
totalPages: number;
|
|
error: string;
|
|
dir: string;
|
|
pages: Record<string, string>;
|
|
failedList: FailedItem[];
|
|
selected: boolean;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new Task(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.gid = source["gid"];
|
|
this.token = source["token"];
|
|
this.raw = source["raw"];
|
|
this.title = source["title"];
|
|
this.status = source["status"];
|
|
this.done = source["done"];
|
|
this.failed = source["failed"];
|
|
this.totalPages = source["totalPages"];
|
|
this.error = source["error"];
|
|
this.dir = source["dir"];
|
|
this.pages = source["pages"];
|
|
this.failedList = this.convertValues(source["failedList"], FailedItem);
|
|
this.selected = source["selected"];
|
|
this.createdAt = source["createdAt"];
|
|
this.updatedAt = source["updatedAt"];
|
|
}
|
|
|
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
if (!a) {
|
|
return a;
|
|
}
|
|
if (a.slice && a.map) {
|
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
} else if ("object" === typeof a) {
|
|
if (asMap) {
|
|
for (const key of Object.keys(a)) {
|
|
a[key] = new classs(a[key]);
|
|
}
|
|
return a;
|
|
}
|
|
return new classs(a);
|
|
}
|
|
return a;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export namespace main {
|
|
|
|
export class AddTasksResult {
|
|
added: number;
|
|
skipped: number;
|
|
warning: string;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new AddTasksResult(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.added = source["added"];
|
|
this.skipped = source["skipped"];
|
|
this.warning = source["warning"];
|
|
}
|
|
}
|
|
export class DirPreviewResult {
|
|
ok: boolean;
|
|
path: string;
|
|
error: string;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new DirPreviewResult(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.ok = source["ok"];
|
|
this.path = source["path"];
|
|
this.error = source["error"];
|
|
}
|
|
}
|
|
export class EngineInfo {
|
|
running: boolean;
|
|
queued: number;
|
|
galleryConcurrency: number;
|
|
pageConcurrency: number;
|
|
maxConcurrent: number;
|
|
requestIntervalMs: number;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new EngineInfo(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.running = source["running"];
|
|
this.queued = source["queued"];
|
|
this.galleryConcurrency = source["galleryConcurrency"];
|
|
this.pageConcurrency = source["pageConcurrency"];
|
|
this.maxConcurrent = source["maxConcurrent"];
|
|
this.requestIntervalMs = source["requestIntervalMs"];
|
|
}
|
|
}
|
|
export class ScanManifestsResult {
|
|
found: number;
|
|
adopted: number;
|
|
root: string;
|
|
note: string;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new ScanManifestsResult(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.found = source["found"];
|
|
this.adopted = source["adopted"];
|
|
this.root = source["root"];
|
|
this.note = source["note"];
|
|
}
|
|
}
|
|
export class StartResult {
|
|
started: boolean;
|
|
reason: string;
|
|
count: number;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new StartResult(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.started = source["started"];
|
|
this.reason = source["reason"];
|
|
this.count = source["count"];
|
|
}
|
|
}
|
|
export class StatusResult {
|
|
running: boolean;
|
|
queued: number;
|
|
|
|
static createFrom(source: any = {}) {
|
|
return new StatusResult(source);
|
|
}
|
|
|
|
constructor(source: any = {}) {
|
|
if ('string' === typeof source) source = JSON.parse(source);
|
|
this.running = source["running"];
|
|
this.queued = source["queued"];
|
|
}
|
|
}
|
|
|
|
}
|
|
|