Initial commit
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user