"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.videoActionsRoutes = videoActionsRoutes; const database_1 = require("../../../config/database"); const auth_1 = require("../middleware/auth"); const video_analytics_service_1 = require("../services/video-analytics.service"); const logger_1 = require("../../../utils/logger"); const jsonwebtoken_1 = require("jsonwebtoken"); const env_1 = require("../../../config/env"); async function videoActionsRoutes(fastify) { /** * POST /videos/:id/duplicate * Duplicate a video with a new title */ fastify.post('/:id/duplicate', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); const { title } = request.body || {}; try { const originalVideo = await database_1.prisma.video.findUnique({ where: { id: videoId }, }); if (!originalVideo) { return reply.code(404).send({ message: 'Video not found' }); } // Create duplicate with new title const duplicateTitle = title || `${originalVideo.title || originalVideo.filename} (Copy)`; const duplicate = await database_1.prisma.video.create({ data: { path: originalVideo.path, // Same file path filename: originalVideo.filename, producer: originalVideo.producer, creator: originalVideo.creator, title: duplicateTitle, durationSeconds: originalVideo.durationSeconds, quality: originalVideo.quality, orientation: originalVideo.orientation, hasAudio: originalVideo.hasAudio, fileSize: originalVideo.fileSize, fileHash: originalVideo.fileHash, width: originalVideo.width, height: originalVideo.height, thumbnailPath: originalVideo.thumbnailPath, tags: originalVideo.tags, directoryType: originalVideo.directoryType, category: originalVideo.category, uploaderId: originalVideo.uploaderId, }, }); logger_1.logger.info(`Duplicated video ${videoId} to ${duplicate.id}`, { originalTitle: originalVideo.title, newTitle: duplicateTitle }); return { success: true, duplicate: { id: duplicate.id, title: duplicate.title, }, }; } catch (error) { logger_1.logger.error('Failed to duplicate video', { error, videoId }); return reply.code(500).send({ message: 'Failed to duplicate video' }); } }); /** * POST /videos/:id/replace * Replace video file while keeping metadata and URL * Note: This endpoint accepts a new file path - actual file upload should go through upload routes */ fastify.post('/:id/replace', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); const { newPath, newFilename, durationSeconds, width, height, fileSize } = request.body; try { const existingVideo = await database_1.prisma.video.findUnique({ where: { id: videoId }, }); if (!existingVideo) { return reply.code(404).send({ message: 'Video not found' }); } // Update video with new file details const updatedVideo = await database_1.prisma.video.update({ where: { id: videoId }, data: { path: newPath, filename: newFilename, originalPath: existingVideo.path, // Save old path for reference originalFilename: existingVideo.filename, durationSeconds: durationSeconds || existingVideo.durationSeconds, width: width || existingVideo.width, height: height || existingVideo.height, fileSize: fileSize ? BigInt(fileSize) : existingVideo.fileSize, lastValidated: new Date(), thumbnailPath: null, // Clear thumbnail, will be regenerated }, }); logger_1.logger.info(`Replaced video file for ${videoId}`, { oldPath: existingVideo.path, newPath }); return { success: true, video: { id: updatedVideo.id, title: updatedVideo.title, filename: updatedVideo.filename, }, }; } catch (error) { logger_1.logger.error('Failed to replace video', { error, videoId }); return reply.code(500).send({ message: 'Failed to replace video' }); } }); /** * GET /videos/:id/analytics * Get detailed analytics for a video */ fastify.get('/:id/analytics', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); const { startDate, endDate } = request.query; try { const analytics = await video_analytics_service_1.videoAnalyticsService.getVideoAnalytics(videoId, startDate ? new Date(startDate) : undefined, endDate ? new Date(endDate) : undefined); return analytics; } catch (error) { logger_1.logger.error('Failed to get video analytics', { error, videoId }); if (error instanceof Error && error.message === 'Video not found') { return reply.code(404).send({ message: 'Video not found' }); } return reply.code(500).send({ message: 'Failed to fetch analytics' }); } }); /** * POST /videos/:id/reset-analytics * Reset all analytics for a video */ fastify.post('/:id/reset-analytics', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); try { await video_analytics_service_1.videoAnalyticsService.resetAnalytics(videoId); return { success: true, message: 'Analytics reset successfully', }; } catch (error) { logger_1.logger.error('Failed to reset analytics', { error, videoId }); return reply.code(500).send({ message: 'Failed to reset analytics' }); } }); /** * GET /videos/:id/preview-link * Generate a temporary preview link with expiring JWT token */ fastify.get('/:id/preview-link', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); try { const video = await database_1.prisma.video.findUnique({ where: { id: videoId }, }); if (!video) { return reply.code(404).send({ message: 'Video not found' }); } // Generate JWT token that expires in 24 hours const expiryHours = parseInt(process.env.VIDEO_PREVIEW_LINK_EXPIRY_HOURS || '24'); const token = (0, jsonwebtoken_1.sign)({ videoId, purpose: 'preview', }, env_1.env.JWT_ACCESS_SECRET, { expiresIn: `${expiryHours}h` }); const previewUrl = `${env_1.env.MEDIA_API_URL}/api/videos/${videoId}/preview?token=${token}`; logger_1.logger.info(`Generated preview link for video ${videoId}`, { expiresInHours: expiryHours }); return { previewUrl, expiresAt: new Date(Date.now() + expiryHours * 60 * 60 * 1000).toISOString(), expiryHours, }; } catch (error) { logger_1.logger.error('Failed to generate preview link', { error, videoId }); return reply.code(500).send({ message: 'Failed to generate preview link' }); } }); /** * GET /videos/analytics/top * Get top performing videos */ fastify.get('/analytics/top', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const metric = request.query.metric || 'views'; const limit = parseInt(request.query.limit || '10'); try { const topVideos = await video_analytics_service_1.videoAnalyticsService.getTopVideos(metric, limit); return { metric, videos: topVideos, }; } catch (error) { logger_1.logger.error('Failed to get top videos', { error, metric }); return reply.code(500).send({ message: 'Failed to fetch top videos' }); } }); /** * GET /videos/analytics/overview * Get global analytics overview across all videos */ fastify.get('/analytics/overview', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { try { const [totalVideos, totalViews, totalWatchTime, avgCompletionRate] = await Promise.all([ database_1.prisma.video.count(), database_1.prisma.video.aggregate({ _sum: { viewCount: true, }, }), database_1.prisma.video.aggregate({ _sum: { totalWatchTimeSeconds: true, }, }), database_1.prisma.video.aggregate({ _avg: { completionRate: true, }, }), ]); return { totalVideos, totalViews: totalViews._sum.viewCount || 0, totalWatchTimeSeconds: totalWatchTime._sum.totalWatchTimeSeconds || 0, averageCompletionRate: avgCompletionRate._avg.completionRate?.toNumber() || 0, }; } catch (error) { logger_1.logger.error('Failed to get analytics overview', { error }); return reply.code(500).send({ message: 'Failed to fetch analytics overview' }); } }); } //# sourceMappingURL=video-actions.routes.js.map