Merge branch 'md1'
# Conflicts: # packages/contracts/src/settings.ts
This commit is contained in:
commit
b633eb5379
30 files changed
+3240
-222
No files matched your search
@@ -149,6 +149,12 @@ const FieldOverrideResult = z.object({
|
||||
lockedFields: z.array(z.string()),
|
||||
});
|
||||
const FieldParam = z.object({ id: Id, field: content.EntityMetadataField });
|
||||
const UnitFieldParam = z.object({ id: Id, field: content.UnitMetadataField });
|
||||
const UnitFieldOverrideResult = z.object({
|
||||
contentUnitId: Id,
|
||||
changedFields: z.array(z.string()),
|
||||
lockedFields: z.array(z.string()),
|
||||
});
|
||||
|
||||
/** 引擎 run id 不是 uuid(openworkflow 自有 id) */
|
||||
const EngineRunIdParam = z.object({ id: z.string().min(1).max(200) });
|
||||
@@ -219,8 +225,10 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/media-entities/{id}/refresh",
|
||||
scope: "organize.execute",
|
||||
summary: "刷新元数据(字段级;用户锁优先,不动路径)",
|
||||
summary:
|
||||
"刷新元数据(字段级;用户锁优先,不动路径)。标题 / 简介按设置 metadata.languages 回退;full=1 拉取全部季的每集信息",
|
||||
params: IdParam,
|
||||
query: z.object({ full: z.enum(["0", "1", "true", "false"]).optional() }),
|
||||
response: TaskAccepted,
|
||||
}),
|
||||
setField: ep({
|
||||
@@ -241,16 +249,35 @@ export const Api = {
|
||||
response: FieldOverrideResult,
|
||||
}),
|
||||
},
|
||||
contentUnits: crud(
|
||||
"/content-units",
|
||||
"内容单元",
|
||||
{
|
||||
row: content.ContentUnit,
|
||||
update: content.ContentUnitUpdate,
|
||||
filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() },
|
||||
},
|
||||
R,
|
||||
),
|
||||
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",
|
||||
@@ -2239,6 +2266,14 @@ export const Api = {
|
||||
query: search.TmdbMediaQuery,
|
||||
response: search.TmdbMediaDetail,
|
||||
}),
|
||||
tmdbSeason: ep({
|
||||
method: "get",
|
||||
path: "/media/tmdb/tv/{id}/season/{season}",
|
||||
scope: "read",
|
||||
summary: "仅 TMDB 剧集的季详情与每集信息(标题 / 简介按语言回退链;走 TMDB 请求缓存)",
|
||||
params: search.TmdbSeasonParams,
|
||||
response: search.TmdbSeasonDetailView,
|
||||
}),
|
||||
},
|
||||
|
||||
// ── 浏览器(C1-E):站点浏览器会话、原站视图、人工接管;写动作需人工持有控制权(接管) ──
|
||||
|
||||
@@ -198,9 +198,56 @@ export const EntityMetadataField = z.enum([
|
||||
"episodeCount",
|
||||
"genres",
|
||||
"posterUrl",
|
||||
// MD-1 TMDB 详情(对照 MP 媒体详情):中文名、标语、最近播出、状态、评分、时长、季数、主创、外部 ID
|
||||
"titleZh",
|
||||
"tagline",
|
||||
"lastAirDate",
|
||||
"airStatus",
|
||||
"voteAverage",
|
||||
"voteCount",
|
||||
"runtime",
|
||||
"seasonCount",
|
||||
"cast",
|
||||
"crew",
|
||||
"externalIds",
|
||||
]);
|
||||
export type EntityMetadataField = z.infer<typeof EntityMetadataField>;
|
||||
|
||||
/**
|
||||
* 内容单元(季 / 集)可覆盖 / 锁定的字段(MD-1):来源值写 metadata_field(content_unit_id),
|
||||
* 最终值投影到 content_unit.metadata;name → title 列、airDate → air_date 列。编号不属于元数据。
|
||||
*/
|
||||
export const UnitMetadataField = z.enum([
|
||||
"name",
|
||||
"overview",
|
||||
"airDate",
|
||||
"runtime",
|
||||
"stillPath",
|
||||
"posterPath",
|
||||
"voteAverage",
|
||||
"voteCount",
|
||||
]);
|
||||
export type UnitMetadataField = z.infer<typeof UnitMetadataField>;
|
||||
|
||||
/** 主创(cast / crew 字段的元素):TMDB 人物 ID、姓名(按回退链语言)、角色或职务、头像路径(经图片代理显示) */
|
||||
export const MediaCredit = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
originalName: z.string().nullable(),
|
||||
/** 演员:角色名;幕后:职务(Director / Creator …) */
|
||||
role: z.string().nullable(),
|
||||
profilePath: z.string().nullable(),
|
||||
});
|
||||
export type MediaCredit = z.infer<typeof MediaCredit>;
|
||||
|
||||
/** externalIds 字段的值 */
|
||||
export const MediaExternalIds = z.object({
|
||||
imdb: z.string().nullable(),
|
||||
tvdb: z.string().nullable(),
|
||||
wikidata: z.string().nullable(),
|
||||
});
|
||||
export type MediaExternalIds = z.infer<typeof MediaExternalIds>;
|
||||
|
||||
export const FieldOverrideInput = z.object({
|
||||
value: z.unknown(),
|
||||
lock: z.boolean().default(true),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { Id, NonNegInt, Timestamp } from "./common.ts";
|
||||
import { MediaEntityStatus, MediaKind } from "./content.ts";
|
||||
import { MediaCredit, MediaEntityStatus, MediaKind } from "./content.ts";
|
||||
import { ManualAcquisitionRequest } from "./execution.ts";
|
||||
import { InventoryEntry, MediaServerInventory, SlotStatusCounts } from "./organization.ts";
|
||||
|
||||
@@ -338,6 +338,8 @@ export const TmdbMediaSeason = z.object({
|
||||
airDate: z.string().nullable(),
|
||||
/** 本地已入库的集数(整季资产按全季计);未收录为 null */
|
||||
localHave: NonNegInt.nullable(),
|
||||
overview: z.string().nullable().optional(),
|
||||
posterPath: z.string().nullable().optional(),
|
||||
});
|
||||
export type TmdbMediaSeason = z.infer<typeof TmdbMediaSeason>;
|
||||
|
||||
@@ -367,5 +369,55 @@ export const TmdbMediaDetail = z.object({
|
||||
mediaServers: MediaServerInventory.nullable(),
|
||||
/** 搜资源时服务端会用的查询词 */
|
||||
queries: z.array(z.string()),
|
||||
// MD-1:按语言回退链取的详情字段(媒体详情页与媒体库详情共用展示)
|
||||
/** 中文名:回退链中第一个中文值(zh-CN 优先);没有为 null */
|
||||
titleZh: z.string().nullable().optional(),
|
||||
tagline: z.string().nullable().optional(),
|
||||
genres: z.array(z.string()).optional(),
|
||||
voteAverage: z.number().nullable().optional(),
|
||||
voteCount: z.number().int().nullable().optional(),
|
||||
/** TMDB status 原文(Returning Series / Ended / Released …) */
|
||||
airStatus: z.string().nullable().optional(),
|
||||
/** 分钟:电影片长;剧集为单集时长 */
|
||||
runtime: z.number().int().nullable().optional(),
|
||||
episodeCount: z.number().int().nullable().optional(),
|
||||
lastAirDate: z.string().nullable().optional(),
|
||||
platform: z.string().nullable().optional(),
|
||||
cast: z.array(MediaCredit).optional(),
|
||||
crew: z.array(MediaCredit).optional(),
|
||||
});
|
||||
export type TmdbMediaDetail = z.infer<typeof TmdbMediaDetail>;
|
||||
|
||||
/** 一集的 TMDB 信息(标题 / 简介按语言回退链,language 记实际取到的语言) */
|
||||
export const MediaEpisodeInfo = z.object({
|
||||
seasonNumber: z.number().int(),
|
||||
episodeNumber: z.number().int(),
|
||||
name: z.string().nullable(),
|
||||
nameLanguage: z.string().nullable(),
|
||||
overview: z.string().nullable(),
|
||||
overviewLanguage: z.string().nullable(),
|
||||
airDate: z.string().nullable(),
|
||||
runtime: z.number().int().nullable(),
|
||||
stillPath: z.string().nullable(),
|
||||
voteAverage: z.number().nullable(),
|
||||
voteCount: z.number().int().nullable(),
|
||||
});
|
||||
export type MediaEpisodeInfo = z.infer<typeof MediaEpisodeInfo>;
|
||||
|
||||
export const TmdbSeasonParams = z.object({
|
||||
id: z.string().regex(/^\d{1,12}$/),
|
||||
season: z.coerce.number().int().min(0).max(999),
|
||||
});
|
||||
export type TmdbSeasonParams = z.infer<typeof TmdbSeasonParams>;
|
||||
|
||||
/** GET /media/tmdb/tv/{id}/season/{season}:季详情与集列表(仅 TMDB 作品详情页按季展开时拉取) */
|
||||
export const TmdbSeasonDetailView = z.object({
|
||||
tvId: z.string(),
|
||||
seasonNumber: z.number().int(),
|
||||
name: z.string().nullable(),
|
||||
overview: z.string().nullable(),
|
||||
airDate: z.string().nullable(),
|
||||
posterPath: z.string().nullable(),
|
||||
episodes: z.array(MediaEpisodeInfo),
|
||||
});
|
||||
export type TmdbSeasonDetailView = z.infer<typeof TmdbSeasonDetailView>;
|
||||
@@ -217,6 +217,15 @@ export const MetadataSettings = z.object({
|
||||
tmdbApiKeySecretRef: Id.nullable(),
|
||||
/** 自建反代或测试 mock;null 用官方地址 */
|
||||
tmdbBaseUrl: z.url().nullable(),
|
||||
/**
|
||||
* 元数据语言回退链(MD-1):作品 / 季 / 集的标题与简介按顺序取第一个非空值;中文名(titleZh)取链中第一个中文值。
|
||||
* BCP47 语言-地区,如 zh-CN;缺省 zh-CN → zh-SG → zh-TW → zh-HK → en-US
|
||||
*/
|
||||
languages: z
|
||||
.array(z.string().regex(/^[a-z]{2,3}(-[A-Z]{2})?$/))
|
||||
.min(1)
|
||||
.max(8)
|
||||
.default(["zh-CN", "zh-SG", "zh-TW", "zh-HK", "en-US"]),
|
||||
});
|
||||
export type MetadataSettings = z.infer<typeof MetadataSettings>;
|
||||
|
||||
@@ -602,7 +611,7 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
sessionDir: null,
|
||||
webSearch: DEFAULT_WEB_SEARCH,
|
||||
},
|
||||
metadata: { tmdbApiKeySecretRef: null, tmdbBaseUrl: null },
|
||||
metadata: { tmdbApiKeySecretRef: null, tmdbBaseUrl: null, languages: ["zh-CN", "zh-SG", "zh-TW", "zh-HK", "en-US"] },
|
||||
sites: {
|
||||
defaultRateLimit: { requestsPerMinute: 20, concurrency: 2 },
|
||||
defaultSnapshotIntervalSec: 3600,
|
||||
@@ -896,6 +905,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
fields: [
|
||||
"metadata.tmdbApiKeySecretRef",
|
||||
"metadata.tmdbBaseUrl",
|
||||
"metadata.languages",
|
||||
"cache.tmdbMaxSize",
|
||||
"identity.eagerTmdbLookup",
|
||||
"identity.autoEnrich",
|
||||
|
||||
Reference in new issue
Block a user