feat: Erscheinungsdaten von xREL (us-cine/de-cine/us-web/de-disc)

This commit is contained in:
2026-06-11 11:07:39 +02:00
parent c6d2fc2f11
commit 38d47d65f0
2 changed files with 53 additions and 31 deletions

View File

@@ -66,28 +66,28 @@ function extractFSK(rd) {
return null;
}
// 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)',
};
const RELEASE_TYPE_LABEL = { 2:'Kino (limited)', 3:'Kinostart', 4:'Web', 5:'Retail', 6:'TV' };
// Holt xREL Erscheinungsdaten für eine ext_info_id
async function getXrelReleaseDates(extInfoId) {
try {
const data = await xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' });
if (!Array.isArray(data.release_dates)) return [];
return data.release_dates
.filter(r => XREL_DATE_LABEL[r.type] && r.date)
.map(r => ({ label: XREL_DATE_LABEL[r.type], date: r.date }))
.sort((a, b) => a.date.localeCompare(b.date));
} catch(_) { return []; }
function extractReleaseDates(rd) {
if (!rd?.results) return [];
// Nur DE und US, Typen 2-5 (kein Premiere=1, kein TV=6)
const entries = [];
for (const country of ['DE', 'US']) {
const c = rd.results.find(r => r.iso_3166_1 === country);
if (!c) continue;
for (const r of c.release_dates) {
if (!r.release_date) continue;
const label = RELEASE_TYPE_LABEL[r.type];
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) {
const first = await tmdb(path, { page: 1 });
@@ -145,6 +145,27 @@ async function xrelFetch(path, params = {}) {
// Einzige Funktion für xREL: benutzt immer die ext_info_id und gibt max 5 deutsche ≤1080p Releases
// Cache-Key: tmdb_id (immer eindeutig, konsistent zwischen Badge und Modal)
// xREL Erscheinungsdaten via /ext_info/info.json
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)',
};
async function getXrelReleaseDates(extInfoId) {
try {
const data = await xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' });
if (!Array.isArray(data.release_dates)) return [];
return data.release_dates
.filter(r => XREL_DATE_LABEL[r.type] && r.date)
.map(r => ({ label: XREL_DATE_LABEL[r.type], date: r.date }))
.sort((a, b) => a.date.localeCompare(b.date));
} catch(_) { return []; }
}
async function getXrelData(tmdbId, imdbId, title, originalTitle) {
const cacheKey = `xrel:tmdb:${tmdbId}`;
const cached = cacheGet(cacheKey);
@@ -203,10 +224,11 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
return [];
}
// Releases holen: Scene + P2P parallel, nur german, nur ≤1080p, max 5
const [sceneData, p2pData] = await Promise.all([
// Releases + Erscheinungsdaten parallel holen
const [sceneData, p2pData, xrelDates] = await Promise.all([
xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }).catch(()=>({})),
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }).catch(()=>({})),
getXrelReleaseDates(extInfoId),
]);
const results = [];
@@ -309,8 +331,8 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
if (!forceRefresh) {
const cached = cacheGet(cacheKey);
if (cached !== undefined) {
const releases = cached?.releases || cached || [];
const dates = cached?.dates || [];
const releases = Array.isArray(cached) ? cached : (cached.releases || []);
const dates = Array.isArray(cached) ? [] : (cached.dates || []);
if (releases.length > 0 || dates.length > 0) {
return res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
}
@@ -337,9 +359,9 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
origTitle = detail.original_title || null;
}
const payload = await getXrelData(req.params.id, imdbId, title, origTitle);
const releases = payload?.releases || [];
const dates = payload?.dates || [];
const payload = await getXrelData(req.params.id, imdbId, title, origTitle);
const releases = Array.isArray(payload) ? payload : (payload?.releases || []);
const dates = Array.isArray(payload) ? [] : (payload?.dates || []);
res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
} catch(e) { res.status(500).json({ error: e.message }); }
});
@@ -354,7 +376,7 @@ router.get('/xrel-cached', authenticate, (req, res) => {
for (const id of ids) {
const cached = cacheGet(`xrel:tmdb:${id}`);
if (cached === undefined) { result[id] = false; continue; }
const releases = cached?.releases || cached || [];
const releases = Array.isArray(cached) ? cached : (cached.releases || []);
result[id] = releases.length > 0;
}
res.json(result);

View File

@@ -457,7 +457,7 @@ function FavoritenKalender({ onOpenModal }) {
// ── Film-Detail-Modal (via Portal) ────────────────────────────────────────────
function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
const [detail, setDetail] = useState(null);
const [xrel, setXrel] = useState(null); // null=nicht gesucht
const [xrel, setXrel] = useState(null); // null=nicht gesucht, []=gesucht
const [xrelDates, setXrelDates] = useState([]);
const [xrelLoading, setXrelLoading] = useState(false);
const [loading, setLoading] = useState(true);
@@ -467,7 +467,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
const detailRef = useRef(null);
useEffect(() => {
setLoading(true); setDetail(null); setXrel(null); setError('');
setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError('');
detailRef.current = null;
api(`/tools/media/movie/${tmdbId}`)
.then(d => {