]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/job-queue/handlers/video-import.ts
Fix import timeout inconsistency
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-import.ts
CommitLineData
41fb13c3 1import { Job } from 'bull'
2158ac90 2import { move, remove, stat } from 'fs-extra'
44d1f7f2 3import { retryTransactionWrapper } from '@server/helpers/database-utils'
62549e6c 4import { YoutubeDLWrapper } from '@server/helpers/youtube-dl'
2158ac90 5import { isPostImportVideoAccepted } from '@server/lib/moderation'
0305db28 6import { generateWebTorrentVideoFilename } from '@server/lib/paths'
2158ac90 7import { Hooks } from '@server/lib/plugins/hooks'
2539932e 8import { ServerConfigManager } from '@server/lib/server-config-manager'
fb719404 9import { isAbleToUploadVideo } from '@server/lib/user'
0305db28
JB
10import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video'
11import { VideoPathManager } from '@server/lib/video-path-manager'
12import { buildNextVideoState } from '@server/lib/video-state'
44d1f7f2 13import { ThumbnailModel } from '@server/models/video/thumbnail'
26d6bf65 14import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
d17c7b4e
C
15import { getLowercaseExtension } from '@shared/core-utils'
16import { isAudioFile } from '@shared/extra-utils'
2158ac90 17import {
d17c7b4e 18 ThumbnailType,
2158ac90 19 VideoImportPayload,
d17c7b4e 20 VideoImportState,
2158ac90
RK
21 VideoImportTorrentPayload,
22 VideoImportTorrentPayloadType,
23 VideoImportYoutubeDLPayload,
24 VideoImportYoutubeDLPayloadType,
d17c7b4e 25 VideoResolution,
2158ac90 26 VideoState
d17c7b4e 27} from '@shared/models'
482b2623 28import { ffprobePromise, getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
2158ac90 29import { logger } from '../../../helpers/logger'
990b6a0b 30import { getSecureTorrentName } from '../../../helpers/utils'
2158ac90 31import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
7630e1c8 32import { JOB_TTL } from '../../../initializers/constants'
74dc3bca 33import { sequelizeTypescript } from '../../../initializers/database'
2158ac90
RK
34import { VideoModel } from '../../../models/video/video'
35import { VideoFileModel } from '../../../models/video/video-file'
36import { VideoImportModel } from '../../../models/video/video-import'
26d6bf65 37import { MThumbnail } from '../../../types/models/video/thumbnail'
2158ac90
RK
38import { federateVideoIfNeeded } from '../../activitypub/videos'
39import { Notifier } from '../../notifier'
40import { generateVideoMiniature } from '../../thumbnail'
ce33919c 41
41fb13c3 42async function processVideoImport (job: Job) {
fbad87b0 43 const payload = job.data as VideoImportPayload
fbad87b0 44
d511df28 45 const videoImport = await getVideoImportOrDie(payload)
419b520c
C
46 if (videoImport.state === VideoImportState.CANCELLED) {
47 logger.info('Do not process import since it has been cancelled', { payload })
48 return
49 }
50
51 videoImport.state = VideoImportState.PROCESSING
52 await videoImport.save()
53
54 if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, videoImport, payload)
55 if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, videoImport, payload)
ce33919c
C
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 processVideoImport
62}
63
64// ---------------------------------------------------------------------------
65
419b520c 66async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
ce33919c
C
67 logger.info('Processing torrent video import in job %d.', job.id)
68
419b520c 69 const options = { type: payload.type, videoImportId: payload.videoImportId }
990b6a0b 70
990b6a0b
C
71 const target = {
72 torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
02b286f8 73 uri: videoImport.magnetUri
990b6a0b 74 }
7630e1c8 75 return processFile(() => downloadWebTorrentVideo(target, JOB_TTL['video-import']), videoImport, options)
ce33919c
C
76}
77
419b520c 78async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
ce33919c
C
79 logger.info('Processing youtubeDL video import in job %d.', job.id)
80
419b520c 81 const options = { type: payload.type, videoImportId: videoImport.id }
ce33919c 82
62549e6c 83 const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
1bcb03a1 84
454c20fa 85 return processFile(
7630e1c8 86 () => youtubeDL.downloadVideo(payload.fileExt, JOB_TTL['video-import']),
454c20fa
RK
87 videoImport,
88 options
89 )
ce33919c
C
90}
91
d511df28
C
92async function getVideoImportOrDie (payload: VideoImportPayload) {
93 const videoImport = await VideoImportModel.loadAndPopulateVideo(payload.videoImportId)
516df59b 94 if (!videoImport || !videoImport.Video) {
d511df28 95 throw new Error(`Cannot import video ${payload.videoImportId}: the video import or video linked to this import does not exist anymore.`)
516df59b 96 }
fbad87b0 97
ce33919c
C
98 return videoImport
99}
100
101type ProcessFileOptions = {
2158ac90 102 type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType
ce33919c 103 videoImportId: number
ce33919c 104}
0283eaac 105async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
fbad87b0 106 let tempVideoPath: string
516df59b 107 let videoFile: VideoFileModel
6040f87d 108
fbad87b0
C
109 try {
110 // Download video from youtubeDL
ce33919c 111 tempVideoPath = await downloader()
fbad87b0
C
112
113 // Get information about this video
62689b94 114 const stats = await stat(tempVideoPath)
fb719404 115 const isAble = await isAbleToUploadVideo(videoImport.User.id, stats.size)
a84b8fa5
C
116 if (isAble === false) {
117 throw new Error('The user video quota is exceeded with this video to import.')
118 }
119
482b2623
C
120 const probe = await ffprobePromise(tempVideoPath)
121
122 const { resolution } = await isAudioFile(tempVideoPath, probe)
5354af75
C
123 ? { resolution: VideoResolution.H_NOVIDEO }
124 : await getVideoFileResolution(tempVideoPath)
482b2623
C
125
126 const fps = await getVideoFileFPS(tempVideoPath, probe)
127 const duration = await getDurationFromVideoFile(tempVideoPath, probe)
fbad87b0 128
2158ac90 129 // Prepare video file object for creation in database
ea54cd04 130 const fileExt = getLowercaseExtension(tempVideoPath)
fbad87b0 131 const videoFileData = {
90a8bd30 132 extname: fileExt,
679c12e6 133 resolution,
3e17515e 134 size: stats.size,
679c12e6 135 filename: generateWebTorrentVideoFilename(resolution, fileExt),
fbad87b0
C
136 fps,
137 videoId: videoImport.videoId
138 }
516df59b 139 videoFile = new VideoFileModel(videoFileData)
0283eaac 140
2158ac90
RK
141 const hookName = options.type === 'youtube-dl'
142 ? 'filter:api.video.post-import-url.accept.result'
143 : 'filter:api.video.post-import-torrent.accept.result'
144
145 // Check we accept this video
146 const acceptParameters = {
147 videoImport,
148 video: videoImport.Video,
149 videoFilePath: tempVideoPath,
150 videoFile,
151 user: videoImport.User
152 }
153 const acceptedResult = await Hooks.wrapFun(isPostImportVideoAccepted, acceptParameters, hookName)
154
155 if (acceptedResult.accepted !== true) {
156 logger.info('Refused imported video.', { acceptedResult, acceptParameters })
157
158 videoImport.state = VideoImportState.REJECTED
159 await videoImport.save()
160
161 throw new Error(acceptedResult.errorMessage)
162 }
163
164 // Video is accepted, resuming preparation
d7a25329 165 const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] })
58d515e3 166 // To clean files if the import fails
0283eaac 167 const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
fbad87b0
C
168
169 // Move file
0305db28 170 const videoDestFile = VideoPathManager.Instance.getFSVideoFileOutputPath(videoImportWithFiles.Video, videoFile)
f481c4f9 171 await move(tempVideoPath, videoDestFile)
516df59b 172 tempVideoPath = null // This path is not used anymore
fbad87b0 173
e3b4c084 174 // Generate miniature if the import did not created it
453e83ea 175 let thumbnailModel: MThumbnail
44d1f7f2 176 let thumbnailSave: object
e3b4c084 177 if (!videoImportWithFiles.Video.getMiniature()) {
a35a2279
C
178 thumbnailModel = await generateVideoMiniature({
179 video: videoImportWithFiles.Video,
180 videoFile,
181 type: ThumbnailType.MINIATURE
182 })
44d1f7f2 183 thumbnailSave = thumbnailModel.toJSON()
fbad87b0
C
184 }
185
e3b4c084 186 // Generate preview if the import did not created it
453e83ea 187 let previewModel: MThumbnail
44d1f7f2 188 let previewSave: object
e3b4c084 189 if (!videoImportWithFiles.Video.getPreview()) {
a35a2279
C
190 previewModel = await generateVideoMiniature({
191 video: videoImportWithFiles.Video,
192 videoFile,
193 type: ThumbnailType.PREVIEW
194 })
44d1f7f2 195 previewSave = previewModel.toJSON()
fbad87b0
C
196 }
197
198 // Create torrent
8efc27bf 199 await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile)
fbad87b0 200
44d1f7f2 201 const videoFileSave = videoFile.toJSON()
453e83ea 202
44d1f7f2
C
203 const { videoImportUpdated, video } = await retryTransactionWrapper(() => {
204 return sequelizeTypescript.transaction(async t => {
205 const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
516df59b 206
44d1f7f2
C
207 // Refresh video
208 const video = await VideoModel.load(videoImportToUpdate.videoId, t)
209 if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.')
fbad87b0 210
44d1f7f2 211 const videoFileCreated = await videoFile.save({ transaction: t })
fbad87b0 212
44d1f7f2
C
213 // Update video DB object
214 video.duration = duration
0305db28 215 video.state = buildNextVideoState(video.state)
44d1f7f2 216 await video.save({ transaction: t })
e8bafea3 217
44d1f7f2
C
218 if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
219 if (previewModel) await video.addAndSaveThumbnail(previewModel, t)
fbad87b0 220
44d1f7f2
C
221 // Now we can federate the video (reload from database, we need more attributes)
222 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
223 await federateVideoIfNeeded(videoForFederation, true, t)
fbad87b0 224
44d1f7f2
C
225 // Update video import object
226 videoImportToUpdate.state = VideoImportState.SUCCESS
227 const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo
228 videoImportUpdated.Video = video
fbad87b0 229
44d1f7f2
C
230 videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] })
231
232 logger.info('Video %s imported.', video.uuid)
233
234 return { videoImportUpdated, video: videoForFederation }
235 }).catch(err => {
236 // Reset fields
237 if (thumbnailModel) thumbnailModel = new ThumbnailModel(thumbnailSave)
238 if (previewModel) previewModel = new ThumbnailModel(previewSave)
239
240 videoFile = new VideoFileModel(videoFileSave)
241
242 throw err
243 })
fbad87b0
C
244 })
245
d26836cd 246 Notifier.Instance.notifyOnFinishedVideoImport({ videoImport: videoImportUpdated, success: true })
e8d246d5 247
453e83ea 248 if (video.isBlacklisted()) {
8424c402
C
249 const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
250
251 Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
7ccddd7b 252 } else {
453e83ea 253 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
7ccddd7b
JM
254 }
255
0305db28
JB
256 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
257 return addMoveToObjectStorageJob(videoImportUpdated.Video)
258 }
259
fbad87b0 260 // Create transcoding jobs?
453e83ea 261 if (video.state === VideoState.TO_TRANSCODE) {
77d7e851 262 await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile, videoImport.User)
fbad87b0
C
263 }
264
265 } catch (err) {
266 try {
e95e0463 267 if (tempVideoPath) await remove(tempVideoPath)
fbad87b0 268 } catch (errUnlink) {
516df59b 269 logger.warn('Cannot cleanup files after a video import error.', { err: errUnlink })
fbad87b0
C
270 }
271
d7f83948 272 videoImport.error = err.message
2158ac90
RK
273 if (videoImport.state !== VideoImportState.REJECTED) {
274 videoImport.state = VideoImportState.FAILED
275 }
fbad87b0
C
276 await videoImport.save()
277
d26836cd 278 Notifier.Instance.notifyOnFinishedVideoImport({ videoImport, success: false })
dc133480 279
fbad87b0
C
280 throw err
281 }
282}