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