mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 06:15:39 -04:00
- New `lists` service fetches Sonarr/Radarr library via their REST APIs and auto-imports matching titles from TMDB into the user's watchlist - `app/api/lists/[token]/route.ts` webhook endpoint triggers a list sync - Unified `IntegrationCard` component replaces `WebhookCard`, handling both webhook-style (Plex/Jellyfin/Emby) and list-style (Sonarr/Radarr) integrations with per-type config forms - Schema migration adds `sonarr` and `radarr` to the integration type enum and a `listConnections` table for list-based integrations - 212 tests added for the lists service covering import, deduplication, and error handling
65 lines
1.6 KiB
TypeScript
65 lines
1.6 KiB
TypeScript
import path from "node:path";
|
|
import { type NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import {
|
|
fetchAndMaybeCache,
|
|
imageCacheEnabled,
|
|
} from "@/lib/services/image-cache";
|
|
|
|
const categorySchema = z.enum([
|
|
"posters",
|
|
"backdrops",
|
|
"stills",
|
|
"logos",
|
|
"profiles",
|
|
]);
|
|
|
|
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
|
|
export async function GET(
|
|
_req: NextRequest,
|
|
{ params }: { params: Promise<{ path: string[] }> },
|
|
) {
|
|
const segments = await params;
|
|
|
|
if (!imageCacheEnabled()) {
|
|
return NextResponse.json(
|
|
{ error: "Image cache disabled" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
if (segments.path.length !== 2) {
|
|
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
}
|
|
|
|
const [rawCategory, rawFilename] = segments.path;
|
|
|
|
const catResult = categorySchema.safeParse(rawCategory);
|
|
if (!catResult.success) {
|
|
return NextResponse.json({ error: "Invalid category" }, { status: 400 });
|
|
}
|
|
const category = catResult.data;
|
|
|
|
// Sanitize filename — only allow basename to prevent path traversal
|
|
const filename = path.basename(rawFilename);
|
|
if (!filename || filename !== rawFilename || filename.includes("..")) {
|
|
return NextResponse.json({ error: "Invalid filename" }, { status: 400 });
|
|
}
|
|
|
|
const tmdbPath = `/${filename}`;
|
|
const result = await fetchAndMaybeCache(tmdbPath, category);
|
|
|
|
if (!result) {
|
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
}
|
|
|
|
return new NextResponse(new Uint8Array(result.buffer), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": result.contentType,
|
|
"Cache-Control": IMMUTABLE_CACHE,
|
|
},
|
|
});
|
|
}
|