// HTML search view — replaces search.php (the public, human-facing version). import { readFile } from 'node:fs/promises'; import { entryPath } from '../lib/paths.js'; import { getRatings, lookupRating } from '../lib/imdbRatings.js'; import { parseQuery } from '../lib/queryParser.js'; import { filterAndLower } from '../lib/titleFilter.js'; import { search as runSearch } from '../lib/searchEngine.js'; import { formatCurrency, formatRuntime, pad2 } from '../lib/format.js'; import { POSTER_URL, NO_POSTER_URL, MOVIE_URL, TV_URL, IMDB_URL, } from '../config.js'; function esc(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function center(msg) { return `
${esc(msg)}
`; } async function getDetail(type, id) { try { return JSON.parse(await readFile(entryPath(type, id), 'utf8')); } catch { return null; } } async function render(query) { if (!query) return ''; const parsed = parseQuery(query); if (!parsed) return ''; if (parsed.error) return center(parsed.error); const { type, titlein, yearin, episodein } = parsed; const filteredTitleIn = filterAndLower(titlein); const matches = await runSearch(type, filteredTitleIn, yearin); if (!matches.length) { return center('Not found in localized and original titles database'); } const ratings = await getRatings(); const movietvurl = type === 'movie' ? MOVIE_URL : TV_URL; let html = '
'; for (const m of matches) { const detail = await getDetail(type, m.tmdb); if (!detail) continue; const poster = detail.poster_path; const src = poster ? `${POSTER_URL}/${poster}` : NO_POSTER_URL; let genres = ''; if (Array.isArray(detail.genres)) { for (const g of detail.genres) genres += `${g.name} `; } let countries = ''; if (Array.isArray(detail.production_countries)) { for (const c of detail.production_countries) countries += `${c.iso_3166_1} `; } const runtime = detail.runtime; const imdb = !episodein ? detail.imdb_id : detail?.external_ids?.imdb_id; const { rating: ivote, votes: ivoteCount } = lookupRating(ratings, imdb); const tvote = Math.round((parseFloat(detail.vote_average) || 0) * 10) / 10; const tvoteCount = parseInt(detail.vote_count, 10) || 0; const budget = detail.budget; const revenue = detail.revenue; let seasons; if (episodein && Array.isArray(detail.seasons)) { seasons = detail.seasons.map((s) => `S${pad2(s.season_number || 0)}E${pad2(s.episode_count || 0)}`); } html += ''; html += ``; html += '
'; html += '

'; if (m.filteredTitle) html += `FR ${esc(m.title)} ${esc(m.year)}
`; if (m.filteredEnglishTitle) html += `EN ${esc(m.englishTitle)} ${esc(m.year)}
`; if (m.filteredOriginalTitle) html += `VO ${esc(m.originalTitle)} ${esc(m.year)}
`; html += '

'; if (genres) html += esc(genres); if (countries) html += `${esc(countries)}`; if (runtime) html += formatRuntime(runtime); html += '

'; html += '

'; if (imdb) { html += ``; html += ` IMDb  ${esc(imdb)} ${esc(ivote)} ${esc(ivoteCount)} `; } html += ``; html += ` TMDb  ${esc(m.tmdb)} ${esc(tvote)} ${esc(tvoteCount)}
`; if (budget || revenue) { html += `${esc(formatCurrency(budget))} ${esc(formatCurrency(revenue))}`; } html += '

'; html += '

'; if (seasons) { html += 'Episodes finaux '; for (const s of seasons) html += `${esc(s)} `; } html += '

'; html += '

'; html += `${esc(detail.tagline || '')}`; html += '

'; html += '

'; if (detail.overview) html += esc(detail.overview); html += '

'; html += '
'; } html += '
'; return html; } async function handle(req, reply) { reply.header('Content-Type', 'text/html; charset=utf-8'); return render(req.query?.query || ''); } export default async function searchRoutes(fastify) { fastify.get('/search', handle); fastify.get('/search.php', handle); }