265 lines
11 KiB
JavaScript
265 lines
11 KiB
JavaScript
"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(', ')}`,
|
|
});
|
|
}
|
|
// Extract metadata fields from form data
|
|
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);
|
|
// 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
|
|
logger_1.logger.info(`Uploading video to ${filePath}`);
|
|
await (0, promises_1.pipeline)(data.file, (0, fs_1.createWriteStream)(filePath));
|
|
// 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,
|
|
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,
|
|
},
|
|
});
|
|
// 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
|