diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 08b3913..a2747a2 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -244,14 +244,20 @@ router.get('/movie/:id', authenticate, async (req, res) => { } catch(e) { res.status(500).json({ error: e.message }); } }); -// xREL separat – lädt im Hintergrund, cached per tmdb_id +// xREL on-demand – ?refresh=1 ignoriert den Cache (erneute Suche) router.get('/movie/:id/xrel', authenticate, async (req, res) => { try { const cacheKey = `xrel:tmdb:${req.params.id}`; - const cached = cacheGet(cacheKey); - if (cached !== undefined) return res.json({ xrel: cached, has_xrel: cached.length > 0 }); + const forceRefresh = req.query.refresh === '1'; + + if (!forceRefresh) { + const cached = cacheGet(cacheKey); + if (cached !== undefined) return res.json({ xrel: cached, has_xrel: cached.length > 0 }); + } else { + // Cache-Eintrag löschen für diesen Film + db.prepare('DELETE FROM xrel_cache WHERE cache_key=?').run(cacheKey); + } - // Brauchen Titel + IMDb-ID const [detail, extIds] = await Promise.all([ tmdb(`/movie/${req.params.id}`, { append_to_response: '' }).catch(()=>({})), tmdb(`/movie/${req.params.id}/external_ids`).catch(()=>({})), @@ -262,23 +268,7 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => { } catch(e) { res.status(500).json({ error: e.message }); } }); -// Badge-Check: bis zu 3 Filme parallel, cached in xrel:tmdb:ID -router.post('/xrel-check', authenticate, async (req, res) => { - const movies = (req.body.movies||[]).slice(0,3); - if (!movies.length) return res.json({}); - const result = {}; - await Promise.all(movies.map(async m => { - const cacheKey = `xrel:tmdb:${m.id}`; - const cached = cacheGet(cacheKey); - if (cached !== undefined) { result[m.id] = cached.length > 0; return; } - try { - const xrel = await getXrelData(m.id, m.imdb_id||null, m.title, m.original_title); - result[m.id] = xrel.length > 0; - } catch(_) { result[m.id] = false; } - })); - res.json(result); -}); router.get('/favorites', authenticate, (req, res) => { res.json(db.prepare('SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC').all(req.user.id)); @@ -304,8 +294,6 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => { genreCache = null; res.json({ ok: true }); }); -router.delete('/xrel-cache', authenticate, requireAdmin, (req, res) => { - res.json({ deleted: db.prepare('DELETE FROM xrel_cache').run().changes }); -}); + module.exports = router; diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 965763c..296b96d 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -40,16 +40,8 @@ const FSK_COLOR = fsk => { export default function Media({ toast, mobile }) { const [activeTool, setActiveTool] = useState('kino'); const [modal, setModal] = useState(null); + const [xrelIds, setXrelIds] = useState(new Set()); // tmdb_ids mit bekannten Releases - // 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 }); @@ -126,40 +118,25 @@ export default function Media({ toast, mobile }) { )} - {isAdmin && ( -
- -
- )} +
{active?.label}
{active?.desc}
- {activeTool === 'kino' && } - {activeTool === 'demnächst' && } + {activeTool === 'kino' && } + {activeTool === 'demnächst' && } {activeTool === 'favoriten' && }
{modal && createPortal( - setModal(null)} mobile={mobile} />, + setModal(null)} + mobile={mobile} + onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))} + />, document.body )} @@ -167,11 +144,10 @@ export default function Media({ toast, mobile }) { } // ── Film-Karte ──────────────────────────────────────────────────────────────── -function MovieCard({ movie, favIds, xrelIds, onToggleFav, onOpenModal }) { +function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) { const isFav = favIds.has(movie.id); const isTop = movie.rank != null && movie.rank <= 10; const fskStyle = FSK_COLOR(movie.fsk); - const hasXrel = xrelIds?.has(movie.id); return (
m.id).join(','); - useEffect(() => { - if (!movieIds) return; - const moviesMeta = movies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title })); - let active = true; - (async () => { - for (let bi = 0; bi < moviesMeta.length; bi += 3) { - if (!active) break; - const chunk = moviesMeta.slice(bi, bi + 3); - try { - const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }); - if (!active) break; - chunk.forEach(m => { if (result[m.id]) setXrelIds(prev => new Set([...prev, m.id])); }); - } catch (_) {} - } - })(); - return () => { active = false; }; - }, [movieIds]); const onToggle = async (movie, isFav) => { try { await toggleFav(movie, isFav); } catch (e) { alert(e.message); } @@ -333,7 +290,7 @@ function KinoGrid({ onOpenModal }) { <>
★ TOP 10 – MEISTGESEHEN
- {top10.map(m => )} + {top10.map(m => )}
)} @@ -341,7 +298,7 @@ function KinoGrid({ onOpenModal }) { <>
WEITERE FILME IM KINO
- {rest.map(m => )} + {rest.map(m => )}
)} @@ -350,11 +307,10 @@ function KinoGrid({ onOpenModal }) { } // ── Demnächst – wochenweise Neustarts ──────────────────────────────────────── -function DemnächstGrid({ onOpenModal }) { +function DemnächstGrid({ onOpenModal, xrelIds }) { const [groups, setGroups] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const [xrelIds, setXrelIds] = useState(new Set()); const [favIds, toggleFav] = useFavIds(); useEffect(() => { @@ -365,25 +321,6 @@ function DemnächstGrid({ onOpenModal }) { .finally(() => setLoading(false)); }, []); - const groupIds = Object.values(groups).flat().map(m => m.id).join(','); - useEffect(() => { - if (!groupIds) return; - const allMovies = Object.values(groups).flat(); - const moviesMeta = allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title })); - let active = true; - (async () => { - for (let bi = 0; bi < moviesMeta.length; bi += 3) { - if (!active) break; - const chunk = moviesMeta.slice(bi, bi + 3); - try { - const result = await api('/tools/media/xrel-check', { method:'POST', body:{ movies: chunk } }); - if (!active) break; - chunk.forEach(m => { if (result[m.id]) setXrelIds(prev => new Set([...prev, m.id])); }); - } catch (_) {} - } - })(); - return () => { active = false; }; - }, [groupIds]); const onToggle = async (movie, isFav) => { try { await toggleFav(movie, isFav); } catch (e) { alert(e.message); } @@ -405,7 +342,7 @@ function DemnächstGrid({ onOpenModal }) { {movies.length} Film{movies.length!==1?'e':''}
- {movies.map(m => )} + {movies.map(m => )}
))} @@ -501,28 +438,34 @@ function FavoritenKalender({ onOpenModal }) { } // ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── -function MovieModal({ tmdbId, onClose, mobile }) { - const [detail, setDetail] = useState(null); - const [xrel, setXrel] = useState(null); // null=lädt, []+=fertig - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); +function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { + const [detail, setDetail] = useState(null); + const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht + const [xrelLoading, setXrelLoading] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); useEffect(() => { setLoading(true); setDetail(null); setXrel(null); setError(''); - // TMDb-Daten sofort laden und anzeigen api(`/tools/media/movie/${tmdbId}`) .then(d => { setDetail(d); setLoading(false); }) .catch(e => { setError(e.message); setLoading(false); }); - // xREL separat und unabhängig nachladen - api(`/tools/media/movie/${tmdbId}/xrel`) - .then(d => setXrel(d.xrel || [])) - .catch(() => setXrel([])); - // Body-Scroll sperren const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [tmdbId]); + const searchXrel = async (refresh = false) => { + setXrelLoading(true); + try { + const url = `/tools/media/movie/${tmdbId}/xrel${refresh ? '?refresh=1' : ''}`; + const d = await api(url); + setXrel(d.xrel || []); + if (d.has_xrel) onHasXrel?.(tmdbId); + } catch(_) { setXrel([]); } + finally { setXrelLoading(false); } + }; + // ESC schließt useEffect(() => { const h = e => { if (e.key === 'Escape') onClose(); }; @@ -657,13 +600,30 @@ function MovieModal({ tmdbId, onClose, mobile }) { {/* xREL Releases */} - {xrel === null && ( -
⏳ Suche deutsche Releases…
- )} +
+
+
RELEASES (XREL.TO)
+ +
+ {xrel !== null && xrel.length === 0 && ( +
+ Keine deutschen Releases gefunden +
+ )} +
{xrel !== null && xrel.length > 0 && ( -
-
RELEASES (XREL.TO)
+
{xrel.map((r, i) => (