diff options
Diffstat (limited to 'server/controllers/api/remote/videos.ts')
-rw-r--r-- | server/controllers/api/remote/videos.ts | 521 |
1 files changed, 521 insertions, 0 deletions
diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts new file mode 100644 index 000000000..df4ba8309 --- /dev/null +++ b/server/controllers/api/remote/videos.ts | |||
@@ -0,0 +1,521 @@ | |||
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 | } | ||