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 && (
-