feat: TMDB-Adresse konfigurierbar und lokaler Ersatz für die Suche
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user