feat(tmdb): 多语言保留规则——译名 / 别名只留简繁中文、英文与原语言
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
69d820a6bf
commit
9c5f04b02f
8 files changed
+423
-29
No files matched your search
@@ -6,8 +6,11 @@
|
||||
* 请求缓存语义同 client.ts(MP tmdbv3api request 的 @cached:成功才缓存、空结果短 TTL)。
|
||||
* 调整:标题 / 简介按语言回退链取(translations.ts);季集在首选语言缺标题(占位名)或缺简介时按链逐个语言补拉,
|
||||
* 链走完或全部补齐即停。MD-2:简介 / 标语只按 overviewLanguages(缺省只到中文变体,不回退英文)取,取不到留空。主创只留前 10 位演员 + 导演 / 主创(created_by),头像只存路径(前端经图片代理)。
|
||||
* 多语言保留规则(language-policy.ts):translations / alternative_titles 只留简中 / 繁中 / 英文 / 原语言,
|
||||
* 语言链按作品收敛(去掉既不是中英文、也不是作品原语言的语言)。
|
||||
*/
|
||||
import type { MediaEntityType, TmdbClient } from "./client.ts";
|
||||
import { allowedLanguageChain, altTitleLanguage, isAllowedLanguage, type WorkLanguages } from "./language-policy.ts";
|
||||
import {
|
||||
DEFAULT_METADATA_LANGUAGES,
|
||||
isPlaceholderEpisodeName,
|
||||
@@ -85,7 +88,7 @@ export interface TmdbRichDetail {
|
||||
translations: TmdbTranslation[];
|
||||
alternativeTitles: TmdbAltTitle[];
|
||||
seasons: TmdbRichSeason[];
|
||||
/** 本次使用的语言链 */
|
||||
/** 本次使用的语言链(按作品收敛后) */
|
||||
languages: string[];
|
||||
}
|
||||
|
||||
@@ -126,6 +129,12 @@ export interface RichTextOptions {
|
||||
const chainOf = (languages?: readonly string[]) =>
|
||||
languages?.length ? [...languages] : [...DEFAULT_METADATA_LANGUAGES];
|
||||
|
||||
/** 语言链按作品收敛(保留规则);全被去掉时用缺省链 */
|
||||
export function workLanguageChain(languages: readonly string[] | undefined, work: WorkLanguages): string[] {
|
||||
const eff = allowedLanguageChain(chainOf(languages), work);
|
||||
return eff.length ? eff : allowedLanguageChain(DEFAULT_METADATA_LANGUAGES, work);
|
||||
}
|
||||
|
||||
function credits(raw: unknown, type: MediaEntityType, createdBy: unknown): { cast: TmdbCredit[]; crew: TmdbCredit[] } {
|
||||
const c = (raw ?? {}) as Json;
|
||||
const cast = ((Array.isArray(c.cast) ? c.cast : []) as Json[])
|
||||
@@ -165,24 +174,40 @@ export async function fetchRichDetail(
|
||||
languages?: readonly string[],
|
||||
opts: RichTextOptions = {},
|
||||
): Promise<TmdbRichDetail> {
|
||||
const chain = chainOf(languages);
|
||||
const textChain = overviewLanguages(chain, opts.overviewNonChineseFallback);
|
||||
const requested = chainOf(languages);
|
||||
const j = await client.getJson<Json>(`/${type}/${id}`, {
|
||||
language: chain[0],
|
||||
language: requested[0],
|
||||
append_to_response: "alternative_titles,credits,external_ids,translations",
|
||||
});
|
||||
const titleKey = type === "movie" ? "title" : "name";
|
||||
const translations = parseTranslations(j.translations, titleKey);
|
||||
const altTitles = parseAltTitles(j.alternative_titles);
|
||||
const originalTitle = s(type === "movie" ? j.original_title : j.original_name);
|
||||
const originalLanguage = s(j.original_language);
|
||||
const primary = s(j[titleKey]);
|
||||
const originCountry = Array.isArray(j.origin_country)
|
||||
? (j.origin_country as unknown[]).filter((x): x is string => typeof x === "string")
|
||||
: [];
|
||||
const work: WorkLanguages = { originalLanguage, originCountry };
|
||||
// 保留规则:译名只留允许语言;别名按地区 + 文字推断语言,推断不出的丢弃;语言链按作品收敛
|
||||
const translations = parseTranslations(j.translations, titleKey).filter((t) => isAllowedLanguage(t.lang, work));
|
||||
const altTitles = parseAltTitles(j.alternative_titles)
|
||||
.map((a) => ({ ...a, language: altTitleLanguage(a, work) }))
|
||||
.filter((a) => a.language);
|
||||
const chain = workLanguageChain(requested, work);
|
||||
const textChain = overviewLanguages(chain, opts.overviewNonChineseFallback);
|
||||
// 请求语言不在保留范围内时,请求语言下的值不可信(是被去掉的语言),不拿来兜底
|
||||
const reqLang = isAllowedLanguage(requested[0], work) ? requested[0]! : null;
|
||||
const primary = reqLang ? s(j[titleKey]) : null;
|
||||
const titleSrc = { translations, altTitles, originalTitle, originalLanguage, primary };
|
||||
const title = pickTitle(chain, titleSrc);
|
||||
const zh = pickChineseTitle(chain, titleSrc);
|
||||
// 请求语言 = 链首,链首总在 textChain 里(见 overviewLanguages),所以 primary 可以兜底
|
||||
const overview = pickText(textChain, translations, "overview", { value: s(j.overview), language: chain[0] ?? null });
|
||||
const tagline = pickText(textChain, translations, "tagline", { value: s(j.tagline), language: chain[0] ?? null });
|
||||
const overview = pickText(textChain, translations, "overview", {
|
||||
value: reqLang ? s(j.overview) : null,
|
||||
language: reqLang,
|
||||
});
|
||||
const tagline = pickText(textChain, translations, "tagline", {
|
||||
value: reqLang ? s(j.tagline) : null,
|
||||
language: reqLang,
|
||||
});
|
||||
const date = s(type === "movie" ? j.release_date : j.first_air_date);
|
||||
const ext = (j.external_ids ?? {}) as Json;
|
||||
const runtimes = Array.isArray(j.episode_run_time) ? (j.episode_run_time as unknown[]) : [];
|
||||
@@ -190,7 +215,7 @@ export async function fetchRichDetail(
|
||||
return {
|
||||
entityType: type,
|
||||
externalId: String(id),
|
||||
title: title?.value ?? primary ?? originalTitle ?? "",
|
||||
title: title?.value ?? primary ?? originalTitle ?? s(j[titleKey]) ?? "",
|
||||
titleLanguage: title?.language ?? null,
|
||||
titleZh: zh?.value ?? null,
|
||||
titleZhLanguage: zh?.language ?? null,
|
||||
@@ -212,9 +237,7 @@ export async function fetchRichDetail(
|
||||
networks: ((Array.isArray(j.networks) ? j.networks : []) as Json[])
|
||||
.map((x) => s(x.name))
|
||||
.filter((x): x is string => !!x),
|
||||
originCountry: Array.isArray(j.origin_country)
|
||||
? (j.origin_country as unknown[]).filter((x): x is string => typeof x === "string")
|
||||
: [],
|
||||
originCountry,
|
||||
posterPath: s(j.poster_path),
|
||||
backdropPath: s(j.backdrop_path),
|
||||
cast,
|
||||
@@ -268,9 +291,18 @@ export async function fetchRichSeason(
|
||||
tvId: string | number,
|
||||
seasonNumber: number,
|
||||
languages?: readonly string[],
|
||||
opts: { now?: Date } & RichTextOptions = {},
|
||||
opts: { now?: Date; originalLanguage?: string | null } & RichTextOptions = {},
|
||||
): Promise<TmdbRichSeasonDetail> {
|
||||
const chain = chainOf(languages);
|
||||
const requested = chainOf(languages);
|
||||
// 保留规则:链里有中英文以外的语言时要知道作品原语言才能判断;调用方没给就读一次作品详情(请求缓存)
|
||||
let originalLanguage = opts.originalLanguage;
|
||||
if (originalLanguage === undefined && allowedLanguageChain(requested, {}).length !== requested.length) {
|
||||
originalLanguage = await client
|
||||
.getJson<Json>(`/tv/${tvId}`, { language: requested[0] })
|
||||
.then((d) => s(d.original_language))
|
||||
.catch(() => null);
|
||||
}
|
||||
const chain = workLanguageChain(requested, { originalLanguage: originalLanguage ?? null });
|
||||
const first = chain[0]!;
|
||||
// MD-2:简介只从这些语言取(缺省只到中文变体);标题仍按完整链
|
||||
const textChain = overviewLanguages(chain, opts.overviewNonChineseFallback);
|
||||
|
||||
Reference in new issue
Block a user