fix: TV-Modal, Suche/Favoriten nach Typ getrennt, Pushover korrekte Tabelle
This commit is contained in:
@@ -361,6 +361,39 @@ router.get('/search', authenticate, async (req, res) => {
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// TV-Serie Detail
|
||||
router.get('/tv/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [detail, genreMap] = await Promise.all([
|
||||
tmdb(`/tv/${req.params.id}`, { append_to_response: 'credits,content_ratings' }),
|
||||
getGenreMap(),
|
||||
]);
|
||||
// FSK aus content_ratings (DE)
|
||||
let fsk = '';
|
||||
const deRating = (detail.content_ratings?.results || []).find(r => r.iso_3166_1 === 'DE');
|
||||
if (deRating?.rating) fsk = deRating.rating;
|
||||
|
||||
res.json({
|
||||
id: detail.id, media_type: 'tv',
|
||||
title: detail.name || detail.original_name,
|
||||
original_title: detail.original_name,
|
||||
tagline: detail.tagline || '',
|
||||
overview: detail.overview,
|
||||
poster_path: detail.poster_path,
|
||||
backdrop_path: detail.backdrop_path,
|
||||
release_date: detail.first_air_date || '',
|
||||
runtime: detail.episode_run_time?.[0] || null,
|
||||
vote_average: detail.vote_average,
|
||||
genres: (detail.genres||[]).map(g=>g.name),
|
||||
fsk,
|
||||
cast: (detail.credits?.cast||[]).slice(0,10).map(c=>({ name:c.name, character:c.character, profile_path:c.profile_path })),
|
||||
number_of_seasons: detail.number_of_seasons,
|
||||
number_of_episodes: detail.number_of_episodes,
|
||||
status: detail.status,
|
||||
});
|
||||
} 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 {
|
||||
@@ -496,15 +529,19 @@ router.post('/favorites', authenticate, (req, res) => {
|
||||
|
||||
if (r.changes > 0) {
|
||||
try {
|
||||
const pushToken = db.prepare("SELECT value FROM admin_settings WHERE key='pushover_token'").get()?.value;
|
||||
const pushUser = db.prepare("SELECT value FROM admin_settings WHERE key='pushover_user'").get()?.value;
|
||||
// Admin-Pushover-Einstellungen aus pushover_settings Tabelle
|
||||
const adminUser = db.prepare("SELECT id FROM users WHERE role='admin' LIMIT 1").get();
|
||||
const poCfg = adminUser
|
||||
? db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(adminUser.id)
|
||||
: null;
|
||||
const user = db.prepare('SELECT username FROM users WHERE id=?').get(req.user.id);
|
||||
if (pushToken && pushUser) {
|
||||
if (poCfg?.app_token && poCfg?.user_key) {
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: pushToken, user: pushUser,
|
||||
token: poCfg.app_token,
|
||||
user: poCfg.user_key,
|
||||
message: `${user?.username || 'Jemand'} hat "${title}" zur Merkliste hinzugefügt`,
|
||||
title: '⭐ Neue Favoriten',
|
||||
priority: 0,
|
||||
|
||||
@@ -40,7 +40,7 @@ const FSK_COLOR = fsk => {
|
||||
|
||||
export default function Media({ toast, mobile }) {
|
||||
const [activeTool, setActiveTool] = useState('kino');
|
||||
const [modal, setModal] = useState(null);
|
||||
const [modal, setModal] = useState(null); // { id, media_type }
|
||||
const [xrelIds, setXrelIds] = useState(new Set()); // tmdb_ids mit bekannten Releases
|
||||
|
||||
const chipScrollRef = useRef(null);
|
||||
@@ -134,7 +134,8 @@ export default function Media({ toast, mobile }) {
|
||||
|
||||
{modal && createPortal(
|
||||
<MovieModal
|
||||
tmdbId={modal}
|
||||
tmdbId={modal?.id || modal}
|
||||
mediaType={modal?.media_type || 'movie'}
|
||||
onClose={() => setModal(null)}
|
||||
mobile={mobile}
|
||||
onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))}
|
||||
@@ -158,7 +159,7 @@ function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) {
|
||||
borderRadius:10, overflow:'hidden', display:'flex', flexDirection:'column', cursor:'pointer',
|
||||
}}>
|
||||
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}
|
||||
onClick={() => onOpenModal(movie.id)}>
|
||||
onClick={() => onOpenModal({ id: movie.id, media_type: movie.media_type || 'movie' })}>
|
||||
{movie.poster_path
|
||||
? <img src={POSTER + movie.poster_path} alt={movie.title}
|
||||
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||
@@ -212,7 +213,7 @@ function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) {
|
||||
}}>{isFav ? '❤' : '🤍'}</button>
|
||||
</div>
|
||||
<div style={{ padding:'8px 10px', flex:1, display:'flex', flexDirection:'column', gap:3 }}
|
||||
onClick={() => onOpenModal(movie.id)}>
|
||||
onClick={() => onOpenModal({ id: movie.id, media_type: movie.media_type || 'movie' })}>
|
||||
{isTop && (
|
||||
<div style={{ fontSize:9, color:'#ffd700', fontFamily:'monospace', fontWeight:700, letterSpacing:1 }}>
|
||||
★ TOP {movie.rank <= 10 ? '10' : ''}
|
||||
@@ -459,17 +460,76 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
||||
)}
|
||||
|
||||
{results !== null && results.length === 0 && (
|
||||
<StatusBox>Keine Filme gefunden für „{query}"</StatusBox>
|
||||
<StatusBox>Keine Ergebnisse für „{query}"</StatusBox>
|
||||
)}
|
||||
|
||||
{results !== null && results.length > 0 && (
|
||||
{results !== null && results.length > 0 && (() => {
|
||||
const films = results.filter(m => m.media_type !== 'tv');
|
||||
const serien = results.filter(m => m.media_type === 'tv');
|
||||
return (
|
||||
<div>
|
||||
{films.length > 0 && (
|
||||
<div style={{marginBottom:20}}>
|
||||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,letterSpacing:1,marginBottom:8}}>🎬 FILME ({films.length})</div>
|
||||
<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} />
|
||||
))}
|
||||
{films.map(m => <MovieCard key={'m'+m.id} movie={m} favIds={favIds} hasXrel={xrelIds?.has(m.id)} onToggleFav={onToggle} onOpenModal={onOpenModal}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{serien.length > 0 && (
|
||||
<div>
|
||||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,letterSpacing:1,marginBottom:8}}>📺 SERIEN ({serien.length})</div>
|
||||
<div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(130px,1fr))',gap:12}}>
|
||||
{serien.map(m => <MovieCard key={'t'+m.id} movie={m} favIds={favIds} hasXrel={xrelIds?.has(m.id)} onToggleFav={onToggle} onOpenModal={onOpenModal}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
return (
|
||||
<div onClick={() => 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', position:'relative',
|
||||
}}>
|
||||
<div style={{ width:44, height:66, flexShrink:0, borderRadius:5, overflow:'hidden', background:'#111' }}>
|
||||
{f.poster_path
|
||||
? <img src={POSTER + f.poster_path} alt={f.title} style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center', justifyContent:'center', fontSize:18 }}>
|
||||
{f.media_type==='tv'?'📺':'🎬'}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div style={{ flex:1, minWidth:0 }}>
|
||||
<div style={{ fontSize:13, fontWeight:700, color:'#fff', fontFamily:"'Space Mono',monospace",
|
||||
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{f.title}</div>
|
||||
<div style={{ fontSize:10, color:'rgba(255,255,255,0.4)', marginTop:3, fontFamily:'monospace' }}>
|
||||
🇩🇪 {f.release_date_de || '–'}
|
||||
</div>
|
||||
{genres.length > 0 && (
|
||||
<div style={{ fontSize:10, color:'#4ecdc4', marginTop:2, fontFamily:'monospace' }}>
|
||||
{genres.slice(0,3).join(' · ')}
|
||||
</div>
|
||||
)}
|
||||
{f.fsk && (
|
||||
<span style={{ ...fskStyle, fontSize:9, padding:'1px 5px', borderRadius:3, marginTop:4, display:'inline-block' }}>
|
||||
FSK {f.fsk}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={e=>{ e.stopPropagation(); onRemove(f.tmdb_id); }} style={{
|
||||
background:'rgba(248,113,113,0.1)', border:'1px solid rgba(248,113,113,0.25)',
|
||||
borderRadius:5, padding:'3px 8px', color:'#f87171', fontFamily:'monospace',
|
||||
fontSize:10, cursor:'pointer', flexShrink:0,
|
||||
}}>✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -606,14 +666,23 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
const groups = {};
|
||||
for (const f of favs) {
|
||||
// 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 (!groups[key]) groups[key] = { label, items:[] };
|
||||
groups[key].items.push(f);
|
||||
if (!g[key]) g[key] = { label, items:[] };
|
||||
g[key].items.push(f);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
const filmGroups = buildGroups(filmeFavs);
|
||||
const serienGroups = buildGroups(serienFavs);
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:24 }}>
|
||||
@@ -631,7 +700,46 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
|
||||
{/* Filme */}
|
||||
{filmeFavs.length > 0 && (
|
||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,fontWeight:700,letterSpacing:1,
|
||||
borderBottom:'1px solid rgba(255,255,255,0.07)',paddingBottom:6}}>
|
||||
🎬 FILME ({filmeFavs.length})
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(filmGroups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
|
||||
<div key={'film-'+key}>
|
||||
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>
|
||||
<span>{group.label.toUpperCase()}</span>
|
||||
<div style={{ flex:1, height:1, background:'rgba(255,255,255,0.07)' }}/>
|
||||
<span style={{ fontSize:9 }}>{group.items.length} Film{group.items.length!==1?'e':''}</span>
|
||||
</div>
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:8 }}>
|
||||
{group.items.map(f => <FavCard key={f.tmdb_id} f={f} onOpenModal={onOpenModal} onRemove={removeFav}/>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Serien */}
|
||||
{serienFavs.length > 0 && (
|
||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,fontWeight:700,letterSpacing:1,
|
||||
borderBottom:'1px solid rgba(255,255,255,0.07)',paddingBottom:6,marginTop:8}}>
|
||||
📺 SERIEN ({serienFavs.length})
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(serienGroups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
|
||||
<div key={'serie-'+key}>
|
||||
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>
|
||||
<span>{group.label.toUpperCase()}</span>
|
||||
<div style={{ flex:1, height:1, background:'rgba(255,255,255,0.07)' }}/>
|
||||
<span style={{ fontSize:9 }}>{group.items.length} Serie{group.items.length!==1?'n':''}</span>
|
||||
</div>
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:8 }}>
|
||||
{group.items.map(f => <FavCard key={f.tmdb_id} f={f} onOpenModal={onOpenModal} onRemove={removeFav}/>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Alte groups-Schleife entfernt – DUMMY damit Code nicht bricht */}
|
||||
{false && Object.entries({}).map(([key, group]) => (
|
||||
<div key={key}>
|
||||
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>
|
||||
<span>{group.label.toUpperCase()}</span>
|
||||
@@ -643,7 +751,7 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
const genres = (() => { try { return JSON.parse(f.genres||'[]'); } catch { return []; } })();
|
||||
const fskStyle = FSK_COLOR(f.fsk);
|
||||
return (
|
||||
<div key={f.tmdb_id} onClick={() => onOpenModal(f.tmdb_id)} style={{
|
||||
<div key={f.tmdb_id} onClick={() => 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',
|
||||
@@ -686,7 +794,7 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
}
|
||||
|
||||
// ── Film-Detail-Modal (via Portal) ────────────────────────────────────────────
|
||||
function MovieModal({ tmdbId, onClose, mobile, onHasXrel, noYearFilter }) {
|
||||
function MovieModal({ tmdbId, mediaType='movie', onClose, mobile, onHasXrel, noYearFilter }) {
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht
|
||||
const [xrelDates, setXrelDates] = useState([]);
|
||||
@@ -700,7 +808,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel, noYearFilter }) {
|
||||
useEffect(() => {
|
||||
setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError('');
|
||||
detailRef.current = null;
|
||||
api(`/tools/media/movie/${tmdbId}`)
|
||||
api(`/tools/media/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}`)
|
||||
.then(d => {
|
||||
setDetail(d);
|
||||
detailRef.current = d;
|
||||
|
||||
Reference in New Issue
Block a user