diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index f686fee..d592d7f 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -66,28 +66,28 @@ function extractFSK(rd) { return null; } -// 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)', -}; +const RELEASE_TYPE_LABEL = { 2:'Kino (limited)', 3:'Kinostart', 4:'Web', 5:'Retail', 6:'TV' }; -// 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 []; } +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; } async function fetchAllPages(path, maxPages = 3) { const first = await tmdb(path, { page: 1 }); @@ -145,6 +145,27 @@ async function xrelFetch(path, params = {}) { // Einzige Funktion für xREL: benutzt immer die ext_info_id und gibt max 5 deutsche ≤1080p Releases // Cache-Key: tmdb_id (immer eindeutig, konsistent zwischen Badge und Modal) +// xREL Erscheinungsdaten via /ext_info/info.json +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)', +}; + +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 getXrelData(tmdbId, imdbId, title, originalTitle) { const cacheKey = `xrel:tmdb:${tmdbId}`; const cached = cacheGet(cacheKey); @@ -203,10 +224,11 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) { return []; } - // Releases holen: Scene + P2P parallel, nur german, nur ≤1080p, max 5 - const [sceneData, p2pData] = await Promise.all([ + // Releases + Erscheinungsdaten parallel holen + const [sceneData, p2pData, xrelDates] = await Promise.all([ xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }).catch(()=>({})), xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }).catch(()=>({})), + getXrelReleaseDates(extInfoId), ]); const results = []; @@ -309,8 +331,8 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { if (!forceRefresh) { const cached = cacheGet(cacheKey); if (cached !== undefined) { - const releases = cached?.releases || cached || []; - const dates = cached?.dates || []; + const releases = Array.isArray(cached) ? cached : (cached.releases || []); + const dates = Array.isArray(cached) ? [] : (cached.dates || []); if (releases.length > 0 || dates.length > 0) { return res.json({ xrel: releases, dates, has_xrel: releases.length > 0 }); } @@ -337,9 +359,9 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { origTitle = detail.original_title || null; } - const payload = await getXrelData(req.params.id, imdbId, title, origTitle); - const releases = payload?.releases || []; - const dates = payload?.dates || []; + const payload = await getXrelData(req.params.id, imdbId, title, origTitle); + const releases = Array.isArray(payload) ? payload : (payload?.releases || []); + const dates = Array.isArray(payload) ? [] : (payload?.dates || []); res.json({ xrel: releases, dates, has_xrel: releases.length > 0 }); } catch(e) { res.status(500).json({ error: e.message }); } }); @@ -354,7 +376,7 @@ router.get('/xrel-cached', authenticate, (req, res) => { for (const id of ids) { const cached = cacheGet(`xrel:tmdb:${id}`); if (cached === undefined) { result[id] = false; continue; } - const releases = cached?.releases || cached || []; + const releases = Array.isArray(cached) ? cached : (cached.releases || []); result[id] = releases.length > 0; } res.json(result); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 955c72d..efdf45a 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -457,7 +457,7 @@ 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 + const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht const [xrelDates, setXrelDates] = useState([]); const [xrelLoading, setXrelLoading] = useState(false); const [loading, setLoading] = useState(true); @@ -467,7 +467,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { const detailRef = useRef(null); useEffect(() => { - setLoading(true); setDetail(null); setXrel(null); setError(''); + setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError(''); detailRef.current = null; api(`/tools/media/movie/${tmdbId}`) .then(d => {