From 7d868d77fe272d3f837fc5957ee0dbd1bab1b8e7 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 5 Jul 2026 11:53:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(tmdb):=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20best-effort=20fallback=20fetch,=20/embed/=20scope,=20names-o?= =?UTF-8?q?nly=20trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A failing original-language fallback fetch no longer costs the already-fetched primary details payload (Codex P2) — it is wrapped best-effort and the primary payload is returned on error. - The YouTube Referer shim now scopes to /embed/ paths, so regular www.youtube.com requests keep their real (missing) Referer (greptile). - seasonNeedsTextFallback also triggers when episode overviews exist but ALL episode names are empty (partially translated seasons; greptile). Co-Authored-By: Claude Fable 5 --- .../request-header-overrides.service.spec.ts | 15 +++++++++++++++ .../request-header-overrides.service.ts | 11 ++++++++--- .../src/lib/tmdb/tmdb-enrichment.service.ts | 19 +++++++++++++------ .../lib/tmdb/tmdb-language-fallback.spec.ts | 16 +++++++++++++++- .../src/lib/tmdb/tmdb-language-fallback.ts | 17 +++++++++++++---- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/apps/electron-backend/src/app/services/request-header-overrides.service.spec.ts b/apps/electron-backend/src/app/services/request-header-overrides.service.spec.ts index 3f864d44f..63ae4ce55 100644 --- a/apps/electron-backend/src/app/services/request-header-overrides.service.spec.ts +++ b/apps/electron-backend/src/app/services/request-header-overrides.service.spec.ts @@ -281,6 +281,21 @@ describe('request header overrides', () => { expect(headers['Referer']).toBeUndefined(); }); + it('does not inject the Referer for non-embed YouTube paths', async () => { + const { registerStaticHeaderShims } = await import( + './request-header-overrides.service' + ); + + registerStaticHeaderShims(); + const listener = mockOnBeforeSendHeaders.mock.calls[0][1]; + const headers = runHeaderListener( + listener, + 'https://www.youtube.com/watch?v=abc123' + ); + + expect(headers['Referer']).toBeUndefined(); + }); + it('does not inject the embed Referer for non-YouTube hosts', async () => { const { registerStaticHeaderShims } = await import( './request-header-overrides.service' diff --git a/apps/electron-backend/src/app/services/request-header-overrides.service.ts b/apps/electron-backend/src/app/services/request-header-overrides.service.ts index 6c4024a6e..9376d6a88 100644 --- a/apps/electron-backend/src/app/services/request-header-overrides.service.ts +++ b/apps/electron-backend/src/app/services/request-header-overrides.service.ts @@ -77,13 +77,18 @@ function applyYoutubeEmbedRefererShim( url: string, requestHeaders: Record ): void { - let host: string; + let parsed: URL; try { - host = new URL(url).hostname; + parsed = new URL(url); } catch { return; } - if (!YOUTUBE_EMBED_HOSTS.has(host)) { + // Scope strictly to embed player requests — www.youtube.com also + // serves regular pages that must keep their real (missing) Referer + if ( + !YOUTUBE_EMBED_HOSTS.has(parsed.hostname) || + !parsed.pathname.startsWith('/embed/') + ) { return; } diff --git a/libs/services/src/lib/tmdb/tmdb-enrichment.service.ts b/libs/services/src/lib/tmdb/tmdb-enrichment.service.ts index b9dbf5cf7..5bad35c98 100644 --- a/libs/services/src/lib/tmdb/tmdb-enrichment.service.ts +++ b/libs/services/src/lib/tmdb/tmdb-enrichment.service.ts @@ -222,12 +222,19 @@ export class TmdbEnrichmentService { if (!fallbackLanguage) { return details; } - const fallback = await this.fetchDetails( - mediaType, - tmdbId, - fallbackLanguage - ); - return fillDetailsFromFallback(details, fallback); + // Best-effort: a failing fallback fetch must never cost the + // already-fetched primary payload (cast, artwork, trailer, ...) + try { + const fallback = await this.fetchDetails( + mediaType, + tmdbId, + fallbackLanguage + ); + return fillDetailsFromFallback(details, fallback); + } catch (error) { + console.warn('TMDB fallback-language fetch failed:', error); + return details; + } } private async fetchDetails( diff --git a/libs/services/src/lib/tmdb/tmdb-language-fallback.spec.ts b/libs/services/src/lib/tmdb/tmdb-language-fallback.spec.ts index 06921f3ee..0d7e8731d 100644 --- a/libs/services/src/lib/tmdb/tmdb-language-fallback.spec.ts +++ b/libs/services/src/lib/tmdb/tmdb-language-fallback.spec.ts @@ -83,11 +83,25 @@ describe('season fallback', () => { expect( seasonNeedsTextFallback({ overview: '', - episodes: [{ episode_number: 1, overview: 'Text' }], + episodes: [ + { episode_number: 1, name: 'Ep 1', overview: 'Text' }, + ], }) ).toBe(false); }); + it('triggers when episode overviews exist but ALL names are empty', () => { + expect( + seasonNeedsTextFallback({ + overview: 'Season text', + episodes: [ + { episode_number: 1, name: '', overview: 'Text 1' }, + { episode_number: 2, name: ' ', overview: 'Text 2' }, + ], + }) + ).toBe(true); + }); + it('fills missing season overview and episode texts by number', () => { const merged = fillSeasonFromFallback(emptySeason, russianSeason); expect(merged.overview).toBe('Описание сезона'); diff --git a/libs/services/src/lib/tmdb/tmdb-language-fallback.ts b/libs/services/src/lib/tmdb/tmdb-language-fallback.ts index 8f027a5af..3623d9ee9 100644 --- a/libs/services/src/lib/tmdb/tmdb-language-fallback.ts +++ b/libs/services/src/lib/tmdb/tmdb-language-fallback.ts @@ -42,14 +42,23 @@ export function fillDetailsFromFallback( : primary; } -/** True when the season payload carries no usable text at all */ +/** + * True when the season payload carries no usable text: no overviews at + * all, or episodes whose names are ALL empty (partially translated + * seasons keep localized overviews but lose the episode names). + */ export function seasonNeedsTextFallback(season: TmdbSeasonDetails): boolean { + const episodes = season.episodes ?? []; + if ( + episodes.length > 0 && + !episodes.some((episode) => hasText(episode.name)) + ) { + return true; + } if (hasText(season.overview)) { return false; } - return !(season.episodes ?? []).some((episode) => - hasText(episode.overview) - ); + return !episodes.some((episode) => hasText(episode.overview)); } /**