aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/activitypub/client.ts5
-rw-r--r--server/controllers/api/accounts.ts5
-rw-r--r--server/controllers/api/users/index.ts6
-rw-r--r--server/controllers/api/video-channel.ts7
-rw-r--r--server/controllers/api/video-playlist.ts25
-rw-r--r--server/helpers/custom-validators/activitypub/playlist.ts6
-rw-r--r--server/helpers/custom-validators/video-playlists.ts17
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/initializers/installer.ts4
-rw-r--r--server/lib/activitypub/playlist.ts4
-rw-r--r--server/lib/activitypub/process/process-delete.ts24
-rw-r--r--server/lib/activitypub/send/send-create.ts2
-rw-r--r--server/lib/activitypub/send/send-delete.ts7
-rw-r--r--server/lib/activitypub/send/send-update.ts4
-rw-r--r--server/lib/user.ts9
-rw-r--r--server/lib/video-playlist.ts29
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts48
-rw-r--r--server/models/video/video-channel.ts6
-rw-r--r--server/models/video/video-playlist-element.ts10
-rw-r--r--server/models/video/video-playlist.ts92
-rw-r--r--server/models/video/video-share.ts2
-rw-r--r--server/models/video/video.ts38
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/video-playlists.ts818
-rw-r--r--server/tests/api/videos/video-playlists.ts728
-rw-r--r--server/tests/fixtures/thumbnail-playlist.jpgbin0 -> 2520 bytes
-rw-r--r--shared/models/activitypub/objects/playlist-object.ts3
-rw-r--r--shared/models/videos/playlist/video-playlist-type.model.ts4
-rw-r--r--shared/models/videos/playlist/video-playlist.model.ts3
-rw-r--r--shared/utils/requests/requests.ts2
-rw-r--r--shared/utils/server/servers.ts4
-rw-r--r--shared/utils/users/users.ts9
-rw-r--r--shared/utils/videos/video-channels.ts18
-rw-r--r--shared/utils/videos/video-playlists.ts117
-rw-r--r--shared/utils/videos/videos.ts29
35 files changed, 1411 insertions, 682 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 59e6c8e9f..f616047b0 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -320,7 +320,10 @@ async function videoRedundancyController (req: express.Request, res: express.Res
320async function videoPlaylistController (req: express.Request, res: express.Response) { 320async function videoPlaylistController (req: express.Request, res: express.Response) {
321 const playlist: VideoPlaylistModel = res.locals.videoPlaylist 321 const playlist: VideoPlaylistModel = res.locals.videoPlaylist
322 322
323 const json = await playlist.toActivityPubObject() 323 // We need more attributes
324 playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
325
326 const json = await playlist.toActivityPubObject(req.query.page, null)
324 const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 327 const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
325 const object = audiencify(json, audience) 328 const object = audiencify(json, audience)
326 329
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 03c831092..e24545de8 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -18,6 +18,7 @@ import { JobQueue } from '../../lib/job-queue'
18import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
19import { VideoPlaylistModel } from '../../models/video/video-playlist' 19import { VideoPlaylistModel } from '../../models/video/video-playlist'
20import { UserModel } from '../../models/account/user' 20import { UserModel } from '../../models/account/user'
21import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
21 22
22const accountsRouter = express.Router() 23const accountsRouter = express.Router()
23 24
@@ -57,6 +58,7 @@ accountsRouter.get('/:accountName/video-playlists',
57 videoPlaylistsSortValidator, 58 videoPlaylistsSortValidator,
58 setDefaultSort, 59 setDefaultSort,
59 setDefaultPagination, 60 setDefaultPagination,
61 commonVideoPlaylistFiltersValidator,
60 asyncMiddleware(listAccountPlaylists) 62 asyncMiddleware(listAccountPlaylists)
61) 63)
62 64
@@ -106,7 +108,8 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
106 count: req.query.count, 108 count: req.query.count,
107 sort: req.query.sort, 109 sort: req.query.sort,
108 accountId: res.locals.account.id, 110 accountId: res.locals.account.id,
109 privateAndUnlisted 111 privateAndUnlisted,
112 type: req.query.playlistType
110 }) 113 })
111 114
112 return res.json(getFormattedObjects(resultList.data, resultList.total)) 115 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 407f32ac0..5758c8227 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
6import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' 6import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
9import { createUserAccountAndChannel } from '../../../lib/user' 9import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
10import { 10import {
11 asyncMiddleware, 11 asyncMiddleware,
12 asyncRetryTransactionMiddleware, 12 asyncRetryTransactionMiddleware,
@@ -174,7 +174,7 @@ async function createUser (req: express.Request, res: express.Response) {
174 videoQuotaDaily: body.videoQuotaDaily 174 videoQuotaDaily: body.videoQuotaDaily
175 }) 175 })
176 176
177 const { user, account } = await createUserAccountAndChannel(userToCreate) 177 const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate)
178 178
179 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 179 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
180 logger.info('User %s with its channel and account created.', body.username) 180 logger.info('User %s with its channel and account created.', body.username)
@@ -205,7 +205,7 @@ async function registerUser (req: express.Request, res: express.Response) {
205 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null 205 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
206 }) 206 })
207 207
208 const { user } = await createUserAccountAndChannel(userToCreate) 208 const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate)
209 209
210 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) 210 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
211 logger.info('User %s with its channel and account registered.', body.username) 211 logger.info('User %s with its channel and account registered.', body.username)
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 534cc8d7b..c13aed4dc 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -33,6 +33,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
33import { UserModel } from '../../models/account/user' 33import { UserModel } from '../../models/account/user'
34import { JobQueue } from '../../lib/job-queue' 34import { JobQueue } from '../../lib/job-queue'
35import { VideoPlaylistModel } from '../../models/video/video-playlist' 35import { VideoPlaylistModel } from '../../models/video/video-playlist'
36import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
36 37
37const auditLogger = auditLoggerFactory('channels') 38const auditLogger = auditLoggerFactory('channels')
38const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 39const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@@ -85,6 +86,7 @@ videoChannelRouter.get('/:nameWithHost/video-playlists',
85 videoPlaylistsSortValidator, 86 videoPlaylistsSortValidator,
86 setDefaultSort, 87 setDefaultSort,
87 setDefaultPagination, 88 setDefaultPagination,
89 commonVideoPlaylistFiltersValidator,
88 asyncMiddleware(listVideoChannelPlaylists) 90 asyncMiddleware(listVideoChannelPlaylists)
89) 91)
90 92
@@ -197,6 +199,8 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
197 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 199 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
198 200
199 await sequelizeTypescript.transaction(async t => { 201 await sequelizeTypescript.transaction(async t => {
202 await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
203
200 await videoChannelInstance.destroy({ transaction: t }) 204 await videoChannelInstance.destroy({ transaction: t })
201 205
202 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())) 206 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
@@ -225,7 +229,8 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res
225 start: req.query.start, 229 start: req.query.start,
226 count: req.query.count, 230 count: req.query.count,
227 sort: req.query.sort, 231 sort: req.query.sort,
228 videoChannelId: res.locals.videoChannel.id 232 videoChannelId: res.locals.videoChannel.id,
233 type: req.query.playlistType
229 }) 234 })
230 235
231 return res.json(getFormattedObjects(resultList.data, resultList.total)) 236 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index e026b4d16..8605f3dfc 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -17,6 +17,7 @@ import { logger } from '../../helpers/logger'
17import { resetSequelizeInstance } from '../../helpers/database-utils' 17import { resetSequelizeInstance } from '../../helpers/database-utils'
18import { VideoPlaylistModel } from '../../models/video/video-playlist' 18import { VideoPlaylistModel } from '../../models/video/video-playlist'
19import { 19import {
20 commonVideoPlaylistFiltersValidator,
20 videoPlaylistsAddValidator, 21 videoPlaylistsAddValidator,
21 videoPlaylistsAddVideoValidator, 22 videoPlaylistsAddVideoValidator,
22 videoPlaylistsDeleteValidator, 23 videoPlaylistsDeleteValidator,
@@ -45,6 +46,7 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
45import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' 46import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
46import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' 47import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
47import { copy, pathExists } from 'fs-extra' 48import { copy, pathExists } from 'fs-extra'
49import { AccountModel } from '../../models/account/account'
48 50
49const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) 51const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
50 52
@@ -55,6 +57,7 @@ videoPlaylistRouter.get('/',
55 videoPlaylistsSortValidator, 57 videoPlaylistsSortValidator,
56 setDefaultSort, 58 setDefaultSort,
57 setDefaultPagination, 59 setDefaultPagination,
60 commonVideoPlaylistFiltersValidator,
58 asyncMiddleware(listVideoPlaylists) 61 asyncMiddleware(listVideoPlaylists)
59) 62)
60 63
@@ -130,7 +133,8 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
130 followerActorId: serverActor.id, 133 followerActorId: serverActor.id,
131 start: req.query.start, 134 start: req.query.start,
132 count: req.query.count, 135 count: req.query.count,
133 sort: req.query.sort 136 sort: req.query.sort,
137 type: req.query.type
134 }) 138 })
135 139
136 return res.json(getFormattedObjects(resultList.data, resultList.total)) 140 return res.json(getFormattedObjects(resultList.data, resultList.total))
@@ -171,7 +175,8 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
171 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { 175 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
172 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) 176 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
173 177
174 videoPlaylistCreated.OwnerAccount = user.Account 178 // We need more attributes for the federation
179 videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
175 await sendCreateVideoPlaylist(videoPlaylistCreated, t) 180 await sendCreateVideoPlaylist(videoPlaylistCreated, t)
176 181
177 return videoPlaylistCreated 182 return videoPlaylistCreated
@@ -216,6 +221,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
216 const videoChannel = res.locals.videoChannel as VideoChannelModel 221 const videoChannel = res.locals.videoChannel as VideoChannelModel
217 222
218 videoPlaylistInstance.videoChannelId = videoChannel.id 223 videoPlaylistInstance.videoChannelId = videoChannel.id
224 videoPlaylistInstance.VideoChannel = videoChannel
219 } 225 }
220 } 226 }
221 227
@@ -227,6 +233,8 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
227 } 233 }
228 234
229 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) 235 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
236 // We need more attributes for the federation
237 playlistUpdated.OwnerAccount = await AccountModel.load(playlistUpdated.OwnerAccount.id, t)
230 238
231 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE 239 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
232 240
@@ -290,11 +298,15 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
290 const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()) 298 const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
291 299
292 if (await pathExists(playlistThumbnailPath) === false) { 300 if (await pathExists(playlistThumbnailPath) === false) {
301 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
302
293 const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) 303 const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
294 await copy(videoThumbnailPath, playlistThumbnailPath) 304 await copy(videoThumbnailPath, playlistThumbnailPath)
295 } 305 }
296 } 306 }
297 307
308 // We need more attributes for the federation
309 videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
298 await sendUpdateVideoPlaylist(videoPlaylist, t) 310 await sendUpdateVideoPlaylist(videoPlaylist, t)
299 311
300 return playlistElement 312 return playlistElement
@@ -320,6 +332,8 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
320 332
321 const element = await videoPlaylistElement.save({ transaction: t }) 333 const element = await videoPlaylistElement.save({ transaction: t })
322 334
335 // We need more attributes for the federation
336 videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
323 await sendUpdateVideoPlaylist(videoPlaylist, t) 337 await sendUpdateVideoPlaylist(videoPlaylist, t)
324 338
325 return element 339 return element
@@ -341,6 +355,8 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
341 // Decrease position of the next elements 355 // Decrease position of the next elements
342 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t) 356 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t)
343 357
358 // We need more attributes for the federation
359 videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
344 await sendUpdateVideoPlaylist(videoPlaylist, t) 360 await sendUpdateVideoPlaylist(videoPlaylist, t)
345 361
346 logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) 362 logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
@@ -382,6 +398,8 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
382 // Decrease positions of elements after the old position of our ordered elements (decrease) 398 // Decrease positions of elements after the old position of our ordered elements (decrease)
383 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t) 399 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t)
384 400
401 // We need more attributes for the federation
402 videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
385 await sendUpdateVideoPlaylist(videoPlaylist, t) 403 await sendUpdateVideoPlaylist(videoPlaylist, t)
386 }) 404 })
387 405
@@ -415,5 +433,6 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon
415 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 433 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
416 }) 434 })
417 435
418 return res.json(getFormattedObjects(resultList.data, resultList.total)) 436 const additionalAttributes = { playlistInfo: true }
437 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
419} 438}
diff --git a/server/helpers/custom-validators/activitypub/playlist.ts b/server/helpers/custom-validators/activitypub/playlist.ts
index ecdc7975e..6c7bdb193 100644
--- a/server/helpers/custom-validators/activitypub/playlist.ts
+++ b/server/helpers/custom-validators/activitypub/playlist.ts
@@ -1,4 +1,4 @@
1import { exists } from '../misc' 1import { exists, isDateValid } from '../misc'
2import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 2import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object' 4import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object'
@@ -7,7 +7,9 @@ import { isActivityPubUrlValid } from './misc'
7function isPlaylistObjectValid (object: PlaylistObject) { 7function isPlaylistObjectValid (object: PlaylistObject) {
8 return exists(object) && 8 return exists(object) &&
9 object.type === 'Playlist' && 9 object.type === 'Playlist' &&
10 validator.isInt(object.totalItems + '') 10 validator.isInt(object.totalItems + '') &&
11 isDateValid(object.published) &&
12 isDateValid(object.updated)
11} 13}
12 14
13function isPlaylistElementObjectValid (object: PlaylistElementObject) { 15function isPlaylistElementObjectValid (object: PlaylistElementObject) {
diff --git a/server/helpers/custom-validators/video-playlists.ts b/server/helpers/custom-validators/video-playlists.ts
index 0f5af4ec0..c217a39bf 100644
--- a/server/helpers/custom-validators/video-playlists.ts
+++ b/server/helpers/custom-validators/video-playlists.ts
@@ -1,9 +1,8 @@
1import { exists } from './misc' 1import { exists } from './misc'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers' 3import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers'
4import * as express from 'express' 4import * as express from 'express'
5import { VideoPlaylistModel } from '../../models/video/video-playlist' 5import { VideoPlaylistModel } from '../../models/video/video-playlist'
6import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
7 6
8const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS 7const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
9 8
@@ -19,8 +18,16 @@ function isVideoPlaylistPrivacyValid (value: number) {
19 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined 18 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined
20} 19}
21 20
21function isVideoPlaylistTimestampValid (value: any) {
22 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
23}
24
25function isVideoPlaylistTypeValid (value: any) {
26 return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined
27}
28
22async function isVideoPlaylistExist (id: number | string, res: express.Response) { 29async function isVideoPlaylistExist (id: number | string, res: express.Response) {
23 const videoPlaylist = await VideoPlaylistModel.load(id, undefined) 30 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
24 31
25 if (!videoPlaylist) { 32 if (!videoPlaylist) {
26 res.status(404) 33 res.status(404)
@@ -40,5 +47,7 @@ export {
40 isVideoPlaylistExist, 47 isVideoPlaylistExist,
41 isVideoPlaylistNameValid, 48 isVideoPlaylistNameValid,
42 isVideoPlaylistDescriptionValid, 49 isVideoPlaylistDescriptionValid,
43 isVideoPlaylistPrivacyValid 50 isVideoPlaylistPrivacyValid,
51 isVideoPlaylistTimestampValid,
52 isVideoPlaylistTypeValid
44} 53}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 154a9cffe..59c30fdee 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -11,6 +11,7 @@ import { invert } from 'lodash'
11import { CronRepeatOptions, EveryRepeatOptions } from 'bull' 11import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
12import * as bytes from 'bytes' 12import * as bytes from 'bytes'
13import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' 13import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
14import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
14 15
15// Use a variable to reload the configuration if we need 16// Use a variable to reload the configuration if we need
16let config: IConfig = require('config') 17let config: IConfig = require('config')
@@ -522,6 +523,11 @@ const VIDEO_PLAYLIST_PRIVACIES = {
522 [VideoPlaylistPrivacy.PRIVATE]: 'Private' 523 [VideoPlaylistPrivacy.PRIVATE]: 'Private'
523} 524}
524 525
526const VIDEO_PLAYLIST_TYPES = {
527 [VideoPlaylistType.REGULAR]: 'Regular',
528 [VideoPlaylistType.WATCH_LATER]: 'Watch later'
529}
530
525const MIMETYPES = { 531const MIMETYPES = {
526 VIDEO: { 532 VIDEO: {
527 MIMETYPE_EXT: buildVideoMimetypeExt(), 533 MIMETYPE_EXT: buildVideoMimetypeExt(),
@@ -778,6 +784,7 @@ export {
778 STATIC_MAX_AGE, 784 STATIC_MAX_AGE,
779 STATIC_PATHS, 785 STATIC_PATHS,
780 VIDEO_IMPORT_TIMEOUT, 786 VIDEO_IMPORT_TIMEOUT,
787 VIDEO_PLAYLIST_TYPES,
781 ACTIVITY_PUB, 788 ACTIVITY_PUB,
782 ACTIVITY_PUB_ACTOR_TYPES, 789 ACTIVITY_PUB_ACTOR_TYPES,
783 THUMBNAILS_SIZE, 790 THUMBNAILS_SIZE,
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 2b22e16fe..c669606f8 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -1,7 +1,7 @@
1import * as passwordGenerator from 'password-generator' 1import * as passwordGenerator from 'password-generator'
2import { UserRole } from '../../shared' 2import { UserRole } from '../../shared'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { createApplicationActor, createUserAccountAndChannel } from '../lib/user' 4import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { ApplicationModel } from '../models/application/application' 6import { ApplicationModel } from '../models/application/application'
7import { OAuthClientModel } from '../models/oauth/oauth-client' 7import { OAuthClientModel } from '../models/oauth/oauth-client'
@@ -141,7 +141,7 @@ async function createOAuthAdminIfNotExist () {
141 } 141 }
142 const user = new UserModel(userData) 142 const user = new UserModel(userData)
143 143
144 await createUserAccountAndChannel(user, validatePassword) 144 await createUserAccountAndChannelAndPlaylist(user, validatePassword)
145 logger.info('Username: ' + username) 145 logger.info('Username: ' + username)
146 logger.info('User password: ' + password) 146 logger.info('User password: ' + password)
147} 147}
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index c9b428c92..70389044e 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -28,7 +28,9 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
28 url: playlistObject.id, 28 url: playlistObject.id,
29 uuid: playlistObject.uuid, 29 uuid: playlistObject.uuid,
30 ownerAccountId: byAccount.id, 30 ownerAccountId: byAccount.id,
31 videoChannelId: null 31 videoChannelId: null,
32 createdAt: new Date(playlistObject.published),
33 updatedAt: new Date(playlistObject.updated)
32 } 34 }
33} 35}
34 36
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 155d2ffcc..76f07fd8a 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -8,6 +8,7 @@ import { VideoModel } from '../../../models/video/video'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { VideoCommentModel } from '../../../models/video/video-comment' 9import { VideoCommentModel } from '../../../models/video/video-comment'
10import { forwardVideoRelatedActivity } from '../send/utils' 10import { forwardVideoRelatedActivity } from '../send/utils'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
11 12
12async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) { 13async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) {
13 const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id 14 const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id
@@ -45,6 +46,15 @@ async function processDeleteActivity (activity: ActivityDelete, byActor: ActorMo
45 } 46 }
46 } 47 }
47 48
49 {
50 const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
51 if (videoPlaylist) {
52 if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
53
54 return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
55 }
56 }
57
48 return undefined 58 return undefined
49} 59}
50 60
@@ -70,6 +80,20 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
70 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) 80 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
71} 81}
72 82
83async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) {
84 logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
85
86 await sequelizeTypescript.transaction(async t => {
87 if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) {
88 throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url)
89 }
90
91 await playlistToDelete.destroy({ transaction: t })
92 })
93
94 logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
95}
96
73async function processDeleteAccount (accountToRemove: AccountModel) { 97async function processDeleteAccount (accountToRemove: AccountModel) {
74 logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid) 98 logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid)
75 99
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index bacdb97e3..28f18595b 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -45,7 +45,7 @@ async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transac
45 const byActor = playlist.OwnerAccount.Actor 45 const byActor = playlist.OwnerAccount.Actor
46 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 46 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
47 47
48 const object = await playlist.toActivityPubObject() 48 const object = await playlist.toActivityPubObject(null, t)
49 const createActivity = buildCreateActivity(playlist.url, byActor, object, audience) 49 const createActivity = buildCreateActivity(playlist.url, byActor, object, audience)
50 50
51 const serverActor = await getServerActor() 51 const serverActor = await getServerActor()
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index 016811e60..7bf5ca520 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -31,7 +31,12 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
31 const url = getDeleteActivityPubUrl(byActor.url) 31 const url = getDeleteActivityPubUrl(byActor.url)
32 const activity = buildDeleteActivity(url, byActor.url, byActor) 32 const activity = buildDeleteActivity(url, byActor.url, byActor)
33 33
34 const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) 34 const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
35
36 // In case the actor did not have any videos
37 const serverActor = await getServerActor()
38 actorsInvolved.push(serverActor)
39
35 actorsInvolved.push(byActor) 40 actorsInvolved.push(byActor)
36 41
37 return broadcastToFollowers(activity, byActor, actorsInvolved, t) 42 return broadcastToFollowers(activity, byActor, actorsInvolved, t)
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 3eb2704fd..7411c08d5 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -52,7 +52,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
52 let actorsInvolved: ActorModel[] 52 let actorsInvolved: ActorModel[]
53 if (accountOrChannel instanceof AccountModel) { 53 if (accountOrChannel instanceof AccountModel) {
54 // Actors that shared my videos are involved too 54 // Actors that shared my videos are involved too
55 actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) 55 actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
56 } else { 56 } else {
57 // Actors that shared videos of my channel are involved too 57 // Actors that shared videos of my channel are involved too
58 actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t) 58 actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t)
@@ -87,7 +87,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Tr
87 87
88 const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString()) 88 const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString())
89 89
90 const object = await videoPlaylist.toActivityPubObject() 90 const object = await videoPlaylist.toActivityPubObject(null, t)
91 const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC) 91 const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC)
92 92
93 const updateActivity = buildUpdateActivity(url, byActor, object, audience) 93 const updateActivity = buildUpdateActivity(url, byActor, object, audience)
diff --git a/server/lib/user.ts b/server/lib/user.ts
index a39ef6c3d..02a84f15b 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -11,8 +11,9 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
11import { ActorModel } from '../models/activitypub/actor' 11import { ActorModel } from '../models/activitypub/actor'
12import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 12import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
13import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 13import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
14import { createWatchLaterPlaylist } from './video-playlist'
14 15
15async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { 16async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
16 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 17 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
17 const userOptions = { 18 const userOptions = {
18 transaction: t, 19 transaction: t,
@@ -38,7 +39,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
38 } 39 }
39 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t) 40 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
40 41
41 return { user: userCreated, account: accountCreated, videoChannel } 42 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
43
44 return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
42 }) 45 })
43 46
44 const [ accountKeys, channelKeys ] = await Promise.all([ 47 const [ accountKeys, channelKeys ] = await Promise.all([
@@ -89,7 +92,7 @@ async function createApplicationActor (applicationId: number) {
89 92
90export { 93export {
91 createApplicationActor, 94 createApplicationActor,
92 createUserAccountAndChannel, 95 createUserAccountAndChannelAndPlaylist,
93 createLocalAccountWithoutKeys 96 createLocalAccountWithoutKeys
94} 97}
95 98
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
new file mode 100644
index 000000000..6e214e60f
--- /dev/null
+++ b/server/lib/video-playlist.ts
@@ -0,0 +1,29 @@
1import * as Sequelize from 'sequelize'
2import { AccountModel } from '../models/account/account'
3import { VideoPlaylistModel } from '../models/video/video-playlist'
4import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
5import { getVideoPlaylistActivityPubUrl } from './activitypub'
6import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
7
8async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) {
9 const videoPlaylist = new VideoPlaylistModel({
10 name: 'Watch later',
11 privacy: VideoPlaylistPrivacy.PRIVATE,
12 type: VideoPlaylistType.WATCH_LATER,
13 ownerAccountId: account.id
14 })
15
16 videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
17
18 await videoPlaylist.save({ transaction: t })
19
20 videoPlaylist.OwnerAccount = account
21
22 return videoPlaylist
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 createWatchLaterPlaylist
29}
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 0e97c8dc0..796c63748 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, ValidationChain } from 'express-validator/check' 2import { body, param, query, ValidationChain } from 'express-validator/check'
3import { UserRight, VideoPrivacy } from '../../../../shared' 3import { UserRight } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { UserModel } from '../../../models/account/user' 5import { UserModel } from '../../../models/account/user'
6import { areValidationErrors } from '../utils' 6import { areValidationErrors } from '../utils'
@@ -11,7 +11,9 @@ import {
11 isVideoPlaylistDescriptionValid, 11 isVideoPlaylistDescriptionValid,
12 isVideoPlaylistExist, 12 isVideoPlaylistExist,
13 isVideoPlaylistNameValid, 13 isVideoPlaylistNameValid,
14 isVideoPlaylistPrivacyValid 14 isVideoPlaylistPrivacyValid,
15 isVideoPlaylistTimestampValid,
16 isVideoPlaylistTypeValid
15} from '../../../helpers/custom-validators/video-playlists' 17} from '../../../helpers/custom-validators/video-playlists'
16import { VideoPlaylistModel } from '../../../models/video/video-playlist' 18import { VideoPlaylistModel } from '../../../models/video/video-playlist'
17import { cleanUpReqFiles } from '../../../helpers/express-utils' 19import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -20,6 +22,7 @@ import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-
20import { VideoModel } from '../../../models/video/video' 22import { VideoModel } from '../../../models/video/video'
21import { authenticatePromiseIfNeeded } from '../../oauth' 23import { authenticatePromiseIfNeeded } from '../../oauth'
22import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 24import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
25import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
23 26
24const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 27const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
25 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 28 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -56,6 +59,12 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
56 .json({ error: 'Cannot set "private" a video playlist that was not private.' }) 59 .json({ error: 'Cannot set "private" a video playlist that was not private.' })
57 } 60 }
58 61
62 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
63 cleanUpReqFiles(req)
64 return res.status(409)
65 .json({ error: 'Cannot update a watch later playlist.' })
66 }
67
59 if (req.body.videoChannelId && !await isVideoChannelIdExist(req.body.videoChannelId, res)) return cleanUpReqFiles(req) 68 if (req.body.videoChannelId && !await isVideoChannelIdExist(req.body.videoChannelId, res)) return cleanUpReqFiles(req)
60 69
61 return next() 70 return next()
@@ -72,6 +81,13 @@ const videoPlaylistsDeleteValidator = [
72 if (areValidationErrors(req, res)) return 81 if (areValidationErrors(req, res)) return
73 82
74 if (!await isVideoPlaylistExist(req.params.playlistId, res)) return 83 if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
84
85 const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
86 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
87 return res.status(409)
88 .json({ error: 'Cannot delete a watch later playlist.' })
89 }
90
75 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { 91 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
76 return 92 return
77 } 93 }
@@ -127,10 +143,10 @@ const videoPlaylistsAddVideoValidator = [
127 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), 143 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
128 body('startTimestamp') 144 body('startTimestamp')
129 .optional() 145 .optional()
130 .isInt({ min: 0 }).withMessage('Should have a valid start timestamp'), 146 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
131 body('stopTimestamp') 147 body('stopTimestamp')
132 .optional() 148 .optional()
133 .isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'), 149 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
134 150
135 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 151 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
136 logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params }) 152 logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
@@ -167,10 +183,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
167 .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'), 183 .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
168 body('startTimestamp') 184 body('startTimestamp')
169 .optional() 185 .optional()
170 .isInt({ min: 0 }).withMessage('Should have a valid start timestamp'), 186 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
171 body('stopTimestamp') 187 body('stopTimestamp')
172 .optional() 188 .optional()
173 .isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'), 189 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
174 190
175 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 191 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
176 logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params }) 192 logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
@@ -275,6 +291,20 @@ const videoPlaylistsReorderVideosValidator = [
275 } 291 }
276] 292]
277 293
294const commonVideoPlaylistFiltersValidator = [
295 query('playlistType')
296 .optional()
297 .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
298
299 (req: express.Request, res: express.Response, next: express.NextFunction) => {
300 logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
301
302 if (areValidationErrors(req, res)) return
303
304 return next()
305 }
306]
307
278// --------------------------------------------------------------------------- 308// ---------------------------------------------------------------------------
279 309
280export { 310export {
@@ -287,7 +317,9 @@ export {
287 videoPlaylistsUpdateOrRemoveVideoValidator, 317 videoPlaylistsUpdateOrRemoveVideoValidator,
288 videoPlaylistsReorderVideosValidator, 318 videoPlaylistsReorderVideosValidator,
289 319
290 videoPlaylistElementAPGetValidator 320 videoPlaylistElementAPGetValidator,
321
322 commonVideoPlaylistFiltersValidator
291} 323}
292 324
293// --------------------------------------------------------------------------- 325// ---------------------------------------------------------------------------
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index c077fb518..ca06048d1 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -67,9 +67,9 @@ type AvailableForListOptions = {
67 ] 67 ]
68}) 68})
69@Scopes({ 69@Scopes({
70 [ScopeNames.SUMMARY]: (required: boolean, withAccount: boolean) => { 70 [ScopeNames.SUMMARY]: (withAccount = false) => {
71 const base: IFindOptions<VideoChannelModel> = { 71 const base: IFindOptions<VideoChannelModel> = {
72 attributes: [ 'name', 'description', 'id' ], 72 attributes: [ 'name', 'description', 'id', 'actorId' ],
73 include: [ 73 include: [
74 { 74 {
75 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 75 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
@@ -225,7 +225,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
225 foreignKey: { 225 foreignKey: {
226 allowNull: true 226 allowNull: true
227 }, 227 },
228 onDelete: 'cascade', 228 onDelete: 'CASCADE',
229 hooks: true 229 hooks: true
230 }) 230 })
231 VideoPlaylists: VideoPlaylistModel[] 231 VideoPlaylists: VideoPlaylistModel[]
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index 5530e0492..a2bd225a1 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -20,6 +20,7 @@ import { getSort, throwIfNotValid } from '../utils'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21import { CONSTRAINTS_FIELDS } from '../../initializers' 21import { CONSTRAINTS_FIELDS } from '../../initializers'
22import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 22import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
23import * as validator from 'validator'
23 24
24@Table({ 25@Table({
25 tableName: 'videoPlaylistElement', 26 tableName: 'videoPlaylistElement',
@@ -35,10 +36,6 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object
35 unique: true 36 unique: true
36 }, 37 },
37 { 38 {
38 fields: [ 'videoPlaylistId', 'position' ],
39 unique: true
40 },
41 {
42 fields: [ 'url' ], 39 fields: [ 'url' ],
43 unique: true 40 unique: true
44 } 41 }
@@ -143,7 +140,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
143 return VideoPlaylistElementModel.findOne(query) 140 return VideoPlaylistElementModel.findOne(query)
144 } 141 }
145 142
146 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number) { 143 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) {
147 const query = { 144 const query = {
148 attributes: [ 'url' ], 145 attributes: [ 'url' ],
149 offset: start, 146 offset: start,
@@ -151,7 +148,8 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
151 order: getSort('position'), 148 order: getSort('position'),
152 where: { 149 where: {
153 videoPlaylistId 150 videoPlaylistId
154 } 151 },
152 transaction: t
155 } 153 }
156 154
157 return VideoPlaylistElementModel 155 return VideoPlaylistElementModel
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 397887ebf..ce49f77ec 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -24,7 +24,14 @@ import {
24 isVideoPlaylistPrivacyValid 24 isVideoPlaylistPrivacyValid
25} from '../../helpers/custom-validators/video-playlists' 25} from '../../helpers/custom-validators/video-playlists'
26import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 26import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
27import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers' 27import {
28 CONFIG,
29 CONSTRAINTS_FIELDS,
30 STATIC_PATHS,
31 THUMBNAILS_SIZE,
32 VIDEO_PLAYLIST_PRIVACIES,
33 VIDEO_PLAYLIST_TYPES
34} from '../../initializers'
28import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' 35import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
29import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' 36import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
30import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 37import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
@@ -34,22 +41,25 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl
34import { activityPubCollectionPagination } from '../../helpers/activitypub' 41import { activityPubCollectionPagination } from '../../helpers/activitypub'
35import { remove } from 'fs-extra' 42import { remove } from 'fs-extra'
36import { logger } from '../../helpers/logger' 43import { logger } from '../../helpers/logger'
44import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
37 45
38enum ScopeNames { 46enum ScopeNames {
39 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 47 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
40 WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', 48 WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
41 WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' 49 WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
50 WITH_ACCOUNT = 'WITH_ACCOUNT'
42} 51}
43 52
44type AvailableForListOptions = { 53type AvailableForListOptions = {
45 followerActorId: number 54 followerActorId: number
46 accountId?: number, 55 type?: VideoPlaylistType
56 accountId?: number
47 videoChannelId?: number 57 videoChannelId?: number
48 privateAndUnlisted?: boolean 58 privateAndUnlisted?: boolean
49} 59}
50 60
51@Scopes({ 61@Scopes({
52 [ScopeNames.WITH_VIDEOS_LENGTH]: { 62 [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
53 attributes: { 63 attributes: {
54 include: [ 64 include: [
55 [ 65 [
@@ -59,7 +69,15 @@ type AvailableForListOptions = {
59 ] 69 ]
60 } 70 }
61 }, 71 },
62 [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { 72 [ ScopeNames.WITH_ACCOUNT ]: {
73 include: [
74 {
75 model: () => AccountModel,
76 required: true
77 }
78 ]
79 },
80 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
63 include: [ 81 include: [
64 { 82 {
65 model: () => AccountModel.scope(AccountScopeNames.SUMMARY), 83 model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -71,7 +89,7 @@ type AvailableForListOptions = {
71 } 89 }
72 ] 90 ]
73 }, 91 },
74 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { 92 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
75 // Only list local playlists OR playlists that are on an instance followed by actorId 93 // Only list local playlists OR playlists that are on an instance followed by actorId
76 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) 94 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
77 const actorWhere = { 95 const actorWhere = {
@@ -107,6 +125,12 @@ type AvailableForListOptions = {
107 }) 125 })
108 } 126 }
109 127
128 if (options.type) {
129 whereAnd.push({
130 type: options.type
131 })
132 }
133
110 const where = { 134 const where = {
111 [Sequelize.Op.and]: whereAnd 135 [Sequelize.Op.and]: whereAnd
112 } 136 }
@@ -179,6 +203,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
179 @Column(DataType.UUID) 203 @Column(DataType.UUID)
180 uuid: string 204 uuid: string
181 205
206 @AllowNull(false)
207 @Default(VideoPlaylistType.REGULAR)
208 @Column
209 type: VideoPlaylistType
210
182 @ForeignKey(() => AccountModel) 211 @ForeignKey(() => AccountModel)
183 @Column 212 @Column
184 ownerAccountId: number 213 ownerAccountId: number
@@ -208,13 +237,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
208 name: 'videoPlaylistId', 237 name: 'videoPlaylistId',
209 allowNull: false 238 allowNull: false
210 }, 239 },
211 onDelete: 'cascade' 240 onDelete: 'CASCADE'
212 }) 241 })
213 VideoPlaylistElements: VideoPlaylistElementModel[] 242 VideoPlaylistElements: VideoPlaylistElementModel[]
214 243
215 // Calculated field
216 videosLength?: number
217
218 @BeforeDestroy 244 @BeforeDestroy
219 static async removeFiles (instance: VideoPlaylistModel) { 245 static async removeFiles (instance: VideoPlaylistModel) {
220 logger.info('Removing files of video playlist %s.', instance.url) 246 logger.info('Removing files of video playlist %s.', instance.url)
@@ -227,6 +253,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
227 start: number, 253 start: number,
228 count: number, 254 count: number,
229 sort: string, 255 sort: string,
256 type?: VideoPlaylistType,
230 accountId?: number, 257 accountId?: number,
231 videoChannelId?: number, 258 videoChannelId?: number,
232 privateAndUnlisted?: boolean 259 privateAndUnlisted?: boolean
@@ -242,6 +269,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
242 method: [ 269 method: [
243 ScopeNames.AVAILABLE_FOR_LIST, 270 ScopeNames.AVAILABLE_FOR_LIST,
244 { 271 {
272 type: options.type,
245 followerActorId: options.followerActorId, 273 followerActorId: options.followerActorId,
246 accountId: options.accountId, 274 accountId: options.accountId,
247 videoChannelId: options.videoChannelId, 275 videoChannelId: options.videoChannelId,
@@ -289,7 +317,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
289 .then(e => !!e) 317 .then(e => !!e)
290 } 318 }
291 319
292 static load (id: number | string, transaction: Sequelize.Transaction) { 320 static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
293 const where = buildWhereIdOrUUID(id) 321 const where = buildWhereIdOrUUID(id)
294 322
295 const query = { 323 const query = {
@@ -298,14 +326,39 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
298 } 326 }
299 327
300 return VideoPlaylistModel 328 return VideoPlaylistModel
301 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ]) 329 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
302 .findOne(query) 330 .findOne(query)
303 } 331 }
304 332
333 static loadByUrlAndPopulateAccount (url: string) {
334 const query = {
335 where: {
336 url
337 }
338 }
339
340 return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
341 }
342
305 static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { 343 static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
306 return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' 344 return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
307 } 345 }
308 346
347 static getTypeLabel (type: VideoPlaylistType) {
348 return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
349 }
350
351 static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
352 const query = {
353 where: {
354 videoChannelId
355 },
356 transaction
357 }
358
359 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
360 }
361
309 getThumbnailName () { 362 getThumbnailName () {
310 const extension = '.jpg' 363 const extension = '.jpg'
311 364
@@ -345,7 +398,12 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
345 398
346 thumbnailPath: this.getThumbnailStaticPath(), 399 thumbnailPath: this.getThumbnailStaticPath(),
347 400
348 videosLength: this.videosLength, 401 type: {
402 id: this.type,
403 label: VideoPlaylistModel.getTypeLabel(this.type)
404 },
405
406 videosLength: this.get('videosLength'),
349 407
350 createdAt: this.createdAt, 408 createdAt: this.createdAt,
351 updatedAt: this.updatedAt, 409 updatedAt: this.updatedAt,
@@ -355,18 +413,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
355 } 413 }
356 } 414 }
357 415
358 toActivityPubObject (): Promise<PlaylistObject> { 416 toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
359 const handler = (start: number, count: number) => { 417 const handler = (start: number, count: number) => {
360 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count) 418 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
361 } 419 }
362 420
363 return activityPubCollectionPagination(this.url, handler, null) 421 return activityPubCollectionPagination(this.url, handler, page)
364 .then(o => { 422 .then(o => {
365 return Object.assign(o, { 423 return Object.assign(o, {
366 type: 'Playlist' as 'Playlist', 424 type: 'Playlist' as 'Playlist',
367 name: this.name, 425 name: this.name,
368 content: this.description, 426 content: this.description,
369 uuid: this.uuid, 427 uuid: this.uuid,
428 published: this.createdAt.toISOString(),
429 updated: this.updatedAt.toISOString(),
370 attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], 430 attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
371 icon: { 431 icon: {
372 type: 'Image' as 'Image', 432 type: 'Image' as 'Image',
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index c87f71277..7df0ed18d 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -125,7 +125,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
125 .then(res => res.map(r => r.Actor)) 125 .then(res => res.map(r => r.Actor))
126 } 126 }
127 127
128 static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> { 128 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
129 const query = { 129 const query = {
130 attributes: [], 130 attributes: [],
131 include: [ 131 include: [
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 7a102b058..a563f78ef 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -225,7 +225,7 @@ type AvailableForListIDsOptions = {
225 }, 225 },
226 include: [ 226 include: [
227 { 227 {
228 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY) 228 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] })
229 } 229 }
230 ] 230 ]
231 } 231 }
@@ -1535,18 +1535,7 @@ export class VideoModel extends Model<VideoModel> {
1535 1535
1536 if (ids.length === 0) return { data: [], total: count } 1536 if (ids.length === 0) return { data: [], total: count }
1537 1537
1538 // FIXME: typings 1538 const secondQuery: IFindOptions<VideoModel> = {
1539 const apiScope: any[] = [
1540 {
1541 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1542 }
1543 ]
1544
1545 if (options.user) {
1546 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1547 }
1548
1549 const secondQuery = {
1550 offset: 0, 1539 offset: 0,
1551 limit: query.limit, 1540 limit: query.limit,
1552 attributes: query.attributes, 1541 attributes: query.attributes,
@@ -1556,6 +1545,29 @@ export class VideoModel extends Model<VideoModel> {
1556 ) 1545 )
1557 ] 1546 ]
1558 } 1547 }
1548
1549 // FIXME: typing
1550 const apiScope: any[] = []
1551
1552 if (options.user) {
1553 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1554
1555 // Even if the relation is n:m, we know that a user only have 0..1 video history
1556 // So we won't have multiple rows for the same video
1557 // A subquery adds some bugs in our query so disable it
1558 secondQuery.subQuery = false
1559 }
1560
1561 apiScope.push({
1562 method: [
1563 ScopeNames.FOR_API, {
1564 ids, withFiles:
1565 options.withFiles,
1566 videoPlaylistId: options.videoPlaylistId
1567 } as ForAPIOptions
1568 ]
1569 })
1570
1559 const rows = await VideoModel.scope(apiScope).findAll(secondQuery) 1571 const rows = await VideoModel.scope(apiScope).findAll(secondQuery)
1560 1572
1561 return { 1573 return {
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 77c17036a..ca51cd39a 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -16,6 +16,7 @@ import './video-captions'
16import './video-channels' 16import './video-channels'
17import './video-comments' 17import './video-comments'
18import './video-imports' 18import './video-imports'
19import './video-playlists'
19import './videos' 20import './videos'
20import './videos-filter' 21import './videos-filter'
21import './videos-history' 22import './videos-history'
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts
index 68fe362e9..803e7afb9 100644
--- a/server/tests/api/check-params/video-playlists.ts
+++ b/server/tests/api/check-params/video-playlists.ts
@@ -2,20 +2,24 @@
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
5 createUser, 5 addVideoInPlaylist,
6 createVideoPlaylist, 6 createVideoPlaylist,
7 deleteVideoPlaylist, 7 deleteVideoPlaylist,
8 flushTests, 8 flushTests,
9 generateUserAccessToken,
10 getAccountPlaylistsListWithToken,
9 getVideoPlaylist, 11 getVideoPlaylist,
10 immutableAssign, 12 immutableAssign,
11 killallServers, 13 killallServers,
12 makeGetRequest, 14 makeGetRequest,
15 removeVideoFromPlaylist,
16 reorderVideosPlaylist,
13 runServer, 17 runServer,
14 ServerInfo, 18 ServerInfo,
15 setAccessTokensToServers, 19 setAccessTokensToServers,
16 updateVideoPlaylist, 20 updateVideoPlaylist,
17 userLogin, 21 updateVideoPlaylistElement,
18 addVideoInPlaylist, uploadVideo, updateVideoPlaylistElement, removeVideoFromPlaylist, reorderVideosPlaylist 22 uploadVideoAndGetId
19} from '../../../../shared/utils' 23} from '../../../../shared/utils'
20import { 24import {
21 checkBadCountPagination, 25 checkBadCountPagination,
@@ -23,11 +27,13 @@ import {
23 checkBadStartPagination 27 checkBadStartPagination
24} from '../../../../shared/utils/requests/check-api-params' 28} from '../../../../shared/utils/requests/check-api-params'
25import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 29import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
30import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
26 31
27describe('Test video playlists API validator', function () { 32describe('Test video playlists API validator', function () {
28 let server: ServerInfo 33 let server: ServerInfo
29 let userAccessToken = '' 34 let userAccessToken: string
30 let playlistUUID: string 35 let playlistUUID: string
36 let watchLaterPlaylistId: number
31 let videoId: number 37 let videoId: number
32 let videoId2: number 38 let videoId2: number
33 39
@@ -42,19 +48,13 @@ describe('Test video playlists API validator', function () {
42 48
43 await setAccessTokensToServers([ server ]) 49 await setAccessTokensToServers([ server ])
44 50
45 const username = 'user1' 51 userAccessToken = await generateUserAccessToken(server, 'user1')
46 const password = 'my super password' 52 videoId = (await uploadVideoAndGetId({ server, videoName: 'video 1' })).id
47 await createUser(server.url, server.accessToken, username, password) 53 videoId2 = (await uploadVideoAndGetId({ server, videoName: 'video 2' })).id
48 userAccessToken = await userLogin(server, { username, password })
49 54
50 { 55 {
51 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) 56 const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root',0, 5, VideoPlaylistType.WATCH_LATER)
52 videoId = res.body.video.id 57 watchLaterPlaylistId = res.body.data[0].id
53 }
54
55 {
56 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
57 videoId2 = res.body.video.id
58 } 58 }
59 59
60 { 60 {
@@ -93,6 +93,12 @@ describe('Test video playlists API validator', function () {
93 await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) 93 await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
94 }) 94 })
95 95
96 it('Should fail with a bad playlist type', async function () {
97 await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } })
98 await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } })
99 await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } })
100 })
101
96 it('Should fail with a bad account parameter', async function () { 102 it('Should fail with a bad account parameter', async function () {
97 const accountPath = '/api/v1/accounts/root2/video-playlists' 103 const accountPath = '/api/v1/accounts/root2/video-playlists'
98 104
@@ -158,410 +164,250 @@ describe('Test video playlists API validator', function () {
158 }) 164 })
159 165
160 describe('When creating/updating a video playlist', function () { 166 describe('When creating/updating a video playlist', function () {
167 const getBase = (playlistAttrs: any = {}, wrapper: any = {}) => {
168 return Object.assign({
169 expectedStatus: 400,
170 url: server.url,
171 token: server.accessToken,
172 playlistAttrs: Object.assign({
173 displayName: 'display name',
174 privacy: VideoPlaylistPrivacy.UNLISTED,
175 thumbnailfile: 'thumbnail.jpg'
176 }, playlistAttrs)
177 }, wrapper)
178 }
179 const getUpdate = (params: any, playlistId: number | string) => {
180 return immutableAssign(params, { playlistId: playlistId })
181 }
161 182
162 it('Should fail with an unauthenticated user', async function () { 183 it('Should fail with an unauthenticated user', async function () {
163 const baseParams = { 184 const params = getBase({}, { token: null, expectedStatus: 401 })
164 url: server.url,
165 token: null,
166 playlistAttrs: {
167 displayName: 'super playlist',
168 privacy: VideoPlaylistPrivacy.PUBLIC
169 },
170 expectedStatus: 401
171 }
172 185
173 await createVideoPlaylist(baseParams) 186 await createVideoPlaylist(params)
174 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 187 await updateVideoPlaylist(getUpdate(params, playlistUUID))
175 }) 188 })
176 189
177 it('Should fail without displayName', async function () { 190 it('Should fail without displayName', async function () {
178 const baseParams = { 191 const params = getBase({ displayName: undefined })
179 url: server.url,
180 token: server.accessToken,
181 playlistAttrs: {
182 privacy: VideoPlaylistPrivacy.PUBLIC
183 } as any,
184 expectedStatus: 400
185 }
186 192
187 await createVideoPlaylist(baseParams) 193 await createVideoPlaylist(params)
188 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 194 await updateVideoPlaylist(getUpdate(params, playlistUUID))
189 }) 195 })
190 196
191 it('Should fail with an incorrect display name', async function () { 197 it('Should fail with an incorrect display name', async function () {
192 const baseParams = { 198 const params = getBase({ displayName: 's'.repeat(300) })
193 url: server.url,
194 token: server.accessToken,
195 playlistAttrs: {
196 displayName: 's'.repeat(300),
197 privacy: VideoPlaylistPrivacy.PUBLIC
198 },
199 expectedStatus: 400
200 }
201 199
202 await createVideoPlaylist(baseParams) 200 await createVideoPlaylist(params)
203 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 201 await updateVideoPlaylist(getUpdate(params, playlistUUID))
204 }) 202 })
205 203
206 it('Should fail with an incorrect description', async function () { 204 it('Should fail with an incorrect description', async function () {
207 const baseParams = { 205 const params = getBase({ description: 't' })
208 url: server.url,
209 token: server.accessToken,
210 playlistAttrs: {
211 displayName: 'display name',
212 privacy: VideoPlaylistPrivacy.PUBLIC,
213 description: 't'
214 },
215 expectedStatus: 400
216 }
217 206
218 await createVideoPlaylist(baseParams) 207 await createVideoPlaylist(params)
219 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 208 await updateVideoPlaylist(getUpdate(params, playlistUUID))
220 }) 209 })
221 210
222 it('Should fail with an incorrect privacy', async function () { 211 it('Should fail with an incorrect privacy', async function () {
223 const baseParams = { 212 const params = getBase({ privacy: 45 })
224 url: server.url,
225 token: server.accessToken,
226 playlistAttrs: {
227 displayName: 'display name',
228 privacy: 45
229 } as any,
230 expectedStatus: 400
231 }
232 213
233 await createVideoPlaylist(baseParams) 214 await createVideoPlaylist(params)
234 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 215 await updateVideoPlaylist(getUpdate(params, playlistUUID))
235 }) 216 })
236 217
237 it('Should fail with an unknown video channel id', async function () { 218 it('Should fail with an unknown video channel id', async function () {
238 const baseParams = { 219 const params = getBase({ videoChannelId: 42 }, { expectedStatus: 404 })
239 url: server.url,
240 token: server.accessToken,
241 playlistAttrs: {
242 displayName: 'display name',
243 privacy: VideoPlaylistPrivacy.PUBLIC,
244 videoChannelId: 42
245 },
246 expectedStatus: 404
247 }
248 220
249 await createVideoPlaylist(baseParams) 221 await createVideoPlaylist(params)
250 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 222 await updateVideoPlaylist(getUpdate(params, playlistUUID))
251 }) 223 })
252 224
253 it('Should fail with an incorrect thumbnail file', async function () { 225 it('Should fail with an incorrect thumbnail file', async function () {
254 const baseParams = { 226 const params = getBase({ thumbnailfile: 'avatar.png' })
255 url: server.url,
256 token: server.accessToken,
257 playlistAttrs: {
258 displayName: 'display name',
259 privacy: VideoPlaylistPrivacy.PUBLIC,
260 thumbnailfile: 'avatar.png'
261 },
262 expectedStatus: 400
263 }
264 227
265 await createVideoPlaylist(baseParams) 228 await createVideoPlaylist(params)
266 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 229 await updateVideoPlaylist(getUpdate(params, playlistUUID))
267 }) 230 })
268 231
269 it('Should fail with an unknown playlist to update', async function () { 232 it('Should fail with an unknown playlist to update', async function () {
270 await updateVideoPlaylist({ 233 await updateVideoPlaylist(getUpdate(
271 url: server.url, 234 getBase({}, { expectedStatus: 404 }),
272 token: server.accessToken, 235 42
273 playlistId: 42, 236 ))
274 playlistAttrs: {
275 displayName: 'display name',
276 privacy: VideoPlaylistPrivacy.PUBLIC
277 },
278 expectedStatus: 404
279 })
280 }) 237 })
281 238
282 it('Should fail to update a playlist of another user', async function () { 239 it('Should fail to update a playlist of another user', async function () {
283 await updateVideoPlaylist({ 240 await updateVideoPlaylist(getUpdate(
284 url: server.url, 241 getBase({}, { token: userAccessToken, expectedStatus: 403 }),
285 token: userAccessToken, 242 playlistUUID
286 playlistId: playlistUUID, 243 ))
287 playlistAttrs: {
288 displayName: 'display name',
289 privacy: VideoPlaylistPrivacy.PUBLIC
290 },
291 expectedStatus: 403
292 })
293 }) 244 })
294 245
295 it('Should fail to update to private a public/unlisted playlist', async function () { 246 it('Should fail to update to private a public/unlisted playlist', async function () {
296 const res = await createVideoPlaylist({ 247 const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC }, { expectedStatus: 200 })
297 url: server.url, 248
298 token: server.accessToken, 249 const res = await createVideoPlaylist(params)
299 playlistAttrs: {
300 displayName: 'super playlist',
301 privacy: VideoPlaylistPrivacy.PUBLIC
302 }
303 })
304 const playlist = res.body.videoPlaylist 250 const playlist = res.body.videoPlaylist
305 251
306 await updateVideoPlaylist({ 252 const paramsUpdate = getBase({ privacy: VideoPlaylistPrivacy.PRIVATE }, { expectedStatus: 409 })
307 url: server.url, 253
308 token: server.accessToken, 254 await updateVideoPlaylist(getUpdate(paramsUpdate, playlist.id))
309 playlistId: playlist.id, 255 })
310 playlistAttrs: { 256
311 displayName: 'display name', 257 it('Should fail to update the watch later playlist', async function () {
312 privacy: VideoPlaylistPrivacy.PRIVATE 258 await updateVideoPlaylist(getUpdate(
313 }, 259 getBase({}, { expectedStatus: 409 }),
314 expectedStatus: 409 260 watchLaterPlaylistId
315 }) 261 ))
316 }) 262 })
317 263
318 it('Should succeed with the correct params', async function () { 264 it('Should succeed with the correct params', async function () {
319 const baseParams = { 265 {
320 url: server.url, 266 const params = getBase({}, { expectedStatus: 200 })
321 token: server.accessToken, 267 await createVideoPlaylist(params)
322 playlistAttrs: {
323 displayName: 'display name',
324 privacy: VideoPlaylistPrivacy.UNLISTED,
325 thumbnailfile: 'thumbnail.jpg'
326 }
327 } 268 }
328 269
329 await createVideoPlaylist(baseParams) 270 {
330 await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID })) 271 const params = getBase({}, { expectedStatus: 204 })
272 await updateVideoPlaylist(getUpdate(params, playlistUUID))
273 }
331 }) 274 })
332 }) 275 })
333 276
334 describe('When adding an element in a playlist', function () { 277 describe('When adding an element in a playlist', function () {
335 it('Should fail with an unauthenticated user', async function () { 278 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
336 await addVideoInPlaylist({ 279 return Object.assign({
280 expectedStatus: 400,
337 url: server.url, 281 url: server.url,
338 token: null, 282 token: server.accessToken,
339 elementAttrs: {
340 videoId: videoId
341 },
342 playlistId: playlistUUID, 283 playlistId: playlistUUID,
343 expectedStatus: 401 284 elementAttrs: Object.assign({
344 }) 285 videoId: videoId,
286 startTimestamp: 2,
287 stopTimestamp: 3
288 }, elementAttrs)
289 }, wrapper)
290 }
291
292 it('Should fail with an unauthenticated user', async function () {
293 const params = getBase({}, { token: null, expectedStatus: 401 })
294 await addVideoInPlaylist(params)
345 }) 295 })
346 296
347 it('Should fail with the playlist of another user', async function () { 297 it('Should fail with the playlist of another user', async function () {
348 await addVideoInPlaylist({ 298 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
349 url: server.url, 299 await addVideoInPlaylist(params)
350 token: userAccessToken,
351 elementAttrs: {
352 videoId: videoId
353 },
354 playlistId: playlistUUID,
355 expectedStatus: 403
356 })
357 }) 300 })
358 301
359 it('Should fail with an unknown or incorrect playlist id', async function () { 302 it('Should fail with an unknown or incorrect playlist id', async function () {
360 await addVideoInPlaylist({ 303 {
361 url: server.url, 304 const params = getBase({}, { playlistId: 'toto' })
362 token: server.accessToken, 305 await addVideoInPlaylist(params)
363 elementAttrs: { 306 }
364 videoId: videoId
365 },
366 playlistId: 'toto',
367 expectedStatus: 400
368 })
369 307
370 await addVideoInPlaylist({ 308 {
371 url: server.url, 309 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
372 token: server.accessToken, 310 await addVideoInPlaylist(params)
373 elementAttrs: { 311 }
374 videoId: videoId
375 },
376 playlistId: 42,
377 expectedStatus: 404
378 })
379 }) 312 })
380 313
381 it('Should fail with an unknown or incorrect video id', async function () { 314 it('Should fail with an unknown or incorrect video id', async function () {
382 await addVideoInPlaylist({ 315 const params = getBase({ videoId: 42 }, { expectedStatus: 404 })
383 url: server.url, 316 await addVideoInPlaylist(params)
384 token: server.accessToken,
385 elementAttrs: {
386 videoId: 'toto' as any
387 },
388 playlistId: playlistUUID,
389 expectedStatus: 400
390 })
391
392 await addVideoInPlaylist({
393 url: server.url,
394 token: server.accessToken,
395 elementAttrs: {
396 videoId: 42
397 },
398 playlistId: playlistUUID,
399 expectedStatus: 404
400 })
401 }) 317 })
402 318
403 it('Should fail with a bad start/stop timestamp', async function () { 319 it('Should fail with a bad start/stop timestamp', async function () {
404 await addVideoInPlaylist({ 320 {
405 url: server.url, 321 const params = getBase({ startTimestamp: -42 })
406 token: server.accessToken, 322 await addVideoInPlaylist(params)
407 elementAttrs: { 323 }
408 videoId: videoId,
409 startTimestamp: -42
410 },
411 playlistId: playlistUUID,
412 expectedStatus: 400
413 })
414 324
415 await addVideoInPlaylist({ 325 {
416 url: server.url, 326 const params = getBase({ stopTimestamp: 'toto' as any })
417 token: server.accessToken, 327 await addVideoInPlaylist(params)
418 elementAttrs: { 328 }
419 videoId: videoId,
420 stopTimestamp: 'toto' as any
421 },
422 playlistId: playlistUUID,
423 expectedStatus: 400
424 })
425 }) 329 })
426 330
427 it('Succeed with the correct params', async function () { 331 it('Succeed with the correct params', async function () {
428 await addVideoInPlaylist({ 332 const params = getBase({}, { expectedStatus: 200 })
429 url: server.url, 333 await addVideoInPlaylist(params)
430 token: server.accessToken,
431 elementAttrs: {
432 videoId: videoId,
433 stopTimestamp: 3
434 },
435 playlistId: playlistUUID,
436 expectedStatus: 200
437 })
438 }) 334 })
439 335
440 it('Should fail if the video was already added in the playlist', async function () { 336 it('Should fail if the video was already added in the playlist', async function () {
441 await addVideoInPlaylist({ 337 const params = getBase({}, { expectedStatus: 409 })
442 url: server.url, 338 await addVideoInPlaylist(params)
443 token: server.accessToken,
444 elementAttrs: {
445 videoId: videoId,
446 stopTimestamp: 3
447 },
448 playlistId: playlistUUID,
449 expectedStatus: 409
450 })
451 }) 339 })
452 }) 340 })
453 341
454 describe('When updating an element in a playlist', function () { 342 describe('When updating an element in a playlist', function () {
455 it('Should fail with an unauthenticated user', async function () { 343 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
456 await updateVideoPlaylistElement({ 344 return Object.assign({
457 url: server.url, 345 url: server.url,
458 token: null, 346 token: server.accessToken,
459 elementAttrs: { }, 347 elementAttrs: Object.assign({
348 startTimestamp: 1,
349 stopTimestamp: 2
350 }, elementAttrs),
460 videoId: videoId, 351 videoId: videoId,
461 playlistId: playlistUUID, 352 playlistId: playlistUUID,
462 expectedStatus: 401 353 expectedStatus: 400
463 }) 354 }, wrapper)
355 }
356
357 it('Should fail with an unauthenticated user', async function () {
358 const params = getBase({}, { token: null, expectedStatus: 401 })
359 await updateVideoPlaylistElement(params)
464 }) 360 })
465 361
466 it('Should fail with the playlist of another user', async function () { 362 it('Should fail with the playlist of another user', async function () {
467 await updateVideoPlaylistElement({ 363 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
468 url: server.url, 364 await updateVideoPlaylistElement(params)
469 token: userAccessToken,
470 elementAttrs: { },
471 videoId: videoId,
472 playlistId: playlistUUID,
473 expectedStatus: 403
474 })
475 }) 365 })
476 366
477 it('Should fail with an unknown or incorrect playlist id', async function () { 367 it('Should fail with an unknown or incorrect playlist id', async function () {
478 await updateVideoPlaylistElement({ 368 {
479 url: server.url, 369 const params = getBase({}, { playlistId: 'toto' })
480 token: server.accessToken, 370 await updateVideoPlaylistElement(params)
481 elementAttrs: { }, 371 }
482 videoId: videoId,
483 playlistId: 'toto',
484 expectedStatus: 400
485 })
486 372
487 await updateVideoPlaylistElement({ 373 {
488 url: server.url, 374 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
489 token: server.accessToken, 375 await updateVideoPlaylistElement(params)
490 elementAttrs: { }, 376 }
491 videoId: videoId,
492 playlistId: 42,
493 expectedStatus: 404
494 })
495 }) 377 })
496 378
497 it('Should fail with an unknown or incorrect video id', async function () { 379 it('Should fail with an unknown or incorrect video id', async function () {
498 await updateVideoPlaylistElement({ 380 {
499 url: server.url, 381 const params = getBase({}, { videoId: 'toto' })
500 token: server.accessToken, 382 await updateVideoPlaylistElement(params)
501 elementAttrs: { }, 383 }
502 videoId: 'toto',
503 playlistId: playlistUUID,
504 expectedStatus: 400
505 })
506 384
507 await updateVideoPlaylistElement({ 385 {
508 url: server.url, 386 const params = getBase({}, { videoId: 42, expectedStatus: 404 })
509 token: server.accessToken, 387 await updateVideoPlaylistElement(params)
510 elementAttrs: { }, 388 }
511 videoId: 42,
512 playlistId: playlistUUID,
513 expectedStatus: 404
514 })
515 }) 389 })
516 390
517 it('Should fail with a bad start/stop timestamp', async function () { 391 it('Should fail with a bad start/stop timestamp', async function () {
518 await updateVideoPlaylistElement({ 392 {
519 url: server.url, 393 const params = getBase({ startTimestamp: 'toto' as any })
520 token: server.accessToken, 394 await updateVideoPlaylistElement(params)
521 elementAttrs: { 395 }
522 startTimestamp: 'toto' as any
523 },
524 videoId: videoId,
525 playlistId: playlistUUID,
526 expectedStatus: 400
527 })
528 396
529 await updateVideoPlaylistElement({ 397 {
530 url: server.url, 398 const params = getBase({ stopTimestamp: -42 })
531 token: server.accessToken, 399 await updateVideoPlaylistElement(params)
532 elementAttrs: { 400 }
533 stopTimestamp: -42
534 },
535 videoId: videoId,
536 playlistId: playlistUUID,
537 expectedStatus: 400
538 })
539 }) 401 })
540 402
541 it('Should fail with an unknown element', async function () { 403 it('Should fail with an unknown element', async function () {
542 await updateVideoPlaylistElement({ 404 const params = getBase({}, { videoId: videoId2, expectedStatus: 404 })
543 url: server.url, 405 await updateVideoPlaylistElement(params)
544 token: server.accessToken,
545 elementAttrs: {
546 stopTimestamp: 2
547 },
548 videoId: videoId2,
549 playlistId: playlistUUID,
550 expectedStatus: 404
551 })
552 }) 406 })
553 407
554 it('Succeed with the correct params', async function () { 408 it('Succeed with the correct params', async function () {
555 await updateVideoPlaylistElement({ 409 const params = getBase({}, { expectedStatus: 204 })
556 url: server.url, 410 await updateVideoPlaylistElement(params)
557 token: server.accessToken,
558 elementAttrs: {
559 stopTimestamp: 2
560 },
561 videoId: videoId,
562 playlistId: playlistUUID,
563 expectedStatus: 204
564 })
565 }) 411 })
566 }) 412 })
567 413
@@ -569,280 +415,166 @@ describe('Test video playlists API validator', function () {
569 let videoId3: number 415 let videoId3: number
570 let videoId4: number 416 let videoId4: number
571 417
572 before(async function () { 418 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
573 { 419 return Object.assign({
574 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
575 videoId3 = res.body.video.id
576 }
577
578 {
579 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
580 videoId4 = res.body.video.id
581 }
582
583 await addVideoInPlaylist({
584 url: server.url, 420 url: server.url,
585 token: server.accessToken, 421 token: server.accessToken,
586 playlistId: playlistUUID, 422 playlistId: playlistUUID,
587 elementAttrs: { videoId: videoId3 } 423 elementAttrs: Object.assign({
588 }) 424 startPosition: 1,
425 insertAfterPosition: 2,
426 reorderLength: 3
427 }, elementAttrs),
428 expectedStatus: 400
429 }, wrapper)
430 }
589 431
590 await addVideoInPlaylist({ 432 before(async function () {
591 url: server.url, 433 videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id
592 token: server.accessToken, 434 videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id
593 playlistId: playlistUUID, 435
594 elementAttrs: { videoId: videoId4 } 436 for (let id of [ videoId3, videoId4 ]) {
595 }) 437 await addVideoInPlaylist({
438 url: server.url,
439 token: server.accessToken,
440 playlistId: playlistUUID,
441 elementAttrs: { videoId: id }
442 })
443 }
596 }) 444 })
597 445
598 it('Should fail with an unauthenticated user', async function () { 446 it('Should fail with an unauthenticated user', async function () {
599 await reorderVideosPlaylist({ 447 const params = getBase({}, { token: null, expectedStatus: 401 })
600 url: server.url, 448 await reorderVideosPlaylist(params)
601 token: null,
602 playlistId: playlistUUID,
603 elementAttrs: {
604 startPosition: 1,
605 insertAfterPosition: 2
606 },
607 expectedStatus: 401
608 })
609 }) 449 })
610 450
611 it('Should fail with the playlist of another user', async function () { 451 it('Should fail with the playlist of another user', async function () {
612 await reorderVideosPlaylist({ 452 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
613 url: server.url, 453 await reorderVideosPlaylist(params)
614 token: userAccessToken,
615 playlistId: playlistUUID,
616 elementAttrs: {
617 startPosition: 1,
618 insertAfterPosition: 2
619 },
620 expectedStatus: 403
621 })
622 }) 454 })
623 455
624 it('Should fail with an invalid playlist', async function () { 456 it('Should fail with an invalid playlist', async function () {
625 await reorderVideosPlaylist({ 457 {
626 url: server.url, 458 const params = getBase({}, { playlistId: 'toto' })
627 token: server.accessToken, 459 await reorderVideosPlaylist(params)
628 playlistId: 'toto', 460 }
629 elementAttrs: {
630 startPosition: 1,
631 insertAfterPosition: 2
632 },
633 expectedStatus: 400
634 })
635 461
636 await reorderVideosPlaylist({ 462 {
637 url: server.url, 463 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
638 token: server.accessToken, 464 await reorderVideosPlaylist(params)
639 playlistId: 42, 465 }
640 elementAttrs: {
641 startPosition: 1,
642 insertAfterPosition: 2
643 },
644 expectedStatus: 404
645 })
646 }) 466 })
647 467
648 it('Should fail with an invalid start position', async function () { 468 it('Should fail with an invalid start position', async function () {
649 await reorderVideosPlaylist({ 469 {
650 url: server.url, 470 const params = getBase({ startPosition: -1 })
651 token: server.accessToken, 471 await reorderVideosPlaylist(params)
652 playlistId: playlistUUID, 472 }
653 elementAttrs: {
654 startPosition: -1,
655 insertAfterPosition: 2
656 },
657 expectedStatus: 400
658 })
659 473
660 await reorderVideosPlaylist({ 474 {
661 url: server.url, 475 const params = getBase({ startPosition: 'toto' as any })
662 token: server.accessToken, 476 await reorderVideosPlaylist(params)
663 playlistId: playlistUUID, 477 }
664 elementAttrs: {
665 startPosition: 'toto' as any,
666 insertAfterPosition: 2
667 },
668 expectedStatus: 400
669 })
670 478
671 await reorderVideosPlaylist({ 479 {
672 url: server.url, 480 const params = getBase({ startPosition: 42 })
673 token: server.accessToken, 481 await reorderVideosPlaylist(params)
674 playlistId: playlistUUID, 482 }
675 elementAttrs: {
676 startPosition: 42,
677 insertAfterPosition: 2
678 },
679 expectedStatus: 400
680 })
681 }) 483 })
682 484
683 it('Should fail with an invalid insert after position', async function () { 485 it('Should fail with an invalid insert after position', async function () {
684 await reorderVideosPlaylist({ 486 {
685 url: server.url, 487 const params = getBase({ insertAfterPosition: 'toto' as any })
686 token: server.accessToken, 488 await reorderVideosPlaylist(params)
687 playlistId: playlistUUID, 489 }
688 elementAttrs: {
689 startPosition: 1,
690 insertAfterPosition: 'toto' as any
691 },
692 expectedStatus: 400
693 })
694 490
695 await reorderVideosPlaylist({ 491 {
696 url: server.url, 492 const params = getBase({ insertAfterPosition: -2 })
697 token: server.accessToken, 493 await reorderVideosPlaylist(params)
698 playlistId: playlistUUID, 494 }
699 elementAttrs: {
700 startPosition: 1,
701 insertAfterPosition: -2
702 },
703 expectedStatus: 400
704 })
705 495
706 await reorderVideosPlaylist({ 496 {
707 url: server.url, 497 const params = getBase({ insertAfterPosition: 42 })
708 token: server.accessToken, 498 await reorderVideosPlaylist(params)
709 playlistId: playlistUUID, 499 }
710 elementAttrs: {
711 startPosition: 1,
712 insertAfterPosition: 42
713 },
714 expectedStatus: 400
715 })
716 }) 500 })
717 501
718 it('Should fail with an invalid reorder length', async function () { 502 it('Should fail with an invalid reorder length', async function () {
719 await reorderVideosPlaylist({ 503 {
720 url: server.url, 504 const params = getBase({ reorderLength: 'toto' as any })
721 token: server.accessToken, 505 await reorderVideosPlaylist(params)
722 playlistId: playlistUUID, 506 }
723 elementAttrs: {
724 startPosition: 1,
725 insertAfterPosition: 2,
726 reorderLength: 'toto' as any
727 },
728 expectedStatus: 400
729 })
730 507
731 await reorderVideosPlaylist({ 508 {
732 url: server.url, 509 const params = getBase({ reorderLength: -2 })
733 token: server.accessToken, 510 await reorderVideosPlaylist(params)
734 playlistId: playlistUUID, 511 }
735 elementAttrs: {
736 startPosition: 1,
737 insertAfterPosition: 2,
738 reorderLength: -1
739 },
740 expectedStatus: 400
741 })
742 512
743 await reorderVideosPlaylist({ 513 {
744 url: server.url, 514 const params = getBase({ reorderLength: 42 })
745 token: server.accessToken, 515 await reorderVideosPlaylist(params)
746 playlistId: playlistUUID, 516 }
747 elementAttrs: {
748 startPosition: 1,
749 insertAfterPosition: 2,
750 reorderLength: 4
751 },
752 expectedStatus: 400
753 })
754 }) 517 })
755 518
756 it('Succeed with the correct params', async function () { 519 it('Succeed with the correct params', async function () {
757 await reorderVideosPlaylist({ 520 const params = getBase({}, { expectedStatus: 204 })
758 url: server.url, 521 await reorderVideosPlaylist(params)
759 token: server.accessToken,
760 playlistId: playlistUUID,
761 elementAttrs: {
762 startPosition: 1,
763 insertAfterPosition: 2,
764 reorderLength: 3
765 },
766 expectedStatus: 204
767 })
768 }) 522 })
769 }) 523 })
770 524
771 describe('When deleting an element in a playlist', function () { 525 describe('When deleting an element in a playlist', function () {
772 it('Should fail with an unauthenticated user', async function () { 526 const getBase = (wrapper: any = {}) => {
773 await removeVideoFromPlaylist({ 527 return Object.assign({
774 url: server.url, 528 url: server.url,
775 token: null, 529 token: server.accessToken,
776 videoId, 530 videoId: videoId,
777 playlistId: playlistUUID, 531 playlistId: playlistUUID,
778 expectedStatus: 401 532 expectedStatus: 400
779 }) 533 }, wrapper)
534 }
535
536 it('Should fail with an unauthenticated user', async function () {
537 const params = getBase({ token: null, expectedStatus: 401 })
538 await removeVideoFromPlaylist(params)
780 }) 539 })
781 540
782 it('Should fail with the playlist of another user', async function () { 541 it('Should fail with the playlist of another user', async function () {
783 await removeVideoFromPlaylist({ 542 const params = getBase({ token: userAccessToken, expectedStatus: 403 })
784 url: server.url, 543 await removeVideoFromPlaylist(params)
785 token: userAccessToken,
786 videoId,
787 playlistId: playlistUUID,
788 expectedStatus: 403
789 })
790 }) 544 })
791 545
792 it('Should fail with an unknown or incorrect playlist id', async function () { 546 it('Should fail with an unknown or incorrect playlist id', async function () {
793 await removeVideoFromPlaylist({ 547 {
794 url: server.url, 548 const params = getBase({ playlistId: 'toto' })
795 token: server.accessToken, 549 await removeVideoFromPlaylist(params)
796 videoId, 550 }
797 playlistId: 'toto',
798 expectedStatus: 400
799 })
800 551
801 await removeVideoFromPlaylist({ 552 {
802 url: server.url, 553 const params = getBase({ playlistId: 42, expectedStatus: 404 })
803 token: server.accessToken, 554 await removeVideoFromPlaylist(params)
804 videoId, 555 }
805 playlistId: 42,
806 expectedStatus: 404
807 })
808 }) 556 })
809 557
810 it('Should fail with an unknown or incorrect video id', async function () { 558 it('Should fail with an unknown or incorrect video id', async function () {
811 await removeVideoFromPlaylist({ 559 {
812 url: server.url, 560 const params = getBase({ videoId: 'toto' })
813 token: server.accessToken, 561 await removeVideoFromPlaylist(params)
814 videoId: 'toto', 562 }
815 playlistId: playlistUUID,
816 expectedStatus: 400
817 })
818 563
819 await removeVideoFromPlaylist({ 564 {
820 url: server.url, 565 const params = getBase({ videoId: 42, expectedStatus: 404 })
821 token: server.accessToken, 566 await removeVideoFromPlaylist(params)
822 videoId: 42, 567 }
823 playlistId: playlistUUID,
824 expectedStatus: 404
825 })
826 }) 568 })
827 569
828 it('Should fail with an unknown element', async function () { 570 it('Should fail with an unknown element', async function () {
829 await removeVideoFromPlaylist({ 571 const params = getBase({ videoId: videoId2, expectedStatus: 404 })
830 url: server.url, 572 await removeVideoFromPlaylist(params)
831 token: server.accessToken,
832 videoId: videoId2,
833 playlistId: playlistUUID,
834 expectedStatus: 404
835 })
836 }) 573 })
837 574
838 it('Succeed with the correct params', async function () { 575 it('Succeed with the correct params', async function () {
839 await removeVideoFromPlaylist({ 576 const params = getBase({ expectedStatus: 204 })
840 url: server.url, 577 await removeVideoFromPlaylist(params)
841 token: server.accessToken,
842 videoId: videoId,
843 playlistId: playlistUUID,
844 expectedStatus: 204
845 })
846 }) 578 })
847 }) 579 })
848 580
@@ -855,6 +587,10 @@ describe('Test video playlists API validator', function () {
855 await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, 403) 587 await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, 403)
856 }) 588 })
857 589
590 it('Should fail with the watch later playlist', async function () {
591 await deleteVideoPlaylist(server.url, server.accessToken, watchLaterPlaylistId, 409)
592 })
593
858 it('Should succeed with the correct params', async function () { 594 it('Should succeed with the correct params', async function () {
859 await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID) 595 await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID)
860 }) 596 })
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts
index cb23239da..7dd1563fc 100644
--- a/server/tests/api/videos/video-playlists.ts
+++ b/server/tests/api/videos/video-playlists.ts
@@ -2,152 +2,768 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { join } from 'path'
6import * as request from 'supertest'
7import { VideoPrivacy } from '../../../../shared/models/videos'
8import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
9import { 5import {
10 addVideoChannel, 6 addVideoChannel,
11 checkTmpIsEmpty, 7 addVideoInPlaylist,
12 checkVideoFilesWereRemoved, 8 checkPlaylistFilesWereRemoved,
13 completeVideoCheck,
14 createUser, 9 createUser,
15 dateIsValid, 10 createVideoPlaylist,
11 deleteVideoChannel,
12 deleteVideoPlaylist,
16 doubleFollow, 13 doubleFollow,
17 flushAndRunMultipleServers, 14 flushAndRunMultipleServers,
18 flushTests, 15 flushTests,
19 getLocalVideos, 16 getAccountPlaylistsList,
20 getVideo, 17 getAccountPlaylistsListWithToken,
21 getVideoChannelsList, 18 getPlaylistVideos,
22 getVideosList, 19 getVideoChannelPlaylistsList,
20 getVideoPlaylist,
21 getVideoPlaylistsList,
22 getVideoPlaylistWithToken,
23 killallServers, 23 killallServers,
24 rateVideo, 24 removeUser,
25 removeVideo, 25 removeVideoFromPlaylist,
26 reorderVideosPlaylist,
26 ServerInfo, 27 ServerInfo,
27 setAccessTokensToServers, 28 setAccessTokensToServers,
29 setDefaultVideoChannel,
28 testImage, 30 testImage,
29 updateVideo, 31 unfollow,
32 updateVideoPlaylist,
33 updateVideoPlaylistElement,
30 uploadVideo, 34 uploadVideo,
35 uploadVideoAndGetId,
31 userLogin, 36 userLogin,
32 viewVideo, 37 waitJobs
33 wait,
34 webtorrentAdd
35} from '../../../../shared/utils' 38} from '../../../../shared/utils'
36import { 39import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
37 addVideoCommentReply, 40import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model'
38 addVideoCommentThread, 41import { Video } from '../../../../shared/models/videos'
39 deleteVideoComment, 42import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
40 getVideoCommentThreads,
41 getVideoThreadComments
42} from '../../../../shared/utils/videos/video-comments'
43import { waitJobs } from '../../../../shared/utils/server/jobs'
44 43
45const expect = chai.expect 44const expect = chai.expect
46 45
47describe('Test video playlists', function () { 46describe('Test video playlists', function () {
48 let servers: ServerInfo[] = [] 47 let servers: ServerInfo[] = []
49 48
49 let playlistServer2Id1: number
50 let playlistServer2Id2: number
51 let playlistServer2UUID2: number
52
53 let playlistServer1Id: number
54 let playlistServer1UUID: string
55
56 let nsfwVideoServer1: number
57
50 before(async function () { 58 before(async function () {
51 this.timeout(120000) 59 this.timeout(120000)
52 60
53 servers = await flushAndRunMultipleServers(3) 61 servers = await flushAndRunMultipleServers(3, { transcoding: { enabled: false } })
54 62
55 // Get the access tokens 63 // Get the access tokens
56 await setAccessTokensToServers(servers) 64 await setAccessTokensToServers(servers)
65 await setDefaultVideoChannel(servers)
57 66
58 // Server 1 and server 2 follow each other 67 // Server 1 and server 2 follow each other
59 await doubleFollow(servers[0], servers[1]) 68 await doubleFollow(servers[0], servers[1])
60 // Server 1 and server 3 follow each other 69 // Server 1 and server 3 follow each other
61 await doubleFollow(servers[0], servers[2]) 70 await doubleFollow(servers[0], servers[2])
71
72 {
73 const serverPromises: Promise<any>[][] = []
74
75 for (const server of servers) {
76 const videoPromises: Promise<any>[] = []
77
78 for (let i = 0; i < 7; i++) {
79 videoPromises.push(
80 uploadVideo(server.url, server.accessToken, { name: `video ${i} server ${server.serverNumber}`, nsfw: false })
81 .then(res => res.body.video)
82 )
83 }
84
85 serverPromises.push(videoPromises)
86 }
87
88 servers[0].videos = await Promise.all(serverPromises[0])
89 servers[1].videos = await Promise.all(serverPromises[1])
90 servers[2].videos = await Promise.all(serverPromises[2])
91 }
92
93 nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id
94
95 await waitJobs(servers)
62 }) 96 })
63 97
64 it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { 98 it('Should list watch later playlist', async function () {
99 const url = servers[ 0 ].url
100 const accessToken = servers[ 0 ].accessToken
101
102 {
103 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
104
105 expect(res.body.total).to.equal(1)
106 expect(res.body.data).to.have.lengthOf(1)
65 107
108 const playlist: VideoPlaylist = res.body.data[ 0 ]
109 expect(playlist.displayName).to.equal('Watch later')
110 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
111 expect(playlist.type.label).to.equal('Watch later')
112 }
113
114 {
115 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR)
116
117 expect(res.body.total).to.equal(0)
118 expect(res.body.data).to.have.lengthOf(0)
119 }
120
121 {
122 const res = await getAccountPlaylistsList(url, 'root', 0, 5)
123 expect(res.body.total).to.equal(0)
124 expect(res.body.data).to.have.lengthOf(0)
125 }
126 })
127
128 it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
129 this.timeout(30000)
130
131 await createVideoPlaylist({
132 url: servers[0].url,
133 token: servers[0].accessToken,
134 playlistAttrs: {
135 displayName: 'my super playlist',
136 privacy: VideoPlaylistPrivacy.PUBLIC,
137 description: 'my super description',
138 thumbnailfile: 'thumbnail.jpg',
139 videoChannelId: servers[0].videoChannel.id
140 }
141 })
142
143 await waitJobs(servers)
144
145 for (const server of servers) {
146 const res = await getVideoPlaylistsList(server.url, 0, 5)
147 expect(res.body.total).to.equal(1)
148 expect(res.body.data).to.have.lengthOf(1)
149
150 const playlistFromList = res.body.data[0] as VideoPlaylist
151
152 const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
153 const playlistFromGet = res2.body
154
155 for (const playlist of [ playlistFromGet, playlistFromList ]) {
156 expect(playlist.id).to.be.a('number')
157 expect(playlist.uuid).to.be.a('string')
158
159 expect(playlist.isLocal).to.equal(server.serverNumber === 1)
160
161 expect(playlist.displayName).to.equal('my super playlist')
162 expect(playlist.description).to.equal('my super description')
163 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
164 expect(playlist.privacy.label).to.equal('Public')
165 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
166 expect(playlist.type.label).to.equal('Regular')
167
168 expect(playlist.videosLength).to.equal(0)
169
170 expect(playlist.ownerAccount.name).to.equal('root')
171 expect(playlist.ownerAccount.displayName).to.equal('root')
172 expect(playlist.videoChannel.name).to.equal('root_channel')
173 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
174 }
175 }
66 }) 176 })
67 177
68 it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { 178 it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
69 // create 2 playlists (with videos and no videos) 179 this.timeout(30000)
70 // With thumbnail and no thumbnail 180
181 {
182 const res = await createVideoPlaylist({
183 url: servers[1].url,
184 token: servers[1].accessToken,
185 playlistAttrs: {
186 displayName: 'playlist 2',
187 privacy: VideoPlaylistPrivacy.PUBLIC
188 }
189 })
190 playlistServer2Id1 = res.body.videoPlaylist.id
191 }
192
193 {
194 const res = await createVideoPlaylist({
195 url: servers[ 1 ].url,
196 token: servers[ 1 ].accessToken,
197 playlistAttrs: {
198 displayName: 'playlist 3',
199 privacy: VideoPlaylistPrivacy.PUBLIC,
200 thumbnailfile: 'thumbnail.jpg'
201 }
202 })
203
204 playlistServer2Id2 = res.body.videoPlaylist.id
205 playlistServer2UUID2 = res.body.videoPlaylist.uuid
206 }
207
208 for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) {
209 await addVideoInPlaylist({
210 url: servers[ 1 ].url,
211 token: servers[ 1 ].accessToken,
212 playlistId: id,
213 elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 }
214 })
215 await addVideoInPlaylist({
216 url: servers[ 1 ].url,
217 token: servers[ 1 ].accessToken,
218 playlistId: id,
219 elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id }
220 })
221 }
222
223 await waitJobs(servers)
224
225 for (const server of [ servers[0], servers[1] ]) {
226 const res = await getVideoPlaylistsList(server.url, 0, 5)
227
228 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
229 expect(playlist2).to.not.be.undefined
230 await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
231
232 const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3')
233 expect(playlist3).to.not.be.undefined
234 await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
235 }
236
237 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
238 expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
239 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
71 }) 240 })
72 241
73 it('Should have the playlist on server 3 after a new follow', async function () { 242 it('Should have the playlist on server 3 after a new follow', async function () {
243 this.timeout(30000)
244
74 // Server 2 and server 3 follow each other 245 // Server 2 and server 3 follow each other
75 await doubleFollow(servers[1], servers[2]) 246 await doubleFollow(servers[1], servers[2])
247
248 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
249
250 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
251 expect(playlist2).to.not.be.undefined
252 await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
253
254 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
76 }) 255 })
77 256
78 it('Should create some playlists and list them correctly', async function () { 257 it('Should correctly list the playlists', async function () {
79 // create 3 playlists with some videos in it 258 this.timeout(30000)
80 // check pagination 259
81 // check sort 260 {
82 // check empty 261 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt')
262
263 expect(res.body.total).to.equal(3)
264
265 const data: VideoPlaylist[] = res.body.data
266 expect(data).to.have.lengthOf(2)
267 expect(data[ 0 ].displayName).to.equal('playlist 2')
268 expect(data[ 1 ].displayName).to.equal('playlist 3')
269 }
270
271 {
272 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt')
273
274 expect(res.body.total).to.equal(3)
275
276 const data: VideoPlaylist[] = res.body.data
277 expect(data).to.have.lengthOf(2)
278 expect(data[ 0 ].displayName).to.equal('playlist 2')
279 expect(data[ 1 ].displayName).to.equal('my super playlist')
280 }
83 }) 281 })
84 282
85 it('Should list video channel playlists', async function () { 283 it('Should list video channel playlists', async function () {
86 // check pagination 284 this.timeout(30000)
87 // check sort 285
88 // check empty 286 {
287 const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt')
288
289 expect(res.body.total).to.equal(1)
290
291 const data: VideoPlaylist[] = res.body.data
292 expect(data).to.have.lengthOf(1)
293 expect(data[ 0 ].displayName).to.equal('my super playlist')
294 }
89 }) 295 })
90 296
91 it('Should list account playlists', async function () { 297 it('Should list account playlists', async function () {
92 // check pagination 298 this.timeout(30000)
93 // check sort 299
94 // check empty 300 {
301 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt')
302
303 expect(res.body.total).to.equal(2)
304
305 const data: VideoPlaylist[] = res.body.data
306 expect(data).to.have.lengthOf(1)
307 expect(data[ 0 ].displayName).to.equal('playlist 2')
308 }
309
310 {
311 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt')
312
313 expect(res.body.total).to.equal(2)
314
315 const data: VideoPlaylist[] = res.body.data
316 expect(data).to.have.lengthOf(1)
317 expect(data[ 0 ].displayName).to.equal('playlist 3')
318 }
95 }) 319 })
96 320
97 it('Should get a playlist', async function () { 321 it('Should not list unlisted or private playlists', async function () {
98 // get empty playlist 322 this.timeout(30000)
99 // get non empty playlist 323
324 await createVideoPlaylist({
325 url: servers[ 1 ].url,
326 token: servers[ 1 ].accessToken,
327 playlistAttrs: {
328 displayName: 'playlist unlisted',
329 privacy: VideoPlaylistPrivacy.UNLISTED
330 }
331 })
332
333 await createVideoPlaylist({
334 url: servers[ 1 ].url,
335 token: servers[ 1 ].accessToken,
336 playlistAttrs: {
337 displayName: 'playlist private',
338 privacy: VideoPlaylistPrivacy.PRIVATE
339 }
340 })
341
342 await waitJobs(servers)
343
344 for (const server of servers) {
345 const results = [
346 await getAccountPlaylistsList(server.url, 'root@localhost:9002', 0, 5, '-createdAt'),
347 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
348 ]
349
350 expect(results[0].body.total).to.equal(2)
351 expect(results[1].body.total).to.equal(3)
352
353 for (const res of results) {
354 const data: VideoPlaylist[] = res.body.data
355 expect(data).to.have.lengthOf(2)
356 expect(data[ 0 ].displayName).to.equal('playlist 3')
357 expect(data[ 1 ].displayName).to.equal('playlist 2')
358 }
359 }
100 }) 360 })
101 361
102 it('Should update a playlist', async function () { 362 it('Should update a playlist', async function () {
103 // update thumbnail 363 this.timeout(30000)
104 364
105 // update other details 365 await updateVideoPlaylist({
366 url: servers[1].url,
367 token: servers[1].accessToken,
368 playlistAttrs: {
369 displayName: 'playlist 3 updated',
370 description: 'description updated',
371 privacy: VideoPlaylistPrivacy.UNLISTED,
372 thumbnailfile: 'thumbnail.jpg',
373 videoChannelId: servers[1].videoChannel.id
374 },
375 playlistId: playlistServer2Id2
376 })
377
378 await waitJobs(servers)
379
380 for (const server of servers) {
381 const res = await getVideoPlaylist(server.url, playlistServer2UUID2)
382 const playlist: VideoPlaylist = res.body
383
384 expect(playlist.displayName).to.equal('playlist 3 updated')
385 expect(playlist.description).to.equal('description updated')
386
387 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
388 expect(playlist.privacy.label).to.equal('Unlisted')
389
390 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
391 expect(playlist.type.label).to.equal('Regular')
392
393 expect(playlist.videosLength).to.equal(2)
394
395 expect(playlist.ownerAccount.name).to.equal('root')
396 expect(playlist.ownerAccount.displayName).to.equal('root')
397 expect(playlist.videoChannel.name).to.equal('root_channel')
398 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
399 }
106 }) 400 })
107 401
108 it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { 402 it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
403 this.timeout(30000)
404
405 const addVideo = (elementAttrs: any) => {
406 return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
407 }
109 408
409 const res = await createVideoPlaylist({
410 url: servers[ 0 ].url,
411 token: servers[ 0 ].accessToken,
412 playlistAttrs: {
413 displayName: 'playlist 4',
414 privacy: VideoPlaylistPrivacy.PUBLIC
415 }
416 })
417
418 playlistServer1Id = res.body.videoPlaylist.id
419 playlistServer1UUID = res.body.videoPlaylist.uuid
420
421 await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
422 await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
423 await addVideo({ videoId: servers[2].videos[2].uuid })
424 await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
425 await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
426 await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
427
428 await waitJobs(servers)
110 }) 429 })
111 430
112 it('Should correctly list playlist videos', async function () { 431 it('Should correctly list playlist videos', async function () {
113 // empty 432 this.timeout(30000)
114 // some filters? 433
434 for (const server of servers) {
435 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
436
437 expect(res.body.total).to.equal(6)
438
439 const videos: Video[] = res.body.data
440 expect(videos).to.have.lengthOf(6)
441
442 expect(videos[0].name).to.equal('video 0 server 1')
443 expect(videos[0].playlistElement.position).to.equal(1)
444 expect(videos[0].playlistElement.startTimestamp).to.equal(15)
445 expect(videos[0].playlistElement.stopTimestamp).to.equal(28)
446
447 expect(videos[1].name).to.equal('video 1 server 3')
448 expect(videos[1].playlistElement.position).to.equal(2)
449 expect(videos[1].playlistElement.startTimestamp).to.equal(35)
450 expect(videos[1].playlistElement.stopTimestamp).to.be.null
451
452 expect(videos[2].name).to.equal('video 2 server 3')
453 expect(videos[2].playlistElement.position).to.equal(3)
454 expect(videos[2].playlistElement.startTimestamp).to.be.null
455 expect(videos[2].playlistElement.stopTimestamp).to.be.null
456
457 expect(videos[3].name).to.equal('video 3 server 1')
458 expect(videos[3].playlistElement.position).to.equal(4)
459 expect(videos[3].playlistElement.startTimestamp).to.be.null
460 expect(videos[3].playlistElement.stopTimestamp).to.equal(35)
461
462 expect(videos[4].name).to.equal('video 4 server 1')
463 expect(videos[4].playlistElement.position).to.equal(5)
464 expect(videos[4].playlistElement.startTimestamp).to.equal(45)
465 expect(videos[4].playlistElement.stopTimestamp).to.equal(60)
466
467 expect(videos[5].name).to.equal('NSFW video')
468 expect(videos[5].playlistElement.position).to.equal(6)
469 expect(videos[5].playlistElement.startTimestamp).to.equal(5)
470 expect(videos[5].playlistElement.stopTimestamp).to.be.null
471
472 const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false })
473 expect(res2.body.total).to.equal(5)
474 expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined
475
476 const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
477 expect(res3.body.data).to.have.lengthOf(2)
478 }
115 }) 479 })
116 480
117 it('Should reorder the playlist', async function () { 481 it('Should reorder the playlist', async function () {
118 // reorder 1 element 482 this.timeout(30000)
119 // reorder 3 elements 483
120 // reorder at the beginning 484 {
121 // reorder at the end 485 await reorderVideosPlaylist({
122 // reorder before/after 486 url: servers[ 0 ].url,
487 token: servers[ 0 ].accessToken,
488 playlistId: playlistServer1Id,
489 elementAttrs: {
490 startPosition: 2,
491 insertAfterPosition: 3
492 }
493 })
494
495 await waitJobs(servers)
496
497 for (const server of servers) {
498 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
499 const names = res.body.data.map(v => v.name)
500
501 expect(names).to.deep.equal([
502 'video 0 server 1',
503 'video 2 server 3',
504 'video 1 server 3',
505 'video 3 server 1',
506 'video 4 server 1',
507 'NSFW video'
508 ])
509 }
510 }
511
512 {
513 await reorderVideosPlaylist({
514 url: servers[0].url,
515 token: servers[0].accessToken,
516 playlistId: playlistServer1Id,
517 elementAttrs: {
518 startPosition: 1,
519 reorderLength: 3,
520 insertAfterPosition: 4
521 }
522 })
523
524 await waitJobs(servers)
525
526 for (const server of servers) {
527 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
528 const names = res.body.data.map(v => v.name)
529
530 expect(names).to.deep.equal([
531 'video 3 server 1',
532 'video 0 server 1',
533 'video 2 server 3',
534 'video 1 server 3',
535 'video 4 server 1',
536 'NSFW video'
537 ])
538 }
539 }
540
541 {
542 await reorderVideosPlaylist({
543 url: servers[0].url,
544 token: servers[0].accessToken,
545 playlistId: playlistServer1Id,
546 elementAttrs: {
547 startPosition: 6,
548 insertAfterPosition: 3
549 }
550 })
551
552 await waitJobs(servers)
553
554 for (const server of servers) {
555 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
556 const videos: Video[] = res.body.data
557
558 const names = videos.map(v => v.name)
559
560 expect(names).to.deep.equal([
561 'video 3 server 1',
562 'video 0 server 1',
563 'video 2 server 3',
564 'NSFW video',
565 'video 1 server 3',
566 'video 4 server 1'
567 ])
568
569 for (let i = 1; i <= videos.length; i++) {
570 expect(videos[i - 1].playlistElement.position).to.equal(i)
571 }
572 }
573 }
123 }) 574 })
124 575
125 it('Should update startTimestamp/endTimestamp of some elements', async function () { 576 it('Should update startTimestamp/endTimestamp of some elements', async function () {
126 577 this.timeout(30000)
578
579 await updateVideoPlaylistElement({
580 url: servers[0].url,
581 token: servers[0].accessToken,
582 playlistId: playlistServer1Id,
583 videoId: servers[0].videos[3].uuid,
584 elementAttrs: {
585 startTimestamp: 1
586 }
587 })
588
589 await updateVideoPlaylistElement({
590 url: servers[0].url,
591 token: servers[0].accessToken,
592 playlistId: playlistServer1Id,
593 videoId: servers[0].videos[4].uuid,
594 elementAttrs: {
595 stopTimestamp: null
596 }
597 })
598
599 await waitJobs(servers)
600
601 for (const server of servers) {
602 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
603 const videos: Video[] = res.body.data
604
605 expect(videos[0].name).to.equal('video 3 server 1')
606 expect(videos[0].playlistElement.position).to.equal(1)
607 expect(videos[0].playlistElement.startTimestamp).to.equal(1)
608 expect(videos[0].playlistElement.stopTimestamp).to.equal(35)
609
610 expect(videos[5].name).to.equal('video 4 server 1')
611 expect(videos[5].playlistElement.position).to.equal(6)
612 expect(videos[5].playlistElement.startTimestamp).to.equal(45)
613 expect(videos[5].playlistElement.stopTimestamp).to.be.null
614 }
127 }) 615 })
128 616
129 it('Should delete some elements', async function () { 617 it('Should delete some elements', async function () {
618 this.timeout(30000)
619
620 await removeVideoFromPlaylist({
621 url: servers[0].url,
622 token: servers[0].accessToken,
623 playlistId: playlistServer1Id,
624 videoId: servers[0].videos[3].uuid
625 })
626
627 await removeVideoFromPlaylist({
628 url: servers[0].url,
629 token: servers[0].accessToken,
630 playlistId: playlistServer1Id,
631 videoId: nsfwVideoServer1
632 })
633
634 await waitJobs(servers)
635
636 for (const server of servers) {
637 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
638
639 expect(res.body.total).to.equal(4)
640
641 const videos: Video[] = res.body.data
642 expect(videos).to.have.lengthOf(4)
130 643
644 expect(videos[ 0 ].name).to.equal('video 0 server 1')
645 expect(videos[ 0 ].playlistElement.position).to.equal(1)
646
647 expect(videos[ 1 ].name).to.equal('video 2 server 3')
648 expect(videos[ 1 ].playlistElement.position).to.equal(2)
649
650 expect(videos[ 2 ].name).to.equal('video 1 server 3')
651 expect(videos[ 2 ].playlistElement.position).to.equal(3)
652
653 expect(videos[ 3 ].name).to.equal('video 4 server 1')
654 expect(videos[ 3 ].playlistElement.position).to.equal(4)
655 }
131 }) 656 })
132 657
133 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { 658 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
659 this.timeout(30000)
134 660
661 await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
662
663 await waitJobs(servers)
664
665 for (const server of servers) {
666 await getVideoPlaylist(server.url, playlistServer1UUID, 404)
667 }
135 }) 668 })
136 669
137 it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { 670 it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
671 this.timeout(30000)
138 672
673 for (const server of servers) {
674 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.serverNumber)
675 }
139 }) 676 })
140 677
141 it('Should unfollow servers 1 and 2 and hide their playlists', async function () { 678 it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
679 this.timeout(30000)
142 680
681 const finder = data => data.find(p => p.displayName === 'my super playlist')
682
683 {
684 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
685 expect(res.body.total).to.equal(2)
686 expect(finder(res.body.data)).to.not.be.undefined
687 }
688
689 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
690
691 {
692 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
693 expect(res.body.total).to.equal(1)
694
695 expect(finder(res.body.data)).to.be.undefined
696 }
143 }) 697 })
144 698
145 it('Should delete a channel and remove the associated playlist', async function () { 699 it('Should delete a channel and put the associated playlist in private mode', async function () {
700 this.timeout(30000)
701
702 const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
703 const videoChannelId = res.body.videoChannel.id
146 704
705 const res2 = await createVideoPlaylist({
706 url: servers[0].url,
707 token: servers[0].accessToken,
708 playlistAttrs: {
709 displayName: 'channel playlist',
710 privacy: VideoPlaylistPrivacy.PUBLIC,
711 videoChannelId
712 }
713 })
714 const videoPlaylistUUID = res2.body.videoPlaylist.uuid
715
716 await waitJobs(servers)
717
718 await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
719
720 await waitJobs(servers)
721
722 const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
723 expect(res3.body.displayName).to.equal('channel playlist')
724 expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
725
726 await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404)
147 }) 727 })
148 728
149 it('Should delete an account and delete its playlists', async function () { 729 it('Should delete an account and delete its playlists', async function () {
730 this.timeout(30000)
731
732 const user = { username: 'user_1', password: 'password' }
733 const res = await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
734
735 const userId = res.body.user.id
736 const userAccessToken = await userLogin(servers[0], user)
150 737
738 await createVideoPlaylist({
739 url: servers[0].url,
740 token: userAccessToken,
741 playlistAttrs: {
742 displayName: 'playlist to be deleted',
743 privacy: VideoPlaylistPrivacy.PUBLIC
744 }
745 })
746
747 await waitJobs(servers)
748
749 const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
750
751 {
752 for (const server of [ servers[0], servers[1] ]) {
753 const res = await getVideoPlaylistsList(server.url, 0, 15)
754 expect(finder(res.body.data)).to.not.be.undefined
755 }
756 }
757
758 await removeUser(servers[0].url, userId, servers[0].accessToken)
759 await waitJobs(servers)
760
761 {
762 for (const server of [ servers[0], servers[1] ]) {
763 const res = await getVideoPlaylistsList(server.url, 0, 15)
764 expect(finder(res.body.data)).to.be.undefined
765 }
766 }
151 }) 767 })
152 768
153 after(async function () { 769 after(async function () {
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg
new file mode 100644
index 000000000..19db4f18c
--- /dev/null
+++ b/server/tests/fixtures/thumbnail-playlist.jpg
Binary files differ
diff --git a/shared/models/activitypub/objects/playlist-object.ts b/shared/models/activitypub/objects/playlist-object.ts
index 5f6733f92..c11a23a69 100644
--- a/shared/models/activitypub/objects/playlist-object.ts
+++ b/shared/models/activitypub/objects/playlist-object.ts
@@ -13,6 +13,9 @@ export interface PlaylistObject {
13 13
14 icon: ActivityIconObject 14 icon: ActivityIconObject
15 15
16 published: string
17 updated: string
18
16 orderedItems?: string[] 19 orderedItems?: string[]
17 20
18 partOf?: string 21 partOf?: string
diff --git a/shared/models/videos/playlist/video-playlist-type.model.ts b/shared/models/videos/playlist/video-playlist-type.model.ts
new file mode 100644
index 000000000..49233b743
--- /dev/null
+++ b/shared/models/videos/playlist/video-playlist-type.model.ts
@@ -0,0 +1,4 @@
1export enum VideoPlaylistType {
2 REGULAR = 1,
3 WATCH_LATER = 2
4}
diff --git a/shared/models/videos/playlist/video-playlist.model.ts b/shared/models/videos/playlist/video-playlist.model.ts
index 6aa04048c..7fec0e42b 100644
--- a/shared/models/videos/playlist/video-playlist.model.ts
+++ b/shared/models/videos/playlist/video-playlist.model.ts
@@ -1,6 +1,7 @@
1import { AccountSummary } from '../../actors/index' 1import { AccountSummary } from '../../actors/index'
2import { VideoChannelSummary, VideoConstant } from '..' 2import { VideoChannelSummary, VideoConstant } from '..'
3import { VideoPlaylistPrivacy } from './video-playlist-privacy.model' 3import { VideoPlaylistPrivacy } from './video-playlist-privacy.model'
4import { VideoPlaylistType } from './video-playlist-type.model'
4 5
5export interface VideoPlaylist { 6export interface VideoPlaylist {
6 id: number 7 id: number
@@ -15,6 +16,8 @@ export interface VideoPlaylist {
15 16
16 videosLength: number 17 videosLength: number
17 18
19 type: VideoConstant<VideoPlaylistType>
20
18 createdAt: Date | string 21 createdAt: Date | string
19 updatedAt: Date | string 22 updatedAt: Date | string
20 23
diff --git a/shared/utils/requests/requests.ts b/shared/utils/requests/requests.ts
index dc2d4abe5..3532fb429 100644
--- a/shared/utils/requests/requests.ts
+++ b/shared/utils/requests/requests.ts
@@ -77,6 +77,8 @@ function makeUploadRequest (options: {
77 Object.keys(options.fields).forEach(field => { 77 Object.keys(options.fields).forEach(field => {
78 const value = options.fields[field] 78 const value = options.fields[field]
79 79
80 if (value === undefined) return
81
80 if (Array.isArray(value)) { 82 if (Array.isArray(value)) {
81 for (let i = 0; i < value.length; i++) { 83 for (let i = 0; i < value.length; i++) {
82 req.field(field + '[' + i + ']', value[i]) 84 req.field(field + '[' + i + ']', value[i])
diff --git a/shared/utils/server/servers.ts b/shared/utils/server/servers.ts
index bde7dd5c2..5288d253a 100644
--- a/shared/utils/server/servers.ts
+++ b/shared/utils/server/servers.ts
@@ -6,6 +6,7 @@ import { root, wait } from '../miscs/miscs'
6import { readdir, readFile } from 'fs-extra' 6import { readdir, readFile } from 'fs-extra'
7import { existsSync } from 'fs' 7import { existsSync } from 'fs'
8import { expect } from 'chai' 8import { expect } from 'chai'
9import { VideoChannel } from '../../models/videos'
9 10
10interface ServerInfo { 11interface ServerInfo {
11 app: ChildProcess, 12 app: ChildProcess,
@@ -25,6 +26,7 @@ interface ServerInfo {
25 } 26 }
26 27
27 accessToken?: string 28 accessToken?: string
29 videoChannel?: VideoChannel
28 30
29 video?: { 31 video?: {
30 id: number 32 id: number
@@ -39,6 +41,8 @@ interface ServerInfo {
39 id: number 41 id: number
40 uuid: string 42 uuid: string
41 } 43 }
44
45 videos?: { id: number, uuid: string }[]
42} 46}
43 47
44function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) { 48function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts
index 7191b263e..e3c14a4a3 100644
--- a/shared/utils/users/users.ts
+++ b/shared/utils/users/users.ts
@@ -3,6 +3,7 @@ import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '..
3 3
4import { UserRole } from '../../index' 4import { UserRole } from '../../index'
5import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type' 5import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
6import { ServerInfo, userLogin } from '..'
6 7
7function createUser ( 8function createUser (
8 url: string, 9 url: string,
@@ -32,6 +33,13 @@ function createUser (
32 .expect(specialStatus) 33 .expect(specialStatus)
33} 34}
34 35
36async function generateUserAccessToken (server: ServerInfo, username: string) {
37 const password = 'my super password'
38 await createUser(server.url, server.accessToken, username, password)
39
40 return userLogin(server, { username, password })
41}
42
35function registerUser (url: string, username: string, password: string, specialStatus = 204) { 43function registerUser (url: string, username: string, password: string, specialStatus = 204) {
36 const path = '/api/v1/users/register' 44 const path = '/api/v1/users/register'
37 const body = { 45 const body = {
@@ -300,5 +308,6 @@ export {
300 resetPassword, 308 resetPassword,
301 updateMyAvatar, 309 updateMyAvatar,
302 askSendVerifyEmail, 310 askSendVerifyEmail,
311 generateUserAccessToken,
303 verifyEmail 312 verifyEmail
304} 313}
diff --git a/shared/utils/videos/video-channels.ts b/shared/utils/videos/video-channels.ts
index 3935c261e..93a257bf9 100644
--- a/shared/utils/videos/video-channels.ts
+++ b/shared/utils/videos/video-channels.ts
@@ -1,6 +1,8 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos' 2import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
3import { updateAvatarRequest } from '../requests/requests' 3import { updateAvatarRequest } from '../requests/requests'
4import { getMyUserInformation, ServerInfo } from '..'
5import { User } from '../..'
4 6
5function getVideoChannelsList (url: string, start: number, count: number, sort?: string) { 7function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
6 const path = '/api/v1/video-channels' 8 const path = '/api/v1/video-channels'
@@ -105,6 +107,19 @@ function updateVideoChannelAvatar (options: {
105 return updateAvatarRequest(Object.assign(options, { path })) 107 return updateAvatarRequest(Object.assign(options, { path }))
106} 108}
107 109
110function setDefaultVideoChannel (servers: ServerInfo[]) {
111 const tasks: Promise<any>[] = []
112
113 for (const server of servers) {
114 const p = getMyUserInformation(server.url, server.accessToken)
115 .then(res => server.videoChannel = (res.body as User).videoChannels[0])
116
117 tasks.push(p)
118 }
119
120 return Promise.all(tasks)
121}
122
108// --------------------------------------------------------------------------- 123// ---------------------------------------------------------------------------
109 124
110export { 125export {
@@ -114,5 +129,6 @@ export {
114 addVideoChannel, 129 addVideoChannel,
115 updateVideoChannel, 130 updateVideoChannel,
116 deleteVideoChannel, 131 deleteVideoChannel,
117 getVideoChannel 132 getVideoChannel,
133 setDefaultVideoChannel
118} 134}
diff --git a/shared/utils/videos/video-playlists.ts b/shared/utils/videos/video-playlists.ts
index 21285688a..4af52ec0f 100644
--- a/shared/utils/videos/video-playlists.ts
+++ b/shared/utils/videos/video-playlists.ts
@@ -4,6 +4,12 @@ import { omit } from 'lodash'
4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model' 4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model' 5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model' 6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
7import { videoUUIDToId } from './videos'
8import { join } from 'path'
9import { root } from '..'
10import { readdir } from 'fs-extra'
11import { expect } from 'chai'
12import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
7 13
8function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) { 14function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
9 const path = '/api/v1/video-playlists' 15 const path = '/api/v1/video-playlists'
@@ -17,7 +23,67 @@ function getVideoPlaylistsList (url: string, start: number, count: number, sort?
17 return makeGetRequest({ 23 return makeGetRequest({
18 url, 24 url,
19 path, 25 path,
20 query 26 query,
27 statusCodeExpected: 200
28 })
29}
30
31function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
32 const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
33
34 const query = {
35 start,
36 count,
37 sort
38 }
39
40 return makeGetRequest({
41 url,
42 path,
43 query,
44 statusCodeExpected: 200
45 })
46}
47
48function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string) {
49 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
50
51 const query = {
52 start,
53 count,
54 sort
55 }
56
57 return makeGetRequest({
58 url,
59 path,
60 query,
61 statusCodeExpected: 200
62 })
63}
64
65function getAccountPlaylistsListWithToken (
66 url: string,
67 token: string,
68 accountName: string,
69 start: number,
70 count: number,
71 playlistType?: VideoPlaylistType
72) {
73 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
74
75 const query = {
76 start,
77 count,
78 playlistType
79 }
80
81 return makeGetRequest({
82 url,
83 token,
84 path,
85 query,
86 statusCodeExpected: 200
21 }) 87 })
22} 88}
23 89
@@ -31,6 +97,17 @@ function getVideoPlaylist (url: string, playlistId: number | string, statusCodeE
31 }) 97 })
32} 98}
33 99
100function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = 200) {
101 const path = '/api/v1/video-playlists/' + playlistId
102
103 return makeGetRequest({
104 url,
105 token,
106 path,
107 statusCodeExpected
108 })
109}
110
34function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) { 111function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) {
35 const path = '/api/v1/video-playlists/' + playlistId 112 const path = '/api/v1/video-playlists/' + playlistId
36 113
@@ -93,13 +170,15 @@ function updateVideoPlaylist (options: {
93 }) 170 })
94} 171}
95 172
96function addVideoInPlaylist (options: { 173async function addVideoInPlaylist (options: {
97 url: string, 174 url: string,
98 token: string, 175 token: string,
99 playlistId: number | string, 176 playlistId: number | string,
100 elementAttrs: VideoPlaylistElementCreate 177 elementAttrs: VideoPlaylistElementCreate | { videoId: string }
101 expectedStatus?: number 178 expectedStatus?: number
102}) { 179}) {
180 options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
181
103 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos' 182 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
104 183
105 return makePostBodyRequest({ 184 return makePostBodyRequest({
@@ -135,7 +214,7 @@ function removeVideoFromPlaylist (options: {
135 token: string, 214 token: string,
136 playlistId: number | string, 215 playlistId: number | string,
137 videoId: number | string, 216 videoId: number | string,
138 expectedStatus: number 217 expectedStatus?: number
139}) { 218}) {
140 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId 219 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
141 220
@@ -156,7 +235,7 @@ function reorderVideosPlaylist (options: {
156 insertAfterPosition: number, 235 insertAfterPosition: number,
157 reorderLength?: number 236 reorderLength?: number
158 }, 237 },
159 expectedStatus: number 238 expectedStatus?: number
160}) { 239}) {
161 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder' 240 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
162 241
@@ -165,15 +244,37 @@ function reorderVideosPlaylist (options: {
165 path, 244 path,
166 token: options.token, 245 token: options.token,
167 fields: options.elementAttrs, 246 fields: options.elementAttrs,
168 statusCodeExpected: options.expectedStatus 247 statusCodeExpected: options.expectedStatus || 204
169 }) 248 })
170} 249}
171 250
251async function checkPlaylistFilesWereRemoved (
252 playlistUUID: string,
253 serverNumber: number,
254 directories = [ 'thumbnails' ]
255) {
256 const testDirectory = 'test' + serverNumber
257
258 for (const directory of directories) {
259 const directoryPath = join(root(), testDirectory, directory)
260
261 const files = await readdir(directoryPath)
262 for (const file of files) {
263 expect(file).to.not.contain(playlistUUID)
264 }
265 }
266}
267
172// --------------------------------------------------------------------------- 268// ---------------------------------------------------------------------------
173 269
174export { 270export {
175 getVideoPlaylistsList, 271 getVideoPlaylistsList,
272 getVideoChannelPlaylistsList,
273 getAccountPlaylistsList,
274 getAccountPlaylistsListWithToken,
275
176 getVideoPlaylist, 276 getVideoPlaylist,
277 getVideoPlaylistWithToken,
177 278
178 createVideoPlaylist, 279 createVideoPlaylist,
179 updateVideoPlaylist, 280 updateVideoPlaylist,
@@ -183,5 +284,7 @@ export {
183 updateVideoPlaylistElement, 284 updateVideoPlaylistElement,
184 removeVideoFromPlaylist, 285 removeVideoFromPlaylist,
185 286
186 reorderVideosPlaylist 287 reorderVideosPlaylist,
288
289 checkPlaylistFilesWereRemoved
187} 290}
diff --git a/shared/utils/videos/videos.ts b/shared/utils/videos/videos.ts
index 2c09f0086..16b5165f1 100644
--- a/shared/utils/videos/videos.ts
+++ b/shared/utils/videos/videos.ts
@@ -1,7 +1,7 @@
1/* tslint:disable:no-unused-expression */ 1/* tslint:disable:no-unused-expression */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { existsSync, readdir, readFile } from 'fs-extra' 4import { pathExists, readdir, readFile } from 'fs-extra'
5import * as parseTorrent from 'parse-torrent' 5import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path' 6import { extname, join } from 'path'
7import * as request from 'supertest' 7import * as request from 'supertest'
@@ -16,7 +16,7 @@ import {
16 ServerInfo, 16 ServerInfo,
17 testImage 17 testImage
18} from '../' 18} from '../'
19 19import * as validator from 'validator'
20import { VideoDetails, VideoPrivacy } from '../../models/videos' 20import { VideoDetails, VideoPrivacy } from '../../models/videos'
21import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 21import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
22import { dateIsValid, webtorrentAdd } from '../miscs/miscs' 22import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
@@ -311,8 +311,8 @@ async function checkVideoFilesWereRemoved (
311 for (const directory of directories) { 311 for (const directory of directories) {
312 const directoryPath = join(root(), testDirectory, directory) 312 const directoryPath = join(root(), testDirectory, directory)
313 313
314 const directoryExists = existsSync(directoryPath) 314 const directoryExists = await pathExists(directoryPath)
315 if (!directoryExists) continue 315 if (directoryExists === false) continue
316 316
317 const files = await readdir(directoryPath) 317 const files = await readdir(directoryPath)
318 for (const file of files) { 318 for (const file of files) {
@@ -597,12 +597,30 @@ async function completeVideoCheck (
597 } 597 }
598} 598}
599 599
600async function videoUUIDToId (url: string, id: number | string) {
601 if (validator.isUUID('' + id) === false) return id
602
603 const res = await getVideo(url, id)
604 return res.body.id
605}
606
607async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
608 const videoAttrs: any = { name: options.videoName }
609 if (options.nsfw) videoAttrs.nsfw = options.nsfw
610
611
612 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
613
614 return { id: res.body.video.id, uuid: res.body.video.uuid }
615}
616
600// --------------------------------------------------------------------------- 617// ---------------------------------------------------------------------------
601 618
602export { 619export {
603 getVideoDescription, 620 getVideoDescription,
604 getVideoCategories, 621 getVideoCategories,
605 getVideoLicences, 622 getVideoLicences,
623 videoUUIDToId,
606 getVideoPrivacies, 624 getVideoPrivacies,
607 getVideoLanguages, 625 getVideoLanguages,
608 getMyVideos, 626 getMyVideos,
@@ -624,5 +642,6 @@ export {
624 getLocalVideos, 642 getLocalVideos,
625 completeVideoCheck, 643 completeVideoCheck,
626 checkVideoFilesWereRemoved, 644 checkVideoFilesWereRemoved,
627 getPlaylistVideos 645 getPlaylistVideos,
646 uploadVideoAndGetId
628} 647}