simplify video stuff
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user