]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.js
Better real world test
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.js
CommitLineData
528a9efa
C
1'use strict'
2
1a42c9e2 3const eachSeries = require('async/eachSeries')
528a9efa 4const express = require('express')
feb4bdfd 5const waterfall = require('async/waterfall')
528a9efa 6
a6fd2b30 7const db = require('../../../initializers/database')
62f4ef41 8const constants = require('../../../initializers/constants')
a6fd2b30 9const middlewares = require('../../../middlewares')
528a9efa 10const secureMiddleware = middlewares.secure
55fa55a9
C
11const videosValidators = middlewares.validators.remote.videos
12const signatureValidators = middlewares.validators.remote.signature
a6fd2b30 13const logger = require('../../../helpers/logger')
d38b8281 14const friends = require('../../../lib/friends')
4df023f2 15const databaseUtils = require('../../../helpers/database-utils')
528a9efa 16
62f4ef41
C
17const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
18
19// Functions to call when processing a remote request
20const functionsHash = {}
21functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
22functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
23functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
24functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
25
528a9efa
C
26const router = express.Router()
27
a6fd2b30 28router.post('/',
55fa55a9 29 signatureValidators.signature,
0eb78d53 30 secureMiddleware.checkSignature,
55fa55a9 31 videosValidators.remoteVideos,
528a9efa
C
32 remoteVideos
33)
34
9e167724
C
35router.post('/qadu',
36 signatureValidators.signature,
37 secureMiddleware.checkSignature,
38 videosValidators.remoteQaduVideos,
39 remoteVideosQadu
40)
41
e4c87ec2
C
42router.post('/events',
43 signatureValidators.signature,
44 secureMiddleware.checkSignature,
45 videosValidators.remoteEventsVideos,
46 remoteVideosEvents
47)
48
528a9efa
C
49// ---------------------------------------------------------------------------
50
51module.exports = router
52
53// ---------------------------------------------------------------------------
54
55function remoteVideos (req, res, next) {
56 const requests = req.body.data
4ff0d862 57 const fromPod = res.locals.secure.pod
528a9efa
C
58
59 // We need to process in the same order to keep consistency
60 // TODO: optimization
1a42c9e2 61 eachSeries(requests, function (request, callbackEach) {
55fa55a9 62 const data = request.data
528a9efa 63
62f4ef41
C
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)
528a9efa 69 }
62f4ef41
C
70
71 fun.call(this, data, fromPod, callbackEach)
aaf61f38
C
72 }, function (err) {
73 if (err) logger.error('Error managing remote videos.', { error: err })
528a9efa
C
74 })
75
76 // We don't need to keep the other pod waiting
77 return res.type('json').status(204).end()
78}
79
9e167724
C
80function remoteVideosQadu (req, res, next) {
81 const requests = req.body.data
82 const fromPod = res.locals.secure.pod
83
84 eachSeries(requests, function (request, callbackEach) {
85 const videoData = request.data
86
87 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
88 }, function (err) {
89 if (err) logger.error('Error managing remote videos.', { error: err })
90 })
91
92 return res.type('json').status(204).end()
93}
94
e4c87ec2
C
95function remoteVideosEvents (req, res, next) {
96 const requests = req.body.data
97 const fromPod = res.locals.secure.pod
98
99 eachSeries(requests, function (request, callbackEach) {
100 const eventData = request.data
101
102 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
103 }, function (err) {
104 if (err) logger.error('Error managing remote videos.', { error: err })
105 })
106
107 return res.type('json').status(204).end()
108}
109
110function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) {
111 const options = {
112 arguments: [ eventData, fromPod ],
113 errorMessage: 'Cannot process videos events with many retries.'
114 }
115
116 databaseUtils.retryTransactionWrapper(processVideosEvents, options, finalCallback)
117}
118
119function processVideosEvents (eventData, fromPod, finalCallback) {
120 waterfall([
121 databaseUtils.startSerializableTransaction,
122
123 function findVideo (t, callback) {
124 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
125 return callback(err, t, videoInstance)
126 })
127 },
128
129 function updateVideoIntoDB (t, videoInstance, callback) {
130 const options = { transaction: t }
131
132 let columnToUpdate
d38b8281 133 let qaduType
e4c87ec2
C
134
135 switch (eventData.eventType) {
136 case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
137 columnToUpdate = 'views'
d38b8281 138 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
e4c87ec2
C
139 break
140
141 case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
142 columnToUpdate = 'likes'
d38b8281 143 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
e4c87ec2
C
144 break
145
146 case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
147 columnToUpdate = 'dislikes'
d38b8281 148 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
e4c87ec2
C
149 break
150
151 default:
152 return callback(new Error('Unknown video event type.'))
153 }
154
155 const query = {}
156 query[columnToUpdate] = eventData.count
157
158 videoInstance.increment(query, options).asCallback(function (err) {
d38b8281
C
159 return callback(err, t, videoInstance, qaduType)
160 })
161 },
162
163 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
164 const qadusParams = [
165 {
166 videoId: videoInstance.id,
167 type: qaduType
168 }
169 ]
170
171 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
e4c87ec2
C
172 return callback(err, t)
173 })
174 },
175
176 databaseUtils.commitTransaction
177
178 ], function (err, t) {
179 if (err) {
e4c87ec2
C
180 logger.debug('Cannot process a video event.', { error: err })
181 return databaseUtils.rollbackTransaction(err, t, finalCallback)
182 }
183
184 logger.info('Remote video event processed for video %s.', eventData.remoteId)
185 return finalCallback(null)
186 })
187}
188
9e167724
C
189function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) {
190 const options = {
191 arguments: [ videoData, fromPod ],
192 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
193 }
194
195 databaseUtils.retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
196}
197
198function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
f148e5ed
C
199 let videoName
200
9e167724
C
201 waterfall([
202 databaseUtils.startSerializableTransaction,
203
204 function findVideo (t, callback) {
e4c87ec2 205 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
9e167724
C
206 return callback(err, t, videoInstance)
207 })
208 },
209
210 function updateVideoIntoDB (t, videoInstance, callback) {
211 const options = { transaction: t }
212
f148e5ed
C
213 videoName = videoInstance.name
214
9e167724
C
215 if (videoData.views) {
216 videoInstance.set('views', videoData.views)
217 }
218
219 if (videoData.likes) {
220 videoInstance.set('likes', videoData.likes)
221 }
222
223 if (videoData.dislikes) {
224 videoInstance.set('dislikes', videoData.dislikes)
225 }
226
227 videoInstance.save(options).asCallback(function (err) {
228 return callback(err, t)
229 })
230 },
231
232 databaseUtils.commitTransaction
233
234 ], function (err, t) {
235 if (err) {
236 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
237 return databaseUtils.rollbackTransaction(err, t, finalCallback)
238 }
239
f148e5ed 240 logger.info('Remote video %s quick and dirty updated', videoName)
9e167724
C
241 return finalCallback(null)
242 })
243}
244
ed04d94f
C
245// Handle retries on fail
246function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
d6a5b018
C
247 const options = {
248 arguments: [ videoToCreateData, fromPod ],
249 errorMessage: 'Cannot insert the remote video with many retries.'
250 }
ed04d94f 251
4df023f2 252 databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
ed04d94f
C
253}
254
4ff0d862 255function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
ed04d94f 256 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
6666aad4 257
feb4bdfd
C
258 waterfall([
259
4df023f2 260 databaseUtils.startSerializableTransaction,
7920c273 261
cddadde8
C
262 function assertRemoteIdAndHostUnique (t, callback) {
263 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
264 if (err) return callback(err)
265
266 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
267
268 return callback(null, t)
269 })
270 },
271
4ff0d862
C
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
276 const userId = null
feb4bdfd 277
4ff0d862
C
278 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
279 return callback(err, t, authorInstance)
feb4bdfd
C
280 })
281 },
282
7920c273
C
283 function findOrCreateTags (t, author, callback) {
284 const tags = videoToCreateData.tags
7920c273 285
4ff0d862 286 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
7920c273
C
287 return callback(err, t, author, tagInstances)
288 })
289 },
290
291 function createVideoObject (t, author, tagInstances, callback) {
feb4bdfd
C
292 const videoData = {
293 name: videoToCreateData.name,
294 remoteId: videoToCreateData.remoteId,
295 extname: videoToCreateData.extname,
296 infoHash: videoToCreateData.infoHash,
297 description: videoToCreateData.description,
298 authorId: author.id,
124648d7 299 duration: videoToCreateData.duration,
79066fdf 300 createdAt: videoToCreateData.createdAt,
ed04d94f 301 // FIXME: updatedAt does not seems to be considered by Sequelize
d38b8281
C
302 updatedAt: videoToCreateData.updatedAt,
303 views: videoToCreateData.views,
304 likes: videoToCreateData.likes,
305 dislikes: videoToCreateData.dislikes
feb4bdfd
C
306 }
307
308 const video = db.Video.build(videoData)
309
7920c273 310 return callback(null, t, tagInstances, video)
feb4bdfd
C
311 },
312
7920c273 313 function generateThumbnail (t, tagInstances, video, callback) {
4d324488 314 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
feb4bdfd 315 if (err) {
4d324488 316 logger.error('Cannot generate thumbnail from data.', { error: err })
feb4bdfd
C
317 return callback(err)
318 }
319
7920c273 320 return callback(err, t, tagInstances, video)
feb4bdfd
C
321 })
322 },
323
7920c273
C
324 function insertVideoIntoDB (t, tagInstances, video, callback) {
325 const options = {
326 transaction: t
327 }
328
329 video.save(options).asCallback(function (err, videoCreated) {
330 return callback(err, t, tagInstances, videoCreated)
331 })
332 },
333
334 function associateTagsToVideo (t, tagInstances, video, callback) {
62f4ef41
C
335 const options = {
336 transaction: t
337 }
7920c273
C
338
339 video.setTags(tagInstances, options).asCallback(function (err) {
340 return callback(err, t)
341 })
4145c1c6
C
342 },
343
344 databaseUtils.commitTransaction
c77fa067 345
7920c273
C
346 ], function (err, t) {
347 if (err) {
ed04d94f
C
348 // This is just a debug because we will retry the insert
349 logger.debug('Cannot insert the remote video.', { error: err })
4145c1c6 350 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7920c273
C
351 }
352
4145c1c6
C
353 logger.info('Remote video %s inserted.', videoToCreateData.name)
354 return finalCallback(null)
7920c273 355 })
528a9efa
C
356}
357
ed04d94f
C
358// Handle retries on fail
359function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
d6a5b018 360 const options = {
fbc22d79 361 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
362 errorMessage: 'Cannot update the remote video with many retries'
363 }
ed04d94f 364
fbc22d79 365 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
ed04d94f
C
366}
367
3d118fb5 368function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
ed04d94f 369 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
feb4bdfd 370
3d118fb5
C
371 waterfall([
372
4df023f2 373 databaseUtils.startSerializableTransaction,
3d118fb5
C
374
375 function findVideo (t, callback) {
e4c87ec2 376 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
55fa55a9 377 return callback(err, t, videoInstance)
3d118fb5
C
378 })
379 },
380
381 function findOrCreateTags (t, videoInstance, callback) {
382 const tags = videoAttributesToUpdate.tags
383
384 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
385 return callback(err, t, videoInstance, tagInstances)
386 })
387 },
388
389 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
390 const options = { transaction: t }
391
392 videoInstance.set('name', videoAttributesToUpdate.name)
393 videoInstance.set('description', videoAttributesToUpdate.description)
394 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
395 videoInstance.set('duration', videoAttributesToUpdate.duration)
396 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
79066fdf 397 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
3d118fb5 398 videoInstance.set('extname', videoAttributesToUpdate.extname)
d38b8281
C
399 videoInstance.set('views', videoAttributesToUpdate.views)
400 videoInstance.set('likes', videoAttributesToUpdate.likes)
401 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
3d118fb5
C
402
403 videoInstance.save(options).asCallback(function (err) {
404 return callback(err, t, videoInstance, tagInstances)
405 })
406 },
407
408 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
409 const options = { transaction: t }
410
411 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
412 return callback(err, t)
413 })
4145c1c6
C
414 },
415
416 databaseUtils.commitTransaction
528a9efa 417
3d118fb5
C
418 ], function (err, t) {
419 if (err) {
ed04d94f
C
420 // This is just a debug because we will retry the insert
421 logger.debug('Cannot update the remote video.', { error: err })
4145c1c6 422 return databaseUtils.rollbackTransaction(err, t, finalCallback)
6666aad4
C
423 }
424
4145c1c6
C
425 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
426 return finalCallback(null)
3d118fb5
C
427 })
428}
429
430function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
431 // We need the instance because we have to remove some other stuffs (thumbnail etc)
e4c87ec2 432 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
d8cc063e
C
433 // Do not return the error, continue the process
434 if (err) return callback(null)
55fa55a9
C
435
436 logger.debug('Removing remote video %s.', video.remoteId)
d8cc063e
C
437 video.destroy().asCallback(function (err) {
438 // Do not return the error, continue the process
439 if (err) {
440 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
441 }
442
443 return callback(null)
444 })
55fa55a9
C
445 })
446}
447
448function reportAbuseRemoteVideo (reportData, fromPod, callback) {
e4c87ec2 449 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
3d118fb5 450 if (err || !video) {
55fa55a9
C
451 if (!err) err = new Error('video not found')
452
ed04d94f 453 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
d8cc063e
C
454 // Do not return the error, continue the process
455 return callback(null)
3d118fb5 456 }
6666aad4 457
55fa55a9
C
458 logger.debug('Reporting remote abuse for video %s.', video.id)
459
460 const videoAbuseData = {
461 reporterUsername: reportData.reporterUsername,
462 reason: reportData.reportReason,
463 reporterPodId: fromPod.id,
464 videoId: video.id
465 }
466
d8cc063e
C
467 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
468 if (err) {
469 logger.error('Cannot create remote abuse video.', { error: err })
470 }
471
472 return callback(null)
473 })
55fa55a9
C
474 })
475}
476
e4c87ec2
C
477function fetchOwnedVideo (id, callback) {
478 db.Video.load(id, function (err, video) {
479 if (err || !video) {
480 if (!err) err = new Error('video not found')
481
482 logger.error('Cannot load owned video from id.', { error: err, id })
483 return callback(err)
484 }
485
486 return callback(null, video)
487 })
488}
489
490function fetchRemoteVideo (podHost, remoteId, callback) {
55fa55a9
C
491 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
492 if (err || !video) {
493 if (!err) err = new Error('video not found')
494
ed04d94f 495 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
55fa55a9
C
496 return callback(err)
497 }
498
499 return callback(null, video)
528a9efa
C
500 })
501}