"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.uploadRoutes = uploadRoutes; const promises_1 = require("stream/promises"); const fs_1 = require("fs"); const promises_2 = require("fs/promises"); const path_1 = require("path"); const crypto_1 = require("crypto"); const database_1 = require("../../../config/database"); const ffprobe_service_1 = require("../services/ffprobe.service"); const thumbnail_service_1 = require("../services/thumbnail.service"); const logger_1 = require("../../../utils/logger"); const zod_1 = require("zod"); const auth_1 = require("../middleware/auth"); // Allowed video extensions const ALLOWED_EXTENSIONS = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.flv']; // Zod schema for upload metadata const UploadMetadataSchema = zod_1.z.object({ title: zod_1.z.string().optional(), producer: zod_1.z.string().optional(), creator: zod_1.z.string().optional(), }); /** * Upload a single video file */ async function uploadVideo(request, reply) { let tempFilePath = null; try { // Get the uploaded file const data = await request.file(); if (!data) { return reply.code(400).send({ message: 'No file uploaded' }); } // Validate file extension const ext = (0, path_1.extname)(data.filename).toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { return reply.code(400).send({ message: `Invalid file type. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`, }); } // Generate unique filename const filename = `${(0, crypto_1.randomUUID)()}${ext}`; const inboxDir = '/media/local/inbox'; // Ensure inbox directory exists await (0, promises_2.mkdir)(inboxDir, { recursive: true }); const filePath = (0, path_1.join)(inboxDir, filename); tempFilePath = filePath; // Stream file to disk (must consume file stream BEFORE reading metadata fields, // because Fastify multipart/busboy only makes fields available after the file stream ends) logger_1.logger.info(`Uploading video to ${filePath}`); await (0, promises_1.pipeline)(data.file, (0, fs_1.createWriteStream)(filePath)); // Extract metadata fields from form data (now available after file stream consumed) const metadataFields = data.fields; const metadata = { title: metadataFields.title?.value, producer: metadataFields.producer?.value, creator: metadataFields.creator?.value, }; // Validate metadata const validatedMetadata = UploadMetadataSchema.parse(metadata); // Validate video file logger_1.logger.info(`Validating video file: ${filePath}`); const isValid = await (0, ffprobe_service_1.validateVideoFile)(filePath); if (!isValid) { await (0, promises_2.unlink)(filePath); return reply.code(400).send({ message: 'Invalid or corrupted video file' }); } // Extract metadata logger_1.logger.info(`Extracting metadata from: ${filePath}`); const videoMetadata = await (0, ffprobe_service_1.extractVideoMetadata)(filePath); // Insert into database const video = await database_1.prisma.video.create({ data: { path: filePath, // Store full path: /media/local/inbox/uuid.mp4 filename, originalFilename: data.filename, title: validatedMetadata.title || data.filename, durationSeconds: videoMetadata.durationSeconds, width: videoMetadata.width, height: videoMetadata.height, orientation: videoMetadata.orientation, quality: videoMetadata.quality, hasAudio: videoMetadata.hasAudio, fileSize: videoMetadata.fileSize, directoryType: 'inbox', isValid: true, isShort: videoMetadata.durationSeconds != null && videoMetadata.durationSeconds <= 60, producer: validatedMetadata.producer || null, creator: validatedMetadata.creator || null, }, }); logger_1.logger.info(`Video uploaded successfully: ${video.id}`); // Generate thumbnail try { const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({ videoPath: filePath, videoId: video.id, duration: videoMetadata.durationSeconds, orientation: videoMetadata.orientation, }); // Update video with thumbnail path await database_1.prisma.video.update({ where: { id: video.id }, data: { thumbnailPath }, }); logger_1.logger.info(`Thumbnail generated for video ${video.id}`); } catch (thumbnailError) { // Log error but don't fail the upload logger_1.logger.error(`Failed to generate thumbnail for video ${video.id}:`, thumbnailError); } return reply.code(201).send({ message: 'Video uploaded successfully', video, }); } catch (error) { // Clean up file on error if (tempFilePath) { try { await (0, promises_2.unlink)(tempFilePath); } catch (cleanupError) { logger_1.logger.error('Failed to clean up file after error:', cleanupError); } } logger_1.logger.error('Video upload failed:', error); if (error instanceof zod_1.z.ZodError) { return reply.code(400).send({ message: 'Invalid metadata', errors: error.errors, }); } return reply.code(500).send({ message: error instanceof Error ? error.message : 'Upload failed', }); } } /** * Upload multiple video files in batch */ async function uploadBatch(request, reply) { try { const files = request.files(); const results = []; for await (const file of files) { let tempFilePath = null; try { // Validate file extension const ext = (0, path_1.extname)(file.filename).toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { results.push({ filename: file.filename, success: false, error: `Invalid file type. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`, }); continue; } // Generate unique filename const filename = `${(0, crypto_1.randomUUID)()}${ext}`; const inboxDir = '/media/local/inbox'; // Ensure inbox directory exists await (0, promises_2.mkdir)(inboxDir, { recursive: true }); const filePath = (0, path_1.join)(inboxDir, filename); tempFilePath = filePath; // Stream file to disk await (0, promises_1.pipeline)(file.file, (0, fs_1.createWriteStream)(filePath)); // Validate video file const isValid = await (0, ffprobe_service_1.validateVideoFile)(filePath); if (!isValid) { await (0, promises_2.unlink)(filePath); results.push({ filename: file.filename, success: false, error: 'Invalid or corrupted video file', }); continue; } // Extract metadata const videoMetadata = await (0, ffprobe_service_1.extractVideoMetadata)(filePath); // Insert into database const video = await database_1.prisma.video.create({ data: { path: filePath, // Store full path: /media/local/inbox/uuid.mp4 filename, originalFilename: file.filename, title: file.filename, durationSeconds: videoMetadata.durationSeconds, width: videoMetadata.width, height: videoMetadata.height, orientation: videoMetadata.orientation, quality: videoMetadata.quality, hasAudio: videoMetadata.hasAudio, fileSize: videoMetadata.fileSize, directoryType: 'inbox', isValid: true, isShort: videoMetadata.durationSeconds != null && videoMetadata.durationSeconds <= 60, }, }); // Generate thumbnail try { const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({ videoPath: filePath, videoId: video.id, duration: videoMetadata.durationSeconds, orientation: videoMetadata.orientation, }); // Update video with thumbnail path await database_1.prisma.video.update({ where: { id: video.id }, data: { thumbnailPath }, }); logger_1.logger.info(`Thumbnail generated for video ${video.id}`); } catch (thumbnailError) { // Log error but don't fail the upload logger_1.logger.error(`Failed to generate thumbnail for video ${video.id}:`, thumbnailError); } results.push({ filename: file.filename, success: true, video, }); logger_1.logger.info(`Batch upload successful: ${file.filename} -> ${video.id}`); } catch (error) { // Clean up file on error if (tempFilePath) { try { await (0, promises_2.unlink)(tempFilePath); } catch (cleanupError) { logger_1.logger.error('Failed to clean up file after error:', cleanupError); } } logger_1.logger.error(`Batch upload failed for ${file.filename}:`, error); results.push({ filename: file.filename, success: false, error: error instanceof Error ? error.message : 'Upload failed', }); } } const successCount = results.filter((r) => r.success).length; const failCount = results.length - successCount; return reply.code(207).send({ message: `Batch upload complete: ${successCount} succeeded, ${failCount} failed`, results, }); } catch (error) { logger_1.logger.error('Batch upload failed:', error); return reply.code(500).send({ message: error instanceof Error ? error.message : 'Batch upload failed', }); } } async function uploadRoutes(fastify) { // Single file upload fastify.post('/upload', { preHandler: auth_1.requireAdminRole, }, uploadVideo); // Batch upload fastify.post('/upload/batch', { preHandler: auth_1.requireAdminRole, }, uploadBatch); } //# sourceMappingURL=upload.routes.js.map