diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index e299bf9..a3a111f 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -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 }); } }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 3a22319..d30144f 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -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 }) { )} + {isAdmin && ( +