1 import * as express from 'express'
2 import * as Sequelize from 'sequelize'
3 import { eachSeries, waterfall } from 'async'
5 import { database as db } from '../../../initializers/database'
7 REQUEST_ENDPOINT_ACTIONS,
9 REQUEST_VIDEO_EVENT_TYPES,
10 REQUEST_VIDEO_QADU_TYPES
11 } from '../../../initializers'
15 remoteVideosValidator,
16 remoteQaduVideosValidator,
17 remoteEventsVideosValidator
18 } from '../../../middlewares'
22 retryTransactionWrapper,
24 startSerializableTransaction
25 } from '../../../helpers'
26 import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
27 import { PodInstance, VideoInstance } from '../../../models'
29 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
31 // Functions to call when processing a remote request
32 const functionsHash = {}
33 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
34 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
35 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
36 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
38 const remoteVideosRouter = express.Router()
40 remoteVideosRouter.post('/',
43 remoteVideosValidator,
47 remoteVideosRouter.post('/qadu',
50 remoteQaduVideosValidator,
54 remoteVideosRouter.post('/events',
57 remoteEventsVideosValidator,
61 // ---------------------------------------------------------------------------
67 // ---------------------------------------------------------------------------
69 function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
70 const requests = req.body.data
71 const fromPod = res.locals.secure.pod
73 // We need to process in the same order to keep consistency
75 eachSeries(requests, function (request: any, callbackEach) {
76 const data = request.data
78 // Get the function we need to call in order to process the request
79 const fun = functionsHash[request.type]
80 if (fun === undefined) {
81 logger.error('Unkown remote request type %s.', request.type)
82 return callbackEach(null)
85 fun.call(this, data, fromPod, callbackEach)
87 if (err) logger.error('Error managing remote videos.', { error: err })
90 // We don't need to keep the other pod waiting
91 return res.type('json').status(204).end()
94 function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
95 const requests = req.body.data
96 const fromPod = res.locals.secure.pod
98 eachSeries(requests, function (request: any, callbackEach) {
99 const videoData = request.data
101 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
103 if (err) logger.error('Error managing remote videos.', { error: err })
106 return res.type('json').status(204).end()
109 function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
110 const requests = req.body.data
111 const fromPod = res.locals.secure.pod
113 eachSeries(requests, function (request: any, callbackEach) {
114 const eventData = request.data
116 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
118 if (err) logger.error('Error managing remote videos.', { error: err })
121 return res.type('json').status(204).end()
124 function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
126 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.'
130 retryTransactionWrapper(processVideosEvents, options, finalCallback)
133 function processVideosEvents (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
135 startSerializableTransaction,
137 function findVideo (t, callback) {
138 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
139 return callback(err, t, videoInstance)
143 function updateVideoIntoDB (t, videoInstance, callback) {
144 const options = { transaction: t }
149 switch (eventData.eventType) {
150 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
151 columnToUpdate = 'views'
152 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
155 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
156 columnToUpdate = 'likes'
157 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
160 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
161 columnToUpdate = 'dislikes'
162 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
166 return callback(new Error('Unknown video event type.'))
170 query[columnToUpdate] = eventData.count
172 videoInstance.increment(query, options).asCallback(function (err) {
173 return callback(err, t, videoInstance, qaduType)
177 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
178 const qadusParams = [
180 videoId: videoInstance.id,
185 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
186 return callback(err, t)
192 ], function (err: Error, t: Sequelize.Transaction) {
194 logger.debug('Cannot process a video event.', { error: err })
195 return rollbackTransaction(err, t, finalCallback)
198 logger.info('Remote video event processed for video %s.', eventData.remoteId)
199 return finalCallback(null)
203 function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
205 arguments: [ videoData, fromPod ],
206 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
209 retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
212 function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
216 startSerializableTransaction,
218 function findVideo (t, callback) {
219 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
220 return callback(err, t, videoInstance)
224 function updateVideoIntoDB (t, videoInstance, callback) {
225 const options = { transaction: t }
227 videoName = videoInstance.name
229 if (videoData.views) {
230 videoInstance.set('views', videoData.views)
233 if (videoData.likes) {
234 videoInstance.set('likes', videoData.likes)
237 if (videoData.dislikes) {
238 videoInstance.set('dislikes', videoData.dislikes)
241 videoInstance.save(options).asCallback(function (err) {
242 return callback(err, t)
248 ], function (err: Error, t: Sequelize.Transaction) {
250 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
251 return rollbackTransaction(err, t, finalCallback)
254 logger.info('Remote video %s quick and dirty updated', videoName)
255 return finalCallback(null)
259 // Handle retries on fail
260 function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
262 arguments: [ videoToCreateData, fromPod ],
263 errorMessage: 'Cannot insert the remote video with many retries.'
266 retryTransactionWrapper(addRemoteVideo, options, finalCallback)
269 function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
270 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
274 startSerializableTransaction,
276 function assertRemoteIdAndHostUnique (t, callback) {
277 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
278 if (err) return callback(err)
280 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
282 return callback(null, t)
286 function findOrCreateAuthor (t, callback) {
287 const name = videoToCreateData.author
288 const podId = fromPod.id
289 // This author is from another pod so we do not associate a user
292 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
293 return callback(err, t, authorInstance)
297 function findOrCreateTags (t, author, callback) {
298 const tags = videoToCreateData.tags
300 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
301 return callback(err, t, author, tagInstances)
305 function createVideoObject (t, author, tagInstances, callback) {
307 name: videoToCreateData.name,
308 remoteId: videoToCreateData.remoteId,
309 extname: videoToCreateData.extname,
310 infoHash: videoToCreateData.infoHash,
311 category: videoToCreateData.category,
312 licence: videoToCreateData.licence,
313 language: videoToCreateData.language,
314 nsfw: videoToCreateData.nsfw,
315 description: videoToCreateData.description,
317 duration: videoToCreateData.duration,
318 createdAt: videoToCreateData.createdAt,
319 // FIXME: updatedAt does not seems to be considered by Sequelize
320 updatedAt: videoToCreateData.updatedAt,
321 views: videoToCreateData.views,
322 likes: videoToCreateData.likes,
323 dislikes: videoToCreateData.dislikes
326 const video = db.Video.build(videoData)
328 return callback(null, t, tagInstances, video)
331 function generateThumbnail (t, tagInstances, video, callback) {
332 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
334 logger.error('Cannot generate thumbnail from data.', { error: err })
338 return callback(err, t, tagInstances, video)
342 function insertVideoIntoDB (t, tagInstances, video, callback) {
347 video.save(options).asCallback(function (err, videoCreated) {
348 return callback(err, t, tagInstances, videoCreated)
352 function associateTagsToVideo (t, tagInstances, video, callback) {
357 video.setTags(tagInstances, options).asCallback(function (err) {
358 return callback(err, t)
364 ], function (err: Error, t: Sequelize.Transaction) {
366 // This is just a debug because we will retry the insert
367 logger.debug('Cannot insert the remote video.', { error: err })
368 return rollbackTransaction(err, t, finalCallback)
371 logger.info('Remote video %s inserted.', videoToCreateData.name)
372 return finalCallback(null)
376 // Handle retries on fail
377 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
379 arguments: [ videoAttributesToUpdate, fromPod ],
380 errorMessage: 'Cannot update the remote video with many retries'
383 retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
386 function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
387 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
391 startSerializableTransaction,
393 function findVideo (t, callback) {
394 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
395 return callback(err, t, videoInstance)
399 function findOrCreateTags (t, videoInstance, callback) {
400 const tags = videoAttributesToUpdate.tags
402 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
403 return callback(err, t, videoInstance, tagInstances)
407 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
408 const options = { transaction: t }
410 videoInstance.set('name', videoAttributesToUpdate.name)
411 videoInstance.set('category', videoAttributesToUpdate.category)
412 videoInstance.set('licence', videoAttributesToUpdate.licence)
413 videoInstance.set('language', videoAttributesToUpdate.language)
414 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
415 videoInstance.set('description', videoAttributesToUpdate.description)
416 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
417 videoInstance.set('duration', videoAttributesToUpdate.duration)
418 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
419 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
420 videoInstance.set('extname', videoAttributesToUpdate.extname)
421 videoInstance.set('views', videoAttributesToUpdate.views)
422 videoInstance.set('likes', videoAttributesToUpdate.likes)
423 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
425 videoInstance.save(options).asCallback(function (err) {
426 return callback(err, t, videoInstance, tagInstances)
430 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
431 const options = { transaction: t }
433 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
434 return callback(err, t)
440 ], function (err: Error, t: Sequelize.Transaction) {
442 // This is just a debug because we will retry the insert
443 logger.debug('Cannot update the remote video.', { error: err })
444 return rollbackTransaction(err, t, finalCallback)
447 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
448 return finalCallback(null)
452 function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance, callback: (err: Error) => void) {
453 // We need the instance because we have to remove some other stuffs (thumbnail etc)
454 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
455 // Do not return the error, continue the process
456 if (err) return callback(null)
458 logger.debug('Removing remote video %s.', video.remoteId)
459 video.destroy().asCallback(function (err) {
460 // Do not return the error, continue the process
462 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
465 return callback(null)
470 function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance, callback: (err: Error) => void) {
471 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
473 if (!err) err = new Error('video not found')
475 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
476 // Do not return the error, continue the process
477 return callback(null)
480 logger.debug('Reporting remote abuse for video %s.', video.id)
482 const videoAbuseData = {
483 reporterUsername: reportData.reporterUsername,
484 reason: reportData.reportReason,
485 reporterPodId: fromPod.id,
489 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
491 logger.error('Cannot create remote abuse video.', { error: err })
494 return callback(null)
499 function fetchOwnedVideo (id: string, callback: (err: Error, video?: VideoInstance) => void) {
500 db.Video.load(id, function (err, video) {
502 if (!err) err = new Error('video not found')
504 logger.error('Cannot load owned video from id.', { error: err, id })
508 return callback(null, video)
512 function fetchRemoteVideo (podHost: string, remoteId: string, callback: (err: Error, video?: VideoInstance) => void) {
513 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
515 if (!err) err = new Error('video not found')
517 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
521 return callback(null, video)