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