"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.videosRoutes = videosRoutes; const database_1 = require("../../../config/database"); const auth_1 = require("../middleware/auth"); const zod_1 = require("zod"); const promises_1 = require("fs/promises"); const thumbnail_service_1 = require("../services/thumbnail.service"); const logger_1 = require("../../../utils/logger"); async function videosRoutes(fastify) { // List videos (admin only) fastify.get('/', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const limit = parseInt(request.query.limit || '50'); const offset = parseInt(request.query.offset || '0'); const search = request.query.search; const orientation = request.query.orientation; const producers = request.query.producers?.split(',').filter(Boolean); const isShort = request.query.isShort; // Build Prisma WHERE clause const where = {}; if (isShort !== undefined) { where.isShort = isShort === 'true'; } if (search) { where.title = { contains: search, mode: 'insensitive', }; } if (orientation) { where.orientation = orientation; } if (producers && producers.length > 0) { where.producer = { in: producers, }; } const videos = await database_1.prisma.video.findMany({ where, select: { id: true, title: true, filename: true, durationSeconds: true, fileSize: true, width: true, height: true, orientation: true, producer: true, thumbnailPath: true, createdAt: true, isPublished: true, publishedAt: true, scheduledPublishAt: true, scheduledUnpublishAt: true, category: true, isShort: true, accessLevel: true, }, orderBy: { createdAt: 'desc', }, take: limit, skip: offset, }); // Get total count const total = await database_1.prisma.video.count({ where }); // Map videos to include thumbnailUrl const videosWithThumbnails = videos.map((video) => ({ ...video, duration: video.durationSeconds ?? 0, // Add duration alias for frontend thumbnailUrl: video.thumbnailPath ? `/media/videos/${video.id}/thumbnail` : null, })); return { videos: videosWithThumbnails, total, limit, offset, }; }); // Get single video (admin only for now) fastify.get('/:id', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videoId = parseInt(request.params.id); const video = await database_1.prisma.video.findUnique({ where: { id: videoId }, }); if (!video) { return reply.code(404).send({ message: 'Video not found' }); } return { video: { ...video, duration: video.durationSeconds ?? 0, thumbnailUrl: video.thumbnailPath ? `/media/videos/${video.id}/thumbnail` : null, }, }; }); // Get list of producers (admin only) fastify.get('/producers', { preHandler: auth_1.requireAdminRole, }, async (request, reply) => { const videos = await database_1.prisma.video.findMany({ where: { producer: { not: null }, }, select: { producer: true, }, distinct: ['producer'], }); return videos.map((v) => v.producer).filter(Boolean); }); // Health check for videos routes fastify.get('/health', async (request, reply) => { // Test database connection by counting videos const count = await database_1.prisma.video.count(); return { status: 'ok', videosCount: count, }; }); // ======================================================================== // PUBLISHING ROUTES (replaces copy-to-public) // ======================================================================== // Zod schemas for publishing const PublishSchema = zod_1.z.object({ category: zod_1.z.enum(['videos', 'curated', 'compilations', 'playback', 'highlights']), }); const BulkPublishSchema = zod_1.z.object({ videoIds: zod_1.z.array(zod_1.z.number().int().positive()).min(1).max(100), category: zod_1.z.enum(['videos', 'curated', 'compilations', 'playback', 'highlights']), }); const BulkUnpublishSchema = zod_1.z.object({ videoIds: zod_1.z.array(zod_1.z.number().int().positive()).min(1).max(100), }); // POST /videos/:id/publish - Publish single video fastify.post('/:id/publish', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const videoId = parseInt(request.params.id); const parseResult = PublishSchema.safeParse(request.body); if (!parseResult.success) { return reply.code(400).send({ message: 'Invalid category', errors: parseResult.error.errors }); } const { category } = parseResult.data; try { const video = await database_1.prisma.video.update({ where: { id: videoId }, data: { isPublished: true, publishedAt: new Date(), category, }, }); logger_1.logger.info(`Video ${videoId} published to ${category}`); return { success: true, video }; } catch (error) { logger_1.logger.error(`Error publishing video ${videoId}:`, error); return reply.code(500).send({ message: 'Failed to publish video', error: error.message }); } }); // POST /videos/:id/unpublish - Unpublish single video fastify.post('/:id/unpublish', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const videoId = parseInt(request.params.id); try { const video = await database_1.prisma.video.update({ where: { id: videoId }, data: { isPublished: false, publishedAt: null, // Keep category for re-publishing }, }); logger_1.logger.info(`Video ${videoId} unpublished`); return { success: true, video }; } catch (error) { logger_1.logger.error(`Error unpublishing video ${videoId}:`, error); return reply.code(500).send({ message: 'Failed to unpublish video', error: error.message }); } }); // POST /videos/bulk-publish - Publish multiple videos fastify.post('/bulk-publish', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const parseResult = BulkPublishSchema.safeParse(request.body); if (!parseResult.success) { return reply.code(400).send({ message: 'Invalid request', errors: parseResult.error.errors }); } const { videoIds, category } = parseResult.data; try { const result = await database_1.prisma.video.updateMany({ where: { id: { in: videoIds } }, data: { isPublished: true, publishedAt: new Date(), category, }, }); logger_1.logger.info(`Bulk published ${result.count} videos to ${category}`); return { success: true, count: result.count }; } catch (error) { logger_1.logger.error(`Error bulk publishing videos:`, error); return reply.code(500).send({ message: 'Failed to publish videos', error: error.message }); } }); // POST /videos/bulk-unpublish - Unpublish multiple videos fastify.post('/bulk-unpublish', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const parseResult = BulkUnpublishSchema.safeParse(request.body); if (!parseResult.success) { return reply.code(400).send({ message: 'Invalid request', errors: parseResult.error.errors }); } const { videoIds } = parseResult.data; try { const result = await database_1.prisma.video.updateMany({ where: { id: { in: videoIds } }, data: { isPublished: false, publishedAt: null, }, }); logger_1.logger.info(`Bulk unpublished ${result.count} videos`); return { success: true, count: result.count }; } catch (error) { logger_1.logger.error(`Error bulk unpublishing videos:`, error); return reply.code(500).send({ message: 'Failed to unpublish videos', error: error.message }); } }); // POST /videos/:id/lock - Lock published video fastify.post('/:id/lock', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const videoId = parseInt(request.params.id); const userId = request.user?.id; try { const video = await database_1.prisma.video.update({ where: { id: videoId }, data: { isLocked: true, lockedAt: new Date(), lockedById: userId, }, }); logger_1.logger.info(`Video ${videoId} locked by user ${userId}`); return { success: true, video }; } catch (error) { logger_1.logger.error(`Error locking video ${videoId}:`, error); return reply.code(500).send({ message: 'Failed to lock video', error: error.message }); } }); // POST /videos/:id/unlock - Unlock published video fastify.post('/:id/unlock', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { const videoId = parseInt(request.params.id); try { const video = await database_1.prisma.video.update({ where: { id: videoId }, data: { isLocked: false, lockedAt: null, lockedById: null, }, }); logger_1.logger.info(`Video ${videoId} unlocked`); return { success: true, video }; } catch (error) { logger_1.logger.error(`Error unlocking video ${videoId}:`, error); return reply.code(500).send({ message: 'Failed to unlock video', error: error.message }); } }); // ======================================================================== // THUMBNAIL GENERATION ROUTES // ======================================================================== // POST /videos/:id/generate-thumbnail - Generate thumbnail for single video fastify.post('/:id/generate-thumbnail', { 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' }); } // Check if video file exists try { await (0, promises_1.access)(video.path); } catch { return reply.code(400).send({ message: 'Video file not found on disk' }); } // Generate thumbnail const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({ videoPath: video.path, videoId: video.id, duration: video.durationSeconds ?? 0, orientation: video.orientation ?? '', }); // Update video with thumbnail path const updatedVideo = await database_1.prisma.video.update({ where: { id: videoId }, data: { thumbnailPath }, }); logger_1.logger.info(`Thumbnail generated for video ${videoId}`); return { success: true, video: updatedVideo }; } catch (error) { logger_1.logger.error(`Error generating thumbnail for video ${videoId}:`, error); return reply.code(500).send({ message: 'Failed to generate thumbnail', error: error.message }); } }); // POST /videos/bulk-generate-thumbnails - Generate thumbnails for all videos without them fastify.post('/bulk-generate-thumbnails', { preHandler: auth_1.requireAdminRole }, async (request, reply) => { try { // Find all videos without thumbnails const videos = await database_1.prisma.video.findMany({ where: { thumbnailPath: null, isValid: true, }, select: { id: true, path: true, durationSeconds: true, orientation: true, }, }); logger_1.logger.info(`Found ${videos.length} videos without thumbnails`); const results = { total: videos.length, succeeded: 0, failed: 0, errors: [], }; for (const video of videos) { try { // Check if video file exists await (0, promises_1.access)(video.path); // Generate thumbnail const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({ videoPath: video.path, videoId: video.id, duration: video.durationSeconds ?? 0, orientation: video.orientation ?? '', }); // Update video with thumbnail path await database_1.prisma.video.update({ where: { id: video.id }, data: { thumbnailPath }, }); results.succeeded++; logger_1.logger.info(`Thumbnail generated for video ${video.id}`); } catch (error) { results.failed++; results.errors.push({ videoId: video.id, error: error.message, }); logger_1.logger.error(`Failed to generate thumbnail for video ${video.id}:`, error); } } return { success: true, results, }; } catch (error) { logger_1.logger.error('Error bulk generating thumbnails:', error); return reply.code(500).send({ message: 'Failed to bulk generate thumbnails', error: error.message }); } }); } //# sourceMappingURL=videos.routes.js.map