feat: Streaming-Tab mit Top 10 pro Anbieter (Netflix, Disney+, Prime, Apple TV+, WOW, RTL+)

This commit is contained in:
2026-06-19 01:34:42 +02:00
parent 39116d33a5
commit 026455f4ba
2 changed files with 116 additions and 1 deletions

View File

@@ -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

View File

@@ -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' && <KinoGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
{activeTool === 'demnächst' && <DemnächstGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
{activeTool === 'streaming' && <StreamingGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
{activeTool === 'favoriten' && <FavoritenKalender onOpenModal={setModal} />}
{activeTool === 'suchen' && <SucheGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
</div>
@@ -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 (
<div>
{/* Anbieter-Auswahl */}
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
{providers.map(p => (
<button key={p.id} onClick={() => setActiveProvider(p.id)} style={{
background: activeProvider===p.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
border: `1px solid ${activeProvider===p.id ? 'rgba(78,205,196,0.5)' : 'rgba(255,255,255,0.1)'}`,
borderRadius:20, padding:'6px 14px',
color: activeProvider===p.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
fontFamily:'monospace', fontSize:12, cursor:'pointer', fontWeight: activeProvider===p.id?700:400,
}}>{p.label}</button>
))}
</div>
{error && <ErrorBox msg={error} />}
{loading && <StatusBox> Lädt Top 10</StatusBox>}
{!loading && data && (
<SectionedGrid
films={data.movies || []}
serien={data.tvShows || []}
favIds={favIds} xrelIds={xrelIds} onToggle={onToggle} onOpenModal={onOpenModal}
/>
)}
</div>
);
}
function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(null); // null=noch nicht gesucht