feat: media – Genres, FSK-Badges, TR-Filter, Film-Modal, Favoriten-Kalender

This commit is contained in:
2026-06-06 19:05:44 +02:00
parent 8a28a97b81
commit 4a5e314aea
3 changed files with 429 additions and 110 deletions

View File

@@ -23,59 +23,168 @@ async function tmdb(path, params = {}) {
return res.json();
}
// Genre-Cache (in-memory, wird bei erstem Aufruf befüllt)
let genreCache = null;
async function getGenreMap() {
if (genreCache) return genreCache;
const data = await tmdb('/genre/movie/list');
genreCache = {};
for (const g of data.genres) genreCache[g.id] = g.name;
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;
}
// Einen Film anreichern (Genres + FSK aus append_to_response)
function enrichMovie(movie, genreMap, releaseDates) {
return {
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),
};
}
// GET /api/tools/media/now-playing
router.get('/now-playing', authenticate, async (req, res) => {
try {
const data = await tmdb('/movie/now_playing');
res.json(data.results.slice(0, 10));
const [listData, genreMap] = await Promise.all([
tmdb('/movie/now_playing'),
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);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/tools/media/upcoming
router.get('/upcoming', authenticate, async (req, res) => {
try {
const data = await tmdb('/movie/upcoming');
res.json(data.results.slice(0, 10));
const [listData, genreMap] = await Promise.all([
tmdb('/movie/upcoming'),
getGenreMap(),
]);
const filtered = listData.results
.filter(m => m.original_language !== 'tr')
.slice(0, 10);
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);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/tools/media/movie/:id Details + 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,
original_title: detail.original_title,
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,
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/tools/media/favorites
router.get('/favorites', authenticate, (req, res) => {
const rows = db.prepare(
'SELECT * FROM movie_favorites WHERE user_id=? ORDER BY added_at DESC'
'SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC, title ASC'
).all(req.user.id);
res.json(rows);
});
// POST /api/tools/media/favorites
router.post('/favorites', authenticate, (req, res) => {
const { tmdb_id, title, poster_path, release_date_de } = req.body;
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk } = req.body;
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
try {
db.prepare(
'INSERT OR IGNORE INTO movie_favorites (user_id,tmdb_id,title,poster_path,release_date_de) VALUES (?,?,?,?,?)'
).run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'');
`INSERT OR IGNORE INTO movie_favorites
(user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk)
VALUES (?,?,?,?,?,?,?,?)`
).run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'',
release_date||'', JSON.stringify(genres||[]), fsk||'');
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// DELETE /api/tools/media/favorites/:tmdb_id
router.delete('/favorites/:tmdb_id', authenticate, (req, res) => {
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?')
.run(req.user.id, Number(req.params.tmdb_id));
res.json({ ok: true });
});
// GET /api/tools/media/settings
router.get('/settings', authenticate, requireAdmin, (req, res) => {
const token = getToken();
res.json({ configured: !!token });
res.json({ configured: !!getToken() });
});
// PUT /api/tools/media/settings
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
res.json({ ok: true });
});