diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 5df4735..8c7385b 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -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 }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index f114272..fed3fe8 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -531,6 +531,63 @@ function MovieModal({ tmdbId, onClose, mobile }) { )} + + {/* Links */} +
+ {detail.tmdb_url && ( + 🎬 TMDb + )} + {detail.imdb_url && ( + ⭐ IMDb + )} +
+ + {/* xREL Releases */} + {detail.xrel?.length > 0 && ( +
+
RELEASES (XREL.TO)
+
+ {detail.xrel.map((r, i) => ( + + + {r.p2p ? 'P2P' : 'SCN'} + + + {r.dirname} + + {r.size && ( + + {r.size} + + )} + {r.date && ( + + {r.date} + + )} + + ))} +
+
+ )} )}