diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index becb08a..5bc146f 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -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; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 85a3b7f..f90be49 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1981,9 +1981,9 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 5px #ffe66d' }}/> )} - {/* Admin: unquittierte Favoriten */} - {user?.role==='admin' && unackedFavs > 0 && ( - )} -
- {allFavs.map(f => ( -
- {f.poster_path - ? {f.title} - :
- {f.media_type==='tv'?'📺':'🎬'} -
- } -
-
- {f.title} - {f.media_type==='tv' && SERIE} -
-
- 👤 {f.username} · ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'} - {!!f.acknowledged && ✓ quittiert} -
+ {!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 && ( + + )} +
+ ))}
- {!f.acknowledged && ( - - )}
- ))} - {!allFavs.length && Noch keine Favoriten von irgendjemandem.} -
+ )) + }
); } - if (!favs.length) return (
{isAdmin && (