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(); } }); });