feat: media modal – FSK-Fix, TMDb+IMDb Links, xREL Releases
This commit is contained in:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user