Prefer NL then English TMDB posters over original-language art.
Avoids German (and similar) posters when no Dutch artwork exists but English does.
This commit is contained in:
parent
40a5ecd435
commit
14556c82f3
2 changed files with 87 additions and 57 deletions
|
|
@ -132,6 +132,28 @@ async function fetchTmdbJson(url: string, retries = 4): Promise<Response | null>
|
||||||
const METAHUB_LOGO = (imdbId: string) =>
|
const METAHUB_LOGO = (imdbId: string) =>
|
||||||
`https://images.metahub.space/logo/medium/${imdbId}/img`;
|
`https://images.metahub.space/logo/medium/${imdbId}/img`;
|
||||||
|
|
||||||
|
type TmdbImageAsset = {
|
||||||
|
file_path?: string;
|
||||||
|
iso_639_1?: string | null;
|
||||||
|
vote_average?: number;
|
||||||
|
aspect_ratio?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** NL → EN → textless → hoogste score (voorkomt Duitse posters bij ontbrekende NL). */
|
||||||
|
export function pickPreferredTmdbImage(images: TmdbImageAsset[]): TmdbImageAsset | undefined {
|
||||||
|
if (!images.length) return undefined;
|
||||||
|
const ranked = (lang: string | null) =>
|
||||||
|
images
|
||||||
|
.filter((i) => (lang === null ? !i.iso_639_1 : i.iso_639_1 === lang))
|
||||||
|
.sort((a, b) => (b.vote_average ?? 0) - (a.vote_average ?? 0))[0];
|
||||||
|
return (
|
||||||
|
ranked("nl") ||
|
||||||
|
ranked("en") ||
|
||||||
|
ranked(null) ||
|
||||||
|
[...images].sort((a, b) => (b.vote_average ?? 0) - (a.vote_average ?? 0))[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type TmdbMeta = {
|
type TmdbMeta = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -2408,6 +2430,7 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const imdbId = await this.fetchTmdbMovieImdbId(tmdbId);
|
const imdbId = await this.fetchTmdbMovieImdbId(tmdbId);
|
||||||
|
const art = await this.fetchTmdbPreferredArt("movie", tmdbId);
|
||||||
const meta: TmdbMeta = {
|
const meta: TmdbMeta = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: preferNlOrEnglishTitle({
|
title: preferNlOrEnglishTitle({
|
||||||
|
|
@ -2421,10 +2444,12 @@ export class MetadataService {
|
||||||
? parseInt(item.release_date.slice(0, 4), 10)
|
? parseInt(item.release_date.slice(0, 4), 10)
|
||||||
: fallbackYear,
|
: fallbackYear,
|
||||||
overview: item.overview?.trim() || englishOverview?.trim() || undefined,
|
overview: item.overview?.trim() || englishOverview?.trim() || undefined,
|
||||||
posterUrl: item.poster_path ? `${TMDB_IMAGE_BASE}${item.poster_path}` : undefined,
|
posterUrl:
|
||||||
backdropUrl: item.backdrop_path
|
art.posterUrl ||
|
||||||
? `${TMDB_BACKDROP_BASE}${item.backdrop_path}`
|
(item.poster_path ? `${TMDB_IMAGE_BASE}${item.poster_path}` : undefined),
|
||||||
: undefined,
|
backdropUrl:
|
||||||
|
art.backdropUrl ||
|
||||||
|
(item.backdrop_path ? `${TMDB_BACKDROP_BASE}${item.backdrop_path}` : undefined),
|
||||||
imdbId,
|
imdbId,
|
||||||
adult: item.adult === true,
|
adult: item.adult === true,
|
||||||
releaseDate: item.release_date || undefined,
|
releaseDate: item.release_date || undefined,
|
||||||
|
|
@ -2583,7 +2608,8 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoUrl = await this.fetchTmdbSeriesLogo(tmdbId);
|
const art = await this.fetchTmdbPreferredArt("tv", tmdbId);
|
||||||
|
const logoUrl = art.logoUrl || (await this.fetchTmdbSeriesLogo(tmdbId));
|
||||||
const imdbId = await this.fetchTmdbSeriesImdbId(tmdbId);
|
const imdbId = await this.fetchTmdbSeriesImdbId(tmdbId);
|
||||||
const meta: TmdbMeta = {
|
const meta: TmdbMeta = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|
@ -2598,10 +2624,12 @@ export class MetadataService {
|
||||||
? parseInt(item.first_air_date.slice(0, 4), 10)
|
? parseInt(item.first_air_date.slice(0, 4), 10)
|
||||||
: undefined,
|
: undefined,
|
||||||
overview: item.overview?.trim() || englishOverview?.trim() || undefined,
|
overview: item.overview?.trim() || englishOverview?.trim() || undefined,
|
||||||
posterUrl: item.poster_path ? `${TMDB_IMAGE_BASE}${item.poster_path}` : undefined,
|
posterUrl:
|
||||||
backdropUrl: item.backdrop_path
|
art.posterUrl ||
|
||||||
? `${TMDB_BACKDROP_BASE}${item.backdrop_path}`
|
(item.poster_path ? `${TMDB_IMAGE_BASE}${item.poster_path}` : undefined),
|
||||||
: undefined,
|
backdropUrl:
|
||||||
|
art.backdropUrl ||
|
||||||
|
(item.backdrop_path ? `${TMDB_BACKDROP_BASE}${item.backdrop_path}` : undefined),
|
||||||
logoUrl,
|
logoUrl,
|
||||||
imdbId,
|
imdbId,
|
||||||
adult: item.adult === true,
|
adult: item.adult === true,
|
||||||
|
|
@ -2610,6 +2638,38 @@ export class MetadataService {
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fetchTmdbPreferredArt(
|
||||||
|
kind: "movie" | "tv",
|
||||||
|
tmdbId: number
|
||||||
|
): Promise<{ posterUrl?: string; backdropUrl?: string; logoUrl?: string }> {
|
||||||
|
if (!this.tmdbApiKey) return {};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
api_key: this.tmdbApiKey,
|
||||||
|
include_image_language: "nl,en,null",
|
||||||
|
});
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.themoviedb.org/3/${kind}/${tmdbId}/images?${params}`
|
||||||
|
);
|
||||||
|
if (!res.ok) return {};
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
posters?: TmdbImageAsset[];
|
||||||
|
backdrops?: TmdbImageAsset[];
|
||||||
|
logos?: TmdbImageAsset[];
|
||||||
|
};
|
||||||
|
const poster = pickPreferredTmdbImage(data.posters ?? []);
|
||||||
|
const backdrop = pickPreferredTmdbImage(data.backdrops ?? []);
|
||||||
|
const logos = data.logos ?? [];
|
||||||
|
const wide = logos.filter((l) => (l.aspect_ratio ?? 0) >= 1.5);
|
||||||
|
const logo = pickPreferredTmdbImage(wide.length ? wide : logos);
|
||||||
|
return {
|
||||||
|
posterUrl: poster?.file_path ? `${TMDB_IMAGE_BASE}${poster.file_path}` : undefined,
|
||||||
|
backdropUrl: backdrop?.file_path
|
||||||
|
? `${TMDB_BACKDROP_BASE}${backdrop.file_path}`
|
||||||
|
: undefined,
|
||||||
|
logoUrl: logo?.file_path ? `${TMDB_LOGO_BASE}${logo.file_path}` : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async fetchTmdbSeriesImdbId(tmdbId: number): Promise<string | undefined> {
|
private async fetchTmdbSeriesImdbId(tmdbId: number): Promise<string | undefined> {
|
||||||
if (!this.tmdbApiKey) return undefined;
|
if (!this.tmdbApiKey) return undefined;
|
||||||
const params = new URLSearchParams({ api_key: this.tmdbApiKey });
|
const params = new URLSearchParams({ api_key: this.tmdbApiKey });
|
||||||
|
|
@ -2623,56 +2683,13 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchTmdbSeriesLogo(tmdbId: number): Promise<string | undefined> {
|
private async fetchTmdbSeriesLogo(tmdbId: number): Promise<string | undefined> {
|
||||||
if (!this.tmdbApiKey) return undefined;
|
const art = await this.fetchTmdbPreferredArt("tv", tmdbId);
|
||||||
const params = new URLSearchParams({
|
return art.logoUrl;
|
||||||
api_key: this.tmdbApiKey,
|
|
||||||
include_image_language: "nl,en,null",
|
|
||||||
});
|
|
||||||
const res = await fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/images?${params}`);
|
|
||||||
if (!res.ok) return undefined;
|
|
||||||
const data = (await res.json()) as {
|
|
||||||
logos?: Array<{
|
|
||||||
file_path?: string;
|
|
||||||
iso_639_1?: string | null;
|
|
||||||
aspect_ratio?: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
const logos = data.logos ?? [];
|
|
||||||
// Prefer wide clearlogos (aspect > 1.5) — tall/square logos blow up in Stremio desktop.
|
|
||||||
const wide = logos.filter((l) => (l.aspect_ratio ?? 0) >= 1.5);
|
|
||||||
const pool = wide.length ? wide : logos;
|
|
||||||
const preferred =
|
|
||||||
pool.find((l) => l.iso_639_1 === "nl") ||
|
|
||||||
pool.find((l) => l.iso_639_1 === "en") ||
|
|
||||||
pool.find((l) => !l.iso_639_1) ||
|
|
||||||
pool[0];
|
|
||||||
return preferred?.file_path ? `${TMDB_LOGO_BASE}${preferred.file_path}` : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchTmdbMovieLogo(tmdbId: number): Promise<string | undefined> {
|
private async fetchTmdbMovieLogo(tmdbId: number): Promise<string | undefined> {
|
||||||
if (!this.tmdbApiKey) return undefined;
|
const art = await this.fetchTmdbPreferredArt("movie", tmdbId);
|
||||||
const params = new URLSearchParams({
|
return art.logoUrl;
|
||||||
api_key: this.tmdbApiKey,
|
|
||||||
include_image_language: "nl,en,null",
|
|
||||||
});
|
|
||||||
const res = await fetch(`https://api.themoviedb.org/3/movie/${tmdbId}/images?${params}`);
|
|
||||||
if (!res.ok) return undefined;
|
|
||||||
const data = (await res.json()) as {
|
|
||||||
logos?: Array<{
|
|
||||||
file_path?: string;
|
|
||||||
iso_639_1?: string | null;
|
|
||||||
aspect_ratio?: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
const logos = data.logos ?? [];
|
|
||||||
const wide = logos.filter((l) => (l.aspect_ratio ?? 0) >= 1.5);
|
|
||||||
const pool = wide.length ? wide : logos;
|
|
||||||
const preferred =
|
|
||||||
pool.find((l) => l.iso_639_1 === "nl") ||
|
|
||||||
pool.find((l) => l.iso_639_1 === "en") ||
|
|
||||||
pool.find((l) => !l.iso_639_1) ||
|
|
||||||
pool[0];
|
|
||||||
return preferred?.file_path ? `${TMDB_LOGO_BASE}${preferred.file_path}` : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshMovieNl(movieId: string, tmdbId: number) {
|
private async refreshMovieNl(movieId: string, tmdbId: number) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { preferNlOrEnglishTitle } from "./service";
|
import { preferNlOrEnglishTitle, pickPreferredTmdbImage } from "./service";
|
||||||
|
|
||||||
function assert(condition: unknown, message: string): asserts condition {
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
if (!condition) throw new Error(message);
|
if (!condition) throw new Error(message);
|
||||||
|
|
@ -45,6 +45,19 @@ function run() {
|
||||||
"should keep English originals"
|
"should keep English originals"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const poster = pickPreferredTmdbImage([
|
||||||
|
{ file_path: "/de.jpg", iso_639_1: "de", vote_average: 9 },
|
||||||
|
{ file_path: "/en.jpg", iso_639_1: "en", vote_average: 7 },
|
||||||
|
{ file_path: "/nl.jpg", iso_639_1: "nl", vote_average: 5 },
|
||||||
|
]);
|
||||||
|
assert(poster?.file_path === "/nl.jpg", "should prefer NL poster over EN/DE");
|
||||||
|
|
||||||
|
const posterEn = pickPreferredTmdbImage([
|
||||||
|
{ file_path: "/de.jpg", iso_639_1: "de", vote_average: 9 },
|
||||||
|
{ file_path: "/en.jpg", iso_639_1: "en", vote_average: 7 },
|
||||||
|
]);
|
||||||
|
assert(posterEn?.file_path === "/en.jpg", "should prefer EN poster over DE");
|
||||||
|
|
||||||
console.log("title-locale tests passed");
|
console.log("title-locale tests passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue