From 102ab274c75bd4085b2f5c59dd83226b7fb1b94f Mon Sep 17 00:00:00 2001 From: Dicken Date: Wed, 10 Jun 2026 19:19:28 +0200 Subject: [PATCH] fix: modal X-Button fixed, xREL parallel (Scene+P2P), Badge sequential batches --- backend/src/tools/media/routes.js | 133 ++++++++++++------------------ frontend/src/tools/media.jsx | 51 ++++++++---- 2 files changed, 84 insertions(+), 100 deletions(-) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 79ff379..14753f1 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -101,78 +101,69 @@ async function xrelFetch(path, params = {}, timeoutMs = 7000) { return res.json(); } -// Findet ext_info_id: nimmt einfach den ersten movie-Treffer -// (xREL's eigene Datenbank – der erste Treffer bei Filmtitel ist fast immer korrekt) +// Findet ext_info_id: sucht original + deutsch parallel, nimmt ersten Treffer async function findExtInfoId(title, originalTitle) { const queries = [originalTitle, title] .filter(Boolean) .filter((v, i, a) => a.indexOf(v) === i); - for (const q of queries) { + // Alle Queries parallel starten + const results = await Promise.all(queries.map(async q => { try { - const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 5 }); + const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 5 }, 5000); const hits = Array.isArray(data.results) ? data.results : []; - // Ersten movie-Treffer nehmen - const hit = hits.find(h => h.type === 'movie'); - if (hit?.id) return hit.id; - } catch (_) {} - } - return null; + return hits.find(h => h.type === 'movie')?.id || null; + } catch (_) { return null; } + })); + return results.find(id => id != null) || null; } -// Holt alle deutschen Releases für eine ext_info_id (alle Seiten) +// Holt alle deutschen Releases für eine ext_info_id – Scene + P2P parallel async function getGermanReleasesById(extInfoId) { + const [sceneData, p2pData] = await Promise.all([ + xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 100 }, 6000).catch(() => ({})), + xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100 }, 6000).catch(() => ({})), + ]); + const results = []; - // Scene Releases (normalerweise 1 Seite reicht, max 3) - try { - const page1 = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 100 }); - const list = Array.isArray(page1.list) ? page1.list : []; - for (const r of list) { - if (!/german|deutsch/i.test(r.dirname || '')) continue; - results.push({ - dirname: r.dirname, - size: r.size ? `${r.size.number} ${r.size.unit}` : null, - category: 'Scene', - url: r.link_href, - date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null, - p2p: false, - }); - } - } catch (_) {} + // Scene + for (const r of sceneData.list || []) { + if (!/german|deutsch/i.test(r.dirname || '')) continue; + results.push({ + dirname: r.dirname, + size: r.size ? `${r.size.number} ${r.size.unit}` : null, + category: 'Scene', + url: r.link_href, + date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null, + p2p: false, + }); + } - // P2P Releases – alle Seiten holen - try { - const page1 = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100 }); - const totalPages = Math.min(page1.pagination?.total_pages || 1, 5); - const allLists = [page1.list || []]; - - if (totalPages > 1) { - const extra = await Promise.all( - Array.from({ length: totalPages - 1 }, (_, i) => - xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100, page: i + 2 }) - .then(d => d.list || []).catch(() => []) - ) - ); - allLists.push(...extra); - } - - for (const list of allLists) { - for (const r of list) { - // P2P: main_lang === 'german' ist das zuverlässige Feld - if (r.main_lang !== 'german') continue; - const sizeMb = r.size_mb || 0; - results.push({ - dirname: r.dirname, - size: sizeMb ? `${(sizeMb / 1024).toFixed(1)} GB` : null, - category: r.category?.sub_cat || 'P2P', - url: r.link_href, - date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null, - p2p: true, - }); - } - } - } catch (_) {} + // P2P – alle Seiten parallel holen + const p2pTotalPages = Math.min(p2pData.pagination?.total_pages || 1, 4); + const allP2P = [...(p2pData.list || [])]; + if (p2pTotalPages > 1) { + const extra = await Promise.all( + Array.from({ length: p2pTotalPages - 1 }, (_, i) => + xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100, page: i + 2 }, 6000) + .then(d => d.list || []).catch(() => []) + ) + ); + for (const list of extra) allP2P.push(...list); + } + for (const r of allP2P) { + if (r.main_lang !== 'german') continue; + const sizeMb = r.size_mb || 0; + results.push({ + dirname: r.dirname, + size: sizeMb ? `${(sizeMb / 1024).toFixed(1)} GB` : null, + category: r.category?.sub_cat || 'P2P', + url: r.link_href, + date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null, + p2p: true, + }); + } results.sort((a, b) => (b.date || '').localeCompare(a.date || '')); return results; @@ -224,6 +215,7 @@ router.get('/movie/:id', authenticate, async (req, res) => { const fsk = extractFSK(detail.release_dates); const imdbId = extIds.imdb_id || detail.imdb_id || null; + // xREL: findExtInfoId + getGermanReleasesById sequenziell aber mit kürzerem Timeout let xrel = []; try { const extInfoId = await findExtInfoId(detail.title, detail.original_title); @@ -309,27 +301,4 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => { res.json({ ok: true }); }); -// DEBUG – nach Test entfernen -router.get('/debug-xrel2', authenticate, async (req, res) => { - const title = req.query.title || 'Thunderbolts'; - const original = req.query.original || title; - const log = {}; - try { - log.step1_query = title; - const searchData = await xrelFetch('/search/ext_info.json', { q: title, type: 'movie', limit: 5 }); - log.step1_results = (searchData.results || []).map(r => ({ id: r.id, title: r.title, type: r.type })); - const extInfoId = (searchData.results || []).find(h => h.type === 'movie')?.id; - log.ext_info_id = extInfoId || null; - if (extInfoId) { - const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 5 }); - log.p2p_total = p2p.total_count; - log.p2p_sample = (p2p.list || []).slice(0, 3).map(r => ({ dirname: r.dirname, main_lang: r.main_lang, pub_time: r.pub_time })); - const scene = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 5 }); - log.scene_total = scene.total_count; - log.scene_sample = (scene.list || []).slice(0, 3).map(r => ({ dirname: r.dirname, time: r.time })); - } - } catch (e) { log.error = e.message; } - res.json(log); -}); - module.exports = router; diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index c6f4851..dddae24 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -261,19 +261,21 @@ function KinoGrid({ onOpenModal }) { api('/tools/media/now-playing') .then(list => { setMovies(list); - // xREL-Badge: progressive 5er-Batches + // xREL-Badge: sequentielle 5er-Batches (Rate-Limit-sicher) const moviesMeta = list.map(m => ({ id: m.id, title: m.title, original_title: m.original_title })); - for (let i = 0; i < moviesMeta.length; i += 5) { - const chunk = moviesMeta.slice(i, i + 5); - api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }) - .then(result => { + (async () => { + for (let i = 0; i < moviesMeta.length; i += 5) { + try { + const chunk = moviesMeta.slice(i, i + 5); + const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }); setXrelIds(prev => { const next = new Set(prev); Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); }); return next; }); - }).catch(() => {}); - } + } catch (_) {} + } + })(); }) .catch(e => setError(e.message)) .finally(() => setLoading(false)); @@ -327,17 +329,19 @@ function DemnächstGrid({ onOpenModal }) { setGroups(grps); const allMovies = Object.values(grps).flat(); const moviesMeta = allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title })); - for (let i = 0; i < moviesMeta.length; i += 5) { - const chunk = moviesMeta.slice(i, i + 5); - api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }) - .then(result => { + (async () => { + for (let i = 0; i < moviesMeta.length; i += 5) { + try { + const chunk = moviesMeta.slice(i, i + 5); + const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }); setXrelIds(prev => { const next = new Set(prev); Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); }); return next; }); - }).catch(() => {}); - } + } catch (_) {} + } + })(); }) .catch(e => setError(e.message)) .finally(() => setLoading(false)); @@ -461,14 +465,21 @@ function FavoritenKalender({ onOpenModal }) { // ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── function MovieModal({ tmdbId, onClose, mobile }) { const [detail, setDetail] = useState(null); + const [xrel, setXrel] = useState(null); // null=lädt, []+=fertig const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { - setLoading(true); setError(''); + setLoading(true); setDetail(null); setXrel(null); setError(''); api(`/tools/media/movie/${tmdbId}`) - .then(setDetail).catch(e => setError(e.message)).finally(() => setLoading(false)); - + .then(d => { + // TMDb-Daten sofort anzeigen + setDetail(d); + setLoading(false); + // xREL ist bereits im response enthalten (Backend lädt parallel) + setXrel(Array.isArray(d.xrel) ? d.xrel : []); + }) + .catch(e => { setError(e.message); setLoading(false); }); // Body-Scroll sperren const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; @@ -608,11 +619,15 @@ function MovieModal({ tmdbId, onClose, mobile }) { {/* xREL Releases */} - {detail.xrel?.length > 0 && ( + {xrel === null && ( +
⏳ Suche deutsche Releases…
+ )} + {xrel !== null && xrel.length > 0 && (
RELEASES (XREL.TO)
- {detail.xrel.map((r, i) => ( + {xrel.map((r, i) => (