Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle video file move in a job #4282

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/controllers/api/videos/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {

const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
video,
// @ts-expect-error
files: req.files,
fallback: type => {
return updateVideoMiniatureFromExisting({
Expand Down
4 changes: 3 additions & 1 deletion server/controllers/api/videos/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ export async function updateVideo (req: express.Request, res: express.Response)
const wasConfidentialVideo = videoInstance.isConfidential()
const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()

// @ts-expect-error
const files: { [fieldname: string]: Express.Multer.File[] } = req.files
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
video: videoInstance,
files: req.files,
files,
fallback: () => Promise.resolve(undefined),
automaticallyGenerated: false
})
Expand Down
159 changes: 28 additions & 131 deletions server/controllers/api/videos/upload.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import * as express from 'express'
import { move } from 'fs-extra'
import { getLowercaseExtension } from '@server/helpers/core-utils'
import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
import { uuidToShort } from '@server/helpers/uuid'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
import { buildLocalVideoFromReq, setVideoTags } from '@server/lib/video'
import { openapiOperationDoc } from '@server/middlewares/doc'
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
import { MVideoFullLight } from '@server/types/models'
import { uploadx } from '@uploadx/core'
import { VideoCreate, VideoState } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { createReqFiles } from '../../../helpers/express-utils'
import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
import { logger, loggerTagsFactory } from '../../../helpers/logger'
import { CONFIG } from '../../../initializers/config'
import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants'
import { MIMETYPES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
import { Notifier } from '../../../lib/notifier'
import { Hooks } from '../../../lib/plugins/hooks'
import { generateVideoMiniature } from '../../../lib/thumbnail'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
Expand All @@ -34,8 +22,8 @@ import {
videosAddResumableValidator
} from '../../../middlewares'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
import { VideoFileModel } from '../../../models/video/video-file'
import { JobQueue } from '@server/lib/job-queue'
import { VideoModel } from '@server/models/video/video'

const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
Expand Down Expand Up @@ -111,75 +99,44 @@ export async function addVideoLegacy (req: express.Request, res: express.Respons

const videoPhysicalFile = req.files['videofile'][0]
const videoInfo: VideoCreate = req.body
const files = req.files

return addVideo({ res, videoPhysicalFile, videoInfo, files })
return addVideo({ res, videoInfo, videoPhysicalFile })
}

export async function addVideoResumable (_req: express.Request, res: express.Response) {
const videoPhysicalFile = res.locals.videoFileResumable
const videoInfo = videoPhysicalFile.metadata
const files = { previewfile: videoInfo.previewfile }

// Don't need the meta file anymore
await deleteResumableUploadMetaFile(videoPhysicalFile.path)

return addVideo({ res, videoPhysicalFile, videoInfo, files })
return addVideo({ res, videoInfo, videoPhysicalFile })
}

async function addVideo (options: {
res: express.Response
videoPhysicalFile: express.VideoUploadFile
videoInfo: VideoCreate
files: express.UploadFiles
videoPhysicalFile: express.VideoUploadFile
}) {
const { res, videoPhysicalFile, videoInfo, files } = options
const videoChannel = res.locals.videoChannel
const user = res.locals.oauth.token.User

const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)

videoData.state = CONFIG.TRANSCODING.ENABLED
? VideoState.TO_TRANSCODE
: VideoState.PUBLISHED

videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware

const { res, videoInfo, videoPhysicalFile } = options
const files = { previewfile: videoInfo.previewfile }
const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id)
const video = new VideoModel(videoData) as MVideoFullLight
video.VideoChannel = videoChannel
video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object

const videoFile = await buildNewFile(video, videoPhysicalFile)

// Move physical file
const destination = getVideoFilePath(video, videoFile)
await move(videoPhysicalFile.path, destination)
// This is important in case if there is another attempt in the retry process
videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
videoPhysicalFile.path = destination

const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
video,
files,
fallback: type => generateVideoMiniature({ video, videoFile, type })
})
video.state = VideoState.TO_PROCESS
video.duration = 0
video.url = ''
video.VideoFiles = []
// const videoFile = res.locals.videoFile
const user = res.locals.oauth.token.User

const { videoCreated } = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }

const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight

await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
await videoCreated.addAndSaveThumbnail(previewModel, t)

// Do not forget to add video channel information to the created video
videoCreated.VideoChannel = res.locals.videoChannel

videoFile.videoId = video.id
await videoFile.save(sequelizeOptions)

video.VideoFiles = [ videoFile ]

await setVideoTags({ video, tags: videoInfo.tags, transaction: t })

// Schedule an update in the future?
Expand All @@ -191,31 +148,23 @@ async function addVideo (options: {
}, sequelizeOptions)
}

// Channel has a new content, set as updated
await videoCreated.VideoChannel.setAsUpdated(t)

await autoBlacklistVideoIfNeeded({
video,
user,
isRemote: false,
isNew: true,
transaction: t
})

auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))

JobQueue.Instance.createJob({
type: 'video-process',
payload: {
previewFilePath: files.previewfile?.[0].path,
videoFilePath: res.locals.videoFileResumable.path,
videoId: videoCreated.id,
videoPhysicalFile,
userId: user.id
}
})

return { videoCreated }
})

createTorrentFederate(video, videoFile)

if (video.state === VideoState.TO_TRANSCODE) {
await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
}

Hooks.runAction('action:api.video.uploaded', { video: videoCreated })

return res.json({
video: {
id: videoCreated.id,
Expand All @@ -224,55 +173,3 @@ async function addVideo (options: {
}
})
}

async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) {
const videoFile = new VideoFileModel({
extname: getLowercaseExtension(videoPhysicalFile.filename),
size: videoPhysicalFile.size,
videoStreamingPlaylistId: null,
metadata: await getMetadataFromFile(videoPhysicalFile.path)
})

if (videoFile.isAudio()) {
videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
} else {
videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
}

videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)

return videoFile
}

async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
await createTorrentAndSetInfoHash(video, fileArg)

// Refresh videoFile because the createTorrentAndSetInfoHash could be long
const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
// File does not exist anymore, remove the generated torrent
if (!refreshedFile) return fileArg.removeTorrent()

refreshedFile.infoHash = fileArg.infoHash
refreshedFile.torrentFilename = fileArg.torrentFilename

return refreshedFile.save()
}

function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
// Create the torrent file in async way because it could be long
createTorrentAndSetInfoHashAsync(video, videoFile)
.catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
.then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
.then(refreshedVideo => {
if (!refreshedVideo) return

// Only federate and notify after the torrent creation
Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)

return retryTransactionWrapper(() => {
return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
})
})
.catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
}
12 changes: 8 additions & 4 deletions server/initializers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
'videos-views': 1,
'activitypub-refresher': 1,
'video-redundancy': 1,
'video-live-ending': 1
'video-live-ending': 1,
'video-process': 1
}
// Excluded keys are jobs that can be configured by admins
const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
Expand All @@ -162,7 +163,8 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
'videos-views': 1,
'activitypub-refresher': 1,
'video-redundancy': 1,
'video-live-ending': 10
'video-live-ending': 10,
'video-process': 1
}
const JOB_TTL: { [id in JobType]: number } = {
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
Expand All @@ -178,7 +180,8 @@ const JOB_TTL: { [id in JobType]: number } = {
'videos-views': undefined, // Unlimited
'activitypub-refresher': 60000 * 10, // 10 minutes
'video-redundancy': 1000 * 3600 * 3, // 3 hours
'video-live-ending': 1000 * 60 * 10 // 10 minutes
'video-live-ending': 1000 * 60 * 10, // 10 minutes
'video-process': 1000 * 3600 // 1 hour
}
const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = {
'videos-views': {
Expand Down Expand Up @@ -412,7 +415,8 @@ const VIDEO_STATES: { [ id in VideoState ]: string } = {
[VideoState.TO_TRANSCODE]: 'To transcode',
[VideoState.TO_IMPORT]: 'To import',
[VideoState.WAITING_FOR_LIVE]: 'Waiting for livestream',
[VideoState.LIVE_ENDED]: 'Livestream ended'
[VideoState.LIVE_ENDED]: 'Livestream ended',
[VideoState.TO_PROCESS]: 'To process'
}

const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = {
Expand Down
Loading