diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index d592d7f..479782a 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -125,6 +125,12 @@ function enrichMovie(m, genreMap, fsk, rank) { } // ── xREL ───────────────────────────────────────────────────────────────────── +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)', +}; + const QUALITY_BLOCKLIST = r => /\b(2160p|4k|uhd|uhdbluray|uhdbd|remux)\b/i.test(r) || /COMPLETE[._](?:BLURAY|BD)/i.test(r); async function xrelFetch(path, params = {}) { @@ -145,27 +151,6 @@ 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); @@ -224,13 +209,19 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) { return []; } - // Releases + Erscheinungsdaten parallel holen - const [sceneData, p2pData, xrelDates] = await Promise.all([ + // Releases + ext_info Details parallel holen + const [sceneData, p2pData, infoData] = 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), + xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' }).catch(()=>({})), ]); + // xREL Erscheinungsdaten + const xrel_dates = (infoData.release_dates||[]) + .filter(d => XREL_DATE_LABEL[d.type]) + .map(d => ({ label: XREL_DATE_LABEL[d.type], date: d.date })) + .sort((a,b) => a.date.localeCompare(b.date)); + const results = []; for (const r of sceneData.list||[]) { @@ -254,9 +245,8 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) { results.sort((a,b) => (b.date||'').localeCompare(a.date||'')); const top5 = results.slice(0, 5); - 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); + const payload = { releases: top5, xrel_dates }; + cacheSet(cacheKey, payload, top5.length > 0 || xrel_dates.length > 0 ? 12*60*60*1000 : 60*60*1000); return payload; } @@ -332,9 +322,9 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { const cached = cacheGet(cacheKey); if (cached !== undefined) { 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 }); + const xrel_dates = Array.isArray(cached) ? [] : (cached.xrel_dates || []); + if (releases.length > 0 || xrel_dates.length > 0) { + return res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 }); } } } @@ -359,10 +349,10 @@ 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 = Array.isArray(payload) ? payload : (payload?.releases || []); - const dates = Array.isArray(payload) ? [] : (payload?.dates || []); - res.json({ xrel: releases, dates, has_xrel: releases.length > 0 }); + const data = await getXrelData(req.params.id, imdbId, title, origTitle); + const releases = Array.isArray(data) ? data : (data.releases || []); + const xrel_dates = Array.isArray(data) ? [] : (data.xrel_dates || []); + res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 }); } catch(e) { res.status(500).json({ error: e.message }); } }); @@ -375,9 +365,7 @@ router.get('/xrel-cached', authenticate, (req, res) => { const result = {}; for (const id of ids) { const cached = cacheGet(`xrel:tmdb:${id}`); - if (cached === undefined) { result[id] = false; continue; } - const releases = Array.isArray(cached) ? cached : (cached.releases || []); - result[id] = releases.length > 0; + result[id] = cached !== undefined && (Array.isArray(cached) ? cached.length > 0 : cached.releases?.length > 0); } res.json(result); }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index efdf45a..fc709a6 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -458,7 +458,6 @@ function FavoritenKalender({ onOpenModal }) { function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { const [detail, setDetail] = useState(null); const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht - const [xrelDates, setXrelDates] = useState([]); const [xrelLoading, setXrelLoading] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -467,7 +466,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { const detailRef = useRef(null); useEffect(() => { - setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError(''); + setLoading(true); setDetail(null); setXrel(null); setError(''); detailRef.current = null; api(`/tools/media/movie/${tmdbId}`) .then(d => { @@ -503,7 +502,6 @@ 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); } @@ -582,7 +580,6 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { {detail.vote_average > 0 && ★ {detail.vote_average.toFixed(1)}} {detail.runtime > 0 && ⏱ {detail.runtime} Min} {fskStyle && FSK {detail.fsk}} - {detail.release_date && 🇩🇪 {fmtDE(detail.release_date)}} {detail.countries?.length > 0 && 🌍 {detail.countries.slice(0,2).join(' · ')}} {detail.genres?.length > 0 && ( @@ -596,11 +593,11 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { - {xrelDates.length > 0 && ( + {detail.release_dates?.length > 0 && (
ERSCHEINUNGSDATEN
- {xrelDates.map((r, i) => ( + {detail.release_dates.map((r, i) => (