248 lines
7.2 KiB
TypeScript
248 lines
7.2 KiB
TypeScript
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()
|
|
}
|
|
]
|