Restaure routes/search.js pour compat /search.php (HTML format PHP)
This commit is contained in:
143
routes/search.js
Normal file
143
routes/search.js
Normal file
@@ -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, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function center(msg) {
|
||||||
|
return `<div style="text-align: center;">${esc(msg)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<div style="text-align: center; font-size: 14px; font-family: sans-serif;">';
|
||||||
|
|
||||||
|
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 += '<span style="display: inline-block; margin: 10px; vertical-align: top;">';
|
||||||
|
html += `<img src="${esc(src)}" width="200" height="300"/>`;
|
||||||
|
html +=
|
||||||
|
'<div style="display: inline-block; white-space: normal; overflow: auto; vertical-align: top; width: 400px; height: 300px; background-color: #484848; color: #ffffff;">';
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px;">';
|
||||||
|
if (m.filteredTitle) html += `FR <b>${esc(m.title)}</b> ${esc(m.year)}<br />`;
|
||||||
|
if (m.filteredEnglishTitle) html += `EN <b>${esc(m.englishTitle)}</b> ${esc(m.year)}<br />`;
|
||||||
|
if (m.filteredOriginalTitle) html += `VO <b>${esc(m.originalTitle)}</b> ${esc(m.year)}<br />`;
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px;">';
|
||||||
|
if (genres) html += esc(genres);
|
||||||
|
if (countries) html += `<b>${esc(countries)}</b>`;
|
||||||
|
if (runtime) html += formatRuntime(runtime);
|
||||||
|
html += '</p>';
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px;">';
|
||||||
|
if (imdb) {
|
||||||
|
html += `<a href="${esc(IMDB_URL)}/${esc(imdb)}" style="background-color: #f3ce13; color: #000000; text-decoration: none;" onclick="this.target='_blank';">`;
|
||||||
|
html += ` IMDb </a> ${esc(imdb)} <b>${esc(ivote)}</b> ${esc(ivoteCount)} `;
|
||||||
|
}
|
||||||
|
html += `<a href="${esc(movietvurl)}/${esc(m.tmdb)}" style="background-color: #01b4e4; color: #000000; text-decoration: none;" onclick="this.target='_blank';">`;
|
||||||
|
html += ` TMDb </a> ${esc(m.tmdb)} <b>${esc(tvote)}</b> ${esc(tvoteCount)}<br />`;
|
||||||
|
|
||||||
|
if (budget || revenue) {
|
||||||
|
html += `${esc(formatCurrency(budget))} ${esc(formatCurrency(revenue))}`;
|
||||||
|
}
|
||||||
|
html += '</p>';
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px; text-align: left;">';
|
||||||
|
if (seasons) {
|
||||||
|
html += '<b>Episodes finaux</b> ';
|
||||||
|
for (const s of seasons) html += `${esc(s)} `;
|
||||||
|
}
|
||||||
|
html += '</p>';
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px; text-align: left;">';
|
||||||
|
html += `<b>${esc(detail.tagline || '')}</b>`;
|
||||||
|
html += '</p>';
|
||||||
|
|
||||||
|
html += '<p style="margin: 10px; text-align: justify;">';
|
||||||
|
if (detail.overview) html += esc(detail.overview);
|
||||||
|
html += '</p>';
|
||||||
|
|
||||||
|
html += '</div></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { getPool } from './lib/searchEngine.js';
|
|||||||
import adminRoutes from './routes/admin.js';
|
import adminRoutes from './routes/admin.js';
|
||||||
import apiRoutes from './routes/api.js';
|
import apiRoutes from './routes/api.js';
|
||||||
import healthRoutes from './routes/health.js';
|
import healthRoutes from './routes/health.js';
|
||||||
|
import searchRoutes from './routes/search.js';
|
||||||
|
|
||||||
const fastify = Fastify({ logger: true, trustProxy: true });
|
const fastify = Fastify({ logger: true, trustProxy: true });
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ await fastify.register(fastifyStatic, {
|
|||||||
|
|
||||||
await fastify.register(adminRoutes);
|
await fastify.register(adminRoutes);
|
||||||
await fastify.register(apiRoutes);
|
await fastify.register(apiRoutes);
|
||||||
|
await fastify.register(searchRoutes);
|
||||||
await fastify.register(healthRoutes);
|
await fastify.register(healthRoutes);
|
||||||
|
|
||||||
// Serve raw project files only under /admin/files (still session-protected
|
// Serve raw project files only under /admin/files (still session-protected
|
||||||
|
|||||||
Reference in New Issue
Block a user