import { NextRequest } from "next/server"; import JSZip from "jszip"; import { getServerSession } from "@/lib/auth/guards"; import { prisma } from "@/lib/db"; import { storage } from "@/lib/storage"; import type { StructuredScript } from "@/lib/ai/types"; export const dynamic = "force-dynamic"; /** * Bundle everything for an episode into a single .zip download: * - audio.mp3 (the rendered episode, if present) * - cover.png (the cover art, if present) * - script.txt (the full transcript, speaker-labelled) * - show-notes.md (title + section outline) * * Ownership-gated: only the owning user (or an admin) may export. */ export async function GET( _req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const session = await getServerSession(); if (!session) return new Response("Unauthorized", { status: 401 }); const episode = await prisma.episode.findUnique({ where: { id }, include: { script: true, audioAsset: true, coverArt: true, speakers: true, }, }); if (!episode) return new Response("Not found", { status: 404 }); if (episode.userId !== session.user.id && session.user.role !== "admin") { return new Response("Forbidden", { status: 403 }); } const speakerNames: Record = {}; for (const s of episode.speakers) speakerNames[s.speakerKey] = s.displayName; const zip = new JSZip(); // Audio (if rendered). if (episode.audioAsset && (await storage().exists(episode.audioAsset.storageKey))) { const audio = await storage().get(episode.audioAsset.storageKey); const ext = episode.audioAsset.format || "mp3"; zip.file(`audio.${ext}`, audio); } // Cover art (if present). if (episode.coverArt && (await storage().exists(episode.coverArt.storageKey))) { const art = await storage().get(episode.coverArt.storageKey); const ext = episode.coverArt.storageKey.split(".").pop()?.toLowerCase() ?? "png"; zip.file(`cover.${ext}`, art); } // Transcript + show notes derived from the structured script. if (episode.script) { const script = episode.script.content as unknown as StructuredScript; zip.file("script.txt", buildTranscript(episode.title, script, speakerNames)); zip.file("show-notes.md", buildShowNotes(episode, script)); } const blob = await zip.generateAsync({ type: "nodebuffer" }); const filename = `${slugify(episode.title) || "episode"}.zip`; return new Response(blob as BodyInit, { headers: { "Content-Type": "application/zip", "Content-Length": String(blob.byteLength), "Content-Disposition": `attachment; filename="${filename}"`, "Cache-Control": "private, no-store", }, }); } function buildTranscript( title: string, script: StructuredScript, speakerNames: Record ): string { const lines: string[] = [title, "=".repeat(title.length), ""]; for (const section of script.sections ?? []) { lines.push(`## ${section.title}`, ""); for (const turn of section.turns ?? []) { const name = speakerNames[turn.speakerKey] ?? turn.speakerKey; lines.push(`${name}: ${turn.text}`, ""); } } return lines.join("\n").trimEnd() + "\n"; } function buildShowNotes( episode: { title: string; topic: string; format: string; language: string; targetLengthMin: number }, script: StructuredScript ): string { const lines: string[] = [ `# ${episode.title}`, "", episode.topic, "", `- **Format:** ${episode.format.replace("_", "-").toLowerCase()}`, `- **Language:** ${episode.language.toUpperCase()}`, `- **Target length:** ${episode.targetLengthMin} min`, "", "## In this episode", "", ]; for (const section of script.sections ?? []) { lines.push(`- ${section.title}`); } lines.push("", "---", "Generated with Podcast Distribution AI."); return lines.join("\n") + "\n"; } function slugify(s: string): string { return s .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 60); }