]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
Support uploads of videos with unknown duration
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import express from 'express'
2 import { body, header, param, query, ValidationChain } from 'express-validator'
3 import { isTestInstance } from '@server/helpers/core-utils'
4 import { getResumableUploadPath } from '@server/helpers/upload'
5 import { Redis } from '@server/lib/redis'
6 import { getServerActor } from '@server/models/application/application'
7 import { ExpressPromiseHandler } from '@server/types/express-handler'
8 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
9 import { getAllPrivacies } from '@shared/core-utils'
10 import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models'
11 import {
12 exists,
13 isBooleanValid,
14 isDateValid,
15 isFileValid,
16 isIdValid,
17 isUUIDValid,
18 toArray,
19 toBooleanOrNull,
20 toIntOrNull,
21 toValueOrNull
22 } from '../../../helpers/custom-validators/misc'
23 import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
24 import {
25 areVideoTagsValid,
26 isScheduleVideoUpdatePrivacyValid,
27 isVideoCategoryValid,
28 isVideoDescriptionValid,
29 isVideoFileMimeTypeValid,
30 isVideoFileSizeValid,
31 isVideoFilterValid,
32 isVideoImageValid,
33 isVideoIncludeValid,
34 isVideoLanguageValid,
35 isVideoLicenceValid,
36 isVideoNameValid,
37 isVideoOriginallyPublishedAtValid,
38 isVideoPrivacyValid,
39 isVideoSupportValid
40 } from '../../../helpers/custom-validators/videos'
41 import { cleanUpReqFiles } from '../../../helpers/express-utils'
42 import { getVideoStreamDuration } from '../../../helpers/ffmpeg'
43 import { logger } from '../../../helpers/logger'
44 import { deleteFileAndCatch } from '../../../helpers/utils'
45 import { getVideoWithAttributes } from '../../../helpers/video'
46 import { CONFIG } from '../../../initializers/config'
47 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
48 import { isLocalVideoAccepted } from '../../../lib/moderation'
49 import { Hooks } from '../../../lib/plugins/hooks'
50 import { VideoModel } from '../../../models/video/video'
51 import {
52 areValidationErrors,
53 checkCanSeePrivateVideo,
54 checkUserCanManageVideo,
55 checkUserQuota,
56 doesVideoChannelOfAccountExist,
57 doesVideoExist,
58 doesVideoFileOfVideoExist,
59 isValidVideoIdParam
60 } from '../shared'
61
62 const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
63 body('videofile')
64 .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
65 .withMessage('Should have a file'),
66 body('name')
67 .trim()
68 .custom(isVideoNameValid).withMessage(
69 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
70 ),
71 body('channelId')
72 .customSanitizer(toIntOrNull)
73 .custom(isIdValid).withMessage('Should have correct video channel id'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
77
78 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
79
80 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
81 const user = res.locals.oauth.token.User
82
83 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
84 return cleanUpReqFiles(req)
85 }
86
87 try {
88 if (!videoFile.duration) await addDurationToVideo(videoFile)
89 } catch (err) {
90 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
91
92 res.fail({
93 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
94 message: 'Video file unreadable.'
95 })
96 return cleanUpReqFiles(req)
97 }
98
99 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
100
101 return next()
102 }
103 ])
104
105 /**
106 * Gets called after the last PUT request
107 */
108 const videosAddResumableValidator = [
109 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 const user = res.locals.oauth.token.User
111 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
112 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename }
113 const cleanup = () => deleteFileAndCatch(file.path)
114
115 const uploadId = req.query.upload_id
116 const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)
117
118 if (sessionExists) {
119 const sessionResponse = await Redis.Instance.getUploadSession(uploadId)
120
121 if (!sessionResponse) {
122 res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion
123
124 return res.fail({
125 status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
126 message: 'The upload is already being processed'
127 })
128 }
129
130 if (isTestInstance()) {
131 res.setHeader('x-resumable-upload-cached', 'true')
132 }
133
134 return res.json(sessionResponse)
135 }
136
137 await Redis.Instance.setUploadSession(uploadId)
138
139 if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
140
141 try {
142 if (!file.duration) await addDurationToVideo(file)
143 } catch (err) {
144 logger.error('Invalid input file in videosAddResumableValidator.', { err })
145
146 res.fail({
147 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
148 message: 'Video file unreadable.'
149 })
150 return cleanup()
151 }
152
153 if (!await isVideoAccepted(req, res, file)) return cleanup()
154
155 res.locals.videoFileResumable = file
156
157 return next()
158 }
159 ]
160
161 /**
162 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
163 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
164 *
165 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
166 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
167 *
168 */
169 const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
170 body('filename')
171 .isString()
172 .exists()
173 .withMessage('Should have a valid filename'),
174 body('name')
175 .trim()
176 .custom(isVideoNameValid).withMessage(
177 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
178 ),
179 body('channelId')
180 .customSanitizer(toIntOrNull)
181 .custom(isIdValid).withMessage('Should have correct video channel id'),
182
183 header('x-upload-content-length')
184 .isNumeric()
185 .exists()
186 .withMessage('Should specify the file length'),
187 header('x-upload-content-type')
188 .isString()
189 .exists()
190 .withMessage('Should specify the file mimetype'),
191
192 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
193 const videoFileMetadata = {
194 mimetype: req.headers['x-upload-content-type'] as string,
195 size: +req.headers['x-upload-content-length'],
196 originalname: req.body.filename
197 }
198
199 const user = res.locals.oauth.token.User
200 const cleanup = () => cleanUpReqFiles(req)
201
202 logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
203 parameters: req.body,
204 headers: req.headers,
205 files: req.files
206 })
207
208 if (areValidationErrors(req, res)) return cleanup()
209
210 const files = { videofile: [ videoFileMetadata ] }
211 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
212
213 // multer required unsetting the Content-Type, now we can set it for node-uploadx
214 req.headers['content-type'] = 'application/json; charset=utf-8'
215 // place previewfile in metadata so that uploadx saves it in .META
216 if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile']
217
218 return next()
219 }
220 ])
221
222 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
223 isValidVideoIdParam('id'),
224
225 body('name')
226 .optional()
227 .trim()
228 .custom(isVideoNameValid).withMessage(
229 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
230 ),
231 body('channelId')
232 .optional()
233 .customSanitizer(toIntOrNull)
234 .custom(isIdValid).withMessage('Should have correct video channel id'),
235
236 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
237 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
238
239 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
240 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
241 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
242
243 // Check if the user who did the request is able to update the video
244 const user = res.locals.oauth.token.User
245 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
246
247 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
248
249 return next()
250 }
251 ])
252
253 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
254 const video = getVideoWithAttributes(res)
255
256 // Anybody can watch local videos
257 if (video.isOwned() === true) return next()
258
259 // Logged user
260 if (res.locals.oauth) {
261 // Users can search or watch remote videos
262 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
263 }
264
265 // Anybody can search or watch remote videos
266 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
267
268 // Check our instance follows an actor that shared this video
269 const serverActor = await getServerActor()
270 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
271
272 return res.fail({
273 status: HttpStatusCode.FORBIDDEN_403,
274 message: 'Cannot get this video regarding follow constraints',
275 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
276 data: {
277 originUrl: video.url
278 }
279 })
280 }
281
282 const videosCustomGetValidator = (
283 fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes',
284 authenticateInQuery = false
285 ) => {
286 return [
287 isValidVideoIdParam('id'),
288
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 logger.debug('Checking videosGet parameters', { parameters: req.params })
291
292 if (areValidationErrors(req, res)) return
293 if (!await doesVideoExist(req.params.id, res, fetchType)) return
294
295 // Controllers does not need to check video rights
296 if (fetchType === 'only-immutable-attributes') return next()
297
298 const video = getVideoWithAttributes(res) as MVideoFullLight
299
300 // Video private or blacklisted
301 if (video.requiresAuth()) {
302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) {
303 return next()
304 }
305
306 return
307 }
308
309 // Video is public, anyone can access it
310 if (video.privacy === VideoPrivacy.PUBLIC) return next()
311
312 // Video is unlisted, check we used the uuid to fetch it
313 if (video.privacy === VideoPrivacy.UNLISTED) {
314 if (isUUIDValid(req.params.id)) return next()
315
316 // Don't leak this unlisted video
317 return res.fail({
318 status: HttpStatusCode.NOT_FOUND_404,
319 message: 'Video not found'
320 })
321 }
322 }
323 ]
324 }
325
326 const videosGetValidator = videosCustomGetValidator('all')
327 const videosDownloadValidator = videosCustomGetValidator('all', true)
328
329 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
330 isValidVideoIdParam('id'),
331
332 param('videoFileId')
333 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
334
335 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
336 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
337
338 if (areValidationErrors(req, res)) return
339 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
340
341 return next()
342 }
343 ])
344
345 const videosRemoveValidator = [
346 isValidVideoIdParam('id'),
347
348 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
349 logger.debug('Checking videosRemove parameters', { parameters: req.params })
350
351 if (areValidationErrors(req, res)) return
352 if (!await doesVideoExist(req.params.id, res)) return
353
354 // Check if the user who did the request is able to delete the video
355 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
356
357 return next()
358 }
359 ]
360
361 const videosOverviewValidator = [
362 query('page')
363 .optional()
364 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
365 .withMessage('Should have a valid pagination'),
366
367 (req: express.Request, res: express.Response, next: express.NextFunction) => {
368 if (areValidationErrors(req, res)) return
369
370 return next()
371 }
372 ]
373
374 function getCommonVideoEditAttributes () {
375 return [
376 body('thumbnailfile')
377 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
378 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
379 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
380 ),
381 body('previewfile')
382 .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage(
383 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
384 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
385 ),
386
387 body('category')
388 .optional()
389 .customSanitizer(toIntOrNull)
390 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
391 body('licence')
392 .optional()
393 .customSanitizer(toIntOrNull)
394 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
395 body('language')
396 .optional()
397 .customSanitizer(toValueOrNull)
398 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
399 body('nsfw')
400 .optional()
401 .customSanitizer(toBooleanOrNull)
402 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
403 body('waitTranscoding')
404 .optional()
405 .customSanitizer(toBooleanOrNull)
406 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
407 body('privacy')
408 .optional()
409 .customSanitizer(toValueOrNull)
410 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
411 body('description')
412 .optional()
413 .customSanitizer(toValueOrNull)
414 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
415 body('support')
416 .optional()
417 .customSanitizer(toValueOrNull)
418 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
419 body('tags')
420 .optional()
421 .customSanitizer(toValueOrNull)
422 .custom(areVideoTagsValid)
423 .withMessage(
424 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
425 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
426 ),
427 body('commentsEnabled')
428 .optional()
429 .customSanitizer(toBooleanOrNull)
430 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
431 body('downloadEnabled')
432 .optional()
433 .customSanitizer(toBooleanOrNull)
434 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
435 body('originallyPublishedAt')
436 .optional()
437 .customSanitizer(toValueOrNull)
438 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
439 body('scheduleUpdate')
440 .optional()
441 .customSanitizer(toValueOrNull),
442 body('scheduleUpdate.updateAt')
443 .optional()
444 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
445 body('scheduleUpdate.privacy')
446 .optional()
447 .customSanitizer(toIntOrNull)
448 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
449 ] as (ValidationChain | ExpressPromiseHandler)[]
450 }
451
452 const commonVideosFiltersValidator = [
453 query('categoryOneOf')
454 .optional()
455 .customSanitizer(toArray)
456 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
457 query('licenceOneOf')
458 .optional()
459 .customSanitizer(toArray)
460 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
461 query('languageOneOf')
462 .optional()
463 .customSanitizer(toArray)
464 .custom(isStringArray).withMessage('Should have a valid one of language array'),
465 query('privacyOneOf')
466 .optional()
467 .customSanitizer(toArray)
468 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
469 query('tagsOneOf')
470 .optional()
471 .customSanitizer(toArray)
472 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
473 query('tagsAllOf')
474 .optional()
475 .customSanitizer(toArray)
476 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
477 query('nsfw')
478 .optional()
479 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
480 query('isLive')
481 .optional()
482 .customSanitizer(toBooleanOrNull)
483 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
484 query('filter')
485 .optional()
486 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
487 query('include')
488 .optional()
489 .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
490 query('isLocal')
491 .optional()
492 .customSanitizer(toBooleanOrNull)
493 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
494 query('hasHLSFiles')
495 .optional()
496 .customSanitizer(toBooleanOrNull)
497 .custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
498 query('hasWebtorrentFiles')
499 .optional()
500 .customSanitizer(toBooleanOrNull)
501 .custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
502 query('skipCount')
503 .optional()
504 .customSanitizer(toBooleanOrNull)
505 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
506 query('search')
507 .optional()
508 .custom(exists).withMessage('Should have a valid search'),
509
510 (req: express.Request, res: express.Response, next: express.NextFunction) => {
511 logger.debug('Checking commons video filters query', { parameters: req.query })
512
513 if (areValidationErrors(req, res)) return
514
515 // FIXME: deprecated in 4.0, to remove
516 {
517 if (req.query.filter === 'all-local') {
518 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
519 req.query.isLocal = true
520 req.query.privacyOneOf = getAllPrivacies()
521 } else if (req.query.filter === 'all') {
522 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
523 req.query.privacyOneOf = getAllPrivacies()
524 } else if (req.query.filter === 'local') {
525 req.query.isLocal = true
526 }
527
528 req.query.filter = undefined
529 }
530
531 const user = res.locals.oauth?.token.User
532
533 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
534 if (req.query.include || req.query.privacyOneOf) {
535 return res.fail({
536 status: HttpStatusCode.UNAUTHORIZED_401,
537 message: 'You are not allowed to see all videos.'
538 })
539 }
540 }
541
542 return next()
543 }
544 ]
545
546 // ---------------------------------------------------------------------------
547
548 export {
549 videosAddLegacyValidator,
550 videosAddResumableValidator,
551 videosAddResumableInitValidator,
552
553 videosUpdateValidator,
554 videosGetValidator,
555 videoFileMetadataGetValidator,
556 videosDownloadValidator,
557 checkVideoFollowConstraints,
558 videosCustomGetValidator,
559 videosRemoveValidator,
560
561 getCommonVideoEditAttributes,
562
563 commonVideosFiltersValidator,
564
565 videosOverviewValidator
566 }
567
568 // ---------------------------------------------------------------------------
569
570 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
571 if (req.body.scheduleUpdate) {
572 if (!req.body.scheduleUpdate.updateAt) {
573 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
574
575 res.fail({ message: 'Schedule update at is mandatory.' })
576 return true
577 }
578 }
579
580 return false
581 }
582
583 async function commonVideoChecksPass (parameters: {
584 req: express.Request
585 res: express.Response
586 user: MUserAccountId
587 videoFileSize: number
588 files: express.UploadFilesForCheck
589 }): Promise<boolean> {
590 const { req, res, user, videoFileSize, files } = parameters
591
592 if (areErrorsInScheduleUpdate(req, res)) return false
593
594 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
595
596 if (!isVideoFileMimeTypeValid(files)) {
597 res.fail({
598 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
599 message: 'This file is not supported. Please, make sure it is of the following type: ' +
600 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
601 })
602 return false
603 }
604
605 if (!isVideoFileSizeValid(videoFileSize.toString())) {
606 res.fail({
607 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
608 message: 'This file is too large. It exceeds the maximum file size authorized.',
609 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
610 })
611 return false
612 }
613
614 if (await checkUserQuota(user, videoFileSize, res) === false) return false
615
616 return true
617 }
618
619 export async function isVideoAccepted (
620 req: express.Request,
621 res: express.Response,
622 videoFile: express.VideoUploadFile
623 ) {
624 // Check we accept this video
625 const acceptParameters = {
626 videoBody: req.body,
627 videoFile,
628 user: res.locals.oauth.token.User
629 }
630 const acceptedResult = await Hooks.wrapFun(
631 isLocalVideoAccepted,
632 acceptParameters,
633 'filter:api.video.upload.accept.result'
634 )
635
636 if (!acceptedResult || acceptedResult.accepted !== true) {
637 logger.info('Refused local video.', { acceptedResult, acceptParameters })
638 res.fail({
639 status: HttpStatusCode.FORBIDDEN_403,
640 message: acceptedResult.errorMessage || 'Refused local video'
641 })
642 return false
643 }
644
645 return true
646 }
647
648 async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
649 const duration = await getVideoStreamDuration(videoFile.path)
650
651 // FFmpeg may not be able to guess video duration
652 // For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
653 if (isNaN(duration)) videoFile.duration = 0
654 else videoFile.duration = duration
655 }