Initial commit

This commit is contained in:
twotalesanimation
2026-06-11 10:46:09 +02:00
commit 81ad7e4ea9
223 changed files with 39530 additions and 0 deletions
+150
View File
@@ -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 });
}
}