fix: xREL – ersten movie-Treffer nehmen, P2P alle Seiten, korrekte Felder aus Debug

This commit is contained in:
2026-06-06 23:54:37 +02:00
parent 7b18e7fb32
commit 0f65d5d68e

View File

@@ -88,12 +88,9 @@ function enrichMovie(movie, genreMap, fsk, rank) {
}; };
} }
// ── xREL: 2-Schritt Ansatz ──────────────────────────────────────────────────── // ── xREL Helper ───────────────────────────────────────────────────────────────
// Schritt 1: Titel → ext_info_id (eindeutige xREL Film-ID)
// Schritt 2: ext_info_id → alle Releases (Scene + P2P)
// So gibt es keine falschen Film-Matches mehr.
async function xrelFetch(path, params = {}, timeoutMs = 6000) { async function xrelFetch(path, params = {}, timeoutMs = 7000) {
const url = new URL(XREL_BASE + path); const url = new URL(XREL_BASE + path);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v)); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
const res = await fetch(url.toString(), { const res = await fetch(url.toString(), {
@@ -104,72 +101,79 @@ async function xrelFetch(path, params = {}, timeoutMs = 6000) {
return res.json(); return res.json();
} }
// Findet die passende ext_info_id für einen Film-Titel // Findet ext_info_id: nimmt einfach den ersten movie-Treffer
// Prüft ob Titel ähnlich genug ist (exakte oder starke Übereinstimmung) // (xREL's eigene Datenbank der erste Treffer bei Filmtitel ist fast immer korrekt)
async function findExtInfoId(title, originalTitle) { async function findExtInfoId(title, originalTitle) {
const queries = [originalTitle, title].filter(Boolean) const queries = [originalTitle, title]
.filter((v, i, a) => v && a.indexOf(v) === i); .filter(Boolean)
.filter((v, i, a) => a.indexOf(v) === i);
for (const q of queries) { for (const q of queries) {
try { 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 });
// data.results = Array von ShortExtInfo
const hits = Array.isArray(data.results) ? data.results : []; const hits = Array.isArray(data.results) ? data.results : [];
for (const hit of hits) { // Ersten movie-Treffer nehmen
const hitTitle = (hit.title || '').toLowerCase().trim(); const hit = hits.find(h => h.type === 'movie');
const searchQ = q.toLowerCase().trim(); if (hit?.id) return hit.id;
// Titel muss sehr ähnlich sein exakt oder beginnt damit
if (hitTitle === searchQ || hitTitle.startsWith(searchQ) || searchQ.startsWith(hitTitle)) {
return hit.id;
}
}
} catch (_) {} } catch (_) {}
} }
return null; return null;
} }
// Holt alle deutschen Releases für eine ext_info_id // Holt alle deutschen Releases für eine ext_info_id (alle Seiten)
async function getGermanReleasesById(extInfoId) { async function getGermanReleasesById(extInfoId) {
const results = []; const results = [];
// Scene Releases (normalerweise 1 Seite reicht, max 3)
try { try {
// Scene Releases: { list: [...Release], total_count, pagination } const page1 = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 100 });
const sceneData = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }); const list = Array.isArray(page1.list) ? page1.list : [];
const sceneList = Array.isArray(sceneData.list) ? sceneData.list : []; for (const r of list) {
for (const r of sceneList) {
// Scene: German-Filter über dirname
if (!/german|deutsch/i.test(r.dirname || '')) continue; if (!/german|deutsch/i.test(r.dirname || '')) continue;
results.push({ results.push({
dirname: r.dirname, dirname: r.dirname,
size: r.size ? `${r.size.number} ${r.size.unit}` : null, size: r.size ? `${r.size.number} ${r.size.unit}` : null,
category: 'Scene', category: 'Scene',
url: r.link_href || `https://www.xrel.to/release/${r.id}.html`, url: r.link_href,
date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null, date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null,
p2p: false, p2p: false,
}); });
} }
} catch (_) {} } catch (_) {}
// P2P Releases alle Seiten holen
try { try {
// P2P Releases: { list: [...P2PRelease], total_count, pagination } const page1 = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100 });
const p2pData = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }); const totalPages = Math.min(page1.pagination?.total_pages || 1, 5);
const p2pList = Array.isArray(p2pData.list) ? p2pData.list : []; const allLists = [page1.list || []];
for (const r of p2pList) {
// P2P: German-Filter über main_lang Feld (zuverlässig!) 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; if (r.main_lang !== 'german') continue;
const sizeMb = r.size_mb || 0; const sizeMb = r.size_mb || 0;
results.push({ results.push({
dirname: r.dirname, dirname: r.dirname,
size: sizeMb ? `${(sizeMb / 1024).toFixed(1)} GB` : null, size: sizeMb ? `${(sizeMb / 1024).toFixed(1)} GB` : null,
category: 'P2P', category: r.category?.sub_cat || 'P2P',
url: r.link_href || `https://www.xrel.to/p2p-release/${r.id}.html`, url: r.link_href,
date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null, date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null,
p2p: true, p2p: true,
}); });
} }
}
} catch (_) {} } catch (_) {}
// Neueste zuerst
results.sort((a, b) => (b.date || '').localeCompare(a.date || '')); results.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
return results; return results;
} }
@@ -220,7 +224,6 @@ 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: 2-Schritt
let xrel = []; let xrel = [];
try { try {
const extInfoId = await findExtInfoId(detail.title, detail.original_title); const extInfoId = await findExtInfoId(detail.title, detail.original_title);
@@ -256,20 +259,26 @@ router.post('/xrel-check', authenticate, async (req, res) => {
if (!movies.length) return res.json({}); if (!movies.length) return res.json({});
const result = {}; const result = {};
// 2er-Batches (2 API-Calls pro Film: search + releases) // 3er-Batches
for (let i = 0; i < movies.length; i += 2) { for (let i = 0; i < movies.length; i += 3) {
const chunk = movies.slice(i, i + 2); const chunk = movies.slice(i, i + 3);
await Promise.all(chunk.map(async m => { await Promise.all(chunk.map(async m => {
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; return; } if (!extInfoId) { result[m.id] = false; return; }
const releases = await getGermanReleasesById(extInfoId); // Schnell-Check: nur P2P Seite 1 prüfen ob german vorhanden
result[m.id] = releases.length > 0; const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 25 });
const p2pHit = (p2p.list || []).some(r => r.main_lang === 'german');
if (p2pHit) { result[m.id] = true; return; }
// Auch Scene prüfen
const scene = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 25 });
const sceneHit = (scene.list || []).some(r => /german|deutsch/i.test(r.dirname || ''));
result[m.id] = sceneHit;
} catch (_) { } catch (_) {
result[m.id] = false; result[m.id] = false;
} }
})); }));
if (i + 2 < movies.length) await new Promise(r => setTimeout(r, 300)); if (i + 3 < movies.length) await new Promise(r => setTimeout(r, 250));
} }
res.json(result); res.json(result);
}); });
@@ -307,39 +316,3 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => {
}); });
module.exports = router; module.exports = router;
// DEBUG temporär, nach Test entfernen
router.get('/debug-xrel', authenticate, async (req, res) => {
const title = req.query.title || 'Mandalorian';
const log = [];
try {
// Step 1: ext_info search
const searchUrl = `${XREL_BASE}/search/ext_info.json?q=${encodeURIComponent(title)}&type=movie&limit=5`;
log.push({ step: 'search_url', url: searchUrl });
const searchRes = await fetch(searchUrl, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(6000) });
log.push({ step: 'search_status', status: searchRes.status });
const searchData = await searchRes.json();
log.push({ step: 'search_response', data: searchData });
if (searchData.results?.length) {
const extInfoId = searchData.results[0].id;
log.push({ step: 'ext_info_id', id: extInfoId });
// Step 2a: Scene releases
const sceneUrl = `${XREL_BASE}/release/ext_info.json?id=${extInfoId}&per_page=5`;
const sceneRes = await fetch(sceneUrl, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(6000) });
const sceneData = await sceneRes.json();
log.push({ step: 'scene_releases', status: sceneRes.status, keys: Object.keys(sceneData), sample: JSON.stringify(sceneData).slice(0, 500) });
// Step 2b: P2P releases
const p2pUrl = `${XREL_BASE}/p2p/releases.json?ext_info_id=${extInfoId}&per_page=5`;
const p2pRes = await fetch(p2pUrl, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(6000) });
const p2pData = await p2pRes.json();
log.push({ step: 'p2p_releases', status: p2pRes.status, keys: Object.keys(p2pData), sample: JSON.stringify(p2pData).slice(0, 500) });
}
} catch (e) {
log.push({ step: 'error', message: e.message });
}
res.json(log);
});
module.exports = router;