feat: Erscheinungsdaten von xREL (us-cine/de-cine/us-web etc.) statt TMDb
This commit is contained in:
@@ -125,6 +125,12 @@ function enrichMovie(m, genreMap, fsk, rank) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── xREL ─────────────────────────────────────────────────────────────────────
|
// ── xREL ─────────────────────────────────────────────────────────────────────
|
||||||
|
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)',
|
||||||
|
};
|
||||||
|
|
||||||
const QUALITY_BLOCKLIST = r => /\b(2160p|4k|uhd|uhdbluray|uhdbd|remux)\b/i.test(r) || /COMPLETE[._](?:BLURAY|BD)/i.test(r);
|
const QUALITY_BLOCKLIST = r => /\b(2160p|4k|uhd|uhdbluray|uhdbd|remux)\b/i.test(r) || /COMPLETE[._](?:BLURAY|BD)/i.test(r);
|
||||||
|
|
||||||
async function xrelFetch(path, params = {}) {
|
async function xrelFetch(path, params = {}) {
|
||||||
@@ -145,27 +151,6 @@ async function xrelFetch(path, params = {}) {
|
|||||||
|
|
||||||
// Einzige Funktion für xREL: benutzt immer die ext_info_id und gibt max 5 deutsche ≤1080p Releases
|
// 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)
|
// 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) {
|
async function getXrelData(tmdbId, imdbId, title, originalTitle) {
|
||||||
const cacheKey = `xrel:tmdb:${tmdbId}`;
|
const cacheKey = `xrel:tmdb:${tmdbId}`;
|
||||||
const cached = cacheGet(cacheKey);
|
const cached = cacheGet(cacheKey);
|
||||||
@@ -224,13 +209,19 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Releases + Erscheinungsdaten parallel holen
|
// Releases + ext_info Details parallel holen
|
||||||
const [sceneData, p2pData, xrelDates] = await Promise.all([
|
const [sceneData, p2pData, infoData] = await Promise.all([
|
||||||
xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }).catch(()=>({})),
|
xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }).catch(()=>({})),
|
||||||
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }).catch(()=>({})),
|
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }).catch(()=>({})),
|
||||||
getXrelReleaseDates(extInfoId),
|
xrelFetch('/ext_info/info.json', { id: extInfoId, type: 'movie' }).catch(()=>({})),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// xREL Erscheinungsdaten
|
||||||
|
const xrel_dates = (infoData.release_dates||[])
|
||||||
|
.filter(d => XREL_DATE_LABEL[d.type])
|
||||||
|
.map(d => ({ label: XREL_DATE_LABEL[d.type], date: d.date }))
|
||||||
|
.sort((a,b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const r of sceneData.list||[]) {
|
for (const r of sceneData.list||[]) {
|
||||||
@@ -254,9 +245,8 @@ 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);
|
||||||
const payload = { releases: top5, dates: xrelDates };
|
const payload = { releases: top5, xrel_dates };
|
||||||
const ttl = (top5.length > 0 || xrelDates.length > 0) ? 12*60*60*1000 : 60*60*1000;
|
cacheSet(cacheKey, payload, top5.length > 0 || xrel_dates.length > 0 ? 12*60*60*1000 : 60*60*1000);
|
||||||
cacheSet(cacheKey, payload, ttl);
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,9 +322,9 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
|
|||||||
const cached = cacheGet(cacheKey);
|
const cached = cacheGet(cacheKey);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
const releases = Array.isArray(cached) ? cached : (cached.releases || []);
|
const releases = Array.isArray(cached) ? cached : (cached.releases || []);
|
||||||
const dates = Array.isArray(cached) ? [] : (cached.dates || []);
|
const xrel_dates = Array.isArray(cached) ? [] : (cached.xrel_dates || []);
|
||||||
if (releases.length > 0 || dates.length > 0) {
|
if (releases.length > 0 || xrel_dates.length > 0) {
|
||||||
return res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
|
return res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,10 +349,10 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
|
|||||||
origTitle = detail.original_title || null;
|
origTitle = detail.original_title || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await getXrelData(req.params.id, imdbId, title, origTitle);
|
const data = await getXrelData(req.params.id, imdbId, title, origTitle);
|
||||||
const releases = Array.isArray(payload) ? payload : (payload?.releases || []);
|
const releases = Array.isArray(data) ? data : (data.releases || []);
|
||||||
const dates = Array.isArray(payload) ? [] : (payload?.dates || []);
|
const xrel_dates = Array.isArray(data) ? [] : (data.xrel_dates || []);
|
||||||
res.json({ xrel: releases, dates, has_xrel: releases.length > 0 });
|
res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 });
|
||||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -375,9 +365,7 @@ 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}`);
|
||||||
if (cached === undefined) { result[id] = false; continue; }
|
result[id] = cached !== undefined && (Array.isArray(cached) ? cached.length > 0 : cached.releases?.length > 0);
|
||||||
const releases = Array.isArray(cached) ? cached : (cached.releases || []);
|
|
||||||
result[id] = releases.length > 0;
|
|
||||||
}
|
}
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -458,7 +458,6 @@ function FavoritenKalender({ onOpenModal }) {
|
|||||||
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, []=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('');
|
||||||
@@ -467,7 +466,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
|
|||||||
const detailRef = useRef(null);
|
const detailRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true); setDetail(null); setXrel(null); setXrelDates([]); setError('');
|
setLoading(true); setDetail(null); setXrel(null); setError('');
|
||||||
detailRef.current = null;
|
detailRef.current = null;
|
||||||
api(`/tools/media/movie/${tmdbId}`)
|
api(`/tools/media/movie/${tmdbId}`)
|
||||||
.then(d => {
|
.then(d => {
|
||||||
@@ -503,7 +502,6 @@ 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); }
|
||||||
@@ -582,7 +580,6 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
|
|||||||
{detail.vote_average > 0 && <Badge bg="rgba(255,230,109,0.15)" color="#ffe66d">★ {detail.vote_average.toFixed(1)}</Badge>}
|
{detail.vote_average > 0 && <Badge bg="rgba(255,230,109,0.15)" color="#ffe66d">★ {detail.vote_average.toFixed(1)}</Badge>}
|
||||||
{detail.runtime > 0 && <Badge bg="rgba(255,255,255,0.07)" color="rgba(255,255,255,0.7)">⏱ {detail.runtime} Min</Badge>}
|
{detail.runtime > 0 && <Badge bg="rgba(255,255,255,0.07)" color="rgba(255,255,255,0.7)">⏱ {detail.runtime} Min</Badge>}
|
||||||
{fskStyle && <Badge bg={fskStyle.bg} color={fskStyle.color}>FSK {detail.fsk}</Badge>}
|
{fskStyle && <Badge bg={fskStyle.bg} color={fskStyle.color}>FSK {detail.fsk}</Badge>}
|
||||||
{detail.release_date && <Badge bg="rgba(255,255,255,0.07)" color="rgba(255,255,255,0.55)">🇩🇪 {fmtDE(detail.release_date)}</Badge>}
|
|
||||||
{detail.countries?.length > 0 && <Badge bg="rgba(255,255,255,0.07)" color="rgba(255,255,255,0.4)">🌍 {detail.countries.slice(0,2).join(' · ')}</Badge>}
|
{detail.countries?.length > 0 && <Badge bg="rgba(255,255,255,0.07)" color="rgba(255,255,255,0.4)">🌍 {detail.countries.slice(0,2).join(' · ')}</Badge>}
|
||||||
</div>
|
</div>
|
||||||
{detail.genres?.length > 0 && (
|
{detail.genres?.length > 0 && (
|
||||||
@@ -596,11 +593,11 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{xrelDates.length > 0 && (
|
{detail.release_dates?.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 }}>
|
||||||
{xrelDates.map((r, i) => (
|
{detail.release_dates.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)' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user