- GET /download-save-paths:按下载器列出默认保存目录与路径映射的下载器侧目录,并给出「自动」会用的下载器(站点指定 > 默认 > 第一个)
- POST /acquisition-plans 新增 identity(本地作品或 TMDB 外部身份):转调 /source-releases/{id}/identity 建 / 绑作品并加人工锁,再按该作品下载
- 测试 harness 支持装配 agent 模块;pg 测试覆盖保存目录选项与指定身份下载
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createHarness, pgAvailable, seedBase, seedEntity, seedRelease } from "./harness.ts";
|
|
|
|
// 手动下载对话框:保存目录选项(默认目录 + 路径映射、「自动」按站点 / 默认下载器解析)与下载前指定作品。
|
|
|
|
const hasPg = await pgAvailable();
|
|
describe.skipIf(!hasPg)("手动下载选项", () => {
|
|
it("保存目录选项:默认目录在前、映射目录去重、自动按站点指定的下载器", async () => {
|
|
const h = await createHarness("manual_save_paths");
|
|
try {
|
|
const base = await seedBase(h.db, { libraryPath: "/lib", cachePath: "/downloads", qbUrl: "http://qb.invalid" });
|
|
const other = await h.db
|
|
.insertInto("downloader_instance")
|
|
.values({
|
|
name: "zz-mapped",
|
|
base_url: "http://qb2.invalid",
|
|
username: "admin",
|
|
is_default: true,
|
|
settings: JSON.stringify({ savePath: "/data/downloads" }),
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
await h.db
|
|
.insertInto("path_mapping")
|
|
.values([
|
|
{
|
|
downloader_instance_id: other.id,
|
|
view_kind: "downloader",
|
|
remote_prefix: "/data/downloads",
|
|
local_prefix: "/mnt/dl",
|
|
},
|
|
{
|
|
downloader_instance_id: other.id,
|
|
view_kind: "downloader",
|
|
remote_prefix: "/data/tv",
|
|
local_prefix: "/mnt/tv",
|
|
},
|
|
])
|
|
.execute();
|
|
// 没有保存目录的下载器不列出
|
|
await h.db
|
|
.insertInto("downloader_instance")
|
|
.values({ name: "no-path", base_url: "http://qb3.invalid", username: "admin" })
|
|
.execute();
|
|
|
|
const all = await h.api("GET", "/download-save-paths");
|
|
expect(all.status, JSON.stringify(all.body)).toBe(200);
|
|
expect(all.body.data.autoInstanceId).toBe(other.id);
|
|
expect(all.body.data.instances).toEqual([
|
|
{
|
|
instanceId: base.instanceId,
|
|
name: expect.any(String),
|
|
isDefault: false,
|
|
dirs: [{ path: "/downloads", hostPath: "/downloads", isDefault: true }],
|
|
},
|
|
{
|
|
instanceId: other.id,
|
|
name: "zz-mapped",
|
|
isDefault: true,
|
|
dirs: [
|
|
{ path: "/data/downloads", hostPath: "/mnt/dl", isDefault: true },
|
|
{ path: "/data/tv", hostPath: "/mnt/tv", isDefault: false },
|
|
],
|
|
},
|
|
]);
|
|
|
|
await h.db
|
|
.updateTable("site")
|
|
.set({ downloader_instance_id: base.instanceId })
|
|
.where("id", "=", base.siteId)
|
|
.execute();
|
|
const bySite = await h.api("GET", `/download-save-paths?siteId=${base.siteId}`);
|
|
expect(bySite.body.data.autoInstanceId).toBe(base.instanceId);
|
|
} finally {
|
|
await h.close();
|
|
}
|
|
});
|
|
|
|
it("下载前指定作品:按 TMDB 身份绑到已登记作品并加人工锁;本地作品同样绑定", async () => {
|
|
const h = await createHarness("manual_identity", {}, { withAgent: true });
|
|
try {
|
|
const base = await seedBase(h.db, { libraryPath: "/lib", cachePath: "/downloads", qbUrl: "http://qb.invalid" });
|
|
const entityId = await seedEntity(h.db, { kind: "movie", title: "Spider-Man", year: 2026 });
|
|
await h.db
|
|
.insertInto("external_identity")
|
|
.values({
|
|
media_entity_id: entityId,
|
|
provider: "tmdb",
|
|
entity_type: "movie",
|
|
external_id: "969681",
|
|
status: "accepted",
|
|
})
|
|
.execute();
|
|
const relId = await seedRelease(h.db, {
|
|
siteId: base.siteId,
|
|
entityId,
|
|
title: "Spider-Man.Brand.New.Day.2026.1080p.WEB-DL.mkv",
|
|
size: 300_000,
|
|
hash: "a".repeat(40),
|
|
});
|
|
await h.db
|
|
.updateTable("source_release")
|
|
.set({ media_entity_id: null, resolution_status: "unresolved" })
|
|
.where("id", "=", relId)
|
|
.execute();
|
|
|
|
const r = await h.api("POST", "/acquisition-plans", {
|
|
sourceReleaseId: relId,
|
|
identity: { external: { provider: "tmdb", entityType: "movie", externalId: "969681" } },
|
|
});
|
|
expect(r.status, JSON.stringify(r.body)).toBe(200);
|
|
expect(r.body.data).toMatchObject({ submitted: true, rejected: [] });
|
|
expect(r.body.data.plan.evaluation.mediaEntityId).toBe(entityId);
|
|
const rel = await h.db
|
|
.selectFrom("source_release")
|
|
.select(["media_entity_id", "resolution_locked"])
|
|
.where("id", "=", relId)
|
|
.executeTakeFirstOrThrow();
|
|
expect(rel).toEqual({ media_entity_id: entityId, resolution_locked: true });
|
|
|
|
// 本地作品:同一端点绑定,旧的 entityId 被指定的作品取代
|
|
const other = await seedEntity(h.db, { kind: "movie", title: "Other Movie", year: 2025 });
|
|
const rel2 = await seedRelease(h.db, {
|
|
siteId: base.siteId,
|
|
entityId,
|
|
title: "Other.Movie.2025.2160p.mkv",
|
|
size: 300_000,
|
|
hash: "b".repeat(40),
|
|
});
|
|
const r2 = await h.api("POST", "/acquisition-plans", {
|
|
sourceReleaseId: rel2,
|
|
entityId,
|
|
identity: { mediaEntityId: other },
|
|
});
|
|
expect(r2.status, JSON.stringify(r2.body)).toBe(200);
|
|
expect(r2.body.data.plan.evaluation.mediaEntityId).toBe(other);
|
|
expect(
|
|
(
|
|
await h.db
|
|
.selectFrom("source_release")
|
|
.select("media_entity_id")
|
|
.where("id", "=", rel2)
|
|
.executeTakeFirstOrThrow()
|
|
).media_entity_id,
|
|
).toBe(other);
|
|
|
|
// 本地作品与外部身份只能给一个,入参校验直接拒绝
|
|
const bad = await h.api("POST", "/acquisition-plans", {
|
|
sourceReleaseId: rel2,
|
|
identity: { mediaEntityId: other, external: { provider: "tmdb", entityType: "movie", externalId: "1" } },
|
|
});
|
|
expect(bad.status).toBe(422);
|
|
} finally {
|
|
await h.close();
|
|
}
|
|
});
|
|
});
|