feat: media modal – FSK-Fix, TMDb+IMDb Links, xREL Releases

This commit is contained in:
2026-06-06 21:14:41 +02:00
parent 2e206da879
commit e7c9c0f52f
2 changed files with 111 additions and 19 deletions

View File

@@ -4,6 +4,7 @@ const db = require('../../db');
const { authenticate, requireAdmin } = require('../../middleware/auth');
const TMDB_BASE = 'https://api.themoviedb.org/3';
const XREL_BASE = 'https://api.xrel.to/v2';
function getToken() {
return db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get()?.value || '';
@@ -33,13 +34,18 @@ async function getGenreMap() {
return genreCache;
}
// FSK-Fix: alle release_dates durchsuchen, ersten nicht-leeren Cert nehmen
// Bevorzugt Typ 3 (Kino), aber nimmt jeden nicht-leeren Wert
function extractFSK(releaseDates) {
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
if (!de) return null;
return de.release_dates.find(r => r.certification)?.certification || null;
// Erst Kino (type=3), dann alle anderen
const sorted = [...de.release_dates].sort((a, b) =>
(a.type === 3 ? 0 : 1) - (b.type === 3 ? 0 : 1)
);
return sorted.find(r => r.certification && r.certification.trim() !== '')?.certification || null;
}
// Alle Seiten holen (parallel)
async function fetchAllPages(path, maxPages = 3) {
const first = await tmdb(path, { page: 1 });
const total = Math.min(first.total_pages || 1, maxPages);
@@ -69,28 +75,23 @@ function enrichMovie(movie, genreMap, fsk, rank) {
}
// GET /api/tools/media/now-playing
// FSK nur für Top 10 parallel holen, Rest ohne kein sequentielles Warten
router.get('/now-playing', authenticate, async (req, res) => {
try {
const [pages, genreMap] = await Promise.all([
fetchAllPages('/movie/now_playing', 3),
getGenreMap(),
]);
const filtered = pages.filter(m => m.original_language !== 'tr');
// Top 10: FSK parallel holen
const top10raw = filtered.slice(0, 10);
const restRaw = filtered.slice(10);
// Top 10: FSK parallel holen
const top10 = await Promise.all(top10raw.map(async (m, i) => {
let fsk = null;
try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
return enrichMovie(m, genreMap, fsk, i + 1);
}));
const rest = restRaw.map(m => enrichMovie(m, genreMap, null, null));
res.json([...top10, ...rest]);
} catch (e) {
res.status(500).json({ error: e.message });
@@ -98,51 +99,81 @@ router.get('/now-playing', authenticate, async (req, res) => {
});
// GET /api/tools/media/upcoming
// Wochenweise gruppiert, kein FSK-Lookup beim Listing
router.get('/upcoming', authenticate, async (req, res) => {
try {
const [pages, genreMap] = await Promise.all([
fetchAllPages('/movie/upcoming', 3),
getGenreMap(),
]);
const seen = new Set();
const filtered = pages
.filter(m => m.original_language !== 'tr')
.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; })
.filter(m => m.release_date);
filtered.sort((a, b) => a.release_date.localeCompare(b.release_date));
// Wochenweise gruppieren (Montag als Key)
const groups = {};
for (const m of filtered) {
const d = new Date(m.release_date);
const dow = d.getDay(); // 0=So
const dow = d.getDay();
const diff = dow === 0 ? -6 : 1 - dow;
const mon = new Date(d);
mon.setDate(d.getDate() + diff);
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(enrichMovie(m, genreMap, null, null));
}
res.json(groups);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/tools/media/movie/:id Details + Credits (nur für Modal)
// GET /api/tools/media/movie/:id Details + Credits + xREL + Links
router.get('/movie/:id', authenticate, async (req, res) => {
try {
const [detail, genreMap] = await Promise.all([
const [detail, genreMap, extIds] = await Promise.all([
tmdb(`/movie/${req.params.id}`, { append_to_response: 'credits,release_dates' }),
getGenreMap(),
tmdb(`/movie/${req.params.id}/external_ids`).catch(() => ({})),
]);
const fsk = extractFSK(detail.release_dates);
const imdb_id = extIds.imdb_id || detail.imdb_id || null;
const tmdb_url = `https://www.themoviedb.org/movie/${detail.id}`;
const imdb_url = imdb_id ? `https://www.imdb.com/title/${imdb_id}` : null;
// xREL-Suche: original_title + Jahr
let xrel = [];
try {
const year = detail.release_date?.slice(0, 4) || '';
const query = encodeURIComponent(detail.original_title || detail.title);
const xUrl = `${XREL_BASE}/search/releases.json?q=${query}&scene=true&p2p=true&limit=25`;
const xRes = await fetch(xUrl, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(4000) });
if (xRes.ok) {
const xData = await xRes.json();
const hits = (xData.results?.scene?.releases || []).concat(xData.results?.p2p?.releases || []);
// Nur Movie-Releases, nach Erscheinungsjahr filtern wenn bekannt
xrel = hits
.filter(r => {
const cat = r.category?.name || '';
return /movie|film|xvid|x264|x265|bluray|uhd|dvdr|hdtv/i.test(cat);
})
.map(r => ({
dirname: r.dirname,
size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null,
category: r.category?.name || '',
url: r.link_href || `https://www.xrel.to/release/${r.id}.html`,
date: r.time ? new Date(r.time * 1000).toISOString().slice(0,10) : null,
p2p: !!r.p2p,
}))
.slice(0, 15);
}
} catch (_) {}
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,
@@ -150,10 +181,14 @@ router.get('/movie/:id', authenticate, async (req, res) => {
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),
fsk,
cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
tmdb_url,
imdb_url,
xrel,
});
} catch (e) {
res.status(500).json({ error: e.message });