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
+109
View File
@@ -0,0 +1,109 @@
// app/api/videos/[id]/route.ts
import { NextResponse } from "next/server";
import { prisma } from "../../../../lib/prisma";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth-options";
import { getVideoUrls } from "../../../../lib/video-urls";
export async function GET(req: Request, context: any) {
try {
// unwrap params
let params = context?.params;
if (typeof params?.then === "function") params = await params;
const id = params?.id;
if (!id) return NextResponse.json({ error: "Missing video id" }, { status: 400 });
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
include: {
enrollments: true,
},
});
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const video = await prisma.video.findUnique({
where: { id },
include: {
playlist: {
include: {
course: true,
courses: true,
},
},
uploader: {
select: {
id: true,
name: true,
image: true,
},
},
videoCourses: true,
},
});
if (!video) return NextResponse.json({ error: "Not found" }, { status: 404 });
const userCourseIds = user.enrollments.map((enrollment) => enrollment.courseId);
const playlistCourseIds = new Set<string>([video.playlist.courseId]);
(video.playlist.courses || []).forEach((mapping) => {
playlistCourseIds.add(mapping.courseId);
});
const hasPlaylistAccess = [...playlistCourseIds].some((courseId) =>
userCourseIds.includes(courseId)
);
if (!hasPlaylistAccess) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const restrictedCourseIds = (video.videoCourses || [])
.filter((assignment) => assignment.exclusive)
.map((assignment) => assignment.courseId);
if (
restrictedCourseIds.length > 0 &&
!restrictedCourseIds.some((courseId) => userCourseIds.includes(courseId))
) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Find the next video (index = current.index + 1)
const next = await prisma.video.findFirst({
where: {
playlistId: video.playlistId,
index: video.index + 1,
},
include: {
uploader: {
select: {
id: true,
name: true,
image: true,
},
},
},
});
// Add video URLs with HLS support
const videoUrls = getVideoUrls(video.id, video.url, video.transcodingStatus as any);
const responseVideo = {
...video,
videoUrls,
transcodingStatus: video.transcodingStatus,
restrictedCourseIds,
};
// Ensure we return the URL field explicitly (player expects `video.url`).
return NextResponse.json({ video: responseVideo, next });
} catch (err: any) {
console.error("GET /api/videos/[id] error:", err);
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}
+109
View File
@@ -0,0 +1,109 @@
// /api/videos/hls/[...path]/route.ts
// This endpoint serves HLS playlists and segments from the HLS directory
import { NextResponse } from 'next/server';
import * as fs from 'fs';
import * as path from 'path';
// Path to HLS directory (should match UPLOADS_DIR/hls)
const HLS_DIR = path.join(process.env.UPLOADS_DIR || '/uploads', 'hls');
export async function GET(
request: Request,
context: { params: Promise<{ path?: string[] }> }
) {
try {
const params = await context.params;
const pathSegments = params?.path || [];
if (pathSegments.length === 0) {
return NextResponse.json(
{ error: 'Invalid HLS request' },
{ status: 400 }
);
}
// Construct the file path and validate it
const requestedPath = path.join(HLS_DIR, ...pathSegments);
// Security: prevent directory traversal attacks
if (!requestedPath.startsWith(HLS_DIR)) {
return NextResponse.json(
{ error: 'Invalid request' },
{ status: 403 }
);
}
// Check if file exists
if (!fs.existsSync(requestedPath)) {
console.log(`[HLS] File not found: ${requestedPath}`);
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
// Read the file
const fileContent = fs.readFileSync(requestedPath);
// Determine content type based on file extension
let contentType = 'application/octet-stream';
if (requestedPath.endsWith('.m3u8')) {
contentType = 'application/vnd.apple.mpegurl';
} else if (requestedPath.endsWith('.ts')) {
contentType = 'video/mp2t';
} else if (requestedPath.endsWith('.mp4')) {
contentType = 'video/mp4';
}
// Return the file with appropriate headers
return new NextResponse(fileContent, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
'Access-Control-Allow-Origin': '*', // Allow CORS if needed
'Accept-Ranges': 'bytes',
},
});
} catch (error) {
console.error('[HLS] Error serving HLS file:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// HEAD request support for playlist validation
export async function HEAD(
request: Request,
context: { params: Promise<{ path?: string[] }> }
) {
try {
const params = await context.params;
const pathSegments = params?.path || [];
if (pathSegments.length === 0) {
return new NextResponse(null, { status: 400 });
}
const requestedPath = path.join(HLS_DIR, ...pathSegments);
// Security: prevent directory traversal attacks
if (!requestedPath.startsWith(HLS_DIR)) {
return new NextResponse(null, { status: 403 });
}
// Check if file exists
if (!fs.existsSync(requestedPath)) {
return new NextResponse(null, { status: 404 });
}
return new NextResponse(null, { status: 200 });
} catch (error) {
console.error('[HLS] Error in HEAD request:', error);
return new NextResponse(null, { status: 500 });
}
}
+81
View File
@@ -0,0 +1,81 @@
// app/api/videos/latest/route.ts
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth-options';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Get all courses the user is enrolled in
const enrolledCourses = await prisma.enrollment.findMany({
where: { userId: user.id },
select: { courseId: true },
});
const enrolledCourseIds = enrolledCourses.map((e) => e.courseId);
if (enrolledCourseIds.length === 0) {
return NextResponse.json({ videos: [] });
}
// Get the latest 10 videos from those courses
// Videos can be in playlists that belong to enrolled courses
const videos = await prisma.video.findMany({
where: {
playlist: {
OR: [
{ courseId: { in: enrolledCourseIds } },
{
courses: {
some: { courseId: { in: enrolledCourseIds } },
},
},
],
},
},
select: {
id: true,
title: true,
durationSec: true,
thumbnail: true,
createdAt: true,
uploader: {
select: {
id: true,
name: true,
image: true,
},
},
playlist: {
select: {
id: true,
title: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 10,
});
return NextResponse.json({ videos });
} catch (err: any) {
console.error('GET /api/videos/latest error', err);
return NextResponse.json(
{ error: err?.message ?? 'Server error' },
{ status: 500 }
);
}
}