import { z } from "zod"; import * as assets from "../assets.ts"; import { Id, IdParam } from "../common.ts"; import * as content from "../content.ts"; import * as identity from "../identity.ts"; import { crud, del, ep, FieldOverrideResult, FieldParam, IdentityRevisionList, R, TaskAccepted, UnitFieldOverrideResult, UnitFieldParam, } from "./shared.ts"; export const contentApi = { // ── 内容 / 身份 ── mediaEntities: { ...crud( "/media-entities", "作品", { row: content.MediaEntity, create: content.MediaEntityCreate, update: content.MediaEntityUpdate, filters: { kind: content.MediaKind.optional(), status: content.MediaEntityStatus.optional(), q: z.string().max(200).optional(), }, }, R, ), merge: ep({ method: "post", path: "/media-entities/{id}/merge", scope: "admin", summary: "合并重复作品(保留旧 ID 跳转)", params: IdParam, body: z.object({ intoId: Id, reason: z.string().max(2000) }), response: content.MediaEntity, }), detail: ep({ method: "get", path: "/media-entities/{id}/detail", scope: "read", summary: "作品详情:外部身份、别名、内容单元、字段来源与覆盖(旧 ID 自动跳转)", params: IdParam, response: content.MediaEntityDetail, }), setIdentity: ep({ method: "post", path: "/media-entities/{id}/identities", scope: "organize.execute", summary: "人工纠错外部映射:旧映射退役(留修订),新映射 accepted + 人工锁", params: IdParam, body: identity.EntityIdentitySet, response: z.object({ mediaEntityId: Id, changed: z.boolean(), identityId: Id.optional(), retired: z.array(Id).optional(), }), }), refresh: ep({ method: "post", path: "/media-entities/{id}/refresh", scope: "organize.execute", summary: "刷新元数据(字段级;用户锁优先,不动路径)。标题 / 简介按设置 metadata.languages 回退;full=1 拉取全部季的每集信息", params: IdParam, query: z.object({ full: z.enum(["0", "1", "true", "false"]).optional() }), response: TaskAccepted, }), setField: ep({ method: "put", path: "/media-entities/{id}/fields/{field}", scope: "organize.execute", summary: "覆盖作品字段并锁定(自动刷新不再改写)", params: FieldParam, body: content.FieldOverrideInput, response: FieldOverrideResult, }), clearField: ep({ method: "delete", path: "/media-entities/{id}/fields/{field}", scope: "organize.execute", summary: "取消字段覆盖与锁定", params: FieldParam, response: FieldOverrideResult, }), }, contentUnits: { ...crud( "/content-units", "内容单元", { row: content.ContentUnit, update: content.ContentUnitUpdate, filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() }, }, R, ), setField: ep({ method: "put", path: "/content-units/{id}/fields/{field}", scope: "organize.execute", summary: "覆盖季 / 集字段并锁定(元数据刷新不再改写)", params: UnitFieldParam, body: content.FieldOverrideInput, response: UnitFieldOverrideResult, }), clearField: ep({ method: "delete", path: "/content-units/{id}/fields/{field}", scope: "organize.execute", summary: "取消季 / 集字段覆盖与锁定", params: UnitFieldParam, response: UnitFieldOverrideResult, }), }, titleAliases: { ...crud( "/title-aliases", "别名", { row: content.TitleAlias, create: content.TitleAliasCreate, filters: { mediaEntityId: Id.optional(), q: z.string().max(200).optional() }, }, R, ), remove: del("/title-aliases/{id}", "删除别名"), }, externalIdentities: { ...crud( "/external-identities", "外部身份", { row: identity.ExternalIdentity, create: identity.ExternalIdentityCreate, update: identity.ExternalIdentityUpdate, filters: { mediaEntityId: Id.optional(), provider: z.string().optional(), status: identity.IdentityStatus.optional(), }, }, R, ), revisions: ep({ method: "get", path: "/external-identities/{id}/revisions", scope: "read", summary: "身份修订历史", params: IdParam, response: IdentityRevisionList, }), }, // ── 文件(只读;写入只经整理/对账流程,不开放直接改) ── mediaAssets: crud( "/media-assets", "资产", { row: assets.MediaAsset, filters: { mediaEntityId: Id.optional(), status: assets.AssetStatus.optional() }, }, R, ), assetUnitCoverage: crud( "/asset-unit-coverage", "资产覆盖", { row: assets.AssetUnitCoverage, filters: { mediaAssetId: Id.optional(), contentUnitId: Id.optional() }, }, R, ), assetLocations: crud( "/asset-locations", "资产位置", { row: assets.AssetLocation, filters: { mediaAssetId: Id.optional(), status: assets.LocationStatus.optional() }, }, R, ), pathBindings: crud( "/path-bindings", "路径绑定", { row: assets.PathBinding, filters: { mediaEntityId: Id.optional(), status: assets.BindingStatus.optional() }, }, R, ), } as const;