diff --git a/src/components/Poster.tsx b/src/components/Poster.tsx index d4abad9..50f7083 100644 --- a/src/components/Poster.tsx +++ b/src/components/Poster.tsx @@ -1,5 +1,5 @@ import { clsx } from "clsx"; -import { posterUrl, type ImageSize } from "@/lib/images"; +import { posterUrl, type ImageSize } from "@/lib/image-url"; import { FlimmerMark } from "@/components/brand/FlimmerLogo"; /** diff --git a/src/components/ProviderBadges.tsx b/src/components/ProviderBadges.tsx index 6b7a991..44f108f 100644 --- a/src/components/ProviderBadges.tsx +++ b/src/components/ProviderBadges.tsx @@ -1,4 +1,4 @@ -import { posterUrl } from "@/lib/images"; +import { posterUrl } from "@/lib/image-url"; /** * Streamingdienste als Badges. diff --git a/src/lib/image-url.ts b/src/lib/image-url.ts new file mode 100644 index 0000000..e604783 --- /dev/null +++ b/src/lib/image-url.ts @@ -0,0 +1,42 @@ +/** + * Bild-URLs und ihre Prüfung. + * + * Bewusst **ohne** `server-only`: Posterkacheln werden auch aus + * Client-Komponenten heraus gerendert (etwa in der sortierbaren Liste eines + * Universums). Alles, was tatsächlich den Server braucht — Abruf bei TMDB und + * der Dateicache — liegt in `images.ts`. + */ + +/** Zugelassene Größen — verhindert, dass über den Parameter beliebige URLs entstehen. */ +export const IMAGE_SIZES = [ + "w92", + "w154", + "w185", + "w300", + "w342", + "w500", + "w780", + "w1280", + "original", +] as const; + +export type ImageSize = (typeof IMAGE_SIZES)[number]; + +const PATH_PATTERN = /^\/[A-Za-z0-9]{8,64}\.(jpg|jpeg|png|svg|webp)$/; + +export function isValidImagePath(path: string): boolean { + return PATH_PATTERN.test(path); +} + +export function isValidSize(size: string): size is ImageSize { + return (IMAGE_SIZES as readonly string[]).includes(size); +} + +/** URL des eigenen Bild-Endpunkts — überall im UI statt der TMDB-Adresse benutzen. */ +export function posterUrl( + path: string | null | undefined, + size: ImageSize = "w342", +): string | null { + if (!path || !isValidImagePath(path)) return null; + return `/api/image?size=${size}&path=${encodeURIComponent(path)}`; +} diff --git a/src/lib/images.ts b/src/lib/images.ts index d6561ed..36ff997 100644 --- a/src/lib/images.ts +++ b/src/lib/images.ts @@ -4,6 +4,7 @@ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { env } from "./env"; import { IMAGE_BASE } from "./tmdb"; +import type { ImageSize } from "./image-url"; /** * Lokaler Bild-Zwischenspeicher. @@ -12,32 +13,13 @@ import { IMAGE_BASE } from "./tmdb"; * eigenen Server ausgeliefert. Das spart Anfragen, macht die Oberfläche auch * ohne Internetverbindung nutzbar und verhindert, dass jeder Seitenaufruf * einen Abruf bei einem Dritten auslöst. + * + * Die URL-Bildung selbst steht in `image-url.ts` — die brauchen auch + * Client-Komponenten, und sie darf deshalb nicht hinter `server-only` liegen. */ -/** Zugelassene Größen — verhindert, dass jemand über den Parameter beliebige URLs baut. */ -export const IMAGE_SIZES = [ - "w92", - "w154", - "w185", - "w300", - "w342", - "w500", - "w780", - "w1280", - "original", -] as const; - -export type ImageSize = (typeof IMAGE_SIZES)[number]; - -const PATH_PATTERN = /^\/[A-Za-z0-9]{8,64}\.(jpg|jpeg|png|svg|webp)$/; - -export function isValidImagePath(path: string): boolean { - return PATH_PATTERN.test(path); -} - -export function isValidSize(size: string): size is ImageSize { - return (IMAGE_SIZES as readonly string[]).includes(size); -} +export { IMAGE_SIZES, isValidImagePath, isValidSize, posterUrl } from "./image-url"; +export type { ImageSize } from "./image-url"; function cacheDir() { return join(env.dataDir, "images"); @@ -99,9 +81,3 @@ export async function getCachedImage(size: ImageSize, path: string): Promise