diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 9e677a9..f686fee 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -66,28 +66,28 @@ function extractFSK(rd) { return null; } -const RELEASE_TYPE_LABEL = { 2:'Kino (limited)', 3:'Kinostart', 4:'Web', 5:'Retail', 6:'TV' }; +// xREL release_date type labels +const XREL_DATE_LABEL = { + 'us-cine': 'Kinostart (US)', + 'de-cine': 'Kinostart (DE)', + 'us-web': 'Web (US)', + 'de-web': 'Web (DE)', + 'us-disc': 'Disc (US)', + 'de-disc': 'Disc (DE)', + 'us-tv': 'TV (US)', + 'de-tv': 'TV (DE)', +}; -function extractReleaseDates(rd) { - if (!rd?.results) return []; - // Nur DE und US, Typen 2-5 (kein Premiere=1, kein TV=6) - const entries = []; - for (const country of ['DE', 'US']) { - const c = rd.results.find(r => r.iso_3166_1 === country); - if (!c) continue; - for (const r of c.release_dates) { - if (!r.release_date) continue; - const label = RELEASE_TYPE_LABEL[r.type]; - if (!label) continue; - entries.push({ - country, - label: `${label} (${country})`, - date: r.release_date.slice(0, 10), - }); - } - } - entries.sort((a, b) => a.date.localeCompare(b.date)); - return entries; +// Holt xREL Erscheinungsdaten für eine ext_info_id +async function getXrelReleaseDates(extInfoId) { + try { + const data = await xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' }); + if (!Array.isArray(data.release_dates)) return []; + return data.release_dates + .filter(r => XREL_DATE_LABEL[r.type] && r.date) + .map(r => ({ label: XREL_DATE_LABEL[r.type], date: r.date })) + .sort((a, b) => a.date.localeCompare(b.date)); + } catch(_) { return []; } } async function fetchAllPages(path, maxPages = 3) { const first = await tmdb(path, { page: 1 }); @@ -232,8 +232,10 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) { results.sort((a,b) => (b.date||'').localeCompare(a.date||'')); const top5 = results.slice(0, 5); - cacheSet(cacheKey, top5, top5.length > 0 ? 12*60*60*1000 : 60*60*1000); - return top5; + const payload = { releases: top5, dates: xrelDates }; + const ttl = (top5.length > 0 || xrelDates.length > 0) ? 12*60*60*1000 : 60*60*1000; + cacheSet(cacheKey, payload, ttl); + return payload; } // ── Routes ──────────────────────────────────────────────────────────────────── @@ -286,7 +288,6 @@ router.get('/movie/:id', authenticate, async (req, res) => { backdrop_path: detail.backdrop_path, release_date: detail.release_date, runtime: detail.runtime, vote_average: detail.vote_average, genres: (detail.genres||[]).map(g => g.name), fsk, - release_dates: extractReleaseDates(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, tmdb_url: `https://www.themoviedb.org/movie/${detail.id}`, @@ -307,8 +308,12 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { // Bei refresh=1 oder leerem Cache: xREL aufrufen if (!forceRefresh) { const cached = cacheGet(cacheKey); - if (cached !== undefined && cached.length > 0) { - return res.json({ xrel: cached, has_xrel: true }); + if (cached !== undefined) { + const releases = cached?.releases || cached || []; + const dates = cached?.dates || []; + if (releases.length > 0 || dates.length > 0) { + return res.json({ xrel: releases, dates, has_xrel: releases.length > 0 }); + } } } // Cache löschen bei refresh (damit neu gecacht wird) @@ -332,8 +337,10 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { origTitle = detail.original_title || null; } - const xrel = await getXrelData(req.params.id, imdbId, title, origTitle); - res.json({ xrel, has_xrel: xrel.length > 0 }); + const payload = await getXrelData(req.params.id, imdbId, title, origTitle); + const releases = payload?.releases || []; + const dates = payload?.dates || []; + res.json({ xrel: releases, dates, has_xrel: releases.length > 0 }); } catch(e) { res.status(500).json({ error: e.message }); } }); @@ -346,7 +353,9 @@ router.get('/xrel-cached', authenticate, (req, res) => { const result = {}; for (const id of ids) { const cached = cacheGet(`xrel:tmdb:${id}`); - result[id] = cached !== undefined && cached.length > 0; + if (cached === undefined) { result[id] = false; continue; } + const releases = cached?.releases || cached || []; + result[id] = releases.length > 0; } res.json(result); }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index d00504d..955c72d 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -457,7 +457,8 @@ function FavoritenKalender({ onOpenModal }) { // ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { const [detail, setDetail] = useState(null); - const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht + const [xrel, setXrel] = useState(null); // null=nicht gesucht + const [xrelDates, setXrelDates] = useState([]); const [xrelLoading, setXrelLoading] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -502,6 +503,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { if (forceRefresh) params.set('refresh', '1'); const r = await api(`/tools/media/movie/${tmdbId}/xrel?${params}`); setXrel(r.xrel || []); + setXrelDates(r.dates || []); if (r.has_xrel) onHasXrel?.(tmdbId); } catch(_) { setXrel([]); } finally { setXrelLoading(false); } @@ -594,11 +596,11 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { - {detail.release_dates?.length > 0 && ( + {xrelDates.length > 0 && (
ERSCHEINUNGSDATEN
- {detail.release_dates.map((r, i) => ( + {xrelDates.map((r, i) => (