1 import * as express from 'express'
2 import { eachSeries, waterfall } from 'async'
4 import { database as db } from '../../../initializers/database'
6 REQUEST_ENDPOINT_ACTIONS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10 } from '../../../initializers'
14 remoteVideosValidator,
15 remoteQaduVideosValidator,
16 remoteEventsVideosValidator
17 } from '../../../middlewares'
21 retryTransactionWrapper,
23 startSerializableTransaction
24 } from '../../../helpers'
25 import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
27 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
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
36 const remoteVideosRouter = express.Router()
38 remoteVideosRouter.post('/',
41 remoteVideosValidator,
45 remoteVideosRouter.post('/qadu',
48 remoteQaduVideosValidator,
52 remoteVideosRouter.post('/events',
55 remoteEventsVideosValidator,
59 // ---------------------------------------------------------------------------
65 // ---------------------------------------------------------------------------
67 function remoteVideos (req, res, next) {
68 const requests = req.body.data
69 const fromPod = res.locals.secure.pod
71 // We need to process in the same order to keep consistency
73 eachSeries(requests, function (request: any, callbackEach) {
74 const data = request.data
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)
83 fun.call(this, data, fromPod, callbackEach)
85 if (err) logger.error('Error managing remote videos.', { error: err })
88 // We don't need to keep the other pod waiting
89 return res.type('json').status(204).end()
92 function remoteVideosQadu (req, res, next) {
93 const requests = req.body.data
94 const fromPod = res.locals.secure.pod
96 eachSeries(requests, function (request: any, callbackEach) {
97 const videoData = request.data
99 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
101 if (err) logger.error('Error managing remote videos.', { error: err })
104 return res.type('json').status(204).end()
107 function remoteVideosEvents (req, res, next) {
108 const requests = req.body.data
109 const fromPod = res.locals.secure.pod
111 eachSeries(requests, function (request: any, callbackEach) {
112 const eventData = request.data
114 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
116 if (err) logger.error('Error managing remote videos.', { error: err })
119 return res.type('json').status(204).end()
122 function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) {
124 arguments: [ eventData, fromPod ],
125 errorMessage: 'Cannot process videos events with many retries.'
128 retryTransactionWrapper(processVideosEvents, options, finalCallback)
131 function processVideosEvents (eventData, fromPod, finalCallback) {
133 startSerializableTransaction,
135 function findVideo (t, callback) {
136 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
137 return callback(err, t, videoInstance)
141 function updateVideoIntoDB (t, videoInstance, callback) {
142 const options = { transaction: t }
147 switch (eventData.eventType) {
148 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
149 columnToUpdate = 'views'
150 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
153 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
154 columnToUpdate = 'likes'
155 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
158 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
159 columnToUpdate = 'dislikes'
160 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
164 return callback(new Error('Unknown video event type.'))
168 query[columnToUpdate] = eventData.count
170 videoInstance.increment(query, options).asCallback(function (err) {
171 return callback(err, t, videoInstance, qaduType)
175 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
176 const qadusParams = [
178 videoId: videoInstance.id,
183 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
184 return callback(err, t)
190 ], function (err, t) {
192 logger.debug('Cannot process a video event.', { error: err })
193 return rollbackTransaction(err, t, finalCallback)
196 logger.info('Remote video event processed for video %s.', eventData.remoteId)
197 return finalCallback(null)
201 function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) {
203 arguments: [ videoData, fromPod ],
204 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
207 retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
210 function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
214 startSerializableTransaction,
216 function findVideo (t, callback) {
217 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
218 return callback(err, t, videoInstance)
222 function updateVideoIntoDB (t, videoInstance, callback) {
223 const options = { transaction: t }
225 videoName = videoInstance.name
227 if (videoData.views) {
228 videoInstance.set('views', videoData.views)
231 if (videoData.likes) {
232 videoInstance.set('likes', videoData.likes)
235 if (videoData.dislikes) {
236 videoInstance.set('dislikes', videoData.dislikes)
239 videoInstance.save(options).asCallback(function (err) {
240 return callback(err, t)
246 ], function (err, t) {
248 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
249 return rollbackTransaction(err, t, finalCallback)
252 logger.info('Remote video %s quick and dirty updated', videoName)
253 return finalCallback(null)
257 // Handle retries on fail
258 function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
260 arguments: [ videoToCreateData, fromPod ],
261 errorMessage: 'Cannot insert the remote video with many retries.'
264 retryTransactionWrapper(addRemoteVideo, options, finalCallback)
267 function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
268 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
272 startSerializableTransaction,
274 function assertRemoteIdAndHostUnique (t, callback) {
275 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
276 if (err) return callback(err)
278 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
280 return callback(null, t)
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
290 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
291 return callback(err, t, authorInstance)
295 function findOrCreateTags (t, author, callback) {
296 const tags = videoToCreateData.tags
298 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
299 return callback(err, t, author, tagInstances)
303 function createVideoObject (t, author, tagInstances, callback) {
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,
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
324 const video = db.Video.build(videoData)
326 return callback(null, t, tagInstances, video)
329 function generateThumbnail (t, tagInstances, video, callback) {
330 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
332 logger.error('Cannot generate thumbnail from data.', { error: err })
336 return callback(err, t, tagInstances, video)
340 function insertVideoIntoDB (t, tagInstances, video, callback) {
345 video.save(options).asCallback(function (err, videoCreated) {
346 return callback(err, t, tagInstances, videoCreated)
350 function associateTagsToVideo (t, tagInstances, video, callback) {
355 video.setTags(tagInstances, options).asCallback(function (err) {
356 return callback(err, t)
362 ], function (err, t) {
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)
369 logger.info('Remote video %s inserted.', videoToCreateData.name)
370 return finalCallback(null)
374 // Handle retries on fail
375 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
377 arguments: [ videoAttributesToUpdate, fromPod ],
378 errorMessage: 'Cannot update the remote video with many retries'
381 retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
384 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
385 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
389 startSerializableTransaction,
391 function findVideo (t, callback) {
392 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
393 return callback(err, t, videoInstance)
397 function findOrCreateTags (t, videoInstance, callback) {
398 const tags = videoAttributesToUpdate.tags
400 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
401 return callback(err, t, videoInstance, tagInstances)
405 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
406 const options = { transaction: t }
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)
423 videoInstance.save(options).asCallback(function (err) {
424 return callback(err, t, videoInstance, tagInstances)
428 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
429 const options = { transaction: t }
431 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
432 return callback(err, t)
438 ], function (err, t) {
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)
445 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
446 return finalCallback(null)
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)
456 logger.debug('Removing remote video %s.', video.remoteId)
457 video.destroy().asCallback(function (err) {
458 // Do not return the error, continue the process
460 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
463 return callback(null)
468 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
469 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
471 if (!err) err = new Error('video not found')
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)
478 logger.debug('Reporting remote abuse for video %s.', video.id)
480 const videoAbuseData = {
481 reporterUsername: reportData.reporterUsername,
482 reason: reportData.reportReason,
483 reporterPodId: fromPod.id,
487 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
489 logger.error('Cannot create remote abuse video.', { error: err })
492 return callback(null)
497 function fetchOwnedVideo (id, callback) {
498 db.Video.load(id, function (err, video) {
500 if (!err) err = new Error('video not found')
502 logger.error('Cannot load owned video from id.', { error: err, id })
506 return callback(null, video)
510 function fetchRemoteVideo (podHost, remoteId, callback) {
511 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
513 if (!err) err = new Error('video not found')
515 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
519 return callback(null, video)