fix: modal X-Button fixed, xREL parallel (Scene+P2P), Badge sequential batches

This commit is contained in:
2026-06-10 19:19:28 +02:00
parent e53e8e4331
commit 102ab274c7
2 changed files with 84 additions and 100 deletions

View File

@@ -101,78 +101,69 @@ async function xrelFetch(path, params = {}, timeoutMs = 7000) {
return res.json(); return res.json();
} }
// Findet ext_info_id: nimmt einfach den ersten movie-Treffer // Findet ext_info_id: sucht original + deutsch parallel, nimmt ersten Treffer
// (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] const queries = [originalTitle, title]
.filter(Boolean) .filter(Boolean)
.filter((v, i, a) => a.indexOf(v) === i); .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 { 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 : []; const hits = Array.isArray(data.results) ? data.results : [];
// Ersten movie-Treffer nehmen return hits.find(h => h.type === 'movie')?.id || null;
const hit = hits.find(h => h.type === 'movie'); } catch (_) { return null; }
if (hit?.id) return hit.id; }));
} catch (_) {} return results.find(id => id != null) || null;
}
return 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) { 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 = []; const results = [];
// Scene Releases (normalerweise 1 Seite reicht, max 3) // Scene
try { for (const r of sceneData.list || []) {
const page1 = await xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 100 }); if (!/german|deutsch/i.test(r.dirname || '')) continue;
const list = Array.isArray(page1.list) ? page1.list : []; results.push({
for (const r of list) { dirname: r.dirname,
if (!/german|deutsch/i.test(r.dirname || '')) continue; size: r.size ? `${r.size.number} ${r.size.unit}` : null,
results.push({ category: 'Scene',
dirname: r.dirname, url: r.link_href,
size: r.size ? `${r.size.number} ${r.size.unit}` : null, date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null,
category: 'Scene', p2p: false,
url: r.link_href, });
date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null, }
p2p: false,
});
}
} catch (_) {}
// P2P Releases alle Seiten holen // P2P alle Seiten parallel holen
try { const p2pTotalPages = Math.min(p2pData.pagination?.total_pages || 1, 4);
const page1 = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100 }); const allP2P = [...(p2pData.list || [])];
const totalPages = Math.min(page1.pagination?.total_pages || 1, 5); if (p2pTotalPages > 1) {
const allLists = [page1.list || []]; const extra = await Promise.all(
Array.from({ length: p2pTotalPages - 1 }, (_, i) =>
if (totalPages > 1) { xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100, page: i + 2 }, 6000)
const extra = await Promise.all( .then(d => d.list || []).catch(() => [])
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(() => []) for (const list of extra) allP2P.push(...list);
) }
); for (const r of allP2P) {
allLists.push(...extra); if (r.main_lang !== 'german') continue;
} const sizeMb = r.size_mb || 0;
results.push({
for (const list of allLists) { dirname: r.dirname,
for (const r of list) { size: sizeMb ? `${(sizeMb / 1024).toFixed(1)} GB` : null,
// P2P: main_lang === 'german' ist das zuverlässige Feld category: r.category?.sub_cat || 'P2P',
if (r.main_lang !== 'german') continue; url: r.link_href,
const sizeMb = r.size_mb || 0; date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null,
results.push({ p2p: true,
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 (_) {}
results.sort((a, b) => (b.date || '').localeCompare(a.date || '')); results.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
return results; return results;
@@ -224,6 +215,7 @@ 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
let xrel = []; let xrel = [];
try { try {
const extInfoId = await findExtInfoId(detail.title, detail.original_title); const extInfoId = await findExtInfoId(detail.title, detail.original_title);
@@ -309,27 +301,4 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => {
res.json({ ok: true }); 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; module.exports = router;

View File

@@ -261,19 +261,21 @@ function KinoGrid({ onOpenModal }) {
api('/tools/media/now-playing') api('/tools/media/now-playing')
.then(list => { .then(list => {
setMovies(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 })); 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) { (async () => {
const chunk = moviesMeta.slice(i, i + 5); for (let i = 0; i < moviesMeta.length; i += 5) {
api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }) try {
.then(result => { const chunk = moviesMeta.slice(i, i + 5);
const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } });
setXrelIds(prev => { setXrelIds(prev => {
const next = new Set(prev); const next = new Set(prev);
Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); }); Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); });
return next; return next;
}); });
}).catch(() => {}); } catch (_) {}
} }
})();
}) })
.catch(e => setError(e.message)) .catch(e => setError(e.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -327,17 +329,19 @@ function DemnächstGrid({ onOpenModal }) {
setGroups(grps); setGroups(grps);
const allMovies = Object.values(grps).flat(); const allMovies = Object.values(grps).flat();
const moviesMeta = allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title })); 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) { (async () => {
const chunk = moviesMeta.slice(i, i + 5); for (let i = 0; i < moviesMeta.length; i += 5) {
api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }) try {
.then(result => { const chunk = moviesMeta.slice(i, i + 5);
const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } });
setXrelIds(prev => { setXrelIds(prev => {
const next = new Set(prev); const next = new Set(prev);
Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); }); Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); });
return next; return next;
}); });
}).catch(() => {}); } catch (_) {}
} }
})();
}) })
.catch(e => setError(e.message)) .catch(e => setError(e.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -461,14 +465,21 @@ function FavoritenKalender({ onOpenModal }) {
// ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── // ── Film-Detail-Modal (via Portal) ────────────────────────────────────────────
function MovieModal({ tmdbId, onClose, mobile }) { function MovieModal({ tmdbId, onClose, mobile }) {
const [detail, setDetail] = useState(null); const [detail, setDetail] = useState(null);
const [xrel, setXrel] = useState(null); // null=lädt, []+=fertig
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
useEffect(() => { useEffect(() => {
setLoading(true); setError(''); setLoading(true); setDetail(null); setXrel(null); setError('');
api(`/tools/media/movie/${tmdbId}`) 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 // Body-Scroll sperren
const prev = document.body.style.overflow; const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@@ -608,11 +619,15 @@ function MovieModal({ tmdbId, onClose, mobile }) {
</div> </div>
{/* xREL Releases */} {/* xREL Releases */}
{detail.xrel?.length > 0 && ( {xrel === null && (
<div style={{ marginTop:16, fontSize:11, color:'rgba(255,255,255,0.3)',
fontFamily:'monospace', fontStyle:'italic' }}> Suche deutsche Releases</div>
)}
{xrel !== null && xrel.length > 0 && (
<div style={{ marginTop:18 }}> <div style={{ marginTop:18 }}>
<div style={{ ...S.head, marginBottom:8 }}>RELEASES (XREL.TO)</div> <div style={{ ...S.head, marginBottom:8 }}>RELEASES (XREL.TO)</div>
<div style={{ display:'flex', flexDirection:'column', gap:4 }}> <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
{detail.xrel.map((r, i) => ( {xrel.map((r, i) => (
<a key={i} href={r.url} target="_blank" rel="noopener noreferrer" style={{ <a key={i} href={r.url} target="_blank" rel="noopener noreferrer" style={{
display:'flex', alignItems:'flex-start', gap:8, display:'flex', alignItems:'flex-start', gap:8,
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.07)', background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.07)',