X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fjob-queue%2Fhandlers%2Fvideo-import.ts;h=4ce1a6c30d9b52d22dc0e1a6fdeaacee353cfe7a;hb=854f533c12bd2b88c70f9d5aeab770059e9a6861;hp=09f225cec8311b6c06fd3608e1b3cfceb7aeb16d;hpb=a15871560f80e07386c1dabb8370cd2664ecfd1f;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 09f225cec..4ce1a6c30 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -1,45 +1,43 @@ -import * as Bull from 'bull' -import { logger } from '../../../helpers/logger' -import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl' -import { VideoImportModel } from '../../../models/video/video-import' +import { Job } from 'bull' +import { move, remove, stat } from 'fs-extra' +import { getLowercaseExtension } from '@server/helpers/core-utils' +import { retryTransactionWrapper } from '@server/helpers/database-utils' +import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' +import { isPostImportVideoAccepted } from '@server/lib/moderation' +import { generateWebTorrentVideoFilename } from '@server/lib/paths' +import { Hooks } from '@server/lib/plugins/hooks' +import { ServerConfigManager } from '@server/lib/server-config-manager' +import { isAbleToUploadVideo } from '@server/lib/user' +import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video' +import { VideoPathManager } from '@server/lib/video-path-manager' +import { buildNextVideoState } from '@server/lib/video-state' +import { ThumbnailModel } from '@server/models/video/thumbnail' +import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import' +import { + VideoImportPayload, + VideoImportTorrentPayload, + VideoImportTorrentPayloadType, + VideoImportYoutubeDLPayload, + VideoImportYoutubeDLPayloadType, + VideoState +} from '../../../../shared' import { VideoImportState } from '../../../../shared/models/videos' -import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' -import { extname } from 'path' -import { VideoFileModel } from '../../../models/video/video-file' +import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' +import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' +import { logger } from '../../../helpers/logger' +import { getSecureTorrentName } from '../../../helpers/utils' +import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' -import { VideoState } from '../../../../shared' -import { JobQueue } from '../index' -import { federateVideoIfNeeded } from '../../activitypub' +import { sequelizeTypescript } from '../../../initializers/database' import { VideoModel } from '../../../models/video/video' -import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' -import { getSecureTorrentName } from '../../../helpers/utils' -import { move, remove, stat } from 'fs-extra' +import { VideoFileModel } from '../../../models/video/video-file' +import { VideoImportModel } from '../../../models/video/video-import' +import { MThumbnail } from '../../../types/models/video/thumbnail' +import { federateVideoIfNeeded } from '../../activitypub/videos' import { Notifier } from '../../notifier' -import { CONFIG } from '../../../initializers/config' -import { sequelizeTypescript } from '../../../initializers/database' -import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' -import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' -import { MThumbnail } from '../../../typings/models/video/thumbnail' -import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' -import { getVideoFilePath } from '@server/lib/video-paths' - -type VideoImportYoutubeDLPayload = { - type: 'youtube-dl' - videoImportId: number - - thumbnailUrl: string - downloadThumbnail: boolean - downloadPreview: boolean -} - -type VideoImportTorrentPayload = { - type: 'magnet-uri' | 'torrent-file' - videoImportId: number -} - -export type VideoImportPayload = VideoImportYoutubeDLPayload | VideoImportTorrentPayload +import { generateVideoMiniature } from '../../thumbnail' -async function processVideoImport (job: Bull.Job) { +async function processVideoImport (job: Job) { const payload = job.data as VideoImportPayload if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload) @@ -54,43 +52,38 @@ export { // --------------------------------------------------------------------------- -async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentPayload) { +async function processTorrentImport (job: Job, payload: VideoImportTorrentPayload) { logger.info('Processing torrent video import in job %d.', job.id) const videoImport = await getVideoImportOrDie(payload.videoImportId) const options = { - videoImportId: payload.videoImportId, - - downloadThumbnail: false, - downloadPreview: false, - - generateThumbnail: true, - generatePreview: true + type: payload.type, + videoImportId: payload.videoImportId } const target = { torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined, - magnetUri: videoImport.magnetUri + uri: videoImport.magnetUri } return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options) } -async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutubeDLPayload) { +async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPayload) { logger.info('Processing youtubeDL video import in job %d.', job.id) const videoImport = await getVideoImportOrDie(payload.videoImportId) const options = { - videoImportId: videoImport.id, - - downloadThumbnail: payload.downloadThumbnail, - downloadPreview: payload.downloadPreview, - thumbnailUrl: payload.thumbnailUrl, - - generateThumbnail: false, - generatePreview: false + type: payload.type, + videoImportId: videoImport.id } - return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) + const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) + + return processFile( + () => youtubeDL.downloadVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), + videoImport, + options + ) } async function getVideoImportOrDie (videoImportId: number) { @@ -103,18 +96,11 @@ async function getVideoImportOrDie (videoImportId: number) { } type ProcessFileOptions = { + type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType videoImportId: number - - downloadThumbnail: boolean - downloadPreview: boolean - thumbnailUrl?: string - - generateThumbnail: boolean - generatePreview: boolean } async function processFile (downloader: () => Promise, videoImport: MVideoImportDefault, options: ProcessFileOptions) { let tempVideoPath: string - let videoDestFile: string let videoFile: VideoFileModel try { @@ -123,86 +109,133 @@ async function processFile (downloader: () => Promise, videoImport: MVid // Get information about this video const stats = await stat(tempVideoPath) - const isAble = await videoImport.User.isAbleToUploadVideo({ size: stats.size }) + const isAble = await isAbleToUploadVideo(videoImport.User.id, stats.size) if (isAble === false) { throw new Error('The user video quota is exceeded with this video to import.') } - const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) + const { resolution } = await getVideoFileResolution(tempVideoPath) const fps = await getVideoFileFPS(tempVideoPath) const duration = await getDurationFromVideoFile(tempVideoPath) - // Create video file object in database + // Prepare video file object for creation in database + const fileExt = getLowercaseExtension(tempVideoPath) const videoFileData = { - extname: extname(tempVideoPath), - resolution: videoFileResolution, + extname: fileExt, + resolution, size: stats.size, + filename: generateWebTorrentVideoFilename(resolution, fileExt), fps, videoId: videoImport.videoId } videoFile = new VideoFileModel(videoFileData) + const hookName = options.type === 'youtube-dl' + ? 'filter:api.video.post-import-url.accept.result' + : 'filter:api.video.post-import-torrent.accept.result' + + // Check we accept this video + const acceptParameters = { + videoImport, + video: videoImport.Video, + videoFilePath: tempVideoPath, + videoFile, + user: videoImport.User + } + const acceptedResult = await Hooks.wrapFun(isPostImportVideoAccepted, acceptParameters, hookName) + + if (acceptedResult.accepted !== true) { + logger.info('Refused imported video.', { acceptedResult, acceptParameters }) + + videoImport.state = VideoImportState.REJECTED + await videoImport.save() + + throw new Error(acceptedResult.errorMessage) + } + + // Video is accepted, resuming preparation const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] }) // To clean files if the import fails const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles }) // Move file - videoDestFile = getVideoFilePath(videoImportWithFiles.Video, videoFile) + const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile) await move(tempVideoPath, videoDestFile) tempVideoPath = null // This path is not used anymore - // Process thumbnail + // Generate miniature if the import did not created it let thumbnailModel: MThumbnail - if (options.downloadThumbnail && options.thumbnailUrl) { - thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE) - } else if (options.generateThumbnail || options.downloadThumbnail) { - thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) + let thumbnailSave: object + if (!videoImportWithFiles.Video.getMiniature()) { + thumbnailModel = await generateVideoMiniature({ + video: videoImportWithFiles.Video, + videoFile, + type: ThumbnailType.MINIATURE + }) + thumbnailSave = thumbnailModel.toJSON() } - // Process preview + // Generate preview if the import did not created it let previewModel: MThumbnail - if (options.downloadPreview && options.thumbnailUrl) { - previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW) - } else if (options.generatePreview || options.downloadPreview) { - previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) + let previewSave: object + if (!videoImportWithFiles.Video.getPreview()) { + previewModel = await generateVideoMiniature({ + video: videoImportWithFiles.Video, + videoFile, + type: ThumbnailType.PREVIEW + }) + previewSave = previewModel.toJSON() } // Create torrent await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile) - const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => { - const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo + const videoFileSave = videoFile.toJSON() + + const { videoImportUpdated, video } = await retryTransactionWrapper(() => { + return sequelizeTypescript.transaction(async t => { + const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo + + // Refresh video + const video = await VideoModel.load(videoImportToUpdate.videoId, t) + if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.') - // Refresh video - const video = await VideoModel.load(videoImportToUpdate.videoId, t) - if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.') + const videoFileCreated = await videoFile.save({ transaction: t }) - const videoFileCreated = await videoFile.save({ transaction: t }) - videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] }) + // Update video DB object + video.duration = duration + video.state = buildNextVideoState(video.state) + await video.save({ transaction: t }) - // Update video DB object - video.duration = duration - video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED - await video.save({ transaction: t }) + if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t) + if (previewModel) await video.addAndSaveThumbnail(previewModel, t) - if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t) - if (previewModel) await video.addAndSaveThumbnail(previewModel, t) + // Now we can federate the video (reload from database, we need more attributes) + const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) + await federateVideoIfNeeded(videoForFederation, true, t) - // Now we can federate the video (reload from database, we need more attributes) - const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) - await federateVideoIfNeeded(videoForFederation, true, t) + // Update video import object + videoImportToUpdate.state = VideoImportState.SUCCESS + const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo + videoImportUpdated.Video = video - // Update video import object - videoImportToUpdate.state = VideoImportState.SUCCESS - const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo - videoImportUpdated.Video = video + videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] }) - logger.info('Video %s imported.', video.uuid) + logger.info('Video %s imported.', video.uuid) - return { videoImportUpdated, video: videoForFederation } + return { videoImportUpdated, video: videoForFederation } + }).catch(err => { + // Reset fields + if (thumbnailModel) thumbnailModel = new ThumbnailModel(thumbnailSave) + if (previewModel) previewModel = new ThumbnailModel(previewSave) + + videoFile = new VideoFileModel(videoFileSave) + + throw err + }) }) - Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) + Notifier.Instance.notifyOnFinishedVideoImport({ videoImport: videoImportUpdated, success: true }) if (video.isBlacklisted()) { const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video }) @@ -212,16 +245,13 @@ async function processFile (downloader: () => Promise, videoImport: MVid Notifier.Instance.notifyOnNewVideoIfNeeded(video) } + if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { + return addMoveToObjectStorageJob(videoImportUpdated.Video) + } + // Create transcoding jobs? if (video.state === VideoState.TO_TRANSCODE) { - // Put uuid because we don't have id auto incremented for now - const dataInput = { - type: 'optimize' as 'optimize', - videoUUID: videoImportUpdated.Video.uuid, - isNewVideo: true - } - - await JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput }) + await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile, videoImport.User) } } catch (err) { @@ -232,10 +262,12 @@ async function processFile (downloader: () => Promise, videoImport: MVid } videoImport.error = err.message - videoImport.state = VideoImportState.FAILED + if (videoImport.state !== VideoImportState.REJECTED) { + videoImport.state = VideoImportState.FAILED + } await videoImport.save() - Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false) + Notifier.Instance.notifyOnFinishedVideoImport({ videoImport, success: false }) throw err }