aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/process/process-update.ts2
-rw-r--r--server/lib/activitypub/videos.ts3
-rw-r--r--server/lib/job-queue/handlers/video-import.ts145
-rw-r--r--server/lib/job-queue/job-queue.ts25
-rw-r--r--server/lib/schedulers/youtube-dl-update-scheduler.ts72
-rw-r--r--server/lib/user.ts1
6 files changed, 236 insertions, 12 deletions
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 62791ff1b..82b661a03 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -108,7 +108,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
108 await Promise.all(videoFileDestroyTasks) 108 await Promise.all(videoFileDestroyTasks)
109 109
110 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject) 110 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
111 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) 111 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, sequelizeOptions))
112 await Promise.all(tasks) 112 await Promise.all(tasks)
113 113
114 // Update Tags 114 // Update Tags
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index b3fbf88d0..e2f46bd02 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -162,7 +162,8 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObje
162 infoHash: parsed.infoHash, 162 infoHash: parsed.infoHash,
163 resolution: fileUrl.width, 163 resolution: fileUrl.width,
164 size: fileUrl.size, 164 size: fileUrl.size,
165 videoId: videoCreated.id 165 videoId: videoCreated.id,
166 fps: fileUrl.fps
166 } as VideoFileModel 167 } as VideoFileModel
167 attributes.push(attribute) 168 attributes.push(attribute)
168 } 169 }
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)
diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts
new file mode 100644
index 000000000..a2d919603
--- /dev/null
+++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts
@@ -0,0 +1,72 @@
1// Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
2// We rewrote it to avoid sync calls
3
4import { AbstractScheduler } from './abstract-scheduler'
5import { SCHEDULER_INTERVALS_MS } from '../../initializers'
6import { logger } from '../../helpers/logger'
7import * as request from 'request'
8import { createWriteStream, writeFile } from 'fs'
9import { join } from 'path'
10import { root } from '../../helpers/core-utils'
11
12export class YoutubeDlUpdateScheduler extends AbstractScheduler {
13
14 private static instance: AbstractScheduler
15
16 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.youtubeDLUpdate
17
18 private constructor () {
19 super()
20 }
21
22 async execute () {
23 const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
24 const bin = join(binDirectory, 'youtube-dl')
25 const detailsPath = join(binDirectory, 'details')
26 const url = 'https://yt-dl.org/downloads/latest/youtube-dl'
27
28 request.get(url, { followRedirect: false }, (err, res) => {
29 if (err) {
30 logger.error('Cannot update youtube-dl.', { err })
31 return
32 }
33
34 if (res.statusCode !== 302) {
35 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', res.statusCode)
36 return
37 }
38
39 const url = res.headers.location
40 const downloadFile = request.get(url)
41 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
42
43 downloadFile.on('response', res => {
44 if (res.statusCode !== 200) {
45 logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', res.statusCode)
46 return
47 }
48
49 downloadFile.pipe(createWriteStream(bin, { mode: 493 }))
50 })
51
52 downloadFile.on('error', err => logger.error('youtube-dl update error.', { err }))
53
54 downloadFile.on('end', () => {
55 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
56 writeFile(detailsPath, details, { encoding: 'utf8' }, err => {
57 if (err) {
58 logger.error('youtube-dl update error: cannot write details.', { err })
59 return
60 }
61
62 logger.info('youtube-dl updated to version %s.', newVersion)
63 })
64 })
65
66 })
67 }
68
69 static get Instance () {
70 return this.instance || (this.instance = new this())
71 }
72}
diff --git a/server/lib/user.ts b/server/lib/user.ts
index ac5f55260..e7a45f5aa 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -17,6 +17,7 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
17 17
18 const userCreated = await userToCreate.save(userOptions) 18 const userCreated = await userToCreate.save(userOptions)
19 const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t) 19 const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t)
20 userCreated.Account = accountCreated
20 21
21 const videoChannelDisplayName = `Default ${userCreated.username} channel` 22 const videoChannelDisplayName = `Default ${userCreated.username} channel`
22 const videoChannelInfo = { 23 const videoChannelInfo = {