feat(organize): 整理记录列表带出二级分类

列表与详情的 media.category 取自冻结计划里的 classification.category,供整理记录行显示分类(对照 MP 历史记录页);不需要迁移。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 11:59:15 +08:00
1 parent fb6c76875c
commit d98899a5f8
3 files changed
+15 -2

No files matched your search

@@ -70,6 +70,10 @@ export async function loadRecordsPublic(exec: Exec, rows: OrganizationRecordRow[
const v = (metadata as { posterPath?: { value?: unknown } } | null)?.posterPath?.value; const v = (metadata as { posterPath?: { value?: unknown } } | null)?.posterPath?.value;
return typeof v === "string" && v ? v : null; return typeof v === "string" && v ? v : null;
}; };
const categoryOf = (plan: unknown) => {
const v = (plan as { classification?: { category?: unknown } } | null)?.classification?.category;
return typeof v === "string" && v ? v : null;
};
// blocked:* 的说明在 error.conflicts[].detail(中文),有就用它,不把内部串直接给人看 // blocked:* 的说明在 error.conflicts[].detail(中文),有就用它,不把内部串直接给人看
const conflictText = (err: unknown) => const conflictText = (err: unknown) =>
((err as { conflicts?: { detail?: string }[] } | null)?.conflicts ?? []) ((err as { conflicts?: { detail?: string }[] } | null)?.conflicts ?? [])
@@ -119,6 +123,7 @@ export async function loadRecordsPublic(exec: Exec, rows: OrganizationRecordRow[
externalId: i.external_id, externalId: i.external_id,
})), })),
posterPath: posterOf(e.metadata), posterPath: posterOf(e.metadata),
category: categoryOf(r.plan),
} }
: null, : null,
trigger: r.trigger, trigger: r.trigger,
@@ -44,6 +44,10 @@ describe.skipIf(skip)("整理记录筛选与统计", () => {
media_entity_id: r.entity, media_entity_id: r.entity,
download_task_id: "task" in r ? r.task : null, download_task_id: "task" in r ? r.task : null,
source_path: r.src, source_path: r.src,
// 冻结计划里的二级分类(列表带出 media.category)
...(r.k === "film"
? { plan: JSON.stringify({ classification: { category: "欧美电影", matchedFields: [], fallback: false } }) }
: {}),
action: r.action, action: r.action,
status: r.status, status: r.status,
...("at" in r ? { created_at: r.at } : {}), ...("at" in r ? { created_at: r.at } : {}),
@@ -135,13 +139,15 @@ describe.skipIf(skip)("整理记录筛选与统计", () => {
} }
}); });
it("列表项带海报、大小、下载器名称与 hash", async () => { it("列表项带海报、分类、大小、下载器名称与 hash", async () => {
const [film] = await list("q=Arrival.2016"); const [film] = await list("q=Arrival.2016");
expect(film).toMatchObject({ expect(film).toMatchObject({
media: { title: "Arrival", posterPath: "/arrival.jpg" }, media: { title: "Arrival", posterPath: "/arrival.jpg", category: "欧美电影" },
sizeBytes: 1000, sizeBytes: 1000,
download: { name: "Arrival.2016", hash: "abcdef0123456789abcdef0123456789abcdef01", downloaderName: "家里的 qB" }, download: { name: "Arrival.2016", hash: "abcdef0123456789abcdef0123456789abcdef01", downloaderName: "家里的 qB" },
}); });
const [show] = await list("q=Severance");
expect(show).toMatchObject({ media: { title: "Severance", category: null } });
const [unknown] = await list("kind=none"); const [unknown] = await list("kind=none");
expect(unknown).toMatchObject({ media: null, download: null, sizeBytes: 1000 }); expect(unknown).toMatchObject({ media: null, download: null, sizeBytes: 1000 });
}); });
+2
View File
@@ -232,6 +232,8 @@ export const PublicOrganizationRecord = z.object({
externalIds: z.array(z.object({ provider: z.string(), entityType: z.string(), externalId: z.string() })), externalIds: z.array(z.object({ provider: z.string(), entityType: z.string(), externalId: z.string() })),
/** HIS-1:作品海报(TMDB poster_path,如 /abc.jpg);没有为 null */ /** HIS-1:作品海报(TMDB poster_path,如 /abc.jpg);没有为 null */
posterPath: z.string().nullable().optional(), posterPath: z.string().nullable().optional(),
/** 整理时按二级分类策略得到的分类(如「纪录片」「欧美电影」,取自冻结计划);没有为 null */
category: z.string().nullable().optional(),
}) })
.nullable(), .nullable(),
trigger: OrganizationTrigger, trigger: OrganizationTrigger,