feat: Originaltitel in Klammern, xREL Jahresfilter, Wildcard-Suche (hulk*2008), Suche neueste zuerst

This commit is contained in:
2026-06-11 15:34:46 +02:00
parent 6b566d721f
commit 945a947824
2 changed files with 49 additions and 9 deletions

View File

@@ -159,7 +159,7 @@ 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)
async function getXrelData(tmdbId, imdbId, title, originalTitle) { async function getXrelData(tmdbId, imdbId, title, originalTitle, movieYear) {
const cacheKey = `xrel:tmdb:${tmdbId}`; const cacheKey = `xrel:tmdb:${tmdbId}`;
const cached = cacheGet(cacheKey); const cached = cacheGet(cacheKey);
if (cached !== undefined) return cached; if (cached !== undefined) return cached;
@@ -232,22 +232,31 @@ async function getXrelData(tmdbId, imdbId, title, originalTitle) {
const results = []; const results = [];
// Jahresfilter: Release darf nicht älter sein als Filmjahr - 1
const releaseYearOk = (releaseDate) => {
if (!movieYear || !releaseDate) return true;
const y = parseInt(releaseDate.slice(0,4));
return y >= movieYear - 1;
};
for (const r of sceneData.list||[]) { for (const r of sceneData.list||[]) {
if (!/\b(german|deutsch)(\.dl)?\b/i.test(r.dirname||'')) continue; if (!/\b(german|deutsch)(\.dl)?\b/i.test(r.dirname||'')) continue;
if (QUALITY_BLOCKLIST(r.dirname||'')) continue; if (QUALITY_BLOCKLIST(r.dirname||'')) continue;
const date = r.time ? new Date(r.time*1000).toISOString().slice(0,10) : null;
if (!releaseYearOk(date)) continue;
results.push({ results.push({
dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null, dirname: r.dirname, size: r.size ? `${r.size.number} ${r.size.unit}` : null,
category: 'Scene', url: r.link_href, category: 'Scene', url: r.link_href, date, p2p: false,
date: r.time ? new Date(r.time*1000).toISOString().slice(0,10) : null, p2p: false,
}); });
} }
for (const r of p2pData.list||[]) { for (const r of p2pData.list||[]) {
if (r.main_lang !== 'german') continue; if (r.main_lang !== 'german') continue;
if (QUALITY_BLOCKLIST(r.dirname||'')) continue; if (QUALITY_BLOCKLIST(r.dirname||'')) continue;
const date = r.pub_time ? new Date(r.pub_time*1000).toISOString().slice(0,10) : null;
if (!releaseYearOk(date)) continue;
results.push({ results.push({
dirname: r.dirname, size: r.size_mb ? `${(r.size_mb/1024).toFixed(1)} GB` : null, 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, category: r.category?.sub_cat||'P2P', url: r.link_href, date, p2p: true,
date: r.pub_time ? new Date(r.pub_time*1000).toISOString().slice(0,10) : null, p2p: true,
}); });
} }
@@ -291,15 +300,29 @@ router.get('/upcoming', authenticate, async (req, res) => {
} catch(e) { res.status(500).json({ error: e.message }); } } catch(e) { res.status(500).json({ error: e.message }); }
}); });
// GET /api/tools/media/search?q=TITEL Filmsuche // GET /api/tools/media/search?q=TITEL Filmsuche mit Wildcard-Support
// hulk*2008 → sucht "hulk" mit year=2008
router.get('/search', authenticate, async (req, res) => { router.get('/search', authenticate, async (req, res) => {
try { try {
const q = (req.query.q || '').trim(); let q = (req.query.q || '').trim();
if (!q || q.length < 2) return res.json([]); if (!q || q.length < 2) return res.json([]);
// Wildcard: z.B. "hulk*2008" → query="hulk", year="2008"
let yearFilter = null;
const wildcardMatch = q.match(/^(.+?)\*(\d{4})$/);
if (wildcardMatch) {
q = wildcardMatch[1].trim();
yearFilter = wildcardMatch[2];
}
const searchParams = { query: q, include_adult: 'false' };
if (yearFilter) searchParams.year = yearFilter;
const [results, genreMap] = await Promise.all([ const [results, genreMap] = await Promise.all([
tmdb('/search/movie', { query: q, include_adult: 'false' }), tmdb('/search/movie', searchParams),
getGenreMap(), getGenreMap(),
]); ]);
const seen = new Set(); const seen = new Set();
const filtered = (results.results || []) const filtered = (results.results || [])
.filter(m => { .filter(m => {
@@ -310,6 +333,9 @@ router.get('/search', authenticate, async (req, res) => {
}) })
.slice(0, 20) .slice(0, 20)
.map(m => enrichMovie(m, genreMap, null, null)); .map(m => enrichMovie(m, genreMap, null, null));
// Neueste zuerst (Backend sortiert, Frontend sortiert auch konsistent)
filtered.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''));
res.json(filtered); res.json(filtered);
} catch(e) { res.status(500).json({ error: e.message }); } } catch(e) { res.status(500).json({ error: e.message }); }
}); });
@@ -380,7 +406,11 @@ router.get('/movie/:id/xrel', authenticate, async (req, res) => {
origTitle = detail.original_title || null; origTitle = detail.original_title || null;
} }
const data = await getXrelData(req.params.id, imdbId, title, origTitle); const movieYear = title ? (infoData?.release_dates?.[0]?.date?.slice(0,4) ? null : null) : null;
// Filmjahr aus TMDb release_date
let filmYear = null;
if (req.query.year) filmYear = parseInt(req.query.year);
const data = await getXrelData(req.params.id, imdbId, title, origTitle, filmYear);
const releases = Array.isArray(data) ? data : (data.releases || []); const releases = Array.isArray(data) ? data : (data.releases || []);
const xrel_dates = Array.isArray(data) ? [] : (data.xrel_dates || []); const xrel_dates = Array.isArray(data) ? [] : (data.xrel_dates || []);
res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 }); res.json({ xrel: releases, xrel_dates, has_xrel: releases.length > 0 });

View File

@@ -214,6 +214,12 @@ function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) {
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}> display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
{movie.title} {movie.title}
</div> </div>
{movie.original_title && movie.original_title !== movie.title && (
<div style={{ fontSize:9, color:'rgba(255,255,255,0.35)', fontFamily:'monospace',
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', fontStyle:'italic' }}>
({movie.original_title})
</div>
)}
{movie.genres?.length > 0 && ( {movie.genres?.length > 0 && (
<div style={{ fontSize:9, color:'#4ecdc4', fontFamily:'monospace', overflow:'hidden', <div style={{ fontSize:9, color:'#4ecdc4', fontFamily:'monospace', overflow:'hidden',
textOverflow:'ellipsis', whiteSpace:'nowrap' }}> textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
@@ -384,6 +390,8 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
setLoading(true); setError(''); setResults(null); setLoading(true); setError(''); setResults(null);
try { try {
const list = await api(`/tools/media/search?q=${encodeURIComponent(trimmed)}`); const list = await api(`/tools/media/search?q=${encodeURIComponent(trimmed)}`);
// Neueste zuerst
list.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''));
setResults(list); setResults(list);
// xREL-Badges aus Cache // xREL-Badges aus Cache
if (list.length) { if (list.length) {
@@ -568,6 +576,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
title: d.title || '', title: d.title || '',
orig: d.original_title || '', orig: d.original_title || '',
imdb_id: d.imdb_id || '', imdb_id: d.imdb_id || '',
year: d.release_date ? d.release_date.slice(0,4) : '',
}); });
api(`/tools/media/movie/${tmdbId}/xrel?${params}`) api(`/tools/media/movie/${tmdbId}/xrel?${params}`)
.then(r => { .then(r => {
@@ -591,6 +600,7 @@ function MovieModal({ tmdbId, onClose, mobile, onHasXrel }) {
title: d.title || '', title: d.title || '',
orig: d.original_title || '', orig: d.original_title || '',
imdb_id: d.imdb_id || '', imdb_id: d.imdb_id || '',
year: d.release_date ? d.release_date.slice(0,4) : '',
}); });
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}`);