feat: Katalogschicht und lokaler Bild-Cache
Der Katalog ist bewusst gemeinsam für alle Nutzer und über (Typ, TMDB-ID) dedupliziert: zehn Leute mit derselben Serie brauchen die Episodenliste nur einmal. Persönliches steckt ausschließlich in LibraryEntry und Watch. Poster laufen über /api/image statt direkt zu TMDB — einmal holen, danach vom eigenen Server ausliefern. Größe und Pfad werden streng geprüft, damit daraus kein offener Proxy wird. Staffel 0 (Specials) wird importiert, zählt aber nicht zum Fortschritt; Episoden ohne eigene Laufzeit fallen auf die durchschnittliche Episodenlänge der Serie zurück.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCachedImage, isValidImagePath, isValidSize } from "@/lib/images";
|
||||
|
||||
/**
|
||||
* Ausliefern zwischengespeicherter TMDB-Bilder.
|
||||
*
|
||||
* /api/image?size=w342&path=/abc123.jpg
|
||||
*
|
||||
* Größe und Pfad werden streng geprüft — der Endpunkt soll kein offener Proxy
|
||||
* für beliebige Adressen werden.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const size = url.searchParams.get("size") ?? "w342";
|
||||
const path = url.searchParams.get("path") ?? "";
|
||||
|
||||
if (!isValidSize(size) || !isValidImagePath(path)) {
|
||||
return NextResponse.json({ error: "Ungültige Bildanfrage." }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await getCachedImage(size, path);
|
||||
return new NextResponse(new Uint8Array(image.data), {
|
||||
headers: {
|
||||
"content-type": image.contentType,
|
||||
// Bilder unter einem TMDB-Pfad ändern sich nicht mehr.
|
||||
"cache-control": "public, max-age=31536000, immutable",
|
||||
"x-flimmer-cache": image.fromCache ? "hit" : "miss",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Bild nicht verfügbar." }, { status: 404 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user