fix: media – FSK alle Einträge, Duplikate, Jahres-Filter, xREL Response-Struktur, xREL-Cover-Badge
This commit is contained in:
@@ -6,6 +6,9 @@ const { authenticate, requireAdmin } = require('../../middleware/auth');
|
|||||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||||
const XREL_BASE = 'https://api.xrel.to/v2';
|
const XREL_BASE = 'https://api.xrel.to/v2';
|
||||||
|
|
||||||
|
const THIS_YEAR = new Date().getFullYear();
|
||||||
|
const MIN_YEAR = THIS_YEAR - 1; // nur Filme ab Vorjahr
|
||||||
|
|
||||||
function getToken() {
|
function getToken() {
|
||||||
return db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get()?.value || '';
|
return db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get()?.value || '';
|
||||||
}
|
}
|
||||||
@@ -24,7 +27,7 @@ async function tmdb(path, params = {}) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genre-Cache
|
// Genre-Cache (in-memory)
|
||||||
let genreCache = null;
|
let genreCache = null;
|
||||||
async function getGenreMap() {
|
async function getGenreMap() {
|
||||||
if (genreCache) return genreCache;
|
if (genreCache) return genreCache;
|
||||||
@@ -34,16 +37,17 @@ async function getGenreMap() {
|
|||||||
return genreCache;
|
return genreCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FSK-Fix: alle release_dates durchsuchen, ersten nicht-leeren Cert nehmen
|
// FSK: alle Einträge prüfen, bevorzugt Typ 3 (Kino), jeden nicht-leeren nehmen
|
||||||
// Bevorzugt Typ 3 (Kino), aber nimmt jeden nicht-leeren Wert
|
|
||||||
function extractFSK(releaseDates) {
|
function extractFSK(releaseDates) {
|
||||||
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
|
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
|
||||||
if (!de) return null;
|
if (!de?.release_dates?.length) return null;
|
||||||
// Erst Kino (type=3), dann alle anderen
|
// Sortierung: Typ 3 (Kino) zuerst, dann alle anderen
|
||||||
const sorted = [...de.release_dates].sort((a, b) =>
|
const sorted = [...de.release_dates].sort((a, b) => (a.type === 3 ? -1 : 1));
|
||||||
(a.type === 3 ? 0 : 1) - (b.type === 3 ? 0 : 1)
|
for (const r of sorted) {
|
||||||
);
|
const c = (r.certification || '').trim();
|
||||||
return sorted.find(r => r.certification && r.certification.trim() !== '')?.certification || null;
|
if (c !== '') return c;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAllPages(path, maxPages = 3) {
|
async function fetchAllPages(path, maxPages = 3) {
|
||||||
@@ -59,38 +63,117 @@ async function fetchAllPages(path, maxPages = 3) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function movieYear(m) {
|
||||||
|
return m.release_date ? parseInt(m.release_date.slice(0, 4)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMovies(list) {
|
||||||
|
const seen = new Set();
|
||||||
|
return list.filter(m => {
|
||||||
|
if (m.original_language === 'tr') return false;
|
||||||
|
const y = movieYear(m);
|
||||||
|
if (y > 0 && y < MIN_YEAR) return false;
|
||||||
|
if (seen.has(m.id)) return false;
|
||||||
|
seen.add(m.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function enrichMovie(movie, genreMap, fsk, rank) {
|
function enrichMovie(movie, genreMap, fsk, rank) {
|
||||||
return {
|
return {
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
title: movie.title || movie.original_title,
|
title: movie.title || movie.original_title,
|
||||||
|
original_title: movie.original_title,
|
||||||
original_language: movie.original_language,
|
original_language: movie.original_language,
|
||||||
poster_path: movie.poster_path,
|
poster_path: movie.poster_path,
|
||||||
overview: movie.overview,
|
overview: movie.overview,
|
||||||
release_date: movie.release_date,
|
release_date: movie.release_date,
|
||||||
vote_average: movie.vote_average,
|
vote_average: movie.vote_average,
|
||||||
genres: (movie.genre_ids || []).map(id => genreMap[id]).filter(Boolean),
|
genres: (movie.genre_ids || []).map(id => genreMap[id]).filter(Boolean),
|
||||||
fsk: fsk || null,
|
fsk: fsk ?? null,
|
||||||
rank: rank || null,
|
rank: rank ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xREL: Suche nach Titel, gibt bereinigte Release-Liste zurück
|
||||||
|
async function fetchXrelReleases(title, originalTitle, year) {
|
||||||
|
const queries = [originalTitle, title].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i);
|
||||||
|
const results = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
for (const q of queries) {
|
||||||
|
try {
|
||||||
|
// Scene
|
||||||
|
const sceneUrl = `${XREL_BASE}/search/releases.json?q=${encodeURIComponent(q)}&scene=true&p2p=false&limit=25`;
|
||||||
|
const sceneRes = await fetch(sceneUrl, { headers:{ Accept:'application/json' }, signal: AbortSignal.timeout(5000) });
|
||||||
|
if (sceneRes.ok) {
|
||||||
|
const data = await sceneRes.json();
|
||||||
|
// Response: { results: [...] } – flat array für search
|
||||||
|
const hits = Array.isArray(data.results) ? data.results : [];
|
||||||
|
for (const r of hits) {
|
||||||
|
if (seen.has(r.dirname)) continue;
|
||||||
|
// Jahr-Check: dirname oder time
|
||||||
|
if (year && r.time) {
|
||||||
|
const rYear = new Date(r.time * 1000).getFullYear();
|
||||||
|
if (Math.abs(rYear - year) > 1) continue;
|
||||||
|
}
|
||||||
|
seen.add(r.dirname);
|
||||||
|
results.push({ ...r, p2p: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// P2P
|
||||||
|
const p2pUrl = `${XREL_BASE}/search/releases.json?q=${encodeURIComponent(q)}&scene=false&p2p=true&limit=25`;
|
||||||
|
const p2pRes = await fetch(p2pUrl, { headers:{ Accept:'application/json' }, signal: AbortSignal.timeout(5000) });
|
||||||
|
if (p2pRes.ok) {
|
||||||
|
const data = await p2pRes.json();
|
||||||
|
const hits = Array.isArray(data.results) ? data.results : [];
|
||||||
|
for (const r of hits) {
|
||||||
|
if (seen.has(r.dirname)) continue;
|
||||||
|
if (year && r.time) {
|
||||||
|
const rYear = new Date(r.time * 1000).getFullYear();
|
||||||
|
if (Math.abs(rYear - year) > 1) continue;
|
||||||
|
}
|
||||||
|
seen.add(r.dirname);
|
||||||
|
results.push({ ...r, p2p: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortieren: neueste zuerst
|
||||||
|
results.sort((a, b) => (b.time || 0) - (a.time || 0));
|
||||||
|
|
||||||
|
return results.slice(0, 20).map(r => ({
|
||||||
|
dirname: r.dirname,
|
||||||
|
size: r.size_mb ? `${(r.size_mb / 1024).toFixed(1)} GB` : null,
|
||||||
|
category: r.category?.name || '',
|
||||||
|
url: r.link_href || `https://www.xrel.to/release/${r.id}.html`,
|
||||||
|
date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null,
|
||||||
|
p2p: !!r.p2p,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// GET /api/tools/media/now-playing
|
// GET /api/tools/media/now-playing
|
||||||
router.get('/now-playing', authenticate, async (req, res) => {
|
router.get('/now-playing', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [pages, genreMap] = await Promise.all([
|
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/now_playing', 3), getGenreMap()]);
|
||||||
fetchAllPages('/movie/now_playing', 3),
|
const filtered = filterMovies(pages);
|
||||||
getGenreMap(),
|
|
||||||
]);
|
// Top 10: FSK parallel holen
|
||||||
const filtered = pages.filter(m => m.original_language !== 'tr');
|
|
||||||
const top10raw = filtered.slice(0, 10);
|
const top10raw = filtered.slice(0, 10);
|
||||||
const restRaw = filtered.slice(10);
|
const restRaw = filtered.slice(10);
|
||||||
|
|
||||||
// Top 10: FSK parallel holen
|
|
||||||
const top10 = await Promise.all(top10raw.map(async (m, i) => {
|
const top10 = await Promise.all(top10raw.map(async (m, i) => {
|
||||||
let fsk = null;
|
let fsk = null;
|
||||||
try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
|
try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
|
||||||
return enrichMovie(m, genreMap, fsk, i + 1);
|
return enrichMovie(m, genreMap, fsk, i + 1);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const rest = restRaw.map(m => enrichMovie(m, genreMap, null, null));
|
const rest = restRaw.map(m => enrichMovie(m, genreMap, null, null));
|
||||||
res.json([...top10, ...rest]);
|
res.json([...top10, ...rest]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -101,23 +184,16 @@ router.get('/now-playing', authenticate, async (req, res) => {
|
|||||||
// GET /api/tools/media/upcoming
|
// GET /api/tools/media/upcoming
|
||||||
router.get('/upcoming', authenticate, async (req, res) => {
|
router.get('/upcoming', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [pages, genreMap] = await Promise.all([
|
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/upcoming', 3), getGenreMap()]);
|
||||||
fetchAllPages('/movie/upcoming', 3),
|
const filtered = filterMovies(pages).filter(m => m.release_date);
|
||||||
getGenreMap(),
|
|
||||||
]);
|
|
||||||
const seen = new Set();
|
|
||||||
const filtered = pages
|
|
||||||
.filter(m => m.original_language !== 'tr')
|
|
||||||
.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; })
|
|
||||||
.filter(m => m.release_date);
|
|
||||||
filtered.sort((a, b) => a.release_date.localeCompare(b.release_date));
|
filtered.sort((a, b) => a.release_date.localeCompare(b.release_date));
|
||||||
|
|
||||||
const groups = {};
|
const groups = {};
|
||||||
for (const m of filtered) {
|
for (const m of filtered) {
|
||||||
const d = new Date(m.release_date);
|
const d = new Date(m.release_date);
|
||||||
const dow = d.getDay();
|
const dow = d.getDay();
|
||||||
const diff = dow === 0 ? -6 : 1 - dow;
|
const mon = new Date(d);
|
||||||
const mon = new Date(d); mon.setDate(d.getDate() + diff);
|
mon.setDate(d.getDate() + (dow === 0 ? -6 : 1 - dow));
|
||||||
const key = mon.toISOString().slice(0, 10);
|
const key = mon.toISOString().slice(0, 10);
|
||||||
if (!groups[key]) groups[key] = [];
|
if (!groups[key]) groups[key] = [];
|
||||||
groups[key].push(enrichMovie(m, genreMap, null, null));
|
groups[key].push(enrichMovie(m, genreMap, null, null));
|
||||||
@@ -128,7 +204,7 @@ router.get('/upcoming', authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/movie/:id – Details + Credits + xREL + Links
|
// GET /api/tools/media/movie/:id – Details + xREL-Check fürs Cover-Badge
|
||||||
router.get('/movie/:id', authenticate, async (req, res) => {
|
router.get('/movie/:id', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [detail, genreMap, extIds] = await Promise.all([
|
const [detail, genreMap, extIds] = await Promise.all([
|
||||||
@@ -137,38 +213,11 @@ router.get('/movie/:id', authenticate, async (req, res) => {
|
|||||||
tmdb(`/movie/${req.params.id}/external_ids`).catch(() => ({})),
|
tmdb(`/movie/${req.params.id}/external_ids`).catch(() => ({})),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fsk = extractFSK(detail.release_dates);
|
const fsk = extractFSK(detail.release_dates);
|
||||||
const imdb_id = extIds.imdb_id || detail.imdb_id || null;
|
const imdb_id = extIds.imdb_id || detail.imdb_id || null;
|
||||||
const tmdb_url = `https://www.themoviedb.org/movie/${detail.id}`;
|
const year = detail.release_date ? parseInt(detail.release_date.slice(0, 4)) : 0;
|
||||||
const imdb_url = imdb_id ? `https://www.imdb.com/title/${imdb_id}` : null;
|
|
||||||
|
|
||||||
// xREL-Suche: original_title + Jahr
|
const xrel = await fetchXrelReleases(detail.title, detail.original_title, year);
|
||||||
let xrel = [];
|
|
||||||
try {
|
|
||||||
const year = detail.release_date?.slice(0, 4) || '';
|
|
||||||
const query = encodeURIComponent(detail.original_title || detail.title);
|
|
||||||
const xUrl = `${XREL_BASE}/search/releases.json?q=${query}&scene=true&p2p=true&limit=25`;
|
|
||||||
const xRes = await fetch(xUrl, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(4000) });
|
|
||||||
if (xRes.ok) {
|
|
||||||
const xData = await xRes.json();
|
|
||||||
const hits = (xData.results?.scene?.releases || []).concat(xData.results?.p2p?.releases || []);
|
|
||||||
// Nur Movie-Releases, nach Erscheinungsjahr filtern wenn bekannt
|
|
||||||
xrel = hits
|
|
||||||
.filter(r => {
|
|
||||||
const cat = r.category?.name || '';
|
|
||||||
return /movie|film|xvid|x264|x265|bluray|uhd|dvdr|hdtv/i.test(cat);
|
|
||||||
})
|
|
||||||
.map(r => ({
|
|
||||||
dirname: r.dirname,
|
|
||||||
size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null,
|
|
||||||
category: r.category?.name || '',
|
|
||||||
url: r.link_href || `https://www.xrel.to/release/${r.id}.html`,
|
|
||||||
date: r.time ? new Date(r.time * 1000).toISOString().slice(0,10) : null,
|
|
||||||
p2p: !!r.p2p,
|
|
||||||
}))
|
|
||||||
.slice(0, 15);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
id: detail.id,
|
id: detail.id,
|
||||||
@@ -186,15 +235,50 @@ router.get('/movie/:id', authenticate, async (req, res) => {
|
|||||||
fsk,
|
fsk,
|
||||||
cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
|
cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
|
||||||
director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
|
director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
|
||||||
tmdb_url,
|
tmdb_url: `https://www.themoviedb.org/movie/${detail.id}`,
|
||||||
imdb_url,
|
imdb_url: imdb_id ? `https://www.imdb.com/title/${imdb_id}` : null,
|
||||||
xrel,
|
xrel,
|
||||||
|
has_xrel: xrel.length > 0,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/tools/media/xrel-check – Batch-Check ob xREL-Releases existieren (für Cover-Badges)
|
||||||
|
// Body: { movies: [{ id, title, original_title, release_date }] }
|
||||||
|
router.post('/xrel-check', authenticate, async (req, res) => {
|
||||||
|
const movies = req.body.movies || [];
|
||||||
|
if (!movies.length) return res.json({});
|
||||||
|
|
||||||
|
// Max 5 parallel um xREL nicht zu überlasten
|
||||||
|
const result = {};
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < movies.length; i += 5) chunks.push(movies.slice(i, i + 5));
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
await Promise.all(chunk.map(async m => {
|
||||||
|
try {
|
||||||
|
const year = m.release_date ? parseInt(m.release_date.slice(0, 4)) : 0;
|
||||||
|
const q = encodeURIComponent(m.original_title || m.title);
|
||||||
|
const url = `${XREL_BASE}/search/releases.json?q=${q}&scene=true&p2p=true&limit=5`;
|
||||||
|
const r = await fetch(url, { headers:{ Accept:'application/json' }, signal: AbortSignal.timeout(4000) });
|
||||||
|
if (!r.ok) { result[m.id] = false; return; }
|
||||||
|
const data = await r.json();
|
||||||
|
const hits = Array.isArray(data.results) ? data.results : [];
|
||||||
|
const found = hits.some(h => {
|
||||||
|
if (!year || !h.time) return true;
|
||||||
|
return Math.abs(new Date(h.time * 1000).getFullYear() - year) <= 1;
|
||||||
|
});
|
||||||
|
result[m.id] = found;
|
||||||
|
} catch (_) {
|
||||||
|
result[m.id] = false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/favorites
|
// GET /api/tools/media/favorites
|
||||||
router.get('/favorites', authenticate, (req, res) => {
|
router.get('/favorites', authenticate, (req, res) => {
|
||||||
const rows = db.prepare(
|
const rows = db.prepare(
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ export default function Media({ toast, mobile }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Film-Karte ────────────────────────────────────────────────────────────────
|
// ── Film-Karte ────────────────────────────────────────────────────────────────
|
||||||
function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
|
function MovieCard({ movie, favIds, xrelIds, onToggleFav, onOpenModal }) {
|
||||||
const isFav = favIds.has(movie.id);
|
const isFav = favIds.has(movie.id);
|
||||||
const isTop = movie.rank != null && movie.rank <= 10;
|
const isTop = movie.rank != null && movie.rank <= 10;
|
||||||
const fskStyle = FSK_COLOR(movie.fsk);
|
const fskStyle = FSK_COLOR(movie.fsk);
|
||||||
|
const hasXrel = xrelIds?.has(movie.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -177,6 +178,14 @@ function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
|
|||||||
{movie.fsk}
|
{movie.fsk}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* xREL-Badge */}
|
||||||
|
{hasXrel && (
|
||||||
|
<div style={{ position:'absolute', bottom:6, right:6, background:'rgba(78,205,196,0.9)',
|
||||||
|
borderRadius:4, padding:'1px 5px', fontSize:9, color:'#000',
|
||||||
|
fontWeight:900, fontFamily:"'Space Mono',monospace", lineHeight:1.5 }}>
|
||||||
|
xREL
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Fav */}
|
{/* Fav */}
|
||||||
<button onClick={e => { e.stopPropagation(); onToggleFav(movie, isFav); }} style={{
|
<button onClick={e => { e.stopPropagation(); onToggleFav(movie, isFav); }} style={{
|
||||||
position:'absolute', top:5, right:5, background:'rgba(0,0,0,0.65)',
|
position:'absolute', top:5, right:5, background:'rgba(0,0,0,0.65)',
|
||||||
@@ -241,14 +250,27 @@ function useFavIds() {
|
|||||||
|
|
||||||
// ── Kino Grid – alle laufenden Filme ─────────────────────────────────────────
|
// ── Kino Grid – alle laufenden Filme ─────────────────────────────────────────
|
||||||
function KinoGrid({ onOpenModal }) {
|
function KinoGrid({ onOpenModal }) {
|
||||||
const [movies, setMovies] = useState([]);
|
const [movies, setMovies] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [favIds, toggleFav] = useFavIds();
|
const [xrelIds, setXrelIds] = useState(new Set());
|
||||||
|
const [favIds, toggleFav] = useFavIds();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true); setError('');
|
setLoading(true); setError('');
|
||||||
api('/tools/media/now-playing').then(setMovies).catch(e => setError(e.message)).finally(() => setLoading(false));
|
api('/tools/media/now-playing')
|
||||||
|
.then(list => {
|
||||||
|
setMovies(list);
|
||||||
|
// xREL-Batch-Check nach Laden
|
||||||
|
api('/tools/media/xrel-check', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { movies: list.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) }
|
||||||
|
}).then(result => {
|
||||||
|
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k))));
|
||||||
|
}).catch(() => {});
|
||||||
|
})
|
||||||
|
.catch(e => setError(e.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onToggle = async (movie, isFav) => {
|
const onToggle = async (movie, isFav) => {
|
||||||
@@ -268,7 +290,7 @@ function KinoGrid({ onOpenModal }) {
|
|||||||
<>
|
<>
|
||||||
<div style={{ ...S.head, marginBottom:12 }}>★ TOP 10 – MEISTGESEHEN</div>
|
<div style={{ ...S.head, marginBottom:12 }}>★ TOP 10 – MEISTGESEHEN</div>
|
||||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12, marginBottom:24 }}>
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12, marginBottom:24 }}>
|
||||||
{top10.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
{top10.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} xrelIds={xrelIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -276,7 +298,7 @@ function KinoGrid({ onOpenModal }) {
|
|||||||
<>
|
<>
|
||||||
<div style={{ ...S.head, marginBottom:12 }}>WEITERE FILME IM KINO</div>
|
<div style={{ ...S.head, marginBottom:12 }}>WEITERE FILME IM KINO</div>
|
||||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
|
||||||
{rest.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
{rest.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} xrelIds={xrelIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -289,11 +311,24 @@ function DemnächstGrid({ onOpenModal }) {
|
|||||||
const [groups, setGroups] = useState({});
|
const [groups, setGroups] = useState({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [xrelIds, setXrelIds] = useState(new Set());
|
||||||
const [favIds, toggleFav] = useFavIds();
|
const [favIds, toggleFav] = useFavIds();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true); setError('');
|
setLoading(true); setError('');
|
||||||
api('/tools/media/upcoming').then(setGroups).catch(e => setError(e.message)).finally(() => setLoading(false));
|
api('/tools/media/upcoming')
|
||||||
|
.then(grps => {
|
||||||
|
setGroups(grps);
|
||||||
|
const allMovies = Object.values(grps).flat();
|
||||||
|
api('/tools/media/xrel-check', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { movies: allMovies.map(m => ({ id: m.id, title: m.title, original_title: m.original_title, release_date: m.release_date })) }
|
||||||
|
}).then(result => {
|
||||||
|
setXrelIds(new Set(Object.entries(result).filter(([,v]) => v).map(([k]) => Number(k))));
|
||||||
|
}).catch(() => {});
|
||||||
|
})
|
||||||
|
.catch(e => setError(e.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onToggle = async (movie, isFav) => {
|
const onToggle = async (movie, isFav) => {
|
||||||
@@ -316,7 +351,7 @@ function DemnächstGrid({ onOpenModal }) {
|
|||||||
<span style={{ fontSize:9 }}>{movies.length} Film{movies.length!==1?'e':''}</span>
|
<span style={{ fontSize:9 }}>{movies.length} Film{movies.length!==1?'e':''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
|
||||||
{movies.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
{movies.map(m => <MovieCard key={m.id} movie={m} favIds={favIds} xrelIds={xrelIds} onToggleFav={onToggle} onOpenModal={onOpenModal} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user