From cf38bd68544059b84d6afd7717208797caac153b Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 1 Sep 2026 20:25:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20TMDB-Adresse=20konfigurierbar=20und=20l?= =?UTF-8?q?okaler=20Ersatz=20f=C3=BCr=20die=20Suche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TMDB_BASE_URL hilft Instanzen hinter einem eigenen Proxy — und erlaubt es, die Suche ohne Schlüssel aus den Beispieldaten zu bedienen. Damit lässt sich die Oberfläche ausprobieren, bevor man einen Schlüssel besorgt, und die Screenshots der Anleitung zeigen eine Suche, die tatsächlich etwas findet. --- .env.example | 3 ++ scripts/tmdb-stub.mjs | 98 +++++++++++++++++++++++++++++++++++++++++++ src/lib/env.ts | 6 +++ src/lib/tmdb.ts | 4 +- 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 scripts/tmdb-stub.mjs 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() {