diff options
-rw-r--r-- | client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts | 35 | ||||
-rw-r--r-- | client/src/app/shared/video-playlist/video-playlist.model.ts | 13 | ||||
-rw-r--r-- | server/controllers/api/video-playlist.ts | 67 | ||||
-rw-r--r-- | server/controllers/api/videos/import.ts | 4 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 8 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/initializers/migrations/0415-thumbnail-auto-generated.ts | 35 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 3 | ||||
-rw-r--r-- | server/lib/thumbnail.ts | 26 | ||||
-rw-r--r-- | server/models/video/thumbnail.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-playlist-element.ts | 18 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 5 | ||||
-rw-r--r-- | server/tests/api/videos/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/videos/video-playlist-thumbnails.ts | 262 | ||||
-rw-r--r-- | server/tests/api/videos/video-playlists.ts | 1 |
15 files changed, 443 insertions, 43 deletions
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts index 6434b9e50..6f307a058 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts | |||
@@ -63,24 +63,26 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
63 | 63 | ||
64 | if (oldPosition > insertAfter) insertAfter-- | 64 | if (oldPosition > insertAfter) insertAfter-- |
65 | 65 | ||
66 | this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) | ||
67 | .subscribe( | ||
68 | () => { /* nothing to do */ }, | ||
69 | |||
70 | err => this.notifier.error(err.message) | ||
71 | ) | ||
72 | |||
73 | const element = this.playlistElements[previousIndex] | 66 | const element = this.playlistElements[previousIndex] |
74 | 67 | ||
75 | this.playlistElements.splice(previousIndex, 1) | 68 | this.playlistElements.splice(previousIndex, 1) |
76 | this.playlistElements.splice(newIndex, 0, element) | 69 | this.playlistElements.splice(newIndex, 0, element) |
77 | 70 | ||
78 | this.reorderClientPositions() | 71 | this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) |
72 | .subscribe( | ||
73 | () => { | ||
74 | this.reorderClientPositions() | ||
75 | }, | ||
76 | |||
77 | err => this.notifier.error(err.message) | ||
78 | ) | ||
79 | } | 79 | } |
80 | 80 | ||
81 | onElementRemoved (element: VideoPlaylistElement) { | 81 | onElementRemoved (element: VideoPlaylistElement) { |
82 | const oldFirst = this.findFirst() | ||
83 | |||
82 | this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) | 84 | this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) |
83 | this.reorderClientPositions() | 85 | this.reorderClientPositions(oldFirst) |
84 | } | 86 | } |
85 | 87 | ||
86 | onNearOfBottom () { | 88 | onNearOfBottom () { |
@@ -110,12 +112,25 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
110 | }) | 112 | }) |
111 | } | 113 | } |
112 | 114 | ||
113 | private reorderClientPositions () { | 115 | private reorderClientPositions (first?: VideoPlaylistElement) { |
116 | if (this.playlistElements.length === 0) return | ||
117 | |||
118 | const oldFirst = first || this.findFirst() | ||
114 | let i = 1 | 119 | let i = 1 |
115 | 120 | ||
116 | for (const element of this.playlistElements) { | 121 | for (const element of this.playlistElements) { |
117 | element.position = i | 122 | element.position = i |
118 | i++ | 123 | i++ |
119 | } | 124 | } |
125 | |||
126 | // Reload playlist thumbnail if the first element changed | ||
127 | const newFirst = this.findFirst() | ||
128 | if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { | ||
129 | this.playlist.refreshThumbnail() | ||
130 | } | ||
131 | } | ||
132 | |||
133 | private findFirst () { | ||
134 | return this.playlistElements.find(e => e.position === 1) | ||
120 | } | 135 | } |
121 | } | 136 | } |
diff --git a/client/src/app/shared/video-playlist/video-playlist.model.ts b/client/src/app/shared/video-playlist/video-playlist.model.ts index 7e311aa54..6f27e7475 100644 --- a/client/src/app/shared/video-playlist/video-playlist.model.ts +++ b/client/src/app/shared/video-playlist/video-playlist.model.ts | |||
@@ -38,6 +38,9 @@ export class VideoPlaylist implements ServerVideoPlaylist { | |||
38 | videoChannelBy?: string | 38 | videoChannelBy?: string |
39 | videoChannelAvatarUrl?: string | 39 | videoChannelAvatarUrl?: string |
40 | 40 | ||
41 | private thumbnailVersion: number | ||
42 | private originThumbnailUrl: string | ||
43 | |||
41 | constructor (hash: ServerVideoPlaylist, translations: {}) { | 44 | constructor (hash: ServerVideoPlaylist, translations: {}) { |
42 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 45 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
43 | 46 | ||
@@ -54,6 +57,7 @@ export class VideoPlaylist implements ServerVideoPlaylist { | |||
54 | 57 | ||
55 | if (this.thumbnailPath) { | 58 | if (this.thumbnailPath) { |
56 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath | 59 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath |
60 | this.originThumbnailUrl = this.thumbnailUrl | ||
57 | } else { | 61 | } else { |
58 | this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' | 62 | this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' |
59 | } | 63 | } |
@@ -81,4 +85,13 @@ export class VideoPlaylist implements ServerVideoPlaylist { | |||
81 | this.displayName = peertubeTranslate(this.displayName, translations) | 85 | this.displayName = peertubeTranslate(this.displayName, translations) |
82 | } | 86 | } |
83 | } | 87 | } |
88 | |||
89 | refreshThumbnail () { | ||
90 | if (!this.originThumbnailUrl) return | ||
91 | |||
92 | if (!this.thumbnailVersion) this.thumbnailVersion = 0 | ||
93 | this.thumbnailVersion++ | ||
94 | |||
95 | this.thumbnailUrl = this.originThumbnailUrl + '?v' + this.thumbnailVersion | ||
96 | } | ||
84 | } | 97 | } |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 540120cca..bd454f553 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -40,6 +40,7 @@ import { JobQueue } from '../../lib/job-queue' | |||
40 | import { CONFIG } from '../../initializers/config' | 40 | import { CONFIG } from '../../initializers/config' |
41 | import { sequelizeTypescript } from '../../initializers/database' | 41 | import { sequelizeTypescript } from '../../initializers/database' |
42 | import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' | 42 | import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' |
43 | import { VideoModel } from '../../models/video/video' | ||
43 | 44 | ||
44 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) | 45 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) |
45 | 46 | ||
@@ -171,13 +172,16 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
171 | 172 | ||
172 | const thumbnailField = req.files['thumbnailfile'] | 173 | const thumbnailField = req.files['thumbnailfile'] |
173 | const thumbnailModel = thumbnailField | 174 | const thumbnailModel = thumbnailField |
174 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist) | 175 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false) |
175 | : undefined | 176 | : undefined |
176 | 177 | ||
177 | const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { | 178 | const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { |
178 | const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) | 179 | const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) |
179 | 180 | ||
180 | if (thumbnailModel) await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t) | 181 | if (thumbnailModel) { |
182 | thumbnailModel.automaticallyGenerated = false | ||
183 | await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t) | ||
184 | } | ||
181 | 185 | ||
182 | // We need more attributes for the federation | 186 | // We need more attributes for the federation |
183 | videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) | 187 | videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) |
@@ -206,7 +210,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
206 | 210 | ||
207 | const thumbnailField = req.files['thumbnailfile'] | 211 | const thumbnailField = req.files['thumbnailfile'] |
208 | const thumbnailModel = thumbnailField | 212 | const thumbnailModel = thumbnailField |
209 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance) | 213 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false) |
210 | : undefined | 214 | : undefined |
211 | 215 | ||
212 | try { | 216 | try { |
@@ -239,7 +243,10 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
239 | 243 | ||
240 | const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) | 244 | const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) |
241 | 245 | ||
242 | if (thumbnailModel) await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t) | 246 | if (thumbnailModel) { |
247 | thumbnailModel.automaticallyGenerated = false | ||
248 | await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t) | ||
249 | } | ||
243 | 250 | ||
244 | const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE | 251 | const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE |
245 | 252 | ||
@@ -301,23 +308,17 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response) | |||
301 | videoPlaylist.changed('updatedAt', true) | 308 | videoPlaylist.changed('updatedAt', true) |
302 | await videoPlaylist.save({ transaction: t }) | 309 | await videoPlaylist.save({ transaction: t }) |
303 | 310 | ||
304 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
305 | |||
306 | return playlistElement | 311 | return playlistElement |
307 | }) | 312 | }) |
308 | 313 | ||
309 | // If the user did not set a thumbnail, automatically take the video thumbnail | 314 | // If the user did not set a thumbnail, automatically take the video thumbnail |
310 | if (videoPlaylist.hasThumbnail() === false) { | 315 | if (videoPlaylist.hasThumbnail() === false || (videoPlaylist.hasGeneratedThumbnail() && playlistElement.position === 1)) { |
311 | logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) | 316 | await generateThumbnailForPlaylist(videoPlaylist, video) |
312 | |||
313 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename) | ||
314 | const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true) | ||
315 | |||
316 | thumbnailModel.videoPlaylistId = videoPlaylist.id | ||
317 | |||
318 | await thumbnailModel.save() | ||
319 | } | 317 | } |
320 | 318 | ||
319 | sendUpdateVideoPlaylist(videoPlaylist, undefined) | ||
320 | .catch(err => logger.error('Cannot send video playlist update.', { err })) | ||
321 | |||
321 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) | 322 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) |
322 | 323 | ||
323 | return res.json({ | 324 | return res.json({ |
@@ -365,11 +366,17 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo | |||
365 | videoPlaylist.changed('updatedAt', true) | 366 | videoPlaylist.changed('updatedAt', true) |
366 | await videoPlaylist.save({ transaction: t }) | 367 | await videoPlaylist.save({ transaction: t }) |
367 | 368 | ||
368 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
369 | |||
370 | logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) | 369 | logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) |
371 | }) | 370 | }) |
372 | 371 | ||
372 | // Do we need to regenerate the default thumbnail? | ||
373 | if (positionToDelete === 1 && videoPlaylist.hasGeneratedThumbnail()) { | ||
374 | await regeneratePlaylistThumbnail(videoPlaylist) | ||
375 | } | ||
376 | |||
377 | sendUpdateVideoPlaylist(videoPlaylist, undefined) | ||
378 | .catch(err => logger.error('Cannot send video playlist update.', { err })) | ||
379 | |||
373 | return res.type('json').status(204).end() | 380 | return res.type('json').status(204).end() |
374 | } | 381 | } |
375 | 382 | ||
@@ -413,8 +420,13 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons | |||
413 | await sendUpdateVideoPlaylist(videoPlaylist, t) | 420 | await sendUpdateVideoPlaylist(videoPlaylist, t) |
414 | }) | 421 | }) |
415 | 422 | ||
423 | // The first element changed | ||
424 | if ((start === 1 || insertAfter === 0) && videoPlaylist.hasGeneratedThumbnail()) { | ||
425 | await regeneratePlaylistThumbnail(videoPlaylist) | ||
426 | } | ||
427 | |||
416 | logger.info( | 428 | logger.info( |
417 | 'Reordered playlist %s (inserted after %d elements %d - %d).', | 429 | 'Reordered playlist %s (inserted after position %d elements %d - %d).', |
418 | videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1 | 430 | videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1 |
419 | ) | 431 | ) |
420 | 432 | ||
@@ -440,3 +452,22 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon | |||
440 | } | 452 | } |
441 | return res.json(getFormattedObjects(resultList.data, resultList.total, options)) | 453 | return res.json(getFormattedObjects(resultList.data, resultList.total, options)) |
442 | } | 454 | } |
455 | |||
456 | async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) { | ||
457 | await videoPlaylist.Thumbnail.destroy() | ||
458 | videoPlaylist.Thumbnail = null | ||
459 | |||
460 | const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id) | ||
461 | if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video) | ||
462 | } | ||
463 | |||
464 | async function generateThumbnailForPlaylist (videoPlaylist: VideoPlaylistModel, video: VideoModel) { | ||
465 | logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) | ||
466 | |||
467 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename) | ||
468 | const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true) | ||
469 | |||
470 | thumbnailModel.videoPlaylistId = videoPlaylist.id | ||
471 | |||
472 | videoPlaylist.Thumbnail = await thumbnailModel.save() | ||
473 | } | ||
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 1f08fe20a..04c9b547b 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -207,7 +207,7 @@ async function processThumbnail (req: express.Request, video: VideoModel) { | |||
207 | if (thumbnailField) { | 207 | if (thumbnailField) { |
208 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | 208 | const thumbnailPhysicalFile = thumbnailField[ 0 ] |
209 | 209 | ||
210 | return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE) | 210 | return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false) |
211 | } | 211 | } |
212 | 212 | ||
213 | return undefined | 213 | return undefined |
@@ -218,7 +218,7 @@ async function processPreview (req: express.Request, video: VideoModel) { | |||
218 | if (previewField) { | 218 | if (previewField) { |
219 | const previewPhysicalFile = previewField[0] | 219 | const previewPhysicalFile = previewField[0] |
220 | 220 | ||
221 | return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW) | 221 | return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW, false) |
222 | } | 222 | } |
223 | 223 | ||
224 | return undefined | 224 | return undefined |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 973bf1123..155ca4678 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -223,13 +223,13 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
223 | // Process thumbnail or create it from the video | 223 | // Process thumbnail or create it from the video |
224 | const thumbnailField = req.files['thumbnailfile'] | 224 | const thumbnailField = req.files['thumbnailfile'] |
225 | const thumbnailModel = thumbnailField | 225 | const thumbnailModel = thumbnailField |
226 | ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE) | 226 | ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE, false) |
227 | : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE) | 227 | : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE) |
228 | 228 | ||
229 | // Process preview or create it from the video | 229 | // Process preview or create it from the video |
230 | const previewField = req.files['previewfile'] | 230 | const previewField = req.files['previewfile'] |
231 | const previewModel = previewField | 231 | const previewModel = previewField |
232 | ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW) | 232 | ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW, false) |
233 | : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW) | 233 | : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW) |
234 | 234 | ||
235 | // Create the torrent file | 235 | // Create the torrent file |
@@ -329,11 +329,11 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
329 | 329 | ||
330 | // Process thumbnail or create it from the video | 330 | // Process thumbnail or create it from the video |
331 | const thumbnailModel = req.files && req.files['thumbnailfile'] | 331 | const thumbnailModel = req.files && req.files['thumbnailfile'] |
332 | ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE) | 332 | ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE, false) |
333 | : undefined | 333 | : undefined |
334 | 334 | ||
335 | const previewModel = req.files && req.files['previewfile'] | 335 | const previewModel = req.files && req.files['previewfile'] |
336 | ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW) | 336 | ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW, false) |
337 | : undefined | 337 | : undefined |
338 | 338 | ||
339 | try { | 339 | try { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 8ab7c6bbd..b9d90b2bd 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 410 | 17 | const LAST_MIGRATION_VERSION = 415 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
diff --git a/server/initializers/migrations/0415-thumbnail-auto-generated.ts b/server/initializers/migrations/0415-thumbnail-auto-generated.ts new file mode 100644 index 000000000..f822a4c05 --- /dev/null +++ b/server/initializers/migrations/0415-thumbnail-auto-generated.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.BOOLEAN, | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.addColumn('thumbnail', 'automaticallyGenerated', data) | ||
17 | } | ||
18 | |||
19 | { | ||
20 | // Set auto generated to true for watch later playlists | ||
21 | const query = 'UPDATE thumbnail SET "automaticallyGenerated" = true WHERE "videoPlaylistId" IN ' + | ||
22 | '(SELECT id FROM "videoPlaylist" WHERE type = 2)' | ||
23 | |||
24 | await utils.sequelize.query(query) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | function down (options) { | ||
29 | throw new Error('Not implemented.') | ||
30 | } | ||
31 | |||
32 | export { | ||
33 | up, | ||
34 | down | ||
35 | } | ||
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index 36a91faec..f569d881c 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -105,6 +105,9 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc | |||
105 | } catch (err) { | 105 | } catch (err) { |
106 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | 106 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) |
107 | } | 107 | } |
108 | } else if (refreshedPlaylist.hasThumbnail()) { | ||
109 | await refreshedPlaylist.Thumbnail.destroy() | ||
110 | refreshedPlaylist.Thumbnail = null | ||
108 | } | 111 | } |
109 | 112 | ||
110 | return resetVideoPlaylistElements(accItems, refreshedPlaylist) | 113 | return resetVideoPlaylistElements(accItems, refreshedPlaylist) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 18bdcded4..a59773f5a 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -12,12 +12,18 @@ import { VideoPlaylistModel } from '../models/video/video-playlist' | |||
12 | 12 | ||
13 | type ImageSize = { height: number, width: number } | 13 | type ImageSize = { height: number, width: number } |
14 | 14 | ||
15 | function createPlaylistMiniatureFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) { | 15 | function createPlaylistMiniatureFromExisting ( |
16 | inputPath: string, | ||
17 | playlist: VideoPlaylistModel, | ||
18 | automaticallyGenerated: boolean, | ||
19 | keepOriginal = false, | ||
20 | size?: ImageSize | ||
21 | ) { | ||
16 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) | 22 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) |
17 | const type = ThumbnailType.MINIATURE | 23 | const type = ThumbnailType.MINIATURE |
18 | 24 | ||
19 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) | 25 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) |
20 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | 26 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) |
21 | } | 27 | } |
22 | 28 | ||
23 | function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) { | 29 | function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) { |
@@ -35,11 +41,17 @@ function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type: | |||
35 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 41 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) |
36 | } | 42 | } |
37 | 43 | ||
38 | function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { | 44 | function createVideoMiniatureFromExisting ( |
45 | inputPath: string, | ||
46 | video: VideoModel, | ||
47 | type: ThumbnailType, | ||
48 | automaticallyGenerated: boolean, | ||
49 | size?: ImageSize | ||
50 | ) { | ||
39 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 51 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
40 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }) | 52 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }) |
41 | 53 | ||
42 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | 54 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) |
43 | } | 55 | } |
44 | 56 | ||
45 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { | 57 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { |
@@ -50,7 +62,7 @@ function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, t | |||
50 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) | 62 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) |
51 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | 63 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) |
52 | 64 | ||
53 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | 65 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail }) |
54 | } | 66 | } |
55 | 67 | ||
56 | function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) { | 68 | function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) { |
@@ -134,10 +146,11 @@ async function createThumbnailFromFunction (parameters: { | |||
134 | height: number, | 146 | height: number, |
135 | width: number, | 147 | width: number, |
136 | type: ThumbnailType, | 148 | type: ThumbnailType, |
149 | automaticallyGenerated?: boolean, | ||
137 | fileUrl?: string, | 150 | fileUrl?: string, |
138 | existingThumbnail?: ThumbnailModel | 151 | existingThumbnail?: ThumbnailModel |
139 | }) { | 152 | }) { |
140 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, fileUrl = null } = parameters | 153 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters |
141 | 154 | ||
142 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() | 155 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() |
143 | 156 | ||
@@ -146,6 +159,7 @@ async function createThumbnailFromFunction (parameters: { | |||
146 | thumbnail.width = width | 159 | thumbnail.width = width |
147 | thumbnail.type = type | 160 | thumbnail.type = type |
148 | thumbnail.fileUrl = fileUrl | 161 | thumbnail.fileUrl = fileUrl |
162 | thumbnail.automaticallyGenerated = automaticallyGenerated | ||
149 | 163 | ||
150 | await thumbnailCreator() | 164 | await thumbnailCreator() |
151 | 165 | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index 8faf0adba..b767a6874 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -44,6 +44,10 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
44 | @Column | 44 | @Column |
45 | fileUrl: string | 45 | fileUrl: string |
46 | 46 | ||
47 | @AllowNull(true) | ||
48 | @Column | ||
49 | automaticallyGenerated: boolean | ||
50 | |||
47 | @ForeignKey(() => VideoModel) | 51 | @ForeignKey(() => VideoModel) |
48 | @Column | 52 | @Column |
49 | videoId: number | 53 | videoId: number |
@@ -88,7 +92,7 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
88 | } | 92 | } |
89 | 93 | ||
90 | @AfterDestroy | 94 | @AfterDestroy |
91 | static removeFilesAndSendDelete (instance: ThumbnailModel) { | 95 | static removeFiles (instance: ThumbnailModel) { |
92 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) | 96 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) |
93 | 97 | ||
94 | // Don't block the transaction | 98 | // Don't block the transaction |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index bed6f8eaf..dd7653533 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -218,6 +218,24 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
218 | }) | 218 | }) |
219 | } | 219 | } |
220 | 220 | ||
221 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number) { | ||
222 | const query = { | ||
223 | order: getSort('position'), | ||
224 | where: { | ||
225 | videoPlaylistId | ||
226 | }, | ||
227 | include: [ | ||
228 | { | ||
229 | model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS), | ||
230 | required: true | ||
231 | } | ||
232 | ] | ||
233 | } | ||
234 | |||
235 | return VideoPlaylistElementModel | ||
236 | .findOne(query) | ||
237 | } | ||
238 | |||
221 | static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) { | 239 | static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) { |
222 | const query: AggregateOptions<number> = { | 240 | const query: AggregateOptions<number> = { |
223 | where: { | 241 | where: { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 61ff78bd2..c8e97c491 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -265,7 +265,6 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
265 | VideoPlaylistElements: VideoPlaylistElementModel[] | 265 | VideoPlaylistElements: VideoPlaylistElementModel[] |
266 | 266 | ||
267 | @HasOne(() => ThumbnailModel, { | 267 | @HasOne(() => ThumbnailModel, { |
268 | |||
269 | foreignKey: { | 268 | foreignKey: { |
270 | name: 'videoPlaylistId', | 269 | name: 'videoPlaylistId', |
271 | allowNull: true | 270 | allowNull: true |
@@ -434,6 +433,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
434 | return !!this.Thumbnail | 433 | return !!this.Thumbnail |
435 | } | 434 | } |
436 | 435 | ||
436 | hasGeneratedThumbnail () { | ||
437 | return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true | ||
438 | } | ||
439 | |||
437 | generateThumbnailName () { | 440 | generateThumbnailName () { |
438 | const extension = '.jpg' | 441 | const extension = '.jpg' |
439 | 442 | ||
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 93e1f3e98..72e6061bb 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -12,6 +12,7 @@ import './video-hls' | |||
12 | import './video-imports' | 12 | import './video-imports' |
13 | import './video-nsfw' | 13 | import './video-nsfw' |
14 | import './video-playlists' | 14 | import './video-playlists' |
15 | import './video-playlist-thumbnails' | ||
15 | import './video-privacy' | 16 | import './video-privacy' |
16 | import './video-schedule-update' | 17 | import './video-schedule-update' |
17 | import './video-transcoder' | 18 | import './video-transcoder' |
diff --git a/server/tests/api/videos/video-playlist-thumbnails.ts b/server/tests/api/videos/video-playlist-thumbnails.ts new file mode 100644 index 000000000..73ab02c17 --- /dev/null +++ b/server/tests/api/videos/video-playlist-thumbnails.ts | |||
@@ -0,0 +1,262 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | addVideoInPlaylist, | ||
7 | cleanupTests, | ||
8 | createVideoPlaylist, | ||
9 | doubleFollow, | ||
10 | flushAndRunMultipleServers, | ||
11 | getVideoPlaylistsList, removeVideoFromPlaylist, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel, | ||
15 | testImage, | ||
16 | uploadVideoAndGetId, | ||
17 | waitJobs, | ||
18 | reorderVideosPlaylist | ||
19 | } from '../../../../shared/extra-utils' | ||
20 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
21 | |||
22 | const expect = chai.expect | ||
23 | |||
24 | describe('Playlist thumbnail', function () { | ||
25 | let servers: ServerInfo[] = [] | ||
26 | |||
27 | let playlistWithoutThumbnail: number | ||
28 | let playlistWithThumbnail: number | ||
29 | |||
30 | let withThumbnailE1: number | ||
31 | let withThumbnailE2: number | ||
32 | let withoutThumbnailE1: number | ||
33 | let withoutThumbnailE2: number | ||
34 | |||
35 | let video1: number | ||
36 | let video2: number | ||
37 | |||
38 | async function getPlaylistWithoutThumbnail (server: ServerInfo) { | ||
39 | const res = await getVideoPlaylistsList(server.url, 0, 10) | ||
40 | |||
41 | return res.body.data.find(p => p.displayName === 'playlist without thumbnail') | ||
42 | } | ||
43 | |||
44 | async function getPlaylistWithThumbnail (server: ServerInfo) { | ||
45 | const res = await getVideoPlaylistsList(server.url, 0, 10) | ||
46 | |||
47 | return res.body.data.find(p => p.displayName === 'playlist with thumbnail') | ||
48 | } | ||
49 | |||
50 | before(async function () { | ||
51 | this.timeout(120000) | ||
52 | |||
53 | servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: false } }) | ||
54 | |||
55 | // Get the access tokens | ||
56 | await setAccessTokensToServers(servers) | ||
57 | await setDefaultVideoChannel(servers) | ||
58 | |||
59 | // Server 1 and server 2 follow each other | ||
60 | await doubleFollow(servers[0], servers[1]) | ||
61 | |||
62 | video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).id | ||
63 | video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).id | ||
64 | |||
65 | await waitJobs(servers) | ||
66 | }) | ||
67 | |||
68 | it('Should automatically update the thumbnail when adding an element', async function () { | ||
69 | this.timeout(30000) | ||
70 | |||
71 | const res = await createVideoPlaylist({ | ||
72 | url: servers[ 1 ].url, | ||
73 | token: servers[ 1 ].accessToken, | ||
74 | playlistAttrs: { | ||
75 | displayName: 'playlist without thumbnail', | ||
76 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
77 | videoChannelId: servers[ 1 ].videoChannel.id | ||
78 | } | ||
79 | }) | ||
80 | playlistWithoutThumbnail = res.body.videoPlaylist.id | ||
81 | |||
82 | const res2 = await addVideoInPlaylist({ | ||
83 | url: servers[ 1 ].url, | ||
84 | token: servers[ 1 ].accessToken, | ||
85 | playlistId: playlistWithoutThumbnail, | ||
86 | elementAttrs: { videoId: video1 } | ||
87 | }) | ||
88 | withoutThumbnailE1 = res2.body.videoPlaylistElement.id | ||
89 | |||
90 | await waitJobs(servers) | ||
91 | |||
92 | for (const server of servers) { | ||
93 | const p = await getPlaylistWithoutThumbnail(server) | ||
94 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
95 | } | ||
96 | }) | ||
97 | |||
98 | it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () { | ||
99 | this.timeout(30000) | ||
100 | |||
101 | const res = await createVideoPlaylist({ | ||
102 | url: servers[ 1 ].url, | ||
103 | token: servers[ 1 ].accessToken, | ||
104 | playlistAttrs: { | ||
105 | displayName: 'playlist with thumbnail', | ||
106 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
107 | videoChannelId: servers[ 1 ].videoChannel.id, | ||
108 | thumbnailfile: 'thumbnail.jpg' | ||
109 | } | ||
110 | }) | ||
111 | playlistWithThumbnail = res.body.videoPlaylist.id | ||
112 | |||
113 | const res2 = await addVideoInPlaylist({ | ||
114 | url: servers[ 1 ].url, | ||
115 | token: servers[ 1 ].accessToken, | ||
116 | playlistId: playlistWithThumbnail, | ||
117 | elementAttrs: { videoId: video1 } | ||
118 | }) | ||
119 | withThumbnailE1 = res2.body.videoPlaylistElement.id | ||
120 | |||
121 | await waitJobs(servers) | ||
122 | |||
123 | for (const server of servers) { | ||
124 | const p = await getPlaylistWithThumbnail(server) | ||
125 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | ||
126 | } | ||
127 | }) | ||
128 | |||
129 | it('Should automatically update the thumbnail when moving the first element', async function () { | ||
130 | this.timeout(30000) | ||
131 | |||
132 | const res = await addVideoInPlaylist({ | ||
133 | url: servers[ 1 ].url, | ||
134 | token: servers[ 1 ].accessToken, | ||
135 | playlistId: playlistWithoutThumbnail, | ||
136 | elementAttrs: { videoId: video2 } | ||
137 | }) | ||
138 | withoutThumbnailE2 = res.body.videoPlaylistElement.id | ||
139 | |||
140 | await reorderVideosPlaylist({ | ||
141 | url: servers[1].url, | ||
142 | token: servers[1].accessToken, | ||
143 | playlistId: playlistWithoutThumbnail, | ||
144 | elementAttrs: { | ||
145 | startPosition: 1, | ||
146 | insertAfterPosition: 2 | ||
147 | } | ||
148 | }) | ||
149 | |||
150 | await waitJobs(servers) | ||
151 | |||
152 | for (const server of servers) { | ||
153 | const p = await getPlaylistWithoutThumbnail(server) | ||
154 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
155 | } | ||
156 | }) | ||
157 | |||
158 | it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () { | ||
159 | this.timeout(30000) | ||
160 | |||
161 | const res = await addVideoInPlaylist({ | ||
162 | url: servers[ 1 ].url, | ||
163 | token: servers[ 1 ].accessToken, | ||
164 | playlistId: playlistWithThumbnail, | ||
165 | elementAttrs: { videoId: video2 } | ||
166 | }) | ||
167 | withThumbnailE2 = res.body.videoPlaylistElement.id | ||
168 | |||
169 | await reorderVideosPlaylist({ | ||
170 | url: servers[1].url, | ||
171 | token: servers[1].accessToken, | ||
172 | playlistId: playlistWithThumbnail, | ||
173 | elementAttrs: { | ||
174 | startPosition: 1, | ||
175 | insertAfterPosition: 2 | ||
176 | } | ||
177 | }) | ||
178 | |||
179 | await waitJobs(servers) | ||
180 | |||
181 | for (const server of servers) { | ||
182 | const p = await getPlaylistWithThumbnail(server) | ||
183 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | ||
184 | } | ||
185 | }) | ||
186 | |||
187 | it('Should automatically update the thumbnail when deleting the first element', async function () { | ||
188 | this.timeout(30000) | ||
189 | |||
190 | await removeVideoFromPlaylist({ | ||
191 | url: servers[ 1 ].url, | ||
192 | token: servers[ 1 ].accessToken, | ||
193 | playlistId: playlistWithoutThumbnail, | ||
194 | playlistElementId: withoutThumbnailE1 | ||
195 | }) | ||
196 | |||
197 | await waitJobs(servers) | ||
198 | |||
199 | for (const server of servers) { | ||
200 | const p = await getPlaylistWithoutThumbnail(server) | ||
201 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
202 | } | ||
203 | }) | ||
204 | |||
205 | it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () { | ||
206 | this.timeout(30000) | ||
207 | |||
208 | await removeVideoFromPlaylist({ | ||
209 | url: servers[ 1 ].url, | ||
210 | token: servers[ 1 ].accessToken, | ||
211 | playlistId: playlistWithThumbnail, | ||
212 | playlistElementId: withThumbnailE1 | ||
213 | }) | ||
214 | |||
215 | await waitJobs(servers) | ||
216 | |||
217 | for (const server of servers) { | ||
218 | const p = await getPlaylistWithThumbnail(server) | ||
219 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | ||
220 | } | ||
221 | }) | ||
222 | |||
223 | it('Should the thumbnail when we delete the last element', async function () { | ||
224 | this.timeout(30000) | ||
225 | |||
226 | await removeVideoFromPlaylist({ | ||
227 | url: servers[ 1 ].url, | ||
228 | token: servers[ 1 ].accessToken, | ||
229 | playlistId: playlistWithoutThumbnail, | ||
230 | playlistElementId: withoutThumbnailE2 | ||
231 | }) | ||
232 | |||
233 | await waitJobs(servers) | ||
234 | |||
235 | for (const server of servers) { | ||
236 | const p = await getPlaylistWithoutThumbnail(server) | ||
237 | expect(p.thumbnailPath).to.be.null | ||
238 | } | ||
239 | }) | ||
240 | |||
241 | it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () { | ||
242 | this.timeout(30000) | ||
243 | |||
244 | await removeVideoFromPlaylist({ | ||
245 | url: servers[ 1 ].url, | ||
246 | token: servers[ 1 ].accessToken, | ||
247 | playlistId: playlistWithThumbnail, | ||
248 | playlistElementId: withThumbnailE2 | ||
249 | }) | ||
250 | |||
251 | await waitJobs(servers) | ||
252 | |||
253 | for (const server of servers) { | ||
254 | const p = await getPlaylistWithThumbnail(server) | ||
255 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | ||
256 | } | ||
257 | }) | ||
258 | |||
259 | after(async function () { | ||
260 | await cleanupTests(servers) | ||
261 | }) | ||
262 | }) | ||
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 7d5e3914b..424b217fb 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -344,6 +344,7 @@ describe('Test video playlists', function () { | |||
344 | }) | 344 | }) |
345 | 345 | ||
346 | describe('List playlists', function () { | 346 | describe('List playlists', function () { |
347 | |||
347 | it('Should correctly list the playlists', async function () { | 348 | it('Should correctly list the playlists', async function () { |
348 | this.timeout(30000) | 349 | this.timeout(30000) |
349 | 350 | ||