diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index deb2f33..8701bd1 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -534,8 +534,6 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { const provider = STREAMING_PROVIDERS.find(p => p.id === req.params.providerId); if (!provider) return res.status(404).json({ error: 'Unbekannter Anbieter' }); - // Ergebnis 6h cachen – die Trending+Provider-Checks machen mehrere - // zusätzliche TMDb-Calls, das muss nicht bei jedem Seitenaufruf neu laufen const streamCacheKey = `streaming-top10:${provider.id}`; const cachedResult = cacheGet(streamCacheKey); if (cachedResult !== undefined) return res.json(cachedResult); @@ -544,49 +542,12 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { const minYear = new Date().getFullYear() - 1; const minDate = `${minYear}-01-01`; - // ── Strategie 1: TMDb "trending diese Woche" + Provider-Check pro Titel ── - // /trending unterstützt KEINEN watch_provider-Filter, deshalb: erst die - // wöchentlich angesagten Titel holen (kleine Liste, ~20 Treffer), dann für - // jeden einzeln per /watch/providers prüfen ob er beim gewählten Anbieter - // in DE verfügbar ist. So tauchen frische virale Hits (z.B. ein Titel der - // gerade erst diese Woche durchstartet) sofort auf, statt erst Wochen - // später über die reine Gesamt-Popularität reinzurutschen. - async function getTrendingForProvider(mediaKind) { - try { - const path = mediaKind === 'movie' ? '/trending/movie/week' : '/trending/tv/week'; - const trending = await tmdb(path, {}); - const candidates = (trending.results || []).slice(0, 20); - - const checked = await Promise.all(candidates.map(async item => { - try { - const provPath = mediaKind === 'movie' - ? `/movie/${item.id}/watch/providers` - : `/tv/${item.id}/watch/providers`; - const provData = await tmdb(provPath, {}); - const deFlat = provData.results?.DE?.flatrate || []; - const matchIds = provider.tmdbId.split('|').map(Number); - const available = deFlat.some(p => matchIds.includes(p.provider_id)); - return available ? item : null; - } catch { return null; } - })); - return checked.filter(Boolean); - } catch { return []; } // Trending-Strategie ist nur eine Ergänzung – bei Fehler einfach leer - } - - // Höhere vote_count-Schwelle filtert Nischentitel jeder Herkunft heraus. - // Bei kleineren Katalogen (z.B. RTL+) reicht das oft nicht für 10 Treffer – - // dann wird mit niedrigerer Schwelle nachgeladen (siehe fetchWithFallback unten). - const MIN_VOTES = 300; - const FALLBACK_MIN_VOTES = 50; - - // Sprachgewichtung statt harter Quote: Deutsch & Englisch sind unbegrenzt - // erlaubt (das schaut hierzulande eh fast jeder). Für alle anderen Sprachen - // gilt ein Deckel von 2 Plätzen in den Top 10 – AUSSER ein Titel hat eine - // sehr hohe vote_count (= ist ein echter, in DE bekannter Crossover-Hit wie - // Squid Game), dann zählt er nicht gegen den Deckel. + // Deutsch & Englisch sind unbegrenzt erlaubt. Alle anderen Sprachen + // zusammen (Koreanisch, Japanisch, Spanisch, etc.) dürfen max. 2 Plätze + // in den Top 10 belegen – KEINE Ausnahme mehr für "hohe vote_count", + // weil genau das die Liste mit populären Anime/K-Dramen geflutet hat. const UNLIMITED_LANGS = new Set(['de', 'en']); - const MAX_PER_OTHER_LANGUAGE = 2; - const CROSSOVER_HIT_VOTE_THRESHOLD = 3000; // ab hier zählt ein Titel als "bekannt genug" + const MAX_OTHER_LANGUAGE_TOTAL = 2; function buildParams(mediaKind, minVotes) { return { @@ -601,76 +562,59 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { }; } - async function fetchThreePages(mediaKind, minVotes) { + async function fetchPages(mediaKind, minVotes, pageCount) { const params = buildParams(mediaKind, minVotes); const path = mediaKind === 'movie' ? '/discover/movie' : '/discover/tv'; - const [p1, p2, p3] = await Promise.all([ - tmdb(path, { ...params, page: 1 }), - tmdb(path, { ...params, page: 2 }), - tmdb(path, { ...params, page: 3 }), - ]); - return [...(p1.results||[]), ...(p2.results||[]), ...(p3.results||[])]; + const calls = []; + for (let p = 1; p <= pageCount; p++) calls.push(tmdb(path, { ...params, page: p })); + const pages = await Promise.all(calls); + return pages.flatMap(p => p.results || []); } - // Erst Trending+Provider-Check (frische Hits), dann Discover mit hoher - // Schwelle, bei zu wenig Ergebnissen mit niedrigerer Schwelle nachladen - async function fetchWithFallback(mediaKind) { - const trendingHits = await getTrendingForProvider(mediaKind); - - let raw = await fetchThreePages(mediaKind, MIN_VOTES); + // Robuster Fallback in drei Stufen, garantiert genug Kandidaten: + // 1) hohe Schwelle (bekannte Titel), 2) mittlere, 3) niedrige (kleine Kataloge wie RTL+) + async function fetchCandidates(mediaKind) { + let raw = await fetchPages(mediaKind, 300, 2); let filtered = raw.filter(m => !BLOCKED_LANGS.has(m.original_language)); - - // Trending-Treffer vorne einfügen (Duplikate vermeiden) - const seenIds = new Set(filtered.map(m => m.id)); - const merged = []; - for (const t of trendingHits) { - if (!seenIds.has(t.id) && !BLOCKED_LANGS.has(t.original_language)) { - merged.push(t); - seenIds.add(t.id); - } + if (filtered.length < 15) { + const more = await fetchPages(mediaKind, 50, 2); + 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); } } - merged.push(...filtered); - filtered = merged; - if (filtered.length < 10) { - const fallback = await fetchThreePages(mediaKind, FALLBACK_MIN_VOTES); - for (const m of fallback) { - if (!BLOCKED_LANGS.has(m.original_language) && !seenIds.has(m.id)) { - filtered.push(m); - seenIds.add(m.id); - } - } + const more = await fetchPages(mediaKind, 0, 2); + 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; } const [movieRaw, tvRaw, genreMap] = await Promise.all([ - fetchWithFallback('movie'), - fetchWithFallback('tv'), + fetchCandidates('movie'), + fetchCandidates('tv'), getGenreMap(), ]); - // Liste bleibt nach Popularität sortiert. Deutsch/Englisch immer erlaubt. - // Andere Sprachen: max. 2 Plätze, außer der Titel ist ein nachweislicher - // Crossover-Hit (sehr hohe vote_count) – der zählt dann nicht gegen den Deckel. + // Liste bleibt nach Popularität sortiert. DE/EN unbegrenzt, alle anderen + // Sprachen zusammen maximal 2 Plätze in den Top 10. function applyLanguageWeighting(rawList) { - const otherLangCount = {}; + let otherCount = 0; const picked = []; const overflow = []; for (const item of rawList) { const lang = item.original_language || 'xx'; - const isUnlimited = UNLIMITED_LANGS.has(lang); - const isCrossoverHit = (item.vote_count || 0) >= CROSSOVER_HIT_VOTE_THRESHOLD; - if (isUnlimited || isCrossoverHit) { + if (UNLIMITED_LANGS.has(lang)) { picked.push(item); - } else if ((otherLangCount[lang] || 0) < MAX_PER_OTHER_LANGUAGE) { + } else if (otherCount < MAX_OTHER_LANGUAGE_TOTAL) { picked.push(item); - otherLangCount[lang] = (otherLangCount[lang] || 0) + 1; + otherCount++; } else { overflow.push(item); } if (picked.length >= 10) break; } + // Falls weniger als 10 zusammenkommen (z.B. sehr kleiner Katalog), + // mit den nächstbeliebtesten auffüllen – auch über den Sprachdeckel hinaus for (const item of overflow) { if (picked.length >= 10) break; picked.push(item); @@ -697,11 +641,7 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { const result = { provider: { id: provider.id, label: provider.label }, movies, tvShows, - filterInfo: { - minYear, - minVotes: MIN_VOTES, - maxPerOtherLanguage: MAX_PER_OTHER_LANGUAGE, - }, + filterInfo: { minYear, maxOtherLanguageTotal: MAX_OTHER_LANGUAGE_TOTAL }, }; cacheSet(streamCacheKey, result, 6 * 60 * 60 * 1000); // 6h Cache res.json(result); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index d974c8b..a69facb 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -478,7 +478,7 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität (Näherung, keine offiziellen Anbieter-Daten)
- Nur wirklich bekannte Titel · Deutsch & Englisch unbegrenzt, andere Sprachen max. 2 Plätze – echte Hits wie „Squid Game" rutschen trotzdem rein + Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze in den Top 10
{/* Anbieter-Auswahl */}