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