feat: Sub-Tabs Top10/Neuerscheinungen mit Sprachgewichtung, debug-network entfernt (Endpoint existiert nicht bei TMDb)
This commit is contained in:
@@ -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')}`;
|
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) {
|
function baseParams(mediaKind) {
|
||||||
return {
|
return {
|
||||||
watch_region: 'DE',
|
watch_region: 'DE',
|
||||||
with_watch_providers: provider.tmdbId,
|
with_watch_providers: provider.tmdbId,
|
||||||
with_watch_monetization_types: 'flatrate',
|
with_watch_monetization_types: 'flatrate',
|
||||||
sort_by: mediaKind === 'movie' ? 'primary_release_date.desc' : 'first_air_date.desc',
|
sort_by: 'popularity.desc',
|
||||||
...(mediaKind === 'movie'
|
...(mediaKind === 'movie'
|
||||||
? { 'primary_release_date.gte': since, 'primary_release_date.lte': today }
|
? { 'primary_release_date.gte': since, 'primary_release_date.lte': today }
|
||||||
: { 'first_air_date.gte': since, 'first_air_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: 1 }),
|
||||||
|
tmdb('/discover/movie', { ...baseParams('movie'), page: 2 }),
|
||||||
tmdb('/discover/tv', { ...baseParams('tv'), page: 1 }),
|
tmdb('/discover/tv', { ...baseParams('tv'), page: 1 }),
|
||||||
|
tmdb('/discover/tv', { ...baseParams('tv'), page: 2 }),
|
||||||
getGenreMap(),
|
getGenreMap(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const movies = (movieData.results || [])
|
const movieRaw = [...(movieP1.results||[]), ...(movieP2.results||[])]
|
||||||
.filter(m => !BLOCKED_LANGS.has(m.original_language))
|
.filter(m => !BLOCKED_LANGS.has(m.original_language));
|
||||||
.slice(0, 10)
|
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' }));
|
.map((m, i) => ({ ...enrichMovie(m, genreMap, null, i + 1), media_type: 'movie' }));
|
||||||
|
|
||||||
const tvShows = (tvData.results || [])
|
const tvShows = applyLanguageWeighting(tvRaw)
|
||||||
.filter(m => !BLOCKED_LANGS.has(m.original_language))
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((m, i) => ({
|
.map((m, i) => ({
|
||||||
id: m.id, media_type: 'tv', rank: i + 1,
|
id: m.id, media_type: 'tv', rank: i + 1,
|
||||||
title: m.name || m.original_name,
|
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 }); }
|
} 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) => {
|
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 });
|
res.json({ ok: true, cleared: r.changes });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
|
|||||||
|
|
||||||
const [providers, setProviders] = useState([]);
|
const [providers, setProviders] = useState([]);
|
||||||
const [activeProvider, setActiveProvider] = useState(null);
|
const [activeProvider, setActiveProvider] = useState(null);
|
||||||
|
const [subTab, setSubTab] = useState('top10'); // 'top10' | 'new'
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -468,18 +469,47 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeProvider) return;
|
if (!activeProvider) return;
|
||||||
setLoading(true); setError('');
|
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)
|
.then(setData)
|
||||||
.catch(e => setError(e.message))
|
.catch(e => setError(e.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [activeProvider]);
|
}, [activeProvider, subTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Sub-Tabs: Top 10 / Neuerscheinungen */}
|
||||||
|
<div style={{display:'flex',gap:8,marginBottom:14}}>
|
||||||
|
<button onClick={() => setSubTab('top10')} style={{
|
||||||
|
background: subTab==='top10' ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
||||||
|
border: `1px solid ${subTab==='top10' ? 'rgba(78,205,196,0.5)' : 'rgba(255,255,255,0.1)'}`,
|
||||||
|
borderRadius:8, padding:'6px 14px',
|
||||||
|
color: subTab==='top10' ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
|
||||||
|
fontFamily:'monospace', fontSize:12, cursor:'pointer', fontWeight: subTab==='top10'?700:400,
|
||||||
|
}}>⭐ Top 10</button>
|
||||||
|
<button onClick={() => setSubTab('new')} style={{
|
||||||
|
background: subTab==='new' ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
||||||
|
border: `1px solid ${subTab==='new' ? 'rgba(78,205,196,0.5)' : 'rgba(255,255,255,0.1)'}`,
|
||||||
|
borderRadius:8, padding:'6px 14px',
|
||||||
|
color: subTab==='new' ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
|
||||||
|
fontFamily:'monospace', fontSize:12, cursor:'pointer', fontWeight: subTab==='new'?700:400,
|
||||||
|
}}>🆕 Neuerscheinungen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subTab === 'top10' ? (
|
||||||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
|
||||||
Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität (Näherung, keine offiziellen Anbieter-Daten)<br/>
|
Top 10 Filme & Serien pro Anbieter (DE) · nur Titel ab {new Date().getFullYear()-1} · sortiert nach Popularität (Näherung, keine offiziellen Anbieter-Daten)<br/>
|
||||||
Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze in den Top 10
|
Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze in den Top 10
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginBottom:12,lineHeight:1.6}}>
|
||||||
|
Was in den letzten 60 Tagen neu beim Anbieter erschienen ist (DE), sortiert nach Beliebtheit<br/>
|
||||||
|
Deutsch & Englisch unbegrenzt · alle anderen Sprachen zusammen max. 2 Plätze
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Anbieter-Auswahl */}
|
{/* Anbieter-Auswahl */}
|
||||||
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
|
||||||
{providers.map(p => (
|
{providers.map(p => (
|
||||||
@@ -494,7 +524,7 @@ function StreamingGrid({ onOpenModal, xrelIds, onXrelLoaded, favIds: favIdsProp,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <ErrorBox msg={error} />}
|
{error && <ErrorBox msg={error} />}
|
||||||
{loading && <StatusBox>⏳ Lädt Top 10…</StatusBox>}
|
{loading && <StatusBox>⏳ Lädt…</StatusBox>}
|
||||||
|
|
||||||
{!loading && data && (
|
{!loading && data && (
|
||||||
<SectionedGrid
|
<SectionedGrid
|
||||||
|
|||||||
Reference in New Issue
Block a user