fix: Route-Reihenfolge Favoriten, Dashboard-Badge, TV-Suche, User-gruppierte Admin-Liste
This commit is contained in:
@@ -320,11 +320,14 @@ router.get('/search', authenticate, async (req, res) => {
|
||||
const searchParams = { query: q, include_adult: 'false' };
|
||||
if (yearFilter) searchParams.year = yearFilter;
|
||||
|
||||
const [movieResults, tvResults, genreMap] = await Promise.all([
|
||||
const [movieResults, tvResults, genreMap, tvGenreMap] = await Promise.all([
|
||||
tmdb('/search/movie', searchParams),
|
||||
tmdb('/search/tv', { query: q, include_adult: 'false', ...(yearFilter ? { first_air_date_year: yearFilter } : {}) }),
|
||||
getGenreMap(),
|
||||
tmdb('/genre/tv/list').catch(()=>({genres:[]})),
|
||||
]);
|
||||
const tvGenres = {};
|
||||
(tvGenreMap.genres||[]).forEach(g => { tvGenres[g.id] = g.name; });
|
||||
|
||||
const seen = new Set();
|
||||
const movies = (movieResults.results || [])
|
||||
@@ -332,20 +335,26 @@ router.get('/search', authenticate, async (req, res) => {
|
||||
.map(m => ({ ...enrichMovie(m, genreMap, null, null), media_type: 'movie' }));
|
||||
|
||||
const tvShows = (tvResults.results || [])
|
||||
.filter(m => { if (BLOCKED_LANGS.has(m.original_language)) return false; if (seen.has('t'+m.id)) return false; seen.add('t'+m.id); return true; })
|
||||
.filter(m => { if (seen.has('t'+m.id)) return false; seen.add('t'+m.id); return true; })
|
||||
.map(m => ({
|
||||
id: m.id, media_type: 'tv',
|
||||
title: m.name || m.original_name,
|
||||
original_title: m.original_name,
|
||||
release_date: m.first_air_date || '',
|
||||
poster_path: m.poster_path || '',
|
||||
genres: (m.genre_ids||[]).map(id => genreMap[id]).filter(Boolean),
|
||||
genres: (m.genre_ids||[]).map(id => tvGenres[id] || genreMap[id]).filter(Boolean),
|
||||
vote_average: m.vote_average,
|
||||
original_language: m.original_language,
|
||||
}));
|
||||
|
||||
// Sortierung: mit Datum zuerst (neueste), ohne Datum danach
|
||||
const combined = [...movies, ...tvShows]
|
||||
.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''))
|
||||
.sort((a,b) => {
|
||||
if (!a.release_date && !b.release_date) return 0;
|
||||
if (!a.release_date) return 1;
|
||||
if (!b.release_date) return -1;
|
||||
return b.release_date.localeCompare(a.release_date);
|
||||
})
|
||||
.slice(0, 30);
|
||||
|
||||
res.json(combined);
|
||||
@@ -442,9 +451,40 @@ router.get('/xrel-cached', authenticate, (req, res) => {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// ── Favorites Routen (feste VOR :param) ──────────────────────────────────────
|
||||
|
||||
// GET /favorites – eigene Favoriten
|
||||
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));
|
||||
});
|
||||
|
||||
// GET /favorites/all – Admin: alle User gruppiert
|
||||
router.get('/favorites/all', authenticate, requireAdmin, (req, res) => {
|
||||
const favs = db.prepare(`
|
||||
SELECT f.*, u.username
|
||||
FROM movie_favorites f
|
||||
JOIN users u ON u.id = f.user_id
|
||||
ORDER BY u.username ASC, f.added_at DESC
|
||||
`).all();
|
||||
res.json(favs);
|
||||
});
|
||||
|
||||
// GET /favorites/unacked – unquittierte Zählung
|
||||
router.get('/favorites/unacked', authenticate, (req, res) => {
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const count = isAdmin
|
||||
? db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get()
|
||||
: db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE user_id=? AND acknowledged=0').get(req.user.id);
|
||||
res.json({ count: count.n });
|
||||
});
|
||||
|
||||
// POST /favorites/acknowledge-all – Admin: alle quittieren
|
||||
router.post('/favorites/acknowledge-all', authenticate, requireAdmin, (req, res) => {
|
||||
db.prepare('UPDATE movie_favorites SET acknowledged=1, acknowledged_at=CURRENT_TIMESTAMP WHERE acknowledged=0').run();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// POST /favorites – Favorit hinzufügen (alle User inkl. Admin bekommen Pushover)
|
||||
router.post('/favorites', authenticate, (req, res) => {
|
||||
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk, media_type } = req.body;
|
||||
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
|
||||
@@ -455,11 +495,10 @@ router.post('/favorites', authenticate, (req, res) => {
|
||||
.run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'', release_date||'', JSON.stringify(genres||[]), fsk||'', media_type||'movie');
|
||||
|
||||
if (r.changes > 0) {
|
||||
// Pushover-Benachrichtigung an Admin (fire-and-forget, nie emergency)
|
||||
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;
|
||||
const user = db.prepare('SELECT username FROM users WHERE id=?').get(req.user.id);
|
||||
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;
|
||||
const user = db.prepare('SELECT username FROM users WHERE id=?').get(req.user.id);
|
||||
if (pushToken && pushUser) {
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
@@ -478,39 +517,18 @@ router.post('/favorites', authenticate, (req, res) => {
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// DELETE /favorites/:tmdb_id – eigenen Favoriten löschen (jeder User)
|
||||
router.delete('/favorites/:tmdb_id', authenticate, (req, res) => {
|
||||
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?').run(req.user.id, Number(req.params.tmdb_id));
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Admin: alle Favoriten aller User
|
||||
router.get('/favorites/all', authenticate, requireAdmin, (req, res) => {
|
||||
const favs = db.prepare(`
|
||||
SELECT f.*, u.username
|
||||
FROM movie_favorites f
|
||||
JOIN users u ON u.id = f.user_id
|
||||
ORDER BY f.acknowledged ASC, f.added_at DESC
|
||||
`).all();
|
||||
res.json(favs);
|
||||
});
|
||||
|
||||
// Admin: Favoritenanzahl unquittiert (für Dashboard)
|
||||
router.get('/favorites/unacked', authenticate, requireAdmin, (req, res) => {
|
||||
const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get();
|
||||
res.json({ count: count.n });
|
||||
});
|
||||
|
||||
// Admin: Favorit quittieren
|
||||
// POST /favorites/:id/acknowledge – Admin: einzelnen quittieren
|
||||
router.post('/favorites/:id/acknowledge', authenticate, requireAdmin, (req, res) => {
|
||||
db.prepare('UPDATE movie_favorites SET acknowledged=1, acknowledged_at=CURRENT_TIMESTAMP WHERE id=?').run(req.params.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Admin: alle quittieren
|
||||
router.post('/favorites/acknowledge-all', authenticate, requireAdmin, (req, res) => {
|
||||
db.prepare('UPDATE movie_favorites SET acknowledged=1, acknowledged_at=CURRENT_TIMESTAMP WHERE acknowledged=0').run();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
router.get('/settings', authenticate, requireAdmin, (req, res) => res.json({ configured: !!getToken() }));
|
||||
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||
const { tmdb_token } = req.body;
|
||||
|
||||
@@ -1981,9 +1981,9 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr
|
||||
borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 5px #ffe66d' }}/>
|
||||
)}
|
||||
</button>
|
||||
{/* Admin: unquittierte Favoriten */}
|
||||
{user?.role==='admin' && unackedFavs > 0 && (
|
||||
<button onClick={()=>{ setActive('media'); onAckFavs(); }} style={{
|
||||
{/* Unquittierte Favoriten für alle User */}
|
||||
{unackedFavs > 0 && (
|
||||
<button onClick={()=>{ setActive('media'); onAckFavs?.(); }} style={{
|
||||
background:'rgba(245,158,11,0.12)', border:'1px solid rgba(245,158,11,0.35)',
|
||||
borderRadius:7, color:'#f59e0b', cursor:'pointer',
|
||||
padding:'4px 10px', fontSize:9, fontFamily:'monospace', letterSpacing:1,
|
||||
@@ -3655,6 +3655,12 @@ export default function App() {
|
||||
return()=>clearInterval(t);
|
||||
},[user]);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!user)return;
|
||||
const poll=()=>api('/tools/media/favorites/unacked').then(d=>setUnackedFavs(d.count||0)).catch(()=>{});
|
||||
poll(); const ti=setInterval(poll,60000); return()=>clearInterval(ti);
|
||||
},[user]);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!user)return;
|
||||
const poll=()=>api('/dashboard/changelog/unread').then(d=>setUnreadChangelog(d.unread||0)).catch(()=>{});
|
||||
@@ -3711,7 +3717,7 @@ export default function App() {
|
||||
unreadMsgs={unreadMsgs}/>
|
||||
)}
|
||||
<main ref={mainRef} style={mainStyle}>
|
||||
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
|
||||
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} unackedFavs={unackedFavs} onAckFavs={()=>api('/tools/media/favorites/acknowledge-all',{method:'POST',body:{}}).then(()=>setUnackedFavs(0)).catch(()=>{})} user={user}/>}
|
||||
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user} nav={devToolsNav}/>}
|
||||
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive} {...(activeTool.id==='devtools'?{nav:devToolsNav}:{})}/>}
|
||||
</main>
|
||||
|
||||
@@ -509,12 +509,17 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
||||
if (error) return <ErrorBox msg={error} />;
|
||||
|
||||
// Admin-Ansicht: alle Favoriten aller User
|
||||
// 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 (
|
||||
<div>
|
||||
<div style={{display:'flex',gap:8,marginBottom:16,alignItems:'center'}}>
|
||||
<div style={{display:'flex',gap:8,marginBottom:16,alignItems:'center',flexWrap:'wrap'}}>
|
||||
<button onClick={()=>setAdminTab(false)}
|
||||
style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',
|
||||
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
|
||||
@@ -528,47 +533,66 @@ function FavoritenKalender({ onOpenModal }) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{display:'flex',flexDirection:'column',gap:6}}>
|
||||
{allFavs.map(f => (
|
||||
<div key={f.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',
|
||||
background: !f.acknowledged ? 'rgba(245,158,11,0.06)' : 'rgba(255,255,255,0.02)',
|
||||
border:`1px solid ${!f.acknowledged ? 'rgba(245,158,11,0.25)' : 'rgba(255,255,255,0.06)'}`,
|
||||
borderRadius:8}}>
|
||||
{f.poster_path
|
||||
? <img src={POSTER+f.poster_path} alt={f.title} style={{width:32,height:48,objectFit:'cover',borderRadius:4,flexShrink:0}}/>
|
||||
: <div style={{width:32,height:48,background:'#111',borderRadius:4,flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',fontSize:14}}>
|
||||
{f.media_type==='tv'?'📺':'🎬'}
|
||||
</div>
|
||||
}
|
||||
<div style={{flex:1,minWidth:0}}>
|
||||
<div style={{color:'rgba(255,255,255,0.85)',fontFamily:'monospace',fontSize:12,
|
||||
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
|
||||
{f.title}
|
||||
{f.media_type==='tv' && <span style={{marginLeft:6,color:'#60a5fa',fontSize:9}}>SERIE</span>}
|
||||
</div>
|
||||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||||
👤 {f.username} · ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
|
||||
{!!f.acknowledged && <span style={{marginLeft:6,color:'rgba(255,255,255,0.2)'}}>✓ quittiert</span>}
|
||||
</div>
|
||||
{!allFavs.length
|
||||
? <StatusBox>Noch keine Favoriten von irgendjemandem.</StatusBox>
|
||||
: Object.entries(userGroups).map(([username, items]) => (
|
||||
<div key={username} style={{marginBottom:20}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:8,
|
||||
padding:'6px 10px',background:'rgba(255,255,255,0.04)',borderRadius:8}}>
|
||||
<span style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:12,fontWeight:700}}>
|
||||
👤 {username}
|
||||
</span>
|
||||
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10}}>
|
||||
{items.length} Favorit{items.length!==1?'en':''}
|
||||
</span>
|
||||
{items.filter(f=>!f.acknowledged).length > 0 && (
|
||||
<span style={{background:'rgba(245,158,11,0.2)',border:'1px solid rgba(245,158,11,0.4)',
|
||||
borderRadius:10,padding:'1px 7px',color:'#f59e0b',fontFamily:'monospace',fontSize:9}}>
|
||||
{items.filter(f=>!f.acknowledged).length} neu
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{display:'flex',flexDirection:'column',gap:4}}>
|
||||
{items.map(f => (
|
||||
<div key={f.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',
|
||||
background:!f.acknowledged?'rgba(245,158,11,0.05)':'rgba(255,255,255,0.02)',
|
||||
border:`1px solid ${!f.acknowledged?'rgba(245,158,11,0.2)':'rgba(255,255,255,0.05)'}`,
|
||||
borderRadius:8}}>
|
||||
{f.poster_path
|
||||
? <img src={POSTER+f.poster_path} alt={f.title} style={{width:32,height:48,objectFit:'cover',borderRadius:4,flexShrink:0}}/>
|
||||
: <div style={{width:32,height:48,background:'#111',borderRadius:4,flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',fontSize:14}}>
|
||||
{f.media_type==='tv'?'📺':'🎬'}
|
||||
</div>
|
||||
}
|
||||
<div style={{flex:1,minWidth:0}}>
|
||||
<div style={{color:'rgba(255,255,255,0.85)',fontFamily:'monospace',fontSize:12,
|
||||
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
|
||||
{f.title}
|
||||
{f.media_type==='tv' && <span style={{marginLeft:6,color:'#60a5fa',fontSize:9}}>SERIE</span>}
|
||||
</div>
|
||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||||
⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
|
||||
{!!f.acknowledged && <span style={{marginLeft:6}}>✓</span>}
|
||||
</div>
|
||||
</div>
|
||||
{!f.acknowledged && (
|
||||
<button onClick={async e=>{e.stopPropagation();
|
||||
await api(`/tools/media/favorites/${f.id}/acknowledge`,{method:'POST',body:{}});
|
||||
setAllFavs(prev=>prev.map(x=>x.id===f.id?{...x,acknowledged:1}:x));
|
||||
}} style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',
|
||||
borderRadius:6,padding:'3px 10px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
|
||||
✓
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!f.acknowledged && (
|
||||
<button onClick={async(e)=>{e.stopPropagation();
|
||||
await api(`/tools/media/favorites/${f.id}/acknowledge`,{method:'POST',body:{}});
|
||||
setAllFavs(prev=>prev.map(x=>x.id===f.id?{...x,acknowledged:1}:x));
|
||||
}}
|
||||
style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',
|
||||
borderRadius:6,padding:'3px 10px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
|
||||
✓
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!allFavs.length && <StatusBox>Noch keine Favoriten von irgendjemandem.</StatusBox>}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!favs.length) return (
|
||||
<div>
|
||||
{isAdmin && (
|
||||
|
||||
Reference in New Issue
Block a user