feat: media – portal modal scroll fix, alle Kinofilme, KW-Neustarts, Top-Badges, GlobalSearch
This commit is contained in:
@@ -23,7 +23,7 @@ async function tmdb(path, params = {}) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Genre-Cache (in-memory, wird bei erstem Aufruf befüllt)
|
||||
// Genre-Cache
|
||||
let genreCache = null;
|
||||
async function getGenreMap() {
|
||||
if (genreCache) return genreCache;
|
||||
@@ -33,109 +33,139 @@ async function getGenreMap() {
|
||||
return genreCache;
|
||||
}
|
||||
|
||||
// FSK aus release_dates extrahieren
|
||||
function extractFSK(releaseDates) {
|
||||
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
|
||||
if (!de) return null;
|
||||
const cert = de.release_dates.find(r => r.certification)?.certification;
|
||||
return cert || null;
|
||||
return de.release_dates.find(r => r.certification)?.certification || null;
|
||||
}
|
||||
|
||||
// Einen Film anreichern (Genres + FSK aus append_to_response)
|
||||
function enrichMovie(movie, genreMap, releaseDates) {
|
||||
// Alle Seiten einer TMDb-Liste holen (bis max. maxPages)
|
||||
async function fetchAllPages(path, maxPages = 5) {
|
||||
const first = await tmdb(path, { page: 1 });
|
||||
const total = Math.min(first.total_pages || 1, maxPages);
|
||||
let results = [...first.results];
|
||||
const extra = await Promise.all(
|
||||
Array.from({ length: total - 1 }, (_, i) => tmdb(path, { page: i + 2 }))
|
||||
);
|
||||
for (const p of extra) results = results.concat(p.results);
|
||||
return results;
|
||||
}
|
||||
|
||||
function enrichMovie(movie, genreMap, fsk, rank) {
|
||||
return {
|
||||
id: movie.id,
|
||||
title: movie.title || movie.original_title,
|
||||
original_title: movie.original_title,
|
||||
id: movie.id,
|
||||
title: movie.title || movie.original_title,
|
||||
original_title: movie.original_title,
|
||||
original_language: movie.original_language,
|
||||
poster_path: movie.poster_path,
|
||||
backdrop_path: movie.backdrop_path,
|
||||
overview: movie.overview,
|
||||
release_date: movie.release_date,
|
||||
vote_average: movie.vote_average,
|
||||
genres: (movie.genre_ids || movie.genres?.map(g=>g.id) || [])
|
||||
.map(id => genreMap[id]).filter(Boolean),
|
||||
fsk: extractFSK(releaseDates),
|
||||
poster_path: movie.poster_path,
|
||||
overview: movie.overview,
|
||||
release_date: movie.release_date,
|
||||
vote_average: movie.vote_average,
|
||||
genres: (movie.genre_ids || movie.genres?.map(g => g.id) || [])
|
||||
.map(id => genreMap[id]).filter(Boolean),
|
||||
fsk: fsk || null,
|
||||
rank: rank || null,
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/tools/media/now-playing
|
||||
// FSK für eine Liste von Filmen parallel holen
|
||||
async function withFSK(movies, genreMap, topN = 10) {
|
||||
return Promise.all(movies.map(async (m, i) => {
|
||||
let fsk = null;
|
||||
try {
|
||||
const detail = await tmdb(`/movie/${m.id}/release_dates`);
|
||||
fsk = extractFSK(detail);
|
||||
} catch (_) {}
|
||||
return enrichMovie(m, genreMap, fsk, i < topN ? i + 1 : null);
|
||||
}));
|
||||
}
|
||||
|
||||
// GET /api/tools/media/now-playing – alles was aktuell in DE läuft
|
||||
router.get('/now-playing', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [listData, genreMap] = await Promise.all([
|
||||
tmdb('/movie/now_playing'),
|
||||
const [all, genreMap] = await Promise.all([
|
||||
fetchAllPages('/movie/now_playing', 3),
|
||||
getGenreMap(),
|
||||
]);
|
||||
// Türkische Produktionen rausfiltern, max 10
|
||||
const filtered = listData.results
|
||||
.filter(m => m.original_language !== 'tr')
|
||||
.slice(0, 10);
|
||||
|
||||
// FSK für jeden Film parallel holen
|
||||
const withFSK = await Promise.all(filtered.map(async (m, idx) => {
|
||||
try {
|
||||
const detail = await tmdb(`/movie/${m.id}/release_dates`);
|
||||
return { ...enrichMovie(m, genreMap, detail), rank: idx + 1 };
|
||||
} catch {
|
||||
return { ...enrichMovie(m, genreMap, null), rank: idx + 1 };
|
||||
}
|
||||
}));
|
||||
res.json(withFSK);
|
||||
const filtered = all.filter(m => m.original_language !== 'tr');
|
||||
const enriched = await withFSK(filtered, genreMap, 10);
|
||||
res.json(enriched);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tools/media/upcoming
|
||||
// GET /api/tools/media/upcoming – wochenweise Neustarts, alle verfügbaren
|
||||
router.get('/upcoming', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [listData, genreMap] = await Promise.all([
|
||||
tmdb('/movie/upcoming'),
|
||||
const [all, genreMap] = await Promise.all([
|
||||
fetchAllPages('/movie/upcoming', 4),
|
||||
getGenreMap(),
|
||||
]);
|
||||
const filtered = listData.results
|
||||
.filter(m => m.original_language !== 'tr')
|
||||
.slice(0, 10);
|
||||
const filtered = all.filter(m => m.original_language !== 'tr');
|
||||
|
||||
const withFSK = await Promise.all(filtered.map(async (m, idx) => {
|
||||
try {
|
||||
const detail = await tmdb(`/movie/${m.id}/release_dates`);
|
||||
return { ...enrichMovie(m, genreMap, detail), rank: idx + 1 };
|
||||
} catch {
|
||||
return { ...enrichMovie(m, genreMap, null), rank: idx + 1 };
|
||||
}
|
||||
}));
|
||||
res.json(withFSK);
|
||||
// Deduplizieren nach ID
|
||||
const seen = new Set();
|
||||
const unique = filtered.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });
|
||||
|
||||
// Nach Erscheinungsdatum sortieren
|
||||
unique.sort((a, b) => (a.release_date || '').localeCompare(b.release_date || ''));
|
||||
|
||||
// Wochenweise gruppieren (Montag der jeweiligen Woche als Key)
|
||||
const groups = {};
|
||||
for (const m of unique) {
|
||||
if (!m.release_date) continue;
|
||||
const d = new Date(m.release_date);
|
||||
// Montag der Woche berechnen
|
||||
const day = d.getDay(); // 0=So
|
||||
const diff = (day === 0 ? -6 : 1 - day);
|
||||
const mon = new Date(d);
|
||||
mon.setDate(d.getDate() + diff);
|
||||
const key = mon.toISOString().slice(0, 10);
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(m);
|
||||
}
|
||||
|
||||
// Anreichern: nur die erste Gruppe bekommt FSK (spart API-Calls), Rest ohne
|
||||
const result = {};
|
||||
let globalRank = 0;
|
||||
for (const [week, movies] of Object.entries(groups)) {
|
||||
result[week] = await Promise.all(movies.map(async m => {
|
||||
globalRank++;
|
||||
let fsk = null;
|
||||
try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
|
||||
return enrichMovie(m, genreMap, fsk, null);
|
||||
}));
|
||||
}
|
||||
res.json(result); // { "2026-06-02": [...], "2026-06-09": [...], ... }
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tools/media/movie/:id – Details + Credits für Modal
|
||||
// GET /api/tools/media/movie/:id – Detail + Credits für Modal
|
||||
router.get('/movie/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [detail, genreMap] = await Promise.all([
|
||||
tmdb(`/movie/${req.params.id}`, { append_to_response: 'credits,release_dates' }),
|
||||
getGenreMap(),
|
||||
]);
|
||||
const cast = (detail.credits?.cast || []).slice(0, 8).map(a => a.name);
|
||||
const director = detail.credits?.crew?.find(c => c.job === 'Director')?.name || null;
|
||||
res.json({
|
||||
id: detail.id,
|
||||
title: detail.title || detail.original_title,
|
||||
id: detail.id,
|
||||
title: detail.title || detail.original_title,
|
||||
original_title: detail.original_title,
|
||||
tagline: detail.tagline,
|
||||
overview: detail.overview,
|
||||
poster_path: detail.poster_path,
|
||||
tagline: detail.tagline,
|
||||
overview: detail.overview,
|
||||
poster_path: detail.poster_path,
|
||||
backdrop_path: detail.backdrop_path,
|
||||
release_date: detail.release_date,
|
||||
runtime: detail.runtime,
|
||||
vote_average: detail.vote_average,
|
||||
vote_count: detail.vote_count,
|
||||
genres: (detail.genres || []).map(g => g.name),
|
||||
fsk: extractFSK(detail.release_dates),
|
||||
cast,
|
||||
director,
|
||||
release_date: detail.release_date,
|
||||
runtime: detail.runtime,
|
||||
vote_average: detail.vote_average,
|
||||
vote_count: detail.vote_count,
|
||||
genres: (detail.genres || []).map(g => g.name),
|
||||
fsk: extractFSK(detail.release_dates),
|
||||
cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
|
||||
director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
@@ -184,7 +214,7 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||
const { tmdb_token } = req.body;
|
||||
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
|
||||
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run('tmdb_token', tmdb_token.trim());
|
||||
genreCache = null; // Cache invalidieren
|
||||
genreCache = null;
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user