2024-03-15 15:47:18 +01:00
|
|
|
import { ffprobePromise, getChaptersFromContainer } from '@peertube/peertube-ffmpeg'
|
|
|
|
import { HttpStatusCode, ThumbnailType, VideoCreate } from '@peertube/peertube-models'
|
|
|
|
import { uuidToShort } from '@peertube/peertube-node-utils'
|
2023-07-31 14:34:36 +02:00
|
|
|
import { getResumableUploadPath } from '@server/helpers/upload.js'
|
2024-03-15 15:47:18 +01:00
|
|
|
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
2023-07-31 14:34:36 +02:00
|
|
|
import { Redis } from '@server/lib/redis.js'
|
2024-02-21 07:32:47 +01:00
|
|
|
import { setupUploadResumableRoutes, uploadx } from '@server/lib/uploadx.js'
|
2023-07-31 14:34:36 +02:00
|
|
|
import { buildNextVideoState } from '@server/lib/video-state.js'
|
|
|
|
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
2024-03-15 15:47:18 +01:00
|
|
|
import express from 'express'
|
|
|
|
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
|
2023-07-31 14:34:36 +02:00
|
|
|
import { createReqFiles } from '../../../helpers/express-utils.js'
|
|
|
|
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
2023-10-12 15:32:01 +02:00
|
|
|
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
2023-07-31 14:34:36 +02:00
|
|
|
import { Hooks } from '../../../lib/plugins/hooks.js'
|
2021-05-12 14:51:17 +02:00
|
|
|
import {
|
|
|
|
asyncMiddleware,
|
|
|
|
asyncRetryTransactionMiddleware,
|
|
|
|
authenticate,
|
|
|
|
videosAddLegacyValidator,
|
|
|
|
videosAddResumableInitValidator,
|
2022-01-18 09:29:46 +01:00
|
|
|
videosAddResumableValidator
|
2023-07-31 14:34:36 +02:00
|
|
|
} from '../../../middlewares/index.js'
|
2021-05-12 14:51:17 +02:00
|
|
|
|
|
|
|
const lTags = loggerTagsFactory('api', 'video')
|
|
|
|
const auditLogger = auditLoggerFactory('videos')
|
|
|
|
const uploadRouter = express.Router()
|
2021-11-10 09:42:37 +01:00
|
|
|
|
2021-05-12 14:51:17 +02:00
|
|
|
const reqVideoFileAdd = createReqFiles(
|
|
|
|
[ 'videofile', 'thumbnailfile', 'previewfile' ],
|
2022-03-04 10:57:36 +01:00
|
|
|
{ ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
|
2021-05-12 14:51:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const reqVideoFileAddResumable = createReqFiles(
|
|
|
|
[ 'thumbnailfile', 'previewfile' ],
|
|
|
|
MIMETYPES.IMAGE.MIMETYPE_EXT,
|
2022-03-04 10:57:36 +01:00
|
|
|
getResumableUploadPath()
|
2021-05-12 14:51:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
uploadRouter.post('/upload',
|
2021-06-04 08:57:07 +02:00
|
|
|
openapiOperationDoc({ operationId: 'uploadLegacy' }),
|
2021-05-12 14:51:17 +02:00
|
|
|
authenticate,
|
|
|
|
reqVideoFileAdd,
|
|
|
|
asyncMiddleware(videosAddLegacyValidator),
|
|
|
|
asyncRetryTransactionMiddleware(addVideoLegacy)
|
|
|
|
)
|
|
|
|
|
2024-02-14 10:20:02 +01:00
|
|
|
setupUploadResumableRoutes({
|
|
|
|
routePath: '/upload-resumable',
|
|
|
|
router: uploadRouter,
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2024-02-14 10:20:02 +01:00
|
|
|
uploadInitBeforeMiddlewares: [
|
|
|
|
openapiOperationDoc({ operationId: 'uploadResumableInit' }),
|
|
|
|
reqVideoFileAddResumable
|
|
|
|
],
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2024-02-14 10:20:02 +01:00
|
|
|
uploadInitAfterMiddlewares: [ asyncMiddleware(videosAddResumableInitValidator) ],
|
|
|
|
|
|
|
|
uploadDeleteMiddlewares: [ asyncMiddleware(deleteUploadResumableCache) ],
|
|
|
|
|
|
|
|
uploadedMiddlewares: [
|
|
|
|
openapiOperationDoc({ operationId: 'uploadResumable' }),
|
|
|
|
asyncMiddleware(videosAddResumableValidator)
|
|
|
|
],
|
|
|
|
uploadedController: asyncMiddleware(addVideoResumable)
|
|
|
|
})
|
2021-05-12 14:51:17 +02:00
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
export {
|
|
|
|
uploadRouter
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2021-11-10 09:42:37 +01:00
|
|
|
async function addVideoLegacy (req: express.Request, res: express.Response) {
|
2021-05-12 14:51:17 +02:00
|
|
|
// Uploading the video could be long
|
|
|
|
// Set timeout to 10 minutes, as Express's default is 2 minutes
|
|
|
|
req.setTimeout(1000 * 60 * 10, () => {
|
2021-06-01 01:36:53 +02:00
|
|
|
logger.error('Video upload has timed out.')
|
|
|
|
return res.fail({
|
|
|
|
status: HttpStatusCode.REQUEST_TIMEOUT_408,
|
|
|
|
message: 'Video upload has timed out.'
|
|
|
|
})
|
2021-05-12 14:51:17 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
const videoPhysicalFile = req.files['videofile'][0]
|
|
|
|
const videoInfo: VideoCreate = req.body
|
|
|
|
const files = req.files
|
|
|
|
|
2021-11-24 14:33:14 +01:00
|
|
|
const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })
|
2021-10-25 17:42:20 +02:00
|
|
|
|
|
|
|
return res.json(response)
|
2021-05-12 14:51:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-10 09:42:37 +01:00
|
|
|
async function addVideoResumable (req: express.Request, res: express.Response) {
|
2023-07-19 16:02:49 +02:00
|
|
|
const videoPhysicalFile = res.locals.uploadVideoFileResumable
|
2021-05-12 14:51:17 +02:00
|
|
|
const videoInfo = videoPhysicalFile.metadata
|
2023-06-06 11:14:13 +02:00
|
|
|
const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile }
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2021-11-24 14:33:14 +01:00
|
|
|
const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })
|
2024-02-19 14:41:38 +01:00
|
|
|
await Redis.Instance.deleteUploadSession(req.query.upload_id)
|
|
|
|
await uploadx.storage.delete(res.locals.uploadVideoFileResumable)
|
2021-10-25 17:42:20 +02:00
|
|
|
|
|
|
|
return res.json(response)
|
2021-05-12 14:51:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function addVideo (options: {
|
2021-11-24 14:33:14 +01:00
|
|
|
req: express.Request
|
2021-05-12 14:51:17 +02:00
|
|
|
res: express.Response
|
2024-02-14 10:20:02 +01:00
|
|
|
videoPhysicalFile: express.VideoLegacyUploadFile
|
2021-05-12 14:51:17 +02:00
|
|
|
videoInfo: VideoCreate
|
|
|
|
files: express.UploadFiles
|
|
|
|
}) {
|
2021-11-24 14:33:14 +01:00
|
|
|
const { req, res, videoPhysicalFile, videoInfo, files } = options
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2023-10-19 14:18:22 +02:00
|
|
|
const ffprobe = await ffprobePromise(videoPhysicalFile.path)
|
|
|
|
|
2023-10-12 15:32:01 +02:00
|
|
|
const containerChapters = await getChaptersFromContainer({
|
|
|
|
path: videoPhysicalFile.path,
|
2023-10-19 14:18:22 +02:00
|
|
|
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
|
|
|
|
ffprobe
|
2023-10-12 15:32:01 +02:00
|
|
|
})
|
2024-02-13 14:23:32 +01:00
|
|
|
logger.debug(`Got ${containerChapters.length} chapters from video "${videoInfo.name}" container`, { containerChapters, ...lTags() })
|
|
|
|
|
|
|
|
const thumbnails = [ { type: ThumbnailType.MINIATURE, field: 'thumbnailfile' }, { type: ThumbnailType.PREVIEW, field: 'previewfile' } ]
|
|
|
|
.filter(({ field }) => !!files?.[field]?.[0])
|
|
|
|
.map(({ type, field }) => ({
|
|
|
|
path: files[field][0].path,
|
|
|
|
type,
|
|
|
|
automaticallyGenerated: false,
|
|
|
|
keepOriginal: false
|
|
|
|
}))
|
|
|
|
|
|
|
|
const localVideoCreator = new LocalVideoCreator({
|
|
|
|
lTags,
|
2024-03-15 15:47:18 +01:00
|
|
|
|
|
|
|
videoFile: {
|
|
|
|
path: videoPhysicalFile.path,
|
|
|
|
probe: res.locals.ffprobe
|
|
|
|
},
|
|
|
|
|
2024-02-13 14:23:32 +01:00
|
|
|
user: res.locals.oauth.token.User,
|
|
|
|
channel: res.locals.videoChannel,
|
|
|
|
|
|
|
|
chapters: undefined,
|
|
|
|
fallbackChapters: {
|
|
|
|
fromDescription: true,
|
|
|
|
finalFallback: containerChapters
|
|
|
|
},
|
|
|
|
|
|
|
|
videoAttributes: {
|
|
|
|
...videoInfo,
|
|
|
|
|
|
|
|
duration: videoPhysicalFile.duration,
|
2024-03-15 15:47:18 +01:00
|
|
|
inputFilename: videoPhysicalFile.originalname,
|
2024-02-13 14:23:32 +01:00
|
|
|
state: buildNextVideoState(),
|
|
|
|
isLive: false
|
|
|
|
},
|
|
|
|
|
|
|
|
liveAttributes: undefined,
|
|
|
|
|
|
|
|
videoAttributeResultHook: 'filter:api.video.upload.video-attribute.result',
|
|
|
|
|
|
|
|
thumbnails
|
2021-05-12 14:51:17 +02:00
|
|
|
})
|
|
|
|
|
2024-02-13 14:23:32 +01:00
|
|
|
const { video } = await localVideoCreator.create()
|
2021-08-30 16:24:25 +02:00
|
|
|
|
2024-02-13 14:23:32 +01:00
|
|
|
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(video.toFormattedDetailsJSON()))
|
|
|
|
logger.info('Video with name %s and uuid %s created.', videoInfo.name, video.uuid, lTags(video.uuid))
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2024-02-13 14:23:32 +01:00
|
|
|
Hooks.runAction('action:api.video.uploaded', { video, req, res })
|
2021-05-12 14:51:17 +02:00
|
|
|
|
2021-10-25 17:42:20 +02:00
|
|
|
return {
|
2021-05-12 14:51:17 +02:00
|
|
|
video: {
|
2024-02-13 14:23:32 +01:00
|
|
|
id: video.id,
|
|
|
|
shortUUID: uuidToShort(video.uuid),
|
|
|
|
uuid: video.uuid
|
2021-05-12 14:51:17 +02:00
|
|
|
}
|
2021-10-25 17:42:20 +02:00
|
|
|
}
|
2021-05-12 14:51:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-10 09:42:37 +01:00
|
|
|
async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
|
|
await Redis.Instance.deleteUploadSession(req.query.upload_id)
|
|
|
|
|
|
|
|
return next()
|
|
|
|
}
|