first draft videos

This commit is contained in:
overcuriousity
2025-08-12 14:53:11 +02:00
parent d6760d0f84
commit f159f904f0
9 changed files with 1789 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
// src/pages/api/video/cached/[...path].ts - Production video serving only
import type { APIRoute } from 'astro';
import { promises as fs } from 'fs';
import path from 'path';
export const GET: APIRoute = async ({ params, request }) => {
try {
const videoPath = params.path;
console.log(`[VIDEO SERVE] Request for cached video: ${videoPath}`);
if (!videoPath || typeof videoPath !== 'string') {
console.warn('[VIDEO SERVE] Invalid video path provided');
return new Response('Video not found', { status: 404 });
}
// Security: Prevent path traversal
const safePath = path.normalize(videoPath).replace(/^(\.\.[\/\\])+/, '');
const cacheDir = process.env.VIDEO_CACHE_DIR || './cache/videos';
const fullPath = path.join(cacheDir, safePath);
console.log(`[VIDEO SERVE] Resolved cache path: ${fullPath}`);
// Ensure the requested file is within the cache directory
if (!fullPath.startsWith(path.resolve(cacheDir))) {
console.error(`[VIDEO SERVE] Path traversal attempt blocked: ${fullPath}`);
return new Response('Access denied', { status: 403 });
}
try {
const stat = await fs.stat(fullPath);
if (!stat.isFile()) {
console.warn(`[VIDEO SERVE] Requested path is not a file: ${fullPath}`);
return new Response('Video not found', { status: 404 });
}
console.log(`[VIDEO SERVE] Serving cached video: ${safePath} (${Math.round(stat.size / 1024 / 1024)}MB)`);
// Update access time for LRU tracking (for emergency cleanup)
const now = new Date();
await fs.utimes(fullPath, now, stat.mtime).catch((err) => {
console.warn(`[VIDEO SERVE] Failed to update access time for ${safePath}: ${err.message}`);
});
// Determine content type
const ext = path.extname(fullPath).toLowerCase();
const contentType = getVideoMimeType(ext);
console.log(`[VIDEO SERVE] Content type: ${contentType}, File size: ${stat.size} bytes`);
// Handle range requests for video streaming
const range = request.headers.get('range');
const fileSize = stat.size;
if (range) {
console.log(`[VIDEO SERVE] Range request: ${range}`);
// Parse range header
const rangeMatch = range.match(/bytes=(\d+)-(\d*)/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1]);
const end = rangeMatch[2] ? parseInt(rangeMatch[2]) : fileSize - 1;
const chunkSize = end - start + 1;
console.log(`[VIDEO SERVE] Range: ${start}-${end}, chunk size: ${chunkSize}`);
if (start >= fileSize || end >= fileSize || start > end) {
console.warn(`[VIDEO SERVE] Invalid range: ${start}-${end} for file size ${fileSize}`);
return new Response('Range not satisfiable', {
status: 416,
headers: {
'Content-Range': `bytes */${fileSize}`
}
});
}
const fileStream = await fs.readFile(fullPath);
const chunk = fileStream.slice(start, end + 1);
console.log(`[VIDEO SERVE] Serving partial content: ${chunk.length} bytes`);
return new Response(chunk, {
status: 206,
headers: {
'Content-Type': contentType,
'Content-Length': chunkSize.toString(),
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=31536000', // Cache for 1 year
'Last-Modified': stat.mtime.toUTCString()
}
});
}
}
// Serve entire file
console.log(`[VIDEO SERVE] Serving complete file: ${safePath}`);
const fileBuffer = await fs.readFile(fullPath);
return new Response(fileBuffer, {
status: 200,
headers: {
'Content-Type': contentType,
'Content-Length': fileSize.toString(),
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=31536000', // Cache for 1 year
'Last-Modified': stat.mtime.toUTCString()
}
});
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(`[VIDEO SERVE] File not found: ${fullPath}`);
return new Response('Video not found', { status: 404 });
}
console.error(`[VIDEO SERVE] File system error for ${fullPath}:`, error);
throw error;
}
} catch (error) {
console.error('[VIDEO SERVE] Unexpected error serving cached video:', error);
return new Response('Internal server error', { status: 500 });
}
};
function getVideoMimeType(extension: string): string {
const mimeTypes: Record<string, string> = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogg': 'video/ogg',
'.mov': 'video/quicktime',
'.avi': 'video/x-msvideo',
'.m4v': 'video/x-m4v',
'.3gp': 'video/3gpp'
};
const mimeType = mimeTypes[extension] || 'application/octet-stream';
console.log(`[VIDEO SERVE] MIME type for ${extension}: ${mimeType}`);
return mimeType;
}

View File

@@ -0,0 +1,43 @@
// src/pages/api/video/process.ts
import type { APIRoute } from 'astro';
import { videoProcessor, type VideoMetadata } from '../../../utils/videoUtils.js';
import { apiResponse, apiError, apiServerError } from '../../../utils/api.js';
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json().catch(() => null);
if (!body) {
return apiError.badRequest('Request body must be valid JSON');
}
const { url, metadata = {}, options = {} } = body;
if (!url || typeof url !== 'string') {
return apiError.badRequest('Video URL is required');
}
// Validate URL
try {
new URL(url);
} catch {
return apiError.badRequest('Invalid video URL format');
}
console.log(`[VIDEO API] Processing video: ${url}`);
const processedVideo = await videoProcessor.processVideoUrl(url, metadata as Partial<VideoMetadata>);
const html = videoProcessor.generateVideoHTML(processedVideo, options);
return apiResponse.success({
processedVideo,
html,
cached: processedVideo.sources.some(s => s.cached),
requiresAuth: processedVideo.requiresAuth
});
} catch (error) {
console.error('[VIDEO API] Processing error:', error);
return apiServerError.internal(`Video processing failed: ${error.message}`);
}
};