fix: badge 1 Call statt 3, COMPLETE.BLURAY/UHD/remux filter, Cache-Button für Admins, Produktionsland

This commit is contained in:
2026-06-11 00:39:18 +02:00
parent ad4f2dbb72
commit 32447b53a7
2 changed files with 70 additions and 9 deletions

View File

@@ -93,7 +93,7 @@ function enrichMovie(m, genreMap, fsk, rank) {
} }
// ── xREL ───────────────────────────────────────────────────────────────────── // ── xREL ─────────────────────────────────────────────────────────────────────
const QUALITY_BLOCKLIST = /\b(2160p|4k|uhd|uhdbluray|uhdbd|complete|bluray|blu-ray|remux|hevc|x265|h265)\b/i; const QUALITY_BLOCKLIST = r => /\b(2160p|4k|uhd|uhdbluray|uhdbd|remux)\b/i.test(r) || /COMPLETE[._](?:BLURAY|BD)/i.test(r);
async function xrelFetch(path, params = {}) { async function xrelFetch(path, params = {}) {
const url = new URL(XREL_BASE + path); const url = new URL(XREL_BASE + path);
@@ -161,7 +161,7 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
for (const r of sceneData.list||[]) { for (const r of sceneData.list||[]) {
if (!/\b(german|deutsch)(\.dl)?\b/i.test(r.dirname||'')) continue; if (!/\b(german|deutsch)(\.dl)?\b/i.test(r.dirname||'')) continue;
if (QUALITY_BLOCKLIST.test(r.dirname||'')) continue; if (QUALITY_BLOCKLIST(r.dirname||'')) continue;
results.push({ results.push({
dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null, dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null,
category: 'Scene', url: r.link_href, category: 'Scene', url: r.link_href,
@@ -170,7 +170,7 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
} }
for (const r of p2pData.list||[]) { for (const r of p2pData.list||[]) {
if (r.main_lang !== 'german') continue; if (r.main_lang !== 'german') continue;
if (QUALITY_BLOCKLIST.test(r.dirname||'')) continue; if (QUALITY_BLOCKLIST(r.dirname||'')) continue;
results.push({ results.push({
dirname: r.dirname, size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null, dirname: r.dirname, size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null,
category: r.category?.sub_cat||'P2P', url: r.link_href, category: r.category?.sub_cat||'P2P', url: r.link_href,
@@ -262,17 +262,47 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
} catch(e) { res.status(500).json({ error: e.message }); } } catch(e) { res.status(500).json({ error: e.message }); }
}); });
// Badge-Check: benutzt denselben Cache wie Modal // Badge-Check: nur prüfen ob ext_info_id existiert (1 Call statt 3)
// Volle Releases werden erst beim Modal-Öffnen geladen
router.post('/xrel-check', authenticate, async (req, res) => { router.post('/xrel-check', authenticate, async (req, res) => {
const movies = (req.body.movies||[]).slice(0,1); const movies = (req.body.movies||[]).slice(0,1);
if (!movies.length) return res.json({}); if (!movies.length) return res.json({});
const m = movies[0]; const m = movies[0];
const cacheKey = `xrel:tmdb:${m.id}`;
// Wenn volle Daten bereits gecacht: sofort antworten
const fullCacheKey = `xrel:tmdb:${m.id}`;
const fullCached = cacheGet(fullCacheKey);
if (fullCached !== undefined) return res.json({ [m.id]: fullCached.length > 0 });
// Sonst: nur ext_info_id suchen (Badge-only Cache, 6h)
const badgeCacheKey = `xrel:badge:${m.id}`;
const badgeCached = cacheGet(badgeCacheKey);
if (badgeCached !== undefined) return res.json({ [m.id]: badgeCached });
try { try {
const cached = cacheGet(cacheKey); const clean = (s) => (s||'').replace(/[*#!?:]/g,'').trim().toLowerCase();
if (cached !== undefined) return res.json({ [m.id]: cached.length > 0 }); const queries = [m.original_title, m.title].filter(Boolean).filter((v,i,a) => a.indexOf(v)===i);
const xrel = await getXrelData(m.id, m.imdb_id||null, m.title, m.original_title); const searchResults = await Promise.all(queries.map(async q => {
res.json({ [m.id]: xrel.length > 0 }); try { return await xrelFetch('/search/ext_info.json', { q: q.replace(/[*#!?:]/g,''), type:'movie', limit:10 }); }
catch(_) { return null; }
}));
let found = false;
// IMDb match
if (m.imdb_id) {
for (const data of searchResults) {
if ((data?.results||[]).some(h => (h.uris||[]).includes(`imdb:${m.imdb_id}`))) { found = true; break; }
}
}
// Titel-match Fallback
if (!found) {
for (const data of searchResults) {
if ((data?.results||[]).some(h => clean(h.title)===clean(queries[0]) || clean(h.alt_title)===clean(queries[0]))) { found = true; break; }
}
}
cacheSet(badgeCacheKey, found, 6*60*60*1000);
res.json({ [m.id]: found });
} catch(_) { res.json({ [m.id]: false }); } } catch(_) { res.json({ [m.id]: false }); }
}); });

View File

@@ -40,6 +40,16 @@ const FSK_COLOR = fsk => {
export default function Media({ toast, mobile }) { export default function Media({ toast, mobile }) {
const [activeTool, setActiveTool] = useState('kino'); const [activeTool, setActiveTool] = useState('kino');
const [modal, setModal] = useState(null); const [modal, setModal] = useState(null);
// Admin-Status aus JWT lesen
const isAdmin = (() => {
try {
const token = localStorage.getItem('sk_token');
if (!token) return false;
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.role === 'admin';
} catch(_) { return false; }
})();
const chipScrollRef = useRef(null); const chipScrollRef = useRef(null);
const [chipScroll, setChipScroll] = useState({ left: false, right: true }); const [chipScroll, setChipScroll] = useState({ left: false, right: true });
@@ -116,6 +126,27 @@ export default function Media({ toast, mobile }) {
</div> </div>
)} )}
{isAdmin && (
<div style={{ display:'flex', justifyContent:'flex-end', marginBottom:8 }}>
<button onClick={async () => {
if (!confirm('xREL-Cache leeren?')) return;
try {
const r = await fetch('/api/tools/media/xrel-cache', {
method: 'DELETE',
headers: { Authorization: 'Bearer ' + localStorage.getItem('sk_token') }
});
const d = await r.json();
toast(`Cache geleert (${d.deleted} Einträge) ✓`);
} catch(e) { toast('Fehler: ' + e.message, 'error'); }
}} style={{
fontSize: 10, fontFamily: 'monospace', cursor: 'pointer',
background: 'rgba(255,107,157,0.1)', border: '1px solid rgba(255,107,157,0.25)',
color: 'rgba(255,107,157,0.7)', borderRadius: 6, padding: '4px 10px',
}}>
🗑 xREL-Cache leeren
</button>
</div>
)}
<div style={{ ...S.card }}> <div style={{ ...S.card }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:18, flexWrap:'wrap' }}> <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:18, flexWrap:'wrap' }}>
<div style={{ ...S.head, marginBottom:0 }}>{active?.label}</div> <div style={{ ...S.head, marginBottom:0 }}>{active?.label}</div>