Add ability for uploaders to schedule video update
This commit is contained in:
parent
bf079b7bfd
commit
2baea0c77c
|
@ -83,7 +83,6 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
// Scroll to the highlighted thread
|
// Scroll to the highlighted thread
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// -60 because of the fixed header
|
// -60 because of the fixed header
|
||||||
console.log(this.commentHighlightBlock.nativeElement.offsetTop)
|
|
||||||
const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
|
const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
|
||||||
window.scroll(0, scrollY)
|
window.scroll(0, scrollY)
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// FIXME: https://github.com/nodejs/node/pull/16853
|
// FIXME: https://github.com/nodejs/node/pull/16853
|
||||||
|
import { ScheduleVideoUpdateModel } from './server/models/video/schedule-video-update'
|
||||||
|
|
||||||
require('tls').DEFAULT_ECDH_CURVE = 'auto'
|
require('tls').DEFAULT_ECDH_CURVE = 'auto'
|
||||||
|
|
||||||
import { isTestInstance } from './server/helpers/core-utils'
|
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)
|
// 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 { 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()
|
const missed = checkMissedConfig()
|
||||||
if (missed.length !== 0) {
|
if (missed.length !== 0) {
|
||||||
|
@ -80,6 +82,7 @@ import {
|
||||||
import { Redis } from './server/lib/redis'
|
import { Redis } from './server/lib/redis'
|
||||||
import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler'
|
import { BadActorFollowScheduler } from './server/lib/schedulers/bad-actor-follow-scheduler'
|
||||||
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
|
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
|
||||||
|
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
|
||||||
|
|
||||||
// ----------- Command line -----------
|
// ----------- Command line -----------
|
||||||
|
|
||||||
|
@ -200,6 +203,7 @@ async function startApplication () {
|
||||||
// Enable Schedulers
|
// Enable Schedulers
|
||||||
BadActorFollowScheduler.Instance.enable()
|
BadActorFollowScheduler.Instance.enable()
|
||||||
RemoveOldJobsScheduler.Instance.enable()
|
RemoveOldJobsScheduler.Instance.enable()
|
||||||
|
UpdateVideosScheduler.Instance.enable()
|
||||||
|
|
||||||
// Redis initialization
|
// Redis initialization
|
||||||
Redis.Instance.init()
|
Redis.Instance.init()
|
||||||
|
|
|
@ -174,7 +174,11 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
|
||||||
false // Display my NSFW videos
|
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 }))
|
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { rateVideoRouter } from './rate'
|
||||||
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
||||||
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
|
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
|
||||||
import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
|
import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
|
||||||
|
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
||||||
|
|
||||||
const videosRouter = express.Router()
|
const videosRouter = express.Router()
|
||||||
|
|
||||||
|
@ -231,6 +232,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
video.VideoFiles = [ videoFile ]
|
video.VideoFiles = [ videoFile ]
|
||||||
|
|
||||||
|
// Create tags
|
||||||
if (videoInfo.tags !== undefined) {
|
if (videoInfo.tags !== undefined) {
|
||||||
const tagInstances = await TagModel.findOrCreateTags(videoInfo.tags, t)
|
const tagInstances = await TagModel.findOrCreateTags(videoInfo.tags, t)
|
||||||
|
|
||||||
|
@ -238,6 +240,15 @@ async function addVideo (req: express.Request, res: express.Response) {
|
||||||
video.Tags = tagInstances
|
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)
|
await federateVideoIfNeeded(video, true, t)
|
||||||
|
|
||||||
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
|
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)
|
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
|
const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
|
||||||
await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
|
await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'express-validator'
|
||||||
import { values } from 'lodash'
|
import { values } from 'lodash'
|
||||||
import 'multer'
|
import 'multer'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
import { UserRight, VideoRateType } from '../../../shared'
|
import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
|
||||||
import {
|
import {
|
||||||
CONSTRAINTS_FIELDS,
|
CONSTRAINTS_FIELDS,
|
||||||
VIDEO_CATEGORIES,
|
VIDEO_CATEGORIES,
|
||||||
|
@ -98,10 +98,18 @@ function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } |
|
||||||
return isFileValid(files, videoImageTypesRegex, field, true)
|
return isFileValid(files, videoImageTypesRegex, field, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVideoPrivacyValid (value: string) {
|
function isVideoPrivacyValid (value: number) {
|
||||||
return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
|
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) {
|
function isVideoFileInfoHashValid (value: string) {
|
||||||
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
|
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
|
||||||
}
|
}
|
||||||
|
@ -174,6 +182,7 @@ export {
|
||||||
isVideoFileInfoHashValid,
|
isVideoFileInfoHashValid,
|
||||||
isVideoNameValid,
|
isVideoNameValid,
|
||||||
isVideoTagsValid,
|
isVideoTagsValid,
|
||||||
|
isScheduleVideoUpdatePrivacyValid,
|
||||||
isVideoAbuseReasonValid,
|
isVideoAbuseReasonValid,
|
||||||
isVideoFile,
|
isVideoFile,
|
||||||
isVideoStateValid,
|
isVideoStateValid,
|
||||||
|
|
|
@ -21,12 +21,16 @@ function retryTransactionWrapper <T, A> (
|
||||||
arg1: A
|
arg1: A
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
|
function retryTransactionWrapper <T> (
|
||||||
|
functionToRetry: () => Promise<T> | Bluebird<T>
|
||||||
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper <T> (
|
function retryTransactionWrapper <T> (
|
||||||
functionToRetry: (...args: any[]) => Promise<T> | Bluebird<T>,
|
functionToRetry: (...args: any[]) => Promise<T> | Bluebird<T>,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return transactionRetryer<T>(callback => {
|
return transactionRetryer<T>(callback => {
|
||||||
functionToRetry.apply(this, args)
|
functionToRetry.apply(null, args)
|
||||||
.then((result: T) => callback(null, result))
|
.then((result: T) => callback(null, result))
|
||||||
.catch(err => callback(err))
|
.catch(err => callback(err))
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { VideoPrivacy } from '../../shared/models/videos'
|
||||||
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
|
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
|
||||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||||
import { invert } from 'lodash'
|
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
|
// Use a variable to reload the configuration if we need
|
||||||
let config: IConfig = require('config')
|
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
|
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
|
||||||
|
|
||||||
// 1 hour
|
// 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
|
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
|
VIDEO_VIEW_LIFETIME = 1000 // 1 second
|
||||||
|
|
||||||
JOB_ATTEMPTS['email'] = 1
|
JOB_ATTEMPTS['email'] = 1
|
||||||
|
@ -513,7 +522,7 @@ export {
|
||||||
JOB_REQUEST_TTL,
|
JOB_REQUEST_TTL,
|
||||||
USER_PASSWORD_RESET_LIFETIME,
|
USER_PASSWORD_RESET_LIFETIME,
|
||||||
IMAGE_MIMETYPE_EXT,
|
IMAGE_MIMETYPE_EXT,
|
||||||
SCHEDULER_INTERVAL,
|
SCHEDULER_INTERVALS_MS,
|
||||||
STATIC_DOWNLOAD_PATHS,
|
STATIC_DOWNLOAD_PATHS,
|
||||||
RATES_LIMIT,
|
RATES_LIMIT,
|
||||||
VIDEO_EXT_MIMETYPE,
|
VIDEO_EXT_MIMETYPE,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { VideoFileModel } from '../models/video/video-file'
|
||||||
import { VideoShareModel } from '../models/video/video-share'
|
import { VideoShareModel } from '../models/video/video-share'
|
||||||
import { VideoTagModel } from '../models/video/video-tag'
|
import { VideoTagModel } from '../models/video/video-tag'
|
||||||
import { CONFIG } from './constants'
|
import { CONFIG } from './constants'
|
||||||
|
import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
|
||||||
|
|
||||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -73,7 +74,8 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
VideoBlacklistModel,
|
VideoBlacklistModel,
|
||||||
VideoTagModel,
|
VideoTagModel,
|
||||||
VideoModel,
|
VideoModel,
|
||||||
VideoCommentModel
|
VideoCommentModel,
|
||||||
|
ScheduleVideoUpdateModel
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!silent) logger.info('Database %s is ready.', dbname)
|
if (!silent) logger.info('Database %s is ready.', dbname)
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { SCHEDULER_INTERVAL } from '../../initializers'
|
|
||||||
|
|
||||||
export abstract class AbstractScheduler {
|
export abstract class AbstractScheduler {
|
||||||
|
|
||||||
|
protected abstract schedulerIntervalMs: number
|
||||||
|
|
||||||
private interval: NodeJS.Timer
|
private interval: NodeJS.Timer
|
||||||
|
|
||||||
enable () {
|
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 () {
|
disable () {
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { isTestInstance } from '../../helpers/core-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||||
import { AbstractScheduler } from './abstract-scheduler'
|
import { AbstractScheduler } from './abstract-scheduler'
|
||||||
|
import { SCHEDULER_INTERVALS_MS } from '../../initializers'
|
||||||
|
|
||||||
export class BadActorFollowScheduler extends AbstractScheduler {
|
export class BadActorFollowScheduler extends AbstractScheduler {
|
||||||
|
|
||||||
private static instance: AbstractScheduler
|
private static instance: AbstractScheduler
|
||||||
|
|
||||||
|
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.badActorFollow
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { isTestInstance } from '../../helpers/core-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { JobQueue } from '../job-queue'
|
import { JobQueue } from '../job-queue'
|
||||||
import { AbstractScheduler } from './abstract-scheduler'
|
import { AbstractScheduler } from './abstract-scheduler'
|
||||||
|
import { SCHEDULER_INTERVALS_MS } from '../../initializers'
|
||||||
|
|
||||||
export class RemoveOldJobsScheduler extends AbstractScheduler {
|
export class RemoveOldJobsScheduler extends AbstractScheduler {
|
||||||
|
|
||||||
private static instance: AbstractScheduler
|
private static instance: AbstractScheduler
|
||||||
|
|
||||||
|
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeOldJobs
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,17 @@ import * as express from 'express'
|
||||||
import 'express-validator'
|
import 'express-validator'
|
||||||
import { body, param, query } from 'express-validator/check'
|
import { body, param, query } from 'express-validator/check'
|
||||||
import { UserRight, VideoPrivacy } from '../../../shared'
|
import { UserRight, VideoPrivacy } from '../../../shared'
|
||||||
import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toValueOrNull } from '../../helpers/custom-validators/misc'
|
|
||||||
import {
|
import {
|
||||||
|
isBooleanValid,
|
||||||
|
isDateValid,
|
||||||
|
isIdOrUUIDValid,
|
||||||
|
isIdValid,
|
||||||
|
isUUIDValid,
|
||||||
|
toIntOrNull,
|
||||||
|
toValueOrNull
|
||||||
|
} from '../../helpers/custom-validators/misc'
|
||||||
|
import {
|
||||||
|
isScheduleVideoUpdatePrivacyValid,
|
||||||
isVideoAbuseReasonValid,
|
isVideoAbuseReasonValid,
|
||||||
isVideoCategoryValid,
|
isVideoCategoryValid,
|
||||||
isVideoChannelOfAccountExist,
|
isVideoChannelOfAccountExist,
|
||||||
|
@ -84,14 +93,21 @@ const videosAddValidator = [
|
||||||
.custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
|
.custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
|
||||||
body('channelId')
|
body('channelId')
|
||||||
.toInt()
|
.toInt()
|
||||||
.custom(isIdValid)
|
.custom(isIdValid).withMessage('Should have correct video channel id'),
|
||||||
.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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (areErrorsInVideoImageFiles(req, res)) return
|
if (areErrorsInVideoImageFiles(req, res)) return
|
||||||
|
if (areErrorsInScheduleUpdate(req, res)) return
|
||||||
|
|
||||||
const videoFile: Express.Multer.File = req.files['videofile'][0]
|
const videoFile: Express.Multer.File = req.files['videofile'][0]
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
|
@ -183,12 +199,20 @@ const videosUpdateValidator = [
|
||||||
.optional()
|
.optional()
|
||||||
.toInt()
|
.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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
|
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (areErrorsInVideoImageFiles(req, res)) return
|
if (areErrorsInVideoImageFiles(req, res)) return
|
||||||
|
if (areErrorsInScheduleUpdate(req, res)) return
|
||||||
if (!await isVideoExist(req.params.id, res)) return
|
if (!await isVideoExist(req.params.id, res)) return
|
||||||
|
|
||||||
const video = res.locals.video
|
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
|
const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
|
||||||
if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
|
if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
|
||||||
res.status(400)
|
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()
|
.end()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -379,3 +403,17 @@ function areErrorsInVideoImageFiles (req: express.Request, res: express.Response
|
||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Default,
|
Default,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
|
HasOne,
|
||||||
IFindOptions,
|
IFindOptions,
|
||||||
Is,
|
Is,
|
||||||
IsInt,
|
IsInt,
|
||||||
|
@ -47,7 +48,8 @@ import {
|
||||||
isVideoLanguageValid,
|
isVideoLanguageValid,
|
||||||
isVideoLicenceValid,
|
isVideoLicenceValid,
|
||||||
isVideoNameValid,
|
isVideoNameValid,
|
||||||
isVideoPrivacyValid, isVideoStateValid,
|
isVideoPrivacyValid,
|
||||||
|
isVideoStateValid,
|
||||||
isVideoSupportValid
|
isVideoSupportValid
|
||||||
} from '../../helpers/custom-validators/videos'
|
} from '../../helpers/custom-validators/videos'
|
||||||
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
|
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
|
||||||
|
@ -66,7 +68,8 @@ import {
|
||||||
VIDEO_EXT_MIMETYPE,
|
VIDEO_EXT_MIMETYPE,
|
||||||
VIDEO_LANGUAGES,
|
VIDEO_LANGUAGES,
|
||||||
VIDEO_LICENCES,
|
VIDEO_LICENCES,
|
||||||
VIDEO_PRIVACIES, VIDEO_STATES
|
VIDEO_PRIVACIES,
|
||||||
|
VIDEO_STATES
|
||||||
} from '../../initializers'
|
} from '../../initializers'
|
||||||
import {
|
import {
|
||||||
getVideoCommentsActivityPubUrl,
|
getVideoCommentsActivityPubUrl,
|
||||||
|
@ -88,8 +91,9 @@ import { VideoCommentModel } from './video-comment'
|
||||||
import { VideoFileModel } from './video-file'
|
import { VideoFileModel } from './video-file'
|
||||||
import { VideoShareModel } from './video-share'
|
import { VideoShareModel } from './video-share'
|
||||||
import { VideoTagModel } from './video-tag'
|
import { VideoTagModel } from './video-tag'
|
||||||
|
import { ScheduleVideoUpdateModel } from './schedule-video-update'
|
||||||
|
|
||||||
enum ScopeNames {
|
export enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
||||||
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
|
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
|
||||||
WITH_TAGS = 'WITH_TAGS',
|
WITH_TAGS = 'WITH_TAGS',
|
||||||
|
@ -495,6 +499,15 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
})
|
})
|
||||||
VideoComments: VideoCommentModel[]
|
VideoComments: VideoCommentModel[]
|
||||||
|
|
||||||
|
@HasOne(() => ScheduleVideoUpdateModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'videoId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'cascade'
|
||||||
|
})
|
||||||
|
ScheduleVideoUpdate: ScheduleVideoUpdateModel
|
||||||
|
|
||||||
@BeforeDestroy
|
@BeforeDestroy
|
||||||
static async sendDelete (instance: VideoModel, options) {
|
static async sendDelete (instance: VideoModel, options) {
|
||||||
if (instance.isOwned()) {
|
if (instance.isOwned()) {
|
||||||
|
@ -673,6 +686,10 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ScheduleVideoUpdateModel,
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1006,7 +1023,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
toFormattedJSON (options?: {
|
toFormattedJSON (options?: {
|
||||||
additionalAttributes: {
|
additionalAttributes: {
|
||||||
state: boolean,
|
state: boolean,
|
||||||
waitTranscoding: boolean
|
waitTranscoding: boolean,
|
||||||
|
scheduledUpdate: boolean
|
||||||
}
|
}
|
||||||
}): Video {
|
}): Video {
|
||||||
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
|
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
|
return videoObject
|
||||||
|
|
|
@ -6,3 +6,4 @@ import './server/jobs'
|
||||||
import './videos/video-comments'
|
import './videos/video-comments'
|
||||||
import './users/users-multiple-servers'
|
import './users/users-multiple-servers'
|
||||||
import './server/handle-down'
|
import './server/handle-down'
|
||||||
|
import './videos/video-schedule-update'
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -35,6 +35,10 @@ type VideoAttributes = {
|
||||||
fixture?: string
|
fixture?: string
|
||||||
thumbnailfile?: string
|
thumbnailfile?: string
|
||||||
previewfile?: string
|
previewfile?: string
|
||||||
|
scheduleUpdate?: {
|
||||||
|
updateAt: string
|
||||||
|
privacy?: VideoPrivacy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoCategories (url: string) {
|
function getVideoCategories (url: string) {
|
||||||
|
@ -371,6 +375,14 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
|
||||||
req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
|
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))
|
return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
|
||||||
.expect(specialStatus)
|
.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.tags) body['tags'] = attributes.tags
|
||||||
if (attributes.privacy) body['privacy'] = attributes.privacy
|
if (attributes.privacy) body['privacy'] = attributes.privacy
|
||||||
if (attributes.channelId) body['channelId'] = attributes.channelId
|
if (attributes.channelId) body['channelId'] = attributes.channelId
|
||||||
|
if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
|
||||||
|
|
||||||
// Upload request
|
// Upload request
|
||||||
if (attributes.thumbnailfile || attributes.previewfile) {
|
if (attributes.thumbnailfile || attributes.previewfile) {
|
||||||
|
|
|
@ -13,4 +13,8 @@ export interface VideoCreate {
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
commentsEnabled?: boolean
|
commentsEnabled?: boolean
|
||||||
privacy: VideoPrivacy
|
privacy: VideoPrivacy
|
||||||
|
scheduleUpdate?: {
|
||||||
|
updateAt: Date
|
||||||
|
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,4 +15,8 @@ export interface VideoUpdate {
|
||||||
channelId?: number
|
channelId?: number
|
||||||
thumbnailfile?: Blob
|
thumbnailfile?: Blob
|
||||||
previewfile?: Blob
|
previewfile?: Blob
|
||||||
|
scheduleUpdate?: {
|
||||||
|
updateAt: Date
|
||||||
|
privacy?: VideoPrivacy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,10 @@ export interface Video {
|
||||||
|
|
||||||
waitTranscoding?: boolean
|
waitTranscoding?: boolean
|
||||||
state?: VideoConstant<VideoState>
|
state?: VideoConstant<VideoState>
|
||||||
|
scheduledUpdate?: {
|
||||||
|
updateAt: Date | string
|
||||||
|
privacy?: VideoPrivacy
|
||||||
|
}
|
||||||
|
|
||||||
account: {
|
account: {
|
||||||
id: number
|
id: number
|
||||||
|
|
Loading…
Reference in New Issue