feat: Serien-Suche, Admin-Favoriten, Pushover, Dashboard-Badge

This commit is contained in:
2026-06-18 02:21:06 +02:00
parent f77c3c60d4
commit 7f1ba07500
5 changed files with 219 additions and 23 deletions

View File

@@ -556,6 +556,12 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='mov
db.exec("ALTER TABLE movie_favorites ADD COLUMN genres TEXT NOT NULL DEFAULT '[]'");
if (!cols.includes('fsk'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN fsk TEXT NOT NULL DEFAULT ''");
if (!cols.includes('acknowledged'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged INTEGER NOT NULL DEFAULT 0");
if (!cols.includes('acknowledged_at'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged_at DATETIME DEFAULT NULL");
if (!cols.includes('media_type'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN media_type TEXT NOT NULL DEFAULT 'movie'");
}
// xREL Badge Cache (persistent, überlebt Container-Restarts)

View File

@@ -320,4 +320,11 @@ router.delete('/push-schedules/:id', authenticate, (req, res) => {
res.json({ ok: true });
});
// Unquittierte Favoriten für Admin-Dashboard-Badge
router.get('/media-favorites/unacked', authenticate, (req, res) => {
if (req.user.role !== 'admin') return res.json({ count: 0 });
const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get();
res.json({ count: count.n });
});
module.exports = router;

View File

@@ -320,25 +320,35 @@ router.get('/search', authenticate, async (req, res) => {
const searchParams = { query: q, include_adult: 'false' };
if (yearFilter) searchParams.year = yearFilter;
const [results, genreMap] = await Promise.all([
const [movieResults, tvResults, genreMap] = await Promise.all([
tmdb('/search/movie', searchParams),
tmdb('/search/tv', { query: q, include_adult: 'false', ...(yearFilter ? { first_air_date_year: yearFilter } : {}) }),
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));
const movies = (movieResults.results || [])
.filter(m => { if (BLOCKED_LANGS.has(m.original_language)) return false; if (seen.has('m'+m.id)) return false; seen.add('m'+m.id); return true; })
.map(m => ({ ...enrichMovie(m, genreMap, null, null), media_type: 'movie' }));
// Neueste zuerst (Backend sortiert, Frontend sortiert auch konsistent)
filtered.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''));
res.json(filtered);
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; })
.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),
vote_average: m.vote_average,
original_language: m.original_language,
}));
const combined = [...movies, ...tvShows]
.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''))
.slice(0, 30);
res.json(combined);
} catch(e) { res.status(500).json({ error: e.message }); }
});
@@ -436,18 +446,71 @@ 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));
});
router.post('/favorites', authenticate, (req, res) => {
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk } = req.body;
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' });
try {
db.prepare(`INSERT OR IGNORE INTO movie_favorites (user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk) VALUES (?,?,?,?,?,?,?,?)`)
.run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'', release_date||'', JSON.stringify(genres||[]), fsk||'');
const r = db.prepare(`INSERT OR IGNORE INTO movie_favorites
(user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk,media_type,added_at)
VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`)
.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);
if (pushToken && pushUser) {
fetch('https://api.pushover.net/1/messages.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: pushToken, user: pushUser,
message: `${user?.username || 'Jemand'} hat "${title}" zur Merkliste hinzugefügt`,
title: '⭐ Neue Favoriten',
priority: 0,
}),
}).catch(() => {});
}
} catch {}
}
res.json({ ok: true });
} catch(e) { res.status(500).json({ error: e.message }); }
});
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
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;