feat: Neuerscheinungen-Zeitfenster wird stufenweise vergrößert (21/45/90/180 Tage) bei wenigen TMDb-Treffern

This commit is contained in:
2026-06-20 01:11:06 +02:00
parent 1aacd8a192
commit afd2174f0e
2 changed files with 48 additions and 27 deletions

View File

@@ -549,33 +549,53 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
const cached = cacheGet(cacheKey);
if (cached !== undefined) return res.json(cached);
// Enges 21-Tage-Fenster, damit "Neuerscheinungen" sich spürbar von "Top 10"
// (letzte 2 Jahre) unterscheidet, statt fast identisch zu sein
const since = (() => {
const d = new Date();
d.setDate(d.getDate() - 21);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
})();
const today = (() => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
})();
function daysAgo(n) {
const d = new Date();
d.setDate(d.getDate() - n);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
// Innerhalb des 21-Tage-Fensters nach POPULARITÄT sortieren (nicht nach
// Datum) sonst landen wenig beachtete Tagesveröffentlichungen vor
// tatsächlich angesagten Titeln. "Neuerscheinungen" heißt: neu UND beachtet.
function baseParams(mediaKind) {
function baseParams(mediaKind, sinceDate) {
return {
watch_region: 'DE',
with_watch_providers: provider.tmdbId,
with_watch_monetization_types: 'flatrate',
sort_by: 'popularity.desc',
...(mediaKind === 'movie'
? { 'primary_release_date.gte': since, 'primary_release_date.lte': today }
: { 'first_air_date.gte': since, 'first_air_date.lte': today }),
? { 'primary_release_date.gte': sinceDate, 'primary_release_date.lte': today }
: { 'first_air_date.gte': sinceDate, 'first_air_date.lte': today }),
};
}
async function fetchWindow(mediaKind, sinceDate) {
const path = mediaKind === 'movie' ? '/discover/movie' : '/discover/tv';
const [p1, p2] = await Promise.all([
tmdb(path, { ...baseParams(mediaKind, sinceDate), page: 1 }),
tmdb(path, { ...baseParams(mediaKind, sinceDate), page: 2 }),
]);
return [...(p1.results||[]), ...(p2.results||[])].filter(m => !BLOCKED_LANGS.has(m.original_language));
}
// Manche Anbieter (z.B. Disney+, RTL+) haben bei TMDb nur sehr wenige Titel
// mit korrekt erfasstem Veröffentlichungsdatum + Verfügbarkeit verknüpft.
// Statt einer fast leeren Liste wird das Zeitfenster schrittweise vergrößert,
// bis genug Titel zusammenkommen der tatsächlich verwendete Zeitraum wird
// im Ergebnis ehrlich mitgegeben (kein versteckter Fake-"neu").
const WINDOW_STAGES = [21, 45, 90, 180];
async function fetchCandidatesWithExpansion(mediaKind) {
let usedWindow = WINDOW_STAGES[0];
let result = await fetchWindow(mediaKind, daysAgo(WINDOW_STAGES[0]));
for (let i = 1; i < WINDOW_STAGES.length && result.length < 6; i++) {
usedWindow = WINDOW_STAGES[i];
result = await fetchWindow(mediaKind, daysAgo(WINDOW_STAGES[i]));
}
return { items: result, usedWindow };
}
// Gleiche Sprachgewichtung wie bei Top 10: Deutsch/Englisch unbegrenzt,
// alle anderen Sprachen zusammen max. 2 Plätze
const UNLIMITED_LANGS = new Set(['de', 'en']);
@@ -603,19 +623,12 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
return picked.slice(0, 10);
}
const [movieP1, movieP2, tvP1, tvP2, genreMap] = await Promise.all([
tmdb('/discover/movie', { ...baseParams('movie'), page: 1 }),
tmdb('/discover/movie', { ...baseParams('movie'), page: 2 }),
tmdb('/discover/tv', { ...baseParams('tv'), page: 1 }),
tmdb('/discover/tv', { ...baseParams('tv'), page: 2 }),
const [movieResult, tvResult, genreMap] = await Promise.all([
fetchCandidatesWithExpansion('movie'),
fetchCandidatesWithExpansion('tv'),
getGenreMap(),
]);
const movieRaw = [...(movieP1.results||[]), ...(movieP2.results||[])]
.filter(m => !BLOCKED_LANGS.has(m.original_language));
const tvRaw = [...(tvP1.results||[]), ...(tvP2.results||[])]
.filter(m => !BLOCKED_LANGS.has(m.original_language));
// Titel die bereits in der (gecachten) Top-10-Liste dieses Anbieters stehen
// aus den Neuerscheinungen ausschließen, damit beide Reiter sich nicht
// überschneiden, selbst wenn sich die Zeitfenster teilweise überlappen
@@ -623,10 +636,10 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
const top10MovieIds = new Set((top10Cached?.movies || []).map(m => m.id));
const top10TvIds = new Set((top10Cached?.tvShows || []).map(m => m.id));
const movies = applyLanguageWeighting(movieRaw.filter(m => !top10MovieIds.has(m.id)))
const movies = applyLanguageWeighting(movieResult.items.filter(m => !top10MovieIds.has(m.id)))
.map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' }));
const tvShows = applyLanguageWeighting(tvRaw.filter(m => !top10TvIds.has(m.id)))
const tvShows = applyLanguageWeighting(tvResult.items.filter(m => !top10TvIds.has(m.id)))
.map((m, i) => ({
id: m.id, media_type: 'tv', rank: i + 1,
title: m.name || m.original_name,
@@ -639,7 +652,12 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
origin_country: m.origin_country || [],
}));
const result = { provider: { id: provider.id, label: provider.label }, movies, tvShows, since };
const result = {
provider: { id: provider.id, label: provider.label },
movies, tvShows,
movieWindowDays: movieResult.usedWindow,
tvWindowDays: tvResult.usedWindow,
};
cacheSet(cacheKey, result, 6 * 60 * 60 * 1000);
res.json(result);
} catch(e) { res.status(500).json({ error: e.message }); }

View File

@@ -505,7 +505,10 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
</div>
) : (
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
Was in den letzten 60 Tagen neu beim Anbieter erschienen ist (DE), sortiert nach Beliebtheit<br/>
Neu erschienen beim Anbieter (DE), sortiert nach Beliebtheit Filme der letzten {data?.movieWindowDays || 21} Tage, Serien der letzten {data?.tvWindowDays || 21} Tage<br/>
{(data?.movieWindowDays > 21 || data?.tvWindowDays > 21) && (
<span style={{color:'#f59e0b'}}>Zeitraum wurde vergrößert, da dieser Anbieter laut TMDb in den letzten Wochen wenige neue Titel hat<br/></span>
)}
Deutsch &amp; Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze
</div>
)}