feat(downloads): live speeds and active/completed split on the download panel (DL-2)
- GET /download-tasks/board?state=active|completed: per-torrent dl/up speed, ETA, seeds/leechs, downloader total speed (transfer/info), counts for both groups, latest organize result (status, reason code, open decision) - live qB data cached per instance for 2 s with in-flight coalescing - panel: total speed line, 进行中 / 已完成 segments remembered locally, 4 s polling while the page is visible, mobile layout, "—" for zero speed Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8086250ec2
commit
3b340745bf
17 files changed
+1113
-93
No files matched your search
@@ -90,6 +90,12 @@ export interface QbTorrent {
|
||||
eta: number | null;
|
||||
ratio: number | null;
|
||||
seedingTimeSec: number | null;
|
||||
/** 已连接的做种 / 下载用户数(num_seeds / num_leechs) */
|
||||
numSeeds: number;
|
||||
numLeechs: number;
|
||||
/** Tracker 报告的做种 / 下载总数(num_complete / num_incomplete;未知为 null) */
|
||||
numComplete: number | null;
|
||||
numIncomplete: number | null;
|
||||
}
|
||||
|
||||
export interface QbFile {
|
||||
@@ -226,6 +232,11 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
function num(v: unknown): number | null {
|
||||
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
||||
}
|
||||
/** qB 用 -1 表示未知(num_complete 等) */
|
||||
function nonNeg(v: unknown): number | null {
|
||||
const n = num(v);
|
||||
return n !== null && n >= 0 ? n : null;
|
||||
}
|
||||
function str(v: unknown): string | null {
|
||||
return typeof v === "string" && v !== "" ? v : null;
|
||||
}
|
||||
@@ -282,6 +293,10 @@ export function normalizeTorrent(instanceId: string, r: Record<string, unknown>)
|
||||
eta: num(r.eta),
|
||||
ratio: num(r.ratio),
|
||||
seedingTimeSec: num(r.seeding_time),
|
||||
numSeeds: num(r.num_seeds) ?? 0,
|
||||
numLeechs: num(r.num_leechs) ?? 0,
|
||||
numComplete: nonNeg(r.num_complete),
|
||||
numIncomplete: nonNeg(r.num_incomplete),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -359,6 +359,22 @@ describe("QbClient 查询与操作", () => {
|
||||
expect((await qb.findByHash("b".repeat(40)))!.state).toBe("missing_files");
|
||||
});
|
||||
|
||||
it("速度、剩余时间、连接数字段(DL-2);-1 的总数记为未知;transfer/info 为全局速度", async () => {
|
||||
const { mock, qb } = await setup();
|
||||
Object.assign(mock.seed({ hash: "c".repeat(40), name: "C", progress: 0.3 }), {
|
||||
dlspeed: 2048,
|
||||
upspeed: 512,
|
||||
eta: 90,
|
||||
num_seeds: 4,
|
||||
num_leechs: 2,
|
||||
num_complete: 30,
|
||||
});
|
||||
const c = (await qb.findByHash("c".repeat(40)))!;
|
||||
expect(c).toMatchObject({ dlspeed: 2048, upspeed: 512, eta: 90, numSeeds: 4, numLeechs: 2, numComplete: 30 });
|
||||
expect(c.numIncomplete).toBeNull();
|
||||
expect(await qb.transferInfo()).toMatchObject({ dl_info_speed: 2048, up_info_speed: 512 });
|
||||
});
|
||||
|
||||
it("文件列表、标签增删、删除必须显式 deleteFiles", async () => {
|
||||
const { mock, qb } = await setup();
|
||||
const t = makeTorrent({
|
||||
|
||||
Reference in new issue
Block a user