feat: Streaming-Tab mit Top 10 pro Anbieter (Netflix, Disney+, Prime, Apple TV+, WOW, RTL+)
This commit is contained in:
@@ -485,6 +485,64 @@ router.get('/xrel-cached', authenticate, (req, res) => {
|
|||||||
res.json(result);
|
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) ──────────────────────────────────────
|
// ── Favorites Routen (feste VOR :param) ──────────────────────────────────────
|
||||||
|
|
||||||
// GET /favorites – eigene Favoriten
|
// GET /favorites – eigene Favoriten
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const BACKDROP = 'https://image.tmdb.org/t/p/w780';
|
|||||||
const TOOL_DEFS = [
|
const TOOL_DEFS = [
|
||||||
{ id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino (DE) – alle laufenden Filme', ready: true },
|
{ 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: '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: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
|
||||||
{ id: 'suchen', label: '🔍 Suchen', desc: 'Filme suchen und Infos abrufen', 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 === '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 === '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 === 'favoriten' && <FavoritenKalender onOpenModal={setModal} />}
|
||||||
{activeTool === 'suchen' && <SucheGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
|
{activeTool === 'suchen' && <SucheGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} favIds={favIds} onToggleFav={toggleFav}/>}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +156,7 @@ export default function Media({ toast, mobile, initTab, onInitTabConsumed }) {
|
|||||||
onClose={() => setModal(null)}
|
onClose={() => setModal(null)}
|
||||||
mobile={mobile}
|
mobile={mobile}
|
||||||
onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))}
|
onHasXrel={id => setXrelIds(prev => new Set([...prev, id]))}
|
||||||
noYearFilter={activeTool === 'suchen'}
|
noYearFilter={activeTool === 'suchen' || activeTool === 'streaming'}
|
||||||
isFav={favIds.has(modal?.id || modal)}
|
isFav={favIds.has(modal?.id || modal)}
|
||||||
onToggleFav={movie => toggleFav(movie, 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 }) {
|
function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [results, setResults] = useState(null); // null=noch nicht gesucht
|
const [results, setResults] = useState(null); // null=noch nicht gesucht
|
||||||
|
|||||||
Reference in New Issue
Block a user