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