149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { Privacy } from "@prisma/client";
|
|
import { requirePaidApiUser } from "@/lib/api-auth";
|
|
import { createVideoJob, saveUploadedFile } from "@/lib/jobs/create-job";
|
|
import {
|
|
applyCreatePlaylistToItems,
|
|
parseCreatePlaylistInput,
|
|
} from "@/lib/jobs/resolve-playlist";
|
|
import { filenameWithoutExtension } from "@/lib/constants";
|
|
import { mapWithConcurrency } from "@/lib/fs-utils";
|
|
import { getPlanLimits } from "@/lib/plans";
|
|
import { checkQuota } from "@/lib/quota";
|
|
import type { CreateJobPayload, CreatePlaylistRequest, ItemMetadata } from "@/lib/types";
|
|
|
|
export const maxDuration = 300;
|
|
|
|
type BatchItemInput = Partial<ItemMetadata> & {
|
|
filename?: string;
|
|
title?: string;
|
|
};
|
|
|
|
function defaultMetadata(title: string): ItemMetadata {
|
|
return {
|
|
title,
|
|
description: "",
|
|
tags: "",
|
|
privacy: "PUBLIC",
|
|
categoryId: "10",
|
|
resolution: "1920x1080",
|
|
notifySubscribers: true,
|
|
madeForKids: false,
|
|
embeddable: true,
|
|
creativeCommons: false,
|
|
includeWatermark: false,
|
|
playlistId: null,
|
|
};
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const { error, user } = await requirePaidApiUser(req);
|
|
if (error || !user) return error!;
|
|
|
|
const formData = await req.formData();
|
|
const image = formData.get("image") as File | null;
|
|
const audioFiles = formData.getAll("audio").filter((f): f is File => f instanceof File);
|
|
const metadataRaw = formData.get("metadata");
|
|
|
|
if (!image || audioFiles.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "Provide multipart fields: image (file), audio (one or more files)" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const limits = getPlanLimits(user.plan);
|
|
if (audioFiles.length > limits.maxBatchSize) {
|
|
return NextResponse.json(
|
|
{ error: `Batch limit exceeded. Your plan allows up to ${limits.maxBatchSize} files per batch.` },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const quotaCheck = await checkQuota(user.id, audioFiles.length);
|
|
if (!quotaCheck.ok) {
|
|
return NextResponse.json({ error: quotaCheck.error }, { status: 403 });
|
|
}
|
|
|
|
let itemMeta: BatchItemInput[] = [];
|
|
let defaults: Partial<ItemMetadata> = {};
|
|
let createPlaylist: CreatePlaylistRequest | null = null;
|
|
|
|
if (metadataRaw) {
|
|
try {
|
|
const parsed = JSON.parse(String(metadataRaw)) as {
|
|
items?: BatchItemInput[];
|
|
defaults?: Partial<ItemMetadata>;
|
|
createPlaylist?: unknown;
|
|
};
|
|
itemMeta = parsed.items ?? [];
|
|
defaults = parsed.defaults ?? {};
|
|
createPlaylist = parseCreatePlaylistInput(parsed.createPlaylist);
|
|
if (parsed.createPlaylist && !createPlaylist) {
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
"createPlaylist requires a title. Optional: description, privacy (public|unlisted|private)",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
} catch {
|
|
return NextResponse.json({ error: "metadata must be valid JSON" }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
const sessionKey = Date.now().toString();
|
|
const imageUpload = await saveUploadedFile(user.id, image, "image", user.plan, { sessionKey });
|
|
|
|
const uploads = await mapWithConcurrency(audioFiles, 4, (audio) =>
|
|
saveUploadedFile(user.id, audio, "audio", user.plan, { sessionKey }),
|
|
);
|
|
|
|
const builtItems: CreateJobPayload["items"] = uploads.map((upload, i) => {
|
|
const audio = audioFiles[i];
|
|
const metaInput = itemMeta[i] ?? itemMeta.find((m) => m.filename === audio.name) ?? {};
|
|
const base = defaultMetadata(
|
|
metaInput.title?.trim() || filenameWithoutExtension(audio.name),
|
|
);
|
|
const metadata: ItemMetadata = {
|
|
...base,
|
|
...defaults,
|
|
...metaInput,
|
|
title: (metaInput.title ?? defaults.title ?? base.title).trim(),
|
|
privacy: (metaInput.privacy ?? defaults.privacy ?? base.privacy) as Privacy,
|
|
};
|
|
|
|
return {
|
|
audioPath: upload.path,
|
|
audioFilename: upload.filename,
|
|
metadata,
|
|
};
|
|
});
|
|
|
|
const { items, playlist } = await applyCreatePlaylistToItems(user, builtItems, createPlaylist);
|
|
|
|
const job = await createVideoJob(user, {
|
|
imagePath: imageUpload.path,
|
|
items,
|
|
});
|
|
|
|
return NextResponse.json({
|
|
jobId: job.id,
|
|
itemCount: job.items.length,
|
|
status: job.status,
|
|
playlist: playlist
|
|
? { id: playlist.id, title: playlist.title, privacy: playlist.privacy }
|
|
: null,
|
|
});
|
|
} catch (err) {
|
|
console.error("Batch upload failed:", err);
|
|
const message = err instanceof Error ? err.message : "Batch upload failed";
|
|
const status = message.includes("Quota exceeded") || message.includes("Batch limit exceeded")
|
|
? 403
|
|
: 500;
|
|
return NextResponse.json({ error: message }, { status });
|
|
}
|
|
}
|