X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fjob-queue%2Fhandlers%2Fvideo-import.ts;h=82edb8d5c0001476efeaa1fed41f2959ef42ac69;hb=cef534ed53e4518fe0acf581bfe880788d42fc36;hp=5a7722153d321476bdaafa865b88ffaa1aeebb8c;hpb=ed31c059851a30bd5ba9999f8ecb3822d576b9f4;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 5a7722153..82edb8d5c 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -6,37 +6,124 @@ import { VideoImportState } from '../../../../shared/models/videos' import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' import { extname, join } from 'path' import { VideoFileModel } from '../../../models/video/video-file' -import { renamePromise, statPromise, unlinkPromise } from '../../../helpers/core-utils' -import { CONFIG, sequelizeTypescript } from '../../../initializers' -import { doRequestAndSaveToFile } from '../../../helpers/requests' +import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' +import { downloadImage } from '../../../helpers/requests' import { VideoState } from '../../../../shared' import { JobQueue } from '../index' import { federateVideoIfNeeded } from '../../activitypub' +import { VideoModel } from '../../../models/video/video' +import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' +import { getSecureTorrentName } from '../../../helpers/utils' +import { remove, move, stat } from 'fs-extra' +import { Notifier } from '../../notifier' -export type VideoImportPayload = { +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 + async function processVideoImport (job: Bull.Job) { const payload = job.data as VideoImportPayload - logger.info('Processing video import in job %d.', job.id) - const videoImport = await VideoImportModel.loadAndPopulateVideo(payload.videoImportId) - if (!videoImport) throw new Error('Cannot import video %s: the video import entry does not exist anymore.') + if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload) + if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload) +} + +// --------------------------------------------------------------------------- + +export { + processVideoImport +} + +// --------------------------------------------------------------------------- + +async function processTorrentImport (job: Bull.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 + } + const target = { + torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined, + magnetUri: videoImport.magnetUri + } + return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options) +} + +async function processYoutubeDLImport (job: Bull.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 + } + + return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) +} + +async function getVideoImportOrDie (videoImportId: number) { + const videoImport = await VideoImportModel.loadAndPopulateVideo(videoImportId) + if (!videoImport || !videoImport.Video) { + throw new Error('Cannot import video %s: the video import or video linked to this import does not exist anymore.') + } + + return videoImport +} + +type ProcessFileOptions = { + videoImportId: number + + downloadThumbnail: boolean + downloadPreview: boolean + thumbnailUrl?: string + + generateThumbnail: boolean + generatePreview: boolean +} +async function processFile (downloader: () => Promise, videoImport: VideoImportModel, options: ProcessFileOptions) { let tempVideoPath: string + let videoDestFile: string + let videoFile: VideoFileModel + try { // Download video from youtubeDL - tempVideoPath = await downloadYoutubeDLVideo(videoImport.targetUrl) + tempVideoPath = await downloader() // Get information about this video + const stats = await stat(tempVideoPath) + const isAble = await videoImport.User.isAbleToUploadVideo({ size: 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 fps = await getVideoFileFPS(tempVideoPath + 's') - const stats = await statPromise(tempVideoPath) + const fps = await getVideoFileFPS(tempVideoPath) const duration = await getDurationFromVideoFile(tempVideoPath) // Create video file object in database @@ -47,51 +134,64 @@ async function processVideoImport (job: Bull.Job) { fps, videoId: videoImport.videoId } - const videoFile = new VideoFileModel(videoFileData) + videoFile = new VideoFileModel(videoFileData) + // To clean files if the import fails + videoImport.Video.VideoFiles = [ videoFile ] // Move file - const destination = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile)) - await renamePromise(tempVideoPath, destination) + videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile)) + await move(tempVideoPath, videoDestFile) + tempVideoPath = null // This path is not used anymore // Process thumbnail - if (payload.downloadThumbnail) { - if (payload.thumbnailUrl) { - const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) - await doRequestAndSaveToFile({ method: 'GET', uri: payload.thumbnailUrl }, destThumbnailPath) + if (options.downloadThumbnail) { + if (options.thumbnailUrl) { + await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE) } else { await videoImport.Video.createThumbnail(videoFile) } + } else if (options.generateThumbnail) { + await videoImport.Video.createThumbnail(videoFile) } // Process preview - if (payload.downloadPreview) { - if (payload.thumbnailUrl) { - const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) - await doRequestAndSaveToFile({ method: 'GET', uri: payload.thumbnailUrl }, destPreviewPath) + if (options.downloadPreview) { + if (options.thumbnailUrl) { + await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE) } else { await videoImport.Video.createPreview(videoFile) } + } else if (options.generatePreview) { + await videoImport.Video.createPreview(videoFile) } // Create torrent await videoImport.Video.createTorrentAndSetInfoHash(videoFile) const videoImportUpdated: VideoImportModel = await sequelizeTypescript.transaction(async t => { - await videoFile.save({ transaction: t }) + // Refresh video + const video = await VideoModel.load(videoImport.videoId, t) + if (!video) throw new Error('Video linked to import ' + videoImport.videoId + ' does not exist anymore.') + videoImport.Video = video + + const videoFileCreated = await videoFile.save({ transaction: t }) + video.VideoFiles = [ videoFileCreated ] // Update video DB object - videoImport.Video.duration = duration - videoImport.Video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED - const videoUpdated = await videoImport.Video.save({ transaction: t }) + video.duration = duration + video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED + const videoUpdated = await video.save({ transaction: t }) - // Now we can federate the video - await federateVideoIfNeeded(videoImport.Video, 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) + Notifier.Instance.notifyOnNewVideo(videoForFederation) // Update video import object videoImport.state = VideoImportState.SUCCESS const videoImportUpdated = await videoImport.save({ transaction: t }) - logger.info('Video %s imported.', videoImport.targetUrl) + logger.info('Video %s imported.', video.uuid) videoImportUpdated.Video = videoUpdated return videoImportUpdated @@ -110,20 +210,15 @@ async function processVideoImport (job: Bull.Job) { } catch (err) { try { - if (tempVideoPath) await unlinkPromise(tempVideoPath) + if (tempVideoPath) await remove(tempVideoPath) } catch (errUnlink) { - logger.error('Cannot cleanup files after a video import error.', { err: errUnlink }) + logger.warn('Cannot cleanup files after a video import error.', { err: errUnlink }) } + videoImport.error = err.message videoImport.state = VideoImportState.FAILED await videoImport.save() throw err } } - -// --------------------------------------------------------------------------- - -export { - processVideoImport -}