3 const eachSeries
= require('async/eachSeries')
4 const express
= require('express')
5 const waterfall
= require('async/waterfall')
7 const db
= require('../../../initializers/database')
8 const constants
= require('../../../initializers/constants')
9 const middlewares
= require('../../../middlewares')
10 const secureMiddleware
= middlewares
.secure
11 const videosValidators
= middlewares
.validators
.remote
.videos
12 const signatureValidators
= middlewares
.validators
.remote
.signature
13 const logger
= require('../../../helpers/logger')
14 const friends
= require('../../../lib/friends')
15 const databaseUtils
= require('../../../helpers/database-utils')
17 const ENDPOINT_ACTIONS
= constants
.REQUEST_ENDPOINT_ACTIONS
[constants
.REQUEST_ENDPOINTS
.VIDEOS
]
19 // Functions to call when processing a remote request
20 const functionsHash
= {}
21 functionsHash
[ENDPOINT_ACTIONS
.ADD
] = addRemoteVideoRetryWrapper
22 functionsHash
[ENDPOINT_ACTIONS
.UPDATE
] = updateRemoteVideoRetryWrapper
23 functionsHash
[ENDPOINT_ACTIONS
.REMOVE
] = removeRemoteVideo
24 functionsHash
[ENDPOINT_ACTIONS
.REPORT_ABUSE
] = reportAbuseRemoteVideo
26 const router
= express
.Router()
29 signatureValidators
.signature
,
30 secureMiddleware
.checkSignature
,
31 videosValidators
.remoteVideos
,
36 signatureValidators
.signature
,
37 secureMiddleware
.checkSignature
,
38 videosValidators
.remoteQaduVideos
,
42 router
.post('/events',
43 signatureValidators
.signature
,
44 secureMiddleware
.checkSignature
,
45 videosValidators
.remoteEventsVideos
,
49 // ---------------------------------------------------------------------------
51 module
.exports
= router
53 // ---------------------------------------------------------------------------
55 function remoteVideos (req
, res
, next
) {
56 const requests
= req
.body
.data
57 const fromPod
= res
.locals
.secure
.pod
59 // We need to process in the same order to keep consistency
61 eachSeries(requests
, function (request
, callbackEach
) {
62 const data
= request
.data
64 // Get the function we need to call in order to process the request
65 const fun
= functionsHash
[request
.type
]
66 if (fun
=== undefined) {
67 logger
.error('Unkown remote request type %s.', request
.type
)
68 return callbackEach(null)
71 fun
.call(this, data
, fromPod
, callbackEach
)
73 if (err
) logger
.error('Error managing remote videos.', { error: err
})
76 // We don't need to keep the other pod waiting
77 return res
.type('json').status(204).end()
80 function remoteVideosQadu (req
, res
, next
) {
81 const requests
= req
.body
.data
82 const fromPod
= res
.locals
.secure
.pod
84 eachSeries(requests
, function (request
, callbackEach
) {
85 const videoData
= request
.data
87 quickAndDirtyUpdateVideoRetryWrapper(videoData
, fromPod
, callbackEach
)
89 if (err
) logger
.error('Error managing remote videos.', { error: err
})
92 return res
.type('json').status(204).end()
95 function remoteVideosEvents (req
, res
, next
) {
96 const requests
= req
.body
.data
97 const fromPod
= res
.locals
.secure
.pod
99 eachSeries(requests
, function (request
, callbackEach
) {
100 const eventData
= request
.data
102 processVideosEventsRetryWrapper(eventData
, fromPod
, callbackEach
)
104 if (err
) logger
.error('Error managing remote videos.', { error: err
})
107 return res
.type('json').status(204).end()
110 function processVideosEventsRetryWrapper (eventData
, fromPod
, finalCallback
) {
112 arguments: [ eventData
, fromPod
],
113 errorMessage: 'Cannot process videos events with many retries.'
116 databaseUtils
.retryTransactionWrapper(processVideosEvents
, options
, finalCallback
)
119 function processVideosEvents (eventData
, fromPod
, finalCallback
) {
121 databaseUtils
.startSerializableTransaction
,
123 function findVideo (t
, callback
) {
124 fetchOwnedVideo(eventData
.remoteId
, function (err
, videoInstance
) {
125 return callback(err
, t
, videoInstance
)
129 function updateVideoIntoDB (t
, videoInstance
, callback
) {
130 const options
= { transaction: t
}
135 switch (eventData
.eventType
) {
136 case constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS:
137 columnToUpdate
= 'views'
138 qaduType
= constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
141 case constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES:
142 columnToUpdate
= 'likes'
143 qaduType
= constants
.REQUEST_VIDEO_QADU_TYPES
.LIKES
146 case constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES:
147 columnToUpdate
= 'dislikes'
148 qaduType
= constants
.REQUEST_VIDEO_QADU_TYPES
.DISLIKES
152 return callback(new Error('Unknown video event type.'))
156 query
[columnToUpdate
] = eventData
.count
158 videoInstance
.increment(query
, options
).asCallback(function (err
) {
159 return callback(err
, t
, videoInstance
, qaduType
)
163 function sendQaduToFriends (t
, videoInstance
, qaduType
, callback
) {
164 const qadusParams
= [
166 videoId: videoInstance
.id
,
171 friends
.quickAndDirtyUpdatesVideoToFriends(qadusParams
, t
, function (err
) {
172 return callback(err
, t
)
176 databaseUtils
.commitTransaction
178 ], function (err
, t
) {
180 logger
.debug('Cannot process a video event.', { error: err
})
181 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
184 logger
.info('Remote video event processed for video %s.', eventData
.remoteId
)
185 return finalCallback(null)
189 function quickAndDirtyUpdateVideoRetryWrapper (videoData
, fromPod
, finalCallback
) {
191 arguments: [ videoData
, fromPod
],
192 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
195 databaseUtils
.retryTransactionWrapper(quickAndDirtyUpdateVideo
, options
, finalCallback
)
198 function quickAndDirtyUpdateVideo (videoData
, fromPod
, finalCallback
) {
202 databaseUtils
.startSerializableTransaction
,
204 function findVideo (t
, callback
) {
205 fetchRemoteVideo(fromPod
.host
, videoData
.remoteId
, function (err
, videoInstance
) {
206 return callback(err
, t
, videoInstance
)
210 function updateVideoIntoDB (t
, videoInstance
, callback
) {
211 const options
= { transaction: t
}
213 videoName
= videoInstance
.name
215 if (videoData
.views
) {
216 videoInstance
.set('views', videoData
.views
)
219 if (videoData
.likes
) {
220 videoInstance
.set('likes', videoData
.likes
)
223 if (videoData
.dislikes
) {
224 videoInstance
.set('dislikes', videoData
.dislikes
)
227 videoInstance
.save(options
).asCallback(function (err
) {
228 return callback(err
, t
)
232 databaseUtils
.commitTransaction
234 ], function (err
, t
) {
236 logger
.debug('Cannot quick and dirty update the remote video.', { error: err
})
237 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
240 logger
.info('Remote video %s quick and dirty updated', videoName
)
241 return finalCallback(null)
245 // Handle retries on fail
246 function addRemoteVideoRetryWrapper (videoToCreateData
, fromPod
, finalCallback
) {
248 arguments: [ videoToCreateData
, fromPod
],
249 errorMessage: 'Cannot insert the remote video with many retries.'
252 databaseUtils
.retryTransactionWrapper(addRemoteVideo
, options
, finalCallback
)
255 function addRemoteVideo (videoToCreateData
, fromPod
, finalCallback
) {
256 logger
.debug('Adding remote video "%s".', videoToCreateData
.remoteId
)
260 databaseUtils
.startSerializableTransaction
,
262 function assertRemoteIdAndHostUnique (t
, callback
) {
263 db
.Video
.loadByHostAndRemoteId(fromPod
.host
, videoToCreateData
.remoteId
, function (err
, video
) {
264 if (err
) return callback(err
)
266 if (video
) return callback(new Error('RemoteId and host pair is not unique.'))
268 return callback(null, t
)
272 function findOrCreateAuthor (t
, callback
) {
273 const name
= videoToCreateData
.author
274 const podId
= fromPod
.id
275 // This author is from another pod so we do not associate a user
278 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
279 return callback(err
, t
, authorInstance
)
283 function findOrCreateTags (t
, author
, callback
) {
284 const tags
= videoToCreateData
.tags
286 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
287 return callback(err
, t
, author
, tagInstances
)
291 function createVideoObject (t
, author
, tagInstances
, callback
) {
293 name: videoToCreateData
.name
,
294 remoteId: videoToCreateData
.remoteId
,
295 extname: videoToCreateData
.extname
,
296 infoHash: videoToCreateData
.infoHash
,
297 category: videoToCreateData
.category
,
298 licence: videoToCreateData
.licence
,
299 nsfw: videoToCreateData
.nsfw
,
300 description: videoToCreateData
.description
,
302 duration: videoToCreateData
.duration
,
303 createdAt: videoToCreateData
.createdAt
,
304 // FIXME: updatedAt does not seems to be considered by Sequelize
305 updatedAt: videoToCreateData
.updatedAt
,
306 views: videoToCreateData
.views
,
307 likes: videoToCreateData
.likes
,
308 dislikes: videoToCreateData
.dislikes
311 const video
= db
.Video
.build(videoData
)
313 return callback(null, t
, tagInstances
, video
)
316 function generateThumbnail (t
, tagInstances
, video
, callback
) {
317 db
.Video
.generateThumbnailFromData(video
, videoToCreateData
.thumbnailData
, function (err
) {
319 logger
.error('Cannot generate thumbnail from data.', { error: err
})
323 return callback(err
, t
, tagInstances
, video
)
327 function insertVideoIntoDB (t
, tagInstances
, video
, callback
) {
332 video
.save(options
).asCallback(function (err
, videoCreated
) {
333 return callback(err
, t
, tagInstances
, videoCreated
)
337 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
342 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
343 return callback(err
, t
)
347 databaseUtils
.commitTransaction
349 ], function (err
, t
) {
351 // This is just a debug because we will retry the insert
352 logger
.debug('Cannot insert the remote video.', { error: err
})
353 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
356 logger
.info('Remote video %s inserted.', videoToCreateData
.name
)
357 return finalCallback(null)
361 // Handle retries on fail
362 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate
, fromPod
, finalCallback
) {
364 arguments: [ videoAttributesToUpdate
, fromPod
],
365 errorMessage: 'Cannot update the remote video with many retries'
368 databaseUtils
.retryTransactionWrapper(updateRemoteVideo
, options
, finalCallback
)
371 function updateRemoteVideo (videoAttributesToUpdate
, fromPod
, finalCallback
) {
372 logger
.debug('Updating remote video "%s".', videoAttributesToUpdate
.remoteId
)
376 databaseUtils
.startSerializableTransaction
,
378 function findVideo (t
, callback
) {
379 fetchRemoteVideo(fromPod
.host
, videoAttributesToUpdate
.remoteId
, function (err
, videoInstance
) {
380 return callback(err
, t
, videoInstance
)
384 function findOrCreateTags (t
, videoInstance
, callback
) {
385 const tags
= videoAttributesToUpdate
.tags
387 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
388 return callback(err
, t
, videoInstance
, tagInstances
)
392 function updateVideoIntoDB (t
, videoInstance
, tagInstances
, callback
) {
393 const options
= { transaction: t
}
395 videoInstance
.set('name', videoAttributesToUpdate
.name
)
396 videoInstance
.set('category', videoAttributesToUpdate
.category
)
397 videoInstance
.set('licence', videoAttributesToUpdate
.licence
)
398 videoInstance
.set('nsfw', videoAttributesToUpdate
.nsfw
)
399 videoInstance
.set('description', videoAttributesToUpdate
.description
)
400 videoInstance
.set('infoHash', videoAttributesToUpdate
.infoHash
)
401 videoInstance
.set('duration', videoAttributesToUpdate
.duration
)
402 videoInstance
.set('createdAt', videoAttributesToUpdate
.createdAt
)
403 videoInstance
.set('updatedAt', videoAttributesToUpdate
.updatedAt
)
404 videoInstance
.set('extname', videoAttributesToUpdate
.extname
)
405 videoInstance
.set('views', videoAttributesToUpdate
.views
)
406 videoInstance
.set('likes', videoAttributesToUpdate
.likes
)
407 videoInstance
.set('dislikes', videoAttributesToUpdate
.dislikes
)
409 videoInstance
.save(options
).asCallback(function (err
) {
410 return callback(err
, t
, videoInstance
, tagInstances
)
414 function associateTagsToVideo (t
, videoInstance
, tagInstances
, callback
) {
415 const options
= { transaction: t
}
417 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
418 return callback(err
, t
)
422 databaseUtils
.commitTransaction
424 ], function (err
, t
) {
426 // This is just a debug because we will retry the insert
427 logger
.debug('Cannot update the remote video.', { error: err
})
428 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
431 logger
.info('Remote video %s updated', videoAttributesToUpdate
.name
)
432 return finalCallback(null)
436 function removeRemoteVideo (videoToRemoveData
, fromPod
, callback
) {
437 // We need the instance because we have to remove some other stuffs (thumbnail etc)
438 fetchRemoteVideo(fromPod
.host
, videoToRemoveData
.remoteId
, function (err
, video
) {
439 // Do not return the error, continue the process
440 if (err
) return callback(null)
442 logger
.debug('Removing remote video %s.', video
.remoteId
)
443 video
.destroy().asCallback(function (err
) {
444 // Do not return the error, continue the process
446 logger
.error('Cannot remove remote video with id %s.', videoToRemoveData
.remoteId
, { error: err
})
449 return callback(null)
454 function reportAbuseRemoteVideo (reportData
, fromPod
, callback
) {
455 fetchOwnedVideo(reportData
.videoRemoteId
, function (err
, video
) {
457 if (!err
) err
= new Error('video not found')
459 logger
.error('Cannot load video from id.', { error: err
, id: reportData
.videoRemoteId
})
460 // Do not return the error, continue the process
461 return callback(null)
464 logger
.debug('Reporting remote abuse for video %s.', video
.id
)
466 const videoAbuseData
= {
467 reporterUsername: reportData
.reporterUsername
,
468 reason: reportData
.reportReason
,
469 reporterPodId: fromPod
.id
,
473 db
.VideoAbuse
.create(videoAbuseData
).asCallback(function (err
) {
475 logger
.error('Cannot create remote abuse video.', { error: err
})
478 return callback(null)
483 function fetchOwnedVideo (id
, callback
) {
484 db
.Video
.load(id
, function (err
, video
) {
486 if (!err
) err
= new Error('video not found')
488 logger
.error('Cannot load owned video from id.', { error: err
, id
})
492 return callback(null, video
)
496 function fetchRemoteVideo (podHost
, remoteId
, callback
) {
497 db
.Video
.loadByHostAndRemoteId(podHost
, remoteId
, function (err
, video
) {
499 if (!err
) err
= new Error('video not found')
501 logger
.error('Cannot load video from host and remote id.', { error: err
, podHost
, remoteId
})
505 return callback(null, video
)