1
0
Fork 0

Automatically update playlist thumbnails

This commit is contained in:
Chocobozzz 2019-08-01 16:54:24 +02:00
parent a21e25ff64
commit 65af03a241
No known key found for this signature in database
GPG key ID: 583A612D890159BE
15 changed files with 443 additions and 43 deletions

View file

@ -63,24 +63,26 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
if (oldPosition > insertAfter) insertAfter-- 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] const element = this.playlistElements[previousIndex]
this.playlistElements.splice(previousIndex, 1) this.playlistElements.splice(previousIndex, 1)
this.playlistElements.splice(newIndex, 0, element) 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) { onElementRemoved (element: VideoPlaylistElement) {
const oldFirst = this.findFirst()
this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) this.playlistElements = this.playlistElements.filter(v => v.id !== element.id)
this.reorderClientPositions() this.reorderClientPositions(oldFirst)
} }
onNearOfBottom () { 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 let i = 1
for (const element of this.playlistElements) { for (const element of this.playlistElements) {
element.position = i element.position = i
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)
} }
} }

View file

@ -38,6 +38,9 @@ export class VideoPlaylist implements ServerVideoPlaylist {
videoChannelBy?: string videoChannelBy?: string
videoChannelAvatarUrl?: string videoChannelAvatarUrl?: string
private thumbnailVersion: number
private originThumbnailUrl: string
constructor (hash: ServerVideoPlaylist, translations: {}) { constructor (hash: ServerVideoPlaylist, translations: {}) {
const absoluteAPIUrl = getAbsoluteAPIUrl() const absoluteAPIUrl = getAbsoluteAPIUrl()
@ -54,6 +57,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
if (this.thumbnailPath) { if (this.thumbnailPath) {
this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
this.originThumbnailUrl = this.thumbnailUrl
} else { } else {
this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' 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) 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
}
} }

View file

@ -40,6 +40,7 @@ import { JobQueue } from '../../lib/job-queue'
import { CONFIG } from '../../initializers/config' import { CONFIG } from '../../initializers/config'
import { sequelizeTypescript } from '../../initializers/database' import { sequelizeTypescript } from '../../initializers/database'
import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' 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 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 thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField const thumbnailModel = thumbnailField
? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist) ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false)
: undefined : undefined
const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
const videoPlaylistCreated = await videoPlaylist.save({ transaction: 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 // We need more attributes for the federation
videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) 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 thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField const thumbnailModel = thumbnailField
? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance) ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false)
: undefined : undefined
try { try {
@ -239,7 +243,10 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) 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 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
@ -301,23 +308,17 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
videoPlaylist.changed('updatedAt', true) videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t }) await videoPlaylist.save({ transaction: t })
await sendUpdateVideoPlaylist(videoPlaylist, t)
return playlistElement return playlistElement
}) })
// If the user did not set a thumbnail, automatically take the video thumbnail // If the user did not set a thumbnail, automatically take the video thumbnail
if (videoPlaylist.hasThumbnail() === false) { if (videoPlaylist.hasThumbnail() === false || (videoPlaylist.hasGeneratedThumbnail() && playlistElement.position === 1)) {
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) await generateThumbnailForPlaylist(videoPlaylist, video)
const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true)
thumbnailModel.videoPlaylistId = videoPlaylist.id
await thumbnailModel.save()
} }
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) logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
return res.json({ return res.json({
@ -365,11 +366,17 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
videoPlaylist.changed('updatedAt', true) videoPlaylist.changed('updatedAt', true)
await videoPlaylist.save({ transaction: t }) await videoPlaylist.save({ transaction: t })
await sendUpdateVideoPlaylist(videoPlaylist, t)
logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) 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() return res.type('json').status(204).end()
} }
@ -413,8 +420,13 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
await sendUpdateVideoPlaylist(videoPlaylist, t) await sendUpdateVideoPlaylist(videoPlaylist, t)
}) })
// The first element changed
if ((start === 1 || insertAfter === 0) && videoPlaylist.hasGeneratedThumbnail()) {
await regeneratePlaylistThumbnail(videoPlaylist)
}
logger.info( 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 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)) 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()
}

View file

@ -207,7 +207,7 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
if (thumbnailField) { if (thumbnailField) {
const thumbnailPhysicalFile = thumbnailField[ 0 ] const thumbnailPhysicalFile = thumbnailField[ 0 ]
return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE) return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false)
} }
return undefined return undefined
@ -218,7 +218,7 @@ async function processPreview (req: express.Request, video: VideoModel) {
if (previewField) { if (previewField) {
const previewPhysicalFile = previewField[0] const previewPhysicalFile = previewField[0]
return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW) return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW, false)
} }
return undefined return undefined

View file

@ -223,13 +223,13 @@ async function addVideo (req: express.Request, res: express.Response) {
// Process thumbnail or create it from the video // Process thumbnail or create it from the video
const thumbnailField = req.files['thumbnailfile'] const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField 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) : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE)
// Process preview or create it from the video // Process preview or create it from the video
const previewField = req.files['previewfile'] const previewField = req.files['previewfile']
const previewModel = previewField 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) : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
// Create the torrent file // 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 // Process thumbnail or create it from the video
const thumbnailModel = req.files && req.files['thumbnailfile'] 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 : undefined
const previewModel = req.files && req.files['previewfile'] 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 : undefined
try { try {

View file

@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 410 const LAST_MIGRATION_VERSION = 415
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -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<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
}

View file

@ -105,6 +105,9 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
} catch (err) { } catch (err) {
logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { 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) return resetVideoPlaylistElements(accItems, refreshedPlaylist)

View file

@ -12,12 +12,18 @@ import { VideoPlaylistModel } from '../models/video/video-playlist'
type ImageSize = { height: number, width: number } 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 { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
const type = ThumbnailType.MINIATURE const type = ThumbnailType.MINIATURE
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) 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) { 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 }) 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 { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }) 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) { 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) ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
: () => generateImageFromVideoFile(input, basePath, filename, { height, width }) : () => 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) { function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) {
@ -134,10 +146,11 @@ async function createThumbnailFromFunction (parameters: {
height: number, height: number,
width: number, width: number,
type: ThumbnailType, type: ThumbnailType,
automaticallyGenerated?: boolean,
fileUrl?: string, fileUrl?: string,
existingThumbnail?: ThumbnailModel 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() const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
@ -146,6 +159,7 @@ async function createThumbnailFromFunction (parameters: {
thumbnail.width = width thumbnail.width = width
thumbnail.type = type thumbnail.type = type
thumbnail.fileUrl = fileUrl thumbnail.fileUrl = fileUrl
thumbnail.automaticallyGenerated = automaticallyGenerated
await thumbnailCreator() await thumbnailCreator()

View file

@ -44,6 +44,10 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
@Column @Column
fileUrl: string fileUrl: string
@AllowNull(true)
@Column
automaticallyGenerated: boolean
@ForeignKey(() => VideoModel) @ForeignKey(() => VideoModel)
@Column @Column
videoId: number videoId: number
@ -88,7 +92,7 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
} }
@AfterDestroy @AfterDestroy
static removeFilesAndSendDelete (instance: ThumbnailModel) { static removeFiles (instance: ThumbnailModel) {
logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename)
// Don't block the transaction // Don't block the transaction

View file

@ -218,6 +218,24 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
}) })
} }
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) { static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
const query: AggregateOptions<number> = { const query: AggregateOptions<number> = {
where: { where: {

View file

@ -265,7 +265,6 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
VideoPlaylistElements: VideoPlaylistElementModel[] VideoPlaylistElements: VideoPlaylistElementModel[]
@HasOne(() => ThumbnailModel, { @HasOne(() => ThumbnailModel, {
foreignKey: { foreignKey: {
name: 'videoPlaylistId', name: 'videoPlaylistId',
allowNull: true allowNull: true
@ -434,6 +433,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
return !!this.Thumbnail return !!this.Thumbnail
} }
hasGeneratedThumbnail () {
return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
}
generateThumbnailName () { generateThumbnailName () {
const extension = '.jpg' const extension = '.jpg'

View file

@ -12,6 +12,7 @@ import './video-hls'
import './video-imports' import './video-imports'
import './video-nsfw' import './video-nsfw'
import './video-playlists' import './video-playlists'
import './video-playlist-thumbnails'
import './video-privacy' import './video-privacy'
import './video-schedule-update' import './video-schedule-update'
import './video-transcoder' import './video-transcoder'

View file

@ -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)
})
})

View file

@ -344,6 +344,7 @@ describe('Test video playlists', function () {
}) })
describe('List playlists', function () { describe('List playlists', function () {
it('Should correctly list the playlists', async function () { it('Should correctly list the playlists', async function () {
this.timeout(30000) this.timeout(30000)