Add ability for uploaders to schedule video update

pull/665/head
Chocobozzz 2018-06-14 18:06:56 +02:00
parent bf079b7bfd
commit 2baea0c77c
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
21 changed files with 469 additions and 22 deletions

View File

@ -83,7 +83,6 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
// Scroll to the highlighted thread
setTimeout(() => {
// -60 because of the fixed header
console.log(this.commentHighlightBlock.nativeElement.offsetTop)
const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
window.scroll(0, scrollY)
}, 500)

View File

@ -1,4 +1,6 @@
// FIXME: https://github.com/nodejs/node/pull/16853
import { ScheduleVideoUpdateModel } from './server/models/video/schedule-video-update'
require('tls').DEFAULT_ECDH_CURVE = 'auto'
import { isTestInstance } from './server/helpers/core-utils'
@ -28,7 +30,7 @@ import { checkMissedConfig, checkFFmpeg, checkConfig } from './server/initialize
// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
import { logger } from './server/helpers/logger'
import { ACCEPT_HEADERS, API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants'
import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants'
const missed = checkMissedConfig()
if (missed.length !== 0) {
@ -80,6 +82,7 @@ import {
import { Redis } from './server/lib/redis'
import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler'
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
// ----------- Command line -----------
@ -200,6 +203,7 @@ async function startApplication () {
// Enable Schedulers
BadActorFollowScheduler.Instance.enable()
RemoveOldJobsScheduler.Instance.enable()
UpdateVideosScheduler.Instance.enable()
// Redis initialization
Redis.Instance.init()

View File

@ -174,7 +174,11 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
false // Display my NSFW videos
)
const additionalAttributes = { waitTranscoding: true, state: true }
const additionalAttributes = {
waitTranscoding: true,
state: true,
scheduledUpdate: true
}
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
}

View File

@ -52,6 +52,7 @@ import { rateVideoRouter } from './rate'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
const videosRouter = express.Router()
@ -231,6 +232,7 @@ async function addVideo (req: express.Request, res: express.Response) {
video.VideoFiles = [ videoFile ]
// Create tags
if (videoInfo.tags !== undefined) {
const tagInstances = await TagModel.findOrCreateTags(videoInfo.tags, t)
@ -238,6 +240,15 @@ async function addVideo (req: express.Request, res: express.Response) {
video.Tags = tagInstances
}
// Schedule an update in the future?
if (videoInfo.scheduleUpdate) {
await ScheduleVideoUpdateModel.create({
videoId: video.id,
updateAt: videoInfo.scheduleUpdate.updateAt,
privacy: videoInfo.scheduleUpdate.privacy || null
}, { transaction: t })
}
await federateVideoIfNeeded(video, true, t)
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
@ -324,6 +335,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
}
// Schedule an update in the future?
if (videoInfoToUpdate.scheduleUpdate) {
await ScheduleVideoUpdateModel.upsert({
videoId: videoInstanceUpdated.id,
updateAt: videoInfoToUpdate.scheduleUpdate.updateAt,
privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
}, { transaction: t })
}
const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
})

View File

@ -3,7 +3,7 @@ import 'express-validator'
import { values } from 'lodash'
import 'multer'
import * as validator from 'validator'
import { UserRight, VideoRateType } from '../../../shared'
import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
import {
CONSTRAINTS_FIELDS,
VIDEO_CATEGORIES,
@ -98,10 +98,18 @@ function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } |
return isFileValid(files, videoImageTypesRegex, field, true)
}
function isVideoPrivacyValid (value: string) {
function isVideoPrivacyValid (value: number) {
return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
}
function isScheduleVideoUpdatePrivacyValid (value: number) {
return validator.isInt(value + '') &&
(
value === VideoPrivacy.UNLISTED ||
value === VideoPrivacy.PUBLIC
)
}
function isVideoFileInfoHashValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
}
@ -174,6 +182,7 @@ export {
isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid,
isVideoFile,
isVideoStateValid,

View File

@ -21,12 +21,16 @@ function retryTransactionWrapper <T, A> (
arg1: A
): Promise<T>
function retryTransactionWrapper <T> (
functionToRetry: () => Promise<T> | Bluebird<T>
): Promise<T>
function retryTransactionWrapper <T> (
functionToRetry: (...args: any[]) => Promise<T> | Bluebird<T>,
...args: any[]
): Promise<T> {
return transactionRetryer<T>(callback => {
functionToRetry.apply(this, args)
functionToRetry.apply(null, args)
.then((result: T) => callback(null, result))
.catch(err => callback(err))
})

View File

@ -8,6 +8,8 @@ import { VideoPrivacy } from '../../shared/models/videos'
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
import { invert } from 'lodash'
import { RemoveOldJobsScheduler } from '../lib/schedulers/remove-old-jobs-scheduler'
import { UpdateVideosScheduler } from '../lib/schedulers/update-videos-scheduler'
// Use a variable to reload the configuration if we need
let config: IConfig = require('config')
@ -94,7 +96,11 @@ const JOB_REQUEST_TTL = 60000 * 10 // 10 minutes
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
// 1 hour
let SCHEDULER_INTERVAL = 60000 * 60
let SCHEDULER_INTERVALS_MS = {
badActorFollow: 60000 * 60, // 1 hour
removeOldJobs: 60000 * 60, // 1 jour
updateVideos: 60000 * 1, // 1 minute
}
// ---------------------------------------------------------------------------
@ -460,7 +466,10 @@ if (isTestInstance() === true) {
CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
SCHEDULER_INTERVAL = 10000
SCHEDULER_INTERVALS_MS.badActorFollow = 10000
SCHEDULER_INTERVALS_MS.removeOldJobs = 10000
SCHEDULER_INTERVALS_MS.updateVideos = 5000
VIDEO_VIEW_LIFETIME = 1000 // 1 second
JOB_ATTEMPTS['email'] = 1
@ -513,7 +522,7 @@ export {
JOB_REQUEST_TTL,
USER_PASSWORD_RESET_LIFETIME,
IMAGE_MIMETYPE_EXT,
SCHEDULER_INTERVAL,
SCHEDULER_INTERVALS_MS,
STATIC_DOWNLOAD_PATHS,
RATES_LIMIT,
VIDEO_EXT_MIMETYPE,

View File

@ -22,6 +22,7 @@ import { VideoFileModel } from '../models/video/video-file'
import { VideoShareModel } from '../models/video/video-share'
import { VideoTagModel } from '../models/video/video-tag'
import { CONFIG } from './constants'
import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
@ -73,7 +74,8 @@ async function initDatabaseModels (silent: boolean) {
VideoBlacklistModel,
VideoTagModel,
VideoModel,
VideoCommentModel
VideoCommentModel,
ScheduleVideoUpdateModel
])
if (!silent) logger.info('Database %s is ready.', dbname)

View File

@ -1,11 +1,13 @@
import { SCHEDULER_INTERVAL } from '../../initializers'
export abstract class AbstractScheduler {
protected abstract schedulerIntervalMs: number
private interval: NodeJS.Timer
enable () {
this.interval = setInterval(() => this.execute(), SCHEDULER_INTERVAL)
if (!this.schedulerIntervalMs) throw new Error('Interval is not correctly set.')
this.interval = setInterval(() => this.execute(), this.schedulerIntervalMs)
}
disable () {

View File

@ -2,11 +2,14 @@ import { isTestInstance } from '../../helpers/core-utils'
import { logger } from '../../helpers/logger'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { AbstractScheduler } from './abstract-scheduler'
import { SCHEDULER_INTERVALS_MS } from '../../initializers'
export class BadActorFollowScheduler extends AbstractScheduler {
private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.badActorFollow
private constructor () {
super()
}

View File

@ -2,11 +2,14 @@ import { isTestInstance } from '../../helpers/core-utils'
import { logger } from '../../helpers/logger'
import { JobQueue } from '../job-queue'
import { AbstractScheduler } from './abstract-scheduler'
import { SCHEDULER_INTERVALS_MS } from '../../initializers'
export class RemoveOldJobsScheduler extends AbstractScheduler {
private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeOldJobs
private constructor () {
super()
}

View File

@ -0,0 +1,62 @@
import { isTestInstance } from '../../helpers/core-utils'
import { logger } from '../../helpers/logger'
import { JobQueue } from '../job-queue'
import { AbstractScheduler } from './abstract-scheduler'
import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update'
import { retryTransactionWrapper } from '../../helpers/database-utils'
import { federateVideoIfNeeded } from '../activitypub'
import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers'
import { VideoPrivacy } from '../../../shared/models/videos'
export class UpdateVideosScheduler extends AbstractScheduler {
private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.updateVideos
private isRunning = false
private constructor () {
super()
}
async execute () {
if (this.isRunning === true) return
this.isRunning = true
try {
await retryTransactionWrapper(this.updateVideos.bind(this))
} catch (err) {
logger.error('Cannot execute update videos scheduler.', { err })
} finally {
this.isRunning = false
}
}
private updateVideos () {
return sequelizeTypescript.transaction(async t => {
const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
for (const schedule of schedules) {
const video = schedule.Video
logger.info('Executing scheduled video update on %s.', video.uuid)
if (schedule.privacy) {
const oldPrivacy = video.privacy
video.privacy = schedule.privacy
await video.save({ transaction: t })
const isNewVideo = oldPrivacy === VideoPrivacy.PRIVATE
await federateVideoIfNeeded(video, isNewVideo, t)
}
await schedule.destroy({ transaction: t })
}
})
}
static get Instance () {
return this.instance || (this.instance = new this())
}
}

View File

@ -2,8 +2,17 @@ import * as express from 'express'
import 'express-validator'
import { body, param, query } from 'express-validator/check'
import { UserRight, VideoPrivacy } from '../../../shared'
import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toValueOrNull } from '../../helpers/custom-validators/misc'
import {
isBooleanValid,
isDateValid,
isIdOrUUIDValid,
isIdValid,
isUUIDValid,
toIntOrNull,
toValueOrNull
} from '../../helpers/custom-validators/misc'
import {
isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid,
isVideoCategoryValid,
isVideoChannelOfAccountExist,
@ -84,14 +93,21 @@ const videosAddValidator = [
.custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
body('channelId')
.toInt()
.custom(isIdValid)
.withMessage('Should have correct video channel id'),
.custom(isIdValid).withMessage('Should have correct video channel id'),
body('scheduleUpdate.updateAt')
.optional()
.custom(isDateValid).withMessage('Should have a valid schedule update date'),
body('scheduleUpdate.privacy')
.optional()
.toInt()
.custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
if (areValidationErrors(req, res)) return
if (areErrorsInVideoImageFiles(req, res)) return
if (areErrorsInScheduleUpdate(req, res)) return
const videoFile: Express.Multer.File = req.files['videofile'][0]
const user = res.locals.oauth.token.User
@ -183,12 +199,20 @@ const videosUpdateValidator = [
.optional()
.toInt()
.custom(isIdValid).withMessage('Should have correct video channel id'),
body('scheduleUpdate.updateAt')
.optional()
.custom(isDateValid).withMessage('Should have a valid schedule update date'),
body('scheduleUpdate.privacy')
.optional()
.toInt()
.custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (areErrorsInVideoImageFiles(req, res)) return
if (areErrorsInScheduleUpdate(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video = res.locals.video
@ -371,7 +395,7 @@ function areErrorsInVideoImageFiles (req: express.Request, res: express.Response
const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
res.status(400)
.send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
.json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
.end()
return true
}
@ -379,3 +403,17 @@ function areErrorsInVideoImageFiles (req: express.Request, res: express.Response
return false
}
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
if (req.body.scheduleUpdate) {
if (!req.body.scheduleUpdate.updateAt) {
res.status(400)
.json({ error: 'Schedule update at is mandatory.' })
.end()
return true
}
}
return false
}

View File

@ -0,0 +1,71 @@
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Sequelize, Table, UpdatedAt } from 'sequelize-typescript'
import { ScopeNames as VideoScopeNames, VideoModel } from './video'
import { VideoPrivacy } from '../../../shared/models/videos'
import { Transaction } from 'sequelize'
@Table({
tableName: 'scheduleVideoUpdate',
indexes: [
{
fields: [ 'videoId' ],
unique: true
},
{
fields: [ 'updateAt' ]
}
]
})
export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
@AllowNull(false)
@Default(null)
@Column
updateAt: Date
@AllowNull(true)
@Default(null)
@Column
privacy: VideoPrivacy
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@ForeignKey(() => VideoModel)
@Column
videoId: number
@BelongsTo(() => VideoModel, {
foreignKey: {
allowNull: false
},
onDelete: 'cascade'
})
Video: VideoModel
static listVideosToUpdate (t: Transaction) {
const query = {
where: {
updateAt: {
[Sequelize.Op.lte]: new Date()
}
},
include: [
{
model: VideoModel.scope(
[
VideoScopeNames.WITH_FILES,
VideoScopeNames.WITH_ACCOUNT_DETAILS
]
)
}
],
transaction: t
}
return ScheduleVideoUpdateModel.findAll(query)
}
}

View File

@ -15,6 +15,7 @@ import {
Default,
ForeignKey,
HasMany,
HasOne,
IFindOptions,
Is,
IsInt,
@ -47,7 +48,8 @@ import {
isVideoLanguageValid,
isVideoLicenceValid,
isVideoNameValid,
isVideoPrivacyValid, isVideoStateValid,
isVideoPrivacyValid,
isVideoStateValid,
isVideoSupportValid
} from '../../helpers/custom-validators/videos'
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@ -66,7 +68,8 @@ import {
VIDEO_EXT_MIMETYPE,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_PRIVACIES, VIDEO_STATES
VIDEO_PRIVACIES,
VIDEO_STATES
} from '../../initializers'
import {
getVideoCommentsActivityPubUrl,
@ -88,8 +91,9 @@ import { VideoCommentModel } from './video-comment'
import { VideoFileModel } from './video-file'
import { VideoShareModel } from './video-share'
import { VideoTagModel } from './video-tag'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
enum ScopeNames {
export enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
@ -495,6 +499,15 @@ export class VideoModel extends Model<VideoModel> {
})
VideoComments: VideoCommentModel[]
@HasOne(() => ScheduleVideoUpdateModel, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
ScheduleVideoUpdate: ScheduleVideoUpdateModel
@BeforeDestroy
static async sendDelete (instance: VideoModel, options) {
if (instance.isOwned()) {
@ -673,6 +686,10 @@ export class VideoModel extends Model<VideoModel> {
required: true
}
]
},
{
model: ScheduleVideoUpdateModel,
required: false
}
]
}
@ -1006,7 +1023,8 @@ export class VideoModel extends Model<VideoModel> {
toFormattedJSON (options?: {
additionalAttributes: {
state: boolean,
waitTranscoding: boolean
waitTranscoding: boolean,
scheduledUpdate: boolean
}
}): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
@ -1073,7 +1091,16 @@ export class VideoModel extends Model<VideoModel> {
}
}
if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
if (options.additionalAttributes.waitTranscoding) {
videoObject.waitTranscoding = this.waitTranscoding
}
if (options.additionalAttributes.scheduledUpdate && this.ScheduleVideoUpdate) {
videoObject.scheduledUpdate = {
updateAt: this.ScheduleVideoUpdate.updateAt,
privacy: this.ScheduleVideoUpdate.privacy || undefined
}
}
}
return videoObject

View File

@ -6,3 +6,4 @@ import './server/jobs'
import './videos/video-comments'
import './users/users-multiple-servers'
import './server/handle-down'
import './videos/video-schedule-update'

View File

@ -0,0 +1,164 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import { VideoPrivacy } from '../../../../shared/models/videos'
import {
doubleFollow,
flushAndRunMultipleServers, getMyVideos,
getVideosList,
killallServers,
ServerInfo,
setAccessTokensToServers, updateVideo,
uploadVideo,
wait
} from '../../utils'
import { join } from 'path'
import { waitJobs } from '../../utils/server/jobs'
const expect = chai.expect
function in10Seconds () {
const now = new Date()
now.setSeconds(now.getSeconds() + 10)
return now
}
describe('Test video update scheduler', function () {
let servers: ServerInfo[] = []
let video2UUID: string
before(async function () {
this.timeout(30000)
// Run servers
servers = await flushAndRunMultipleServers(2)
await setAccessTokensToServers(servers)
await doubleFollow(servers[0], servers[1])
})
it('Should upload a video and schedule an update in 10 seconds', async function () {
this.timeout(10000)
const videoAttributes = {
name: 'video 1',
privacy: VideoPrivacy.PRIVATE,
scheduleUpdate: {
updateAt: in10Seconds().toISOString(),
privacy: VideoPrivacy.PUBLIC
}
}
await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
await waitJobs(servers)
})
it('Should not list the video (in privacy mode)', async function () {
for (const server of servers) {
const res = await getVideosList(server.url)
expect(res.body.total).to.equal(0)
}
})
it('Should have my scheduled video in my account videos', async function () {
const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
expect(res.body.total).to.equal(1)
const video = res.body.data[0]
expect(video.name).to.equal('video 1')
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
})
it('Should wait some seconds and have the video in public privacy', async function () {
this.timeout(20000)
await wait(10000)
await waitJobs(servers)
for (const server of servers) {
const res = await getVideosList(server.url)
expect(res.body.total).to.equal(1)
expect(res.body.data[0].name).to.equal('video 1')
}
})
it('Should upload a video without scheduling an update', async function () {
this.timeout(10000)
const videoAttributes = {
name: 'video 2',
privacy: VideoPrivacy.PRIVATE
}
const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
video2UUID = res.body.video.uuid
await waitJobs(servers)
})
it('Should update a video by scheduling an update', async function () {
this.timeout(10000)
const videoAttributes = {
name: 'video 2 updated',
scheduleUpdate: {
updateAt: in10Seconds().toISOString(),
privacy: VideoPrivacy.PUBLIC
}
}
await updateVideo(servers[0].url, servers[0].accessToken, video2UUID, videoAttributes)
await waitJobs(servers)
})
it('Should not display the updated video', async function () {
for (const server of servers) {
const res = await getVideosList(server.url)
expect(res.body.total).to.equal(1)
}
})
it('Should have my scheduled updated video in my account videos', async function () {
const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
expect(res.body.total).to.equal(2)
const video = res.body.data.find(v => v.uuid === video2UUID)
expect(video).not.to.be.undefined
expect(video.name).to.equal('video 2 updated')
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
})
it('Should wait some seconds and have the updated video in public privacy', async function () {
this.timeout(20000)
await wait(10000)
await waitJobs(servers)
for (const server of servers) {
const res = await getVideosList(server.url)
expect(res.body.total).to.equal(2)
const video = res.body.data.find(v => v.uuid === video2UUID)
expect(video).not.to.be.undefined
expect(video.name).to.equal('video 2 updated')
}
})
after(async function () {
killallServers(servers)
})
})

View File

@ -35,6 +35,10 @@ type VideoAttributes = {
fixture?: string
thumbnailfile?: string
previewfile?: string
scheduleUpdate?: {
updateAt: string
privacy?: VideoPrivacy
}
}
function getVideoCategories (url: string) {
@ -371,6 +375,14 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
}
if (attributes.scheduleUpdate) {
req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
if (attributes.scheduleUpdate.privacy) {
req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
}
}
return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
.expect(specialStatus)
}
@ -389,6 +401,7 @@ function updateVideo (url: string, accessToken: string, id: number | string, att
if (attributes.tags) body['tags'] = attributes.tags
if (attributes.privacy) body['privacy'] = attributes.privacy
if (attributes.channelId) body['channelId'] = attributes.channelId
if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
// Upload request
if (attributes.thumbnailfile || attributes.previewfile) {

View File

@ -13,4 +13,8 @@ export interface VideoCreate {
tags?: string[]
commentsEnabled?: boolean
privacy: VideoPrivacy
scheduleUpdate?: {
updateAt: Date
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
}
}

View File

@ -15,4 +15,8 @@ export interface VideoUpdate {
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob
scheduleUpdate?: {
updateAt: Date
privacy?: VideoPrivacy
}
}

View File

@ -43,6 +43,10 @@ export interface Video {
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: {
updateAt: Date | string
privacy?: VideoPrivacy
}
account: {
id: number