mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(tmdb): fall back to original language for missing trailers too (#1136)
* fix(tmdb): fall back to original language for missing trailers too TMDB language-filters videos the same way it filters overviews: a Russian-only title has its trailer tagged iso_639_1=ru, so an en-US request returns no trailer — the trailer only appeared when the user switched the app language. The original-language fallback previously triggered on an empty overview only, so titles that had a translated overview but a language-only trailer never got one. detailsFallbackLanguage now also triggers when the payload has no usable YouTube trailer, and fillDetailsFromFallback fills the overview and the trailer independently — a present app-language overview is kept while the trailer is pulled from the original-language payload. The fallback fetch stays best-effort and cached per language. Tests: extended tmdb-language-fallback.spec.ts (trailer-missing trigger, independent overview/trailer fill, YouTube-only guard). Docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(tmdb): cover combined overview+trailer fallback fill Adds the missing case where the primary payload lacks both overview and trailer and the fallback supplies both — the two fill branches are independent (greptile). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df01b738c0
commit
64839e3495
4 files changed
+110
-28
No files matched your search
@@ -139,13 +139,15 @@ The `language` param derives from the app language setting
|
||||
(`Language` enum → TMDB code, e.g. `de` → `de-DE`); cache rows are keyed per
|
||||
language, so switching the app language re-fetches localized metadata.
|
||||
|
||||
TMDB does NOT fall back on missing translations — a Russian-only series
|
||||
returns empty overviews for `en-US`. When the app-language payload has no
|
||||
overview, the enrichment refetches once in the content's
|
||||
`original_language` and fills only the missing text
|
||||
(`tmdb-language-fallback.ts`): the details overview, and — via the same
|
||||
rule in `TmdbSeasonService` when a season payload carries no usable text —
|
||||
the season overview and per-episode names/overviews. Genres, credits and
|
||||
TMDB language-filters both text AND videos — a Russian-only title returns
|
||||
an empty overview and no trailer for `en-US` (its trailer is tagged
|
||||
`iso_639_1=ru`). When the app-language payload is missing either, the
|
||||
enrichment refetches once in the content's `original_language` and fills
|
||||
only the missing fields (`tmdb-language-fallback.ts`): the details
|
||||
overview and/or trailer (each independently, so a present app-language
|
||||
overview is kept while the trailer is filled), and — via the same rule in
|
||||
`TmdbSeasonService` when a season payload carries no usable text — the
|
||||
season overview and per-episode names/overviews. Genres, credits and
|
||||
artwork stay in the app language; both language rows land in the cache.
|
||||
|
||||
Trailers embed via `https://www.youtube-nocookie.com/embed/…`. YouTube
|
||||
|
||||
@@ -220,9 +220,9 @@ export class TmdbEnrichmentService {
|
||||
return details;
|
||||
}
|
||||
|
||||
// TMDB does not fall back on missing translations: a Russian-only
|
||||
// title has an empty overview in en-US. Retry once in the content's
|
||||
// original language and fill only the missing text.
|
||||
// TMDB language-filters both text and videos: a Russian-only title
|
||||
// has an empty overview AND no trailer in en-US. Retry once in the
|
||||
// content's original language and fill only the missing fields.
|
||||
const fallbackLanguage = detailsFallbackLanguage(
|
||||
details,
|
||||
this.runtime.language()
|
||||
|
||||
@@ -6,22 +6,35 @@ import {
|
||||
} from './tmdb-language-fallback';
|
||||
import { TmdbSeasonDetails } from './tmdb.types';
|
||||
|
||||
const trailer = { site: 'YouTube', key: 'abc123', type: 'Trailer' };
|
||||
const videos = { results: [trailer] };
|
||||
|
||||
describe('detailsFallbackLanguage', () => {
|
||||
it('returns the original language when the overview is empty', () => {
|
||||
expect(
|
||||
detailsFallbackLanguage(
|
||||
{ id: 1, overview: '', original_language: 'ru' },
|
||||
{ id: 1, overview: '', original_language: 'ru', videos },
|
||||
'en-US'
|
||||
)
|
||||
).toBe('ru');
|
||||
});
|
||||
|
||||
it('returns null when the overview is present', () => {
|
||||
it('returns the original language when the trailer is missing', () => {
|
||||
// Overview present, but TMDB filtered out the Russian-only trailer
|
||||
expect(
|
||||
detailsFallbackLanguage(
|
||||
{ id: 1, overview: 'Plot', original_language: 'ru' },
|
||||
'en-US'
|
||||
)
|
||||
).toBe('ru');
|
||||
});
|
||||
|
||||
it('returns null when both overview and trailer are present', () => {
|
||||
expect(
|
||||
detailsFallbackLanguage(
|
||||
{ id: 1, overview: 'Plot', original_language: 'ru', videos },
|
||||
'en-US'
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
@@ -42,8 +55,8 @@ describe('detailsFallbackLanguage', () => {
|
||||
describe('fillDetailsFromFallback', () => {
|
||||
it('fills only the missing overview', () => {
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: '', original_language: 'ru' },
|
||||
{ id: 1, overview: 'Русское описание' }
|
||||
{ id: 1, overview: '', original_language: 'ru', videos },
|
||||
{ id: 1, overview: 'Русское описание', videos }
|
||||
);
|
||||
expect(merged.overview).toBe('Русское описание');
|
||||
expect(merged.original_language).toBe('ru');
|
||||
@@ -51,11 +64,52 @@ describe('fillDetailsFromFallback', () => {
|
||||
|
||||
it('keeps an existing overview', () => {
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: 'English plot' },
|
||||
{ id: 1, overview: 'Русское описание' }
|
||||
{ id: 1, overview: 'English plot', videos },
|
||||
{ id: 1, overview: 'Русское описание', videos }
|
||||
);
|
||||
expect(merged.overview).toBe('English plot');
|
||||
});
|
||||
|
||||
it('fills both overview and trailer when the primary payload has neither', () => {
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: '', original_language: 'ru' },
|
||||
{ id: 1, overview: 'Русское описание', videos }
|
||||
);
|
||||
expect(merged.overview).toBe('Русское описание');
|
||||
expect(merged.videos?.results?.[0].key).toBe('abc123');
|
||||
});
|
||||
|
||||
it('fills the trailer when the primary payload has none', () => {
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: 'English plot' },
|
||||
{ id: 1, overview: 'Русское описание', videos }
|
||||
);
|
||||
// overview kept (present), trailer pulled from the fallback
|
||||
expect(merged.overview).toBe('English plot');
|
||||
expect(merged.videos?.results?.[0].key).toBe('abc123');
|
||||
});
|
||||
|
||||
it('keeps an existing trailer', () => {
|
||||
const own = {
|
||||
results: [{ site: 'YouTube', key: 'own', type: 'Trailer' }],
|
||||
};
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: 'Plot', videos: own },
|
||||
{ id: 1, overview: 'x', videos }
|
||||
);
|
||||
expect(merged.videos?.results?.[0].key).toBe('own');
|
||||
});
|
||||
|
||||
it('does not treat a non-YouTube video as a usable trailer', () => {
|
||||
const vimeo = {
|
||||
results: [{ site: 'Vimeo', key: 'v', type: 'Trailer' }],
|
||||
};
|
||||
const merged = fillDetailsFromFallback(
|
||||
{ id: 1, overview: 'Plot', videos: vimeo },
|
||||
{ id: 1, overview: 'x', videos }
|
||||
);
|
||||
expect(merged.videos?.results?.[0].key).toBe('abc123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('season fallback', () => {
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
import { TmdbDetails, TmdbEpisode, TmdbSeasonDetails } from './tmdb.types';
|
||||
|
||||
/**
|
||||
* TMDB text fields (overview, episode overviews) are NOT auto-translated:
|
||||
* a Russian-only series returns empty overviews for `en-US`. When the
|
||||
* app-language payload has no text, we refetch once in the content's
|
||||
* `original_language` and fill only the missing text fields — everything
|
||||
* else (genres, credits, artwork) stays in the app language.
|
||||
* TMDB filters BOTH text (overview, episode overviews) AND videos by the
|
||||
* `language` param: a Russian-only title returns an empty overview and no
|
||||
* trailer for `en-US`, because its trailer is tagged `iso_639_1=ru`. When
|
||||
* the app-language payload is missing text or a trailer, we refetch once
|
||||
* in the content's `original_language` and fill only the missing fields —
|
||||
* everything else (genres, credits, artwork) stays in the app language.
|
||||
*/
|
||||
|
||||
function hasText(value: string | null | undefined): value is string {
|
||||
return Boolean(value?.trim());
|
||||
}
|
||||
|
||||
/** True when the payload carries a usable YouTube trailer/teaser */
|
||||
function hasYoutubeTrailer(details: TmdbDetails | null): boolean {
|
||||
return (details?.videos?.results ?? []).some(
|
||||
(video) =>
|
||||
video.site === 'YouTube' &&
|
||||
Boolean(video.key) &&
|
||||
(video.type === 'Trailer' || video.type === 'Teaser')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The original language to retry with, or null when the primary payload
|
||||
* already has an overview / is in that language anyway.
|
||||
* already has both an overview and a trailer / is in that language anyway.
|
||||
*/
|
||||
export function detailsFallbackLanguage(
|
||||
details: TmdbDetails | null,
|
||||
currentLanguage: string
|
||||
): string | null {
|
||||
const original = details?.original_language?.trim();
|
||||
if (!original || hasText(details?.overview)) {
|
||||
if (!original) {
|
||||
return null;
|
||||
}
|
||||
// Trailers, like overviews, are language-filtered by TMDB — retry when
|
||||
// either is missing in the app language.
|
||||
if (hasText(details?.overview) && hasYoutubeTrailer(details)) {
|
||||
return null;
|
||||
}
|
||||
return currentLanguage.toLowerCase().startsWith(original.toLowerCase())
|
||||
@@ -29,17 +45,27 @@ export function detailsFallbackLanguage(
|
||||
: original;
|
||||
}
|
||||
|
||||
/** Fill the primary payload's empty overview from the fallback payload */
|
||||
/**
|
||||
* Fill the primary payload's missing overview and/or trailer from the
|
||||
* fallback (original-language) payload — each field independently, so a
|
||||
* present app-language overview is kept even while the trailer is filled.
|
||||
*/
|
||||
export function fillDetailsFromFallback(
|
||||
primary: TmdbDetails,
|
||||
fallback: TmdbDetails | null
|
||||
): TmdbDetails {
|
||||
if (!fallback || hasText(primary.overview)) {
|
||||
if (!fallback) {
|
||||
return primary;
|
||||
}
|
||||
return hasText(fallback.overview)
|
||||
? { ...primary, overview: fallback.overview }
|
||||
: primary;
|
||||
|
||||
let result = primary;
|
||||
if (!hasText(primary.overview) && hasText(fallback.overview)) {
|
||||
result = { ...result, overview: fallback.overview };
|
||||
}
|
||||
if (!hasYoutubeTrailer(primary) && hasYoutubeTrailer(fallback)) {
|
||||
result = { ...result, videos: fallback.videos };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user