From c729caf6cc34630877a0e5a1bda1719384cd0c8a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 11 Feb 2022 10:51:33 +0100 Subject: Add basic video editor support --- server/middlewares/validators/config.ts | 14 +++ server/middlewares/validators/shared/utils.ts | 1 + server/middlewares/validators/shared/videos.ts | 26 ++++- server/middlewares/validators/videos/index.ts | 1 + .../validators/videos/video-captions.ts | 10 +- .../validators/videos/video-comments.ts | 14 +-- .../middlewares/validators/videos/video-editor.ts | 112 +++++++++++++++++++++ .../validators/videos/video-ownership-changes.ts | 21 +--- .../validators/videos/video-playlists.ts | 4 +- server/middlewares/validators/videos/videos.ts | 40 +++----- 10 files changed, 177 insertions(+), 66 deletions(-) create mode 100644 server/middlewares/validators/videos/video-editor.ts (limited to 'server/middlewares') diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 8b14feb3c..e87b2e39d 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -57,6 +57,8 @@ const customConfigUpdateValidator = [ body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'), + body('videoEditor.enabled').isBoolean().withMessage('Should have a valid video editor enabled boolean'), + body('import.videos.concurrency').isInt({ min: 0 }).withMessage('Should have a valid import concurrency number'), body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), @@ -104,6 +106,7 @@ const customConfigUpdateValidator = [ if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return if (!checkInvalidTranscodingConfig(req.body, res)) return if (!checkInvalidLiveConfig(req.body, res)) return + if (!checkInvalidVideoEditorConfig(req.body, res)) return return next() } @@ -159,3 +162,14 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon return true } + +function checkInvalidVideoEditorConfig (customConfig: CustomConfig, res: express.Response) { + if (customConfig.videoEditor.enabled === false) return true + + if (customConfig.videoEditor.enabled === true && customConfig.transcoding.enabled === false) { + res.fail({ message: 'You cannot enable video editor if transcoding is not enabled' }) + return false + } + + return true +} diff --git a/server/middlewares/validators/shared/utils.ts b/server/middlewares/validators/shared/utils.ts index 104eace91..410de4d80 100644 --- a/server/middlewares/validators/shared/utils.ts +++ b/server/middlewares/validators/shared/utils.ts @@ -8,6 +8,7 @@ function areValidationErrors (req: express.Request, res: express.Response) { if (!errors.isEmpty()) { logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) + res.fail({ message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '), instance: req.originalUrl, diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index fc978b63a..8807435f6 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express' import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' +import { isAbleToUploadVideo } from '@server/lib/user' import { authenticatePromiseIfNeeded } from '@server/middlewares/auth' import { VideoModel } from '@server/models/video/video' import { VideoChannelModel } from '@server/models/video/video-channel' @@ -7,6 +8,7 @@ import { VideoFileModel } from '@server/models/video/video-file' import { MUser, MUserAccountId, + MUserId, MVideo, MVideoAccountLight, MVideoFormattableDetails, @@ -16,7 +18,7 @@ import { MVideoThumbnail, MVideoWithRights } from '@server/types/models' -import { HttpStatusCode, UserRight } from '@shared/models' +import { HttpStatusCode, ServerErrorCode, UserRight } from '@shared/models' async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined @@ -108,6 +110,11 @@ async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVid // Only the owner or a user that have blocklist rights can see the video if (!user || !user.canGetVideo(video)) { + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot fetch information of private/internal/blocklisted video' + }) + return false } @@ -139,13 +146,28 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: return true } +async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { + if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { + res.fail({ + status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, + message: 'The user video quota is exceeded with this video.', + type: ServerErrorCode.QUOTA_REACHED + }) + return false + } + + return true +} + // --------------------------------------------------------------------------- export { doesVideoChannelOfAccountExist, doesVideoExist, doesVideoFileOfVideoExist, + checkUserCanManageVideo, checkCanSeeVideoIfPrivate, - checkCanSeePrivateVideo + checkCanSeePrivateVideo, + checkUserQuota } diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index f365d8ee1..faa082510 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts @@ -2,6 +2,7 @@ export * from './video-blacklist' export * from './video-captions' export * from './video-channels' export * from './video-comments' +export * from './video-editor' export * from './video-files' export * from './video-imports' export * from './video-live' diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index a399871e1..441c6b4be 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts @@ -1,6 +1,6 @@ import express from 'express' import { body, param } from 'express-validator' -import { HttpStatusCode, UserRight } from '@shared/models' +import { UserRight } from '@shared/models' import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' import { cleanUpReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' @@ -74,13 +74,7 @@ const listVideoCaptionsValidator = [ if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return const video = res.locals.onlyVideo - - if (!await checkCanSeeVideoIfPrivate(req, res, video)) { - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: 'Cannot list captions of private/internal/blocklisted video' - }) - } + if (!await checkCanSeeVideoIfPrivate(req, res, video)) return return next() } diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 91e85711d..96d956035 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts @@ -54,12 +54,7 @@ const listVideoCommentThreadsValidator = [ if (areValidationErrors(req, res)) return if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return - if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) { - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: 'Cannot list comments of private/internal/blocklisted video' - }) - } + if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return return next() } @@ -78,12 +73,7 @@ const listVideoThreadCommentsValidator = [ if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return - if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) { - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: 'Cannot list threads of private/internal/blocklisted video' - }) - } + if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return return next() } diff --git a/server/middlewares/validators/videos/video-editor.ts b/server/middlewares/validators/videos/video-editor.ts new file mode 100644 index 000000000..9be97be93 --- /dev/null +++ b/server/middlewares/validators/videos/video-editor.ts @@ -0,0 +1,112 @@ +import express from 'express' +import { body, param } from 'express-validator' +import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc' +import { + isEditorCutTaskValid, + isEditorTaskAddIntroOutroValid, + isEditorTaskAddWatermarkValid, + isValidEditorTasksArray +} from '@server/helpers/custom-validators/video-editor' +import { cleanUpReqFiles } from '@server/helpers/express-utils' +import { CONFIG } from '@server/initializers/config' +import { approximateIntroOutroAdditionalSize, getTaskFile } from '@server/lib/video-editor' +import { isAudioFile } from '@shared/extra-utils' +import { HttpStatusCode, UserRight, VideoEditorCreateEdition, VideoEditorTask, VideoState } from '@shared/models' +import { logger } from '../../../helpers/logger' +import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared' + +const videosEditorAddEditionValidator = [ + param('videoId').custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), + + body('tasks').custom(isValidEditorTasksArray).withMessage('Should have a valid array of tasks'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosEditorAddEditionValidator parameters.', { parameters: req.params, body: req.body, files: req.files }) + + if (CONFIG.VIDEO_EDITOR.ENABLED !== true) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Video editor is disabled on this instance' + }) + + return cleanUpReqFiles(req) + } + + if (areValidationErrors(req, res)) return cleanUpReqFiles(req) + + const body: VideoEditorCreateEdition = req.body + const files = req.files as Express.Multer.File[] + + for (let i = 0; i < body.tasks.length; i++) { + const task = body.tasks[i] + + if (!checkTask(req, task, i)) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: `Task ${task.name} is invalid` + }) + + return cleanUpReqFiles(req) + } + + if (task.name === 'add-intro' || task.name === 'add-outro') { + const filePath = getTaskFile(files, i).path + + // Our concat filter needs a video stream + if (await isAudioFile(filePath)) { + res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: `Task ${task.name} is invalid: file does not contain a video stream` + }) + + return cleanUpReqFiles(req) + } + } + } + + if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req) + + const video = res.locals.videoAll + if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) { + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Cannot edit video that is already waiting for transcoding/edition' + }) + + return cleanUpReqFiles(req) + } + + const user = res.locals.oauth.token.User + if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) + + // Try to make an approximation of bytes added by the intro/outro + const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFile(files, i).path) + if (await checkUserQuota(user, additionalBytes, res) === false) return cleanUpReqFiles(req) + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + videosEditorAddEditionValidator +} + +// --------------------------------------------------------------------------- + +const taskCheckers: { + [id in VideoEditorTask['name']]: (task: VideoEditorTask, indice?: number, files?: Express.Multer.File[]) => boolean +} = { + 'cut': isEditorCutTaskValid, + 'add-intro': isEditorTaskAddIntroOutroValid, + 'add-outro': isEditorTaskAddIntroOutroValid, + 'add-watermark': isEditorTaskAddWatermarkValid +} + +function checkTask (req: express.Request, task: VideoEditorTask, indice?: number) { + const checker = taskCheckers[task.name] + if (!checker) return false + + return checker(task, indice, req.files as Express.Multer.File[]) +} diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts index 95e4cebce..6dcdc05f5 100644 --- a/server/middlewares/validators/videos/video-ownership-changes.ts +++ b/server/middlewares/validators/videos/video-ownership-changes.ts @@ -3,20 +3,13 @@ import { param } from 'express-validator' import { isIdValid } from '@server/helpers/custom-validators/misc' import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership' import { logger } from '@server/helpers/logger' -import { isAbleToUploadVideo } from '@server/lib/user' import { AccountModel } from '@server/models/account/account' import { MVideoWithAllFiles } from '@server/types/models' -import { - HttpStatusCode, - ServerErrorCode, - UserRight, - VideoChangeOwnershipAccept, - VideoChangeOwnershipStatus, - VideoState -} from '@shared/models' +import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models' import { areValidationErrors, checkUserCanManageVideo, + checkUserQuota, doesChangeVideoOwnershipExist, doesVideoChannelOfAccountExist, doesVideoExist, @@ -113,15 +106,7 @@ async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response) const user = res.locals.oauth.token.User - if (!await isAbleToUploadVideo(user.id, video.getMaxQualityFile().size)) { - res.fail({ - status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, - message: 'The user video quota is exceeded with this video.', - type: ServerErrorCode.QUOTA_REACHED - }) - - return false - } + if (!await checkUserQuota(user, video.getMaxQualityFile().size, res)) return false return true } diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index f5fee845e..241b9ed7b 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts @@ -27,7 +27,7 @@ import { isVideoPlaylistTimestampValid, isVideoPlaylistTypeValid } from '../../../helpers/custom-validators/video-playlists' -import { isVideoImage } from '../../../helpers/custom-validators/videos' +import { isVideoImageValid } from '../../../helpers/custom-validators/videos' import { cleanUpReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' @@ -390,7 +390,7 @@ export { function getCommonPlaylistEditAttributes () { return [ body('thumbnailfile') - .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')) + .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')) .withMessage( 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ') diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index b3ffb7007..26597cf7b 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -3,7 +3,6 @@ import { body, header, param, query, ValidationChain } from 'express-validator' import { isTestInstance } from '@server/helpers/core-utils' import { getResumableUploadPath } from '@server/helpers/upload' import { Redis } from '@server/lib/redis' -import { isAbleToUploadVideo } from '@server/lib/user' import { getServerActor } from '@server/models/application/application' import { ExpressPromiseHandler } from '@server/types/express-handler' import { MUserAccountId, MVideoFullLight } from '@server/types/models' @@ -13,7 +12,7 @@ import { exists, isBooleanValid, isDateValid, - isFileFieldValid, + isFileValid, isIdValid, isUUIDValid, toArray, @@ -23,24 +22,24 @@ import { } from '../../../helpers/custom-validators/misc' import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' import { + areVideoTagsValid, isScheduleVideoUpdatePrivacyValid, isVideoCategoryValid, isVideoDescriptionValid, isVideoFileMimeTypeValid, isVideoFileSizeValid, isVideoFilterValid, - isVideoImage, + isVideoImageValid, isVideoIncludeValid, isVideoLanguageValid, isVideoLicenceValid, isVideoNameValid, isVideoOriginallyPublishedAtValid, isVideoPrivacyValid, - isVideoSupportValid, - isVideoTagsValid + isVideoSupportValid } from '../../../helpers/custom-validators/videos' import { cleanUpReqFiles } from '../../../helpers/express-utils' -import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' +import { getVideoStreamDuration } from '../../../helpers/ffmpeg' import { logger } from '../../../helpers/logger' import { deleteFileAndCatch } from '../../../helpers/utils' import { getVideoWithAttributes } from '../../../helpers/video' @@ -53,6 +52,7 @@ import { areValidationErrors, checkCanSeePrivateVideo, checkUserCanManageVideo, + checkUserQuota, doesVideoChannelOfAccountExist, doesVideoExist, doesVideoFileOfVideoExist, @@ -61,7 +61,7 @@ import { const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ body('videofile') - .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) + .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null })) .withMessage('Should have a file'), body('name') .trim() @@ -299,12 +299,11 @@ const videosCustomGetValidator = ( // Video private or blacklisted if (video.requiresAuth()) { - if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) return next() + if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) { + return next() + } - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: 'Cannot get this private/internal or blocklisted video' - }) + return } // Video is public, anyone can access it @@ -375,12 +374,12 @@ const videosOverviewValidator = [ function getCommonVideoEditAttributes () { return [ body('thumbnailfile') - .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( + .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage( 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') ), body('previewfile') - .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( + .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage( 'This preview file is not supported or too large. Please, make sure it is of the following type: ' + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') ), @@ -420,7 +419,7 @@ function getCommonVideoEditAttributes () { body('tags') .optional() .customSanitizer(toValueOrNull) - .custom(isVideoTagsValid) + .custom(areVideoTagsValid) .withMessage( `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` + `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each` @@ -612,14 +611,7 @@ async function commonVideoChecksPass (parameters: { return false } - if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { - res.fail({ - status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, - message: 'The user video quota is exceeded with this video.', - type: ServerErrorCode.QUOTA_REACHED - }) - return false - } + if (await checkUserQuota(user, videoFileSize, res) === false) return false return true } @@ -654,7 +646,7 @@ export async function isVideoAccepted ( } async function addDurationToVideo (videoFile: { path: string, duration?: number }) { - const duration: number = await getDurationFromVideoFile(videoFile.path) + const duration: number = await getVideoStreamDuration(videoFile.path) if (isNaN(duration)) throw new Error(`Couldn't get video duration`) -- cgit v1.2.3