248 lines
10 KiB
JavaScript
248 lines
10 KiB
JavaScript
"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
|