diff --git a/.env.example b/.env.example index 72268b1..cae9b4a 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,9 @@ TMDB_ACCESS_TOKEN= # Sprache der Metadaten und Region für die Streaming-Verfügbarkeit TMDB_LANGUAGE=de-DE TMDB_REGION=DE +# Nur nötig, wenn TMDB über einen eigenen Proxy erreicht werden soll. +# TMDB_BASE_URL=https://api.themoviedb.org/3 +# TMDB_IMAGE_BASE_URL=https://image.tmdb.org/t/p # --- Hintergrundläufe -------------------------------------------------------- # Eingebauter Scheduler (prüft laufende Serien auf neue Episoden, räumt den diff --git a/scripts/tmdb-stub.mjs b/scripts/tmdb-stub.mjs new file mode 100644 index 0000000..d140e41 --- /dev/null +++ b/scripts/tmdb-stub.mjs @@ -0,0 +1,98 @@ +/** + * Kleiner TMDB-Ersatz für Screenshots und manuelles Ausprobieren. + * + * node scripts/tmdb-stub.mjs [port] + * + * Beantwortet die Suchendpunkte aus dem lokalen Katalog (den das Seed-Skript + * angelegt hat) im Format der TMDB-API. Damit lässt sich die Suchseite auch + * ohne echten TMDB-Schlüssel zeigen — für Screenshots der Anleitung und um + * die Oberfläche auszuprobieren, bevor man einen Schlüssel besorgt. + * + * Verwendung zusammen mit: + * TMDB_API_KEY=stub TMDB_BASE_URL=http://127.0.0.1:3099/3 npm run dev + */ +import { createServer } from "node:http"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../src/generated/prisma/client.ts"; + +try { + process.loadEnvFile(".env"); +} catch { + // Werte kommen aus der Umgebung. +} + +const port = Number(process.argv[2] ?? 3099); +const prisma = new PrismaClient({ + adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), +}); + +function toResult(title, mediaType) { + const date = title.releaseDate?.toISOString().slice(0, 10) ?? null; + return { + id: title.tmdbId, + media_type: mediaType, + name: mediaType === "tv" ? title.title : undefined, + title: mediaType === "movie" ? title.title : undefined, + original_name: title.originalTitle ?? undefined, + original_title: title.originalTitle ?? undefined, + overview: title.overview ?? "", + poster_path: title.posterPath, + backdrop_path: title.backdropPath, + first_air_date: mediaType === "tv" ? date : undefined, + release_date: mediaType === "movie" ? date : undefined, + vote_average: title.voteAverage ?? 0, + }; +} + +async function search(query, type) { + const titles = await prisma.title.findMany({ + where: { + tmdbId: { not: null }, + ...(type ? { type } : {}), + OR: [ + { title: { contains: query, mode: "insensitive" } }, + { originalTitle: { contains: query, mode: "insensitive" } }, + { genres: { has: query } }, + ], + }, + orderBy: { releaseDate: "desc" }, + take: 20, + }); + + return titles.map((title) => toResult(title, title.type === "SERIES" ? "tv" : "movie")); +} + +const server = createServer(async (request, response) => { + const url = new URL(request.url ?? "/", `http://localhost:${port}`); + const query = url.searchParams.get("query") ?? ""; + + const send = (body, status = 200) => { + response.writeHead(status, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(body)); + }; + + try { + if (url.pathname.startsWith("/3/search/")) { + const kind = url.pathname.split("/").pop(); + const type = kind === "tv" ? "SERIES" : kind === "movie" ? "MOVIE" : null; + const results = await search(query, type); + return send({ page: 1, results, total_pages: 1, total_results: results.length }); + } + + return send({ status_message: `Kein Ersatz für ${url.pathname}` }, 404); + } catch (error) { + return send({ status_message: String(error) }, 500); + } +}); + +server.listen(port, "127.0.0.1", () => { + console.log(`TMDB-Ersatz läuft auf http://127.0.0.1:${port}/3`); +}); + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => { + server.close(); + void prisma.$disconnect(); + process.exit(0); + }); +} diff --git a/src/lib/env.ts b/src/lib/env.ts index 6232633..e6ddba9 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -49,6 +49,12 @@ export const env = { tmdb: { apiKey: optional("TMDB_API_KEY"), + /** + * Abweichende Adresse der TMDB-API. Gedacht für Instanzen hinter einem + * eigenen Proxy und für den Testbetrieb; leer lassen heißt: direkt zu TMDB. + */ + baseUrl: optional("TMDB_BASE_URL", "https://api.themoviedb.org/3").replace(/\/+$/, ""), + imageBaseUrl: optional("TMDB_IMAGE_BASE_URL", "https://image.tmdb.org/t/p").replace(/\/+$/, ""), accessToken: optional("TMDB_ACCESS_TOKEN"), language: optional("TMDB_LANGUAGE", "de-DE"), region: optional("TMDB_REGION", "DE"), diff --git a/src/lib/tmdb.ts b/src/lib/tmdb.ts index 4d54653..cb6737c 100644 --- a/src/lib/tmdb.ts +++ b/src/lib/tmdb.ts @@ -10,8 +10,8 @@ import { env } from "./env"; * Zeichen einzeln nach draußen trägt. */ -const BASE = "https://api.themoviedb.org/3"; -export const IMAGE_BASE = "https://image.tmdb.org/t/p"; +const BASE = env.tmdb.baseUrl; +export const IMAGE_BASE = env.tmdb.imageBaseUrl; export class TmdbNotConfiguredError extends Error { constructor() {