Refresh playlists
authorChocobozzz <me@florianbigard.com>
Tue, 19 Mar 2019 13:13:53 +0000 (14:13 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 19 Mar 2019 13:13:53 +0000 (14:13 +0100)
13 files changed:
server/controllers/api/video-playlist.ts
server/initializers/constants.ts
server/lib/activitypub/actor.ts
server/lib/activitypub/playlist.ts
server/lib/job-queue/handlers/activitypub-refresher.ts
server/middlewares/validators/videos/video-playlists.ts
server/models/activitypub/actor.ts
server/models/utils.ts
server/models/video/video-playlist.ts
server/models/video/video.ts
server/tests/api/activitypub/refresher.ts
shared/utils/miscs/sql.ts
shared/utils/videos/video-playlists.ts

index 5b1601c4eb303af5e48c910f8b232c1b29213857..feba30564d4e5c7b5f4cfe494993cdce899b5599 100644 (file)
@@ -40,6 +40,7 @@ import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playli
 import { copy, pathExists } from 'fs-extra'
 import { AccountModel } from '../../models/account/account'
 import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
+import { JobQueue } from '../../lib/job-queue'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
 
@@ -142,6 +143,11 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
 function getVideoPlaylist (req: express.Request, res: express.Response) {
   const videoPlaylist = res.locals.videoPlaylist
 
+  if (videoPlaylist.isOutdated()) {
+    JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } })
+            .catch(err => logger.error('Cannot create AP refresher job for playlist %s.', videoPlaylist.url, { err }))
+  }
+
   return res.json(videoPlaylist.toFormattedJSON())
 }
 
index 19e63d6fe5942f865709b09b60da74080a31d1f3..7fac8a4d6f68c59512e92a905fe66b97909b28c2 100644 (file)
@@ -584,7 +584,8 @@ const ACTIVITY_PUB = {
   },
   MAX_RECURSION_COMMENTS: 100,
   ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
-  VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
+  VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
+  VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
 }
 
 const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
@@ -724,6 +725,7 @@ if (isTestInstance() === true) {
   ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
   ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
   ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
+  ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
 
   CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
 
index f77df8b78d5c92820c0ce041fdcebb72cd7ce2d6..63e8106421f9fc49cd9ac2861e109bfa861a978c 100644 (file)
@@ -375,7 +375,8 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
   }
 
   if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
-    throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
+    logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
+    return { result: undefined, statusCode: requestResult.response.statusCode }
   }
 
   const followersCount = await fetchActorTotalItems(actorJSON.followers)
index 70389044e9b1cd24dc79db492254c477159466c2..c4a8f12ecbbc1b26e5fc64f50f2402a0e804d985 100644 (file)
@@ -95,7 +95,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
     return Promise.resolve()
   })
 
-  // Empty playlists generally do not have a miniature, so skip it
+  // Empty playlists generally do not have a miniature, so skip this
   if (accItems.length !== 0) {
     try {
       await generateThumbnailFromUrl(playlist, playlistObject.icon)
@@ -107,13 +107,45 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
   return resetVideoPlaylistElements(accItems, playlist)
 }
 
+async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
+  if (!videoPlaylist.isOutdated()) return videoPlaylist
+
+  try {
+    const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
+    if (statusCode === 404) {
+      logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
+
+      await videoPlaylist.destroy()
+      return undefined
+    }
+
+    if (playlistObject === undefined) {
+      logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
+
+      await videoPlaylist.setAsRefreshed()
+      return videoPlaylist
+    }
+
+    const byAccount = videoPlaylist.OwnerAccount
+    await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to)
+
+    return videoPlaylist
+  } catch (err) {
+    logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
+
+    await videoPlaylist.setAsRefreshed()
+    return videoPlaylist
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   createAccountPlaylists,
   playlistObjectToDBAttributes,
   playlistElementObjectToDBAttributes,
-  createOrUpdateVideoPlaylist
+  createOrUpdateVideoPlaylist,
+  refreshVideoPlaylistIfNeeded
 }
 
 // ---------------------------------------------------------------------------
@@ -162,3 +194,23 @@ function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityI
 
   return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
 }
+
+async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
+  const options = {
+    uri: playlistUrl,
+    method: 'GET',
+    json: true,
+    activityPub: true
+  }
+
+  logger.info('Fetching remote playlist %s.', playlistUrl)
+
+  const { response, body } = await doRequest(options)
+
+  if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
+    logger.debug('Remote video playlist JSON is not valid.', { body })
+    return { statusCode: response.statusCode, playlistObject: undefined }
+  }
+
+  return { statusCode: response.statusCode, playlistObject: body }
+}
index 454b975fefaeb89dcb5416c5ae08acda995b54bc..4d6c38cfa0136ee122af6dc839c49fb6e4861362 100644 (file)
@@ -1,11 +1,12 @@
 import * as Bull from 'bull'
 import { logger } from '../../../helpers/logger'
 import { fetchVideoByUrl } from '../../../helpers/video'
-import { refreshVideoIfNeeded, refreshActorIfNeeded } from '../../activitypub'
+import { refreshActorIfNeeded, refreshVideoIfNeeded, refreshVideoPlaylistIfNeeded } from '../../activitypub'
 import { ActorModel } from '../../../models/activitypub/actor'
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 
 export type RefreshPayload = {
-  type: 'video' | 'actor'
+  type: 'video' | 'video-playlist' | 'actor'
   url: string
 }
 
@@ -15,13 +16,13 @@ async function refreshAPObject (job: Bull.Job) {
   logger.info('Processing AP refresher in job %d for %s.', job.id, payload.url)
 
   if (payload.type === 'video') return refreshVideo(payload.url)
+  if (payload.type === 'video-playlist') return refreshVideoPlaylist(payload.url)
   if (payload.type === 'actor') return refreshActor(payload.url)
 }
 
 // ---------------------------------------------------------------------------
 
 export {
-  refreshActor,
   refreshAPObject
 }
 
@@ -50,5 +51,12 @@ async function refreshActor (actorUrl: string) {
   if (actor) {
     await refreshActorIfNeeded(actor, fetchType)
   }
+}
+
+async function refreshVideoPlaylist (playlistUrl: string) {
+  const playlist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(playlistUrl)
 
+  if (playlist) {
+    await refreshVideoPlaylistIfNeeded(playlist)
+  }
 }
index 55e09e354f66bdb1fc6fe68f9b4b51b06f5f5c06..6ba30fac9823c281b7a3386ad515083a251e6489 100644 (file)
@@ -8,8 +8,8 @@ import { doesVideoExist, isVideoImage } from '../../../helpers/custom-validators
 import { CONSTRAINTS_FIELDS } from '../../../initializers'
 import { isArrayOf, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntArray, toValueOrNull } from '../../../helpers/custom-validators/misc'
 import {
-  isVideoPlaylistDescriptionValid,
   doesVideoPlaylistExist,
+  isVideoPlaylistDescriptionValid,
   isVideoPlaylistNameValid,
   isVideoPlaylistPrivacyValid,
   isVideoPlaylistTimestampValid,
@@ -19,7 +19,6 @@ import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
 import { doesVideoChannelIdExist } from '../../../helpers/custom-validators/video-channels'
 import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
-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'
index 2fceb21ddf87cec326bfb59be39725361ce0b66e..7d91e8a4ac97ac92bc3e575fa64d43b16c5b08d6 100644 (file)
@@ -34,7 +34,7 @@ import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } fr
 import { AccountModel } from '../account/account'
 import { AvatarModel } from '../avatar/avatar'
 import { ServerModel } from '../server/server'
-import { throwIfNotValid } from '../utils'
+import { isOutdated, throwIfNotValid } from '../utils'
 import { VideoChannelModel } from '../video/video-channel'
 import { ActorFollowModel } from './actor-follow'
 import { VideoModel } from '../video/video'
@@ -532,11 +532,6 @@ export class ActorModel extends Model<ActorModel> {
   isOutdated () {
     if (this.isOwned()) return false
 
-    const now = Date.now()
-    const createdAtTime = this.createdAt.getTime()
-    const updatedAtTime = this.updatedAt.getTime()
-
-    return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
-      (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
+    return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
   }
 }
index 4ebd07dabbec5c1745d0faddc2fab73c040df554..f8a71b270df9720359d2e3083a54f7e7a35cc5ee 100644 (file)
@@ -1,5 +1,6 @@
 import { Sequelize } from 'sequelize-typescript'
 import * as validator from 'validator'
+import { ACTIVITY_PUB } from '../initializers'
 
 type SortType = { sortModel: any, sortValue: string }
 
@@ -44,6 +45,14 @@ function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id',
   return [ firstSort, lastSort ]
 }
 
+function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
+  const now = Date.now()
+  const createdAtTime = model.createdAt.getTime()
+  const updatedAtTime = model.updatedAt.getTime()
+
+  return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
+}
+
 function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value') {
   if (validator(value) === false) {
     throw new Error(`"${value}" is not a valid ${fieldName}.`)
@@ -108,7 +117,8 @@ export {
   throwIfNotValid,
   buildServerIdsFollowedBy,
   buildTrigramSearchIndex,
-  buildWhereIdOrUUID
+  buildWhereIdOrUUID,
+  isOutdated
 }
 
 // ---------------------------------------------------------------------------
index 7dbe4ce8d3cf61b9b7949619e95290bb9ba09b59..08e4d32c8cd45153ada4a0c5c2853e6d6aeb34d8 100644 (file)
@@ -17,7 +17,7 @@ import {
 } from 'sequelize-typescript'
 import * as Sequelize from 'sequelize'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, throwIfNotValid } from '../utils'
+import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
 import {
   isVideoPlaylistDescriptionValid,
   isVideoPlaylistNameValid,
@@ -25,6 +25,7 @@ import {
 } from '../../helpers/custom-validators/video-playlists'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import {
+  ACTIVITY_PUB,
   CONFIG,
   CONSTRAINTS_FIELDS,
   STATIC_PATHS,
@@ -429,10 +430,22 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
   }
 
+  setAsRefreshed () {
+    this.changed('updatedAt', true)
+
+    return this.save()
+  }
+
   isOwned () {
     return this.OwnerAccount.isOwned()
   }
 
+  isOutdated () {
+    if (this.isOwned()) return false
+
+    return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
+  }
+
   toFormattedJSON (): VideoPlaylist {
     return {
       id: this.id,
index 946be60958bdd4e11e29f324939c51e9f74624cf..fb037c21a19ec4819c0567510c87186d56c3a74b 100644 (file)
@@ -77,7 +77,7 @@ import {
   buildTrigramSearchIndex,
   buildWhereIdOrUUID,
   createSimilarityAttribute,
-  getVideoSort,
+  getVideoSort, isOutdated,
   throwIfNotValid
 } from '../utils'
 import { TagModel } from './tag'
@@ -1547,7 +1547,7 @@ export class VideoModel extends Model<VideoModel> {
       attributes: query.attributes,
       order: [ // Keep original order
         Sequelize.literal(
-          ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ')
+          ids.map(id => `"VideoModel".id = ${id}`).join(', ')
         )
       ]
     }
@@ -1767,12 +1767,7 @@ export class VideoModel extends Model<VideoModel> {
   isOutdated () {
     if (this.isOwned()) return false
 
-    const now = Date.now()
-    const createdAtTime = this.createdAt.getTime()
-    const updatedAtTime = this.updatedAt.getTime()
-
-    return (now - createdAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL &&
-      (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
+    return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
   }
 
   setAsRefreshed () {
index 62ad8a0b566e97013c4a999174979dddaa2b11a5..ae485907631c5e92c0cce7b47fa7f264b513bb56 100644 (file)
 
 import 'mocha'
 import {
+  createVideoPlaylist,
   doubleFollow,
   flushAndRunMultipleServers,
+  generateUserAccessToken,
   getVideo,
+  getVideoPlaylist,
   killallServers,
   reRunServer,
   ServerInfo,
   setAccessTokensToServers,
+  setActorField,
+  setDefaultVideoChannel,
+  setPlaylistField,
+  setVideoField,
   uploadVideo,
+  uploadVideoAndGetId,
   wait,
-  setVideoField,
   waitJobs
 } from '../../../../shared/utils'
+import { getAccount } from '../../../../shared/utils/users/accounts'
+import { VideoPlaylistPrivacy } from '../../../../shared/models/videos'
 
 describe('Test AP refresher', function () {
   let servers: ServerInfo[] = []
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
+  let playlistUUID1: string
+  let playlistUUID2: string
 
   before(async function () {
     this.timeout(60000)
 
-    servers = await flushAndRunMultipleServers(2)
+    servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: false } })
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    {
+      videoUUID1 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video1' })).uuid
+      videoUUID2 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video2' })).uuid
+      videoUUID3 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video3' })).uuid
+    }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
-      videoUUID1 = res.body.video.uuid
+      const a1 = await generateUserAccessToken(servers[1], 'user1')
+      await uploadVideo(servers[1].url, a1, { name: 'video4' })
+
+      const a2 = await generateUserAccessToken(servers[1], 'user2')
+      await uploadVideo(servers[1].url, a2, { name: 'video5' })
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
-      videoUUID2 = res.body.video.uuid
+      const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
+      const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
+      playlistUUID1 = res.body.videoPlaylist.uuid
     }
 
     {
-      const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video3' })
-      videoUUID3 = res.body.video.uuid
+      const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
+      const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
+      playlistUUID2 = res.body.videoPlaylist.uuid
     }
 
     await doubleFollow(servers[0], servers[1])
   })
 
-  it('Should remove a deleted remote video', async function () {
-    this.timeout(60000)
+  describe('Videos refresher', function () {
+
+    it('Should remove a deleted remote video', async function () {
+      this.timeout(60000)
+
+      await wait(10000)
+
+      // Change UUID so the remote server returns a 404
+      await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
+
+      await getVideo(servers[ 0 ].url, videoUUID1)
+      await getVideo(servers[ 0 ].url, videoUUID2)
+
+      await waitJobs(servers)
 
-    await wait(10000)
+      await getVideo(servers[ 0 ].url, videoUUID1, 404)
+      await getVideo(servers[ 0 ].url, videoUUID2, 200)
+    })
 
-    // Change UUID so the remote server returns a 404
-    await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
+    it('Should not update a remote video if the remote instance is down', async function () {
+      this.timeout(60000)
 
-    await getVideo(servers[0].url, videoUUID1)
-    await getVideo(servers[0].url, videoUUID2)
+      killallServers([ servers[ 1 ] ])
 
-    await waitJobs(servers)
+      await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
 
-    await getVideo(servers[0].url, videoUUID1, 404)
-    await getVideo(servers[0].url, videoUUID2, 200)
+      // Video will need a refresh
+      await wait(10000)
+
+      await getVideo(servers[ 0 ].url, videoUUID3)
+      // The refresh should fail
+      await waitJobs([ servers[ 0 ] ])
+
+      await reRunServer(servers[ 1 ])
+
+      // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances)
+      await getVideo(servers[ 0 ].url, videoUUID3)
+      await waitJobs(servers)
+
+      await getVideo(servers[ 0 ].url, videoUUID3, 200)
+    })
   })
 
-  it('Should not update a remote video if the remote instance is down', async function () {
-    this.timeout(60000)
+  describe('Actors refresher', function () {
+
+    it('Should remove a deleted actor', async function () {
+      this.timeout(60000)
+
+      await wait(10000)
+
+      // Change actor name so the remote server returns a 404
+      await setActorField(2, 'http://localhost:9002/accounts/user2', 'preferredUsername', 'toto')
+
+      await getAccount(servers[ 0 ].url, 'user1@localhost:9002')
+      await getAccount(servers[ 0 ].url, 'user2@localhost:9002')
+
+      await waitJobs(servers)
+
+      await getAccount(servers[ 0 ].url, 'user1@localhost:9002', 200)
+      await getAccount(servers[ 0 ].url, 'user2@localhost:9002', 404)
+    })
+  })
 
-    killallServers([ servers[1] ])
+  describe('Playlist refresher', function () {
 
-    await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
+    it('Should remove a deleted playlist', async function () {
+      this.timeout(60000)
 
-    // Video will need a refresh
-    await wait(10000)
+      await wait(10000)
 
-    await getVideo(servers[0].url, videoUUID3)
-    // The refresh should fail
-    await waitJobs([ servers[0] ])
+      // Change UUID so the remote server returns a 404
+      await setPlaylistField(2, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
 
-    await reRunServer(servers[1])
+      await getVideoPlaylist(servers[ 0 ].url, playlistUUID1)
+      await getVideoPlaylist(servers[ 0 ].url, playlistUUID2)
 
-    // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances)
-    await getVideo(servers[0].url, videoUUID3)
-    await waitJobs(servers)
+      await waitJobs(servers)
 
-    await getVideo(servers[0].url, videoUUID3, 200)
+      await getVideoPlaylist(servers[ 0 ].url, playlistUUID1, 200)
+      await getVideoPlaylist(servers[ 0 ].url, playlistUUID2, 404)
+    })
   })
 
   after(async function () {
index bb3f63837d62e8c9e27b86d9f2e064391189572d..1ce3d801afb4b39f48a2698e3aa129a667d7b540 100644 (file)
@@ -40,6 +40,14 @@ function setVideoField (serverNumber: number, uuid: string, field: string, value
   return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
 }
 
+function setPlaylistField (serverNumber: number, uuid: string, field: string, value: string) {
+  const seq = getSequelize(serverNumber)
+
+  const options = { type: Sequelize.QueryTypes.UPDATE }
+
+  return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
+}
+
 async function closeAllSequelize (servers: any[]) {
   for (let i = 1; i <= servers.length; i++) {
     if (sequelizes[ i ]) {
@@ -51,6 +59,7 @@ async function closeAllSequelize (servers: any[]) {
 
 export {
   setVideoField,
+  setPlaylistField,
   setActorField,
   closeAllSequelize
 }
index 7568852dcb51b3f1e3de0b8c6d6854ec00541843..4d110a13124cebd1bd47ae286788a7d66cf3e013 100644 (file)
@@ -127,7 +127,7 @@ function createVideoPlaylist (options: {
   playlistAttrs: VideoPlaylistCreate,
   expectedStatus?: number
 }) {
-  const path = '/api/v1/video-playlists/'
+  const path = '/api/v1/video-playlists'
 
   const fields = omit(options.playlistAttrs, 'thumbnailfile')