aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/job-queue
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/job-queue')
-rw-r--r--server/lib/job-queue/handlers/video-import.ts145
-rw-r--r--server/lib/job-queue/job-queue.ts25
2 files changed, 160 insertions, 10 deletions
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
new file mode 100644
index 000000000..cdfe412cc
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -0,0 +1,145 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl'
4import { VideoImportModel } from '../../../models/video/video-import'
5import { VideoImportState } from '../../../../shared/models/videos'
6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file'
9import { renamePromise, statPromise, unlinkPromise } from '../../../helpers/core-utils'
10import { CONFIG, sequelizeTypescript } from '../../../initializers'
11import { doRequestAndSaveToFile } from '../../../helpers/requests'
12import { VideoState } from '../../../../shared'
13import { JobQueue } from '../index'
14import { federateVideoIfNeeded } from '../../activitypub'
15import { VideoModel } from '../../../models/video/video'
16
17export type VideoImportPayload = {
18 type: 'youtube-dl'
19 videoImportId: number
20 thumbnailUrl: string
21 downloadThumbnail: boolean
22 downloadPreview: boolean
23}
24
25async function processVideoImport (job: Bull.Job) {
26 const payload = job.data as VideoImportPayload
27 logger.info('Processing video import in job %d.', job.id)
28
29 const videoImport = await VideoImportModel.loadAndPopulateVideo(payload.videoImportId)
30 if (!videoImport || !videoImport.Video) {
31 throw new Error('Cannot import video %s: the video import or video linked to this import does not exist anymore.')
32 }
33
34 let tempVideoPath: string
35 let videoDestFile: string
36 let videoFile: VideoFileModel
37 try {
38 // Download video from youtubeDL
39 tempVideoPath = await downloadYoutubeDLVideo(videoImport.targetUrl)
40
41 // Get information about this video
42 const { videoFileResolution } = await getVideoFileResolution(tempVideoPath)
43 const fps = await getVideoFileFPS(tempVideoPath)
44 const stats = await statPromise(tempVideoPath)
45 const duration = await getDurationFromVideoFile(tempVideoPath)
46
47 // Create video file object in database
48 const videoFileData = {
49 extname: extname(tempVideoPath),
50 resolution: videoFileResolution,
51 size: stats.size,
52 fps,
53 videoId: videoImport.videoId
54 }
55 videoFile = new VideoFileModel(videoFileData)
56 // Import if the import fails, to clean files
57 videoImport.Video.VideoFiles = [ videoFile ]
58
59 // Move file
60 videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile))
61 await renamePromise(tempVideoPath, videoDestFile)
62 tempVideoPath = null // This path is not used anymore
63
64 // Process thumbnail
65 if (payload.downloadThumbnail) {
66 if (payload.thumbnailUrl) {
67 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
68 await doRequestAndSaveToFile({ method: 'GET', uri: payload.thumbnailUrl }, destThumbnailPath)
69 } else {
70 await videoImport.Video.createThumbnail(videoFile)
71 }
72 }
73
74 // Process preview
75 if (payload.downloadPreview) {
76 if (payload.thumbnailUrl) {
77 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
78 await doRequestAndSaveToFile({ method: 'GET', uri: payload.thumbnailUrl }, destPreviewPath)
79 } else {
80 await videoImport.Video.createPreview(videoFile)
81 }
82 }
83
84 // Create torrent
85 await videoImport.Video.createTorrentAndSetInfoHash(videoFile)
86
87 const videoImportUpdated: VideoImportModel = await sequelizeTypescript.transaction(async t => {
88 // Refresh video
89 const video = await VideoModel.load(videoImport.videoId, t)
90 if (!video) throw new Error('Video linked to import ' + videoImport.videoId + ' does not exist anymore.')
91 videoImport.Video = video
92
93 const videoFileCreated = await videoFile.save({ transaction: t })
94 video.VideoFiles = [ videoFileCreated ]
95
96 // Update video DB object
97 video.duration = duration
98 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
99 const videoUpdated = await video.save({ transaction: t })
100
101 // Now we can federate the video (reload from database, we need more attributes)
102 const videoForFederation = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
103 await federateVideoIfNeeded(videoForFederation, true, t)
104
105 // Update video import object
106 videoImport.state = VideoImportState.SUCCESS
107 const videoImportUpdated = await videoImport.save({ transaction: t })
108
109 logger.info('Video %s imported.', videoImport.targetUrl)
110
111 videoImportUpdated.Video = videoUpdated
112 return videoImportUpdated
113 })
114
115 // Create transcoding jobs?
116 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
117 // Put uuid because we don't have id auto incremented for now
118 const dataInput = {
119 videoUUID: videoImportUpdated.Video.uuid,
120 isNewVideo: true
121 }
122
123 await JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
124 }
125
126 } catch (err) {
127 try {
128 if (tempVideoPath) await unlinkPromise(tempVideoPath)
129 } catch (errUnlink) {
130 logger.warn('Cannot cleanup files after a video import error.', { err: errUnlink })
131 }
132
133 videoImport.error = err.message
134 videoImport.state = VideoImportState.FAILED
135 await videoImport.save()
136
137 throw err
138 }
139}
140
141// ---------------------------------------------------------------------------
142
143export {
144 processVideoImport
145}
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 8ff0c169e..8a24604e1 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -2,13 +2,14 @@ import * as Bull from 'bull'
2import { JobState, JobType } from '../../../shared/models' 2import { JobState, JobType } from '../../../shared/models'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { Redis } from '../redis' 4import { Redis } from '../redis'
5import { CONFIG, JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_REQUEST_TTL } from '../../initializers' 5import { CONFIG, JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL } from '../../initializers'
6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' 6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast'
7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' 7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' 8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
9import { EmailPayload, processEmail } from './handlers/email' 9import { EmailPayload, processEmail } from './handlers/email'
10import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' 10import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file'
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import'
12 13
13type CreateJobArgument = 14type CreateJobArgument =
14 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 15 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -17,7 +18,8 @@ type CreateJobArgument =
17 { type: 'activitypub-follow', payload: ActivitypubFollowPayload } | 18 { type: 'activitypub-follow', payload: ActivitypubFollowPayload } |
18 { type: 'video-file-import', payload: VideoFileImportPayload } | 19 { type: 'video-file-import', payload: VideoFileImportPayload } |
19 { type: 'video-file', payload: VideoFilePayload } | 20 { type: 'video-file', payload: VideoFilePayload } |
20 { type: 'email', payload: EmailPayload } 21 { type: 'email', payload: EmailPayload } |
22 { type: 'video-import', payload: VideoImportPayload }
21 23
22const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { 24const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
23 'activitypub-http-broadcast': processActivityPubHttpBroadcast, 25 'activitypub-http-broadcast': processActivityPubHttpBroadcast,
@@ -26,7 +28,8 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
26 'activitypub-follow': processActivityPubFollow, 28 'activitypub-follow': processActivityPubFollow,
27 'video-file-import': processVideoFileImport, 29 'video-file-import': processVideoFileImport,
28 'video-file': processVideoFile, 30 'video-file': processVideoFile,
29 'email': processEmail 31 'email': processEmail,
32 'video-import': processVideoImport
30} 33}
31 34
32const jobsWithRequestTimeout: { [ id in JobType ]?: boolean } = { 35const jobsWithRequestTimeout: { [ id in JobType ]?: boolean } = {
@@ -43,7 +46,8 @@ const jobTypes: JobType[] = [
43 'activitypub-http-unicast', 46 'activitypub-http-unicast',
44 'email', 47 'email',
45 'video-file', 48 'video-file',
46 'video-file-import' 49 'video-file-import',
50 'video-import'
47] 51]
48 52
49class JobQueue { 53class JobQueue {
@@ -75,7 +79,11 @@ class JobQueue {
75 const handler = handlers[handlerName] 79 const handler = handlers[handlerName]
76 80
77 queue.process(JOB_CONCURRENCY[handlerName], handler) 81 queue.process(JOB_CONCURRENCY[handlerName], handler)
78 .catch(err => logger.error('Cannot execute job queue %s.', handlerName, { err })) 82 .catch(err => logger.error('Error in job queue processor %s.', handlerName, { err }))
83
84 queue.on('failed', (job, err) => {
85 logger.error('Cannot execute job %d in queue %s.', job.id, handlerName, { payload: job.data, err })
86 })
79 87
80 queue.on('error', err => { 88 queue.on('error', err => {
81 logger.error('Error in job queue %s.', handlerName, { err }) 89 logger.error('Error in job queue %s.', handlerName, { err })
@@ -102,11 +110,8 @@ class JobQueue {
102 110
103 const jobArgs: Bull.JobOptions = { 111 const jobArgs: Bull.JobOptions = {
104 backoff: { delay: 60 * 1000, type: 'exponential' }, 112 backoff: { delay: 60 * 1000, type: 'exponential' },
105 attempts: JOB_ATTEMPTS[obj.type] 113 attempts: JOB_ATTEMPTS[obj.type],
106 } 114 timeout: JOB_TTL[obj.type]
107
108 if (jobsWithRequestTimeout[obj.type] === true) {
109 jobArgs.timeout = JOB_REQUEST_TTL
110 } 115 }
111 116
112 return queue.add(obj.payload, jobArgs) 117 return queue.add(obj.payload, jobArgs)