fix: xREL progressive 5er-Batches, Badge-Coverage, Dirname zweizeilig nach German.

This commit is contained in:
2026-06-10 17:21:06 +02:00
parent 0f65d5d68e
commit 24975513b6
2 changed files with 71 additions and 52 deletions

View File

@@ -253,33 +253,27 @@ router.get('/movie/:id', authenticate, async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); } } catch (e) { res.status(500).json({ error: e.message }); }
}); });
// POST /api/tools/media/xrel-check Cover-Badge Batch-Check // POST /api/tools/media/xrel-check Check ob deutsche Releases existieren
// Nimmt max 5 Filme pro Request (Frontend schickt mehrere kleine Batches)
router.post('/xrel-check', authenticate, async (req, res) => { router.post('/xrel-check', authenticate, async (req, res) => {
const movies = (req.body.movies || []).slice(0, 20); const movies = (req.body.movies || []).slice(0, 5);
if (!movies.length) return res.json({}); if (!movies.length) return res.json({});
const result = {}; const result = {};
// 3er-Batches await Promise.all(movies.map(async m => {
for (let i = 0; i < movies.length; i += 3) {
const chunk = movies.slice(i, i + 3);
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; }
// Schnell-Check: nur P2P Seite 1 prüfen ob german vorhanden // P2P zuerst main_lang ist zuverlässig
const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 25 }); const p2p = await xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 25 }, 8000);
const p2pHit = (p2p.list || []).some(r => r.main_lang === 'german'); if ((p2p.list || []).some(r => r.main_lang === 'german')) { result[m.id] = true; return; }
if (p2pHit) { result[m.id] = true; return; } // Scene als Fallback
// Auch Scene prüfen 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 }); result[m.id] = (scene.list || []).some(r => /german|deutsch/i.test(r.dirname || ''));
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 + 3 < movies.length) await new Promise(r => setTimeout(r, 250));
}
res.json(result); res.json(result);
}); });

View File

@@ -261,13 +261,19 @@ function KinoGrid({ onOpenModal }) {
api('/tools/media/now-playing') api('/tools/media/now-playing')
.then(list => { .then(list => {
setMovies(list); setMovies(list);
// xREL-Batch-Check nach Laden // xREL-Badge: progressive 5er-Batches
api('/tools/media/xrel-check', { const moviesMeta = list.map(m => ({ id: m.id, title: m.title, original_title: m.original_title }));
method: 'POST', for (let i = 0; i < moviesMeta.length; i += 5) {
body: { movies: list.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) } const chunk = moviesMeta.slice(i, i + 5);
}).then(result => { api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } })
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k)))); .then(result => {
setXrelIds(prev => {
const next = new Set(prev);
Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); });
return next;
});
}).catch(() => {}); }).catch(() => {});
}
}) })
.catch(e => setError(e.message)) .catch(e => setError(e.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -320,12 +326,18 @@ function DemnächstGrid({ onOpenModal }) {
.then(grps => { .then(grps => {
setGroups(grps); setGroups(grps);
const allMovies = Object.values(grps).flat(); const allMovies = Object.values(grps).flat();
api('/tools/media/xrel-check', { const moviesMeta = allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title }));
method: 'POST', for (let i = 0; i < moviesMeta.length; i += 5) {
body: { movies: allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) } const chunk = moviesMeta.slice(i, i + 5);
}).then(result => { api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } })
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k)))); .then(result => {
setXrelIds(prev => {
const next = new Set(prev);
Object.entries(result).forEach(([k,v]) => { if (v) next.add(Number(k)); });
return next;
});
}).catch(() => {}); }).catch(() => {});
}
}) })
.catch(e => setError(e.message)) .catch(e => setError(e.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -594,30 +606,23 @@ function MovieModal({ tmdbId, onClose, mobile }) {
<div style={{ display:'flex', flexDirection:'column', gap:4 }}> <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
{detail.xrel.map((r, i) => ( {detail.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:'center', 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)',
borderRadius:6, padding:'6px 10px', textDecoration:'none', borderRadius:6, padding:'6px 10px', textDecoration:'none',
}}> }}>
<span style={{ fontSize:9, fontWeight:700, fontFamily:'monospace', <span style={{ fontSize:9, fontWeight:700, fontFamily:'monospace',
color: r.p2p ? '#4ecdc4' : '#ff6b9d', color: r.p2p ? '#4ecdc4' : '#ff6b9d',
background: r.p2p ? 'rgba(78,205,196,0.12)' : 'rgba(255,107,157,0.12)', background: r.p2p ? 'rgba(78,205,196,0.12)' : 'rgba(255,107,157,0.12)',
borderRadius:3, padding:'1px 5px', flexShrink:0 }}> borderRadius:3, padding:'1px 5px', flexShrink:0, marginTop:1 }}>
{r.p2p ? 'P2P' : 'SCN'} {r.p2p ? 'P2P' : 'SCN'}
</span> </span>
<span style={{ fontSize:10, color:'rgba(255,255,255,0.75)', fontFamily:'monospace', <div style={{ flex:1, minWidth:0 }}>
flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}> <DirnameWrapped dirname={r.dirname} />
{r.dirname} <div style={{ display:'flex', gap:8, marginTop:2 }}>
</span> {r.size && <span style={{ fontSize:9, color:'rgba(255,255,255,0.35)', fontFamily:'monospace' }}>{r.size}</span>}
{r.size && ( {r.date && <span style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace' }}>{r.date}</span>}
<span style={{ fontSize:9, color:'rgba(255,255,255,0.35)', fontFamily:'monospace', flexShrink:0 }}> </div>
{r.size} </div>
</span>
)}
{r.date && (
<span style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace', flexShrink:0 }}>
{r.date}
</span>
)}
</a> </a>
))} ))}
</div> </div>
@@ -639,6 +644,26 @@ function Badge({ children, bg, color }) {
); );
} }
// Dirname zweizeilig: vor "German." trennen
function DirnameWrapped({ dirname }) {
if (!dirname) return null;
const lower = dirname.toLowerCase();
const idx = lower.indexOf('german.');
if (idx < 0) {
// Kein "German." einfach umbrechen nach 40 Zeichen Worttrennung
return <span style={{ fontSize:10, color:'rgba(255,255,255,0.75)', fontFamily:'monospace',
wordBreak:'break-all', lineHeight:1.4 }}>{dirname}</span>;
}
const before = dirname.slice(0, idx + 7); // inkl. "German."
const after = dirname.slice(idx + 7);
return (
<span style={{ fontSize:10, color:'rgba(255,255,255,0.75)', fontFamily:'monospace', lineHeight:1.4 }}>
{before}
{after && <><br/><span style={{ color:'rgba(255,255,255,0.5)' }}>{after}</span></>}
</span>
);
}
function StatusBox({ children }) { function StatusBox({ children }) {
return ( return (
<div style={{ padding:'28px 20px', textAlign:'center', <div style={{ padding:'28px 20px', textAlign:'center',