perf(server): parallel media-server inventory, queued identify remote lookups, dev DB cleanup
- GET /inventory/media-servers: per-instance Promise.allSettled, per-instance
concurrency cap (2) and timeout (8s); failed/timed-out instances are unknown,
response carries partial=true (field not yet in contracts)
- S-7: POST /files/identify returns local matches plus remoteTaskId/remotePending
and enqueues identifyRemote; the worker runs the TMDB lookups and publishes SSE
identify.completed {taskId,status,items,truncated,excludedBySize,error};
?wait=true keeps the synchronous path, capped at 15s overall
- catalog.lookup / release.search honour the caller's abort signal
- scripts/dev-db-clean.ts: dry-run by default, --yes to drop leftover test
schemas and w32-/w33-/fe4-/qa- prefixed entities; refuses non localhost:55433
- runbook: dev DB cleanup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5b8927d1dc
commit
fd2f7bc33d
10 files changed
+683
-67
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { IDENTIFY_COMPLETED_EVENT, runIdentifyRemote } from "../../src/modules/file-manager/identify.ts";
|
||||
import {
|
||||
createFmHarness,
|
||||
EXEC_KEY,
|
||||
@@ -69,16 +70,74 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
|
||||
expect(pool).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("identify:远端候选(catalog.lookup)", async () => {
|
||||
const r = await h.api("POST", "/files/identify", {
|
||||
it("identify:远端候选(catalog.lookup,wait=true 同步模式)", async () => {
|
||||
const r = await h.api("POST", "/files/identify?wait=true", {
|
||||
items: [{ root: h.dlRoot, path: "Remote.Film.2019.1080p.mkv" }],
|
||||
minSizeBytes: 0,
|
||||
});
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body.data.remoteTaskId).toBeNull();
|
||||
const c = r.body.data.items[0].candidates;
|
||||
expect(c[0]).toMatchObject({ provider: "tmdb", externalId: "999", mediaEntityId: null, kind: "movie" });
|
||||
});
|
||||
|
||||
it("identify:缺省入队远端查询,先回本地命中 + remoteTaskId;任务完成发 identify.completed(S-7)", async () => {
|
||||
const before = h.engine.enqueued.length;
|
||||
const body = {
|
||||
items: [
|
||||
{ root: h.dlRoot, path: "Remote.Film.2019.1080p.mkv" },
|
||||
{ root: h.dlRoot, path: "Blue.Film.2021.2160p.BluRay.x265-GRP.mkv" },
|
||||
],
|
||||
minSizeBytes: 0,
|
||||
};
|
||||
const r = await h.api("POST", "/files/identify", body);
|
||||
expect(r.status).toBe(200);
|
||||
const [remote, local] = r.body.data.items;
|
||||
// 本地命中照常返回;远端候选不在请求内查
|
||||
expect(remote.candidates).toEqual([]);
|
||||
expect(remote.warnings.some((w: string) => w.includes("后台"))).toBe(true);
|
||||
expect(local.candidates[0]).toMatchObject({ mediaEntityId: film });
|
||||
expect(r.body.data.remotePending).toBe(2);
|
||||
const job = h.engine.enqueued.slice(before).find((e) => e.task === "identifyRemote");
|
||||
expect(job).toBeTruthy();
|
||||
expect(r.body.data.remoteTaskId).toMatch(/^run-/);
|
||||
|
||||
// 模拟 worker 执行任务
|
||||
const out = await runIdentifyRemote(
|
||||
h.module.deps(),
|
||||
r.body.data.remoteTaskId,
|
||||
job!.input as Parameters<typeof runIdentifyRemote>[2],
|
||||
);
|
||||
expect(out.status).toBe("succeeded");
|
||||
const ev = h.events.filter((e) => e.type === IDENTIFY_COMPLETED_EVENT).at(-1);
|
||||
expect(ev?.payload.taskId).toBe(r.body.data.remoteTaskId);
|
||||
const done = ev!.payload.items.find((i: { path: string }) => i.path === "Remote.Film.2019.1080p.mkv");
|
||||
expect(done.candidates[0]).toMatchObject({ provider: "tmdb", externalId: "999" });
|
||||
expect(done.warnings.some((w: string) => w.includes("后台"))).toBe(false);
|
||||
});
|
||||
|
||||
it("identify:无需远端查询时不入队(remoteTaskId=null)", async () => {
|
||||
const before = h.engine.enqueued.length;
|
||||
await mkdir(join(h.dl, "untitled"), { recursive: true });
|
||||
await writeFile(join(h.dl, "untitled", "1080p.mkv"), "x");
|
||||
const r = await h.api("POST", "/files/identify", { items: [{ root: h.dlRoot, path: "untitled/1080p.mkv" }] });
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body.data.remoteTaskId).toBeNull();
|
||||
expect(h.engine.enqueued.slice(before).some((e) => e.task === "identifyRemote")).toBe(false);
|
||||
});
|
||||
|
||||
it("identify:任务里识别失败也发 identify.completed(status=failed)", async () => {
|
||||
const out = await runIdentifyRemote(h.module.deps(), "run-x", {
|
||||
body: { items: [{ root: h.dlRoot, path: "no-such-file.mkv" }] } as never,
|
||||
actorId: "system",
|
||||
});
|
||||
expect(out.status).toBe("failed");
|
||||
expect(h.events.at(-1)).toMatchObject({
|
||||
type: IDENTIFY_COMPLETED_EVENT,
|
||||
payload: { taskId: "run-x", status: "failed" },
|
||||
});
|
||||
});
|
||||
|
||||
it("identify:去重后的远端查询单次上限 20,超出的项只本地召回并给 warning(验收 07 S-6)", async () => {
|
||||
await mkdir(join(h.dl, "many"), { recursive: true });
|
||||
const names = Array.from({ length: 23 }, (_, i) => `Distinct Title ${String.fromCharCode(65 + i)}.2019.1080p.mkv`);
|
||||
|
||||
Reference in new issue
Block a user