feat: Diversitäts-Filter gegen Sprachregion-Überflutung, höhere vote_count-Schwelle

This commit is contained in:
2026-06-19 09:58:12 +02:00
parent 4e29c8ecfc
commit 837e3d0a4d
2 changed files with 66 additions and 14 deletions

View File

@@ -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 }); }
});

View File

@@ -476,8 +476,9 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
return (
<div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12}}>
Top 10 Filme &amp; Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
Top 10 Filme &amp; Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität<br/>
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
</div>
{/* Anbieter-Auswahl */}
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>