From 837e3d0a4da861230f9c30a04dfad37cc2ca2866 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 19 Jun 2026 09:58:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Diversit=C3=A4ts-Filter=20gegen=20Sprac?= =?UTF-8?q?hregion-=C3=9Cberflutung,=20h=C3=B6here=20vote=5Fcount-Schwelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/tools/media/routes.js | 75 ++++++++++++++++++++++++++----- frontend/src/tools/media.jsx | 5 ++- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 4e2df6b..a9615e7 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -531,12 +531,22 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { const minYear = new Date().getFullYear() - 1; const minDate = `${minYear}-01-01`; + // Höhere vote_count-Schwelle filtert Nischentitel jeder Herkunft heraus + // (nicht nach Sprache diskriminierend – ein Nischenfilm aus den USA fliegt genauso raus + // wie ein Nischenfilm aus Korea; nur wirklich bekannte Titel bleiben drin) + const MIN_VOTES = 300; + // Zusätzliche Diversitäts-Deckelung: max. 3 Titel je Originalsprache in den Top 10, + // damit eine einzelne Produktionsregion (z.B. durch viele ähnliche K-Dramen) die Liste + // nicht komplett dominiert – ECHTE Hits (z.B. Squid Game) rutschen wegen hoher vote_count + // ohnehin weit nach oben und werden davon nicht ausgebremst. + const MAX_PER_LANGUAGE = 3; + const movieParams = { watch_region: 'DE', with_watch_providers: provider.tmdbId, with_watch_monetization_types: 'flatrate', sort_by: 'popularity.desc', - 'vote_count.gte': 10, + 'vote_count.gte': MIN_VOTES, 'primary_release_date.gte': minDate, }; const tvParams = { @@ -544,24 +554,57 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { with_watch_providers: provider.tmdbId, with_watch_monetization_types: 'flatrate', sort_by: 'popularity.desc', - 'vote_count.gte': 10, + 'vote_count.gte': MIN_VOTES, 'first_air_date.gte': minDate, }; - const [movieResults, tvResults, genreMap] = await Promise.all([ - tmdb('/discover/movie', movieParams), - tmdb('/discover/tv', tvParams), + // 3 Seiten (≈60 Titel) holen, damit nach der Diversitäts-Deckelung noch genug übrig bleibt + const [movieP1, movieP2, movieP3, tvP1, tvP2, tvP3, genreMap] = await Promise.all([ + tmdb('/discover/movie', { ...movieParams, page: 1 }), + tmdb('/discover/movie', { ...movieParams, page: 2 }), + tmdb('/discover/movie', { ...movieParams, page: 3 }), + tmdb('/discover/tv', { ...tvParams, page: 1 }), + tmdb('/discover/tv', { ...tvParams, page: 2 }), + tmdb('/discover/tv', { ...tvParams, page: 3 }), getGenreMap(), ]); - const movies = (movieResults.results || []) - .filter(m => !BLOCKED_LANGS.has(m.original_language)) - .slice(0, 10) + // Diversitäts-Deckelung anwenden: Liste bleibt nach Popularität sortiert, + // aber sobald eine Sprache MAX_PER_LANGUAGE erreicht hat, werden weitere + // Titel dieser Sprache übersprungen (sie können trotzdem auftauchen sobald + // ein Slot durch erschöpfte andere Sprachen frei bleibt – siehe Fallback unten). + function applyDiversityCap(rawList) { + const langCount = {}; + const picked = []; + const overflow = []; + for (const item of rawList) { + const lang = item.original_language || 'xx'; + if ((langCount[lang] || 0) < MAX_PER_LANGUAGE) { + picked.push(item); + langCount[lang] = (langCount[lang] || 0) + 1; + } else { + overflow.push(item); // falls 10 nicht erreicht werden, später auffüllen + } + if (picked.length >= 10) break; + } + // Falls durch die Deckelung weniger als 10 zusammenkommen, mit den + // nächstbeliebtesten Titeln auffüllen (auch wenn Deckel überschritten wird) + for (const item of overflow) { + if (picked.length >= 10) break; + picked.push(item); + } + return picked.slice(0, 10); + } + + const movieRaw = [...(movieP1.results||[]), ...(movieP2.results||[]), ...(movieP3.results||[])] + .filter(m => !BLOCKED_LANGS.has(m.original_language)); + const tvRaw = [...(tvP1.results||[]), ...(tvP2.results||[]), ...(tvP3.results||[])] + .filter(m => !BLOCKED_LANGS.has(m.original_language)); + + const movies = applyDiversityCap(movieRaw) .map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' })); - const tvShows = (tvResults.results || []) - .filter(m => !BLOCKED_LANGS.has(m.original_language)) - .slice(0, 10) + const tvShows = applyDiversityCap(tvRaw) .map((m, i) => ({ id: m.id, media_type: 'tv', rank: i + 1, title: m.name || m.original_name, @@ -574,7 +617,15 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => { origin_country: m.origin_country || [], })); - res.json({ provider: { id: provider.id, label: provider.label }, movies, tvShows }); + res.json({ + provider: { id: provider.id, label: provider.label }, + movies, tvShows, + filterInfo: { + minYear, + minVotes: MIN_VOTES, + maxPerLanguage: MAX_PER_LANGUAGE, + }, + }); } catch(e) { res.status(500).json({ error: e.message }); } }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index e16ecb6..cf6bc0b 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -476,8 +476,9 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp, return (
-
- Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität +
+ Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität
+ Nur wirklich bekannte Titel (Mindestanzahl Bewertungen) · max. 3 Titel je Sprache, damit eine Region die Liste nicht überflutet – echte Hits wie z.B. „Squid Game" rutschen trotzdem ganz nach oben
{/* Anbieter-Auswahl */}