Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
// JSON API — replaces api.php.
|
||||
// GET /api?t=movie&q=<id>
|
||||
// GET /api?t=tv&q=<id>
|
||||
// GET /api?t=search&q=<query>
|
||||
// JSON API.
|
||||
//
|
||||
// GET /api?t=movie&q=<id> -> TMDB detail (movie) + IMDb note
|
||||
// GET /api?t=tv&q=<id> -> TMDB detail (tv) + IMDb note
|
||||
// GET /api?t=imdb&q=<imdb_id> -> redirect-style: returns movie or tv detail
|
||||
// GET /api?t=providers&type=movie&q=<id> -> watch providers JSON
|
||||
// GET /api?t=search&q=<query> -> ranked search results
|
||||
|
||||
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 { LRUCache } from 'lru-cache';
|
||||
import { IMDB_URL, MOVIE_API_URL, MOVIE_URL, POSTER_URL, TV_API_URL, TV_URL } from '../config.js';
|
||||
import { formatCurrency, formatRuntime, pad2 } from '../lib/format.js';
|
||||
import {
|
||||
POSTER_URL, MOVIE_URL, TV_URL, MOVIE_API_URL, TV_API_URL, IMDB_URL,
|
||||
} from '../config.js';
|
||||
import { lookupImdb } from '../lib/imdbMapping.js';
|
||||
import { getRatings, lookupRating } from '../lib/imdbRatings.js';
|
||||
import { searchCacheHits, searchCacheMisses } from '../lib/metrics.js';
|
||||
import { entryPath, justwatchPath } from '../lib/paths.js';
|
||||
import { parseQuery } from '../lib/queryParser.js';
|
||||
import { search as runSearch } from '../lib/searchEngine.js';
|
||||
import { filterAndLower } from '../lib/titleFilter.js';
|
||||
|
||||
const searchCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 });
|
||||
|
||||
async function getDetail(type, id) {
|
||||
try {
|
||||
@@ -23,10 +29,17 @@ async function getDetail(type, id) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getProviders(type, id) {
|
||||
try {
|
||||
return JSON.parse(await readFile(justwatchPath(type, id), 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEntry(type, id) {
|
||||
const obj = await getDetail(type, id);
|
||||
if (!obj) return { error: 'Not found' };
|
||||
|
||||
const imdb = type === 'movie' ? obj.imdb_id : obj?.external_ids?.imdb_id;
|
||||
if (imdb) {
|
||||
const ratings = await getRatings();
|
||||
@@ -39,7 +52,20 @@ async function handleEntry(type, id) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function handleImdbLookup(imdbId) {
|
||||
const found = await lookupImdb(imdbId);
|
||||
if (!found) return { error: 'IMDb id not found in local mappings' };
|
||||
return handleEntry(found.type, found.tmdb);
|
||||
}
|
||||
|
||||
async function handleSearch(query) {
|
||||
const cached = searchCache.get(query);
|
||||
if (cached) {
|
||||
searchCacheHits.inc();
|
||||
return cached;
|
||||
}
|
||||
searchCacheMisses.inc();
|
||||
|
||||
const parsed = parseQuery(query);
|
||||
if (!parsed) return null;
|
||||
if (parsed.error) return { error: parsed.error };
|
||||
@@ -49,7 +75,9 @@ async function handleSearch(query) {
|
||||
|
||||
const matches = await runSearch(type, filteredTitleIn, yearin);
|
||||
if (!matches.length) {
|
||||
return { error: 'Not found in localized and original titles database' };
|
||||
const out = { error: 'Not found in localized and original titles database' };
|
||||
searchCache.set(query, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
const ratings = await getRatings();
|
||||
@@ -108,8 +136,6 @@ async function handleSearch(query) {
|
||||
}
|
||||
|
||||
if (episodein && Array.isArray(detail.seasons)) {
|
||||
// PHP loops and overwrites $data['results'][$j]['season'] for each season,
|
||||
// so only the LAST season is kept. Reproduce that behaviour.
|
||||
let lastSeason;
|
||||
for (const s of detail.seasons) {
|
||||
const sn = pad2(s.season_number || 0);
|
||||
@@ -125,7 +151,9 @@ async function handleSearch(query) {
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
return { results };
|
||||
const out = { results };
|
||||
searchCache.set(query, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function handle(req, reply) {
|
||||
@@ -142,6 +170,22 @@ async function handle(req, reply) {
|
||||
return handleEntry(t, id);
|
||||
}
|
||||
|
||||
if (t === 'imdb') {
|
||||
if (!q) return {};
|
||||
return handleImdbLookup(String(q));
|
||||
}
|
||||
|
||||
if (t === 'providers') {
|
||||
const type = req.query?.type;
|
||||
if (!q || (type !== 'movie' && type !== 'tv')) {
|
||||
return { error: 'providers requires type=movie|tv and q=<id>' };
|
||||
}
|
||||
const id = parseInt(q, 10);
|
||||
if (!Number.isInteger(id)) return {};
|
||||
const data = await getProviders(type, id);
|
||||
return data ?? { error: 'Providers not found' };
|
||||
}
|
||||
|
||||
if (t === 'search') {
|
||||
if (!q) return reply.send('');
|
||||
return (await handleSearch(q)) ?? {};
|
||||
|
||||
Reference in New Issue
Block a user