feat: Suchen-Tab mit Filmsuche, Such-Endpoint mit Sprach-/Duplikat-Filter

This commit is contained in:
2026-06-11 14:33:25 +02:00
parent cd50239fd0
commit 6b566d721f
2 changed files with 112 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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' && <KinoGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
{activeTool === 'demnächst' && <DemnächstGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
{activeTool === 'favoriten' && <FavoritenKalender onOpenModal={setModal} />}
{activeTool === 'suchen' && <SucheGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
</div>
{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 (
<div>
{/* Suchfeld */}
<div style={{ display:'flex', gap:8, marginBottom:20 }}>
<input
ref={inputRef}
value={query}
onChange={e => 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',
}}
/>
<button
onClick={() => doSearch()}
disabled={loading || query.trim().length < 2}
style={{
background:'rgba(78,205,196,0.15)', border:'1px solid rgba(78,205,196,0.3)',
color:'#4ecdc4', borderRadius:8, padding:'9px 18px', cursor:'pointer',
fontFamily:"'Space Mono',monospace", fontSize:12, fontWeight:700,
opacity: loading || query.trim().length < 2 ? 0.5 : 1,
}}
>
{loading ? '⏳' : '🔍 Suchen'}
</button>
</div>
{error && <ErrorBox msg={error} />}
{results === null && !loading && (
<StatusBox>Filmtitel eingeben und Suchen drücken</StatusBox>
)}
{results !== null && results.length === 0 && (
<StatusBox>Keine Filme gefunden für {query}"</StatusBox>
)}
{results !== null && results.length > 0 && (
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
{results.map(m => (
<MovieCard key={m.id} movie={m} favIds={favIds} hasXrel={xrelIds?.has(m.id)}
onToggleFav={onToggle} onOpenModal={onOpenModal} />
))}
</div>
)}
</div>
);
}
// ── Favoriten Kalender ────────────────────────────────────────────────────────
function FavoritenKalender({ onOpenModal }) {
const [favs, setFavs] = useState([]);