import { z } from "zod"; import { DateOnly, EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common"; // 内容域:本地作品、内容单元、别名(REQ §3.1、§5.1、§5.4、§7.5) // 本地 UUID 是主身份;季/集编号、外部 ID 都可修订,不是唯一身份。 export const MediaKind = z.enum(["movie", "series", "variety", "collection", "other"]); export type MediaKind = z.infer; export const MediaEntityStatus = z.enum(["provisional", "active", "merged", "archived"]); export type MediaEntityStatus = z.infer; export const MediaEntity = EntityBase.extend({ kind: MediaKind, title: z.string().min(1), originalTitle: z.string().nullable(), year: z.number().int().min(1870).max(2200).nullable(), status: MediaEntityStatus, /** status=merged 时指向合并后的实体(旧 ID 跳转) */ mergedIntoId: Id.nullable(), /** 最终展示字段(每个字段带来源/语言/时间) */ metadata: JsonObject, /** 人工锁定的字段名;刷新不得覆盖 */ fieldLocks: z.array(z.string()), lastMetadataRefreshAt: Timestamp.nullable(), }); export type MediaEntity = z.infer; export const MediaEntityCreate = MediaEntity.pick({ kind: true, title: true, originalTitle: true, year: true, status: true, metadata: true, fieldLocks: true, }).partial({ originalTitle: true, year: true, status: true, metadata: true, fieldLocks: true }); export type MediaEntityCreate = z.infer; /** 合并通过专门动作完成,Update 不接受 status=merged */ export const MediaEntityUpdate = MediaEntityCreate.partial().extend({ status: MediaEntityStatus.exclude(["merged"]).optional(), }); export type MediaEntityUpdate = z.infer; export const ContentUnitKind = z.enum(["season", "episode", "special", "part"]); export type ContentUnitKind = z.infer; /** 综艺内容类型(REQ §5.1) */ export const ContentType = z.enum([ "main", "extra", "behind_the_scenes", "performance_only", "preview", "reaction", "unknown", ]); export type ContentType = z.infer; export const Segment = z.enum(["full", "upper", "middle", "lower", "unknown"]); export type Segment = z.infer; export const ContentUnitStatus = z.enum(["provisional", "confirmed", "retired"]); export type ContentUnitStatus = z.infer; export const ContentUnit = EntityBase.extend({ mediaEntityId: Id, parentUnitId: Id.nullable(), kind: ContentUnitKind, contentType: ContentType, segment: Segment, seasonNumber: z.number().int().nonnegative().nullable(), episodeNumber: z.number().int().nonnegative().nullable(), /** 来源期号原文,如 "第十期"、"20260927" */ sourceIssue: z.string().nullable(), airDate: DateOnly.nullable(), displayOrder: z.number().nullable(), title: z.string().nullable(), status: ContentUnitStatus, /** 人工确认的编号锁定;刷新不得改编号 */ numberingLocked: z.boolean(), metadata: JsonObject, fieldLocks: z.array(z.string()), }); export type ContentUnit = z.infer; export const ContentUnitCreate = ContentUnit.omit(SERVER_MANAGED).partial({ parentUnitId: true, contentType: true, segment: true, seasonNumber: true, episodeNumber: true, sourceIssue: true, airDate: true, displayOrder: true, title: true, status: true, numberingLocked: true, metadata: true, fieldLocks: true, }); export type ContentUnitCreate = z.infer; export const ContentUnitUpdate = ContentUnitCreate.omit({ mediaEntityId: true }).partial(); export type ContentUnitUpdate = z.infer; export const AliasKind = z.enum(["original", "translated", "short", "site_title", "romanized", "other"]); export type AliasKind = z.infer; export const AliasSource = z.enum(["tmdb", "user", "site", "agent", "nfo", "import"]); export type AliasSource = z.infer; export const TitleAlias = EntityBase.extend({ mediaEntityId: Id, contentUnitId: Id.nullable(), alias: z.string().min(1), /** 归一化后的检索键(小写、去标点、全半角统一),建 trgm 索引 */ normalized: z.string().min(1), language: z.string().nullable(), kind: AliasKind, source: AliasSource, locked: z.boolean(), }); export type TitleAlias = z.infer; /** normalized 由服务端计算,不接受客户端传入 */ export const TitleAliasCreate = TitleAlias.pick({ mediaEntityId: true, contentUnitId: true, alias: true, language: true, kind: true, source: true, locked: true, }).partial({ contentUnitId: true, language: true, kind: true, locked: true }); export type TitleAliasCreate = z.infer; export const TitleAliasUpdate = TitleAliasCreate.omit({ mediaEntityId: true }).partial(); export type TitleAliasUpdate = z.infer;