Files
UHD_Media_Tool/docs/runbook/metadata.md
T
2026-10-06 18:27:51 +08:00

94 lines
11 KiB
Markdown

# 元数据运行手册(MD-1:语言回退链 + 每集元数据 + 媒体详情)
## 语言回退链
- 设置 → 元数据 → 语言与季集 →「语言回退链」,即 `settings.metadata.languages`,缺省 `zh-CN, zh-SG, zh-TW, zh-HK, en-US`。
- 作品标题:按链逐个语言查 TMDB `translations`(语言 + 地区精确匹配);该语言译名为空时查同地区、文字相符的 `alternative_titles`(类型为空的优先);原始语言等于链中语言时原名也算。全部落空用请求语言下的 name / 原名。对照 MP `__update_tmdbinfo_cn_title`(中文名 → CN 别名 → SG 译名)。
- 中文名 `titleZh`:只在链里的中文语言中按顺序取(zh-CN 优先),值必须含汉字。链首改成 en-US 时标题变英文,`titleZh` 不变。
- 简介 / 标语(MD-2):只按「简介链」取 `translations` 的第一个非空值,取不到留空(界面显示「暂无简介」)。简介链 = 回退链里第一个中文之前的语言 + 链里全部中文,之后的非中文去掉;缺省链得到 `zh-CN, zh-SG, zh-TW, zh-HK`。链里没有中文时简介链就是原链。设置 → 元数据 → 语言与季集「简介回退到非中文」(`metadata.overviewNonChineseFallback`,缺省关)打开后简介链 = 原链(旧行为)。原因:TMDB 上很多剧集(如《心动的信号》103147)只有英文"简介",而且常是标题英译;MP 每集信息只按配置语言取,简介为空就为空。请求语言下的原值若等于链外某语言的译文(TMDB / 反代把缺失字段回退了),不当作链内的值。标题 / 别名不受影响。
- 季 / 集:先按链首请求 `/tv/{id}/season/{n}`;已播出的集缺标题(TMDB 占位名"第 N 集""Episode N")或缺简介时,按链逐个语言补拉同一端点,补齐即停(简介只从简介链里的语言取;只缺简介且剩下的语言都不在简介链里时不再补拉);链走完仍没有标题的集保留占位名(语言记为空)。未播出的集不触发补拉。
- 每个字段的来源值写 `metadata_field`,`language` 记实际取到的语言;同字段语言变了,旧语言的 tmdb 行删掉。
- `translations` 与 `alternative_titles` 写 `title_alias`(来源 tmdb,标语言;别名只在地区与文字相符时标,如 CN + 汉字 → zh-CN)。已有同文本别名(繁简归一化后同键)不重复登记,只给语言为空的 tmdb 行补语言。
- 多语言保留规则(2026-10-06):TMDB 的译名 / 别名 / 简介 / 标语只保留简中、繁中、英文和作品原语言,语言链按作品收敛
(链里的 ja-JP 只对日语原作生效,fr-FR 这类对原语言不是它的作品跳过)。规则、清理脚本见 `identity.md`「TMDB 多语言保留规则」。
## 刷新
| 触发 | 拉哪些季的每集信息 | 新建集单元 |
|---|---|---|
| `POST /media-entities/{id}/refresh`(定时追踪刷新同) | 追踪中订阅的季 ∪ 库里已有集单元的季(含季 0,最近 8 季) | 只在追踪中的季 |
| `POST /media-entities/{id}/refresh?full=1`(详情页「刷新元数据」「更新每集信息」) | 全部季(最多 60);TMDB 有季 0 且 `metadata.includeSpecials` 开时含季 0 | 是 |
| 新建档:按 TMDB 候选建作品(订阅建档、搜索 / TMDB 预览页建档、整理向导)、手动指定未登记的 TMDB 身份 | 同 `full=1`(幂等键 `refreshMetadata:created:{id}`) | 是 |
- 新建档时标题 / 简介当场按语言链取(富详情失败沿用基础详情),每集信息由随后入队的 full 刷新补齐,不用手动刷新。自动识别(RSS)新建的作品不入队 full,仍按追踪刷新。
- 刷新是后台任务 `refreshMetadata`(输入 `{mediaEntityId, full?}`),结果里 `titles` 是本次取到的标题 / 语言 / 中文名,`episodes[]` 是各季写入统计(`created` 新建单元数、`changedUnits` 有变化的单元数)。
- 增量:来源值和语言都没变的字段不写;只投影有变化的单元。不改编号、路径绑定和资产覆盖。
- 季 0(特别篇,DH-1):设置 → 元数据 → 语言与季集「包含特别篇(季 0)」(`metadata.includeSpecials`,缺省开)。开时 full 刷新和存量回刷
在 TMDB 有季 0 时一并拉取每集信息并建季 0 的季 / 集单元(对照 MP);季 0 不进正片季总集数账本,不影响订阅完成判定。关掉后
已建的季 0 单元保留,只是不再主动拉取。
- 临时转正式(DH-1):刷新时与 TMDB 季集结构对得上的临时(provisional)季 / 集单元转正式(`status = confirmed`、`source = tmdb`),
id 与所有引用不变;对不上的(季号不在 TMDB、集号超出该季集数)保持临时。只看本次拉了季详情的季(full 刷新即全部季)。
`content_unit.source`(迁移 0053):`release` 资源解析懒建、`file` 整理 / 文件懒建、`tmdb` 按 TMDB 建或转正、`user` 人工;旧行为空。
- 重复单元合并(DH-1):每次刷新先把同作品、同类型、同季同集号、同内容类型与分段的多条未退役单元合并为一条(规则同
`scripts/identity-cleanup.ts --merge-duplicates`,见 `identity.md`),结果里 `mergedUnits` / `promotedUnits` 是合并 / 转正数。
懒建单元(`ensureUnits`)按(作品, 类型, 季, 集)取事务级咨询锁,并发识别不再建出重复。
- 存量回刷(FIX-5):维护任务 `metadata.backfill_languages`「元数据语言回刷」在部署后首次启动即运行,对所有已确认 TMDB 身份
(active / provisional)的作品逐个跑 `full` 刷新。用独立的 TMDB 客户端串行请求,网络请求间隔 ≥ 500 ms(每秒最多 2 次),
请求照常走 `tmdb.request` PG 缓存(对照 MP `@cached`),中断后重跑时已取过的不再请求。进度每 5 秒写进本次运行记录的摘要,
结束写「N 部作品:刷新 / 跳过 / 失败;TMDB 请求 X 次」和前 5 条失败原因。跑完(有个别失败也算)任务自动停用;
计划页「立即执行」可再跑一次。TMDB 未配置时记为跳过、保持启用,第二天再试。只读对账模式下跳过。
代码:`apps/server/src/modules/sources-identity/service/backfill-languages.ts`。
- 单部作品补齐中文名 / 每集信息:对作品跑一次 `full=1`。手动批量:
```sh
# 所有有 TMDB 身份的作品各跑一次(TMDB 请求走缓存;数量多时分批)
curl -s -H "Authorization: Bearer $KEY" "$BASE/api/v1/media-entities?limit=200" | jq -r '.data.items[].id' |
while read -r id; do curl -s -X POST -H "Authorization: Bearer $KEY" "$BASE/api/v1/media-entities/$id/refresh?full=1"; done
```
## 存量简介重算(MD-2)
MD-1 的回退链把 en-US 等非中文简介写进过作品 / 季 / 集。新规则上线后,刷新会按新规则清掉;不想等刷新、或想先看范围时用脚本:
```sh
# 看清单(只读):按作品列出每项「旧语言 旧值 → 新语言 新值(依据)」和合计
DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/overview-recompute.ts
DATABASE_URL=… deno run -A scripts/overview-recompute.ts --entity <作品 id> # 只看一部
DATABASE_URL=… deno run -A scripts/overview-recompute.ts --json # 清单 JSON
DATABASE_URL=… deno run -A scripts/overview-recompute.ts --apply # 写库(单个事务 + 审计 metadata.overview_recompute)
```
- 对象:来源 tmdb、`language` 不为空且不在简介链里的 `overview`(作品 / 季单元 / 集单元,未退役,有已接受的 TMDB 身份)。语言链与开关取库里的设置。
- **不请求 TMDB**:用只读 `kv_cache`(region `tmdb.request`)的客户端跑同一套 `fetchRichDetail` / `fetchRichSeason`,读缓存不续期、不改最近使用时间,过期但还没被清理的行也用。依据写在清单里:
- `缓存`:按缓存的响应重算,可能改成某个中文变体的简介,也可能为空;作品的标语一起按缓存重算;
- `无缓存→清空`:缓存已淘汰。当初能落到非中文,说明那时简介链里的语言都没有简介,按新规则直接清空。
- 写库与刷新同一套:写字段来源(语言变了删旧语言行)再投影;用户锁定 / 覆盖的字段最终值不变。重跑幂等(第二次清单为空)。
- 按 lesson 22:对真实库先跑 dry-run,由协调者过目清单再 `--apply`。代码:`apps/server/src/modules/sources-identity/service/overview-recompute.ts`。
## 集字段(字段级,可锁)
- 集单元字段:`name`(投影到 `content_unit.title`)、`overview`、`airDate`(投影到 `air_date`)、`runtime`、`stillPath`、`voteAverage`、`voteCount`;季单元另有 `posterPath`。最终值在 `content_unit.metadata`,每个字段带来源 / 语言 / 时间 / 锁。
- 覆盖并锁定:`PUT /content-units/{id}/fields/{field}` `{value, lock?, reason?}`;取消:`DELETE` 同路径。锁定的字段刷新不改。界面:媒体库详情 → 季与集 → 每集行右侧铅笔。
## TMDB 请求与缓存
- 富详情一次请求:`/{type}/{id}?language=链首&append_to_response=alternative_titles,credits,external_ids,translations`;季详情每语言一次。都走 `tmdb.request` PG 缓存(TTL / 空结果短 TTL / 条数上限见设置 → 缓存 → TMDB 缓存;对照 MP `@cached`:成功才缓存、业务失败不缓存)。
- 想立刻拿到 TMDB 刚改的数据:设置 → 缓存 → TMDB 缓存「清空」,再刷新。
- 仅 TMDB 作品详情页(`/console/media/tmdb/{type}/{id}`)的季在展开时拉 `GET /api/v1/media/tmdb/tv/{id}/season/{n}`,不落库。
## 媒体详情页
- 媒体库详情与仅 TMDB 作品详情共用 `apps/web/app/components/media/media-detail.tsx`:背景图(图片代理 w1280,scrim 遮罩)、海报、标题 / 原名 / 年份 / 类型 / 评分 / 状态 / 季集数、简介、外部链接,主创(导演 / 主创 + 演员前 10,头像 w185),季折叠 + 每集行(剧照 w300、集号与标题、播出日期、时长、评分、简介折叠、库内状态)。
- 库内状态:作品有订阅时取覆盖矩阵(可切换版本目标);没有订阅时只看是否已入库。默认展开订阅的季,否则库里有文件的最新季,否则最新季。
## 排查
| 现象 | 看哪里 |
|---|---|
| 标题仍是英文原名 | 作品详情「字段来源与锁」里 `标题` 的来源与语言;是否被锁;设置里的语言链;对作品跑 `full=1` |
| 简介显示「暂无简介」 | TMDB 该作品 / 集在简介链的语言里都没有简介,属正常(MD-2 不再用英文顶替);要英文时打开「简介回退到非中文」再刷新;也可在每集行手动填写并锁定 |
| 某集没有中文标题 | 该集在 TMDB 各语言都没有译名时保留占位名 / 英文名,属正常;可在每集行手动改并锁定 |
| 季里没有集 | 默认刷新不为未追踪的季建集单元,点「更新每集信息」(full) |
| 没有特别篇 | TMDB 该剧有没有季 0;设置 `metadata.includeSpecials` 是否关了;对作品跑 `full=1` |
| 集还是「临时」 | 该季是否拉过季详情(跑 `full=1`);集号是否超出 TMDB 该季集数(对不上的保持临时,属正常) |