Add ability for uploaders to schedule video update
This commit is contained in:
parent
bf079b7bfd
commit
2baea0c77c
21 changed files with 469 additions and 22 deletions
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
62
server/lib/schedulers/update-videos-scheduler.ts
Normal file
62
server/lib/schedulers/update-videos-scheduler.ts
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
71
server/models/video/schedule-video-update.ts
Normal file
71
server/models/video/schedule-video-update.ts
Normal 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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
164
server/tests/api/videos/video-schedule-update.ts
Normal file
164
server/tests/api/videos/video-schedule-update.ts
Normal 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)
|
||||
})
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -13,4 +13,8 @@ export interface VideoCreate {
|
|||
tags?: string[]
|
||||
commentsEnabled?: boolean
|
||||
privacy: VideoPrivacy
|
||||
scheduleUpdate?: {
|
||||
updateAt: Date
|
||||
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,4 +15,8 @@ export interface VideoUpdate {
|
|||
channelId?: number
|
||||
thumbnailfile?: Blob
|
||||
previewfile?: Blob
|
||||
scheduleUpdate?: {
|
||||
updateAt: Date
|
||||
privacy?: VideoPrivacy
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@ export interface Video {
|
|||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoState>
|
||||
scheduledUpdate?: {
|
||||
updateAt: Date | string
|
||||
privacy?: VideoPrivacy
|
||||
}
|
||||
|
||||
account: {
|
||||
id: number
|
||||
|
|
Loading…
Reference in a new issue