]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.js
Add video category support
[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 friends = require('../../../lib/friends')
15 const databaseUtils = require('../../../helpers/database-utils')
16
17 const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
18
19 // Functions to call when processing a remote request
20 const functionsHash = {}
21 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
22 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
23 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
24 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
25
26 const router = express.Router()
27
28 router.post('/',
29 signatureValidators.signature,
30 secureMiddleware.checkSignature,
31 videosValidators.remoteVideos,
32 remoteVideos
33 )
34
35 router.post('/qadu',
36 signatureValidators.signature,
37 secureMiddleware.checkSignature,
38 videosValidators.remoteQaduVideos,
39 remoteVideosQadu
40 )
41
42 router.post('/events',
43 signatureValidators.signature,
44 secureMiddleware.checkSignature,
45 videosValidators.remoteEventsVideos,
46 remoteVideosEvents
47 )
48
49 // ---------------------------------------------------------------------------
50
51 module.exports = router
52
53 // ---------------------------------------------------------------------------
54
55 function 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
80 function 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
95 function 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
110 function 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
119 function 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
189 function 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
198 function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
199 let videoName
200
201 waterfall([
202 databaseUtils.startSerializableTransaction,
203
204 function findVideo (t, callback) {
205 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
206 return callback(err, t, videoInstance)
207 })
208 },
209
210 function updateVideoIntoDB (t, videoInstance, callback) {
211 const options = { transaction: t }
212
213 videoName = videoInstance.name
214
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
240 logger.info('Remote video %s quick and dirty updated', videoName)
241 return finalCallback(null)
242 })
243 }
244
245 // Handle retries on fail
246 function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
247 const options = {
248 arguments: [ videoToCreateData, fromPod ],
249 errorMessage: 'Cannot insert the remote video with many retries.'
250 }
251
252 databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
253 }
254
255 function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
256 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
257
258 waterfall([
259
260 databaseUtils.startSerializableTransaction,
261
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
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
277
278 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
279 return callback(err, t, authorInstance)
280 })
281 },
282
283 function findOrCreateTags (t, author, callback) {
284 const tags = videoToCreateData.tags
285
286 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
287 return callback(err, t, author, tagInstances)
288 })
289 },
290
291 function createVideoObject (t, author, tagInstances, callback) {
292 const videoData = {
293 name: videoToCreateData.name,
294 remoteId: videoToCreateData.remoteId,
295 extname: videoToCreateData.extname,
296 infoHash: videoToCreateData.infoHash,
297 category: videoToCreateData.category,
298 description: videoToCreateData.description,
299 authorId: author.id,
300 duration: videoToCreateData.duration,
301 createdAt: videoToCreateData.createdAt,
302 // FIXME: updatedAt does not seems to be considered by Sequelize
303 updatedAt: videoToCreateData.updatedAt,
304 views: videoToCreateData.views,
305 likes: videoToCreateData.likes,
306 dislikes: videoToCreateData.dislikes
307 }
308
309 const video = db.Video.build(videoData)
310
311 return callback(null, t, tagInstances, video)
312 },
313
314 function generateThumbnail (t, tagInstances, video, callback) {
315 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
316 if (err) {
317 logger.error('Cannot generate thumbnail from data.', { error: err })
318 return callback(err)
319 }
320
321 return callback(err, t, tagInstances, video)
322 })
323 },
324
325 function insertVideoIntoDB (t, tagInstances, video, callback) {
326 const options = {
327 transaction: t
328 }
329
330 video.save(options).asCallback(function (err, videoCreated) {
331 return callback(err, t, tagInstances, videoCreated)
332 })
333 },
334
335 function associateTagsToVideo (t, tagInstances, video, callback) {
336 const options = {
337 transaction: t
338 }
339
340 video.setTags(tagInstances, options).asCallback(function (err) {
341 return callback(err, t)
342 })
343 },
344
345 databaseUtils.commitTransaction
346
347 ], function (err, t) {
348 if (err) {
349 // This is just a debug because we will retry the insert
350 logger.debug('Cannot insert the remote video.', { error: err })
351 return databaseUtils.rollbackTransaction(err, t, finalCallback)
352 }
353
354 logger.info('Remote video %s inserted.', videoToCreateData.name)
355 return finalCallback(null)
356 })
357 }
358
359 // Handle retries on fail
360 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
361 const options = {
362 arguments: [ videoAttributesToUpdate, fromPod ],
363 errorMessage: 'Cannot update the remote video with many retries'
364 }
365
366 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
367 }
368
369 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
370 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
371
372 waterfall([
373
374 databaseUtils.startSerializableTransaction,
375
376 function findVideo (t, callback) {
377 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
378 return callback(err, t, videoInstance)
379 })
380 },
381
382 function findOrCreateTags (t, videoInstance, callback) {
383 const tags = videoAttributesToUpdate.tags
384
385 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
386 return callback(err, t, videoInstance, tagInstances)
387 })
388 },
389
390 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
391 const options = { transaction: t }
392
393 videoInstance.set('name', videoAttributesToUpdate.name)
394 videoInstance.set('category', videoAttributesToUpdate.category)
395 videoInstance.set('description', videoAttributesToUpdate.description)
396 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
397 videoInstance.set('duration', videoAttributesToUpdate.duration)
398 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
399 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
400 videoInstance.set('extname', videoAttributesToUpdate.extname)
401 videoInstance.set('views', videoAttributesToUpdate.views)
402 videoInstance.set('likes', videoAttributesToUpdate.likes)
403 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
404
405 videoInstance.save(options).asCallback(function (err) {
406 return callback(err, t, videoInstance, tagInstances)
407 })
408 },
409
410 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
411 const options = { transaction: t }
412
413 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
414 return callback(err, t)
415 })
416 },
417
418 databaseUtils.commitTransaction
419
420 ], function (err, t) {
421 if (err) {
422 // This is just a debug because we will retry the insert
423 logger.debug('Cannot update the remote video.', { error: err })
424 return databaseUtils.rollbackTransaction(err, t, finalCallback)
425 }
426
427 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
428 return finalCallback(null)
429 })
430 }
431
432 function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
433 // We need the instance because we have to remove some other stuffs (thumbnail etc)
434 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
435 // Do not return the error, continue the process
436 if (err) return callback(null)
437
438 logger.debug('Removing remote video %s.', video.remoteId)
439 video.destroy().asCallback(function (err) {
440 // Do not return the error, continue the process
441 if (err) {
442 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
443 }
444
445 return callback(null)
446 })
447 })
448 }
449
450 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
451 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
452 if (err || !video) {
453 if (!err) err = new Error('video not found')
454
455 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
456 // Do not return the error, continue the process
457 return callback(null)
458 }
459
460 logger.debug('Reporting remote abuse for video %s.', video.id)
461
462 const videoAbuseData = {
463 reporterUsername: reportData.reporterUsername,
464 reason: reportData.reportReason,
465 reporterPodId: fromPod.id,
466 videoId: video.id
467 }
468
469 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
470 if (err) {
471 logger.error('Cannot create remote abuse video.', { error: err })
472 }
473
474 return callback(null)
475 })
476 })
477 }
478
479 function fetchOwnedVideo (id, callback) {
480 db.Video.load(id, function (err, video) {
481 if (err || !video) {
482 if (!err) err = new Error('video not found')
483
484 logger.error('Cannot load owned video from id.', { error: err, id })
485 return callback(err)
486 }
487
488 return callback(null, video)
489 })
490 }
491
492 function fetchRemoteVideo (podHost, remoteId, callback) {
493 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
494 if (err || !video) {
495 if (!err) err = new Error('video not found')
496
497 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
498 return callback(err)
499 }
500
501 return callback(null, video)
502 })
503 }