151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
// app/api/admin/upload/finalize/route.ts
|
|
import { NextResponse } from "next/server";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { spawn } from "child_process";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { getServerSession } from "next-auth";
|
|
import { authOptions } from "@/lib/auth-options";
|
|
|
|
const UPLOADS_DIR = process.env.UPLOADS_DIR || "/uploads";
|
|
|
|
async function getVideoDurationFromFile(filePath: string): Promise<number | null> {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
const ffprobe = spawn("ffprobe", [
|
|
"-v", "error",
|
|
"-show_entries", "format=duration",
|
|
"-of", "default=noprint_wrappers=1:nokey=1:nokey=1",
|
|
filePath,
|
|
]);
|
|
|
|
let output = "";
|
|
let timedOut = false;
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
timedOut = true;
|
|
ffprobe.kill();
|
|
resolve(null);
|
|
}, 30000);
|
|
|
|
ffprobe.stdout.on("data", (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
ffprobe.on("close", (code) => {
|
|
clearTimeout(timeoutId);
|
|
if (!timedOut && code === 0) {
|
|
const duration = parseFloat(output.trim());
|
|
if (!isNaN(duration) && isFinite(duration) && duration > 0) {
|
|
resolve(Math.round(duration));
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
|
|
ffprobe.on("error", () => {
|
|
clearTimeout(timeoutId);
|
|
resolve(null);
|
|
});
|
|
} catch (err) {
|
|
resolve(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function checkAdmin() {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.email) throw { status: 401, message: "Unauthorized" };
|
|
const allowed = (process.env.ALLOWED_ADMINS || "")
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
if (allowed.length && !allowed.includes(session.user.email))
|
|
throw { status: 403, message: "Forbidden" };
|
|
return session;
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
try {
|
|
console.log('[Upload/Finalize] Request received');
|
|
|
|
await checkAdmin();
|
|
|
|
const body = await req.json();
|
|
const { videoId } = body;
|
|
|
|
if (!videoId) {
|
|
return NextResponse.json({ error: "videoId required" }, { status: 400 });
|
|
}
|
|
|
|
console.log('[Upload/Finalize] Checking for video:', videoId);
|
|
|
|
// Check if video exists in DB
|
|
const video = await prisma.video.findUnique({
|
|
where: { id: videoId },
|
|
});
|
|
|
|
if (!video) {
|
|
return NextResponse.json({ error: "video not found" }, { status: 404 });
|
|
}
|
|
|
|
// Check if file exists at the expected path
|
|
const videoPath = path.join(UPLOADS_DIR, "videos", `${videoId}.mp4`);
|
|
console.log('[Upload/Finalize] Checking file at:', videoPath);
|
|
|
|
try {
|
|
await fs.promises.access(videoPath, fs.constants.F_OK);
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: `File not found at ${videoPath}. Please copy the file there first.` },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
console.log('[Upload/Finalize] File found, extracting duration');
|
|
|
|
// Extract duration if not already set
|
|
let durationSec = video.durationSec;
|
|
if (!durationSec) {
|
|
try {
|
|
const extractedDuration = await getVideoDurationFromFile(videoPath);
|
|
if (extractedDuration !== null) {
|
|
durationSec = extractedDuration;
|
|
console.log('[Upload/Finalize] Extracted duration:', durationSec);
|
|
} else {
|
|
console.warn('[Upload/Finalize] Could not extract duration');
|
|
}
|
|
} catch (err) {
|
|
console.warn('[Upload/Finalize] Duration extraction error:', err);
|
|
}
|
|
}
|
|
|
|
// Update video: set URL, duration, and transcoding status to trigger processing
|
|
const videoUrl = `/uploads/videos/${videoId}.mp4`;
|
|
const updatedVideo = await prisma.video.update({
|
|
where: { id: videoId },
|
|
data: {
|
|
url: videoUrl,
|
|
transcodingStatus: 'uploaded', // Mark as ready for transcoding
|
|
...(durationSec !== null && { durationSec }),
|
|
},
|
|
});
|
|
|
|
console.log('[Upload/Finalize] Video updated successfully with URL:', videoUrl);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
video: updatedVideo,
|
|
message: `Video finalized${durationSec ? ` (${durationSec}s)` : ''}. HLS transcoding will start shortly.`,
|
|
}, { status: 200 });
|
|
} catch (err: any) {
|
|
console.error("[Upload/Finalize] Error:", err);
|
|
const status = err?.status ?? 500;
|
|
const message = err?.message ?? "server error";
|
|
return NextResponse.json({ error: message }, { status });
|
|
}
|
|
}
|