feat: Neuerscheinungen-Zeitfenster wird stufenweise vergrößert (21/45/90/180 Tage) bei wenigen TMDb-Treffern
This commit is contained in:
@@ -549,33 +549,53 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
|
|||||||
const cached = cacheGet(cacheKey);
|
const cached = cacheGet(cacheKey);
|
||||||
if (cached !== undefined) return res.json(cached);
|
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 today = (() => {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
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
|
function baseParams(mediaKind, sinceDate) {
|
||||||
// Datum) – sonst landen wenig beachtete Tagesveröffentlichungen vor
|
|
||||||
// tatsächlich angesagten Titeln. "Neuerscheinungen" heißt: neu UND beachtet.
|
|
||||||
function baseParams(mediaKind) {
|
|
||||||
return {
|
return {
|
||||||
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',
|
||||||
...(mediaKind === 'movie'
|
...(mediaKind === 'movie'
|
||||||
? { 'primary_release_date.gte': since, 'primary_release_date.lte': today }
|
? { 'primary_release_date.gte': sinceDate, 'primary_release_date.lte': today }
|
||||||
: { 'first_air_date.gte': since, 'first_air_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,
|
// Gleiche Sprachgewichtung wie bei Top 10: Deutsch/Englisch unbegrenzt,
|
||||||
// alle anderen Sprachen zusammen max. 2 Plätze
|
// alle anderen Sprachen zusammen max. 2 Plätze
|
||||||
const UNLIMITED_LANGS = new Set(['de', 'en']);
|
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);
|
return picked.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [movieP1, movieP2, tvP1, tvP2, genreMap] = await Promise.all([
|
const [movieResult, tvResult, genreMap] = await Promise.all([
|
||||||
tmdb('/discover/movie', { ...baseParams('movie'), page: 1 }),
|
fetchCandidatesWithExpansion('movie'),
|
||||||
tmdb('/discover/movie', { ...baseParams('movie'), page: 2 }),
|
fetchCandidatesWithExpansion('tv'),
|
||||||
tmdb('/discover/tv', { ...baseParams('tv'), page: 1 }),
|
|
||||||
tmdb('/discover/tv', { ...baseParams('tv'), page: 2 }),
|
|
||||||
getGenreMap(),
|
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
|
// Titel die bereits in der (gecachten) Top-10-Liste dieses Anbieters stehen
|
||||||
// aus den Neuerscheinungen ausschließen, damit beide Reiter sich nicht
|
// aus den Neuerscheinungen ausschließen, damit beide Reiter sich nicht
|
||||||
// überschneiden, selbst wenn sich die Zeitfenster teilweise überlappen
|
// ü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 top10MovieIds = new Set((top10Cached?.movies || []).map(m => m.id));
|
||||||
const top10TvIds = new Set((top10Cached?.tvShows || []).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' }));
|
.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) => ({
|
.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,
|
||||||
@@ -639,7 +652,12 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => {
|
|||||||
origin_country: m.origin_country || [],
|
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);
|
cacheSet(cacheKey, result, 6 * 60 * 60 * 1000);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
|||||||
@@ -505,7 +505,10 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
|
<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 & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze
|
Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user