]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/controllers/api/remote/videos.js
Add like/dislike system for videos
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.js
... / ...
CommitLineData
1'use strict'
2
3const eachSeries = require('async/eachSeries')
4const express = require('express')
5const waterfall = require('async/waterfall')
6
7const db = require('../../../initializers/database')
8const constants = require('../../../initializers/constants')
9const middlewares = require('../../../middlewares')
10const secureMiddleware = middlewares.secure
11const videosValidators = middlewares.validators.remote.videos
12const signatureValidators = middlewares.validators.remote.signature
13const logger = require('../../../helpers/logger')
14const friends = require('../../../lib/friends')
15const databaseUtils = require('../../../helpers/database-utils')
16
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
26const router = express.Router()
27
28router.post('/',
29 signatureValidators.signature,
30 secureMiddleware.checkSignature,
31 videosValidators.remoteVideos,
32 remoteVideos
33)
34
35router.post('/qadu',
36 signatureValidators.signature,
37 secureMiddleware.checkSignature,
38 videosValidators.remoteQaduVideos,
39 remoteVideosQadu
40)
41
42router.post('/events',
43 signatureValidators.signature,
44 secureMiddleware.checkSignature,
45 videosValidators.remoteEventsVideos,
46 remoteVideosEvents
47)
48
49// ---------------------------------------------------------------------------
50
51module.exports = router
52
53// ---------------------------------------------------------------------------
54
55function remoteVideos (req, res, next) {
56 const requests = req.body.data
57 const fromPod = res.locals.secure.pod
58
59 // We need to process in the same order to keep consistency
60 // TODO: optimization
61 eachSeries(requests, function (request, callbackEach) {
62 const data = request.data
63
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)
69 }
70
71 fun.call(this, data, fromPod, callbackEach)
72 }, function (err) {
73 if (err) logger.error('Error managing remote videos.', { error: err })
74 })
75
76 // We don't need to keep the other pod waiting
77 return res.type('json').status(204).end()
78}
79
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
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
133 let qaduType
134
135 switch (eventData.eventType) {
136 case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
137 columnToUpdate = 'views'
138 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
139 break
140
141 case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
142 columnToUpdate = 'likes'
143 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
144 break
145
146 case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
147 columnToUpdate = 'dislikes'
148 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
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) {
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) {
172 return callback(err, t)
173 })
174 },
175
176 databaseUtils.commitTransaction
177
178 ], function (err, t) {
179 if (err) {
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
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) {
199 waterfall([
200 databaseUtils.startSerializableTransaction,
201
202 function findVideo (t, callback) {
203 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
204 return callback(err, t, videoInstance)
205 })
206 },
207
208 function updateVideoIntoDB (t, videoInstance, callback) {
209 const options = { transaction: t }
210
211 if (videoData.views) {
212 videoInstance.set('views', videoData.views)
213 }
214
215 if (videoData.likes) {
216 videoInstance.set('likes', videoData.likes)
217 }
218
219 if (videoData.dislikes) {
220 videoInstance.set('dislikes', videoData.dislikes)
221 }
222
223 videoInstance.save(options).asCallback(function (err) {
224 return callback(err, t)
225 })
226 },
227
228 databaseUtils.commitTransaction
229
230 ], function (err, t) {
231 if (err) {
232 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
233 return databaseUtils.rollbackTransaction(err, t, finalCallback)
234 }
235
236 logger.info('Remote video %s quick and dirty updated', videoData.name)
237 return finalCallback(null)
238 })
239}
240
241// Handle retries on fail
242function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
243 const options = {
244 arguments: [ videoToCreateData, fromPod ],
245 errorMessage: 'Cannot insert the remote video with many retries.'
246 }
247
248 databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
249}
250
251function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
252 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
253
254 waterfall([
255
256 databaseUtils.startSerializableTransaction,
257
258 function assertRemoteIdAndHostUnique (t, callback) {
259 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
260 if (err) return callback(err)
261
262 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
263
264 return callback(null, t)
265 })
266 },
267
268 function findOrCreateAuthor (t, callback) {
269 const name = videoToCreateData.author
270 const podId = fromPod.id
271 // This author is from another pod so we do not associate a user
272 const userId = null
273
274 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
275 return callback(err, t, authorInstance)
276 })
277 },
278
279 function findOrCreateTags (t, author, callback) {
280 const tags = videoToCreateData.tags
281
282 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
283 return callback(err, t, author, tagInstances)
284 })
285 },
286
287 function createVideoObject (t, author, tagInstances, callback) {
288 const videoData = {
289 name: videoToCreateData.name,
290 remoteId: videoToCreateData.remoteId,
291 extname: videoToCreateData.extname,
292 infoHash: videoToCreateData.infoHash,
293 description: videoToCreateData.description,
294 authorId: author.id,
295 duration: videoToCreateData.duration,
296 createdAt: videoToCreateData.createdAt,
297 // FIXME: updatedAt does not seems to be considered by Sequelize
298 updatedAt: videoToCreateData.updatedAt,
299 views: videoToCreateData.views,
300 likes: videoToCreateData.likes,
301 dislikes: videoToCreateData.dislikes
302 }
303
304 const video = db.Video.build(videoData)
305
306 return callback(null, t, tagInstances, video)
307 },
308
309 function generateThumbnail (t, tagInstances, video, callback) {
310 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
311 if (err) {
312 logger.error('Cannot generate thumbnail from data.', { error: err })
313 return callback(err)
314 }
315
316 return callback(err, t, tagInstances, video)
317 })
318 },
319
320 function insertVideoIntoDB (t, tagInstances, video, callback) {
321 const options = {
322 transaction: t
323 }
324
325 video.save(options).asCallback(function (err, videoCreated) {
326 return callback(err, t, tagInstances, videoCreated)
327 })
328 },
329
330 function associateTagsToVideo (t, tagInstances, video, callback) {
331 const options = {
332 transaction: t
333 }
334
335 video.setTags(tagInstances, options).asCallback(function (err) {
336 return callback(err, t)
337 })
338 },
339
340 databaseUtils.commitTransaction
341
342 ], function (err, t) {
343 if (err) {
344 // This is just a debug because we will retry the insert
345 logger.debug('Cannot insert the remote video.', { error: err })
346 return databaseUtils.rollbackTransaction(err, t, finalCallback)
347 }
348
349 logger.info('Remote video %s inserted.', videoToCreateData.name)
350 return finalCallback(null)
351 })
352}
353
354// Handle retries on fail
355function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
356 const options = {
357 arguments: [ videoAttributesToUpdate, fromPod ],
358 errorMessage: 'Cannot update the remote video with many retries'
359 }
360
361 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
362}
363
364function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
365 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
366
367 waterfall([
368
369 databaseUtils.startSerializableTransaction,
370
371 function findVideo (t, callback) {
372 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
373 return callback(err, t, videoInstance)
374 })
375 },
376
377 function findOrCreateTags (t, videoInstance, callback) {
378 const tags = videoAttributesToUpdate.tags
379
380 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
381 return callback(err, t, videoInstance, tagInstances)
382 })
383 },
384
385 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
386 const options = { transaction: t }
387
388 videoInstance.set('name', videoAttributesToUpdate.name)
389 videoInstance.set('description', videoAttributesToUpdate.description)
390 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
391 videoInstance.set('duration', videoAttributesToUpdate.duration)
392 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
393 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
394 videoInstance.set('extname', videoAttributesToUpdate.extname)
395 videoInstance.set('views', videoAttributesToUpdate.views)
396 videoInstance.set('likes', videoAttributesToUpdate.likes)
397 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
398
399 videoInstance.save(options).asCallback(function (err) {
400 return callback(err, t, videoInstance, tagInstances)
401 })
402 },
403
404 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
405 const options = { transaction: t }
406
407 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
408 return callback(err, t)
409 })
410 },
411
412 databaseUtils.commitTransaction
413
414 ], function (err, t) {
415 if (err) {
416 // This is just a debug because we will retry the insert
417 logger.debug('Cannot update the remote video.', { error: err })
418 return databaseUtils.rollbackTransaction(err, t, finalCallback)
419 }
420
421 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
422 return finalCallback(null)
423 })
424}
425
426function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
427 // We need the instance because we have to remove some other stuffs (thumbnail etc)
428 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
429 // Do not return the error, continue the process
430 if (err) return callback(null)
431
432 logger.debug('Removing remote video %s.', video.remoteId)
433 video.destroy().asCallback(function (err) {
434 // Do not return the error, continue the process
435 if (err) {
436 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
437 }
438
439 return callback(null)
440 })
441 })
442}
443
444function reportAbuseRemoteVideo (reportData, fromPod, callback) {
445 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
446 if (err || !video) {
447 if (!err) err = new Error('video not found')
448
449 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
450 // Do not return the error, continue the process
451 return callback(null)
452 }
453
454 logger.debug('Reporting remote abuse for video %s.', video.id)
455
456 const videoAbuseData = {
457 reporterUsername: reportData.reporterUsername,
458 reason: reportData.reportReason,
459 reporterPodId: fromPod.id,
460 videoId: video.id
461 }
462
463 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
464 if (err) {
465 logger.error('Cannot create remote abuse video.', { error: err })
466 }
467
468 return callback(null)
469 })
470 })
471}
472
473function fetchOwnedVideo (id, callback) {
474 db.Video.load(id, function (err, video) {
475 if (err || !video) {
476 if (!err) err = new Error('video not found')
477
478 logger.error('Cannot load owned video from id.', { error: err, id })
479 return callback(err)
480 }
481
482 return callback(null, video)
483 })
484}
485
486function fetchRemoteVideo (podHost, remoteId, callback) {
487 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
488 if (err || !video) {
489 if (!err) err = new Error('video not found')
490
491 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
492 return callback(err)
493 }
494
495 return callback(null, video)
496 })
497}