Files
sofa/app/api/webhooks/[token]/route.ts
T
jakeandClaude Opus 4.6 67356e04c6 Add Plex and Jellyfin webhook integration for automatic watch tracking
When a user finishes watching on their media server, a webhook fires and
Sofa logs it as watched, triggering all existing auto-transitions. Each
user configures their connection in settings and gets a unique webhook URL.

- Add webhookConnections and webhookEventLog schema tables
- Add TMDB findByExternalId for resolving IMDB/TVDB IDs
- Add source parameter to tracking functions (plex/jellyfin)
- Add webhook processing service with payload parsers, title resolution,
  and deduplication
- Add public webhook receiver route (token-based auth)
- Add authenticated settings API routes for managing connections
- Add Media Servers section to settings UI with Plex/Jellyfin cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:24:39 -05:00

58 lines
1.5 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 { webhookConnections } from "@/lib/db/schema";
import type { WebhookEvent } from "@/lib/services/webhooks";
import {
parseJellyfinPayload,
parsePlexPayload,
processWebhook,
} from "@/lib/services/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 = await db
.select()
.from(webhookConnections)
.where(eq(webhookConnections.token, token))
.get();
if (!connection || !connection.enabled) {
// Always return 200 to avoid retry storms from media servers
return NextResponse.json({ ok: true });
}
try {
let event: WebhookEvent | null;
if (connection.provider === "plex") {
const formData = await req.formData();
event = parsePlexPayload(formData);
} 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,
connection.provider,
event,
);
} catch {
// Swallow errors — never return non-200 to media servers
}
return NextResponse.json({ ok: true });
}