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);
|
||||
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 }); }
|
||||
|
||||
Reference in New Issue
Block a user