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 () {
})
}
- 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)
}
}
videoChannelBy?: string
videoChannelAvatarUrl?: string
+ private thumbnailVersion: number
+ private originThumbnailUrl: string
+
constructor (hash: ServerVideoPlaylist, translations: {}) {
const absoluteAPIUrl = getAbsoluteAPIUrl()
if (this.thumbnailPath) {
this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
+ this.originThumbnailUrl = this.thumbnailUrl
} else {
this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg'
}
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
+ }
}
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 })
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)
const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField
- ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance)
+ ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false)
: undefined
try {
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
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({
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()
}
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
)
}
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()
+}
if (thumbnailField) {
const thumbnailPhysicalFile = thumbnailField[ 0 ]
- return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE)
+ return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false)
}
return undefined
if (previewField) {
const previewPhysicalFile = previewField[0]
- return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
+ return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW, false)
}
return undefined
// 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
// 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 {
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 410
+const LAST_MIGRATION_VERSION = 415
// ---------------------------------------------------------------------------
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ {
+ 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
+}
} 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)
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) {
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) {
? () => 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) {
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()
thumbnail.width = width
thumbnail.type = type
thumbnail.fileUrl = fileUrl
+ thumbnail.automaticallyGenerated = automaticallyGenerated
await thumbnailCreator()
@Column
fileUrl: string
+ @AllowNull(true)
+ @Column
+ automaticallyGenerated: boolean
+
@ForeignKey(() => VideoModel)
@Column
videoId: number
}
@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
})
}
+ 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<number> = {
where: {
VideoPlaylistElements: VideoPlaylistElementModel[]
@HasOne(() => ThumbnailModel, {
-
foreignKey: {
name: 'videoPlaylistId',
allowNull: true
return !!this.Thumbnail
}
+ hasGeneratedThumbnail () {
+ return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
+ }
+
generateThumbnailName () {
const extension = '.jpg'
import './video-imports'
import './video-nsfw'
import './video-playlists'
+import './video-playlist-thumbnails'
import './video-privacy'
import './video-schedule-update'
import './video-transcoder'
--- /dev/null
+/* 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)
+ })
+})
})
describe('List playlists', function () {
+
it('Should correctly list the playlists', async function () {
this.timeout(30000)