From 6b566d721f39ca21b76a4a2c7f4f2cfa1ba5f2df Mon Sep 17 00:00:00 2001 From: Dicken Date: Thu, 11 Jun 2026 14:33:25 +0200 Subject: [PATCH] feat: Suchen-Tab mit Filmsuche, Such-Endpoint mit Sprach-/Duplikat-Filter --- backend/src/tools/media/routes.js | 23 ++++++++ frontend/src/tools/media.jsx | 89 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index ec2bbeb..5e5d636 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -291,6 +291,29 @@ router.get('/upcoming', authenticate, async (req, res) => { } catch(e) { res.status(500).json({ error: e.message }); } }); +// GET /api/tools/media/search?q=TITEL – Filmsuche +router.get('/search', authenticate, async (req, res) => { + try { + const q = (req.query.q || '').trim(); + if (!q || q.length < 2) return res.json([]); + const [results, genreMap] = await Promise.all([ + tmdb('/search/movie', { query: q, include_adult: 'false' }), + getGenreMap(), + ]); + const seen = new Set(); + const filtered = (results.results || []) + .filter(m => { + if (BLOCKED_LANGS.has(m.original_language)) return false; + if (seen.has(m.id)) return false; + seen.add(m.id); + return true; + }) + .slice(0, 20) + .map(m => enrichMovie(m, genreMap, null, null)); + res.json(filtered); + } catch(e) { res.status(500).json({ error: e.message }); } +}); + // Modal: TMDb-Daten sofort, xREL via separaten Endpoint router.get('/movie/:id', authenticate, async (req, res) => { try { diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 51c4699..a0dc9e9 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -9,6 +9,7 @@ const TOOL_DEFS = [ { id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino (DE) – alle laufenden Filme', ready: true }, { id: 'demnΓ€chst', label: 'πŸ—“ DemnΓ€chst', desc: 'Neustarts wochenweise – nΓ€chste Wochen', ready: true }, { id: 'favoriten', label: '❀ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true }, + { id: 'suchen', label: 'πŸ” Suchen', desc: 'Filme suchen und Infos abrufen', ready: true }, ]; const getIcon = lbl => [...lbl][0] ?? '🎬'; @@ -128,6 +129,7 @@ export default function Media({ toast, mobile }) { {activeTool === 'kino' && setXrelIds(prev => new Set([...prev, ...ids]))} />} {activeTool === 'demnΓ€chst' && setXrelIds(prev => new Set([...prev, ...ids]))} />} {activeTool === 'favoriten' && } + {activeTool === 'suchen' && setXrelIds(prev => new Set([...prev, ...ids]))} />} {modal && createPortal( @@ -367,6 +369,93 @@ function DemnΓ€chstGrid({ onOpenModal, xrelIds, onXrelLoaded }) { ); } +// ── Suche ──────────────────────────────────────────────────────────────────── +function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) { + const [query, setQuery] = useState(''); + const [results, setResults] = useState(null); // null=noch nicht gesucht + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [favIds, toggleFav] = useFavIds(); + const inputRef = useRef(null); + + const doSearch = async (q = query) => { + const trimmed = q.trim(); + if (!trimmed || trimmed.length < 2) return; + setLoading(true); setError(''); setResults(null); + try { + const list = await api(`/tools/media/search?q=${encodeURIComponent(trimmed)}`); + setResults(list); + // xREL-Badges aus Cache + if (list.length) { + const ids = list.map(m => m.id).join(','); + api(`/tools/media/xrel-cached?ids=${ids}`) + .then(r => { + const hits = Object.entries(r).filter(([,v])=>v).map(([k])=>Number(k)); + if (hits.length) onXrelLoaded?.(hits); + }).catch(()=>{}); + } + } catch(e) { setError(e.message); } + finally { setLoading(false); } + }; + + const onToggle = async (movie, isFav) => { + try { await toggleFav(movie, isFav); } catch(e) { alert(e.message); } + }; + + const onKey = e => { if (e.key === 'Enter') doSearch(); }; + + return ( +
+ {/* Suchfeld */} +
+ setQuery(e.target.value)} + onKeyDown={onKey} + placeholder="Filmtitel eingeben…" + style={{ + flex:1, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.15)', + borderRadius:8, padding:'9px 14px', color:'#fff', fontFamily:"'Space Mono',monospace", + fontSize:13, outline:'none', + }} + /> + +
+ + {error && } + + {results === null && !loading && ( + Filmtitel eingeben und Suchen drΓΌcken + )} + + {results !== null && results.length === 0 && ( + Keine Filme gefunden fΓΌr β€ž{query}" + )} + + {results !== null && results.length > 0 && ( +
+ {results.map(m => ( + + ))} +
+ )} +
+ ); +} + // ── Favoriten Kalender ──────────────────────────────────────────────────────── function FavoritenKalender({ onOpenModal }) { const [favs, setFavs] = useState([]);