stremio/apps/master-api/src/site/routes.ts
Jos Vooges | STH db8e3e572f Add OMDb admin settings and poster API for IMDb metadata.
Store the key encrypted in the DB (not git) and proxy OMDb posters so clients never see the API key.
2026-09-16 23:33:01 +02:00

104 lines
3.3 KiB
TypeScript

import type { FastifyInstance } from "fastify";
import { prisma } from "../database/client";
import { requireAdmin } from "../auth/routes";
import { AppError } from "../security/errors";
import type { Config } from "../config";
import { createOmdbClientFromDb } from "../settings/omdb";
const SITE_SETTINGS_ID = "public_site";
type PublicSiteConfig = {
installPageEnabled: boolean;
};
function parseConfig(raw: string | null | undefined): Partial<PublicSiteConfig> {
if (!raw) return {};
try {
return JSON.parse(raw) as Partial<PublicSiteConfig>;
} catch {
return {};
}
}
async function readSiteConfig(): Promise<PublicSiteConfig> {
const row = await prisma.integrationSetting.findUnique({ where: { id: SITE_SETTINGS_ID } });
if (!row) {
return { installPageEnabled: true };
}
const parsed = parseConfig(row.configJson);
return {
installPageEnabled: parsed.installPageEnabled ?? row.enabled ?? true,
};
}
async function writeSiteConfig(cfg: PublicSiteConfig) {
await prisma.integrationSetting.upsert({
where: { id: SITE_SETTINGS_ID },
create: {
id: SITE_SETTINGS_ID,
enabled: cfg.installPageEnabled,
configJson: JSON.stringify(cfg),
},
update: {
enabled: cfg.installPageEnabled,
configJson: JSON.stringify(cfg),
},
});
}
export function registerSiteRoutes(app: FastifyInstance, config?: Config) {
app.get("/api/v1/public/site", async () => {
const cfg = await readSiteConfig();
return {
installPageEnabled: cfg.installPageEnabled,
productName: "Vonas Media Cluster",
shortName: "VMC",
docsRepo: "https://git.vonas.nl/jos/stremio.git",
};
});
/** OMDb Poster API proxy — key blijft server-side. */
app.get("/api/v1/public/omdb-poster/:imdbId", async (request, reply) => {
if (!config) {
throw new AppError("CONFIG", "OMDb poster proxy niet beschikbaar", 503);
}
const { imdbId } = request.params as { imdbId: string };
const query = request.query as { h?: string };
const heightRaw = Number.parseInt(query.h ?? "600", 10);
const height = Number.isFinite(heightRaw)
? Math.min(Math.max(heightRaw, 100), 1000)
: 600;
if (!/^tt\d+$/i.test(imdbId)) {
throw new AppError("INVALID_REQUEST", "Ongeldige IMDb-id", 400);
}
const client = await createOmdbClientFromDb(config.SESSION_SECRET, {
envFallback: config.OMDB_API_KEY,
publicBase: config.PUBLIC_URL,
});
if (!client) {
throw new AppError("NOT_CONFIGURED", "OMDb niet geconfigureerd", 404);
}
const poster = await client.fetchPosterBytes(imdbId, height);
if (!poster) {
throw new AppError("NOT_FOUND", "Poster niet gevonden", 404);
}
return reply
.header("Content-Type", poster.contentType)
.header("Cache-Control", "public, max-age=86400")
.send(Buffer.from(poster.body));
});
app.get("/api/v1/admin/site/install-page", { preHandler: requireAdmin }, async () => {
return readSiteConfig();
});
app.put("/api/v1/admin/site/install-page", { preHandler: requireAdmin }, async (request) => {
const body = (request.body || {}) as { enabled?: boolean };
if (typeof body.enabled !== "boolean") {
throw new AppError("INVALID_REQUEST", "enabled (boolean) verplicht", 400);
}
const cfg = { installPageEnabled: body.enabled };
await writeSiteConfig(cfg);
return cfg;
});
}