feat(organize): 整理时指定季集结构(临时剧集组 / 同时设为作品结构)

- 契约:FilesOrganizeRequest.episodeStructures(按作品 groupId / applyToEntity),组响应带 episodeStructure
- 集数映射:createStructureEpisodeMapper 按指定结构的季表(剧集组带原季集对照)换算;作品规则先按作品结构命中,再经 TMDB 原季集换到指定结构
- 资产覆盖:指定结构季集 → TMDB 原季集 → 作品当前结构的单元,换不回不登记集覆盖;冻结计划记 episodeStructure 与 facts.structureEpisodes
- 向导执行勾选「同时设为作品的季集结构」时先走 setEpisodeGroup 切换作品再整理
- 测试:organize-episode-structure(SPLIT_COUR_FIXTURE)

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 16:28:34 +08:00
1 parent 731336ccb1
commit d55fa4b240
10 files changed
+637 -11

No files matched your search

+23
View File
@@ -381,6 +381,18 @@ export const FilesOrganizeItem = FileRef.extend({
});
export type FilesOrganizeItem = z.infer<typeof FilesOrganizeItem>;
/**
* 整理时指定季集结构(对照 MP TransferInfo.episode_group):这次整理按 groupId 的季集换算与命名,不改作品设置;
* groupId null = TMDB 默认季。applyToEntity = 同时设为作品的季集结构(执行时先切换作品,再按新结构整理;预览不切换)。
* 与作品当前结构相同等同跟随作品。只对本地剧集作品生效
*/
export const FilesOrganizeEpisodeStructure = z.object({
mediaEntityId: Id,
groupId: z.string().trim().min(1).max(64).nullable(),
applyToEntity: z.boolean().optional(),
});
export type FilesOrganizeEpisodeStructure = z.infer<typeof FilesOrganizeEpisodeStructure>;
export const FilesOrganizeRequest = z.object({
sourceConfigId: Id.optional(),
idempotencyKey: IdempotencyKey,
@@ -402,6 +414,8 @@ export const FilesOrganizeRequest = z.object({
skipSuccessfulRecords: z.boolean().optional(),
/** 本次入库后写 NFO/图片;缺省沿用已有设置,Local 保留自动补全行为。 */
scrapeAfterImport: z.boolean().optional(),
/** 整理时指定季集结构(按作品,缺省跟随作品),见 FilesOrganizeEpisodeStructure */
episodeStructures: z.array(FilesOrganizeEpisodeStructure).max(50).optional(),
});
export type FilesOrganizeRequest = z.infer<typeof FilesOrganizeRequest>;
@@ -463,6 +477,15 @@ export const FilesOrganizeGroup = z.object({
targetSegments: z
.array(z.object({ text: z.string(), role: z.enum(["root", "season", "dir"]), bound: z.boolean() }))
.optional(),
/** 本组按哪套季集结构整理(请求指定了季集结构时才有);temporary = 与作品结构不同、只这次使用 */
episodeStructure: z
.object({
groupId: z.string().nullable(),
name: z.string(),
temporary: z.boolean(),
appliedToEntity: z.boolean().optional(),
})
.optional(),
/** W9-H(G5):执行后排的元数据刷新 run id(refreshMetadata=true 且该组已提交) */
refreshTaskId: z.string().nullable().optional(),
});