fix: xREL Cache (6h), 429-Retry, 600ms Pause zwischen Requests
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────────
|
// ── xREL Helper ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function xrelFetch(path, params = {}, timeoutMs = 7000) {
|
async function xrelFetch(path, params = {}, timeoutMs = 7000) {
|
||||||
@@ -97,25 +113,42 @@ async function xrelFetch(path, params = {}, timeoutMs = 7000) {
|
|||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
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}`);
|
if (!res.ok) throw new Error(`xREL ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Findet ext_info_id: sucht original + deutsch parallel, nimmt ersten Treffer
|
// Findet ext_info_id: sucht original + deutsch parallel, nimmt ersten Treffer
|
||||||
async function findExtInfoId(title, originalTitle) {
|
async function findExtInfoId(title, originalTitle) {
|
||||||
|
const cacheKey = `extinfo:${originalTitle || title}`;
|
||||||
|
const cached = cacheGet(cacheKey);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
const queries = [originalTitle, title]
|
const queries = [originalTitle, title]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((v, i, a) => a.indexOf(v) === i);
|
.filter((v, i, a) => a.indexOf(v) === i);
|
||||||
|
|
||||||
// Alle Queries parallel starten
|
for (const q of queries) {
|
||||||
const results = await Promise.all(queries.map(async q => {
|
|
||||||
try {
|
try {
|
||||||
const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 5 }, 5000);
|
const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 5 }, 5000);
|
||||||
const hits = Array.isArray(data.results) ? data.results : [];
|
const hits = Array.isArray(data.results) ? data.results : [];
|
||||||
return hits.find(h => h.type === 'movie')?.id || null;
|
const id = hits.find(h => h.type === 'movie')?.id || null;
|
||||||
} catch (_) { return null; }
|
if (id) { cacheSet(cacheKey, id); return id; }
|
||||||
}));
|
} catch (_) {}
|
||||||
return results.find(id => id != null) || null;
|
}
|
||||||
|
cacheSet(cacheKey, null);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holt alle deutschen Releases für eine ext_info_id – Scene + P2P parallel
|
// 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 fsk = extractFSK(detail.release_dates);
|
||||||
const imdbId = extIds.imdb_id || detail.imdb_id || null;
|
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 = [];
|
let xrel = [];
|
||||||
try {
|
try {
|
||||||
|
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);
|
const extInfoId = await findExtInfoId(detail.title, detail.original_title);
|
||||||
if (extInfoId) xrel = await getGermanReleasesById(extInfoId);
|
if (extInfoId) xrel = await getGermanReleasesById(extInfoId);
|
||||||
|
cacheSet(xrelCacheKey, xrel);
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
res.json({
|
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
|
// 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) => {
|
router.post('/xrel-check', authenticate, async (req, res) => {
|
||||||
const movies = (req.body.movies || []).slice(0, 5);
|
const movies = (req.body.movies || []).slice(0, 5);
|
||||||
if (!movies.length) return res.json({});
|
if (!movies.length) return res.json({});
|
||||||
|
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const m of movies) {
|
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 {
|
try {
|
||||||
const extInfoId = await findExtInfoId(m.title, m.original_title);
|
const extInfoId = await findExtInfoId(m.title, m.original_title);
|
||||||
if (!extInfoId) { result[m.id] = false; continue; }
|
if (!extInfoId) { cacheSet(cacheKey, false); result[m.id] = false; continue; }
|
||||||
// P2P zuerst – main_lang ist zuverlässig
|
// P2P zuerst
|
||||||
const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 25 }, 8000);
|
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
|
// Scene als Fallback
|
||||||
const scene = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 25 }, 8000);
|
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 (_) {
|
} catch (_) {
|
||||||
result[m.id] = false;
|
result[m.id] = false;
|
||||||
}
|
}
|
||||||
// Kurze Pause zwischen Filmen – xREL Rate-Limit
|
// Pause zwischen unkacheid Requests
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 600));
|
||||||
}
|
}
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ function KinoGrid({ onOpenModal }) {
|
|||||||
const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k));
|
const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k));
|
||||||
if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits]));
|
if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits]));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
@@ -349,6 +350,7 @@ function DemnächstGrid({ onOpenModal }) {
|
|||||||
const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k));
|
const hits = Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k));
|
||||||
if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits]));
|
if (hits.length > 0) setXrelIds(prev => new Set([...prev, ...hits]));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
|
|||||||
Reference in New Issue
Block a user