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 }); }
});
// 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) => {
const movies = (req.body.movies || []).slice(0, 20);
const movies = (req.body.movies || []).slice(0, 5);
if (!movies.length) return res.json({});
const result = {};
// 3er-Batches
for (let i = 0; i < movies.length; i += 3) {
const chunk = movies.slice(i, i + 3);
await Promise.all(chunk.map(async m => {
try {
const extInfoId = await findExtInfoId(m.title, m.original_title);
if (!extInfoId) { result[m.id] = false; return; }
// Schnell-Check: nur P2P Seite 1 prüfen ob german vorhanden
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 (_) {
result[m.id] = false;
}
}));
if (i + 3 < movies.length) await new Promise(r => setTimeout(r, 250));
}
await Promise.all(movies.map(async m => {
try {
const extInfoId = await findExtInfoId(m.title, m.original_title);
if (!extInfoId) { result[m.id] = false; return; }
// P2P zuerst main_lang ist zuverlässig
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; return; }
// Scene als Fallback
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 || ''));
} catch (_) {
result[m.id] = false;
}
}));
res.json(result);
});

View File

@@ -261,13 +261,19 @@ function KinoGrid({ onOpenModal }) {
api('/tools/media/now-playing')
.then(list => {
setMovies(list);
// xREL-Batch-Check nach Laden
api('/tools/media/xrel-check', {
method: 'POST',
body: { movies: list.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) }
}).then(result => {
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k))));
}).catch(() => {});
// xREL-Badge: progressive 5er-Batches
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) {
const chunk = moviesMeta.slice(i, i + 5);
api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } })
.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(e => setError(e.message))
.finally(() => setLoading(false));
@@ -320,12 +326,18 @@ function DemnächstGrid({ onOpenModal }) {
.then(grps => {
setGroups(grps);
const allMovies = Object.values(grps).flat();
api('/tools/media/xrel-check', {
method: 'POST',
body: { movies: allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) }
}).then(result => {
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k))));
}).catch(() => {});
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) {
const chunk = moviesMeta.slice(i, i + 5);
api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } })
.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(e => setError(e.message))
.finally(() => setLoading(false));
@@ -594,30 +606,23 @@ function MovieModal({ tmdbId, onClose, mobile }) {
<div style={{ display:'flex', flexDirection:'column', gap:4 }}>
{detail.xrel.map((r, i) => (
<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)',
borderRadius:6, padding:'6px 10px', textDecoration:'none',
}}>
<span style={{ fontSize:9, fontWeight:700, fontFamily:'monospace',
color: r.p2p ? '#4ecdc4' : '#ff6b9d',
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'}
</span>
<span style={{ fontSize:10, color:'rgba(255,255,255,0.75)', fontFamily:'monospace',
flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
{r.dirname}
</span>
{r.size && (
<span style={{ fontSize:9, color:'rgba(255,255,255,0.35)', fontFamily:'monospace', flexShrink:0 }}>
{r.size}
</span>
)}
{r.date && (
<span style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace', flexShrink:0 }}>
{r.date}
</span>
)}
<div style={{ flex:1, minWidth:0 }}>
<DirnameWrapped dirname={r.dirname} />
<div style={{ display:'flex', gap:8, marginTop:2 }}>
{r.size && <span style={{ fontSize:9, color:'rgba(255,255,255,0.35)', fontFamily:'monospace' }}>{r.size}</span>}
{r.date && <span style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace' }}>{r.date}</span>}
</div>
</div>
</a>
))}
</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 }) {
return (
<div style={{ padding:'28px 20px', textAlign:'center',