import { HttpStatusCode } from '@peertube/peertube-models' import { isIdValid } from '@server/helpers/custom-validators/misc.js' import { isVideoFileMimeTypeValid, isVideoImageValid } from '@server/helpers/custom-validators/videos.js' import { LiveVideoModel, LiveVideoStatus } from '@server/models/video/live-video.js' import express from 'express' import { body, param, query } from 'express-validator' import { areValidationErrors } from './shared/utils.js' function ensureAdmin (res: express.Response) { const user = res.locals.oauth?.token?.User return user?.role === 0 } async function checkScheduleConflict (req: express.Request, res: express.Response, next: express.NextFunction, excludeId?: number) { const startTime = new Date(req.body.startTime) const endTime = new Date(req.body.endTime) const conflicts = await LiveVideoModel.listConflicts({ targetCamera: req.body.targetCamera, startTime, endTime, excludeId }) if (conflicts.length !== 0) { return res.fail({ status: HttpStatusCode.CONFLICT_409, message: 'Target camera is already assigned to another live video' }) } return next() } export const listLiveVideosValidator = [ query('status') .optional() .isIn(Object.values(LiveVideoStatus)) .withMessage('Invalid live video status'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return return next() } ] export const getLiveVideoValidator = [ param('id') .custom(isIdValid) .withMessage('Invalid live video id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id)) if (!liveVideo) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Live video not found' }) } ;(res.locals as any).liveVideo = liveVideo return next() } ] export const createLiveVideoValidator = [ body('title') .trim() .isLength({ min: 1, max: 255 }) .withMessage('Live video title must be between 1 and 255 characters'), body('startTime') .isISO8601() .withMessage('Start time must be a valid ISO 8601 date'), body('endTime') .isISO8601() .withMessage('End time must be a valid ISO 8601 date'), body('targetCamera') .trim() .isLength({ min: 1, max: 255 }) .withMessage('Target camera is required'), body('thumbnailfile') .custom((_, { req }) => isVideoImageValid(req.files, 'thumbnailfile')) .withMessage('Should have a valid thumbnail file'), body('videofile') .custom((_, { req }) => isVideoFileMimeTypeValid(req.files)) .withMessage('Should have a valid video file'), body('subtitlefile') .optional() .custom((_, { req }) => { if (!req.files || !req.files['subtitlefile']) return true const file = req.files['subtitlefile'][0] const validMimes = [ 'text/vtt', 'application/x-subrip', 'text/plain' ] return validMimes.includes(file.mimetype) }) .withMessage('Should have a valid subtitle file (vtt or srt)'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return if (!ensureAdmin(res)) { return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Only admins can manage live videos' }) } const startTime = new Date(req.body.startTime) const endTime = new Date(req.body.endTime) if (endTime <= startTime) { return res.fail({ status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, message: 'End time must be after start time' }) } return checkScheduleConflict(req, res, next) } ] export const updateLiveVideoValidator = [ param('id') .custom(isIdValid) .withMessage('Invalid live video id'), body('title') .optional() .trim() .isLength({ min: 1, max: 255 }) .withMessage('Live video title must be between 1 and 255 characters'), body('startTime') .optional() .isISO8601() .withMessage('Start time must be a valid ISO 8601 date'), body('endTime') .optional() .isISO8601() .withMessage('End time must be a valid ISO 8601 date'), body('targetCamera') .optional() .trim() .isLength({ min: 1, max: 255 }) .withMessage('Target camera must be between 1 and 255 characters'), body('status') .optional() .isIn(Object.values(LiveVideoStatus)) .withMessage('Invalid live video status'), body('thumbnailfile') .optional() .custom((_, { req }) => isVideoImageValid(req.files, 'thumbnailfile')) .withMessage('Should have a valid thumbnail file'), body('videofile') .optional() .custom((_, { req }) => isVideoFileMimeTypeValid(req.files)) .withMessage('Should have a valid video file'), body('subtitlefile') .optional() .custom((_, { req }) => { if (!req.files || !req.files['subtitlefile']) return true const file = req.files['subtitlefile'][0] const validMimes = [ 'text/vtt', 'application/x-subrip', 'text/plain' ] return validMimes.includes(file.mimetype) }) .withMessage('Should have a valid subtitle file (vtt or srt)'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return if (!ensureAdmin(res)) { return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Only admins can manage live videos' }) } const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id)) if (!liveVideo) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Live video not found' }) } const startTime = req.body.startTime ? new Date(req.body.startTime) : liveVideo.startTime const endTime = req.body.endTime ? new Date(req.body.endTime) : liveVideo.endTime if (endTime <= startTime) { return res.fail({ status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, message: 'End time must be after start time' }) } ;(res.locals as any).liveVideo = liveVideo if (req.body.startTime || req.body.endTime || req.body.targetCamera) { req.body.startTime = startTime.toISOString() req.body.endTime = endTime.toISOString() req.body.targetCamera = req.body.targetCamera ?? liveVideo.targetCamera return checkScheduleConflict(req, res, next, liveVideo.id) } return next() } ] export const deleteLiveVideoValidator = [ param('id') .custom(isIdValid) .withMessage('Invalid live video id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return if (!ensureAdmin(res)) { return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Only admins can manage live videos' }) } const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id)) if (!liveVideo) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, message: 'Live video not found' }) } ;(res.locals as any).liveVideo = liveVideo return next() } ]