]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/job-queue/handlers/video-import.ts
Fix my account settings responsive
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / video-import.ts
CommitLineData
fbad87b0
C
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'
58d515e3 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
6040f87d 10import { downloadImage } from '../../../helpers/requests'
fbad87b0
C
11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub'
516df59b 14import { VideoModel } from '../../../models/video/video'
ce33919c 15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
990b6a0b 16import { getSecureTorrentName } from '../../../helpers/utils'
f481c4f9 17import { remove, move, stat } from 'fs-extra'
cef534ed 18import { Notifier } from '../../notifier'
fbad87b0 19
ce33919c 20type VideoImportYoutubeDLPayload = {
fbad87b0
C
21 type: 'youtube-dl'
22 videoImportId: number
ce33919c 23
fbad87b0
C
24 thumbnailUrl: string
25 downloadThumbnail: boolean
26 downloadPreview: boolean
27}
28
ce33919c 29type VideoImportTorrentPayload = {
990b6a0b 30 type: 'magnet-uri' | 'torrent-file'
ce33919c
C
31 videoImportId: number
32}
33
34export type VideoImportPayload = VideoImportYoutubeDLPayload | VideoImportTorrentPayload
35
fbad87b0
C
36async function processVideoImport (job: Bull.Job) {
37 const payload = job.data as VideoImportPayload
fbad87b0 38
ce33919c 39 if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload)
990b6a0b 40 if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload)
ce33919c
C
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 processVideoImport
47}
48
49// ---------------------------------------------------------------------------
50
51async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentPayload) {
52 logger.info('Processing torrent video import in job %d.', job.id)
53
54 const videoImport = await getVideoImportOrDie(payload.videoImportId)
990b6a0b 55
ce33919c
C
56 const options = {
57 videoImportId: payload.videoImportId,
58
59 downloadThumbnail: false,
60 downloadPreview: false,
61
62 generateThumbnail: true,
63 generatePreview: true
64 }
990b6a0b
C
65 const target = {
66 torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
67 magnetUri: videoImport.magnetUri
68 }
cf9166cf 69 return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options)
ce33919c
C
70}
71
72async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutubeDLPayload) {
73 logger.info('Processing youtubeDL video import in job %d.', job.id)
74
75 const videoImport = await getVideoImportOrDie(payload.videoImportId)
76 const options = {
77 videoImportId: videoImport.id,
78
79 downloadThumbnail: payload.downloadThumbnail,
80 downloadPreview: payload.downloadPreview,
81 thumbnailUrl: payload.thumbnailUrl,
82
83 generateThumbnail: false,
84 generatePreview: false
85 }
86
cf9166cf 87 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options)
ce33919c
C
88}
89
90async function getVideoImportOrDie (videoImportId: number) {
91 const videoImport = await VideoImportModel.loadAndPopulateVideo(videoImportId)
516df59b
C
92 if (!videoImport || !videoImport.Video) {
93 throw new Error('Cannot import video %s: the video import or video linked to this import does not exist anymore.')
94 }
fbad87b0 95
ce33919c
C
96 return videoImport
97}
98
99type ProcessFileOptions = {
100 videoImportId: number
101
102 downloadThumbnail: boolean
103 downloadPreview: boolean
104 thumbnailUrl?: string
105
106 generateThumbnail: boolean
107 generatePreview: boolean
108}
109async function processFile (downloader: () => Promise<string>, videoImport: VideoImportModel, options: ProcessFileOptions) {
fbad87b0 110 let tempVideoPath: string
516df59b
C
111 let videoDestFile: string
112 let videoFile: VideoFileModel
6040f87d 113
fbad87b0
C
114 try {
115 // Download video from youtubeDL
ce33919c 116 tempVideoPath = await downloader()
fbad87b0
C
117
118 // Get information about this video
62689b94 119 const stats = await stat(tempVideoPath)
3e17515e 120 const isAble = await videoImport.User.isAbleToUploadVideo({ size: stats.size })
a84b8fa5
C
121 if (isAble === false) {
122 throw new Error('The user video quota is exceeded with this video to import.')
123 }
124
fbad87b0 125 const { videoFileResolution } = await getVideoFileResolution(tempVideoPath)
d7f83948 126 const fps = await getVideoFileFPS(tempVideoPath)
fbad87b0
C
127 const duration = await getDurationFromVideoFile(tempVideoPath)
128
129 // Create video file object in database
130 const videoFileData = {
131 extname: extname(tempVideoPath),
132 resolution: videoFileResolution,
3e17515e 133 size: stats.size,
fbad87b0
C
134 fps,
135 videoId: videoImport.videoId
136 }
516df59b 137 videoFile = new VideoFileModel(videoFileData)
58d515e3 138 // To clean files if the import fails
516df59b 139 videoImport.Video.VideoFiles = [ videoFile ]
fbad87b0
C
140
141 // Move file
516df59b 142 videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile))
f481c4f9 143 await move(tempVideoPath, videoDestFile)
516df59b 144 tempVideoPath = null // This path is not used anymore
fbad87b0
C
145
146 // Process thumbnail
ce33919c
C
147 if (options.downloadThumbnail) {
148 if (options.thumbnailUrl) {
6040f87d 149 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
fbad87b0
C
150 } else {
151 await videoImport.Video.createThumbnail(videoFile)
152 }
ce33919c
C
153 } else if (options.generateThumbnail) {
154 await videoImport.Video.createThumbnail(videoFile)
fbad87b0
C
155 }
156
157 // Process preview
ce33919c
C
158 if (options.downloadPreview) {
159 if (options.thumbnailUrl) {
6040f87d 160 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
fbad87b0
C
161 } else {
162 await videoImport.Video.createPreview(videoFile)
163 }
ce33919c
C
164 } else if (options.generatePreview) {
165 await videoImport.Video.createPreview(videoFile)
fbad87b0
C
166 }
167
168 // Create torrent
169 await videoImport.Video.createTorrentAndSetInfoHash(videoFile)
170
171 const videoImportUpdated: VideoImportModel = await sequelizeTypescript.transaction(async t => {
516df59b
C
172 // Refresh video
173 const video = await VideoModel.load(videoImport.videoId, t)
174 if (!video) throw new Error('Video linked to import ' + videoImport.videoId + ' does not exist anymore.')
175 videoImport.Video = video
176
177 const videoFileCreated = await videoFile.save({ transaction: t })
178 video.VideoFiles = [ videoFileCreated ]
fbad87b0
C
179
180 // Update video DB object
516df59b
C
181 video.duration = duration
182 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
e8d246d5 183 await video.save({ transaction: t })
fbad87b0 184
590fb506 185 // Now we can federate the video (reload from database, we need more attributes)
627621c1 186 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
590fb506 187 await federateVideoIfNeeded(videoForFederation, true, t)
fbad87b0
C
188
189 // Update video import object
190 videoImport.state = VideoImportState.SUCCESS
191 const videoImportUpdated = await videoImport.save({ transaction: t })
192
541006e3 193 logger.info('Video %s imported.', video.uuid)
fbad87b0 194
e8d246d5 195 videoImportUpdated.Video = videoForFederation
fbad87b0
C
196 return videoImportUpdated
197 })
198
dc133480 199 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
e8d246d5 200
7ccddd7b
JM
201 if (videoImportUpdated.Video.VideoBlacklist) {
202 Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
203 } else {
204 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
205 }
206
fbad87b0
C
207 // Create transcoding jobs?
208 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
209 // Put uuid because we don't have id auto incremented for now
210 const dataInput = {
211 videoUUID: videoImportUpdated.Video.uuid,
212 isNewVideo: true
213 }
214
a0327eed 215 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
fbad87b0
C
216 }
217
218 } catch (err) {
219 try {
e95e0463 220 if (tempVideoPath) await remove(tempVideoPath)
fbad87b0 221 } catch (errUnlink) {
516df59b 222 logger.warn('Cannot cleanup files after a video import error.', { err: errUnlink })
fbad87b0
C
223 }
224
d7f83948 225 videoImport.error = err.message
fbad87b0
C
226 videoImport.state = VideoImportState.FAILED
227 await videoImport.save()
228
dc133480
C
229 Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false)
230
fbad87b0
C
231 throw err
232 }
233}