1 import express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { ExpressPromiseHandler } from '@server/types/express'
4 import { MUserAccountId } from '@server/types/models'
5 import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
6 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
7 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
8 import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
18 } from '../../../helpers/custom-validators/misc'
20 isVideoPlaylistDescriptionValid,
21 isVideoPlaylistNameValid,
22 isVideoPlaylistPrivacyValid,
23 isVideoPlaylistTimestampValid,
24 isVideoPlaylistTypeValid
25 } from '../../../helpers/custom-validators/video-playlists'
26 import { isVideoImage } from '../../../helpers/custom-validators/videos'
27 import { cleanUpReqFiles } from '../../../helpers/express-utils'
28 import { logger } from '../../../helpers/logger'
29 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30 import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
31 import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
32 import { authenticatePromiseIfNeeded } from '../../auth'
35 doesVideoChannelIdExist,
37 doesVideoPlaylistExist,
38 isValidPlaylistIdParam,
39 VideoPlaylistFetchType
42 const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
44 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
46 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
47 logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body })
49 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
51 const body: VideoPlaylistCreate = req.body
52 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
55 !body.videoChannelId &&
56 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
60 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
67 const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
68 isValidPlaylistIdParam('playlistId'),
72 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
74 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
75 logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body })
77 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
79 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
81 const videoPlaylist = getPlaylist(res)
83 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
84 return cleanUpReqFiles(req)
87 const body: VideoPlaylistUpdate = req.body
89 const newPrivacy = body.privacy || videoPlaylist.privacy
90 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
92 (!videoPlaylist.videoChannelId && !body.videoChannelId) ||
93 body.videoChannelId === null
98 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
101 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
104 return res.fail({ message: 'Cannot update a watch later playlist.' })
107 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
113 const videoPlaylistsDeleteValidator = [
114 isValidPlaylistIdParam('playlistId'),
116 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
117 logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params })
119 if (areValidationErrors(req, res)) return
121 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
123 const videoPlaylist = getPlaylist(res)
124 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
125 return res.fail({ message: 'Cannot delete a watch later playlist.' })
128 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
136 const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
138 isValidPlaylistIdParam('playlistId'),
140 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
141 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
143 if (areValidationErrors(req, res)) return
145 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
147 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
149 // Video is unlisted, check we used the uuid to fetch it
150 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
151 if (isUUIDValid(req.params.playlistId)) return next()
154 status: HttpStatusCode.NOT_FOUND_404,
155 message: 'Playlist not found'
159 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
160 await authenticatePromiseIfNeeded(req, res)
162 const user = res.locals.oauth ? res.locals.oauth.token.User : null
166 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
169 status: HttpStatusCode.FORBIDDEN_403,
170 message: 'Cannot get this private video playlist.'
182 const videoPlaylistsSearchValidator = [
183 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
185 (req: express.Request, res: express.Response, next: express.NextFunction) => {
186 logger.debug('Checking videoPlaylists search query', { parameters: req.query })
188 if (areValidationErrors(req, res)) return
194 const videoPlaylistsAddVideoValidator = [
195 isValidPlaylistIdParam('playlistId'),
198 .customSanitizer(toCompleteUUID)
199 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
200 body('startTimestamp')
202 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
203 body('stopTimestamp')
205 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
207 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
208 logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
210 if (areValidationErrors(req, res)) return
212 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
213 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
215 const videoPlaylist = getPlaylist(res)
217 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
225 const videoPlaylistsUpdateOrRemoveVideoValidator = [
226 isValidPlaylistIdParam('playlistId'),
227 param('playlistElementId')
228 .customSanitizer(toCompleteUUID)
229 .custom(isIdValid).withMessage('Should have an element id/uuid'),
230 body('startTimestamp')
232 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
233 body('stopTimestamp')
235 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
237 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
238 logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
240 if (areValidationErrors(req, res)) return
242 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
244 const videoPlaylist = getPlaylist(res)
246 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
247 if (!videoPlaylistElement) {
249 status: HttpStatusCode.NOT_FOUND_404,
250 message: 'Video playlist element not found'
254 res.locals.videoPlaylistElement = videoPlaylistElement
256 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
262 const videoPlaylistElementAPGetValidator = [
263 isValidPlaylistIdParam('playlistId'),
264 param('playlistElementId')
265 .custom(isIdValid).withMessage('Should have an playlist element id'),
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 logger.debug('Checking videoPlaylistElementAPGetValidator parameters', { parameters: req.params })
270 if (areValidationErrors(req, res)) return
272 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
273 const playlistId = req.params.playlistId
275 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
276 if (!videoPlaylistElement) {
278 status: HttpStatusCode.NOT_FOUND_404,
279 message: 'Video playlist element not found'
284 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
286 status: HttpStatusCode.FORBIDDEN_403,
287 message: 'Cannot get this private video playlist.'
291 res.locals.videoPlaylistElementAP = videoPlaylistElement
297 const videoPlaylistsReorderVideosValidator = [
298 isValidPlaylistIdParam('playlistId'),
299 body('startPosition')
300 .isInt({ min: 1 }).withMessage('Should have a valid start position'),
301 body('insertAfterPosition')
302 .isInt({ min: 0 }).withMessage('Should have a valid insert after position'),
303 body('reorderLength')
305 .isInt({ min: 1 }).withMessage('Should have a valid range length'),
307 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
308 logger.debug('Checking videoPlaylistsReorderVideosValidator parameters', { parameters: req.params })
310 if (areValidationErrors(req, res)) return
312 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
314 const videoPlaylist = getPlaylist(res)
315 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
317 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
318 const startPosition: number = req.body.startPosition
319 const insertAfterPosition: number = req.body.insertAfterPosition
320 const reorderLength: number = req.body.reorderLength
322 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
323 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
327 if (reorderLength && reorderLength + startPosition > nextPosition) {
328 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
336 const commonVideoPlaylistFiltersValidator = [
337 query('playlistType')
339 .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
341 (req: express.Request, res: express.Response, next: express.NextFunction) => {
342 logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
344 if (areValidationErrors(req, res)) return
350 const doVideosInPlaylistExistValidator = [
352 .customSanitizer(toIntArray)
353 .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
355 (req: express.Request, res: express.Response, next: express.NextFunction) => {
356 logger.debug('Checking areVideosInPlaylistExistValidator parameters', { parameters: req.query })
358 if (areValidationErrors(req, res)) return
364 // ---------------------------------------------------------------------------
367 videoPlaylistsAddValidator,
368 videoPlaylistsUpdateValidator,
369 videoPlaylistsDeleteValidator,
370 videoPlaylistsGetValidator,
371 videoPlaylistsSearchValidator,
373 videoPlaylistsAddVideoValidator,
374 videoPlaylistsUpdateOrRemoveVideoValidator,
375 videoPlaylistsReorderVideosValidator,
377 videoPlaylistElementAPGetValidator,
379 commonVideoPlaylistFiltersValidator,
381 doVideosInPlaylistExistValidator
384 // ---------------------------------------------------------------------------
386 function getCommonPlaylistEditAttributes () {
388 body('thumbnailfile')
389 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile'))
391 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
392 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
397 .customSanitizer(toValueOrNull)
398 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
401 .customSanitizer(toIntOrNull)
402 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
403 body('videoChannelId')
405 .customSanitizer(toIntOrNull)
406 ] as (ValidationChain | ExpressPromiseHandler)[]
409 function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
410 if (videoPlaylist.isOwned() === false) {
412 status: HttpStatusCode.FORBIDDEN_403,
413 message: 'Cannot manage video playlist of another server.'
418 // Check if the user can manage the video playlist
419 // The user can delete it if s/he is an admin
420 // Or if s/he is the video playlist's owner
421 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
423 status: HttpStatusCode.FORBIDDEN_403,
424 message: 'Cannot manage video playlist of another user'
432 function getPlaylist (res: express.Response) {
433 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary