feat(metadata): overview falls back to Chinese variants only (MD-2)
- adapters: overviewLanguages() limits overview/tagline chain to Chinese variants (zh-CN→zh-SG→zh-TW→zh-HK); titles keep full chain; season supplement fetch skips languages useless for overview - setting metadata.overviewNonChineseFallback (default off) + UI switch - media detail shows "暂无简介" for empty entity/episode overview - scripts/overview-recompute.ts: recompute stored non-Chinese overviews from kv_cache only (no TMDB requests), dry-run by default, --apply - tests: chain unit tests, Heart Signal (103147) fixture, server recompute test, MP differential for episode overview - runbook + CHANGELOG Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
081db4651c
commit
f9901b514b
22 files changed
+887
-38
No files matched your search
@@ -0,0 +1,94 @@
|
||||
// 存量简介重算(MD-2):简介的语言回退只到中文变体后,把之前回退到英文等非中文的作品 / 季 / 集简介按新规则重算。
|
||||
// 不请求 TMDB,只读 kv_cache(tmdb.request)里已缓存的 TMDB 响应;缓存里没有的按"当时中文为空"清空。
|
||||
// 默认只读(dry-run),加 --apply 才改库(单个事务,写一条审计 metadata.overview_recompute)。
|
||||
//
|
||||
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
|
||||
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/overview-recompute.ts # 看清单
|
||||
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --apply # 写库
|
||||
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --entity <作品 id> # 只看 / 只处理一部作品
|
||||
// DATABASE_URL=… deno run -A scripts/overview-recompute.ts --json # 清单输出 JSON
|
||||
//
|
||||
// 语言链与"简介是否允许回退到非中文"取库里的设置 metadata.languages / metadata.overviewNonChineseFallback。
|
||||
// 规则与实现见 apps/server/src/modules/sources-identity/service/overview-recompute.ts、docs/runbook/metadata.md。
|
||||
|
||||
import { DEFAULT_METADATA_LANGUAGES } from "@umt/adapters";
|
||||
import type { DB } from "@umt/db";
|
||||
import { Kysely, PostgresDialect, sql } from "kysely";
|
||||
import pg from "pg";
|
||||
import {
|
||||
applyOverviewRecompute,
|
||||
type OverviewChange,
|
||||
planOverviewRecompute,
|
||||
} from "../apps/server/src/modules/sources-identity/service/overview-recompute.ts";
|
||||
|
||||
const argv = [...Deno.args];
|
||||
const APPLY = argv.includes("--apply");
|
||||
const JSON_OUT = argv.includes("--json");
|
||||
if (APPLY && argv.includes("--dry-run")) {
|
||||
console.error("--apply 与 --dry-run 不能同时给出");
|
||||
Deno.exit(2);
|
||||
}
|
||||
const entityArg = argv.indexOf("--entity");
|
||||
const ONLY_ENTITY = entityArg >= 0 ? (argv[entityArg + 1] ?? null) : null;
|
||||
const url = Deno.env.get("DATABASE_URL");
|
||||
if (!url) {
|
||||
console.error("需要 DATABASE_URL(脚本不带默认库)");
|
||||
Deno.exit(2);
|
||||
}
|
||||
const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: url, max: 2 }) }) });
|
||||
|
||||
const meta = (
|
||||
await sql<{ value: { languages?: string[]; overviewNonChineseFallback?: boolean } | null }>`
|
||||
select value from settings where key = 'metadata'`.execute(db)
|
||||
).rows[0]?.value;
|
||||
const languages = meta?.languages?.length ? meta.languages : [...DEFAULT_METADATA_LANGUAGES];
|
||||
const overviewNonChineseFallback = meta?.overviewNonChineseFallback === true;
|
||||
const now = new Date();
|
||||
|
||||
const cut = (s: string | null, n = 40) => (s === null ? "(空)" : s.length > n ? `${s.slice(0, n)}…` : s);
|
||||
const where = (c: OverviewChange) =>
|
||||
c.target === "entity"
|
||||
? "作品"
|
||||
: c.target === "season"
|
||||
? `S${String(c.season).padStart(2, "0")}`
|
||||
: `S${String(c.season).padStart(2, "0")}E${String(c.episode).padStart(2, "0")}`;
|
||||
|
||||
try {
|
||||
const plan = await planOverviewRecompute(db, { languages, overviewNonChineseFallback, now, entityId: ONLY_ENTITY });
|
||||
if (JSON_OUT && !APPLY) {
|
||||
console.log(JSON.stringify(plan, null, 2));
|
||||
} else {
|
||||
console.log(
|
||||
`# overview-recompute (${APPLY ? "APPLY" : "DRY-RUN"})${ONLY_ENTITY ? ` --entity ${ONLY_ENTITY}` : ""}`,
|
||||
);
|
||||
console.log(`语言链 ${languages.join(" → ")};简介链 ${plan.overviewLanguages.join(" → ")}`);
|
||||
console.log(
|
||||
`非中文简介:作品 ${plan.scanned.entities},季 / 集 ${plan.scanned.units};读缓存 ${plan.cache.reads} 次,未命中 ${plan.cache.misses}\n`,
|
||||
);
|
||||
const byEntity = new Map<string, OverviewChange[]>();
|
||||
for (const c of plan.changes) byEntity.set(c.mediaEntityId, [...(byEntity.get(c.mediaEntityId) ?? []), c]);
|
||||
for (const [id, list] of byEntity) {
|
||||
console.log(`## ${list[0]!.entityTitle} (${id}) ${list.length} 项`);
|
||||
for (const c of list) {
|
||||
const basis = c.basis === "cache" ? "缓存" : "无缓存→清空";
|
||||
console.log(
|
||||
` ${where(c).padEnd(8)} ${c.field.padEnd(8)} [${c.from.language ?? "-"}] ${cut(c.from.value)} → [${c.to.language ?? "-"}] ${cut(c.to.value)} (${basis})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const count = (t: OverviewChange["target"]) => plan.changes.filter((c) => c.target === t).length;
|
||||
console.log(
|
||||
`\n合计 ${plan.changes.length} 项:作品 ${count("entity")},季 ${count("season")},集 ${count("episode")};` +
|
||||
`其中无缓存清空 ${plan.changes.filter((c) => c.basis === "no_cache").length},` +
|
||||
`改成中文 ${plan.changes.filter((c) => c.to.value !== null).length}`,
|
||||
);
|
||||
}
|
||||
if (APPLY && plan.changes.length) {
|
||||
const r = await db.transaction().execute((trx) => applyOverviewRecompute(trx, plan, { now }));
|
||||
console.log(`已写库:最终值有变化的作品 ${r.entities},季 / 集 ${r.units}(锁定 / 覆盖的字段不变)`);
|
||||
} else if (!APPLY) {
|
||||
console.log("dry-run:未改库。确认清单后加 --apply 执行");
|
||||
}
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user