feat: Diversitäts-Filter gegen Sprachregion-Überflutung, höhere vote_count-Schwelle
This commit is contained in:
@@ -531,12 +531,22 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => {
|
|||||||
const minYear = new Date().getFullYear() - 1;
|
const minYear = new Date().getFullYear() - 1;
|
||||||
const minDate = `${minYear}-01-01`;
|
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 = {
|
const movieParams = {
|
||||||
watch_region: 'DE',
|
watch_region: 'DE',
|
||||||
with_watch_providers: provider.tmdbId,
|
with_watch_providers: provider.tmdbId,
|
||||||
with_watch_monetization_types: 'flatrate',
|
with_watch_monetization_types: 'flatrate',
|
||||||
sort_by: 'popularity.desc',
|
sort_by: 'popularity.desc',
|
||||||
'vote_count.gte': 10,
|
'vote_count.gte': MIN_VOTES,
|
||||||
'primary_release_date.gte': minDate,
|
'primary_release_date.gte': minDate,
|
||||||
};
|
};
|
||||||
const tvParams = {
|
const tvParams = {
|
||||||
@@ -544,24 +554,57 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => {
|
|||||||
with_watch_providers: provider.tmdbId,
|
with_watch_providers: provider.tmdbId,
|
||||||
with_watch_monetization_types: 'flatrate',
|
with_watch_monetization_types: 'flatrate',
|
||||||
sort_by: 'popularity.desc',
|
sort_by: 'popularity.desc',
|
||||||
'vote_count.gte': 10,
|
'vote_count.gte': MIN_VOTES,
|
||||||
'first_air_date.gte': minDate,
|
'first_air_date.gte': minDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [movieResults, tvResults, genreMap] = await Promise.all([
|
// 3 Seiten (≈60 Titel) holen, damit nach der Diversitäts-Deckelung noch genug übrig bleibt
|
||||||
tmdb('/discover/movie', movieParams),
|
const [movieP1, movieP2, movieP3, tvP1, tvP2, tvP3, genreMap] = await Promise.all([
|
||||||
tmdb('/discover/tv', tvParams),
|
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(),
|
getGenreMap(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const movies = (movieResults.results || [])
|
// Diversitäts-Deckelung anwenden: Liste bleibt nach Popularität sortiert,
|
||||||
.filter(m => !BLOCKED_LANGS.has(m.original_language))
|
// aber sobald eine Sprache MAX_PER_LANGUAGE erreicht hat, werden weitere
|
||||||
.slice(0, 10)
|
// 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' }));
|
.map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' }));
|
||||||
|
|
||||||
const tvShows = (tvResults.results || [])
|
const tvShows = applyDiversityCap(tvRaw)
|
||||||
.filter(m => !BLOCKED_LANGS.has(m.original_language))
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((m, i) => ({
|
.map((m, i) => ({
|
||||||
id: m.id, media_type: 'tv', rank: i + 1,
|
id: m.id, media_type: 'tv', rank: i + 1,
|
||||||
title: m.name || m.original_name,
|
title: m.name || m.original_name,
|
||||||
@@ -574,7 +617,15 @@ router.get('/streaming/:providerId', authenticate, async (req, res) => {
|
|||||||
origin_country: m.origin_country || [],
|
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 }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -476,8 +476,9 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12}}>
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
|
||||||
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<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>
|
</div>
|
||||||
{/* Anbieter-Auswahl */}
|
{/* Anbieter-Auswahl */}
|
||||||
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
||||||
|
|||||||
Reference in New Issue
Block a user