From ed45b2a957fbbefd9bb1b218658e03aae5f3f12d Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 19 Jun 2026 23:45:23 +0200 Subject: [PATCH] feat: Sub-Tabs Top10/Neuerscheinungen mit Sprachgewichtung, debug-network entfernt (Endpoint existiert nicht bei TMDb) --- backend/src/tools/media/routes.js | 59 ++++++++++++++++++++++--------- frontend/src/tools/media.jsx | 42 ++++++++++++++++++---- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 120273d..ea60e36 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -559,32 +559,65 @@ router.get('/streaming/:providerId/new', authenticate, async (req, res) => { return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; })(); + // Innerhalb des 60-Tage-Fensters nach POPULARITÄT sortieren (nicht nach + // Datum) – sonst landen wenig beachtete Tagesveröffentlichungen vor + // tatsächlich angesagten Titeln. "Neuerscheinungen" heißt: neu UND beachtet. function baseParams(mediaKind) { return { watch_region: 'DE', with_watch_providers: provider.tmdbId, with_watch_monetization_types: 'flatrate', - sort_by: mediaKind === 'movie' ? 'primary_release_date.desc' : 'first_air_date.desc', + sort_by: 'popularity.desc', ...(mediaKind === 'movie' ? { 'primary_release_date.gte': since, 'primary_release_date.lte': today } : { 'first_air_date.gte': since, 'first_air_date.lte': today }), }; } - const [movieData, tvData, genreMap] = await Promise.all([ + // Gleiche Sprachgewichtung wie bei Top 10: Deutsch/Englisch unbegrenzt, + // alle anderen Sprachen zusammen max. 2 Plätze + const UNLIMITED_LANGS = new Set(['de', 'en']); + const MAX_OTHER_LANGUAGE_TOTAL = 2; + function applyLanguageWeighting(rawList) { + let otherCount = 0; + const picked = []; + const overflow = []; + for (const item of rawList) { + const lang = item.original_language || 'xx'; + if (UNLIMITED_LANGS.has(lang)) { + picked.push(item); + } else if (otherCount < MAX_OTHER_LANGUAGE_TOTAL) { + picked.push(item); + otherCount++; + } else { + overflow.push(item); + } + if (picked.length >= 10) break; + } + for (const item of overflow) { + if (picked.length >= 10) break; + picked.push(item); + } + return picked.slice(0, 10); + } + + const [movieP1, movieP2, tvP1, tvP2, genreMap] = await Promise.all([ tmdb('/discover/movie', { ...baseParams('movie'), page: 1 }), + tmdb('/discover/movie', { ...baseParams('movie'), page: 2 }), tmdb('/discover/tv', { ...baseParams('tv'), page: 1 }), + tmdb('/discover/tv', { ...baseParams('tv'), page: 2 }), getGenreMap(), ]); - const movies = (movieData.results || []) - .filter(m => !BLOCKED_LANGS.has(m.original_language)) - .slice(0, 10) + const movieRaw = [...(movieP1.results||[]), ...(movieP2.results||[])] + .filter(m => !BLOCKED_LANGS.has(m.original_language)); + const tvRaw = [...(tvP1.results||[]), ...(tvP2.results||[])] + .filter(m => !BLOCKED_LANGS.has(m.original_language)); + + const movies = applyLanguageWeighting(movieRaw) .map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' })); - const tvShows = (tvData.results || []) - .filter(m => !BLOCKED_LANGS.has(m.original_language)) - .slice(0, 10) + const tvShows = applyLanguageWeighting(tvRaw) .map((m, i) => ({ id: m.id, media_type: 'tv', rank: i + 1, title: m.name || m.original_name, @@ -904,16 +937,8 @@ router.get('/streaming/debug/:providerId', authenticate, requireAdmin, async (re } catch(e) { res.status(500).json({ error: e.message }); } }); -// Debug: TV-Network-ID per Namenssuche finden (für Originals-Filter) -router.get('/streaming/debug-network/:name', authenticate, requireAdmin, async (req, res) => { - try { - const data = await tmdb('/search/network', { query: req.params.name }); - res.json(data.results || []); - } catch(e) { res.status(500).json({ error: e.message }); } -}); - router.post('/streaming/clear-cache', authenticate, requireAdmin, (req, res) => { - const r = db.prepare("DELETE FROM xrel_cache WHERE cache_key LIKE 'streaming-top10:%'").run(); + const r = db.prepare("DELETE FROM xrel_cache WHERE cache_key LIKE 'streaming-top10:%' OR cache_key LIKE 'streaming-new:%'").run(); res.json({ ok: true, cleared: r.changes }); }); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index a69facb..9b09a52 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -455,6 +455,7 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp, const [providers, setProviders] = useState([]); const [activeProvider, setActiveProvider] = useState(null); + const [subTab, setSubTab] = useState('top10'); // 'top10' | 'new' const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -468,18 +469,47 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp, useEffect(() => { if (!activeProvider) return; setLoading(true); setError(''); - api(`/tools/media/streaming/${activeProvider}`) + const path = subTab === 'new' + ? `/tools/media/streaming/${activeProvider}/new` + : `/tools/media/streaming/${activeProvider}`; + api(path) .then(setData) .catch(e => setError(e.message)) .finally(() => setLoading(false)); - }, [activeProvider]); + }, [activeProvider, subTab]); return (
-
- Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität (Näherung, keine offiziellen Anbieter-Daten)
- Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze in den Top 10 + {/* Sub-Tabs: Top 10 / Neuerscheinungen */} +
+ +
+ + {subTab === 'top10' ? ( +
+ Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität (Näherung, keine offiziellen Anbieter-Daten)
+ Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze in den Top 10 +
+ ) : ( +
+ Was in den letzten 60 Tagen neu beim Anbieter erschienen ist (DE), sortiert nach Beliebtheit
+ Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze +
+ )} + {/* Anbieter-Auswahl */}
{providers.map(p => ( @@ -494,7 +524,7 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
{error && } - {loading && ⏳ Lädt Top 10…} + {loading && ⏳ Lädt…} {!loading && data && (