diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 0d9b67b..f5e5671 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -485,6 +485,64 @@ router.get('/xrel-cached', authenticate, (req, res) => { res.json(result); }); +// ── Streaming Top 10 pro Anbieter ────────────────────────────────────────────── +// TMDb Watch-Provider-IDs für Deutschland (watch_region=DE) +const STREAMING_PROVIDERS = [ + { id: 'netflix', label: 'Netflix', tmdbId: 8 }, + { id: 'disney', label: 'Disney+', tmdbId: 337 }, + { id: 'prime', label: 'Amazon Prime Video', tmdbId: 119 }, + { id: 'appletv', label: 'Apple TV+', tmdbId: 350 }, + { id: 'wow', label: 'WOW (Sky)', tmdbId: 30 }, + { id: 'rtlplus', label: 'RTL+', tmdbId: 421 }, +]; + +router.get('/streaming/providers', authenticate, (req, res) => { + res.json(STREAMING_PROVIDERS.map(p => ({ id: p.id, label: p.label }))); +}); + +// GET /streaming/:providerId – Top 10 Filme + Serien für einen Anbieter (DE) +router.get('/streaming/:providerId', authenticate, async (req, res) => { + try { + const provider = STREAMING_PROVIDERS.find(p => p.id === req.params.providerId); + if (!provider) return res.status(404).json({ error: 'Unbekannter Anbieter' }); + + const discoverParams = { + watch_region: 'DE', + with_watch_providers: provider.tmdbId, + with_watch_monetization_types: 'flatrate', + sort_by: 'popularity.desc', + 'vote_count.gte': 10, + }; + + const [movieResults, tvResults, genreMap] = await Promise.all([ + tmdb('/discover/movie', discoverParams), + tmdb('/discover/tv', { ...discoverParams, first_air_date_year: undefined }), + getGenreMap(), + ]); + + const movies = (movieResults.results || []) + .filter(m => !BLOCKED_LANGS.has(m.original_language)) + .slice(0, 10) + .map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' })); + + const tvShows = (tvResults.results || []) + .filter(m => !BLOCKED_LANGS.has(m.original_language)) + .slice(0, 10) + .map((m, i) => ({ + id: m.id, media_type: 'tv', rank: i + 1, + 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(gid => genreMap[gid]).filter(Boolean), + vote_average: m.vote_average, + original_language: m.original_language, + })); + + res.json({ provider: { id: provider.id, label: provider.label }, movies, tvShows }); + } catch(e) { res.status(500).json({ error: e.message }); } +}); + // ── Favorites Routen (feste VOR :param) ────────────────────────────────────── // GET /favorites – eigene Favoriten diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index c807f40..36dcb5c 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -8,6 +8,7 @@ const BACKDROP = 'https://image.tmdb.org/t/p/w780'; const TOOL_DEFS = [ { id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino (DE) – alle laufenden Filme', ready: true }, { id: 'demnächst', label: '🗓 Demnächst', desc: 'Neustarts wochenweise – nächste Wochen', ready: true }, + { id: 'streaming', label: '📡 Streaming', desc: 'Top 10 der deutschen Streaming-Anbieter', ready: true }, { id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true }, { id: 'suchen', label: '🔍 Suchen', desc: 'Filme suchen und Infos abrufen', ready: true }, ]; @@ -143,6 +144,7 @@ export default function Media({ toast, mobile, initTab, onInitTabConsumed }) { {activeTool === 'kino' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} {activeTool === 'demnächst' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} + {activeTool === 'streaming' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} {activeTool === 'favoriten' && } {activeTool === 'suchen' && setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>} @@ -154,7 +156,7 @@ export default function Media({ toast, mobile, initTab, onInitTabConsumed }) { onClose={() => setModal(null)} mobile={mobile} onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))} - noYearFilter={activeTool === 'suchen'} + noYearFilter={activeTool === 'suchen' || activeTool === 'streaming'} isFav={favIds.has(modal?.id || modal)} onToggleFav={movie => toggleFav(movie, favIds.has(modal?.id || modal))} />, @@ -446,6 +448,61 @@ function SectionedGrid({ films, serien, favIds, xrelIds, onToggle, onOpenModal } ); } +function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp, onToggleFav: toggleFavProp }) { + const [favIdsLocal, onToggleLocal] = useFavIds(); + const favIds = favIdsProp || favIdsLocal; + const onToggle = toggleFavProp || onToggleLocal; + + const [providers, setProviders] = useState([]); + const [activeProvider, setActiveProvider] = useState(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + api('/tools/media/streaming/providers') + .then(list => { setProviders(list); if (list.length) setActiveProvider(list[0].id); }) + .catch(e => setError(e.message)); + }, []); + + useEffect(() => { + if (!activeProvider) return; + setLoading(true); setError(''); + api(`/tools/media/streaming/${activeProvider}`) + .then(setData) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }, [activeProvider]); + + return ( +
+ {/* Anbieter-Auswahl */} +
+ {providers.map(p => ( + + ))} +
+ + {error && } + {loading && ⏳ Lädt Top 10…} + + {!loading && data && ( + + )} +
+ ); +} + function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) { const [query, setQuery] = useState(''); const [results, setResults] = useState(null); // null=noch nicht gesucht