From 65af03a241aa83ab7ba71278b6c99acd26428b8a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 1 Aug 2019 16:54:24 +0200 Subject: [PATCH] Automatically update playlist thumbnails --- ...count-video-playlist-elements.component.ts | 35 ++- .../video-playlist/video-playlist.model.ts | 13 + server/controllers/api/video-playlist.ts | 67 +++-- server/controllers/api/videos/import.ts | 4 +- server/controllers/api/videos/index.ts | 8 +- server/initializers/constants.ts | 2 +- .../0415-thumbnail-auto-generated.ts | 35 +++ server/lib/activitypub/playlist.ts | 3 + server/lib/thumbnail.ts | 26 +- server/models/video/thumbnail.ts | 6 +- server/models/video/video-playlist-element.ts | 18 ++ server/models/video/video-playlist.ts | 5 +- server/tests/api/videos/index.ts | 1 + .../api/videos/video-playlist-thumbnails.ts | 262 ++++++++++++++++++ server/tests/api/videos/video-playlists.ts | 1 + 15 files changed, 443 insertions(+), 43 deletions(-) create mode 100644 server/initializers/migrations/0415-thumbnail-auto-generated.ts create mode 100644 server/tests/api/videos/video-playlist-thumbnails.ts 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 if (oldPosition > insertAfter) insertAfter-- - this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) - .subscribe( - () => { /* nothing to do */ }, - - err => this.notifier.error(err.message) - ) - const element = this.playlistElements[previousIndex] this.playlistElements.splice(previousIndex, 1) this.playlistElements.splice(newIndex, 0, element) - this.reorderClientPositions() + this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) + .subscribe( + () => { + this.reorderClientPositions() + }, + + err => this.notifier.error(err.message) + ) } onElementRemoved (element: VideoPlaylistElement) { + const oldFirst = this.findFirst() + this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) - this.reorderClientPositions() + this.reorderClientPositions(oldFirst) } onNearOfBottom () { @@ -110,12 +112,25 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro }) } - private reorderClientPositions () { + private reorderClientPositions (first?: VideoPlaylistElement) { + if (this.playlistElements.length === 0) return + + const oldFirst = first || this.findFirst() let i = 1 for (const element of this.playlistElements) { element.position = i i++ } + + // Reload playlist thumbnail if the first element changed + const newFirst = this.findFirst() + if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { + this.playlist.refreshThumbnail() + } + } + + private findFirst () { + return this.playlistElements.find(e => e.position === 1) } } 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 { videoChannelBy?: string videoChannelAvatarUrl?: string + private thumbnailVersion: number + private originThumbnailUrl: string + constructor (hash: ServerVideoPlaylist, translations: {}) { const absoluteAPIUrl = getAbsoluteAPIUrl() @@ -54,6 +57,7 @@ export class VideoPlaylist implements ServerVideoPlaylist { if (this.thumbnailPath) { this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath + this.originThumbnailUrl = this.thumbnailUrl } else { this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' } @@ -81,4 +85,13 @@ export class VideoPlaylist implements ServerVideoPlaylist { this.displayName = peertubeTranslate(this.displayName, translations) } } + + refreshThumbnail () { + if (!this.originThumbnailUrl) return + + if (!this.thumbnailVersion) this.thumbnailVersion = 0 + this.thumbnailVersion++ + + this.thumbnailUrl = this.originThumbnailUrl + '?v' + this.thumbnailVersion + } } 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' import { CONFIG } from '../../initializers/config' import { sequelizeTypescript } from '../../initializers/database' import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' +import { VideoModel } from '../../models/video/video' const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) @@ -171,13 +172,16 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { const thumbnailField = req.files['thumbnailfile'] const thumbnailModel = thumbnailField - ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist) + ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false) : undefined const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) - if (thumbnailModel) await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t) + if (thumbnailModel) { + thumbnailModel.automaticallyGenerated = false + await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t) + } // We need more attributes for the federation videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) @@ -206,7 +210,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) const thumbnailField = req.files['thumbnailfile'] const thumbnailModel = thumbnailField - ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance) + ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false) : undefined try { @@ -239,7 +243,10 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) - if (thumbnailModel) await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t) + if (thumbnailModel) { + thumbnailModel.automaticallyGenerated = false + await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t) + } const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE @@ -301,23 +308,17 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response) videoPlaylist.changed('updatedAt', true) await videoPlaylist.save({ transaction: t }) - await sendUpdateVideoPlaylist(videoPlaylist, t) - return playlistElement }) // If the user did not set a thumbnail, automatically take the video thumbnail - if (videoPlaylist.hasThumbnail() === false) { - logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) - - const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename) - const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true) - - thumbnailModel.videoPlaylistId = videoPlaylist.id - - await thumbnailModel.save() + if (videoPlaylist.hasThumbnail() === false || (videoPlaylist.hasGeneratedThumbnail() && playlistElement.position === 1)) { + await generateThumbnailForPlaylist(videoPlaylist, video) } + sendUpdateVideoPlaylist(videoPlaylist, undefined) + .catch(err => logger.error('Cannot send video playlist update.', { err })) + logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) return res.json({ @@ -365,11 +366,17 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo videoPlaylist.changed('updatedAt', true) await videoPlaylist.save({ transaction: t }) - await sendUpdateVideoPlaylist(videoPlaylist, t) - logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) }) + // Do we need to regenerate the default thumbnail? + if (positionToDelete === 1 && videoPlaylist.hasGeneratedThumbnail()) { + await regeneratePlaylistThumbnail(videoPlaylist) + } + + sendUpdateVideoPlaylist(videoPlaylist, undefined) + .catch(err => logger.error('Cannot send video playlist update.', { err })) + return res.type('json').status(204).end() } @@ -413,8 +420,13 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons await sendUpdateVideoPlaylist(videoPlaylist, t) }) + // The first element changed + if ((start === 1 || insertAfter === 0) && videoPlaylist.hasGeneratedThumbnail()) { + await regeneratePlaylistThumbnail(videoPlaylist) + } + logger.info( - 'Reordered playlist %s (inserted after %d elements %d - %d).', + 'Reordered playlist %s (inserted after position %d elements %d - %d).', videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1 ) @@ -440,3 +452,22 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon } return res.json(getFormattedObjects(resultList.data, resultList.total, options)) } + +async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) { + await videoPlaylist.Thumbnail.destroy() + videoPlaylist.Thumbnail = null + + const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id) + if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video) +} + +async function generateThumbnailForPlaylist (videoPlaylist: VideoPlaylistModel, video: VideoModel) { + logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) + + const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename) + const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true) + + thumbnailModel.videoPlaylistId = videoPlaylist.id + + videoPlaylist.Thumbnail = await thumbnailModel.save() +} 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) { if (thumbnailField) { const thumbnailPhysicalFile = thumbnailField[ 0 ] - return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE) + return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false) } return undefined @@ -218,7 +218,7 @@ async function processPreview (req: express.Request, video: VideoModel) { if (previewField) { const previewPhysicalFile = previewField[0] - return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW) + return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW, false) } 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) { // Process thumbnail or create it from the video const thumbnailField = req.files['thumbnailfile'] const thumbnailModel = thumbnailField - ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE) + ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE, false) : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE) // Process preview or create it from the video const previewField = req.files['previewfile'] const previewModel = previewField - ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW) + ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW, false) : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW) // Create the torrent file @@ -329,11 +329,11 @@ async function updateVideo (req: express.Request, res: express.Response) { // Process thumbnail or create it from the video const thumbnailModel = req.files && req.files['thumbnailfile'] - ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE) + ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE, false) : undefined const previewModel = req.files && req.files['previewfile'] - ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW) + ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW, false) : undefined 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' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 410 +const LAST_MIGRATION_VERSION = 415 // --------------------------------------------------------------------------- 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 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + { + const data = { + type: Sequelize.BOOLEAN, + allowNull: true, + defaultValue: null + } + + await utils.queryInterface.addColumn('thumbnail', 'automaticallyGenerated', data) + } + + { + // Set auto generated to true for watch later playlists + const query = 'UPDATE thumbnail SET "automaticallyGenerated" = true WHERE "videoPlaylistId" IN ' + + '(SELECT id FROM "videoPlaylist" WHERE type = 2)' + + await utils.sequelize.query(query) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} 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 } catch (err) { logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) } + } else if (refreshedPlaylist.hasThumbnail()) { + await refreshedPlaylist.Thumbnail.destroy() + refreshedPlaylist.Thumbnail = null } 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' type ImageSize = { height: number, width: number } -function createPlaylistMiniatureFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) { +function createPlaylistMiniatureFromExisting ( + inputPath: string, + playlist: VideoPlaylistModel, + automaticallyGenerated: boolean, + keepOriginal = false, + size?: ImageSize +) { const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) const type = ThumbnailType.MINIATURE const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) - return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) + return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) } function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) { @@ -35,11 +41,17 @@ function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type: return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) } -function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { +function createVideoMiniatureFromExisting ( + inputPath: string, + video: VideoModel, + type: ThumbnailType, + automaticallyGenerated: boolean, + size?: ImageSize +) { const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }) - return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) + return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) } function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { @@ -50,7 +62,7 @@ function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, t ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) - return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) + return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail }) } function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) { @@ -134,10 +146,11 @@ async function createThumbnailFromFunction (parameters: { height: number, width: number, type: ThumbnailType, + automaticallyGenerated?: boolean, fileUrl?: string, existingThumbnail?: ThumbnailModel }) { - const { thumbnailCreator, filename, width, height, type, existingThumbnail, fileUrl = null } = parameters + const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() @@ -146,6 +159,7 @@ async function createThumbnailFromFunction (parameters: { thumbnail.width = width thumbnail.type = type thumbnail.fileUrl = fileUrl + thumbnail.automaticallyGenerated = automaticallyGenerated await thumbnailCreator() 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 { @Column fileUrl: string + @AllowNull(true) + @Column + automaticallyGenerated: boolean + @ForeignKey(() => VideoModel) @Column videoId: number @@ -88,7 +92,7 @@ export class ThumbnailModel extends Model { } @AfterDestroy - static removeFilesAndSendDelete (instance: ThumbnailModel) { + static removeFiles (instance: ThumbnailModel) { logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) // 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 }) } + static loadFirstElementWithVideoThumbnail (videoPlaylistId: number) { + const query = { + order: getSort('position'), + where: { + videoPlaylistId + }, + include: [ + { + model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS), + required: true + } + ] + } + + return VideoPlaylistElementModel + .findOne(query) + } + static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) { const query: AggregateOptions = { 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 { VideoPlaylistElements: VideoPlaylistElementModel[] @HasOne(() => ThumbnailModel, { - foreignKey: { name: 'videoPlaylistId', allowNull: true @@ -434,6 +433,10 @@ export class VideoPlaylistModel extends Model { return !!this.Thumbnail } + hasGeneratedThumbnail () { + return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true + } + generateThumbnailName () { const extension = '.jpg' 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' import './video-imports' import './video-nsfw' import './video-playlists' +import './video-playlist-thumbnails' import './video-privacy' import './video-schedule-update' 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 @@ +/* tslint:disable:no-unused-expression */ + +import * as chai from 'chai' +import 'mocha' +import { + addVideoInPlaylist, + cleanupTests, + createVideoPlaylist, + doubleFollow, + flushAndRunMultipleServers, + getVideoPlaylistsList, removeVideoFromPlaylist, + ServerInfo, + setAccessTokensToServers, + setDefaultVideoChannel, + testImage, + uploadVideoAndGetId, + waitJobs, + reorderVideosPlaylist +} from '../../../../shared/extra-utils' +import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' + +const expect = chai.expect + +describe('Playlist thumbnail', function () { + let servers: ServerInfo[] = [] + + let playlistWithoutThumbnail: number + let playlistWithThumbnail: number + + let withThumbnailE1: number + let withThumbnailE2: number + let withoutThumbnailE1: number + let withoutThumbnailE2: number + + let video1: number + let video2: number + + async function getPlaylistWithoutThumbnail (server: ServerInfo) { + const res = await getVideoPlaylistsList(server.url, 0, 10) + + return res.body.data.find(p => p.displayName === 'playlist without thumbnail') + } + + async function getPlaylistWithThumbnail (server: ServerInfo) { + const res = await getVideoPlaylistsList(server.url, 0, 10) + + return res.body.data.find(p => p.displayName === 'playlist with thumbnail') + } + + before(async function () { + this.timeout(120000) + + servers = await flushAndRunMultipleServers(2, { 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]) + + video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).id + video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).id + + await waitJobs(servers) + }) + + it('Should automatically update the thumbnail when adding an element', async function () { + this.timeout(30000) + + const res = await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist without thumbnail', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: servers[ 1 ].videoChannel.id + } + }) + playlistWithoutThumbnail = res.body.videoPlaylist.id + + const res2 = await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithoutThumbnail, + elementAttrs: { videoId: video1 } + }) + withoutThumbnailE1 = res2.body.videoPlaylistElement.id + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithoutThumbnail(server) + await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) + } + }) + + it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () { + this.timeout(30000) + + const res = await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist with thumbnail', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: servers[ 1 ].videoChannel.id, + thumbnailfile: 'thumbnail.jpg' + } + }) + playlistWithThumbnail = res.body.videoPlaylist.id + + const res2 = await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithThumbnail, + elementAttrs: { videoId: video1 } + }) + withThumbnailE1 = res2.body.videoPlaylistElement.id + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithThumbnail(server) + await testImage(server.url, 'thumbnail', p.thumbnailPath) + } + }) + + it('Should automatically update the thumbnail when moving the first element', async function () { + this.timeout(30000) + + const res = await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithoutThumbnail, + elementAttrs: { videoId: video2 } + }) + withoutThumbnailE2 = res.body.videoPlaylistElement.id + + await reorderVideosPlaylist({ + url: servers[1].url, + token: servers[1].accessToken, + playlistId: playlistWithoutThumbnail, + elementAttrs: { + startPosition: 1, + insertAfterPosition: 2 + } + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithoutThumbnail(server) + await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) + } + }) + + it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () { + this.timeout(30000) + + const res = await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithThumbnail, + elementAttrs: { videoId: video2 } + }) + withThumbnailE2 = res.body.videoPlaylistElement.id + + await reorderVideosPlaylist({ + url: servers[1].url, + token: servers[1].accessToken, + playlistId: playlistWithThumbnail, + elementAttrs: { + startPosition: 1, + insertAfterPosition: 2 + } + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithThumbnail(server) + await testImage(server.url, 'thumbnail', p.thumbnailPath) + } + }) + + it('Should automatically update the thumbnail when deleting the first element', async function () { + this.timeout(30000) + + await removeVideoFromPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithoutThumbnail, + playlistElementId: withoutThumbnailE1 + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithoutThumbnail(server) + await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) + } + }) + + it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () { + this.timeout(30000) + + await removeVideoFromPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithThumbnail, + playlistElementId: withThumbnailE1 + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithThumbnail(server) + await testImage(server.url, 'thumbnail', p.thumbnailPath) + } + }) + + it('Should the thumbnail when we delete the last element', async function () { + this.timeout(30000) + + await removeVideoFromPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithoutThumbnail, + playlistElementId: withoutThumbnailE2 + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithoutThumbnail(server) + expect(p.thumbnailPath).to.be.null + } + }) + + it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () { + this.timeout(30000) + + await removeVideoFromPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: playlistWithThumbnail, + playlistElementId: withThumbnailE2 + }) + + await waitJobs(servers) + + for (const server of servers) { + const p = await getPlaylistWithThumbnail(server) + await testImage(server.url, 'thumbnail', p.thumbnailPath) + } + }) + + after(async function () { + await cleanupTests(servers) + }) +}) 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 () { }) describe('List playlists', function () { + it('Should correctly list the playlists', async function () { this.timeout(30000)