Automatically update playlist thumbnails
This commit is contained in:
parent
a21e25ff64
commit
65af03a241
15 changed files with 443 additions and 43 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 410
|
const LAST_MIGRATION_VERSION = 415
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
262
server/tests/api/videos/video-playlist-thumbnails.ts
Normal file
262
server/tests/api/videos/video-playlist-thumbnails.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue