From a7756ba0cac17398fac9874e852688a9d7b9e60b Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 19 Jun 2026 15:40:36 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20RTL+=20Fallback=20ohne=20Jahresfilter=20?= =?UTF-8?q?bei=20kleinen=20Katalogen,=20frisches=2090-Tage-Fenster=20f?= =?UTF-8?q?=C3=BCr=20aktuelle=20Hits=20vorgezogen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/tools/media/routes.js | 60 ++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index c0c3420..46bf338 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -549,21 +549,21 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { const UNLIMITED_LANGS = new Set(['de', 'en']); const MAX_OTHER_LANGUAGE_TOTAL = 2; - function buildParams(mediaKind, minVotes) { + function buildParams(mediaKind, minVotes, dateFrom) { return { watch_region: 'DE', with_watch_providers: provider.tmdbId, with_watch_monetization_types: 'flatrate', sort_by: 'popularity.desc', 'vote_count.gte': minVotes, - ...(mediaKind === 'movie' - ? { 'primary_release_date.gte': minDate } - : { 'first_air_date.gte': minDate }), + ...(dateFrom + ? (mediaKind === 'movie' ? { 'primary_release_date.gte': dateFrom } : { 'first_air_date.gte': dateFrom }) + : {}), }; } - async function fetchPages(mediaKind, minVotes, pageCount) { - const params = buildParams(mediaKind, minVotes); + async function fetchPages(mediaKind, minVotes, pageCount, dateFrom) { + const params = buildParams(mediaKind, minVotes, dateFrom); const path = mediaKind === 'movie' ? '/discover/movie' : '/discover/tv'; const calls = []; for (let p = 1; p <= pageCount; p++) calls.push(tmdb(path, { ...params, page: p })); @@ -571,22 +571,58 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { return pages.flatMap(p => p.results || []); } - // Robuster Fallback in drei Stufen, garantiert genug Kandidaten: - // 1) hohe Schwelle (bekannte Titel), 2) mittlere, 3) niedrige (kleine Kataloge wie RTL+) + // "Ganz frisch"-Fenster: letzte 90 Tage, sehr niedrige Vote-Schwelle. + // TMDb's popularity-Wert reagiert auf kurzfristige Trend-Spitzen (Suchanfragen, + // Watchlist-Einträge etc.), das geht in der riesigen 2-Jahres-Gesamtliste aber + // unter alten Dauerbrennern unter. Durch die enge Zeitspanne kommen aktuell + // virale Titel (z.B. ein Release dieser Woche) automatisch nach vorne – + // ganz ohne zusätzliche Einzel-Abfragen pro Titel. + const freshDate = (() => { + const d = new Date(); + d.setDate(d.getDate() - 90); + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + })(); + + // Robuster Fallback in vier Stufen, garantiert genug Kandidaten: + // 1) hohe Schwelle (letzte 2 Jahre, bekannte Titel) + // 2) mittlere Schwelle (letzte 2 Jahre) + // 3) niedrige Schwelle (letzte 2 Jahre) + // 4) OHNE Jahresfilter (für kleinere Kataloge wie RTL+, die laut TMDb in den + // letzten 2 Jahren kaum neue Lizenzen bekommen haben – die zeigen wir dann + // mit ihren ältesten verfügbaren beliebten Titeln statt einer leeren Liste) async function fetchCandidates(mediaKind) { - let raw = await fetchPages(mediaKind, 300, 2); + // Schritt 1: ganz frische Titel (letzte 90 Tage, niedrige Schwelle) – + // werden später vorne in die Liste gemischt, damit aktuelle Hits Vorrang haben + const freshRaw = await fetchPages(mediaKind, 5, 1, freshDate); + const fresh = freshRaw.filter(m => !BLOCKED_LANGS.has(m.original_language)); + + // Schritt 2: normaler Pool der letzten 2 Jahre, gestaffelte Vote-Schwelle + let raw = await fetchPages(mediaKind, 300, 2, minDate); let filtered = raw.filter(m => !BLOCKED_LANGS.has(m.original_language)); if (filtered.length < 15) { - const more = await fetchPages(mediaKind, 50, 2); + const more = await fetchPages(mediaKind, 50, 2, minDate); const seen = new Set(filtered.map(m => m.id)); for (const m of more) if (!seen.has(m.id) && !BLOCKED_LANGS.has(m.original_language)) { filtered.push(m); seen.add(m.id); } } if (filtered.length < 10) { - const more = await fetchPages(mediaKind, 0, 2); + const more = await fetchPages(mediaKind, 0, 2, minDate); const seen = new Set(filtered.map(m => m.id)); for (const m of more) if (!seen.has(m.id) && !BLOCKED_LANGS.has(m.original_language)) { filtered.push(m); seen.add(m.id); } } - return filtered; + // Schritt 3: kleine Kataloge (z.B. RTL+) – TMDb hat dort oft kaum Titel + // der letzten 2 Jahre korrekt eingetragen, dann ganz ohne Jahresfilter + if (filtered.length < 10) { + const more = await fetchPages(mediaKind, 20, 2, null); + const seen = new Set(filtered.map(m => m.id)); + for (const m of more) if (!seen.has(m.id) && !BLOCKED_LANGS.has(m.original_language)) { filtered.push(m); seen.add(m.id); } + } + + // Frische Titel vorne einmischen (Duplikate raus) + const seenIds = new Set(); + const merged = []; + for (const m of fresh) { if (!seenIds.has(m.id)) { merged.push(m); seenIds.add(m.id); } } + for (const m of filtered) { if (!seenIds.has(m.id)) { merged.push(m); seenIds.add(m.id); } } + return merged; } const [movieRaw, tvRaw, genreMap] = await Promise.all([