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 databaseUtils
= require('../../../helpers/database-utils')
16 const ENDPOINT_ACTIONS
= constants
.REQUEST_ENDPOINT_ACTIONS
[constants
.REQUEST_ENDPOINTS
.VIDEOS
]
18 // Functions to call when processing a remote request
19 const functionsHash
= {}
20 functionsHash
[ENDPOINT_ACTIONS
.ADD
] = addRemoteVideoRetryWrapper
21 functionsHash
[ENDPOINT_ACTIONS
.UPDATE
] = updateRemoteVideoRetryWrapper
22 functionsHash
[ENDPOINT_ACTIONS
.REMOVE
] = removeRemoteVideo
23 functionsHash
[ENDPOINT_ACTIONS
.REPORT_ABUSE
] = reportAbuseRemoteVideo
25 const router
= express
.Router()
28 signatureValidators
.signature
,
29 secureMiddleware
.checkSignature
,
30 videosValidators
.remoteVideos
,
35 signatureValidators
.signature
,
36 secureMiddleware
.checkSignature
,
37 videosValidators
.remoteQaduVideos
,
41 router
.post('/events',
42 signatureValidators
.signature
,
43 secureMiddleware
.checkSignature
,
44 videosValidators
.remoteEventsVideos
,
48 // ---------------------------------------------------------------------------
50 module
.exports
= router
52 // ---------------------------------------------------------------------------
54 function remoteVideos (req
, res
, next
) {
55 const requests
= req
.body
.data
56 const fromPod
= res
.locals
.secure
.pod
58 // We need to process in the same order to keep consistency
60 eachSeries(requests
, function (request
, callbackEach
) {
61 const data
= request
.data
63 // Get the function we need to call in order to process the request
64 const fun
= functionsHash
[request
.type
]
65 if (fun
=== undefined) {
66 logger
.error('Unkown remote request type %s.', request
.type
)
67 return callbackEach(null)
70 fun
.call(this, data
, fromPod
, callbackEach
)
72 if (err
) logger
.error('Error managing remote videos.', { error: err
})
75 // We don't need to keep the other pod waiting
76 return res
.type('json').status(204).end()
79 function remoteVideosQadu (req
, res
, next
) {
80 const requests
= req
.body
.data
81 const fromPod
= res
.locals
.secure
.pod
83 eachSeries(requests
, function (request
, callbackEach
) {
84 const videoData
= request
.data
86 quickAndDirtyUpdateVideoRetryWrapper(videoData
, fromPod
, callbackEach
)
88 if (err
) logger
.error('Error managing remote videos.', { error: err
})
91 return res
.type('json').status(204).end()
94 function remoteVideosEvents (req
, res
, next
) {
95 const requests
= req
.body
.data
96 const fromPod
= res
.locals
.secure
.pod
98 eachSeries(requests
, function (request
, callbackEach
) {
99 const eventData
= request
.data
101 processVideosEventsRetryWrapper(eventData
, fromPod
, callbackEach
)
103 if (err
) logger
.error('Error managing remote videos.', { error: err
})
106 return res
.type('json').status(204).end()
109 function processVideosEventsRetryWrapper (eventData
, fromPod
, finalCallback
) {
111 arguments: [ eventData
, fromPod
],
112 errorMessage: 'Cannot process videos events with many retries.'
115 databaseUtils
.retryTransactionWrapper(processVideosEvents
, options
, finalCallback
)
118 function processVideosEvents (eventData
, fromPod
, finalCallback
) {
120 databaseUtils
.startSerializableTransaction
,
122 function findVideo (t
, callback
) {
123 fetchOwnedVideo(eventData
.remoteId
, function (err
, videoInstance
) {
124 return callback(err
, t
, videoInstance
)
128 function updateVideoIntoDB (t
, videoInstance
, callback
) {
129 const options
= { transaction: t
}
133 switch (eventData
.eventType
) {
134 case constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS:
135 columnToUpdate
= 'views'
138 case constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES:
139 columnToUpdate
= 'likes'
142 case constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES:
143 columnToUpdate
= 'dislikes'
147 return callback(new Error('Unknown video event type.'))
151 query
[columnToUpdate
] = eventData
.count
153 videoInstance
.increment(query
, options
).asCallback(function (err
) {
154 return callback(err
, t
)
158 databaseUtils
.commitTransaction
160 ], function (err
, t
) {
163 logger
.debug('Cannot process a video event.', { error: err
})
164 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
167 logger
.info('Remote video event processed for video %s.', eventData
.remoteId
)
168 return finalCallback(null)
172 function quickAndDirtyUpdateVideoRetryWrapper (videoData
, fromPod
, finalCallback
) {
174 arguments: [ videoData
, fromPod
],
175 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
178 databaseUtils
.retryTransactionWrapper(quickAndDirtyUpdateVideo
, options
, finalCallback
)
181 function quickAndDirtyUpdateVideo (videoData
, fromPod
, finalCallback
) {
183 databaseUtils
.startSerializableTransaction
,
185 function findVideo (t
, callback
) {
186 fetchRemoteVideo(fromPod
.host
, videoData
.remoteId
, function (err
, videoInstance
) {
187 return callback(err
, t
, videoInstance
)
191 function updateVideoIntoDB (t
, videoInstance
, callback
) {
192 const options
= { transaction: t
}
194 if (videoData
.views
) {
195 videoInstance
.set('views', videoData
.views
)
198 if (videoData
.likes
) {
199 videoInstance
.set('likes', videoData
.likes
)
202 if (videoData
.dislikes
) {
203 videoInstance
.set('dislikes', videoData
.dislikes
)
206 videoInstance
.save(options
).asCallback(function (err
) {
207 return callback(err
, t
)
211 databaseUtils
.commitTransaction
213 ], function (err
, t
) {
215 logger
.debug('Cannot quick and dirty update the remote video.', { error: err
})
216 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
219 logger
.info('Remote video %s quick and dirty updated', videoData
.name
)
220 return finalCallback(null)
224 // Handle retries on fail
225 function addRemoteVideoRetryWrapper (videoToCreateData
, fromPod
, finalCallback
) {
227 arguments: [ videoToCreateData
, fromPod
],
228 errorMessage: 'Cannot insert the remote video with many retries.'
231 databaseUtils
.retryTransactionWrapper(addRemoteVideo
, options
, finalCallback
)
234 function addRemoteVideo (videoToCreateData
, fromPod
, finalCallback
) {
235 logger
.debug('Adding remote video "%s".', videoToCreateData
.remoteId
)
239 databaseUtils
.startSerializableTransaction
,
241 function assertRemoteIdAndHostUnique (t
, callback
) {
242 db
.Video
.loadByHostAndRemoteId(fromPod
.host
, videoToCreateData
.remoteId
, function (err
, video
) {
243 if (err
) return callback(err
)
245 if (video
) return callback(new Error('RemoteId and host pair is not unique.'))
247 return callback(null, t
)
251 function findOrCreateAuthor (t
, callback
) {
252 const name
= videoToCreateData
.author
253 const podId
= fromPod
.id
254 // This author is from another pod so we do not associate a user
257 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
258 return callback(err
, t
, authorInstance
)
262 function findOrCreateTags (t
, author
, callback
) {
263 const tags
= videoToCreateData
.tags
265 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
266 return callback(err
, t
, author
, tagInstances
)
270 function createVideoObject (t
, author
, tagInstances
, callback
) {
272 name: videoToCreateData
.name
,
273 remoteId: videoToCreateData
.remoteId
,
274 extname: videoToCreateData
.extname
,
275 infoHash: videoToCreateData
.infoHash
,
276 description: videoToCreateData
.description
,
278 duration: videoToCreateData
.duration
,
279 createdAt: videoToCreateData
.createdAt
,
280 // FIXME: updatedAt does not seems to be considered by Sequelize
281 updatedAt: videoToCreateData
.updatedAt
284 const video
= db
.Video
.build(videoData
)
286 return callback(null, t
, tagInstances
, video
)
289 function generateThumbnail (t
, tagInstances
, video
, callback
) {
290 db
.Video
.generateThumbnailFromData(video
, videoToCreateData
.thumbnailData
, function (err
) {
292 logger
.error('Cannot generate thumbnail from data.', { error: err
})
296 return callback(err
, t
, tagInstances
, video
)
300 function insertVideoIntoDB (t
, tagInstances
, video
, callback
) {
305 video
.save(options
).asCallback(function (err
, videoCreated
) {
306 return callback(err
, t
, tagInstances
, videoCreated
)
310 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
315 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
316 return callback(err
, t
)
320 databaseUtils
.commitTransaction
322 ], function (err
, t
) {
324 // This is just a debug because we will retry the insert
325 logger
.debug('Cannot insert the remote video.', { error: err
})
326 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
329 logger
.info('Remote video %s inserted.', videoToCreateData
.name
)
330 return finalCallback(null)
334 // Handle retries on fail
335 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate
, fromPod
, finalCallback
) {
337 arguments: [ videoAttributesToUpdate
, fromPod
],
338 errorMessage: 'Cannot update the remote video with many retries'
341 databaseUtils
.retryTransactionWrapper(updateRemoteVideo
, options
, finalCallback
)
344 function updateRemoteVideo (videoAttributesToUpdate
, fromPod
, finalCallback
) {
345 logger
.debug('Updating remote video "%s".', videoAttributesToUpdate
.remoteId
)
349 databaseUtils
.startSerializableTransaction
,
351 function findVideo (t
, callback
) {
352 fetchRemoteVideo(fromPod
.host
, videoAttributesToUpdate
.remoteId
, function (err
, videoInstance
) {
353 return callback(err
, t
, videoInstance
)
357 function findOrCreateTags (t
, videoInstance
, callback
) {
358 const tags
= videoAttributesToUpdate
.tags
360 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
361 return callback(err
, t
, videoInstance
, tagInstances
)
365 function updateVideoIntoDB (t
, videoInstance
, tagInstances
, callback
) {
366 const options
= { transaction: t
}
368 videoInstance
.set('name', videoAttributesToUpdate
.name
)
369 videoInstance
.set('description', videoAttributesToUpdate
.description
)
370 videoInstance
.set('infoHash', videoAttributesToUpdate
.infoHash
)
371 videoInstance
.set('duration', videoAttributesToUpdate
.duration
)
372 videoInstance
.set('createdAt', videoAttributesToUpdate
.createdAt
)
373 videoInstance
.set('updatedAt', videoAttributesToUpdate
.updatedAt
)
374 videoInstance
.set('extname', videoAttributesToUpdate
.extname
)
376 videoInstance
.save(options
).asCallback(function (err
) {
377 return callback(err
, t
, videoInstance
, tagInstances
)
381 function associateTagsToVideo (t
, videoInstance
, tagInstances
, callback
) {
382 const options
= { transaction: t
}
384 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
385 return callback(err
, t
)
389 databaseUtils
.commitTransaction
391 ], function (err
, t
) {
393 // This is just a debug because we will retry the insert
394 logger
.debug('Cannot update the remote video.', { error: err
})
395 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
398 logger
.info('Remote video %s updated', videoAttributesToUpdate
.name
)
399 return finalCallback(null)
403 function removeRemoteVideo (videoToRemoveData
, fromPod
, callback
) {
404 // We need the instance because we have to remove some other stuffs (thumbnail etc)
405 fetchRemoteVideo(fromPod
.host
, videoToRemoveData
.remoteId
, function (err
, video
) {
406 // Do not return the error, continue the process
407 if (err
) return callback(null)
409 logger
.debug('Removing remote video %s.', video
.remoteId
)
410 video
.destroy().asCallback(function (err
) {
411 // Do not return the error, continue the process
413 logger
.error('Cannot remove remote video with id %s.', videoToRemoveData
.remoteId
, { error: err
})
416 return callback(null)
421 function reportAbuseRemoteVideo (reportData
, fromPod
, callback
) {
422 fetchOwnedVideo(reportData
.videoRemoteId
, function (err
, video
) {
424 if (!err
) err
= new Error('video not found')
426 logger
.error('Cannot load video from id.', { error: err
, id: reportData
.videoRemoteId
})
427 // Do not return the error, continue the process
428 return callback(null)
431 logger
.debug('Reporting remote abuse for video %s.', video
.id
)
433 const videoAbuseData
= {
434 reporterUsername: reportData
.reporterUsername
,
435 reason: reportData
.reportReason
,
436 reporterPodId: fromPod
.id
,
440 db
.VideoAbuse
.create(videoAbuseData
).asCallback(function (err
) {
442 logger
.error('Cannot create remote abuse video.', { error: err
})
445 return callback(null)
450 function fetchOwnedVideo (id
, callback
) {
451 db
.Video
.load(id
, function (err
, video
) {
453 if (!err
) err
= new Error('video not found')
455 logger
.error('Cannot load owned video from id.', { error: err
, id
})
459 return callback(null, video
)
463 function fetchRemoteVideo (podHost
, remoteId
, callback
) {
464 db
.Video
.loadByHostAndRemoteId(podHost
, remoteId
, function (err
, video
) {
466 if (!err
) err
= new Error('video not found')
468 logger
.error('Cannot load video from host and remote id.', { error: err
, podHost
, remoteId
})
472 return callback(null, video
)