]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/controllers/api/remote/videos.js
Server: add licence video attribute
[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 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
246function 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
255function 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 licence: videoToCreateData.licence,
299 description: videoToCreateData.description,
300 authorId: author.id,
301 duration: videoToCreateData.duration,
302 createdAt: videoToCreateData.createdAt,
303 // FIXME: updatedAt does not seems to be considered by Sequelize
304 updatedAt: videoToCreateData.updatedAt,
305 views: videoToCreateData.views,
306 likes: videoToCreateData.likes,
307 dislikes: videoToCreateData.dislikes
308 }
309
310 const video = db.Video.build(videoData)
311
312 return callback(null, t, tagInstances, video)
313 },
314
315 function generateThumbnail (t, tagInstances, video, callback) {
316 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
317 if (err) {
318 logger.error('Cannot generate thumbnail from data.', { error: err })
319 return callback(err)
320 }
321
322 return callback(err, t, tagInstances, video)
323 })
324 },
325
326 function insertVideoIntoDB (t, tagInstances, video, callback) {
327 const options = {
328 transaction: t
329 }
330
331 video.save(options).asCallback(function (err, videoCreated) {
332 return callback(err, t, tagInstances, videoCreated)
333 })
334 },
335
336 function associateTagsToVideo (t, tagInstances, video, callback) {
337 const options = {
338 transaction: t
339 }
340
341 video.setTags(tagInstances, options).asCallback(function (err) {
342 return callback(err, t)
343 })
344 },
345
346 databaseUtils.commitTransaction
347
348 ], function (err, t) {
349 if (err) {
350 // This is just a debug because we will retry the insert
351 logger.debug('Cannot insert the remote video.', { error: err })
352 return databaseUtils.rollbackTransaction(err, t, finalCallback)
353 }
354
355 logger.info('Remote video %s inserted.', videoToCreateData.name)
356 return finalCallback(null)
357 })
358}
359
360// Handle retries on fail
361function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
362 const options = {
363 arguments: [ videoAttributesToUpdate, fromPod ],
364 errorMessage: 'Cannot update the remote video with many retries'
365 }
366
367 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
368}
369
370function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
371 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
372
373 waterfall([
374
375 databaseUtils.startSerializableTransaction,
376
377 function findVideo (t, callback) {
378 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
379 return callback(err, t, videoInstance)
380 })
381 },
382
383 function findOrCreateTags (t, videoInstance, callback) {
384 const tags = videoAttributesToUpdate.tags
385
386 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
387 return callback(err, t, videoInstance, tagInstances)
388 })
389 },
390
391 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
392 const options = { transaction: t }
393
394 videoInstance.set('name', videoAttributesToUpdate.name)
395 videoInstance.set('category', videoAttributesToUpdate.category)
396 videoInstance.set('licence', videoAttributesToUpdate.licence)
397 videoInstance.set('description', videoAttributesToUpdate.description)
398 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
399 videoInstance.set('duration', videoAttributesToUpdate.duration)
400 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
401 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
402 videoInstance.set('extname', videoAttributesToUpdate.extname)
403 videoInstance.set('views', videoAttributesToUpdate.views)
404 videoInstance.set('likes', videoAttributesToUpdate.likes)
405 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
406
407 videoInstance.save(options).asCallback(function (err) {
408 return callback(err, t, videoInstance, tagInstances)
409 })
410 },
411
412 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
413 const options = { transaction: t }
414
415 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
416 return callback(err, t)
417 })
418 },
419
420 databaseUtils.commitTransaction
421
422 ], function (err, t) {
423 if (err) {
424 // This is just a debug because we will retry the insert
425 logger.debug('Cannot update the remote video.', { error: err })
426 return databaseUtils.rollbackTransaction(err, t, finalCallback)
427 }
428
429 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
430 return finalCallback(null)
431 })
432}
433
434function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
435 // We need the instance because we have to remove some other stuffs (thumbnail etc)
436 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
437 // Do not return the error, continue the process
438 if (err) return callback(null)
439
440 logger.debug('Removing remote video %s.', video.remoteId)
441 video.destroy().asCallback(function (err) {
442 // Do not return the error, continue the process
443 if (err) {
444 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
445 }
446
447 return callback(null)
448 })
449 })
450}
451
452function reportAbuseRemoteVideo (reportData, fromPod, callback) {
453 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
454 if (err || !video) {
455 if (!err) err = new Error('video not found')
456
457 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
458 // Do not return the error, continue the process
459 return callback(null)
460 }
461
462 logger.debug('Reporting remote abuse for video %s.', video.id)
463
464 const videoAbuseData = {
465 reporterUsername: reportData.reporterUsername,
466 reason: reportData.reportReason,
467 reporterPodId: fromPod.id,
468 videoId: video.id
469 }
470
471 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
472 if (err) {
473 logger.error('Cannot create remote abuse video.', { error: err })
474 }
475
476 return callback(null)
477 })
478 })
479}
480
481function fetchOwnedVideo (id, callback) {
482 db.Video.load(id, function (err, video) {
483 if (err || !video) {
484 if (!err) err = new Error('video not found')
485
486 logger.error('Cannot load owned video from id.', { error: err, id })
487 return callback(err)
488 }
489
490 return callback(null, video)
491 })
492}
493
494function fetchRemoteVideo (podHost, remoteId, callback) {
495 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
496 if (err || !video) {
497 if (!err) err = new Error('video not found')
498
499 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
500 return callback(err)
501 }
502
503 return callback(null, video)
504 })
505}