feat: Erscheinungsdaten direkt von xREL (us-cine/de-cine/us-web etc.)

This commit is contained in:
2026-06-11 10:55:41 +02:00
parent 5374935022
commit c6d2fc2f11
2 changed files with 43 additions and 32 deletions

View File

@@ -66,28 +66,28 @@ function extractFSK(rd) {
return null; return null;
} }
const RELEASE_TYPE_LABEL = { 2:'Kino (limited)', 3:'Kinostart', 4:'Web', 5:'Retail', 6:'TV' }; // xREL release_date type labels
const XREL_DATE_LABEL = {
'us-cine': 'Kinostart (US)',
'de-cine': 'Kinostart (DE)',
'us-web': 'Web (US)',
'de-web': 'Web (DE)',
'us-disc': 'Disc (US)',
'de-disc': 'Disc (DE)',
'us-tv': 'TV (US)',
'de-tv': 'TV (DE)',
};
function extractReleaseDates(rd) { // Holt xREL Erscheinungsdaten für eine ext_info_id
if (!rd?.results) return []; async function getXrelReleaseDates(extInfoId) {
// Nur DE und US, Typen 2-5 (kein Premiere=1, kein TV=6) try {
const entries = []; const data = await xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' });
for (const country of ['DE', 'US']) { if (!Array.isArray(data.release_dates)) return [];
const c = rd.results.find(r => r.iso_3166_1 === country); return data.release_dates
if (!c) continue; .filter(r => XREL_DATE_LABEL[r.type] && r.date)
for (const r of c.release_dates) { .map(r => ({ label: XREL_DATE_LABEL[r.type], date: r.date }))
if (!r.release_date) continue; .sort((a, b) => a.date.localeCompare(b.date));
const label = RELEASE_TYPE_LABEL[r.type]; } catch(_) { return []; }
if (!label) continue;
entries.push({
country,
label: `${label} (${country})`,
date: r.release_date.slice(0, 10),
});
}
}
entries.sort((a, b) => a.date.localeCompare(b.date));
return entries;
} }
async function fetchAllPages(path, maxPages = 3) { async function fetchAllPages(path, maxPages = 3) {
const first = await tmdb(path, { page: 1 }); const first = await tmdb(path, { page: 1 });
@@ -232,8 +232,10 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
results.sort((a,b) => (b.date||'').localeCompare(a.date||'')); results.sort((a,b) => (b.date||'').localeCompare(a.date||''));
const top5 = results.slice(0, 5); const top5 = results.slice(0, 5);
cacheSet(cacheKey, top5, top5.length > 0 ? 12*60*60*1000 : 60*60*1000); const payload = { releases: top5, dates: xrelDates };
return top5; const ttl = (top5.length > 0 || xrelDates.length > 0) ? 12*60*60*1000 : 60*60*1000;
cacheSet(cacheKey, payload, ttl);
return payload;
} }
// ── Routes ──────────────────────────────────────────────────────────────────── // ── Routes ────────────────────────────────────────────────────────────────────
@@ -286,7 +288,6 @@ router.get('/movie/:id', authenticate, async (req, res) => {
backdrop_path: detail.backdrop_path, release_date: detail.release_date, backdrop_path: detail.backdrop_path, release_date: detail.release_date,
runtime: detail.runtime, vote_average: detail.vote_average, runtime: detail.runtime, vote_average: detail.vote_average,
genres: (detail.genres||[]).map(g => g.name), fsk, genres: (detail.genres||[]).map(g => g.name), fsk,
release_dates: extractReleaseDates(detail.release_dates),
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: `https://www.themoviedb.org/movie/${detail.id}`, tmdb_url: `https://www.themoviedb.org/movie/${detail.id}`,
@@ -307,8 +308,12 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
// Bei refresh=1 oder leerem Cache: xREL aufrufen // Bei refresh=1 oder leerem Cache: xREL aufrufen
if (!forceRefresh) { if (!forceRefresh) {
const cached = cacheGet(cacheKey); const cached = cacheGet(cacheKey);
if (cached !== undefined && cached.length > 0) { if (cached !== undefined) {
return res.json({ xrel: cached, has_xrel: true }); const releases = cached?.releases || cached || [];
const dates = cached?.dates || [];
if (releases.length > 0 || dates.length > 0) {
return res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
}
} }
} }
// Cache löschen bei refresh (damit neu gecacht wird) // Cache löschen bei refresh (damit neu gecacht wird)
@@ -332,8 +337,10 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
origTitle = detail.original_title || null; origTitle = detail.original_title || null;
} }
const xrel = await getXrelData(req.params.id, imdbId, title, origTitle); const payload = await getXrelData(req.params.id, imdbId, title, origTitle);
res.json({ xrel, has_xrel: xrel.length > 0 }); const releases = payload?.releases || [];
const dates = payload?.dates || [];
res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
} catch(e) { res.status(500).json({ error: e.message }); } } catch(e) { res.status(500).json({ error: e.message }); }
}); });
@@ -346,7 +353,9 @@ router.get('/xrel-cached', authenticate, (req, res) => {
const result = {}; const result = {};
for (const id of ids) { for (const id of ids) {
const cached = cacheGet(`xrel:tmdb:${id}`); const cached = cacheGet(`xrel:tmdb:${id}`);
result[id] = cached !== undefined && cached.length > 0; if (cached === undefined) { result[id] = false; continue; }
const releases = cached?.releases || cached || [];
result[id] = releases.length > 0;
} }
res.json(result); res.json(result);
}); });

View File

@@ -457,7 +457,8 @@ function FavoritenKalender({ onOpenModal }) {
// ── Film-Detail-Modal (via Portal) ──────────────────────────────────────────── // ── Film-Detail-Modal (via Portal) ────────────────────────────────────────────
function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) { function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
const [detail, setDetail] = useState(null); const [detail, setDetail] = useState(null);
const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht const [xrel, setXrel] = useState(null); // null=nicht gesucht
const [xrelDates, setXrelDates] = useState([]);
const [xrelLoading, setXrelLoading] = useState(false); const [xrelLoading, setXrelLoading] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -502,6 +503,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
if (forceRefresh) params.set('refresh', '1'); if (forceRefresh) params.set('refresh', '1');
const r = await api(`/tools/media/movie/${tmdbId}/xrel?${params}`); const r = await api(`/tools/media/movie/${tmdbId}/xrel?${params}`);
setXrel(r.xrel || []); setXrel(r.xrel || []);
setXrelDates(r.dates || []);
if (r.has_xrel) onHasXrel?.(tmdbId); if (r.has_xrel) onHasXrel?.(tmdbId);
} catch(_) { setXrel([]); } } catch(_) { setXrel([]); }
finally { setXrelLoading(false); } finally { setXrelLoading(false); }
@@ -594,11 +596,11 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
</div> </div>
</div> </div>
{detail.release_dates?.length > 0 && ( {xrelDates.length > 0 && (
<div style={{ marginTop:16 }}> <div style={{ marginTop:16 }}>
<div style={{ ...S.head, marginBottom:8 }}>ERSCHEINUNGSDATEN</div> <div style={{ ...S.head, marginBottom:8 }}>ERSCHEINUNGSDATEN</div>
<div style={{ display:'flex', flexDirection:'column', gap:4 }}> <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
{detail.release_dates.map((r, i) => ( {xrelDates.map((r, i) => (
<div key={i} style={{ display:'flex', justifyContent:'space-between', alignItems:'center', <div key={i} style={{ display:'flex', justifyContent:'space-between', alignItems:'center',
fontSize:11, fontFamily:'monospace', padding:'3px 0', fontSize:11, fontFamily:'monospace', padding:'3px 0',
borderBottom:'1px solid rgba(255,255,255,0.05)' }}> borderBottom:'1px solid rgba(255,255,255,0.05)' }}>