feat(identity): 建档与刷新写别名按保留规则标语言,不再写空语言的 other
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
9c5f04b02f
commit
7efbe25c17
3 files changed
+38
-20
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import type { MetadataCandidate } from "@umt/adapters";
|
import { allowedLanguage, type MetadataCandidate, textMatchesLanguage } from "@umt/adapters";
|
||||||
import type { AliasKind, AliasSource, EvidenceLevel, MediaKind } from "@umt/contracts";
|
import type { AliasKind, AliasSource, EvidenceLevel, MediaKind } from "@umt/contracts";
|
||||||
import { sql } from "@umt/db";
|
import { sql } from "@umt/db";
|
||||||
import { normalizeTitle, type ParsedRelease } from "@umt/domain";
|
import { normalizeTitle, type ParsedRelease } from "@umt/domain";
|
||||||
@@ -171,6 +171,11 @@ export async function createEntityFromTmdb(
|
|||||||
return { entityId: entity.id, created: true };
|
return { entityId: entity.id, created: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 详情登记别名(来源 tmdb)。多语言保留规则(adapters language-policy.ts):details() 已只留简中 / 繁中 / 英文 /
|
||||||
|
* 原语言的本地化名、译名与别名(别名带按地区 + 文字推断的语言),这里按语言写入,不再写空语言的 other。
|
||||||
|
* 本地化名的文字与语言不符时(TMDB 缺该语言译名时回退成原名)不按该语言登记。
|
||||||
|
*/
|
||||||
export async function seedAliases(exec: Exec, entityId: string, m: MetadataCandidate): Promise<void> {
|
export async function seedAliases(exec: Exec, entityId: string, m: MetadataCandidate): Promise<void> {
|
||||||
// 同一文本只登记一次(按首个出现的类别):TMDB 的译名/别名列表常重复主标题与原名
|
// 同一文本只登记一次(按首个出现的类别):TMDB 的译名/别名列表常重复主标题与原名
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -180,12 +185,21 @@ export async function seedAliases(exec: Exec, entityId: string, m: MetadataCandi
|
|||||||
seen.add(n);
|
seen.add(n);
|
||||||
await addAlias(exec, entityId, text, kind, "tmdb", lang);
|
await addAlias(exec, entityId, text, kind, "tmdb", lang);
|
||||||
};
|
};
|
||||||
await add(m.title, "translated", null);
|
const work = { originalLanguage: m.originalLanguage, originCountry: m.originCountry };
|
||||||
if (m.originalTitle) await add(m.originalTitle, "original", m.originalLanguage);
|
if (m.originalTitle) await add(m.originalTitle, "original", m.originalLanguage);
|
||||||
|
let n = 0;
|
||||||
for (const t of m.titles) {
|
for (const t of m.titles) {
|
||||||
if (t.kind === "localized") await add(t.text, "translated", t.lang);
|
if (t.kind === "original" || !t.lang || n >= 50) continue;
|
||||||
|
const lang = allowedLanguage(t.lang, work);
|
||||||
|
if (!lang || !textMatchesLanguage(lang, t.text)) continue;
|
||||||
|
const kind: AliasKind = t.kind === "alias" ? (lang.endsWith("-Latn") ? "romanized" : "other") : "translated";
|
||||||
|
await add(t.text, kind, lang);
|
||||||
|
n++;
|
||||||
}
|
}
|
||||||
for (const a of m.aliases.slice(0, 50)) await add(a, "other", null);
|
// 主标题(建档时可能来自语言链):上面没登记到时补一条,语言按同文本的本地化名(文字相符才标)
|
||||||
|
const loc = m.titles.find((t) => t.text === m.title && t.kind === "localized" && t.lang);
|
||||||
|
const titleLang = loc?.lang && textMatchesLanguage(loc.lang, m.title) ? allowedLanguage(loc.lang, work) : null;
|
||||||
|
await add(m.title, "translated", titleLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IdentityConflictError extends Error {
|
export class IdentityConflictError extends Error {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_METADATA_LANGUAGES,
|
DEFAULT_METADATA_LANGUAGES,
|
||||||
scriptMatches,
|
|
||||||
type TmdbRichDetail,
|
type TmdbRichDetail,
|
||||||
type TmdbRichEpisode,
|
type TmdbRichEpisode,
|
||||||
type TmdbRichSeasonDetail,
|
type TmdbRichSeasonDetail,
|
||||||
|
textMatchesLanguage,
|
||||||
} from "@umt/adapters";
|
} from "@umt/adapters";
|
||||||
import type {
|
import type {
|
||||||
EntityMetadataField,
|
EntityMetadataField,
|
||||||
@@ -25,6 +25,7 @@ import { ensureSeason, promoteUnits } from "./metadata-totals.ts";
|
|||||||
// - 作品:TMDB 富详情(fetchRichDetail)按设置 metadata.languages 取标题 / 简介 / 标语(MD-2:简介 / 标语缺省只回退到中文变体),另存中文名 titleZh、类型、评分、
|
// - 作品:TMDB 富详情(fetchRichDetail)按设置 metadata.languages 取标题 / 简介 / 标语(MD-2:简介 / 标语缺省只回退到中文变体),另存中文名 titleZh、类型、评分、
|
||||||
// 状态、时长、季数、主创、外部 ID;来源值写 metadata_field(language = 实际取到的语言),同字段换语言时删旧行;
|
// 状态、时长、季数、主创、外部 ID;来源值写 metadata_field(language = 实际取到的语言),同字段换语言时删旧行;
|
||||||
// - 别名:translations / alternative_titles 写 title_alias(来源 tmdb,标语言);已有同文本别名只补空的语言;
|
// - 别名:translations / alternative_titles 写 title_alias(来源 tmdb,标语言);已有同文本别名只补空的语言;
|
||||||
|
// 只写简中 / 繁中 / 英文 / 原语言(多语言保留规则,adapters language-policy.ts);
|
||||||
// - 季 / 集:fetchRichSeason 的每集 name / overview / airDate / runtime / stillPath / vote 写 metadata_field(content_unit_id),
|
// - 季 / 集:fetchRichSeason 的每集 name / overview / airDate / runtime / stillPath / vote 写 metadata_field(content_unit_id),
|
||||||
// 投影到 content_unit.metadata(字段级,可锁),name → title 列、airDate → air_date 列;只写变化的字段,
|
// 投影到 content_unit.metadata(字段级,可锁),name → title 列、airDate → air_date 列;只写变化的字段,
|
||||||
// 不改编号、不碰路径绑定与资产覆盖。集单元不存在时按 TMDB 建(confirmed,挂季单元)。
|
// 不改编号、不碰路径绑定与资产覆盖。集单元不存在时按 TMDB 建(confirmed,挂季单元)。
|
||||||
@@ -152,26 +153,22 @@ export async function writeRichEntityFields(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 别名的地区 → 语言:只在文字相符时标(CN 地区的英文别名不标 zh-CN) */
|
|
||||||
function altLanguage(region: string | null, text: string): string | null {
|
|
||||||
if (!region) return null;
|
|
||||||
if (["CN", "SG", "TW", "HK", "MO"].includes(region)) return scriptMatches("zh", text) ? `zh-${region}` : null;
|
|
||||||
if (region === "JP") return scriptMatches("ja", text) ? "ja-JP" : null;
|
|
||||||
if (region === "KR") return scriptMatches("ko", text) ? "ko-KR" : null;
|
|
||||||
if (["US", "GB", "CA", "AU", "NZ", "IE"].includes(region)) return scriptMatches("en", text) ? `en-${region}` : null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* translations(译名,kind translated)与 alternative_titles(别名,kind other)写 title_alias,来源 tmdb、标语言。
|
* translations(译名,kind translated)与 alternative_titles(别名,kind other;原产地区罗马字 romanized)写 title_alias,
|
||||||
|
* 来源 tmdb、标语言。多语言保留规则:fetchRichDetail 已只留简中 / 繁中 / 英文 / 原语言的译名,别名带按地区 + 文字推断的语言
|
||||||
|
* (推断不出的已丢弃);这里再要求译名文字与语言相符。
|
||||||
* 同作品已有同文本别名时不再新增,只给来源 tmdb 且语言为空的那行补语言。最多 80 条。
|
* 同作品已有同文本别名时不再新增,只给来源 tmdb 且语言为空的那行补语言。最多 80 条。
|
||||||
*/
|
*/
|
||||||
export async function tagTitleAliases(exec: Exec, entityId: string, rich: TmdbRichDetail): Promise<number> {
|
export async function tagTitleAliases(exec: Exec, entityId: string, rich: TmdbRichDetail): Promise<number> {
|
||||||
const want: { text: string; kind: "translated" | "other"; language: string | null }[] = [];
|
type Kind = "translated" | "other" | "romanized";
|
||||||
|
const want: { text: string; kind: Kind; language: string | null }[] = [];
|
||||||
if (rich.titleZh) want.push({ text: rich.titleZh, kind: "translated", language: rich.titleZhLanguage });
|
if (rich.titleZh) want.push({ text: rich.titleZh, kind: "translated", language: rich.titleZhLanguage });
|
||||||
for (const t of rich.translations) if (t.title) want.push({ text: t.title, kind: "translated", language: t.lang });
|
for (const t of rich.translations)
|
||||||
|
if (t.title && textMatchesLanguage(t.lang, t.title))
|
||||||
|
want.push({ text: t.title, kind: "translated", language: t.lang });
|
||||||
for (const a of rich.alternativeTitles)
|
for (const a of rich.alternativeTitles)
|
||||||
want.push({ text: a.title, kind: "other", language: altLanguage(a.region, a.title) });
|
if (a.language)
|
||||||
|
want.push({ text: a.title, kind: a.language.endsWith("-Latn") ? "romanized" : "other", language: a.language });
|
||||||
const rows = await exec
|
const rows = await exec
|
||||||
.selectFrom("title_alias")
|
.selectFrom("title_alias")
|
||||||
.select(["id", "normalized", "language", "source"])
|
.select(["id", "normalized", "language", "source"])
|
||||||
|
|||||||
@@ -361,7 +361,14 @@ export async function refreshMetadata(
|
|||||||
creatable = new Set(opts.full ? want : tracked);
|
creatable = new Set(opts.full ? want : tracked);
|
||||||
for (const n of want) {
|
for (const n of want) {
|
||||||
try {
|
try {
|
||||||
richSeasons.push(await fetchRichSeason(tmdb, ident.external_id, n, langs, { now: rt.now(), ...textOpts }));
|
richSeasons.push(
|
||||||
|
await fetchRichSeason(tmdb, ident.external_id, n, langs, {
|
||||||
|
now: rt.now(),
|
||||||
|
...textOpts,
|
||||||
|
// 语言链按作品原语言收敛(多语言保留规则)
|
||||||
|
originalLanguage: richDetail?.originalLanguage ?? detail.originalLanguage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.logger.debug({ entityId: id, season: n, err: { message: (e as Error).message } }, "季详情获取失败");
|
ctx.logger.debug({ entityId: id, season: n, err: { message: (e as Error).message } }, "季详情获取失败");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user