diff --git a/routes/search.js b/routes/search.js new file mode 100644 index 0000000..48de404 --- /dev/null +++ b/routes/search.js @@ -0,0 +1,143 @@ +// HTML search view — replaces search.php (the public, human-facing version). + +import { readFile } from 'node:fs/promises'; +import { IMDB_URL, MOVIE_URL, NO_POSTER_URL, POSTER_URL, TV_URL } from '../config.js'; +import { formatCurrency, formatRuntime, pad2 } from '../lib/format.js'; +import { getRatings, lookupRating } from '../lib/imdbRatings.js'; +import { entryPath } from '../lib/paths.js'; +import { parseQuery } from '../lib/queryParser.js'; +import { search as runSearch } from '../lib/searchEngine.js'; +import { filterAndLower } from '../lib/titleFilter.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); +} diff --git a/server.js b/server.js index f25418e..f334a09 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ import { getPool } from './lib/searchEngine.js'; import adminRoutes from './routes/admin.js'; import apiRoutes from './routes/api.js'; import healthRoutes from './routes/health.js'; +import searchRoutes from './routes/search.js'; const fastify = Fastify({ logger: true, trustProxy: true }); @@ -57,6 +58,7 @@ await fastify.register(fastifyStatic, { await fastify.register(adminRoutes); await fastify.register(apiRoutes); +await fastify.register(searchRoutes); await fastify.register(healthRoutes); // Serve raw project files only under /admin/files (still session-protected