import { useState, useEffect, useCallback, useRef } from 'react'; import { createPortal } from 'react-dom'; import { S, api } from '../lib.js'; const POSTER = 'https://image.tmdb.org/t/p/w342'; const BACKDROP = 'https://image.tmdb.org/t/p/w780'; 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: 'streaming', label: 'πŸ“‘ Streaming', desc: 'Top 10 der deutschen Streaming-Anbieter', 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] ?? '🎬'; const getText = lbl => { const c = [...lbl]; return c.slice(1).join('').trim() || lbl; }; const fmtDE = d => d ? new Date(d).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' }) : ''; const fmtWeek = dateStr => { const d = new Date(dateStr); // KW berechnen const jan4 = new Date(d.getFullYear(), 0, 4); const kw = Math.ceil(((d - jan4) / 86400000 + jan4.getDay() + 1) / 7); const end = new Date(d); end.setDate(d.getDate() + 6); return `KW ${kw} Β· ${d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'})} – ${end.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})}`; }; const FSK_COLOR = fsk => { if (!fsk) return null; const n = parseInt(fsk); if (n === 0) return { bg:'#fff', color:'#000' }; if (n === 6) return { bg:'#f9e200', color:'#000' }; if (n === 12) return { bg:'#00a859', color:'#fff' }; if (n === 16) return { bg:'#0070bb', color:'#fff' }; if (n === 18) return { bg:'#e2001a', color:'#fff' }; return { bg:'#444', color:'#fff' }; }; export default function Media({ toast, mobile, initTab, onInitTabConsumed }) { const [activeTool, setActiveTool] = useState('kino'); const [favIds, toggleFav] = useFavIds(); // Auf Custom Event hΓΆren (z.B. Dashboard-Badge β†’ Favoriten) useEffect(() => { const handler = (e) => setActiveTool(e.detail); window.addEventListener('media-navigate', handler); return () => window.removeEventListener('media-navigate', handler); }, []); // Wenn Favoriten-Tab geΓΆffnet wird β†’ user-notify (Badge verschwindet) useEffect(() => { if (activeTool === 'favoriten') { api('/tools/media/favorites/user-notify', { method:'POST', body:{} }).catch(()=>{}); } }, [activeTool]); const [modal, setModal] = useState(null); // { id, media_type } const [xrelIds, setXrelIds] = useState(new Set()); // tmdb_ids mit bekannten Releases const chipScrollRef = useRef(null); const [chipScroll, setChipScroll] = useState({ left: false, right: true }); const onChipScroll = () => { const el = chipScrollRef.current; if (!el) return; setChipScroll({ left: el.scrollLeft > 4, right: el.scrollLeft < el.scrollWidth - el.clientWidth - 4 }); }; const active = TOOL_DEFS.find(t => t.id === activeTool); return (

Media

Kino Β· Vorschau Β· Favoriten
{mobile ? (
{chipScroll.left && ( )}
{TOOL_DEFS.map(t => ( ))}
{chipScroll.right && ( )}
) : (
{TOOL_DEFS.map(t => ( ))}
)}
{active?.label}
{active?.desc}
{activeTool === 'kino' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} {activeTool === 'demnΓ€chst' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} {activeTool === 'streaming' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} {activeTool === 'favoriten' && } {activeTool === 'suchen' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
{modal && createPortal( setModal(null)} mobile={mobile} onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))} noYearFilter={activeTool === 'suchen' || activeTool === 'streaming'} isFav={favIds.has(modal?.id || modal)} onToggleFav={movie => toggleFav(movie, favIds.has(modal?.id || modal))} />, document.body )}
); } // ── Film-Karte ──────────────────────────────────────────────────────────────── 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); return (
onOpenModal({ id: movie.id, media_type: movie.media_type || 'movie' })}> {movie.poster_path ? {movie.title} :
🎬
} {/* TV-Badge */} {movie.media_type === 'tv' && (
SERIE
)} {/* Top-Badge */} {isTop && (
#{movie.rank}
)} {/* Rating */} {movie.vote_average > 0 && (
β˜… {movie.vote_average.toFixed(1)}
)} {/* FSK */} {fskStyle && (
{movie.fsk}
)} {/* xREL-Badge */} {hasXrel && (
xREL
)} {/* Fav */}
onOpenModal({ id: movie.id, media_type: movie.media_type || 'movie' })}> {isTop && (
β˜… TOP {movie.rank <= 10 ? '10' : ''}
)}
{movie.title}
{movie.original_title && movie.original_title !== movie.title && (
({movie.original_title})
)} {movie.genres?.length > 0 && (
{movie.genres.slice(0,2).join(' Β· ')}
)} {movie.release_date && (
{fmtDE(movie.release_date)}
)}
); } // ── Favoriten-Hook ──────────────────────────────────────────────────────────── function useFavIds() { const [favIds, setFavIds] = useState(new Set()); const load = useCallback(async () => { try { const d = await api('/tools/media/favorites'); setFavIds(new Set(d.map(f => f.tmdb_id))); } catch (_) {} }, []); useEffect(() => { load(); }, []); const toggle = async (movie, isFav) => { if (isFav) { await api(`/tools/media/favorites/${movie.id}`, { method:'DELETE' }); setFavIds(prev => { const s = new Set(prev); s.delete(movie.id); return s; }); } else { await api('/tools/media/favorites', { body: { tmdb_id: movie.id, title: movie.title, poster_path: movie.poster_path || '', release_date: movie.release_date || '', release_date_de: fmtDE(movie.release_date), genres: movie.genres || [], fsk: movie.fsk || '', media_type: movie.media_type || 'movie', }}); setFavIds(prev => new Set([...prev, movie.id])); } }; return [favIds, toggle]; } // ── Kino Grid – alle laufenden Filme ───────────────────────────────────────── function KinoGrid({ onOpenModal, xrelIds, onXrelLoaded }) { const [movies, setMovies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [favIds, toggleFav] = useFavIds(); // Filme laden + Cache-Status fΓΌr Badges useEffect(() => { setLoading(true); setError(''); api('/tools/media/now-playing') .then(list => { setMovies(list); // Badges aus Cache laden (kein xREL-Call, nur DB) const ids = list.map(m => m.id).join(','); if (ids) api(`/tools/media/xrel-cached?ids=${ids}`) .then(result => { const hits = Object.entries(result).filter(([,v])=>v).map(([k])=>Number(k)); if (hits.length) onXrelLoaded?.(hits); }).catch(()=>{}); }) .catch(e => setError(e.message)) .finally(() => setLoading(false)); }, []); // xREL-Badges: startet einmalig wenn Filme geladen, ID-String als stable dependency const onToggle = async (movie, isFav) => { try { await toggleFav(movie, isFav); } catch (e) { alert(e.message); } }; if (loading) return ⏳ LΓ€dt Kinocharts…; if (error) return ; if (!movies.length) return Keine Filme gefunden; const top10 = movies.filter(m => m.rank != null); const rest = movies.filter(m => m.rank == null); return (
{top10.length > 0 && ( <>
β˜… TOP 10 – MEISTGESEHEN
{top10.map(m => )}
)} {rest.length > 0 && ( <>
WEITERE FILME IM KINO
{rest.map(m => )}
)}
); } // ── DemnΓ€chst – wochenweise Neustarts ──────────────────────────────────────── function DemnΓ€chstGrid({ onOpenModal, xrelIds, onXrelLoaded }) { const [groups, setGroups] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [favIds, toggleFav] = useFavIds(); useEffect(() => { setLoading(true); setError(''); api('/tools/media/upcoming') .then(grps => { setGroups(grps); const ids = Object.values(grps).flat().map(m => m.id).join(','); if (ids) api(`/tools/media/xrel-cached?ids=${ids}`) .then(result => { const hits = Object.entries(result).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); } }; if (loading) return ⏳ LΓ€dt Neustarts…; if (error) return ; const entries = Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)); if (!entries.length) return Keine kommenden Filme gefunden; return (
{entries.map(([weekStart, movies]) => (
NEUSTART {fmtWeek(weekStart)}
{movies.length} Film{movies.length!==1?'e':''}
{movies.map(m => )}
))}
); } // ── Suche ──────────────────────────────────────────────────────────────────── // ── Aufklappbare Film/Serien-Sektion ───────────────────────────────────────── function CollapsibleSection({ label, count, children, defaultOpen=true }) { const [open, setOpen] = useState(defaultOpen); return (
{open && children}
); } function SectionedGrid({ films, serien, favIds, xrelIds, onToggle, onOpenModal }) { return (
{films.length > 0 && (
{films.map(m => )}
)} {serien.length > 0 && (
{serien.map(m => )}
)}
); } function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp, onToggleFav: toggleFavProp }) { const [favIdsLocal, onToggleLocal] = useFavIds(); const favIds = favIdsProp || favIdsLocal; const onToggle = toggleFavProp || onToggleLocal; const [providers, setProviders] = useState([]); const [activeProvider, setActiveProvider] = useState(null); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { api('/tools/media/streaming/providers') .then(list => { setProviders(list); if (list.length) setActiveProvider(list[0].id); }) .catch(e => setError(e.message)); }, []); useEffect(() => { if (!activeProvider) return; setLoading(true); setError(''); api(`/tools/media/streaming/${activeProvider}`) .then(setData) .catch(e => setError(e.message)) .finally(() => setLoading(false)); }, [activeProvider]); return (
Top 10 Filme & Serien pro Anbieter (DE) Β· nur Titel ab {new Date().getFullYear()-1} Β· sortiert nach PopularitΓ€t
{/* Anbieter-Auswahl */}
{providers.map(p => ( ))}
{error && } {loading && ⏳ LΓ€dt Top 10…} {!loading && data && ( )}
); } 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)}`); // Neueste zuerst list.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||'')); 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="Film- oder Serientitel 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 Ergebnisse fΓΌr β€ž{query}" )} {results !== null && results.length > 0 && (() => { const films = results.filter(m => m.media_type !== 'tv'); const serien = results.filter(m => m.media_type === 'tv'); return ; })()}
); } // ── FavCard – einzelner Favoriten-Eintrag ───────────────────────────────────── function FavCard({ f, onOpenModal, onRemove }) { const genres = (() => { try { return JSON.parse(f.genres||'[]'); } catch { return []; } })(); const fskStyle = FSK_COLOR(f.fsk); const isAcked = !!f.acknowledged; // Badge bleibt bis Favorit gelΓΆscht return (
onOpenModal({ id: f.tmdb_id, media_type: f.media_type || 'movie' })} style={{ display:'flex', gap:12, alignItems:'flex-start', background: isAcked ? 'rgba(74,222,128,0.05)' : 'rgba(255,255,255,0.03)', border: `1px solid ${isAcked ? 'rgba(74,222,128,0.25)' : 'rgba(255,255,255,0.06)'}`, borderRadius:8, padding:'10px 12px', cursor:'pointer', position:'relative', }}> {isAcked && (
βœ“ bestΓ€tigt
)}
{f.poster_path ? {f.title} :
{f.media_type==='tv'?'πŸ“Ί':'🎬'}
}
{f.title}
πŸ‡©πŸ‡ͺ {f.release_date_de || '–'}
{genres.length > 0 && (
{genres.slice(0,3).join(' Β· ')}
)} {f.fsk && ( FSK {f.fsk} )}
); } // ── Favoriten Kalender ──────────────────────────────────────────────────────── function FavoritenKalender({ onOpenModal }) { const [favs, setFavs] = useState([]); const [allFavs, setAllFavs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [adminTab, setAdminTab]= useState(false); const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { setLoading(true); setError(''); Promise.all([ api('/tools/media/favorites'), api('/tools/media/favorites/all').catch(() => null), ]).then(([mine, all]) => { setFavs(mine); if (all) { setAllFavs(all); setIsAdmin(true); } }).catch(e => setError(e.message)).finally(() => setLoading(false)); }, []); const ackAll = async () => { await api('/tools/media/favorites/acknowledge-all', { method:'POST', body:{} }); setAllFavs(prev => prev.map(f => ({ ...f, acknowledged: 1 }))); }; const removeFav = async (tmdbId) => { try { await api(`/tools/media/favorites/${tmdbId}`, { method:'DELETE' }); setFavs(prev => prev.filter(f => f.tmdb_id !== tmdbId)); } catch (e) { alert(e.message); } }; if (loading) return ⏳ LΓ€dt…; if (error) return ; // Admin-Ansicht: alle Favoriten nach User gruppiert if (isAdmin && adminTab) { const unacked = allFavs.filter(f => !f.acknowledged).length; const userGroups = {}; allFavs.forEach(f => { if (!userGroups[f.username]) userGroups[f.username] = []; userGroups[f.username].push(f); }); return (
{unacked > 0 && ( <> ⚠ {unacked} unquittiert )}
{!allFavs.length ? Noch keine Favoriten von irgendjemandem. : Object.entries(userGroups).map(([username, items]) => (
πŸ‘€ {username} {items.length} Favorit{items.length!==1?'en':''} {items.filter(f=>!f.acknowledged).length > 0 && ( {items.filter(f=>!f.acknowledged).length} neu )}
{items.map(f => (
{f.poster_path ? {f.title} :
{f.media_type==='tv'?'πŸ“Ί':'🎬'}
}
{f.title} {f.media_type==='tv' && SERIE}
⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : 'β€”'} {!!f.acknowledged && βœ“}
{!f.acknowledged && ( )}
))}
)) }
); } if (!favs.length) return (
{isAdmin && allFavs.filter(f=>!f.acknowledged).length > 0 && (
⚠ {allFavs.filter(f=>!f.acknowledged).length} unquittierte Favoriten
)} {isAdmin && ( )} Noch keine eigenen Favoriten – markiere Filme mit β™₯
); // Aufteilen in Filme und Serien, dann nach Datum gruppieren const filmeFavs = favs.filter(f => f.media_type !== 'tv'); const serienFavs = favs.filter(f => f.media_type === 'tv'); function buildGroups(list) { const g = {}; for (const f of list) { const d = f.release_date ? new Date(f.release_date) : null; const key = d ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}` : 'unbekannt'; const label = d ? d.toLocaleDateString('de-DE', { month:'long', year:'numeric' }) : 'Unbekannt'; if (!g[key]) g[key] = { label, items:[] }; g[key].items.push(f); } return g; } const filmGroups = buildGroups(filmeFavs); const serienGroups = buildGroups(serienFavs); return (
{isAdmin && (
{allFavs.filter(f=>!f.acknowledged).length > 0 && (
⚠ {allFavs.filter(f=>!f.acknowledged).length} unquittierte Favoriten
)}
)} {isAdmin && allFavs.filter(f=>!f.acknowledged).length > 0 && (
⚠ {allFavs.filter(f=>!f.acknowledged).length} unquittierte Favoriten
)} {filmeFavs.length > 0 && ( {Object.entries(filmGroups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
{group.label.toUpperCase()}
{group.items.length}Γ—
{group.items.map(f => )}
))} )} {serienFavs.length > 0 && ( {Object.entries(serienGroups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
{group.label.toUpperCase()}
{group.items.length}Γ—
{group.items.map(f => )}
))} )} {false && Object.entries({}).map(([key, group]) => (
{group.label.toUpperCase()}
{group.items.length} Film{group.items.length!==1?'e':''}
{group.items.map(f => { const genres = (() => { try { return JSON.parse(f.genres||'[]'); } catch { return []; } })(); const fskStyle = FSK_COLOR(f.fsk); return (
onOpenModal({ id: f.tmdb_id, media_type: f.media_type || 'movie' })} style={{ display:'flex', gap:12, alignItems:'flex-start', background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.06)', borderRadius:8, padding:'10px 12px', cursor:'pointer', }}>
{f.poster_path ? {f.title} :
🎬
}
{f.title}
πŸ‡©πŸ‡ͺ {f.release_date_de || '–'}
{genres.length > 0 && (
{genres.slice(0,3).join(' Β· ')}
)} {fskStyle && (
FSK {f.fsk}
)}
); })}
))}
); } // ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── function MovieModal({ tmdbId, mediaType='movie', onClose, mobile, onHasXrel, noYearFilter, isFav=false, onToggleFav }) { const [detail, setDetail] = useState(null); const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht const [xrelDates, setXrelDates] = useState([]); const [xrelLoading, setXrelLoading] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); // detail als Ref damit searchXrel immer den aktuellen Wert hat const detailRef = useRef(null); useEffect(() => { setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError(''); detailRef.current = null; api(`/tools/media/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}`) .then(d => { setDetail(d); detailRef.current = d; setLoading(false); // Gecachte Releases laden (nur fΓΌr Filme) const xrelParams = new URLSearchParams({ title: d.title || '', orig: d.original_title || '', imdb_id: d.imdb_id || '', year: noYearFilter ? '0' : (d.release_date ? d.release_date.slice(0,4) : ''), media_type: mediaType || 'movie', }); api(`/tools/media/movie/${tmdbId}/xrel?${xrelParams}`) .then(r => { if (r.xrel_dates?.length > 0) setXrelDates(r.xrel_dates); if (r.xrel?.length > 0) { setXrel(r.xrel); onHasXrel?.(tmdbId); } }) .catch(() => {}); }) .catch(e => { setError(e.message); setLoading(false); }); const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [tmdbId, mediaType]); const searchXrel = async (forceRefresh = false) => { const d = detailRef.current; if (!d) return; setXrelLoading(true); try { const params = new URLSearchParams({ title: d.title || '', orig: d.original_title || '', imdb_id: d.imdb_id || '', year: noYearFilter ? '0' : (d.release_date ? d.release_date.slice(0,4) : ''), media_type: mediaType || 'movie', }); if (forceRefresh || noYearFilter) params.set('refresh', '1'); const r = await api(`/tools/media/movie/${tmdbId}/xrel?${params}`); // Nur ΓΌberschreiben wenn neue Daten gefunden – sonst alte behalten if ((r.xrel || []).length > 0) { setXrel(r.xrel); onHasXrel?.(tmdbId); } else if (!forceRefresh) { // Erster Load ohne Ergebnis β†’ explizit leer setzen setXrel([]); } // Sonst: forceRefresh ohne Ergebnis β†’ alte Liste bleibt stehen if (r.xrel_dates?.length > 0) setXrelDates(r.xrel_dates); } catch(_) { setXrel([]); } finally { setXrelLoading(false); } }; // ESC schließt useEffect(() => { const h = e => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]); const fskStyle = detail ? FSK_COLOR(detail.fsk) : null; return (
e.stopPropagation()} style={{ background:'#1a1a1f', border:'1px solid rgba(255,255,255,0.12)', borderRadius: mobile ? '16px 16px 0 0' : 16, width: mobile ? '100%' : 560, maxHeight: mobile ? '88vh' : '85vh', display:'flex', flexDirection:'column', position:'relative', }}> {/* Backdrop + fixer Schließen-Button – NICHT im scrollenden Bereich */}
{detail?.backdrop_path && ( )} {detail?.backdrop_path && (
)}
{/* Schließen + Favorit – sticky außerhalb des Scroll-Containers */} {onToggleFav && detail && ( )} {/* Scrollbarer Inhalt */}
{loading && ⏳ LΓ€dt…} {error && } {detail && (<>
{/* Poster */}
{detail.poster_path ? {detail.title} :
🎬
}
{/* Info */}
{detail.title}
{detail.tagline && (
{detail.tagline}
)}
{detail.vote_average > 0 && β˜… {detail.vote_average.toFixed(1)}} {detail.media_type !== 'tv' && detail.runtime > 0 && ⏱ {detail.runtime} Min} {detail.media_type === 'tv' && detail.number_of_seasons > 0 && πŸ“Ί {detail.number_of_seasons} Staffel{detail.number_of_seasons!==1?'n':''}} {detail.media_type === 'tv' && detail.number_of_episodes > 0 && {detail.number_of_episodes} Ep.} {fskStyle && FSK {detail.fsk}} {detail.release_date && πŸ‡©πŸ‡ͺ {fmtDE(detail.release_date)}} {detail.countries?.length > 0 && 🌍 {detail.countries.slice(0,2).join(' Β· ')}}
{detail.genres?.length > 0 && (
{detail.genres.map(g => ( {g} ))}
)}
{xrelDates.length > 0 && (
ERSCHEINUNGSDATEN
{xrelDates.map((r, i) => (
{r.label} {fmtDE(r.date)}
))}
)} {detail.overview && (
HANDLUNG
{detail.overview}
)} {detail.director && (
REGIE
{detail.director}
)} {detail.cast?.length > 0 && (
BESETZUNG
{detail.cast.map(name => ( {name} ))}
)} {/* Links */}
{detail.tmdb_url && ( 🎬 TMDb )} {detail.imdb_url && ( ⭐ IMDb )}
{/* xREL Releases */}
{/* Web-Release aus frΓΌhestem WEB-Release ableiten */} {xrel?.length > 0 && (() => { const webRelease = xrel .filter(r => /web/i.test(r.dirname||'')) .sort((a,b) => (a.date||'').localeCompare(b.date||''))[0]; return webRelease?.date ? (
🌐 Erster Web-Release: {fmtDE(webRelease.date)} {webRelease.p2p ? 'P2P' : 'SCN'}
) : null; })()}
RELEASES (XREL.TO)
{xrel !== null && xrel.length === 0 && (
Keine deutschen Releases gefunden
)}
{xrel !== null && xrel.length > 0 && ( )} )}
{/* Ende scroll-wrapper */}
); } function Badge({ children, bg, color }) { return ( {children} ); } // Dirname zweizeilig: vor "German." trennen function DirnameWrapped({ dirname }) { if (!dirname) return null; const lower = dirname.toLowerCase(); const idx = lower.indexOf('german.'); if (idx < 0) { // Kein "German." – einfach umbrechen nach 40 Zeichen Worttrennung return {dirname}; } const before = dirname.slice(0, idx + 7); // inkl. "German." const after = dirname.slice(idx + 7); return ( {before} {after && <>
{after}}
); } function StatusBox({ children }) { return (
{children}
); } function ErrorBox({ msg }) { return (
⚠ {msg}
{msg?.includes('Token') && (
Token eintragen: Einstellungen β†’ Benutzer β†’ TMDB API
)}
); }