]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add playlist rest tests
authorChocobozzz <me@florianbigard.com>
Tue, 5 Mar 2019 09:58:44 +0000 (10:58 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 18 Mar 2019 10:17:59 +0000 (11:17 +0100)
35 files changed:
server/controllers/activitypub/client.ts
server/controllers/api/accounts.ts
server/controllers/api/users/index.ts
server/controllers/api/video-channel.ts
server/controllers/api/video-playlist.ts
server/helpers/custom-validators/activitypub/playlist.ts
server/helpers/custom-validators/video-playlists.ts
server/initializers/constants.ts
server/initializers/installer.ts
server/lib/activitypub/playlist.ts
server/lib/activitypub/process/process-delete.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-delete.ts
server/lib/activitypub/send/send-update.ts
server/lib/user.ts
server/lib/video-playlist.ts [new file with mode: 0644]
server/middlewares/validators/videos/video-playlists.ts
server/models/video/video-channel.ts
server/models/video/video-playlist-element.ts
server/models/video/video-playlist.ts
server/models/video/video-share.ts
server/models/video/video.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/video-playlists.ts
server/tests/api/videos/video-playlists.ts
server/tests/fixtures/thumbnail-playlist.jpg [new file with mode: 0644]
shared/models/activitypub/objects/playlist-object.ts
shared/models/videos/playlist/video-playlist-type.model.ts [new file with mode: 0644]
shared/models/videos/playlist/video-playlist.model.ts
shared/utils/requests/requests.ts
shared/utils/server/servers.ts
shared/utils/users/users.ts
shared/utils/videos/video-channels.ts
shared/utils/videos/video-playlists.ts
shared/utils/videos/videos.ts

index 59e6c8e9f0369387b455d8d279ddba33211816b9..f616047b03b3b381914bfad90716695e6d30c919 100644 (file)
@@ -320,7 +320,10 @@ async function videoRedundancyController (req: express.Request, res: express.Res
 async function videoPlaylistController (req: express.Request, res: express.Response) {
   const playlist: VideoPlaylistModel = res.locals.videoPlaylist
 
-  const json = await playlist.toActivityPubObject()
+  // We need more attributes
+  playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
+
+  const json = await playlist.toActivityPubObject(req.query.page, null)
   const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
   const object = audiencify(json, audience)
 
index 03c83109233383af1f5c3d2f6835184e4dd7fac8..e24545de83d3f0a390119cd8e87370918a88d0eb 100644 (file)
@@ -18,6 +18,7 @@ import { JobQueue } from '../../lib/job-queue'
 import { logger } from '../../helpers/logger'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
 import { UserModel } from '../../models/account/user'
+import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
 
 const accountsRouter = express.Router()
 
@@ -57,6 +58,7 @@ accountsRouter.get('/:accountName/video-playlists',
   videoPlaylistsSortValidator,
   setDefaultSort,
   setDefaultPagination,
+  commonVideoPlaylistFiltersValidator,
   asyncMiddleware(listAccountPlaylists)
 )
 
@@ -106,7 +108,8 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
     count: req.query.count,
     sort: req.query.sort,
     accountId: res.locals.account.id,
-    privateAndUnlisted
+    privateAndUnlisted,
+    type: req.query.playlistType
   })
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
index 407f32ac06e4a7fbe15653c9e0efdaaf3b046309..5758c822798a7bcf37b51f5b7fa510a022b17b25 100644 (file)
@@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
 import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
 import { Emailer } from '../../../lib/emailer'
 import { Redis } from '../../../lib/redis'
-import { createUserAccountAndChannel } from '../../../lib/user'
+import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
@@ -174,7 +174,7 @@ async function createUser (req: express.Request, res: express.Response) {
     videoQuotaDaily: body.videoQuotaDaily
   })
 
-  const { user, account } = await createUserAccountAndChannel(userToCreate)
+  const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate)
 
   auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
   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) {
     emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
   })
 
-  const { user } = await createUserAccountAndChannel(userToCreate)
+  const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate)
 
   auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
   logger.info('User %s with its channel and account registered.', body.username)
index 534cc8d7b19f67341eaf17903dae05b1492c6118..c13aed4dc80d2f5e2b72ef6592cff5533ec24184 100644 (file)
@@ -33,6 +33,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
 import { UserModel } from '../../models/account/user'
 import { JobQueue } from '../../lib/job-queue'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
+import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
 
 const auditLogger = auditLoggerFactory('channels')
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@@ -85,6 +86,7 @@ videoChannelRouter.get('/:nameWithHost/video-playlists',
   videoPlaylistsSortValidator,
   setDefaultSort,
   setDefaultPagination,
+  commonVideoPlaylistFiltersValidator,
   asyncMiddleware(listVideoChannelPlaylists)
 )
 
@@ -197,6 +199,8 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
   const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
 
   await sequelizeTypescript.transaction(async t => {
+    await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
+
     await videoChannelInstance.destroy({ transaction: t })
 
     auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
@@ -225,7 +229,8 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
-    videoChannelId: res.locals.videoChannel.id
+    videoChannelId: res.locals.videoChannel.id,
+    type: req.query.playlistType
   })
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
index e026b4d16dc5dbba5b54d2c54715325bc113c8d8..8605f3dfc1e752b436f4a44adf48809a7b149fa1 100644 (file)
@@ -17,6 +17,7 @@ import { logger } from '../../helpers/logger'
 import { resetSequelizeInstance } from '../../helpers/database-utils'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
 import {
+  commonVideoPlaylistFiltersValidator,
   videoPlaylistsAddValidator,
   videoPlaylistsAddVideoValidator,
   videoPlaylistsDeleteValidator,
@@ -45,6 +46,7 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
 import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
 import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
 import { copy, pathExists } from 'fs-extra'
+import { AccountModel } from '../../models/account/account'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
 
@@ -55,6 +57,7 @@ videoPlaylistRouter.get('/',
   videoPlaylistsSortValidator,
   setDefaultSort,
   setDefaultPagination,
+  commonVideoPlaylistFiltersValidator,
   asyncMiddleware(listVideoPlaylists)
 )
 
@@ -130,7 +133,8 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
     followerActorId: serverActor.id,
     start: req.query.start,
     count: req.query.count,
-    sort: req.query.sort
+    sort: req.query.sort,
+    type: req.query.type
   })
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
@@ -171,7 +175,8 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
   const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
     const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
 
-    videoPlaylistCreated.OwnerAccount = user.Account
+    // We need more attributes for the federation
+    videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
     await sendCreateVideoPlaylist(videoPlaylistCreated, t)
 
     return videoPlaylistCreated
@@ -216,6 +221,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
           const videoChannel = res.locals.videoChannel as VideoChannelModel
 
           videoPlaylistInstance.videoChannelId = videoChannel.id
+          videoPlaylistInstance.VideoChannel = videoChannel
         }
       }
 
@@ -227,6 +233,8 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
       }
 
       const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
+      // We need more attributes for the federation
+      playlistUpdated.OwnerAccount = await AccountModel.load(playlistUpdated.OwnerAccount.id, t)
 
       const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
 
@@ -290,11 +298,15 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
       const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
 
       if (await pathExists(playlistThumbnailPath) === false) {
+        logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
+
         const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
         await copy(videoThumbnailPath, playlistThumbnailPath)
       }
     }
 
+    // We need more attributes for the federation
+    videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
     await sendUpdateVideoPlaylist(videoPlaylist, t)
 
     return playlistElement
@@ -320,6 +332,8 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
 
     const element = await videoPlaylistElement.save({ transaction: t })
 
+    // We need more attributes for the federation
+    videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
     await sendUpdateVideoPlaylist(videoPlaylist, t)
 
     return element
@@ -341,6 +355,8 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
     // Decrease position of the next elements
     await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t)
 
+    // We need more attributes for the federation
+    videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
     await sendUpdateVideoPlaylist(videoPlaylist, t)
 
     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
     // Decrease positions of elements after the old position of our ordered elements (decrease)
     await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t)
 
+    // We need more attributes for the federation
+    videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
     await sendUpdateVideoPlaylist(videoPlaylist, t)
   })
 
@@ -415,5 +433,6 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined
   })
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  const additionalAttributes = { playlistInfo: true }
+  return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
 }
index ecdc7975e73435d5a8fe2ca836ed579bb442dfdd..6c7bdb1938e4a0f35b6000978aa143ce3ddbc9c7 100644 (file)
@@ -1,4 +1,4 @@
-import { exists } from '../misc'
+import { exists, isDateValid } from '../misc'
 import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
 import * as validator from 'validator'
 import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object'
@@ -7,7 +7,9 @@ import { isActivityPubUrlValid } from './misc'
 function isPlaylistObjectValid (object: PlaylistObject) {
   return exists(object) &&
     object.type === 'Playlist' &&
-    validator.isInt(object.totalItems + '')
+    validator.isInt(object.totalItems + '') &&
+    isDateValid(object.published) &&
+    isDateValid(object.updated)
 }
 
 function isPlaylistElementObjectValid (object: PlaylistElementObject) {
index 0f5af4ec0749c1432814d82c47510b4f96effc7a..c217a39bf9ed76d16ea52227784202e54f27213a 100644 (file)
@@ -1,9 +1,8 @@
 import { exists } from './misc'
 import * as validator from 'validator'
-import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
+import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers'
 import * as express from 'express'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
 
 const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
 
@@ -19,8 +18,16 @@ function isVideoPlaylistPrivacyValid (value: number) {
   return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined
 }
 
+function isVideoPlaylistTimestampValid (value: any) {
+  return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
+}
+
+function isVideoPlaylistTypeValid (value: any) {
+  return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined
+}
+
 async function isVideoPlaylistExist (id: number | string, res: express.Response) {
-  const videoPlaylist = await VideoPlaylistModel.load(id, undefined)
+  const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
 
   if (!videoPlaylist) {
     res.status(404)
@@ -40,5 +47,7 @@ export {
   isVideoPlaylistExist,
   isVideoPlaylistNameValid,
   isVideoPlaylistDescriptionValid,
-  isVideoPlaylistPrivacyValid
+  isVideoPlaylistPrivacyValid,
+  isVideoPlaylistTimestampValid,
+  isVideoPlaylistTypeValid
 }
index 154a9cffe2330215a071808ec71aafce8269bd58..59c30fdee32fadcd05a3f6025c361cb4074f3a4f 100644 (file)
@@ -11,6 +11,7 @@ import { invert } from 'lodash'
 import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
 import * as bytes from 'bytes'
 import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
 
 // Use a variable to reload the configuration if we need
 let config: IConfig = require('config')
@@ -522,6 +523,11 @@ const VIDEO_PLAYLIST_PRIVACIES = {
   [VideoPlaylistPrivacy.PRIVATE]: 'Private'
 }
 
+const VIDEO_PLAYLIST_TYPES = {
+  [VideoPlaylistType.REGULAR]: 'Regular',
+  [VideoPlaylistType.WATCH_LATER]: 'Watch later'
+}
+
 const MIMETYPES = {
   VIDEO: {
     MIMETYPE_EXT: buildVideoMimetypeExt(),
@@ -778,6 +784,7 @@ export {
   STATIC_MAX_AGE,
   STATIC_PATHS,
   VIDEO_IMPORT_TIMEOUT,
+  VIDEO_PLAYLIST_TYPES,
   ACTIVITY_PUB,
   ACTIVITY_PUB_ACTOR_TYPES,
   THUMBNAILS_SIZE,
index 2b22e16fe5f9d50c4986cf6c0ee5f4e372e4db3f..c669606f87db28f1c791ea19bdaac398710b2482 100644 (file)
@@ -1,7 +1,7 @@
 import * as passwordGenerator from 'password-generator'
 import { UserRole } from '../../shared'
 import { logger } from '../helpers/logger'
-import { createApplicationActor, createUserAccountAndChannel } from '../lib/user'
+import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
 import { UserModel } from '../models/account/user'
 import { ApplicationModel } from '../models/application/application'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
@@ -141,7 +141,7 @@ async function createOAuthAdminIfNotExist () {
   }
   const user = new UserModel(userData)
 
-  await createUserAccountAndChannel(user, validatePassword)
+  await createUserAccountAndChannelAndPlaylist(user, validatePassword)
   logger.info('Username: ' + username)
   logger.info('User password: ' + password)
 }
index c9b428c92c11083dffd0b6b1519d6340d68e3195..70389044e9b1cd24dc79db492254c477159466c2 100644 (file)
@@ -28,7 +28,9 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
     url: playlistObject.id,
     uuid: playlistObject.uuid,
     ownerAccountId: byAccount.id,
-    videoChannelId: null
+    videoChannelId: null,
+    createdAt: new Date(playlistObject.published),
+    updatedAt: new Date(playlistObject.updated)
   }
 }
 
index 155d2ffcc23b391e973aaf071f7fb420541b9267..76f07fd8a8a0537efdc5e5837132990f3f586e09 100644 (file)
@@ -8,6 +8,7 @@ import { VideoModel } from '../../../models/video/video'
 import { VideoChannelModel } from '../../../models/video/video-channel'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { forwardVideoRelatedActivity } from '../send/utils'
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 
 async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) {
   const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id
@@ -45,6 +46,15 @@ async function processDeleteActivity (activity: ActivityDelete, byActor: ActorMo
     }
   }
 
+  {
+    const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
+    if (videoPlaylist) {
+      if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
+
+      return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
+    }
+  }
+
   return undefined
 }
 
@@ -70,6 +80,20 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
   logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
 }
 
+async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) {
+  logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
+
+  await sequelizeTypescript.transaction(async t => {
+    if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) {
+      throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url)
+    }
+
+    await playlistToDelete.destroy({ transaction: t })
+  })
+
+  logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
+}
+
 async function processDeleteAccount (accountToRemove: AccountModel) {
   logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid)
 
index bacdb97e3d3c860f025d368d4baf6f54bb3fdb96..28f18595ba485a76793a19e8d0cb84062dfa5e97 100644 (file)
@@ -45,7 +45,7 @@ async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transac
   const byActor = playlist.OwnerAccount.Actor
   const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
 
-  const object = await playlist.toActivityPubObject()
+  const object = await playlist.toActivityPubObject(null, t)
   const createActivity = buildCreateActivity(playlist.url, byActor, object, audience)
 
   const serverActor = await getServerActor()
index 016811e6069e61255e751fb1ee2d6fe1138a0eaf..7bf5ca520f9165cb46c051d1eafed61f2a222f21 100644 (file)
@@ -31,7 +31,12 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
   const url = getDeleteActivityPubUrl(byActor.url)
   const activity = buildDeleteActivity(url, byActor.url, byActor)
 
-  const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t)
+  const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
+
+  // In case the actor did not have any videos
+  const serverActor = await getServerActor()
+  actorsInvolved.push(serverActor)
+
   actorsInvolved.push(byActor)
 
   return broadcastToFollowers(activity, byActor, actorsInvolved, t)
index 3eb2704fd6882b544eb615ca4b6b559eb7ea84b5..7411c08d578d92e4712381028a1c661451169092 100644 (file)
@@ -52,7 +52,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
   let actorsInvolved: ActorModel[]
   if (accountOrChannel instanceof AccountModel) {
     // Actors that shared my videos are involved too
-    actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t)
+    actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
   } else {
     // Actors that shared videos of my channel are involved too
     actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t)
@@ -87,7 +87,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Tr
 
   const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString())
 
-  const object = await videoPlaylist.toActivityPubObject()
+  const object = await videoPlaylist.toActivityPubObject(null, t)
   const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC)
 
   const updateActivity = buildUpdateActivity(url, byActor, object, audience)
index a39ef6c3d98c69c7d0edd7903d142180dcb486ff..02a84f15be49e95956ad80a253bcc65a06bb857f 100644 (file)
@@ -11,8 +11,9 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
 import { ActorModel } from '../models/activitypub/actor'
 import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
+import { createWatchLaterPlaylist } from './video-playlist'
 
-async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) {
+async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
   const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
     const userOptions = {
       transaction: t,
@@ -38,7 +39,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
     }
     const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
 
-    return { user: userCreated, account: accountCreated, videoChannel }
+    const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
+
+    return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
   })
 
   const [ accountKeys, channelKeys ] = await Promise.all([
@@ -89,7 +92,7 @@ async function createApplicationActor (applicationId: number) {
 
 export {
   createApplicationActor,
-  createUserAccountAndChannel,
+  createUserAccountAndChannelAndPlaylist,
   createLocalAccountWithoutKeys
 }
 
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
new file mode 100644 (file)
index 0000000..6e214e6
--- /dev/null
@@ -0,0 +1,29 @@
+import * as Sequelize from 'sequelize'
+import { AccountModel } from '../models/account/account'
+import { VideoPlaylistModel } from '../models/video/video-playlist'
+import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { getVideoPlaylistActivityPubUrl } from './activitypub'
+import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
+
+async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) {
+  const videoPlaylist = new VideoPlaylistModel({
+    name: 'Watch later',
+    privacy: VideoPlaylistPrivacy.PRIVATE,
+    type: VideoPlaylistType.WATCH_LATER,
+    ownerAccountId: account.id
+  })
+
+  videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
+
+  await videoPlaylist.save({ transaction: t })
+
+  videoPlaylist.OwnerAccount = account
+
+  return videoPlaylist
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  createWatchLaterPlaylist
+}
index 0e97c8dc02dfdd239efa9562e01ed5cc08bf973c..796c63748c9373921cc8f01baa974223d436404e 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
-import { body, param, ValidationChain } from 'express-validator/check'
-import { UserRight, VideoPrivacy } from '../../../../shared'
+import { body, param, query, ValidationChain } from 'express-validator/check'
+import { UserRight } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
 import { UserModel } from '../../../models/account/user'
 import { areValidationErrors } from '../utils'
@@ -11,7 +11,9 @@ import {
   isVideoPlaylistDescriptionValid,
   isVideoPlaylistExist,
   isVideoPlaylistNameValid,
-  isVideoPlaylistPrivacyValid
+  isVideoPlaylistPrivacyValid,
+  isVideoPlaylistTimestampValid,
+  isVideoPlaylistTypeValid
 } from '../../../helpers/custom-validators/video-playlists'
 import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -20,6 +22,7 @@ import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-
 import { VideoModel } from '../../../models/video/video'
 import { authenticatePromiseIfNeeded } from '../../oauth'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
 
 const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -56,6 +59,12 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
                 .json({ error: 'Cannot set "private" a video playlist that was not private.' })
     }
 
+    if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
+      cleanUpReqFiles(req)
+      return res.status(409)
+                .json({ error: 'Cannot update a watch later playlist.' })
+    }
+
     if (req.body.videoChannelId && !await isVideoChannelIdExist(req.body.videoChannelId, res)) return cleanUpReqFiles(req)
 
     return next()
@@ -72,6 +81,13 @@ const videoPlaylistsDeleteValidator = [
     if (areValidationErrors(req, res)) return
 
     if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
+
+    const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
+    if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
+      return res.status(409)
+                .json({ error: 'Cannot delete a watch later playlist.' })
+    }
+
     if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
       return
     }
@@ -127,10 +143,10 @@ const videoPlaylistsAddVideoValidator = [
     .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
   body('startTimestamp')
     .optional()
-    .isInt({ min: 0 }).withMessage('Should have a valid start timestamp'),
+    .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
   body('stopTimestamp')
     .optional()
-    .isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'),
+    .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
@@ -167,10 +183,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
     .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
   body('startTimestamp')
     .optional()
-    .isInt({ min: 0 }).withMessage('Should have a valid start timestamp'),
+    .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
   body('stopTimestamp')
     .optional()
-    .isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'),
+    .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
@@ -275,6 +291,20 @@ const videoPlaylistsReorderVideosValidator = [
   }
 ]
 
+const commonVideoPlaylistFiltersValidator = [
+  query('playlistType')
+    .optional()
+    .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -287,7 +317,9 @@ export {
   videoPlaylistsUpdateOrRemoveVideoValidator,
   videoPlaylistsReorderVideosValidator,
 
-  videoPlaylistElementAPGetValidator
+  videoPlaylistElementAPGetValidator,
+
+  commonVideoPlaylistFiltersValidator
 }
 
 // ---------------------------------------------------------------------------
index c077fb5184650461ab49cc48c06905ca1dc5cb55..ca06048d1c47e65c73231be9d30209f352db8ee9 100644 (file)
@@ -67,9 +67,9 @@ type AvailableForListOptions = {
   ]
 })
 @Scopes({
-  [ScopeNames.SUMMARY]: (required: boolean, withAccount: boolean) => {
+  [ScopeNames.SUMMARY]: (withAccount = false) => {
     const base: IFindOptions<VideoChannelModel> = {
-      attributes: [ 'name', 'description', 'id' ],
+      attributes: [ 'name', 'description', 'id', 'actorId' ],
       include: [
         {
           attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
@@ -225,7 +225,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
     foreignKey: {
       allowNull: true
     },
-    onDelete: 'cascade',
+    onDelete: 'CASCADE',
     hooks: true
   })
   VideoPlaylists: VideoPlaylistModel[]
index 5530e0492d6463db06e95361fdc4788abad66e09..a2bd225a16f84cb8a45313748e5b84495376905c 100644 (file)
@@ -20,6 +20,7 @@ import { getSort, throwIfNotValid } from '../utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS } from '../../initializers'
 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
+import * as validator from 'validator'
 
 @Table({
   tableName: 'videoPlaylistElement',
@@ -34,10 +35,6 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object
       fields: [ 'videoPlaylistId', 'videoId' ],
       unique: true
     },
-    {
-      fields: [ 'videoPlaylistId', 'position' ],
-      unique: true
-    },
     {
       fields: [ 'url' ],
       unique: true
@@ -143,7 +140,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     return VideoPlaylistElementModel.findOne(query)
   }
 
-  static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number) {
+  static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) {
     const query = {
       attributes: [ 'url' ],
       offset: start,
@@ -151,7 +148,8 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
       order: getSort('position'),
       where: {
         videoPlaylistId
-      }
+      },
+      transaction: t
     }
 
     return VideoPlaylistElementModel
index 397887ebf1a6c793d83e75f27e5711d138c8f8a2..ce49f77ecb845dfcb97ba48f2f402d937dd1a936 100644 (file)
@@ -24,7 +24,14 @@ import {
   isVideoPlaylistPrivacyValid
 } from '../../helpers/custom-validators/video-playlists'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
+import {
+  CONFIG,
+  CONSTRAINTS_FIELDS,
+  STATIC_PATHS,
+  THUMBNAILS_SIZE,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PLAYLIST_TYPES
+} from '../../initializers'
 import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
 import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
 import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
@@ -34,22 +41,25 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl
 import { activityPubCollectionPagination } from '../../helpers/activitypub'
 import { remove } from 'fs-extra'
 import { logger } from '../../helpers/logger'
+import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
   WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
-  WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
+  WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
+  WITH_ACCOUNT = 'WITH_ACCOUNT'
 }
 
 type AvailableForListOptions = {
   followerActorId: number
-  accountId?: number,
+  type?: VideoPlaylistType
+  accountId?: number
   videoChannelId?: number
   privateAndUnlisted?: boolean
 }
 
 @Scopes({
-  [ScopeNames.WITH_VIDEOS_LENGTH]: {
+  [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
     attributes: {
       include: [
         [
@@ -59,7 +69,15 @@ type AvailableForListOptions = {
       ]
     }
   },
-  [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
+  [ ScopeNames.WITH_ACCOUNT ]: {
+    include: [
+      {
+        model: () => AccountModel,
+        required: true
+      }
+    ]
+  },
+  [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
     include: [
       {
         model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -71,7 +89,7 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+  [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
     // Only list local playlists OR playlists that are on an instance followed by actorId
     const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
     const actorWhere = {
@@ -107,6 +125,12 @@ type AvailableForListOptions = {
       })
     }
 
+    if (options.type) {
+      whereAnd.push({
+        type: options.type
+      })
+    }
+
     const where = {
       [Sequelize.Op.and]: whereAnd
     }
@@ -179,6 +203,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   @Column(DataType.UUID)
   uuid: string
 
+  @AllowNull(false)
+  @Default(VideoPlaylistType.REGULAR)
+  @Column
+  type: VideoPlaylistType
+
   @ForeignKey(() => AccountModel)
   @Column
   ownerAccountId: number
@@ -208,13 +237,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       name: 'videoPlaylistId',
       allowNull: false
     },
-    onDelete: 'cascade'
+    onDelete: 'CASCADE'
   })
   VideoPlaylistElements: VideoPlaylistElementModel[]
 
-  // Calculated field
-  videosLength?: number
-
   @BeforeDestroy
   static async removeFiles (instance: VideoPlaylistModel) {
     logger.info('Removing files of video playlist %s.', instance.url)
@@ -227,6 +253,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     start: number,
     count: number,
     sort: string,
+    type?: VideoPlaylistType,
     accountId?: number,
     videoChannelId?: number,
     privateAndUnlisted?: boolean
@@ -242,6 +269,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
         method: [
           ScopeNames.AVAILABLE_FOR_LIST,
           {
+            type: options.type,
             followerActorId: options.followerActorId,
             accountId: options.accountId,
             videoChannelId: options.videoChannelId,
@@ -289,7 +317,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .then(e => !!e)
   }
 
-  static load (id: number | string, transaction: Sequelize.Transaction) {
+  static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -298,14 +326,39 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     }
 
     return VideoPlaylistModel
-      .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ])
+      .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
       .findOne(query)
   }
 
+  static loadByUrlAndPopulateAccount (url: string) {
+    const query = {
+      where: {
+        url
+      }
+    }
+
+    return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
+  }
+
   static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
     return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
   }
 
+  static getTypeLabel (type: VideoPlaylistType) {
+    return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
+  }
+
+  static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
+    const query = {
+      where: {
+        videoChannelId
+      },
+      transaction
+    }
+
+    return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
+  }
+
   getThumbnailName () {
     const extension = '.jpg'
 
@@ -345,7 +398,12 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
       thumbnailPath: this.getThumbnailStaticPath(),
 
-      videosLength: this.videosLength,
+      type: {
+        id: this.type,
+        label: VideoPlaylistModel.getTypeLabel(this.type)
+      },
+
+      videosLength: this.get('videosLength'),
 
       createdAt: this.createdAt,
       updatedAt: this.updatedAt,
@@ -355,18 +413,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     }
   }
 
-  toActivityPubObject (): Promise<PlaylistObject> {
+  toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
     const handler = (start: number, count: number) => {
-      return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count)
+      return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
     }
 
-    return activityPubCollectionPagination(this.url, handler, null)
+    return activityPubCollectionPagination(this.url, handler, page)
       .then(o => {
         return Object.assign(o, {
           type: 'Playlist' as 'Playlist',
           name: this.name,
           content: this.description,
           uuid: this.uuid,
+          published: this.createdAt.toISOString(),
+          updated: this.updatedAt.toISOString(),
           attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
           icon: {
             type: 'Image' as 'Image',
index c87f71277876fe46d731a36f1110d2a046b3bc89..7df0ed18df7e89b83aa464035730d2e878ff622b 100644 (file)
@@ -125,7 +125,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
       .then(res => res.map(r => r.Actor))
   }
 
-  static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
+  static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
     const query = {
       attributes: [],
       include: [
index 7a102b058c6679381046391da70003e77d8aceed..a563f78ef01be25bd45fba268b64dfc9d9822052 100644 (file)
@@ -225,7 +225,7 @@ type AvailableForListIDsOptions = {
       },
       include: [
         {
-          model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY)
+          model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] })
         }
       ]
     }
@@ -1535,18 +1535,7 @@ export class VideoModel extends Model<VideoModel> {
 
     if (ids.length === 0) return { data: [], total: count }
 
-    // FIXME: typings
-    const apiScope: any[] = [
-      {
-        method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
-      }
-    ]
-
-    if (options.user) {
-      apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
-    }
-
-    const secondQuery = {
+    const secondQuery: IFindOptions<VideoModel> = {
       offset: 0,
       limit: query.limit,
       attributes: query.attributes,
@@ -1556,6 +1545,29 @@ export class VideoModel extends Model<VideoModel> {
         )
       ]
     }
+
+    // FIXME: typing
+    const apiScope: any[] = []
+
+    if (options.user) {
+      apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
+
+      // Even if the relation is n:m, we know that a user only have 0..1 video history
+      // So we won't have multiple rows for the same video
+      // A subquery adds some bugs in our query so disable it
+      secondQuery.subQuery = false
+    }
+
+    apiScope.push({
+      method: [
+        ScopeNames.FOR_API, {
+          ids, withFiles:
+          options.withFiles,
+          videoPlaylistId: options.videoPlaylistId
+        } as ForAPIOptions
+      ]
+    })
+
     const rows = await VideoModel.scope(apiScope).findAll(secondQuery)
 
     return {
index 77c17036ac67e3cbe5d27b52cf1b17a1fa8e53eb..ca51cd39a229166fc0b29be7cb028b177e8a31ad 100644 (file)
@@ -16,6 +16,7 @@ import './video-captions'
 import './video-channels'
 import './video-comments'
 import './video-imports'
+import './video-playlists'
 import './videos'
 import './videos-filter'
 import './videos-history'
index 68fe362e9c3d55c8e696a07be8901ce82153a74f..803e7afb9cfd8945e92ed3a1dd9031004683aae7 100644 (file)
@@ -2,20 +2,24 @@
 
 import 'mocha'
 import {
-  createUser,
+  addVideoInPlaylist,
   createVideoPlaylist,
   deleteVideoPlaylist,
   flushTests,
+  generateUserAccessToken,
+  getAccountPlaylistsListWithToken,
   getVideoPlaylist,
   immutableAssign,
   killallServers,
   makeGetRequest,
+  removeVideoFromPlaylist,
+  reorderVideosPlaylist,
   runServer,
   ServerInfo,
   setAccessTokensToServers,
   updateVideoPlaylist,
-  userLogin,
-  addVideoInPlaylist, uploadVideo, updateVideoPlaylistElement, removeVideoFromPlaylist, reorderVideosPlaylist
+  updateVideoPlaylistElement,
+  uploadVideoAndGetId
 } from '../../../../shared/utils'
 import {
   checkBadCountPagination,
@@ -23,11 +27,13 @@ import {
   checkBadStartPagination
 } from '../../../../shared/utils/requests/check-api-params'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
 
 describe('Test video playlists API validator', function () {
   let server: ServerInfo
-  let userAccessToken = ''
+  let userAccessToken: string
   let playlistUUID: string
+  let watchLaterPlaylistId: number
   let videoId: number
   let videoId2: number
 
@@ -42,19 +48,13 @@ describe('Test video playlists API validator', function () {
 
     await setAccessTokensToServers([ server ])
 
-    const username = 'user1'
-    const password = 'my super password'
-    await createUser(server.url, server.accessToken, username, password)
-    userAccessToken = await userLogin(server, { username, password })
+    userAccessToken = await generateUserAccessToken(server, 'user1')
+    videoId = (await uploadVideoAndGetId({ server, videoName: 'video 1' })).id
+    videoId2 = (await uploadVideoAndGetId({ server, videoName: 'video 2' })).id
 
     {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
-      videoId = res.body.video.id
-    }
-
-    {
-      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
-      videoId2 = res.body.video.id
+      const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root',0, 5, VideoPlaylistType.WATCH_LATER)
+      watchLaterPlaylistId = res.body.data[0].id
     }
 
     {
@@ -93,6 +93,12 @@ describe('Test video playlists API validator', function () {
       await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
     })
 
+    it('Should fail with a bad playlist type', async function () {
+      await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } })
+      await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } })
+      await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } })
+    })
+
     it('Should fail with a bad account parameter', async function () {
       const accountPath = '/api/v1/accounts/root2/video-playlists'
 
@@ -158,410 +164,250 @@ describe('Test video playlists API validator', function () {
   })
 
   describe('When creating/updating a video playlist', function () {
+    const getBase = (playlistAttrs: any = {}, wrapper: any = {}) => {
+      return Object.assign({
+        expectedStatus: 400,
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: Object.assign({
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.UNLISTED,
+          thumbnailfile: 'thumbnail.jpg'
+        }, playlistAttrs)
+      }, wrapper)
+    }
+    const getUpdate = (params: any, playlistId: number | string) => {
+      return immutableAssign(params, { playlistId: playlistId })
+    }
 
     it('Should fail with an unauthenticated user', async function () {
-      const baseParams = {
-        url: server.url,
-        token: null,
-        playlistAttrs: {
-          displayName: 'super playlist',
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        },
-        expectedStatus: 401
-      }
+      const params = getBase({}, { token: null, expectedStatus: 401 })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail without displayName', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        } as any,
-        expectedStatus: 400
-      }
+      const params = getBase({ displayName: undefined })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an incorrect display name', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 's'.repeat(300),
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        },
-        expectedStatus: 400
-      }
+      const params = getBase({ displayName: 's'.repeat(300) })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an incorrect description', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PUBLIC,
-          description: 't'
-        },
-        expectedStatus: 400
-      }
+      const params = getBase({ description: 't' })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an incorrect privacy', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: 45
-        } as any,
-        expectedStatus: 400
-      }
+      const params = getBase({ privacy: 45 })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an unknown video channel id', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PUBLIC,
-          videoChannelId: 42
-        },
-        expectedStatus: 404
-      }
+      const params = getBase({ videoChannelId: 42 }, { expectedStatus: 404 })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an incorrect thumbnail file', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PUBLIC,
-          thumbnailfile: 'avatar.png'
-        },
-        expectedStatus: 400
-      }
+      const params = getBase({ thumbnailfile: 'avatar.png' })
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      await createVideoPlaylist(params)
+      await updateVideoPlaylist(getUpdate(params, playlistUUID))
     })
 
     it('Should fail with an unknown playlist to update', async function () {
-      await updateVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: 42,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        },
-        expectedStatus: 404
-      })
+      await updateVideoPlaylist(getUpdate(
+        getBase({}, { expectedStatus: 404 }),
+        42
+      ))
     })
 
     it('Should fail to update a playlist of another user', async function () {
-      await updateVideoPlaylist({
-        url: server.url,
-        token: userAccessToken,
-        playlistId: playlistUUID,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        },
-        expectedStatus: 403
-      })
+      await updateVideoPlaylist(getUpdate(
+        getBase({}, { token: userAccessToken, expectedStatus: 403 }),
+        playlistUUID
+      ))
     })
 
     it('Should fail to update to private a public/unlisted playlist', async function () {
-      const res = await createVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'super playlist',
-          privacy: VideoPlaylistPrivacy.PUBLIC
-        }
-      })
+      const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC }, { expectedStatus: 200 })
+
+      const res = await createVideoPlaylist(params)
       const playlist = res.body.videoPlaylist
 
-      await updateVideoPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlist.id,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.PRIVATE
-        },
-        expectedStatus: 409
-      })
+      const paramsUpdate = getBase({ privacy: VideoPlaylistPrivacy.PRIVATE }, { expectedStatus: 409 })
+
+      await updateVideoPlaylist(getUpdate(paramsUpdate, playlist.id))
+    })
+
+    it('Should fail to update the watch later playlist', async function () {
+      await updateVideoPlaylist(getUpdate(
+        getBase({}, { expectedStatus: 409 }),
+        watchLaterPlaylistId
+      ))
     })
 
     it('Should succeed with the correct params', async function () {
-      const baseParams = {
-        url: server.url,
-        token: server.accessToken,
-        playlistAttrs: {
-          displayName: 'display name',
-          privacy: VideoPlaylistPrivacy.UNLISTED,
-          thumbnailfile: 'thumbnail.jpg'
-        }
+      {
+        const params = getBase({}, { expectedStatus: 200 })
+        await createVideoPlaylist(params)
       }
 
-      await createVideoPlaylist(baseParams)
-      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+      {
+        const params = getBase({}, { expectedStatus: 204 })
+        await updateVideoPlaylist(getUpdate(params, playlistUUID))
+      }
     })
   })
 
   describe('When adding an element in a playlist', function () {
-    it('Should fail with an unauthenticated user', async function () {
-      await addVideoInPlaylist({
+    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
+      return Object.assign({
+        expectedStatus: 400,
         url: server.url,
-        token: null,
-        elementAttrs: {
-          videoId: videoId
-        },
+        token: server.accessToken,
         playlistId: playlistUUID,
-        expectedStatus: 401
-      })
+        elementAttrs: Object.assign({
+          videoId: videoId,
+          startTimestamp: 2,
+          stopTimestamp: 3
+        }, elementAttrs)
+      }, wrapper)
+    }
+
+    it('Should fail with an unauthenticated user', async function () {
+      const params = getBase({}, { token: null, expectedStatus: 401 })
+      await addVideoInPlaylist(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: userAccessToken,
-        elementAttrs: {
-          videoId: videoId
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 403
-      })
+      const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
+      await addVideoInPlaylist(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId
-        },
-        playlistId: 'toto',
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({}, { playlistId: 'toto' })
+        await addVideoInPlaylist(params)
+      }
 
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId
-        },
-        playlistId: 42,
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
+        await addVideoInPlaylist(params)
+      }
     })
 
     it('Should fail with an unknown or incorrect video id', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: 'toto' as any
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
-
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: 42
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 404
-      })
+      const params = getBase({ videoId: 42 }, { expectedStatus: 404 })
+      await addVideoInPlaylist(params)
     })
 
     it('Should fail with a bad start/stop timestamp', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId,
-          startTimestamp: -42
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ startTimestamp: -42 })
+        await addVideoInPlaylist(params)
+      }
 
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId,
-          stopTimestamp: 'toto' as any
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ stopTimestamp: 'toto' as any })
+        await addVideoInPlaylist(params)
+      }
     })
 
     it('Succeed with the correct params', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId,
-          stopTimestamp: 3
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 200
-      })
+      const params = getBase({}, { expectedStatus: 200 })
+      await addVideoInPlaylist(params)
     })
 
     it('Should fail if the video was already added in the playlist', async function () {
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          videoId: videoId,
-          stopTimestamp: 3
-        },
-        playlistId: playlistUUID,
-        expectedStatus: 409
-      })
+      const params = getBase({}, { expectedStatus: 409 })
+      await addVideoInPlaylist(params)
     })
   })
 
   describe('When updating an element in a playlist', function () {
-    it('Should fail with an unauthenticated user', async function () {
-      await updateVideoPlaylistElement({
+    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
+      return Object.assign({
         url: server.url,
-        token: null,
-        elementAttrs: { },
+        token: server.accessToken,
+        elementAttrs: Object.assign({
+          startTimestamp: 1,
+          stopTimestamp: 2
+        }, elementAttrs),
         videoId: videoId,
         playlistId: playlistUUID,
-        expectedStatus: 401
-      })
+        expectedStatus: 400
+      }, wrapper)
+    }
+
+    it('Should fail with an unauthenticated user', async function () {
+      const params = getBase({}, { token: null, expectedStatus: 401 })
+      await updateVideoPlaylistElement(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: userAccessToken,
-        elementAttrs: { },
-        videoId: videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 403
-      })
+      const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
+      await updateVideoPlaylistElement(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: { },
-        videoId: videoId,
-        playlistId: 'toto',
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({}, { playlistId: 'toto' })
+        await updateVideoPlaylistElement(params)
+      }
 
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: { },
-        videoId: videoId,
-        playlistId: 42,
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
+        await updateVideoPlaylistElement(params)
+      }
     })
 
     it('Should fail with an unknown or incorrect video id', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: { },
-        videoId: 'toto',
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({}, { videoId: 'toto' })
+        await updateVideoPlaylistElement(params)
+      }
 
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: { },
-        videoId: 42,
-        playlistId: playlistUUID,
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({}, { videoId: 42, expectedStatus: 404 })
+        await updateVideoPlaylistElement(params)
+      }
     })
 
     it('Should fail with a bad start/stop timestamp', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          startTimestamp: 'toto' as any
-        },
-        videoId: videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ startTimestamp: 'toto' as any })
+        await updateVideoPlaylistElement(params)
+      }
 
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          stopTimestamp: -42
-        },
-        videoId: videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ stopTimestamp: -42 })
+        await updateVideoPlaylistElement(params)
+      }
     })
 
     it('Should fail with an unknown element', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          stopTimestamp: 2
-        },
-        videoId: videoId2,
-        playlistId: playlistUUID,
-        expectedStatus: 404
-      })
+      const params = getBase({}, { videoId: videoId2, expectedStatus: 404 })
+      await updateVideoPlaylistElement(params)
     })
 
     it('Succeed with the correct params', async function () {
-      await updateVideoPlaylistElement({
-        url: server.url,
-        token: server.accessToken,
-        elementAttrs: {
-          stopTimestamp: 2
-        },
-        videoId: videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 204
-      })
+      const params = getBase({}, { expectedStatus: 204 })
+      await updateVideoPlaylistElement(params)
     })
   })
 
@@ -569,280 +415,166 @@ describe('Test video playlists API validator', function () {
     let videoId3: number
     let videoId4: number
 
-    before(async function () {
-      {
-        const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
-        videoId3 = res.body.video.id
-      }
-
-      {
-        const res = await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
-        videoId4 = res.body.video.id
-      }
-
-      await addVideoInPlaylist({
+    const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
+      return Object.assign({
         url: server.url,
         token: server.accessToken,
         playlistId: playlistUUID,
-        elementAttrs: { videoId: videoId3 }
-      })
+        elementAttrs: Object.assign({
+          startPosition: 1,
+          insertAfterPosition: 2,
+          reorderLength: 3
+        }, elementAttrs),
+        expectedStatus: 400
+      }, wrapper)
+    }
 
-      await addVideoInPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: { videoId: videoId4 }
-      })
+    before(async function () {
+      videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id
+      videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id
+
+      for (let id of [ videoId3, videoId4 ]) {
+        await addVideoInPlaylist({
+          url: server.url,
+          token: server.accessToken,
+          playlistId: playlistUUID,
+          elementAttrs: { videoId: id }
+        })
+      }
     })
 
     it('Should fail with an unauthenticated user', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: null,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 401
-      })
+      const params = getBase({}, { token: null, expectedStatus: 401 })
+      await reorderVideosPlaylist(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: userAccessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 403
-      })
+      const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
+      await reorderVideosPlaylist(params)
     })
 
     it('Should fail with an invalid playlist', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: 'toto',
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({}, { playlistId: 'toto' })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: 42,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({}, {  playlistId: 42, expectedStatus: 404 })
+        await reorderVideosPlaylist(params)
+      }
     })
 
     it('Should fail with an invalid start position', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: -1,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ startPosition: -1 })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 'toto' as any,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ startPosition: 'toto' as any })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 42,
-          insertAfterPosition: 2
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ startPosition: 42 })
+        await reorderVideosPlaylist(params)
+      }
     })
 
     it('Should fail with an invalid insert after position', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 'toto' as any
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ insertAfterPosition: 'toto' as any })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: -2
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ insertAfterPosition: -2 })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 42
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ insertAfterPosition: 42 })
+        await reorderVideosPlaylist(params)
+      }
     })
 
     it('Should fail with an invalid reorder length', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2,
-          reorderLength: 'toto' as any
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ reorderLength: 'toto' as any })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2,
-          reorderLength: -1
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ reorderLength: -2 })
+        await reorderVideosPlaylist(params)
+      }
 
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2,
-          reorderLength: 4
-        },
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ reorderLength: 42 })
+        await reorderVideosPlaylist(params)
+      }
     })
 
     it('Succeed with the correct params', async function () {
-      await reorderVideosPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        playlistId: playlistUUID,
-        elementAttrs: {
-          startPosition: 1,
-          insertAfterPosition: 2,
-          reorderLength: 3
-        },
-        expectedStatus: 204
-      })
+      const params = getBase({}, { expectedStatus: 204 })
+      await reorderVideosPlaylist(params)
     })
   })
 
   describe('When deleting an element in a playlist', function () {
-    it('Should fail with an unauthenticated user', async function () {
-      await removeVideoFromPlaylist({
+    const getBase = (wrapper: any = {}) => {
+      return Object.assign({
         url: server.url,
-        token: null,
-        videoId,
+        token: server.accessToken,
+        videoId: videoId,
         playlistId: playlistUUID,
-        expectedStatus: 401
-      })
+        expectedStatus: 400
+      }, wrapper)
+    }
+
+    it('Should fail with an unauthenticated user', async function () {
+      const params = getBase({ token: null, expectedStatus: 401 })
+      await removeVideoFromPlaylist(params)
     })
 
     it('Should fail with the playlist of another user', async function () {
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: userAccessToken,
-        videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 403
-      })
+      const params = getBase({ token: userAccessToken, expectedStatus: 403 })
+      await removeVideoFromPlaylist(params)
     })
 
     it('Should fail with an unknown or incorrect playlist id', async function () {
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId,
-        playlistId: 'toto',
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ playlistId: 'toto' })
+        await removeVideoFromPlaylist(params)
+      }
 
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId,
-        playlistId: 42,
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({ playlistId: 42, expectedStatus: 404 })
+        await removeVideoFromPlaylist(params)
+      }
     })
 
     it('Should fail with an unknown or incorrect video id', async function () {
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId: 'toto',
-        playlistId: playlistUUID,
-        expectedStatus: 400
-      })
+      {
+        const params = getBase({ videoId: 'toto' })
+        await removeVideoFromPlaylist(params)
+      }
 
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId: 42,
-        playlistId: playlistUUID,
-        expectedStatus: 404
-      })
+      {
+        const params = getBase({ videoId: 42, expectedStatus: 404 })
+        await removeVideoFromPlaylist(params)
+      }
     })
 
     it('Should fail with an unknown element', async function () {
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId: videoId2,
-        playlistId: playlistUUID,
-        expectedStatus: 404
-      })
+      const params = getBase({ videoId: videoId2, expectedStatus: 404 })
+      await removeVideoFromPlaylist(params)
     })
 
     it('Succeed with the correct params', async function () {
-      await removeVideoFromPlaylist({
-        url: server.url,
-        token: server.accessToken,
-        videoId: videoId,
-        playlistId: playlistUUID,
-        expectedStatus: 204
-      })
+      const params = getBase({ expectedStatus: 204 })
+      await removeVideoFromPlaylist(params)
     })
   })
 
@@ -855,6 +587,10 @@ describe('Test video playlists API validator', function () {
       await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, 403)
     })
 
+    it('Should fail with the watch later playlist', async function () {
+      await deleteVideoPlaylist(server.url, server.accessToken, watchLaterPlaylistId, 409)
+    })
+
     it('Should succeed with the correct params', async function () {
       await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID)
     })
index cb23239da9027330368448c6a3d5dbf1cb73e226..7dd1563fcc98394c45cea3c7997224acb799a469 100644 (file)
 
 import * as chai from 'chai'
 import 'mocha'
-import { join } from 'path'
-import * as request from 'supertest'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
 import {
   addVideoChannel,
-  checkTmpIsEmpty,
-  checkVideoFilesWereRemoved,
-  completeVideoCheck,
+  addVideoInPlaylist,
+  checkPlaylistFilesWereRemoved,
   createUser,
-  dateIsValid,
+  createVideoPlaylist,
+  deleteVideoChannel,
+  deleteVideoPlaylist,
   doubleFollow,
   flushAndRunMultipleServers,
   flushTests,
-  getLocalVideos,
-  getVideo,
-  getVideoChannelsList,
-  getVideosList,
+  getAccountPlaylistsList,
+  getAccountPlaylistsListWithToken,
+  getPlaylistVideos,
+  getVideoChannelPlaylistsList,
+  getVideoPlaylist,
+  getVideoPlaylistsList,
+  getVideoPlaylistWithToken,
   killallServers,
-  rateVideo,
-  removeVideo,
+  removeUser,
+  removeVideoFromPlaylist,
+  reorderVideosPlaylist,
   ServerInfo,
   setAccessTokensToServers,
+  setDefaultVideoChannel,
   testImage,
-  updateVideo,
+  unfollow,
+  updateVideoPlaylist,
+  updateVideoPlaylistElement,
   uploadVideo,
+  uploadVideoAndGetId,
   userLogin,
-  viewVideo,
-  wait,
-  webtorrentAdd
+  waitJobs
 } from '../../../../shared/utils'
-import {
-  addVideoCommentReply,
-  addVideoCommentThread,
-  deleteVideoComment,
-  getVideoCommentThreads,
-  getVideoThreadComments
-} from '../../../../shared/utils/videos/video-comments'
-import { waitJobs } from '../../../../shared/utils/server/jobs'
+import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model'
+import { Video } from '../../../../shared/models/videos'
+import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
 
 const expect = chai.expect
 
 describe('Test video playlists', function () {
   let servers: ServerInfo[] = []
 
+  let playlistServer2Id1: number
+  let playlistServer2Id2: number
+  let playlistServer2UUID2: number
+
+  let playlistServer1Id: number
+  let playlistServer1UUID: string
+
+  let nsfwVideoServer1: number
+
   before(async function () {
     this.timeout(120000)
 
-    servers = await flushAndRunMultipleServers(3)
+    servers = await flushAndRunMultipleServers(3, { transcoding: { enabled: false } })
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
 
     // Server 1 and server 2 follow each other
     await doubleFollow(servers[0], servers[1])
     // Server 1 and server 3 follow each other
     await doubleFollow(servers[0], servers[2])
+
+    {
+      const serverPromises: Promise<any>[][] = []
+
+      for (const server of servers) {
+        const videoPromises: Promise<any>[] = []
+
+        for (let i = 0; i < 7; i++) {
+          videoPromises.push(
+            uploadVideo(server.url, server.accessToken, { name: `video ${i} server ${server.serverNumber}`, nsfw: false })
+              .then(res => res.body.video)
+          )
+        }
+
+        serverPromises.push(videoPromises)
+      }
+
+      servers[0].videos = await Promise.all(serverPromises[0])
+      servers[1].videos = await Promise.all(serverPromises[1])
+      servers[2].videos = await Promise.all(serverPromises[2])
+    }
+
+    nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id
+
+    await waitJobs(servers)
   })
 
-  it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
+  it('Should list watch later playlist', async function () {
+    const url = servers[ 0 ].url
+    const accessToken = servers[ 0 ].accessToken
+
+    {
+      const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
+
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data).to.have.lengthOf(1)
 
+      const playlist: VideoPlaylist = res.body.data[ 0 ]
+      expect(playlist.displayName).to.equal('Watch later')
+      expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
+      expect(playlist.type.label).to.equal('Watch later')
+    }
+
+    {
+      const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR)
+
+      expect(res.body.total).to.equal(0)
+      expect(res.body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const res = await getAccountPlaylistsList(url, 'root', 0, 5)
+      expect(res.body.total).to.equal(0)
+      expect(res.body.data).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
+    this.timeout(30000)
+
+    await createVideoPlaylist({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistAttrs: {
+        displayName: 'my super playlist',
+        privacy: VideoPlaylistPrivacy.PUBLIC,
+        description: 'my super description',
+        thumbnailfile: 'thumbnail.jpg',
+        videoChannelId: servers[0].videoChannel.id
+      }
+    })
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const res = await getVideoPlaylistsList(server.url, 0, 5)
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data).to.have.lengthOf(1)
+
+      const playlistFromList = res.body.data[0] as VideoPlaylist
+
+      const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
+      const playlistFromGet = res2.body
+
+      for (const playlist of [ playlistFromGet, playlistFromList ]) {
+        expect(playlist.id).to.be.a('number')
+        expect(playlist.uuid).to.be.a('string')
+
+        expect(playlist.isLocal).to.equal(server.serverNumber === 1)
+
+        expect(playlist.displayName).to.equal('my super playlist')
+        expect(playlist.description).to.equal('my super description')
+        expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
+        expect(playlist.privacy.label).to.equal('Public')
+        expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
+        expect(playlist.type.label).to.equal('Regular')
+
+        expect(playlist.videosLength).to.equal(0)
+
+        expect(playlist.ownerAccount.name).to.equal('root')
+        expect(playlist.ownerAccount.displayName).to.equal('root')
+        expect(playlist.videoChannel.name).to.equal('root_channel')
+        expect(playlist.videoChannel.displayName).to.equal('Main root channel')
+      }
+    }
   })
 
   it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
-    // create 2 playlists (with videos and no videos)
-    // With thumbnail and no thumbnail
+    this.timeout(30000)
+
+    {
+      const res = await createVideoPlaylist({
+        url: servers[1].url,
+        token: servers[1].accessToken,
+        playlistAttrs: {
+          displayName: 'playlist 2',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        }
+      })
+      playlistServer2Id1 = res.body.videoPlaylist.id
+    }
+
+    {
+      const res = await createVideoPlaylist({
+        url: servers[ 1 ].url,
+        token: servers[ 1 ].accessToken,
+        playlistAttrs: {
+          displayName: 'playlist 3',
+          privacy: VideoPlaylistPrivacy.PUBLIC,
+          thumbnailfile: 'thumbnail.jpg'
+        }
+      })
+
+      playlistServer2Id2 = res.body.videoPlaylist.id
+      playlistServer2UUID2 = res.body.videoPlaylist.uuid
+    }
+
+    for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) {
+      await addVideoInPlaylist({
+        url: servers[ 1 ].url,
+        token: servers[ 1 ].accessToken,
+        playlistId: id,
+        elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 }
+      })
+      await addVideoInPlaylist({
+        url: servers[ 1 ].url,
+        token: servers[ 1 ].accessToken,
+        playlistId: id,
+        elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id }
+      })
+    }
+
+    await waitJobs(servers)
+
+    for (const server of [ servers[0], servers[1] ]) {
+      const res = await getVideoPlaylistsList(server.url, 0, 5)
+
+      const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
+      expect(playlist2).to.not.be.undefined
+      await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
+
+      const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3')
+      expect(playlist3).to.not.be.undefined
+      await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
+    }
+
+    const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
+    expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
+    expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
   })
 
   it('Should have the playlist on server 3 after a new follow', async function () {
+    this.timeout(30000)
+
     // Server 2 and server 3 follow each other
     await doubleFollow(servers[1], servers[2])
+
+    const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
+
+    const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
+    expect(playlist2).to.not.be.undefined
+    await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
+
+    expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
   })
 
-  it('Should create some playlists and list them correctly', async function () {
-    // create 3 playlists with some videos in it
-    // check pagination
-    // check sort
-    // check empty
+  it('Should correctly list the playlists', async function () {
+    this.timeout(30000)
+
+    {
+      const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt')
+
+      expect(res.body.total).to.equal(3)
+
+      const data: VideoPlaylist[] = res.body.data
+      expect(data).to.have.lengthOf(2)
+      expect(data[ 0 ].displayName).to.equal('playlist 2')
+      expect(data[ 1 ].displayName).to.equal('playlist 3')
+    }
+
+    {
+      const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt')
+
+      expect(res.body.total).to.equal(3)
+
+      const data: VideoPlaylist[] = res.body.data
+      expect(data).to.have.lengthOf(2)
+      expect(data[ 0 ].displayName).to.equal('playlist 2')
+      expect(data[ 1 ].displayName).to.equal('my super playlist')
+    }
   })
 
   it('Should list video channel playlists', async function () {
-    // check pagination
-    // check sort
-    // check empty
+    this.timeout(30000)
+
+    {
+      const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt')
+
+      expect(res.body.total).to.equal(1)
+
+      const data: VideoPlaylist[] = res.body.data
+      expect(data).to.have.lengthOf(1)
+      expect(data[ 0 ].displayName).to.equal('my super playlist')
+    }
   })
 
   it('Should list account playlists', async function () {
-    // check pagination
-    // check sort
-    // check empty
+    this.timeout(30000)
+
+    {
+      const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt')
+
+      expect(res.body.total).to.equal(2)
+
+      const data: VideoPlaylist[] = res.body.data
+      expect(data).to.have.lengthOf(1)
+      expect(data[ 0 ].displayName).to.equal('playlist 2')
+    }
+
+    {
+      const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt')
+
+      expect(res.body.total).to.equal(2)
+
+      const data: VideoPlaylist[] = res.body.data
+      expect(data).to.have.lengthOf(1)
+      expect(data[ 0 ].displayName).to.equal('playlist 3')
+    }
   })
 
-  it('Should get a playlist', async function () {
-    // get empty playlist
-    // get non empty playlist
+  it('Should not list unlisted or private playlists', async function () {
+    this.timeout(30000)
+
+    await createVideoPlaylist({
+      url: servers[ 1 ].url,
+      token: servers[ 1 ].accessToken,
+      playlistAttrs: {
+        displayName: 'playlist unlisted',
+        privacy: VideoPlaylistPrivacy.UNLISTED
+      }
+    })
+
+    await createVideoPlaylist({
+      url: servers[ 1 ].url,
+      token: servers[ 1 ].accessToken,
+      playlistAttrs: {
+        displayName: 'playlist private',
+        privacy: VideoPlaylistPrivacy.PRIVATE
+      }
+    })
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const results = [
+        await getAccountPlaylistsList(server.url, 'root@localhost:9002', 0, 5, '-createdAt'),
+        await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
+      ]
+
+      expect(results[0].body.total).to.equal(2)
+      expect(results[1].body.total).to.equal(3)
+
+      for (const res of results) {
+        const data: VideoPlaylist[] = res.body.data
+        expect(data).to.have.lengthOf(2)
+        expect(data[ 0 ].displayName).to.equal('playlist 3')
+        expect(data[ 1 ].displayName).to.equal('playlist 2')
+      }
+    }
   })
 
   it('Should update a playlist', async function () {
-    // update thumbnail
-
-    // update other details
+    this.timeout(30000)
+
+    await updateVideoPlaylist({
+      url: servers[1].url,
+      token: servers[1].accessToken,
+      playlistAttrs: {
+        displayName: 'playlist 3 updated',
+        description: 'description updated',
+        privacy: VideoPlaylistPrivacy.UNLISTED,
+        thumbnailfile: 'thumbnail.jpg',
+        videoChannelId: servers[1].videoChannel.id
+      },
+      playlistId: playlistServer2Id2
+    })
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const res = await getVideoPlaylist(server.url, playlistServer2UUID2)
+      const playlist: VideoPlaylist = res.body
+
+      expect(playlist.displayName).to.equal('playlist 3 updated')
+      expect(playlist.description).to.equal('description updated')
+
+      expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
+      expect(playlist.privacy.label).to.equal('Unlisted')
+
+      expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
+      expect(playlist.type.label).to.equal('Regular')
+
+      expect(playlist.videosLength).to.equal(2)
+
+      expect(playlist.ownerAccount.name).to.equal('root')
+      expect(playlist.ownerAccount.displayName).to.equal('root')
+      expect(playlist.videoChannel.name).to.equal('root_channel')
+      expect(playlist.videoChannel.displayName).to.equal('Main root channel')
+    }
   })
 
   it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
+    this.timeout(30000)
+
+    const addVideo = (elementAttrs: any) => {
+      return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
+    }
 
+    const res = await createVideoPlaylist({
+      url: servers[ 0 ].url,
+      token: servers[ 0 ].accessToken,
+      playlistAttrs: {
+        displayName: 'playlist 4',
+        privacy: VideoPlaylistPrivacy.PUBLIC
+      }
+    })
+
+    playlistServer1Id = res.body.videoPlaylist.id
+    playlistServer1UUID = res.body.videoPlaylist.uuid
+
+    await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
+    await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
+    await addVideo({ videoId: servers[2].videos[2].uuid })
+    await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
+    await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
+    await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
+
+    await waitJobs(servers)
   })
 
   it('Should correctly list playlist videos', async function () {
-    // empty
-    // some filters?
+    this.timeout(30000)
+
+    for (const server of servers) {
+      const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+
+      expect(res.body.total).to.equal(6)
+
+      const videos: Video[] = res.body.data
+      expect(videos).to.have.lengthOf(6)
+
+      expect(videos[0].name).to.equal('video 0 server 1')
+      expect(videos[0].playlistElement.position).to.equal(1)
+      expect(videos[0].playlistElement.startTimestamp).to.equal(15)
+      expect(videos[0].playlistElement.stopTimestamp).to.equal(28)
+
+      expect(videos[1].name).to.equal('video 1 server 3')
+      expect(videos[1].playlistElement.position).to.equal(2)
+      expect(videos[1].playlistElement.startTimestamp).to.equal(35)
+      expect(videos[1].playlistElement.stopTimestamp).to.be.null
+
+      expect(videos[2].name).to.equal('video 2 server 3')
+      expect(videos[2].playlistElement.position).to.equal(3)
+      expect(videos[2].playlistElement.startTimestamp).to.be.null
+      expect(videos[2].playlistElement.stopTimestamp).to.be.null
+
+      expect(videos[3].name).to.equal('video 3 server 1')
+      expect(videos[3].playlistElement.position).to.equal(4)
+      expect(videos[3].playlistElement.startTimestamp).to.be.null
+      expect(videos[3].playlistElement.stopTimestamp).to.equal(35)
+
+      expect(videos[4].name).to.equal('video 4 server 1')
+      expect(videos[4].playlistElement.position).to.equal(5)
+      expect(videos[4].playlistElement.startTimestamp).to.equal(45)
+      expect(videos[4].playlistElement.stopTimestamp).to.equal(60)
+
+      expect(videos[5].name).to.equal('NSFW video')
+      expect(videos[5].playlistElement.position).to.equal(6)
+      expect(videos[5].playlistElement.startTimestamp).to.equal(5)
+      expect(videos[5].playlistElement.stopTimestamp).to.be.null
+
+      const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false })
+      expect(res2.body.total).to.equal(5)
+      expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined
+
+      const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
+      expect(res3.body.data).to.have.lengthOf(2)
+    }
   })
 
   it('Should reorder the playlist', async function () {
-    // reorder 1 element
-    // reorder 3 elements
-    // reorder at the beginning
-    // reorder at the end
-    // reorder before/after
+    this.timeout(30000)
+
+    {
+      await reorderVideosPlaylist({
+        url: servers[ 0 ].url,
+        token: servers[ 0 ].accessToken,
+        playlistId: playlistServer1Id,
+        elementAttrs: {
+          startPosition: 2,
+          insertAfterPosition: 3
+        }
+      })
+
+      await waitJobs(servers)
+
+      for (const server of servers) {
+        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+        const names = res.body.data.map(v => v.name)
+
+        expect(names).to.deep.equal([
+          'video 0 server 1',
+          'video 2 server 3',
+          'video 1 server 3',
+          'video 3 server 1',
+          'video 4 server 1',
+          'NSFW video'
+        ])
+      }
+    }
+
+    {
+      await reorderVideosPlaylist({
+        url: servers[0].url,
+        token: servers[0].accessToken,
+        playlistId: playlistServer1Id,
+        elementAttrs: {
+          startPosition: 1,
+          reorderLength: 3,
+          insertAfterPosition: 4
+        }
+      })
+
+      await waitJobs(servers)
+
+      for (const server of servers) {
+        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+        const names = res.body.data.map(v => v.name)
+
+        expect(names).to.deep.equal([
+          'video 3 server 1',
+          'video 0 server 1',
+          'video 2 server 3',
+          'video 1 server 3',
+          'video 4 server 1',
+          'NSFW video'
+        ])
+      }
+    }
+
+    {
+      await reorderVideosPlaylist({
+        url: servers[0].url,
+        token: servers[0].accessToken,
+        playlistId: playlistServer1Id,
+        elementAttrs: {
+          startPosition: 6,
+          insertAfterPosition: 3
+        }
+      })
+
+      await waitJobs(servers)
+
+      for (const server of servers) {
+        const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+        const videos: Video[] = res.body.data
+
+        const names = videos.map(v => v.name)
+
+        expect(names).to.deep.equal([
+          'video 3 server 1',
+          'video 0 server 1',
+          'video 2 server 3',
+          'NSFW video',
+          'video 1 server 3',
+          'video 4 server 1'
+        ])
+
+        for (let i = 1; i <= videos.length; i++) {
+          expect(videos[i - 1].playlistElement.position).to.equal(i)
+        }
+      }
+    }
   })
 
   it('Should update startTimestamp/endTimestamp of some elements', async function () {
-
+    this.timeout(30000)
+
+    await updateVideoPlaylistElement({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistId: playlistServer1Id,
+      videoId: servers[0].videos[3].uuid,
+      elementAttrs: {
+        startTimestamp: 1
+      }
+    })
+
+    await updateVideoPlaylistElement({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistId: playlistServer1Id,
+      videoId: servers[0].videos[4].uuid,
+      elementAttrs: {
+        stopTimestamp: null
+      }
+    })
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+      const videos: Video[] = res.body.data
+
+      expect(videos[0].name).to.equal('video 3 server 1')
+      expect(videos[0].playlistElement.position).to.equal(1)
+      expect(videos[0].playlistElement.startTimestamp).to.equal(1)
+      expect(videos[0].playlistElement.stopTimestamp).to.equal(35)
+
+      expect(videos[5].name).to.equal('video 4 server 1')
+      expect(videos[5].playlistElement.position).to.equal(6)
+      expect(videos[5].playlistElement.startTimestamp).to.equal(45)
+      expect(videos[5].playlistElement.stopTimestamp).to.be.null
+    }
   })
 
   it('Should delete some elements', async function () {
+    this.timeout(30000)
+
+    await removeVideoFromPlaylist({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistId: playlistServer1Id,
+      videoId: servers[0].videos[3].uuid
+    })
+
+    await removeVideoFromPlaylist({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistId: playlistServer1Id,
+      videoId: nsfwVideoServer1
+    })
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
+
+      expect(res.body.total).to.equal(4)
+
+      const videos: Video[] = res.body.data
+      expect(videos).to.have.lengthOf(4)
 
+      expect(videos[ 0 ].name).to.equal('video 0 server 1')
+      expect(videos[ 0 ].playlistElement.position).to.equal(1)
+
+      expect(videos[ 1 ].name).to.equal('video 2 server 3')
+      expect(videos[ 1 ].playlistElement.position).to.equal(2)
+
+      expect(videos[ 2 ].name).to.equal('video 1 server 3')
+      expect(videos[ 2 ].playlistElement.position).to.equal(3)
+
+      expect(videos[ 3 ].name).to.equal('video 4 server 1')
+      expect(videos[ 3 ].playlistElement.position).to.equal(4)
+    }
   })
 
   it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
+    this.timeout(30000)
 
+    await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      await getVideoPlaylist(server.url, playlistServer1UUID, 404)
+    }
   })
 
   it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
+    this.timeout(30000)
 
+    for (const server of servers) {
+      await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.serverNumber)
+    }
   })
 
   it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
+    this.timeout(30000)
 
+    const finder = data => data.find(p => p.displayName === 'my super playlist')
+
+    {
+      const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
+      expect(res.body.total).to.equal(2)
+      expect(finder(res.body.data)).to.not.be.undefined
+    }
+
+    await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+
+    {
+      const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
+      expect(res.body.total).to.equal(1)
+
+      expect(finder(res.body.data)).to.be.undefined
+    }
   })
 
-  it('Should delete a channel and remove the associated playlist', async function () {
+  it('Should delete a channel and put the associated playlist in private mode', async function () {
+    this.timeout(30000)
+
+    const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
+    const videoChannelId = res.body.videoChannel.id
 
+    const res2 = await createVideoPlaylist({
+      url: servers[0].url,
+      token: servers[0].accessToken,
+      playlistAttrs: {
+        displayName: 'channel playlist',
+        privacy: VideoPlaylistPrivacy.PUBLIC,
+        videoChannelId
+      }
+    })
+    const videoPlaylistUUID = res2.body.videoPlaylist.uuid
+
+    await waitJobs(servers)
+
+    await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
+
+    await waitJobs(servers)
+
+    const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
+    expect(res3.body.displayName).to.equal('channel playlist')
+    expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
+
+    await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404)
   })
 
   it('Should delete an account and delete its playlists', async function () {
+    this.timeout(30000)
+
+    const user = { username: 'user_1', password: 'password' }
+    const res = await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
+
+    const userId = res.body.user.id
+    const userAccessToken = await userLogin(servers[0], user)
 
+    await createVideoPlaylist({
+      url: servers[0].url,
+      token: userAccessToken,
+      playlistAttrs: {
+        displayName: 'playlist to be deleted',
+        privacy: VideoPlaylistPrivacy.PUBLIC
+      }
+    })
+
+    await waitJobs(servers)
+
+    const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
+
+    {
+      for (const server of [ servers[0], servers[1] ]) {
+        const res = await getVideoPlaylistsList(server.url, 0, 15)
+        expect(finder(res.body.data)).to.not.be.undefined
+      }
+    }
+
+    await removeUser(servers[0].url, userId, servers[0].accessToken)
+    await waitJobs(servers)
+
+    {
+      for (const server of [ servers[0], servers[1] ]) {
+        const res = await getVideoPlaylistsList(server.url, 0, 15)
+        expect(finder(res.body.data)).to.be.undefined
+      }
+    }
   })
 
   after(async function () {
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg
new file mode 100644 (file)
index 0000000..19db4f1
Binary files /dev/null and b/server/tests/fixtures/thumbnail-playlist.jpg differ
index 5f6733f92a1b8fa143f236a0d52aa646742fa48d..c11a23a69f806de614eab54fff5bc45c25981d86 100644 (file)
@@ -13,6 +13,9 @@ export interface PlaylistObject {
 
   icon: ActivityIconObject
 
+  published: string
+  updated: string
+
   orderedItems?: string[]
 
   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 (file)
index 0000000..49233b7
--- /dev/null
@@ -0,0 +1,4 @@
+export enum VideoPlaylistType {
+  REGULAR = 1,
+  WATCH_LATER = 2
+}
index 6aa04048c19dd8b654d0ee61d4c512d09a156469..7fec0e42b57ad49e09590c03735f9c8e0d46918e 100644 (file)
@@ -1,6 +1,7 @@
 import { AccountSummary } from '../../actors/index'
 import { VideoChannelSummary, VideoConstant } from '..'
 import { VideoPlaylistPrivacy } from './video-playlist-privacy.model'
+import { VideoPlaylistType } from './video-playlist-type.model'
 
 export interface VideoPlaylist {
   id: number
@@ -15,6 +16,8 @@ export interface VideoPlaylist {
 
   videosLength: number
 
+  type: VideoConstant<VideoPlaylistType>
+
   createdAt: Date | string
   updatedAt: Date | string
 
index dc2d4abe566362878fc40eb262edb6717c21c9bf..3532fb429e75801a3c8a6fe840a54dcdfeb1849c 100644 (file)
@@ -77,6 +77,8 @@ function makeUploadRequest (options: {
   Object.keys(options.fields).forEach(field => {
     const value = options.fields[field]
 
+    if (value === undefined) return
+
     if (Array.isArray(value)) {
       for (let i = 0; i < value.length; i++) {
         req.field(field + '[' + i + ']', value[i])
index bde7dd5c286ee22691ac34d5c705d5c96ce2fb0a..5288d253a5b639f8f0d0f9361da7d88a785ab535 100644 (file)
@@ -6,6 +6,7 @@ import { root, wait } from '../miscs/miscs'
 import { readdir, readFile } from 'fs-extra'
 import { existsSync } from 'fs'
 import { expect } from 'chai'
+import { VideoChannel } from '../../models/videos'
 
 interface ServerInfo {
   app: ChildProcess,
@@ -25,6 +26,7 @@ interface ServerInfo {
   }
 
   accessToken?: string
+  videoChannel?: VideoChannel
 
   video?: {
     id: number
@@ -39,6 +41,8 @@ interface ServerInfo {
     id: number
     uuid: string
   }
+
+  videos?: { id: number, uuid: string }[]
 }
 
 function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
index 7191b263e4c8174f994d72e06c6866f345ca4212..e3c14a4a380ae6369e3e22370914db011bdd3ada 100644 (file)
@@ -3,6 +3,7 @@ import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '..
 
 import { UserRole } from '../../index'
 import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
+import { ServerInfo, userLogin } from '..'
 
 function createUser (
   url: string,
@@ -32,6 +33,13 @@ function createUser (
           .expect(specialStatus)
 }
 
+async function generateUserAccessToken (server: ServerInfo, username: string) {
+  const password = 'my super password'
+  await createUser(server.url, server.accessToken, username, password)
+
+  return userLogin(server, { username, password })
+}
+
 function registerUser (url: string, username: string, password: string, specialStatus = 204) {
   const path = '/api/v1/users/register'
   const body = {
@@ -300,5 +308,6 @@ export {
   resetPassword,
   updateMyAvatar,
   askSendVerifyEmail,
+  generateUserAccessToken,
   verifyEmail
 }
index 3935c261e9803a4d047af6f99c993e3369dd2cea..93a257bf9cf9d1abeee86ecc62a613e734374431 100644 (file)
@@ -1,6 +1,8 @@
 import * as request from 'supertest'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
 import { updateAvatarRequest } from '../requests/requests'
+import { getMyUserInformation, ServerInfo } from '..'
+import { User } from '../..'
 
 function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
   const path = '/api/v1/video-channels'
@@ -105,6 +107,19 @@ function updateVideoChannelAvatar (options: {
   return updateAvatarRequest(Object.assign(options, { path }))
 }
 
+function setDefaultVideoChannel (servers: ServerInfo[]) {
+  const tasks: Promise<any>[] = []
+
+  for (const server of servers) {
+    const p = getMyUserInformation(server.url, server.accessToken)
+      .then(res => server.videoChannel = (res.body as User).videoChannels[0])
+
+    tasks.push(p)
+  }
+
+  return Promise.all(tasks)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -114,5 +129,6 @@ export {
   addVideoChannel,
   updateVideoChannel,
   deleteVideoChannel,
-  getVideoChannel
+  getVideoChannel,
+  setDefaultVideoChannel
 }
index 21285688a4c0be1b52ec0619bca201ebb5432aca..4af52ec0fd76f6d6feb1201d67f2fc378e3063f9 100644 (file)
@@ -4,6 +4,12 @@ import { omit } from 'lodash'
 import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
 import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
 import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
+import { videoUUIDToId } from './videos'
+import { join } from 'path'
+import { root } from '..'
+import { readdir } from 'fs-extra'
+import { expect } from 'chai'
+import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
 
 function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
   const path = '/api/v1/video-playlists'
@@ -17,7 +23,67 @@ function getVideoPlaylistsList (url: string, start: number, count: number, sort?
   return makeGetRequest({
     url,
     path,
-    query
+    query,
+    statusCodeExpected: 200
+  })
+}
+
+function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
+  const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
+
+  const query = {
+    start,
+    count,
+    sort
+  }
+
+  return makeGetRequest({
+    url,
+    path,
+    query,
+    statusCodeExpected: 200
+  })
+}
+
+function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string) {
+  const path = '/api/v1/accounts/' + accountName + '/video-playlists'
+
+  const query = {
+    start,
+    count,
+    sort
+  }
+
+  return makeGetRequest({
+    url,
+    path,
+    query,
+    statusCodeExpected: 200
+  })
+}
+
+function getAccountPlaylistsListWithToken (
+  url: string,
+  token: string,
+  accountName: string,
+  start: number,
+  count: number,
+  playlistType?: VideoPlaylistType
+) {
+  const path = '/api/v1/accounts/' + accountName + '/video-playlists'
+
+  const query = {
+    start,
+    count,
+    playlistType
+  }
+
+  return makeGetRequest({
+    url,
+    token,
+    path,
+    query,
+    statusCodeExpected: 200
   })
 }
 
@@ -31,6 +97,17 @@ function getVideoPlaylist (url: string, playlistId: number | string, statusCodeE
   })
 }
 
+function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = 200) {
+  const path = '/api/v1/video-playlists/' + playlistId
+
+  return makeGetRequest({
+    url,
+    token,
+    path,
+    statusCodeExpected
+  })
+}
+
 function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) {
   const path = '/api/v1/video-playlists/' + playlistId
 
@@ -93,13 +170,15 @@ function updateVideoPlaylist (options: {
   })
 }
 
-function addVideoInPlaylist (options: {
+async function addVideoInPlaylist (options: {
   url: string,
   token: string,
   playlistId: number | string,
-  elementAttrs: VideoPlaylistElementCreate
+  elementAttrs: VideoPlaylistElementCreate | { videoId: string }
   expectedStatus?: number
 }) {
+  options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
+
   const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
 
   return makePostBodyRequest({
@@ -135,7 +214,7 @@ function removeVideoFromPlaylist (options: {
   token: string,
   playlistId: number | string,
   videoId: number | string,
-  expectedStatus: number
+  expectedStatus?: number
 }) {
   const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
 
@@ -156,7 +235,7 @@ function reorderVideosPlaylist (options: {
     insertAfterPosition: number,
     reorderLength?: number
   },
-  expectedStatus: number
+  expectedStatus?: number
 }) {
   const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
 
@@ -165,15 +244,37 @@ function reorderVideosPlaylist (options: {
     path,
     token: options.token,
     fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 204
   })
 }
 
+async function checkPlaylistFilesWereRemoved (
+  playlistUUID: string,
+  serverNumber: number,
+  directories = [ 'thumbnails' ]
+) {
+  const testDirectory = 'test' + serverNumber
+
+  for (const directory of directories) {
+    const directoryPath = join(root(), testDirectory, directory)
+
+    const files = await readdir(directoryPath)
+    for (const file of files) {
+      expect(file).to.not.contain(playlistUUID)
+    }
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   getVideoPlaylistsList,
+  getVideoChannelPlaylistsList,
+  getAccountPlaylistsList,
+  getAccountPlaylistsListWithToken,
+
   getVideoPlaylist,
+  getVideoPlaylistWithToken,
 
   createVideoPlaylist,
   updateVideoPlaylist,
@@ -183,5 +284,7 @@ export {
   updateVideoPlaylistElement,
   removeVideoFromPlaylist,
 
-  reorderVideosPlaylist
+  reorderVideosPlaylist,
+
+  checkPlaylistFilesWereRemoved
 }
index 2c09f008604556cdc226a79f37047b4f33fe9545..16b5165f13349def7515b59364c1c1062d4b1215 100644 (file)
@@ -1,7 +1,7 @@
 /* tslint:disable:no-unused-expression */
 
 import { expect } from 'chai'
-import { existsSync, readdir, readFile } from 'fs-extra'
+import { pathExists, readdir, readFile } from 'fs-extra'
 import * as parseTorrent from 'parse-torrent'
 import { extname, join } from 'path'
 import * as request from 'supertest'
@@ -16,7 +16,7 @@ import {
   ServerInfo,
   testImage
 } from '../'
-
+import * as validator from 'validator'
 import { VideoDetails, VideoPrivacy } from '../../models/videos'
 import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
 import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
@@ -311,8 +311,8 @@ async function checkVideoFilesWereRemoved (
   for (const directory of directories) {
     const directoryPath = join(root(), testDirectory, directory)
 
-    const directoryExists = existsSync(directoryPath)
-    if (!directoryExists) continue
+    const directoryExists = await pathExists(directoryPath)
+    if (directoryExists === false) continue
 
     const files = await readdir(directoryPath)
     for (const file of files) {
@@ -597,12 +597,30 @@ async function completeVideoCheck (
   }
 }
 
+async function videoUUIDToId (url: string, id: number | string) {
+  if (validator.isUUID('' + id) === false) return id
+
+  const res = await getVideo(url, id)
+  return res.body.id
+}
+
+async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
+  const videoAttrs: any = { name: options.videoName }
+  if (options.nsfw) videoAttrs.nsfw = options.nsfw
+
+
+  const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
+
+  return { id: res.body.video.id, uuid: res.body.video.uuid }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   getVideoDescription,
   getVideoCategories,
   getVideoLicences,
+  videoUUIDToId,
   getVideoPrivacies,
   getVideoLanguages,
   getMyVideos,
@@ -624,5 +642,6 @@ export {
   getLocalVideos,
   completeVideoCheck,
   checkVideoFilesWereRemoved,
-  getPlaylistVideos
+  getPlaylistVideos,
+  uploadVideoAndGetId
 }