Files
sofa/app/api/webhooks/[token]/route.ts
T
jake 49cea568a9 Add Sonarr and Radarr list integrations
- 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
2026-03-06 15:34:34 -05:00

68 lines
1.9 KiB
TypeScript

import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { db } from "@/lib/db/client";
import { integrations } from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
import type { WebhookEvent } from "@/lib/services/webhooks";
import {
parseEmbyPayload,
parseJellyfinPayload,
parsePlexPayload,
processWebhook,
} from "@/lib/services/webhooks";
const log = createLogger("webhooks");
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ token: string }> },
) {
const { token } = await params;
// Look up connection by token — this IS the auth
const connection = db
.select()
.from(integrations)
.where(eq(integrations.token, token))
.get();
if (!connection || !connection.enabled) {
// Always return 200 to avoid retry storms from media servers
return NextResponse.json({ ok: true });
}
// Only webhook-type integrations are handled here
if (connection.type !== "webhook") {
return NextResponse.json({ ok: true });
}
const provider = connection.provider as "plex" | "jellyfin" | "emby";
try {
let event: WebhookEvent | null;
if (provider === "plex") {
const formData = await req.formData();
event = parsePlexPayload(formData);
} else if (provider === "emby") {
const body = await req.json();
event = parseEmbyPayload(body);
} else {
const body = await req.json();
event = parseJellyfinPayload(body);
}
if (!event) {
// Not a relevant event type — silently ignore
return NextResponse.json({ ok: true });
}
await processWebhook(connection.id, connection.userId, provider, event);
} catch (err) {
// Swallow errors — never return non-200 to media servers
log.debug("Webhook processing failed:", err);
}
return NextResponse.json({ ok: true });
}