fix: xREL sauber – 1 Cache-Key/Film, Modal sofort, xREL async, Hindi-Filter, max5/≤1080p
This commit is contained in:
@@ -5,23 +5,25 @@ const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||
|
||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||
const XREL_BASE = 'https://api.xrel.to/v2';
|
||||
|
||||
const THIS_YEAR = new Date().getFullYear();
|
||||
const MIN_YEAR = THIS_YEAR - 1;
|
||||
|
||||
// ── SQLite Cache (12h für Treffer, 1h für Leer-Ergebnisse) ───────────────────
|
||||
function dbCacheGet(key) {
|
||||
// Sprachen die wir NICHT wollen
|
||||
const BLOCKED_LANGS = new Set(['tr', 'hi', 'te', 'ta', 'ml', 'kn', 'bn', 'mr', 'pa']);
|
||||
|
||||
// Genre-IDs die wir nicht wollen (nur wenn das der EINZIGE Genre ist)
|
||||
// 16=Animation, 99=Documentary – aber nur rausfiltern wenn explizit kein Spielfilm-Genre dabei
|
||||
const DOC_GENRE_ID = 99;
|
||||
|
||||
// ── SQLite Cache ──────────────────────────────────────────────────────────────
|
||||
function cacheGet(key) {
|
||||
const row = db.prepare('SELECT value, expires_at FROM xrel_cache WHERE cache_key=?').get(key);
|
||||
if (!row) return undefined;
|
||||
if (Date.now() > row.expires_at) {
|
||||
db.prepare('DELETE FROM xrel_cache WHERE cache_key=?').run(key);
|
||||
return undefined;
|
||||
}
|
||||
if (Date.now() > row.expires_at) { db.prepare('DELETE FROM xrel_cache WHERE cache_key=?').run(key); return undefined; }
|
||||
try { return JSON.parse(row.value); } catch { return undefined; }
|
||||
}
|
||||
|
||||
function dbCacheSet(key, value, ttlMs = 12 * 60 * 60 * 1000) {
|
||||
db.prepare('INSERT OR REPLACE INTO xrel_cache (cache_key, value, expires_at) VALUES (?,?,?)')
|
||||
function cacheSet(key, value, ttlMs = 12 * 60 * 60 * 1000) {
|
||||
db.prepare('INSERT OR REPLACE INTO xrel_cache (cache_key,value,expires_at) VALUES (?,?,?)')
|
||||
.run(key, JSON.stringify(value), Date.now() + ttlMs);
|
||||
}
|
||||
|
||||
@@ -29,7 +31,6 @@ function dbCacheSet(key, value, ttlMs = 12 * 60 * 60 * 1000) {
|
||||
function getToken() {
|
||||
return db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get()?.value || '';
|
||||
}
|
||||
|
||||
async function tmdb(path, params = {}) {
|
||||
const token = getToken();
|
||||
if (!token) throw new Error('Kein TMDb API-Token konfiguriert');
|
||||
@@ -37,87 +38,72 @@ async function tmdb(path, params = {}) {
|
||||
url.searchParams.set('language', 'de-DE');
|
||||
url.searchParams.set('region', 'DE');
|
||||
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }
|
||||
});
|
||||
if (!res.ok) throw new Error(`TMDb Fehler: ${res.status}`);
|
||||
const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error(`TMDb ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
let genreCache = null;
|
||||
async function getGenreMap() {
|
||||
if (genreCache) return genreCache;
|
||||
const data = await tmdb('/genre/movie/list');
|
||||
const d = await tmdb('/genre/movie/list');
|
||||
genreCache = {};
|
||||
for (const g of data.genres) genreCache[g.id] = g.name;
|
||||
for (const g of d.genres) genreCache[g.id] = g.name;
|
||||
return genreCache;
|
||||
}
|
||||
|
||||
function extractFSK(releaseDates) {
|
||||
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
|
||||
function extractFSK(rd) {
|
||||
const de = rd?.results?.find(r => r.iso_3166_1 === 'DE');
|
||||
if (!de?.release_dates?.length) return null;
|
||||
const sorted = [...de.release_dates].sort((a, b) => (a.type === 3 ? -1 : 1));
|
||||
for (const r of sorted) {
|
||||
const c = (r.certification || '').trim();
|
||||
if (c) return c;
|
||||
}
|
||||
const sorted = [...de.release_dates].sort((a, b) => a.type === 3 ? -1 : 1);
|
||||
for (const r of sorted) { const c = (r.certification||'').trim(); if (c) return c; }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchAllPages(path, maxPages = 3) {
|
||||
const first = await tmdb(path, { page: 1 });
|
||||
const total = Math.min(first.total_pages || 1, maxPages);
|
||||
let results = [...first.results];
|
||||
if (total > 1) {
|
||||
const extra = await Promise.all(
|
||||
Array.from({ length: total - 1 }, (_, i) => tmdb(path, { page: i + 2 }))
|
||||
);
|
||||
const extra = await Promise.all(Array.from({ length: total-1 }, (_,i) => tmdb(path, { page: i+2 })));
|
||||
for (const p of extra) results = results.concat(p.results);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function filterMovies(list) {
|
||||
const seen = new Set();
|
||||
return list.filter(m => {
|
||||
if (m.original_language === 'tr') return false;
|
||||
const y = m.release_date ? parseInt(m.release_date.slice(0, 4)) : 0;
|
||||
if (BLOCKED_LANGS.has(m.original_language)) return false;
|
||||
const y = m.release_date ? parseInt(m.release_date.slice(0,4)) : 0;
|
||||
if (y > 0 && y < MIN_YEAR) return false;
|
||||
if (seen.has(m.id)) return false;
|
||||
seen.add(m.id);
|
||||
// Reine Dokus rausfiltern
|
||||
const genres = m.genre_ids || [];
|
||||
if (genres.length === 1 && genres[0] === DOC_GENRE_ID) return false;
|
||||
if (genres.length === 0 && m.vote_count < 5) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function enrichMovie(movie, genreMap, fsk, rank) {
|
||||
function enrichMovie(m, genreMap, fsk, rank) {
|
||||
return {
|
||||
id: movie.id,
|
||||
title: movie.title || movie.original_title,
|
||||
original_title: movie.original_title,
|
||||
original_language: movie.original_language,
|
||||
poster_path: movie.poster_path,
|
||||
overview: movie.overview,
|
||||
release_date: movie.release_date,
|
||||
vote_average: movie.vote_average,
|
||||
genres: (movie.genre_ids || []).map(id => genreMap[id]).filter(Boolean),
|
||||
fsk: fsk ?? null,
|
||||
rank: rank ?? null,
|
||||
id: m.id, title: m.title||m.original_title, original_title: m.original_title,
|
||||
original_language: m.original_language, poster_path: m.poster_path,
|
||||
overview: m.overview, release_date: m.release_date, vote_average: m.vote_average,
|
||||
genres: (m.genre_ids||[]).map(id => genreMap[id]).filter(Boolean),
|
||||
fsk: fsk??null, rank: rank??null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── xREL ─────────────────────────────────────────────────────────────────────
|
||||
const QUALITY_BLOCKLIST = /\b(2160p|4k|uhd|uhdbluray|uhdbd)\b/i;
|
||||
|
||||
async function xrelFetch(path, params = {}, timeoutMs = 8000) {
|
||||
async function xrelFetch(path, params = {}) {
|
||||
const url = new URL(XREL_BASE + path);
|
||||
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
|
||||
for (const [k,v] of Object.entries(params)) url.searchParams.set(k, String(v));
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(7000),
|
||||
});
|
||||
if (res.status === 429) {
|
||||
const wait = parseInt(res.headers.get('Retry-After') || '15') * 1000;
|
||||
await new Promise(r => setTimeout(r, wait));
|
||||
const r2 = await fetch(url.toString(), { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
|
||||
await new Promise(r => setTimeout(r, (parseInt(res.headers.get('Retry-After')||'10'))*1000));
|
||||
const r2 = await fetch(url.toString(), { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(7000) });
|
||||
if (!r2.ok) throw new Error(`xREL ${r2.status}`);
|
||||
return r2.json();
|
||||
}
|
||||
@@ -125,209 +111,172 @@ async function xrelFetch(path, params = {}, timeoutMs = 8000) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function isGermanRelease(dirname, mainLang) {
|
||||
if (mainLang === 'german') return true;
|
||||
return /\b(german|deutsch)(\.dl)?\b/i.test(dirname || '');
|
||||
}
|
||||
|
||||
// Sucht xREL ext_info_id über IMDb-ID (exakter Match – keine falschen Treffer)
|
||||
// Falls keine IMDb-ID: Titel-Suche mit strengem Titel-Vergleich als Fallback
|
||||
async function findExtInfoId(imdbId, title, originalTitle) {
|
||||
const cacheKey = `extinfoid:${imdbId || originalTitle || title}`.toLowerCase();
|
||||
const cached = dbCacheGet(cacheKey);
|
||||
// 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)
|
||||
async function getXrelData(tmdbId, imdbId, title, originalTitle) {
|
||||
const cacheKey = `xrel:tmdb:${tmdbId}`;
|
||||
const cached = cacheGet(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
// Strategie 1: IMDb-ID Suche via ext_info search (xREL gibt uris zurück)
|
||||
let extInfoId = null;
|
||||
|
||||
// Suche ext_info_id via IMDb-URI (exakter Match)
|
||||
if (imdbId) {
|
||||
const queries = [originalTitle, title].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i);
|
||||
const queries = [originalTitle, title].filter(Boolean).filter((v,i,a) => a.indexOf(v)===i);
|
||||
for (const q of queries) {
|
||||
try {
|
||||
const data = await xrelFetch('/search/ext_info.json', { q, type: 'movie', limit: 10 });
|
||||
const hits = Array.isArray(data.results) ? data.results : [];
|
||||
// Exakter Match über IMDb-URI
|
||||
const match = hits.find(h =>
|
||||
Array.isArray(h.uris) && h.uris.includes(`imdb:${imdbId}`)
|
||||
);
|
||||
if (match?.id) {
|
||||
dbCacheSet(cacheKey, match.id);
|
||||
return match.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
const data = await xrelFetch('/search/ext_info.json', { q: q.replace(/[*]/g,''), type:'movie', limit:10 });
|
||||
const hit = (data.results||[]).find(h => (h.uris||[]).includes(`imdb:${imdbId}`));
|
||||
if (hit?.id) { extInfoId = hit.id; break; }
|
||||
} catch(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategie 2: Titel-Suche mit strengem Vergleich (Fallback wenn keine IMDb-ID)
|
||||
const cleanQ = (t) => (t || '').replace(/[*#@!?:]/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
const queries = [originalTitle, title].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i);
|
||||
for (const q of queries) {
|
||||
try {
|
||||
const data = await xrelFetch('/search/ext_info.json', { q: q.replace(/[*#@!?:]/g, ''), type: 'movie', limit: 5 });
|
||||
const hits = Array.isArray(data.results) ? data.results : [];
|
||||
// Nur exakter Titel-Match akzeptieren
|
||||
const match = hits.find(h => cleanQ(h.title) === cleanQ(q) || cleanQ(h.alt_title) === cleanQ(q));
|
||||
if (match?.id) {
|
||||
dbCacheSet(cacheKey, match.id);
|
||||
return match.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
// Fallback: exakter Titel-Match
|
||||
if (!extInfoId) {
|
||||
const clean = (s) => (s||'').replace(/[*#!?:]/g,'').trim().toLowerCase();
|
||||
const queries = [originalTitle, title].filter(Boolean).filter((v,i,a) => a.indexOf(v)===i);
|
||||
for (const q of queries) {
|
||||
try {
|
||||
const data = await xrelFetch('/search/ext_info.json', { q: q.replace(/[*#!?:]/g,''), type:'movie', limit:5 });
|
||||
const hit = (data.results||[]).find(h => clean(h.title)===clean(q) || clean(h.alt_title)===clean(q));
|
||||
if (hit?.id) { extInfoId = hit.id; break; }
|
||||
} catch(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
dbCacheSet(cacheKey, null, 60 * 60 * 1000); // 1h für Nicht-Treffer
|
||||
return null;
|
||||
}
|
||||
|
||||
// Holt alle deutschen Releases für eine ext_info_id
|
||||
async function getGermanReleasesById(extInfoId) {
|
||||
const cacheKey = `releases:${extInfoId}`;
|
||||
const cached = dbCacheGet(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
if (!extInfoId) {
|
||||
cacheSet(cacheKey, [], 60*60*1000); // 1h für nicht gefunden
|
||||
return [];
|
||||
}
|
||||
|
||||
// Releases holen: Scene + P2P parallel, nur german, nur ≤1080p, max 5
|
||||
const [sceneData, p2pData] = await Promise.all([
|
||||
xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 100 }).catch(() => ({})),
|
||||
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100 }).catch(() => ({})),
|
||||
xrelFetch('/release/ext_info.json', { id: extInfoId, per_page: 50 }).catch(()=>({})),
|
||||
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 50 }).catch(()=>({})),
|
||||
]);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const r of sceneData.list || []) {
|
||||
if (!isGermanRelease(r.dirname, null)) continue;
|
||||
for (const r of sceneData.list||[]) {
|
||||
if (!/\b(german|deutsch)(\.dl)?\b/i.test(r.dirname||'')) continue;
|
||||
if (QUALITY_BLOCKLIST.test(r.dirname||'')) continue;
|
||||
results.push({
|
||||
dirname: r.dirname,
|
||||
size: r.size ? `${r.size.number} ${r.size.unit}` : null,
|
||||
category: 'Scene',
|
||||
url: r.link_href,
|
||||
date: r.time ? new Date(r.time * 1000).toISOString().slice(0, 10) : null,
|
||||
p2p: false,
|
||||
dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null,
|
||||
category: 'Scene', url: r.link_href,
|
||||
date: r.time ? new Date(r.time*1000).toISOString().slice(0,10) : null, p2p: false,
|
||||
});
|
||||
}
|
||||
|
||||
// P2P: alle Seiten
|
||||
const p2pPages = Math.min(p2pData.pagination?.total_pages || 1, 4);
|
||||
const allP2P = [...(p2pData.list || [])];
|
||||
if (p2pPages > 1) {
|
||||
const extra = await Promise.all(
|
||||
Array.from({ length: p2pPages - 1 }, (_, i) =>
|
||||
xrelFetch('/p2p/releases.json', { ext_info_id: extInfoId, per_page: 100, page: i + 2 })
|
||||
.then(d => d.list || []).catch(() => [])
|
||||
)
|
||||
);
|
||||
for (const list of extra) allP2P.push(...list);
|
||||
}
|
||||
|
||||
for (const r of allP2P) {
|
||||
for (const r of p2pData.list||[]) {
|
||||
if (r.main_lang !== 'german') continue;
|
||||
if (QUALITY_BLOCKLIST.test(r.dirname||'')) continue;
|
||||
results.push({
|
||||
dirname: r.dirname,
|
||||
size: r.size_mb ? `${(r.size_mb / 1024).toFixed(1)} GB` : null,
|
||||
category: r.category?.sub_cat || 'P2P',
|
||||
url: r.link_href,
|
||||
date: r.pub_time ? new Date(r.pub_time * 1000).toISOString().slice(0, 10) : null,
|
||||
p2p: true,
|
||||
dirname: r.dirname, size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null,
|
||||
category: r.category?.sub_cat||'P2P', url: r.link_href,
|
||||
date: r.pub_time ? new Date(r.pub_time*1000).toISOString().slice(0,10) : null, p2p: true,
|
||||
});
|
||||
}
|
||||
|
||||
results.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
|
||||
const ttl = results.length > 0 ? 12 * 60 * 60 * 1000 : 60 * 60 * 1000;
|
||||
dbCacheSet(cacheKey, results, ttl);
|
||||
return results;
|
||||
results.sort((a,b) => (b.date||'').localeCompare(a.date||''));
|
||||
const top5 = results.slice(0, 5);
|
||||
cacheSet(cacheKey, top5, top5.length > 0 ? 12*60*60*1000 : 60*60*1000);
|
||||
return top5;
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/now-playing', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/now_playing', 3), getGenreMap()]);
|
||||
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/now_playing',3), getGenreMap()]);
|
||||
const filtered = filterMovies(pages);
|
||||
const top10raw = filtered.slice(0, 10);
|
||||
const restRaw = filtered.slice(10);
|
||||
const top10 = await Promise.all(top10raw.map(async (m, i) => {
|
||||
const top10raw = filtered.slice(0,10), restRaw = filtered.slice(10);
|
||||
const top10 = await Promise.all(top10raw.map(async (m,i) => {
|
||||
let fsk = null;
|
||||
try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
|
||||
return enrichMovie(m, genreMap, fsk, i + 1);
|
||||
try { fsk = extractFSK(await tmdb(`/movie/${m.id}/release_dates`)); } catch(_) {}
|
||||
return enrichMovie(m, genreMap, fsk, i+1);
|
||||
}));
|
||||
res.json([...top10, ...restRaw.map(m => enrichMovie(m, genreMap, null, null))]);
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
res.json([...top10, ...restRaw.map(m => enrichMovie(m,genreMap,null,null))]);
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
router.get('/upcoming', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/upcoming', 3), getGenreMap()]);
|
||||
const [pages, genreMap] = await Promise.all([fetchAllPages('/movie/upcoming',3), getGenreMap()]);
|
||||
const filtered = filterMovies(pages).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 = {};
|
||||
for (const m of filtered) {
|
||||
const d = new Date(m.release_date);
|
||||
const dow = d.getDay();
|
||||
const mon = new Date(d);
|
||||
mon.setDate(d.getDate() + (dow === 0 ? -6 : 1 - dow));
|
||||
const key = mon.toISOString().slice(0, 10);
|
||||
const d = new Date(m.release_date), dow = d.getDay();
|
||||
const mon = new Date(d); mon.setDate(d.getDate()+(dow===0?-6:1-dow));
|
||||
const key = mon.toISOString().slice(0,10);
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(enrichMovie(m, genreMap, null, null));
|
||||
groups[key].push(enrichMovie(m,genreMap,null,null));
|
||||
}
|
||||
res.json(groups);
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Modal: TMDb-Daten sofort, xREL via separaten Endpoint
|
||||
router.get('/movie/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [detail, genreMap, extIds] = await Promise.all([
|
||||
tmdb(`/movie/${req.params.id}`, { append_to_response: 'credits,release_dates' }),
|
||||
getGenreMap(),
|
||||
tmdb(`/movie/${req.params.id}/external_ids`).catch(() => ({})),
|
||||
tmdb(`/movie/${req.params.id}/external_ids`).catch(()=>({})),
|
||||
]);
|
||||
const fsk = extractFSK(detail.release_dates);
|
||||
const imdbId = extIds.imdb_id || detail.imdb_id || null;
|
||||
|
||||
let xrel = [];
|
||||
try {
|
||||
const extInfoId = await findExtInfoId(imdbId, detail.title, detail.original_title);
|
||||
if (extInfoId) xrel = await getGermanReleasesById(extInfoId);
|
||||
} catch (_) {}
|
||||
|
||||
const fsk = extractFSK(detail.release_dates);
|
||||
const imdbId = extIds.imdb_id || null;
|
||||
res.json({
|
||||
id: detail.id,
|
||||
title: detail.title || detail.original_title,
|
||||
original_title: detail.original_title,
|
||||
tagline: detail.tagline,
|
||||
overview: detail.overview,
|
||||
poster_path: detail.poster_path,
|
||||
backdrop_path: detail.backdrop_path,
|
||||
release_date: detail.release_date,
|
||||
runtime: detail.runtime,
|
||||
vote_average: detail.vote_average,
|
||||
genres: (detail.genres || []).map(g => g.name),
|
||||
fsk,
|
||||
cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
|
||||
director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
|
||||
tmdb_url: `https://www.themoviedb.org/movie/${detail.id}`,
|
||||
imdb_url: imdbId ? `https://www.imdb.com/title/${imdbId}` : null,
|
||||
xrel,
|
||||
has_xrel: xrel.length > 0,
|
||||
id: detail.id, title: detail.title||detail.original_title,
|
||||
original_title: detail.original_title, tagline: detail.tagline,
|
||||
overview: detail.overview, poster_path: detail.poster_path,
|
||||
backdrop_path: detail.backdrop_path, release_date: detail.release_date,
|
||||
runtime: detail.runtime, vote_average: detail.vote_average,
|
||||
genres: (detail.genres||[]).map(g => g.name), fsk,
|
||||
cast: (detail.credits?.cast||[]).slice(0,8).map(a => a.name),
|
||||
director: detail.credits?.crew?.find(c => c.job==='Director')?.name||null,
|
||||
tmdb_url: `https://www.themoviedb.org/movie/${detail.id}`,
|
||||
imdb_url: imdbId ? `https://www.imdb.com/title/${imdbId}` : null,
|
||||
imdb_id: imdbId,
|
||||
});
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// POST /api/tools/media/xrel-check – Badge Check mit IMDb-ID
|
||||
// Frontend schickt { movies: [{ id, title, original_title, imdb_id }] }
|
||||
// xREL separat – lädt im Hintergrund, cached per tmdb_id
|
||||
router.get('/movie/:id/xrel', authenticate, async (req, res) => {
|
||||
try {
|
||||
const cacheKey = `xrel:tmdb:${req.params.id}`;
|
||||
const cached = cacheGet(cacheKey);
|
||||
if (cached !== undefined) return res.json({ xrel: cached, has_xrel: cached.length > 0 });
|
||||
|
||||
// Brauchen Titel + IMDb-ID
|
||||
const [detail, extIds] = await Promise.all([
|
||||
tmdb(`/movie/${req.params.id}`, { append_to_response: '' }).catch(()=>({})),
|
||||
tmdb(`/movie/${req.params.id}/external_ids`).catch(()=>({})),
|
||||
]);
|
||||
const imdbId = extIds.imdb_id || null;
|
||||
const xrel = await getXrelData(req.params.id, imdbId, detail.title, detail.original_title);
|
||||
res.json({ xrel, has_xrel: xrel.length > 0 });
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Badge-Check: benutzt denselben Cache wie Modal
|
||||
router.post('/xrel-check', authenticate, async (req, res) => {
|
||||
const movies = (req.body.movies || []).slice(0, 1);
|
||||
const movies = (req.body.movies||[]).slice(0,1);
|
||||
if (!movies.length) return res.json({});
|
||||
const m = movies[0];
|
||||
const cacheKey = `xrel:tmdb:${m.id}`;
|
||||
try {
|
||||
const extInfoId = await findExtInfoId(m.imdb_id || null, m.title, m.original_title);
|
||||
if (!extInfoId) return res.json({ [m.id]: false });
|
||||
const releases = await getGermanReleasesById(extInfoId);
|
||||
res.json({ [m.id]: releases.length > 0 });
|
||||
} catch (_) {
|
||||
res.json({ [m.id]: false });
|
||||
}
|
||||
const cached = cacheGet(cacheKey);
|
||||
if (cached !== undefined) return res.json({ [m.id]: cached.length > 0 });
|
||||
const xrel = await getXrelData(m.id, m.imdb_id||null, m.title, m.original_title);
|
||||
res.json({ [m.id]: xrel.length > 0 });
|
||||
} catch(_) { res.json({ [m.id]: false }); }
|
||||
});
|
||||
|
||||
router.get('/favorites', authenticate, (req, res) => {
|
||||
const rows = db.prepare('SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC, title ASC').all(req.user.id);
|
||||
res.json(rows);
|
||||
res.json(db.prepare('SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC').all(req.user.id));
|
||||
});
|
||||
|
||||
router.post('/favorites', authenticate, (req, res) => {
|
||||
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk } = req.body;
|
||||
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
|
||||
@@ -335,18 +284,13 @@ router.post('/favorites', authenticate, (req, res) => {
|
||||
db.prepare(`INSERT OR IGNORE INTO movie_favorites (user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk) VALUES (?,?,?,?,?,?,?,?)`)
|
||||
.run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'', release_date||'', JSON.stringify(genres||[]), fsk||'');
|
||||
res.json({ ok: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
router.delete('/favorites/:tmdb_id', authenticate, (req, res) => {
|
||||
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?').run(req.user.id, Number(req.params.tmdb_id));
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.get('/settings', authenticate, requireAdmin, (req, res) => {
|
||||
res.json({ configured: !!getToken() });
|
||||
});
|
||||
|
||||
router.get('/settings', authenticate, requireAdmin, (req, res) => res.json({ configured: !!getToken() }));
|
||||
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||
const { tmdb_token } = req.body;
|
||||
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
|
||||
@@ -354,11 +298,8 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||
genreCache = null;
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// DELETE /api/tools/media/xrel-cache – Cache leeren (Admin)
|
||||
router.delete('/xrel-cache', authenticate, requireAdmin, (req, res) => {
|
||||
const info = db.prepare('DELETE FROM xrel_cache').run();
|
||||
res.json({ deleted: info.changes });
|
||||
res.json({ deleted: db.prepare('DELETE FROM xrel_cache').run().changes });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user