fix: modal X-Button fixed, xREL parallel (Scene+P2P), Badge sequential batches
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
Reference in New Issue
Block a user