feat(scrape): TMDB 标志写 clearlogo.png
- 富详情请求加 images(include_image_language=zh,en,null),作品原语言不是中英文时另取 /images; 选一张 PNG 标志(原语言 → 中文 → 英文 → 无文字,同语言按评分)存作品字段 logoPath,变化时触发重刮 - 刮削布局新角色 clearlogo:电影写影片目录 clearlogo.png、剧集写剧集根目录,库根 / 平铺电影 <视频名>-clearlogo.png; 覆盖方式与 hash 规则同其它图片 - 布局金样差分登记偏离 tmdb_clearlogo;mock TMDB 支持 logos 与 /images Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
1dca95f883
commit
680419bb64
16 files changed
+379
-12
No files matched your search
@@ -82,6 +82,8 @@ export interface TmdbRichDetail {
|
||||
originCountry: string[];
|
||||
posterPath: string | null;
|
||||
backdropPath: string | null;
|
||||
/** 标志(透明 PNG,刮削写 clearlogo.png);语言优先 原语言 → 中文 → 英文 → 无文字;没有为 null(旧数据可缺省) */
|
||||
logoPath?: string | null;
|
||||
cast: TmdbCredit[];
|
||||
crew: TmdbCredit[];
|
||||
externalIds: { imdb: string | null; tvdb: string | null; wikidata: string | null };
|
||||
@@ -163,9 +165,48 @@ function credits(raw: unknown, type: MediaEntityType, createdBy: unknown): { cas
|
||||
return { cast, crew: crew.slice(0, 6) };
|
||||
}
|
||||
|
||||
/** 详情请求带回的图片语言:中文、英文、无文字;作品原语言不在其中时另取一次 /images */
|
||||
export const DETAIL_IMAGE_LANGUAGES = "zh,en,null";
|
||||
|
||||
interface LogoRow {
|
||||
path: string;
|
||||
lang: string | null;
|
||||
vote: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const logoRows = (images: unknown): LogoRow[] => {
|
||||
const logos = ((images ?? {}) as Json).logos;
|
||||
return ((Array.isArray(logos) ? logos : []) as Json[]).flatMap((x) => {
|
||||
const path = s(x.file_path);
|
||||
// 只要 PNG:Kodi / Emby / Jellyfin 的 clearlogo 都是 png,TMDB 的 svg 标志不用
|
||||
if (!path || !/\.png$/i.test(path)) return [];
|
||||
return [{ path, lang: s(x.iso_639_1), vote: n(x.vote_average) ?? 0, count: n(x.vote_count) ?? 0 }];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 富详情:一次请求 `/{type}/{id}?language=链首&append_to_response=alternative_titles,credits,external_ids,translations`,
|
||||
* 标题 / 简介 / 标语按链从 translations(+ 同地区别名)取。不存在抛 TmdbError(notfound)。
|
||||
* 选一张标志:语言优先 作品原语言 → 中文 → 英文 → 无文字;同语言按评分、票数从高到低。没有 PNG 标志返回 null。
|
||||
* images 可以是多份(详情带回的 + 原语言单独取的)。
|
||||
*/
|
||||
export function pickLogo(images: unknown[], originalLanguage: string | null): string | null {
|
||||
const rows = images.flatMap(logoRows);
|
||||
const orig = originalLanguage?.toLowerCase().split("-")[0] ?? null;
|
||||
const order = [orig, "zh", "en", null].filter((x, i, a) => a.indexOf(x) === i && (x !== null || i === a.length - 1));
|
||||
for (const lang of order) {
|
||||
const hit = rows
|
||||
.filter((r) => (r.lang?.toLowerCase() ?? null) === lang)
|
||||
.sort((a, b) => b.vote - a.vote || b.count - a.count)[0];
|
||||
if (hit) return hit.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 富详情:一次请求 `/{type}/{id}?language=链首&append_to_response=alternative_titles,credits,external_ids,translations,images`
|
||||
* (`include_image_language=zh,en,null`),标题 / 简介 / 标语按链从 translations(+ 同地区别名)取。
|
||||
* 作品原语言不是中文 / 英文时另取 `/{type}/{id}/images?include_image_language=<原语言>` 选标志(失败忽略)。
|
||||
* 不存在抛 TmdbError(notfound)。
|
||||
*/
|
||||
export async function fetchRichDetail(
|
||||
client: Pick<TmdbClient, "getJson">,
|
||||
@@ -177,7 +218,8 @@ export async function fetchRichDetail(
|
||||
const requested = chainOf(languages);
|
||||
const j = await client.getJson<Json>(`/${type}/${id}`, {
|
||||
language: requested[0],
|
||||
append_to_response: "alternative_titles,credits,external_ids,translations",
|
||||
append_to_response: "alternative_titles,credits,external_ids,translations,images",
|
||||
include_image_language: DETAIL_IMAGE_LANGUAGES,
|
||||
});
|
||||
const titleKey = type === "movie" ? "title" : "name";
|
||||
const originalTitle = s(type === "movie" ? j.original_title : j.original_name);
|
||||
@@ -212,6 +254,15 @@ export async function fetchRichDetail(
|
||||
const ext = (j.external_ids ?? {}) as Json;
|
||||
const runtimes = Array.isArray(j.episode_run_time) ? (j.episode_run_time as unknown[]) : [];
|
||||
const { cast, crew } = credits(j.credits, type, j.created_by);
|
||||
const logoSources: unknown[] = [j.images];
|
||||
const origLang = originalLanguage?.toLowerCase().split("-")[0] ?? null;
|
||||
if (origLang && origLang !== "zh" && origLang !== "en") {
|
||||
try {
|
||||
logoSources.unshift(await client.getJson<Json>(`/${type}/${id}/images`, { include_image_language: origLang }));
|
||||
} catch {
|
||||
// 原语言标志取不到就用中文 / 英文 / 无文字的
|
||||
}
|
||||
}
|
||||
return {
|
||||
entityType: type,
|
||||
externalId: String(id),
|
||||
@@ -240,6 +291,7 @@ export async function fetchRichDetail(
|
||||
originCountry,
|
||||
posterPath: s(j.poster_path),
|
||||
backdropPath: s(j.backdrop_path),
|
||||
logoPath: pickLogo(logoSources, originalLanguage),
|
||||
cast,
|
||||
crew,
|
||||
externalIds: {
|
||||
|
||||
Reference in new issue
Block a user