]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.js
Add like/dislike system for videos
[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) {
199 waterfall([
200 databaseUtils.startSerializableTransaction,
201
202 function findVideo (t, callback) {
e4c87ec2 203 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
9e167724
C
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
ed04d94f
C
241// Handle retries on fail
242function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
d6a5b018
C
243 const options = {
244 arguments: [ videoToCreateData, fromPod ],
245 errorMessage: 'Cannot insert the remote video with many retries.'
246 }
ed04d94f 247
4df023f2 248 databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
ed04d94f
C
249}
250
4ff0d862 251function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
ed04d94f 252 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
6666aad4 253
feb4bdfd
C
254 waterfall([
255
4df023f2 256 databaseUtils.startSerializableTransaction,
7920c273 257
cddadde8
C
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
4ff0d862
C
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
feb4bdfd 273
4ff0d862
C
274 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
275 return callback(err, t, authorInstance)
feb4bdfd
C
276 })
277 },
278
7920c273
C
279 function findOrCreateTags (t, author, callback) {
280 const tags = videoToCreateData.tags
7920c273 281
4ff0d862 282 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
7920c273
C
283 return callback(err, t, author, tagInstances)
284 })
285 },
286
287 function createVideoObject (t, author, tagInstances, callback) {
feb4bdfd
C
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,
124648d7 295 duration: videoToCreateData.duration,
79066fdf 296 createdAt: videoToCreateData.createdAt,
ed04d94f 297 // FIXME: updatedAt does not seems to be considered by Sequelize
d38b8281
C
298 updatedAt: videoToCreateData.updatedAt,
299 views: videoToCreateData.views,
300 likes: videoToCreateData.likes,
301 dislikes: videoToCreateData.dislikes
feb4bdfd
C
302 }
303
304 const video = db.Video.build(videoData)
305
7920c273 306 return callback(null, t, tagInstances, video)
feb4bdfd
C
307 },
308
7920c273 309 function generateThumbnail (t, tagInstances, video, callback) {
4d324488 310 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
feb4bdfd 311 if (err) {
4d324488 312 logger.error('Cannot generate thumbnail from data.', { error: err })
feb4bdfd
C
313 return callback(err)
314 }
315
7920c273 316 return callback(err, t, tagInstances, video)
feb4bdfd
C
317 })
318 },
319
7920c273
C
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) {
62f4ef41
C
331 const options = {
332 transaction: t
333 }
7920c273
C
334
335 video.setTags(tagInstances, options).asCallback(function (err) {
336 return callback(err, t)
337 })
4145c1c6
C
338 },
339
340 databaseUtils.commitTransaction
c77fa067 341
7920c273
C
342 ], function (err, t) {
343 if (err) {
ed04d94f
C
344 // This is just a debug because we will retry the insert
345 logger.debug('Cannot insert the remote video.', { error: err })
4145c1c6 346 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7920c273
C
347 }
348
4145c1c6
C
349 logger.info('Remote video %s inserted.', videoToCreateData.name)
350 return finalCallback(null)
7920c273 351 })
528a9efa
C
352}
353
ed04d94f
C
354// Handle retries on fail
355function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
d6a5b018 356 const options = {
fbc22d79 357 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
358 errorMessage: 'Cannot update the remote video with many retries'
359 }
ed04d94f 360
fbc22d79 361 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
ed04d94f
C
362}
363
3d118fb5 364function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
ed04d94f 365 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
feb4bdfd 366
3d118fb5
C
367 waterfall([
368
4df023f2 369 databaseUtils.startSerializableTransaction,
3d118fb5
C
370
371 function findVideo (t, callback) {
e4c87ec2 372 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
55fa55a9 373 return callback(err, t, videoInstance)
3d118fb5
C
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)
79066fdf 393 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
3d118fb5 394 videoInstance.set('extname', videoAttributesToUpdate.extname)
d38b8281
C
395 videoInstance.set('views', videoAttributesToUpdate.views)
396 videoInstance.set('likes', videoAttributesToUpdate.likes)
397 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
3d118fb5
C
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 })
4145c1c6
C
410 },
411
412 databaseUtils.commitTransaction
528a9efa 413
3d118fb5
C
414 ], function (err, t) {
415 if (err) {
ed04d94f
C
416 // This is just a debug because we will retry the insert
417 logger.debug('Cannot update the remote video.', { error: err })
4145c1c6 418 return databaseUtils.rollbackTransaction(err, t, finalCallback)
6666aad4
C
419 }
420
4145c1c6
C
421 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
422 return finalCallback(null)
3d118fb5
C
423 })
424}
425
426function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
427 // We need the instance because we have to remove some other stuffs (thumbnail etc)
e4c87ec2 428 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
d8cc063e
C
429 // Do not return the error, continue the process
430 if (err) return callback(null)
55fa55a9
C
431
432 logger.debug('Removing remote video %s.', video.remoteId)
d8cc063e
C
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 })
55fa55a9
C
441 })
442}
443
444function reportAbuseRemoteVideo (reportData, fromPod, callback) {
e4c87ec2 445 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
3d118fb5 446 if (err || !video) {
55fa55a9
C
447 if (!err) err = new Error('video not found')
448
ed04d94f 449 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
d8cc063e
C
450 // Do not return the error, continue the process
451 return callback(null)
3d118fb5 452 }
6666aad4 453
55fa55a9
C
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
d8cc063e
C
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 })
55fa55a9
C
470 })
471}
472
e4c87ec2
C
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) {
55fa55a9
C
487 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
488 if (err || !video) {
489 if (!err) err = new Error('video not found')
490
ed04d94f 491 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
55fa55a9
C
492 return callback(err)
493 }
494
495 return callback(null, video)
528a9efa
C
496 })
497}