- 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]>
377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
/**
|
|
* mock TMDB 固定数据。电影/剧集 ID 取自公开 TMDB 条目以便对照,但标题/集数/日期只保证"合理",
|
|
* 不作为真实数据的权威来源;国产综艺条目 ID(900001)为虚构。
|
|
*/
|
|
export interface TmdbFixtureEpisode {
|
|
episode_number: number;
|
|
air_date: string | null;
|
|
/** 按语言的集名;请求语言没有时 mock 返回 TMDB 式占位名("第 N 集" / "Episode N") */
|
|
names: Record<string, string>;
|
|
/** MD-1:按语言的简介;没有时返回空串 */
|
|
overviews?: Record<string, string>;
|
|
runtime?: number | null;
|
|
still_path?: string | null;
|
|
vote_average?: number;
|
|
vote_count?: number;
|
|
}
|
|
export interface TmdbFixtureSeason {
|
|
season_number: number;
|
|
air_date: string | null;
|
|
names: Record<string, string>;
|
|
overviews?: Record<string, string>;
|
|
poster_path?: string | null;
|
|
episodes: TmdbFixtureEpisode[];
|
|
}
|
|
/** MD-1 富详情字段(credits / 类型 / 评分 / 状态…);缺省不返回 */
|
|
export interface TmdbFixtureRich {
|
|
genres?: string[];
|
|
vote_average?: number;
|
|
vote_count?: number;
|
|
status?: string;
|
|
runtime?: number;
|
|
episode_run_time?: number[];
|
|
last_air_date?: string;
|
|
backdrop_path?: string;
|
|
networks?: string[];
|
|
tagline?: Record<string, string>;
|
|
cast?: { id: number; name: string; character: string; profile_path: string | null }[];
|
|
crew?: { id: number; name: string; job: string; profile_path: string | null }[];
|
|
created_by?: { id: number; name: string; profile_path: string | null }[];
|
|
}
|
|
export interface TmdbFixture {
|
|
type: "movie" | "tv";
|
|
id: number;
|
|
original_title: string;
|
|
original_language: string;
|
|
origin_country: string[];
|
|
date: string;
|
|
titles: Record<string, string>;
|
|
overviews?: Record<string, string>;
|
|
popularity: number;
|
|
alternative_titles: { iso_3166_1: string; title: string; type: string }[];
|
|
external_ids: { imdb_id?: string | null; tvdb_id?: number | null };
|
|
/** 额外译名(titles 中的语言会自动生成 translations);title 为空串表示该语言只有简介没有译名(TMDB 常见) */
|
|
extra_translations?: { iso_639_1: string; iso_3166_1: string; title: string; overview?: string }[];
|
|
rich?: TmdbFixtureRich;
|
|
seasons?: TmdbFixtureSeason[];
|
|
/** 海报路径(拼 image.tmdb.org);缺省 null */
|
|
poster_path?: string | null;
|
|
}
|
|
|
|
function eps(n: number, start: string, zh: (i: number) => string, en: (i: number) => string): TmdbFixtureEpisode[] {
|
|
const base = Date.parse(start);
|
|
return Array.from({ length: n }, (_, k) => ({
|
|
episode_number: k + 1,
|
|
air_date: new Date(base + k * 7 * 86400_000).toISOString().slice(0, 10),
|
|
names: { "zh-CN": zh(k + 1), "en-US": en(k + 1) },
|
|
}));
|
|
}
|
|
|
|
export const TMDB_FIXTURES: TmdbFixture[] = [
|
|
{
|
|
type: "movie",
|
|
id: 872585,
|
|
original_title: "Oppenheimer",
|
|
original_language: "en",
|
|
origin_country: ["US"],
|
|
date: "2023-07-19",
|
|
titles: { "zh-CN": "奥本海默", "en-US": "Oppenheimer" },
|
|
overviews: { "zh-CN": "原子弹之父的故事。", "en-US": "The story of J. Robert Oppenheimer." },
|
|
popularity: 120.5,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "TW", title: "奧本海默", type: "" },
|
|
{ iso_3166_1: "CN", title: "奥本海默传", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt15398776" },
|
|
poster_path: "/oppenheimer.jpg",
|
|
extra_translations: [{ iso_639_1: "ja", iso_3166_1: "JP", title: "オッペンハイマー" }],
|
|
},
|
|
{
|
|
type: "movie",
|
|
id: 693134,
|
|
original_title: "Dune: Part Two",
|
|
original_language: "en",
|
|
origin_country: ["US"],
|
|
date: "2024-02-27",
|
|
titles: { "zh-CN": "沙丘2", "en-US": "Dune: Part Two" },
|
|
popularity: 210.1,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "US", title: "Dune 2", type: "" },
|
|
{ iso_3166_1: "CN", title: "沙丘:第二部", type: "" },
|
|
{ iso_3166_1: "TW", title: "沙丘:第二部", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt15239678" },
|
|
},
|
|
{
|
|
type: "movie",
|
|
id: 842675,
|
|
original_title: "流浪地球2",
|
|
original_language: "zh",
|
|
origin_country: ["CN"],
|
|
date: "2023-01-22",
|
|
titles: { "zh-CN": "流浪地球2", "en-US": "The Wandering Earth II" },
|
|
popularity: 45.3,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "US", title: "The Wandering Earth 2", type: "" },
|
|
{ iso_3166_1: "CN", title: "流浪地球 2", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt13539646" },
|
|
},
|
|
{
|
|
type: "tv",
|
|
id: 136315,
|
|
original_title: "The Bear",
|
|
original_language: "en",
|
|
origin_country: ["US"],
|
|
date: "2022-06-23",
|
|
titles: { "zh-CN": "熊家餐馆", "en-US": "The Bear" },
|
|
popularity: 88.2,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "TW", title: "大熊餐廳", type: "" },
|
|
{ iso_3166_1: "CN", title: "熊家小馆", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt14452776", tvdb_id: 401654 },
|
|
seasons: [
|
|
{
|
|
season_number: 1,
|
|
air_date: "2022-06-23",
|
|
names: { "zh-CN": "第 1 季", "en-US": "Season 1" },
|
|
episodes: eps(
|
|
8,
|
|
"2022-06-23",
|
|
(i) => `第 ${i} 集`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
{
|
|
season_number: 2,
|
|
air_date: "2023-06-22",
|
|
names: { "zh-CN": "第 2 季", "en-US": "Season 2" },
|
|
episodes: eps(
|
|
10,
|
|
"2023-06-22",
|
|
(i) => `第 ${i} 集`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
{
|
|
season_number: 3,
|
|
air_date: "2024-06-26",
|
|
names: { "zh-CN": "第 3 季", "en-US": "Season 3" },
|
|
episodes: eps(
|
|
10,
|
|
"2024-06-26",
|
|
(i) => `第 ${i} 集`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: "tv",
|
|
id: 100088,
|
|
original_title: "The Last of Us",
|
|
original_language: "en",
|
|
origin_country: ["US"],
|
|
date: "2023-01-15",
|
|
titles: { "zh-CN": "最后生还者", "en-US": "The Last of Us" },
|
|
popularity: 150.7,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "CN", title: "最后的生还者", type: "" },
|
|
{ iso_3166_1: "TW", title: "最後生還者", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt3581920", tvdb_id: 392256 },
|
|
seasons: [
|
|
{
|
|
season_number: 1,
|
|
air_date: "2023-01-15",
|
|
names: { "zh-CN": "第 1 季", "en-US": "Season 1" },
|
|
episodes: eps(
|
|
9,
|
|
"2023-01-15",
|
|
(i) => `第 ${i} 集`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: "tv",
|
|
id: 900001,
|
|
original_title: "向往的生活",
|
|
original_language: "zh",
|
|
origin_country: ["CN"],
|
|
date: "2017-01-15",
|
|
titles: { "zh-CN": "向往的生活", "en-US": "Back to Field" },
|
|
popularity: 12.4,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "CN", title: "向往的生活 第一季", type: "" },
|
|
{ iso_3166_1: "CN", title: "向往", type: "简称" },
|
|
],
|
|
external_ids: { imdb_id: null, tvdb_id: null },
|
|
seasons: [
|
|
{
|
|
season_number: 0,
|
|
air_date: "2017-01-10",
|
|
names: { "zh-CN": "特别篇", "en-US": "Specials" },
|
|
episodes: eps(
|
|
2,
|
|
"2017-01-10",
|
|
(i) => `先导片 ${i}`,
|
|
(i) => `Special ${i}`,
|
|
),
|
|
},
|
|
{
|
|
season_number: 1,
|
|
air_date: "2017-01-15",
|
|
names: { "zh-CN": "第 1 季", "en-US": "Season 1" },
|
|
episodes: eps(
|
|
12,
|
|
"2017-01-15",
|
|
(i) => `第 ${i} 期`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
{
|
|
season_number: 2,
|
|
air_date: "2018-04-20",
|
|
names: { "zh-CN": "第 2 季", "en-US": "Season 2" },
|
|
episodes: eps(
|
|
3,
|
|
"2018-04-20",
|
|
(i) => `第 ${i} 期`,
|
|
(i) => `Episode ${i}`,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
// MD-1 语言回退链:zh-CN 译名为空(只有简介)、CN 地区别名是简中名、zh-TW / zh-HK 有译名(对照 TMDB tv/95396 真实形态)
|
|
{
|
|
type: "tv",
|
|
id: 95396,
|
|
original_title: "Severance",
|
|
original_language: "en",
|
|
origin_country: ["US"],
|
|
date: "2022-02-17",
|
|
titles: { "en-US": "Severance", "zh-TW": "人生切割術", "zh-HK": "生活割離術" },
|
|
overviews: { "en-US": "Mark leads a team of office workers.", "zh-TW": "馬克帶領的辦公團隊成員全都接受手術。" },
|
|
popularity: 95.1,
|
|
alternative_titles: [
|
|
{ iso_3166_1: "CN", title: "人生切割术", type: "" },
|
|
{ iso_3166_1: "HK", title: "生活割離術", type: "" },
|
|
],
|
|
external_ids: { imdb_id: "tt11280740", tvdb_id: 371980 },
|
|
extra_translations: [
|
|
{ iso_639_1: "zh", iso_3166_1: "CN", title: "", overview: "故事发生在一家名为卢蒙的超级大公司内。" },
|
|
{ iso_639_1: "zh", iso_3166_1: "SG", title: "", overview: "马克领导着一个办公室里的团队。" },
|
|
],
|
|
poster_path: "/severance.jpg",
|
|
rich: {
|
|
genres: ["剧情", "悬疑", "Sci-Fi & Fantasy"],
|
|
vote_average: 8.395,
|
|
vote_count: 2400,
|
|
status: "Returning Series",
|
|
episode_run_time: [],
|
|
last_air_date: "2025-03-21",
|
|
backdrop_path: "/severance-bg.jpg",
|
|
networks: ["Apple TV"],
|
|
cast: [
|
|
{ id: 4495, name: "亚当·斯科特", character: "Mark Scout", profile_path: "/adam.jpg" },
|
|
{ id: 1319469, name: "布丽特·洛薇尔", character: "Helly Riggs", profile_path: null },
|
|
],
|
|
created_by: [{ id: 3442409, name: "丹·埃里克森", profile_path: "/dan.jpg" }],
|
|
},
|
|
seasons: [
|
|
{
|
|
season_number: 1,
|
|
air_date: "2022-02-17",
|
|
names: { "zh-CN": "第 1 季", "en-US": "Season 1" },
|
|
overviews: { "zh-CN": "在卢蒙工业公司,员工们接受了一种名为“分离”的手术。" },
|
|
poster_path: "/sev-s1.jpg",
|
|
episodes: [
|
|
{
|
|
episode_number: 1,
|
|
air_date: "2022-02-17",
|
|
names: { "zh-CN": "有关地狱的好消息", "en-US": "Good News About Hell" },
|
|
overviews: { "zh-CN": "马克升职成为部门主管。", "en-US": "Mark is promoted." },
|
|
runtime: 59,
|
|
still_path: "/sev-101.jpg",
|
|
vote_average: 8.2,
|
|
vote_count: 171,
|
|
},
|
|
{
|
|
episode_number: 2,
|
|
air_date: "2022-02-17",
|
|
names: { "zh-TW": "半循環", "en-US": "Half Loop" },
|
|
overviews: { "zh-TW": "馬克帶領新員工參觀。", "en-US": "Mark gives Helly a tour." },
|
|
runtime: 53,
|
|
still_path: "/sev-102.jpg",
|
|
vote_average: 8.0,
|
|
vote_count: 150,
|
|
},
|
|
{
|
|
episode_number: 3,
|
|
air_date: "2022-02-25",
|
|
names: { "en-US": "In Perpetuity" },
|
|
overviews: {},
|
|
runtime: 52,
|
|
still_path: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
season_number: 2,
|
|
air_date: "2099-01-17",
|
|
names: { "zh-CN": "第 2 季", "en-US": "Season 2" },
|
|
episodes: [{ episode_number: 1, air_date: "2099-01-17", names: {} }],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
/**
|
|
* MD-2:《心动的信号》(TMDB tv/103147 的真实形态):作品与每期标题有 zh-CN,简介只有 en-US(实为标题英译)。
|
|
* 不在 TMDB_FIXTURES 里,用到的测试自行拼进 fixtures。
|
|
*/
|
|
export const HEART_SIGNAL_FIXTURE: TmdbFixture = {
|
|
type: "tv",
|
|
id: 103147,
|
|
original_title: "心动的信号",
|
|
original_language: "zh",
|
|
origin_country: ["CN"],
|
|
date: "2018-08-01",
|
|
titles: { "zh-CN": "心动的信号", "en-US": "Heart Signal" },
|
|
overviews: { "en-US": "A Chinese dating reality show." },
|
|
popularity: 6.2,
|
|
alternative_titles: [],
|
|
external_ids: { imdb_id: null, tvdb_id: null },
|
|
rich: { genres: ["真人秀"], status: "Returning Series" },
|
|
seasons: [
|
|
{
|
|
season_number: 1,
|
|
air_date: "2018-08-01",
|
|
names: { "zh-CN": "第 1 季", "en-US": "Season 1" },
|
|
episodes: [
|
|
{
|
|
episode_number: 1,
|
|
air_date: "2018-08-01",
|
|
names: { "zh-CN": "心动不如行动,大胆亮出底牌!", "en-US": "Episode 1" },
|
|
overviews: { "en-US": "If Your Heart Skips a Beat, Play Your Cards Openly!" },
|
|
},
|
|
{
|
|
episode_number: 2,
|
|
air_date: "2018-08-08",
|
|
names: { "zh-CN": "第 2 集", "en-US": "Who Is the One?" },
|
|
overviews: { "en-US": "Who Is the One?" },
|
|
},
|
|
{
|
|
episode_number: 3,
|
|
air_date: "2018-08-15",
|
|
names: { "zh-CN": "约会之夜", "en-US": "Date Night" },
|
|
overviews: { "zh-TW": "約會之夜。", "en-US": "Date Night." },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|