fix: badge 1 Call statt 3, COMPLETE.BLURAY/UHD/remux filter, Cache-Button für Admins, Produktionsland
This commit is contained in:
@@ -93,7 +93,7 @@ function enrichMovie(m, genreMap, fsk, rank) {
|
||||
}
|
||||
|
||||
// ── 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 = {}) {
|
||||
const url = new URL(XREL_BASE + path);
|
||||
@@ -161,7 +161,7 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
|
||||
|
||||
for (const r of sceneData.list||[]) {
|
||||
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({
|
||||
dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null,
|
||||
category: 'Scene', url: r.link_href,
|
||||
@@ -170,7 +170,7 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
|
||||
}
|
||||
for (const r of p2pData.list||[]) {
|
||||
if (r.main_lang !== 'german') continue;
|
||||
if (QUALITY_BLOCKLIST.test(r.dirname||'')) continue;
|
||||
if (QUALITY_BLOCKLIST(r.dirname||'')) continue;
|
||||
results.push({
|
||||
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,
|
||||
@@ -262,17 +262,47 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
|
||||
} 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) => {
|
||||
const movies = (req.body.movies||[]).slice(0,1);
|
||||
if (!movies.length) return res.json({});
|
||||
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 {
|
||||
const cached = cacheGet(cacheKey);
|
||||
if (cached !== undefined) return res.json({ [m.id]: cached.length > 0 });
|
||||
const xrel = await getXrelData(m.id, m.imdb_id||null, m.title, m.original_title);
|
||||
res.json({ [m.id]: xrel.length > 0 });
|
||||
const clean = (s) => (s||'').replace(/[*#!?:]/g,'').trim().toLowerCase();
|
||||
const queries = [m.original_title, m.title].filter(Boolean).filter((v,i,a) => a.indexOf(v)===i);
|
||||
const searchResults = await Promise.all(queries.map(async q => {
|
||||
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 }); }
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ const FSK_COLOR = fsk => {
|
||||
export default function Media({ toast, mobile }) {
|
||||
const [activeTool, setActiveTool] = useState('kino');
|
||||
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 [chipScroll, setChipScroll] = useState({ left: false, right: true });
|
||||
|
||||
@@ -116,6 +126,27 @@ export default function Media({ toast, mobile }) {
|
||||
</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={{ display:'flex', alignItems:'center', gap:10, marginBottom:18, flexWrap:'wrap' }}>
|
||||
<div style={{ ...S.head, marginBottom:0 }}>{active?.label}</div>
|
||||
|
||||
Reference in New Issue
Block a user