From 45cf571b21fb18d062f8906b0f81158c38bfb52a Mon Sep 17 00:00:00 2001 From: Dicken Date: Wed, 10 Jun 2026 21:30:28 +0200 Subject: [PATCH] fix: xREL Cache (6h), 429-Retry, 600ms Pause zwischen Requests --- backend/src/tools/media/routes.js | 81 +++++++++++++++++++++++++------ frontend/src/tools/media.jsx | 2 + 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index e674982..d168cb8 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -88,6 +88,22 @@ function enrichMovie(movie, genreMap, fsk, rank) { }; } +// ── xREL Cache ─────────────────────────────────────────────────────────────── +// In-Memory Cache für xREL Ergebnisse – vermeidet Rate-Limits bei wiederholten Abfragen +const xrelCache = new Map(); // key → { value, expiresAt } +const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 Stunden + +function cacheGet(key) { + const entry = xrelCache.get(key); + if (!entry) return undefined; + if (Date.now() > entry.expiresAt) { xrelCache.delete(key); return undefined; } + return entry.value; +} + +function cacheSet(key, value) { + xrelCache.set(key, { value, expiresAt: Date.now() + CACHE_TTL }); +} + // ── xREL Helper ─────────────────────────────────────────────────────────────── async function xrelFetch(path, params = {}, timeoutMs = 7000) { @@ -97,25 +113,42 @@ async function xrelFetch(path, params = {}, timeoutMs = 7000) { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs), }); + if (res.status === 429) { + // Rate-Limit: Retry-After Header auslesen oder 10s warten + const retryAfter = parseInt(res.headers.get('Retry-After') || '10'); + await new Promise(r => setTimeout(r, retryAfter * 1000)); + // Nochmal versuchen + const res2 = await fetch(url.toString(), { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(timeoutMs), + }); + if (!res2.ok) throw new Error(`xREL ${res2.status}`); + return res2.json(); + } if (!res.ok) throw new Error(`xREL ${res.status}`); return res.json(); } // Findet ext_info_id: sucht original + deutsch parallel, nimmt ersten Treffer async function findExtInfoId(title, originalTitle) { + const cacheKey = `extinfo:${originalTitle || title}`; + const cached = cacheGet(cacheKey); + if (cached !== undefined) return cached; + const queries = [originalTitle, title] .filter(Boolean) .filter((v, i, a) => a.indexOf(v) === i); - // Alle Queries parallel starten - const results = await Promise.all(queries.map(async q => { + for (const q of queries) { try { const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 5 }, 5000); const hits = Array.isArray(data.results) ? data.results : []; - return hits.find(h => h.type === 'movie')?.id || null; - } catch (_) { return null; } - })); - return results.find(id => id != null) || null; + const id = hits.find(h => h.type === 'movie')?.id || null; + if (id) { cacheSet(cacheKey, id); return id; } + } catch (_) {} + } + cacheSet(cacheKey, null); + return null; } // Holt alle deutschen Releases für eine ext_info_id – Scene + P2P parallel @@ -215,11 +248,18 @@ 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 + // xREL: aus Cache oder frisch laden let xrel = []; try { - const extInfoId = await findExtInfoId(detail.title, detail.original_title); - if (extInfoId) xrel = await getGermanReleasesById(extInfoId); + const xrelCacheKey = `releases:${detail.original_title || detail.title}`; + const cachedXrel = cacheGet(xrelCacheKey); + if (cachedXrel !== undefined) { + xrel = cachedXrel; + } else { + const extInfoId = await findExtInfoId(detail.title, detail.original_title); + if (extInfoId) xrel = await getGermanReleasesById(extInfoId); + cacheSet(xrelCacheKey, xrel); + } } catch (_) {} res.json({ @@ -246,27 +286,36 @@ router.get('/movie/:id', authenticate, async (req, res) => { }); // POST /api/tools/media/xrel-check – Check ob deutsche Releases existieren -// Filme EINZELN sequenziell prüfen um xREL Rate-Limit zu respektieren +// Sequenziell + Cache – Rate-Limit-sicher router.post('/xrel-check', authenticate, async (req, res) => { const movies = (req.body.movies || []).slice(0, 5); if (!movies.length) return res.json({}); const result = {}; for (const m of movies) { + const cacheKey = `hasde:${m.original_title || m.title}`; + const cached = cacheGet(cacheKey); + if (cached !== undefined) { + result[m.id] = cached; + continue; + } try { const extInfoId = await findExtInfoId(m.title, m.original_title); - if (!extInfoId) { result[m.id] = false; continue; } - // P2P zuerst – main_lang ist zuverlässig + if (!extInfoId) { cacheSet(cacheKey, false); result[m.id] = false; continue; } + // P2P zuerst const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 25 }, 8000); - if ((p2p.list || []).some(r => r.main_lang === 'german')) { result[m.id] = true; continue; } + if ((p2p.list || []).some(r => r.main_lang === 'german')) { + cacheSet(cacheKey, true); result[m.id] = true; continue; + } // Scene als Fallback const scene = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 25 }, 8000); - result[m.id] = (scene.list || []).some(r => /german|deutsch/i.test(r.dirname || '')); + const found = (scene.list || []).some(r => /german|deutsch/i.test(r.dirname || '')); + cacheSet(cacheKey, found); result[m.id] = found; } catch (_) { result[m.id] = false; } - // Kurze Pause zwischen Filmen – xREL Rate-Limit - await new Promise(r => setTimeout(r, 400)); + // Pause zwischen unkacheid Requests + await new Promise(r => setTimeout(r, 600)); } res.json(result); }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 02e2e88..ba7e5c9 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -280,6 +280,7 @@ function KinoGrid({ onOpenModal }) { const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k)); if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits])); } catch (_) {} + await new Promise(r => setTimeout(r, 800)); } })(); return () => { cancelled = true; }; @@ -349,6 +350,7 @@ function DemnächstGrid({ onOpenModal }) { const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k)); if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits])); } catch (_) {} + await new Promise(r => setTimeout(r, 800)); } })(); return () => { cancelled = true; };