]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.ts
First typescript iteration
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
1 import express = require('express')
2 import { eachSeries, waterfall } from 'async'
3
4 const db = require('../../../initializers/database')
5 import {
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_ENDPOINTS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10 } from '../../../initializers'
11 import {
12 checkSignature,
13 signatureValidator,
14 remoteVideosValidator,
15 remoteQaduVideosValidator,
16 remoteEventsVideosValidator
17 } from '../../../middlewares'
18 import {
19 logger,
20 commitTransaction,
21 retryTransactionWrapper,
22 rollbackTransaction,
23 startSerializableTransaction
24 } from '../../../helpers'
25 import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
26
27 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
28
29 // Functions to call when processing a remote request
30 const functionsHash = {}
31 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
32 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
33 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
34 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
35
36 const remoteVideosRouter = express.Router()
37
38 remoteVideosRouter.post('/',
39 signatureValidator,
40 checkSignature,
41 remoteVideosValidator,
42 remoteVideos
43 )
44
45 remoteVideosRouter.post('/qadu',
46 signatureValidator,
47 checkSignature,
48 remoteQaduVideosValidator,
49 remoteVideosQadu
50 )
51
52 remoteVideosRouter.post('/events',
53 signatureValidator,
54 checkSignature,
55 remoteEventsVideosValidator,
56 remoteVideosEvents
57 )
58
59 // ---------------------------------------------------------------------------
60
61 export {
62 remoteVideosRouter
63 }
64
65 // ---------------------------------------------------------------------------
66
67 function remoteVideos (req, res, next) {
68 const requests = req.body.data
69 const fromPod = res.locals.secure.pod
70
71 // We need to process in the same order to keep consistency
72 // TODO: optimization
73 eachSeries(requests, function (request: any, callbackEach) {
74 const data = request.data
75
76 // Get the function we need to call in order to process the request
77 const fun = functionsHash[request.type]
78 if (fun === undefined) {
79 logger.error('Unkown remote request type %s.', request.type)
80 return callbackEach(null)
81 }
82
83 fun.call(this, data, fromPod, callbackEach)
84 }, function (err) {
85 if (err) logger.error('Error managing remote videos.', { error: err })
86 })
87
88 // We don't need to keep the other pod waiting
89 return res.type('json').status(204).end()
90 }
91
92 function remoteVideosQadu (req, res, next) {
93 const requests = req.body.data
94 const fromPod = res.locals.secure.pod
95
96 eachSeries(requests, function (request: any, callbackEach) {
97 const videoData = request.data
98
99 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
100 }, function (err) {
101 if (err) logger.error('Error managing remote videos.', { error: err })
102 })
103
104 return res.type('json').status(204).end()
105 }
106
107 function remoteVideosEvents (req, res, next) {
108 const requests = req.body.data
109 const fromPod = res.locals.secure.pod
110
111 eachSeries(requests, function (request: any, callbackEach) {
112 const eventData = request.data
113
114 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
115 }, function (err) {
116 if (err) logger.error('Error managing remote videos.', { error: err })
117 })
118
119 return res.type('json').status(204).end()
120 }
121
122 function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) {
123 const options = {
124 arguments: [ eventData, fromPod ],
125 errorMessage: 'Cannot process videos events with many retries.'
126 }
127
128 retryTransactionWrapper(processVideosEvents, options, finalCallback)
129 }
130
131 function processVideosEvents (eventData, fromPod, finalCallback) {
132 waterfall([
133 startSerializableTransaction,
134
135 function findVideo (t, callback) {
136 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
137 return callback(err, t, videoInstance)
138 })
139 },
140
141 function updateVideoIntoDB (t, videoInstance, callback) {
142 const options = { transaction: t }
143
144 let columnToUpdate
145 let qaduType
146
147 switch (eventData.eventType) {
148 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
149 columnToUpdate = 'views'
150 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
151 break
152
153 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
154 columnToUpdate = 'likes'
155 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
156 break
157
158 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
159 columnToUpdate = 'dislikes'
160 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
161 break
162
163 default:
164 return callback(new Error('Unknown video event type.'))
165 }
166
167 const query = {}
168 query[columnToUpdate] = eventData.count
169
170 videoInstance.increment(query, options).asCallback(function (err) {
171 return callback(err, t, videoInstance, qaduType)
172 })
173 },
174
175 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
176 const qadusParams = [
177 {
178 videoId: videoInstance.id,
179 type: qaduType
180 }
181 ]
182
183 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
184 return callback(err, t)
185 })
186 },
187
188 commitTransaction
189
190 ], function (err, t) {
191 if (err) {
192 logger.debug('Cannot process a video event.', { error: err })
193 return rollbackTransaction(err, t, finalCallback)
194 }
195
196 logger.info('Remote video event processed for video %s.', eventData.remoteId)
197 return finalCallback(null)
198 })
199 }
200
201 function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) {
202 const options = {
203 arguments: [ videoData, fromPod ],
204 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
205 }
206
207 retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
208 }
209
210 function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
211 let videoName
212
213 waterfall([
214 startSerializableTransaction,
215
216 function findVideo (t, callback) {
217 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
218 return callback(err, t, videoInstance)
219 })
220 },
221
222 function updateVideoIntoDB (t, videoInstance, callback) {
223 const options = { transaction: t }
224
225 videoName = videoInstance.name
226
227 if (videoData.views) {
228 videoInstance.set('views', videoData.views)
229 }
230
231 if (videoData.likes) {
232 videoInstance.set('likes', videoData.likes)
233 }
234
235 if (videoData.dislikes) {
236 videoInstance.set('dislikes', videoData.dislikes)
237 }
238
239 videoInstance.save(options).asCallback(function (err) {
240 return callback(err, t)
241 })
242 },
243
244 commitTransaction
245
246 ], function (err, t) {
247 if (err) {
248 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
249 return rollbackTransaction(err, t, finalCallback)
250 }
251
252 logger.info('Remote video %s quick and dirty updated', videoName)
253 return finalCallback(null)
254 })
255 }
256
257 // Handle retries on fail
258 function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
259 const options = {
260 arguments: [ videoToCreateData, fromPod ],
261 errorMessage: 'Cannot insert the remote video with many retries.'
262 }
263
264 retryTransactionWrapper(addRemoteVideo, options, finalCallback)
265 }
266
267 function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
268 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
269
270 waterfall([
271
272 startSerializableTransaction,
273
274 function assertRemoteIdAndHostUnique (t, callback) {
275 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
276 if (err) return callback(err)
277
278 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
279
280 return callback(null, t)
281 })
282 },
283
284 function findOrCreateAuthor (t, callback) {
285 const name = videoToCreateData.author
286 const podId = fromPod.id
287 // This author is from another pod so we do not associate a user
288 const userId = null
289
290 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
291 return callback(err, t, authorInstance)
292 })
293 },
294
295 function findOrCreateTags (t, author, callback) {
296 const tags = videoToCreateData.tags
297
298 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
299 return callback(err, t, author, tagInstances)
300 })
301 },
302
303 function createVideoObject (t, author, tagInstances, callback) {
304 const videoData = {
305 name: videoToCreateData.name,
306 remoteId: videoToCreateData.remoteId,
307 extname: videoToCreateData.extname,
308 infoHash: videoToCreateData.infoHash,
309 category: videoToCreateData.category,
310 licence: videoToCreateData.licence,
311 language: videoToCreateData.language,
312 nsfw: videoToCreateData.nsfw,
313 description: videoToCreateData.description,
314 authorId: author.id,
315 duration: videoToCreateData.duration,
316 createdAt: videoToCreateData.createdAt,
317 // FIXME: updatedAt does not seems to be considered by Sequelize
318 updatedAt: videoToCreateData.updatedAt,
319 views: videoToCreateData.views,
320 likes: videoToCreateData.likes,
321 dislikes: videoToCreateData.dislikes
322 }
323
324 const video = db.Video.build(videoData)
325
326 return callback(null, t, tagInstances, video)
327 },
328
329 function generateThumbnail (t, tagInstances, video, callback) {
330 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
331 if (err) {
332 logger.error('Cannot generate thumbnail from data.', { error: err })
333 return callback(err)
334 }
335
336 return callback(err, t, tagInstances, video)
337 })
338 },
339
340 function insertVideoIntoDB (t, tagInstances, video, callback) {
341 const options = {
342 transaction: t
343 }
344
345 video.save(options).asCallback(function (err, videoCreated) {
346 return callback(err, t, tagInstances, videoCreated)
347 })
348 },
349
350 function associateTagsToVideo (t, tagInstances, video, callback) {
351 const options = {
352 transaction: t
353 }
354
355 video.setTags(tagInstances, options).asCallback(function (err) {
356 return callback(err, t)
357 })
358 },
359
360 commitTransaction
361
362 ], function (err, t) {
363 if (err) {
364 // This is just a debug because we will retry the insert
365 logger.debug('Cannot insert the remote video.', { error: err })
366 return rollbackTransaction(err, t, finalCallback)
367 }
368
369 logger.info('Remote video %s inserted.', videoToCreateData.name)
370 return finalCallback(null)
371 })
372 }
373
374 // Handle retries on fail
375 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
376 const options = {
377 arguments: [ videoAttributesToUpdate, fromPod ],
378 errorMessage: 'Cannot update the remote video with many retries'
379 }
380
381 retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
382 }
383
384 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
385 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
386
387 waterfall([
388
389 startSerializableTransaction,
390
391 function findVideo (t, callback) {
392 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
393 return callback(err, t, videoInstance)
394 })
395 },
396
397 function findOrCreateTags (t, videoInstance, callback) {
398 const tags = videoAttributesToUpdate.tags
399
400 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
401 return callback(err, t, videoInstance, tagInstances)
402 })
403 },
404
405 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
406 const options = { transaction: t }
407
408 videoInstance.set('name', videoAttributesToUpdate.name)
409 videoInstance.set('category', videoAttributesToUpdate.category)
410 videoInstance.set('licence', videoAttributesToUpdate.licence)
411 videoInstance.set('language', videoAttributesToUpdate.language)
412 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
413 videoInstance.set('description', videoAttributesToUpdate.description)
414 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
415 videoInstance.set('duration', videoAttributesToUpdate.duration)
416 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
417 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
418 videoInstance.set('extname', videoAttributesToUpdate.extname)
419 videoInstance.set('views', videoAttributesToUpdate.views)
420 videoInstance.set('likes', videoAttributesToUpdate.likes)
421 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
422
423 videoInstance.save(options).asCallback(function (err) {
424 return callback(err, t, videoInstance, tagInstances)
425 })
426 },
427
428 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
429 const options = { transaction: t }
430
431 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
432 return callback(err, t)
433 })
434 },
435
436 commitTransaction
437
438 ], function (err, t) {
439 if (err) {
440 // This is just a debug because we will retry the insert
441 logger.debug('Cannot update the remote video.', { error: err })
442 return rollbackTransaction(err, t, finalCallback)
443 }
444
445 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
446 return finalCallback(null)
447 })
448 }
449
450 function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
451 // We need the instance because we have to remove some other stuffs (thumbnail etc)
452 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
453 // Do not return the error, continue the process
454 if (err) return callback(null)
455
456 logger.debug('Removing remote video %s.', video.remoteId)
457 video.destroy().asCallback(function (err) {
458 // Do not return the error, continue the process
459 if (err) {
460 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
461 }
462
463 return callback(null)
464 })
465 })
466 }
467
468 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
469 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
470 if (err || !video) {
471 if (!err) err = new Error('video not found')
472
473 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
474 // Do not return the error, continue the process
475 return callback(null)
476 }
477
478 logger.debug('Reporting remote abuse for video %s.', video.id)
479
480 const videoAbuseData = {
481 reporterUsername: reportData.reporterUsername,
482 reason: reportData.reportReason,
483 reporterPodId: fromPod.id,
484 videoId: video.id
485 }
486
487 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
488 if (err) {
489 logger.error('Cannot create remote abuse video.', { error: err })
490 }
491
492 return callback(null)
493 })
494 })
495 }
496
497 function fetchOwnedVideo (id, callback) {
498 db.Video.load(id, function (err, video) {
499 if (err || !video) {
500 if (!err) err = new Error('video not found')
501
502 logger.error('Cannot load owned video from id.', { error: err, id })
503 return callback(err)
504 }
505
506 return callback(null, video)
507 })
508 }
509
510 function fetchRemoteVideo (podHost, remoteId, callback) {
511 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
512 if (err || !video) {
513 if (!err) err = new Error('video not found')
514
515 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
516 return callback(err)
517 }
518
519 return callback(null, video)
520 })
521 }