merge: ui/j-download-dialog
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
commit
1360120e75
14 files changed
+1272
-123
No files matched your search
@@ -14,6 +14,7 @@ import { createLogger } from "../../src/logger.ts";
|
||||
import { type AoOverrides, createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
|
||||
import type { AoDeps } from "../../src/modules/acquisition-organize/services/common.ts";
|
||||
import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts";
|
||||
import { createAgentModule } from "../../src/modules/agent/index.ts";
|
||||
import { createSourcesIdentityModule } from "../../src/modules/sources-identity/index.ts";
|
||||
import type { ServerContext } from "../../src/modules/types.ts";
|
||||
import { fakeEngine } from "../support/engine.ts";
|
||||
@@ -90,8 +91,8 @@ export interface Harness {
|
||||
export async function createHarness(
|
||||
name: string,
|
||||
overrides: AoOverrides = {},
|
||||
/** withSources:同时装配 sources-identity 模块(待处理中心路由等) */
|
||||
opts: { withSources?: boolean } = {},
|
||||
/** withSources:同时装配 sources-identity 模块(待处理中心路由等);withAgent:再装配 agent 模块(POST /acquisition-plans 等) */
|
||||
opts: { withSources?: boolean; withAgent?: boolean } = {},
|
||||
): Promise<Harness> {
|
||||
const ts = await createTestSchema(uniqueSchema(`ao_test_${name}`));
|
||||
const { db } = ts;
|
||||
@@ -161,7 +162,11 @@ export async function createHarness(
|
||||
role: "all",
|
||||
version: "test",
|
||||
db,
|
||||
modules: opts.withSources ? [module, createSourcesIdentityModule()] : [module],
|
||||
modules: [
|
||||
module,
|
||||
...(opts.withSources || opts.withAgent ? [createSourcesIdentityModule()] : []),
|
||||
...(opts.withAgent ? [createAgentModule()] : []),
|
||||
],
|
||||
ctx,
|
||||
apiKeyStore: {
|
||||
async findByHash(hash) {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user