]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.ts
Type functions
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
CommitLineData
4d4e5cd4 1import * as express from 'express'
69818c93 2import * as Sequelize from 'sequelize'
65fcc311 3import { eachSeries, waterfall } from 'async'
528a9efa 4
e02643f3 5import { database as db } from '../../../initializers/database'
65fcc311
C
6import {
7 REQUEST_ENDPOINT_ACTIONS,
8 REQUEST_ENDPOINTS,
9 REQUEST_VIDEO_EVENT_TYPES,
10 REQUEST_VIDEO_QADU_TYPES
11} from '../../../initializers'
12import {
13 checkSignature,
14 signatureValidator,
15 remoteVideosValidator,
16 remoteQaduVideosValidator,
17 remoteEventsVideosValidator
18} from '../../../middlewares'
19import {
20 logger,
21 commitTransaction,
22 retryTransactionWrapper,
23 rollbackTransaction,
24 startSerializableTransaction
25} from '../../../helpers'
26import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
69818c93 27import { PodInstance, VideoInstance } from '../../../models'
65fcc311
C
28
29const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
62f4ef41
C
30
31// Functions to call when processing a remote request
32const functionsHash = {}
33functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
34functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
35functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
36functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
37
65fcc311 38const remoteVideosRouter = express.Router()
528a9efa 39
65fcc311
C
40remoteVideosRouter.post('/',
41 signatureValidator,
42 checkSignature,
43 remoteVideosValidator,
528a9efa
C
44 remoteVideos
45)
46
65fcc311
C
47remoteVideosRouter.post('/qadu',
48 signatureValidator,
49 checkSignature,
50 remoteQaduVideosValidator,
9e167724
C
51 remoteVideosQadu
52)
53
65fcc311
C
54remoteVideosRouter.post('/events',
55 signatureValidator,
56 checkSignature,
57 remoteEventsVideosValidator,
e4c87ec2
C
58 remoteVideosEvents
59)
60
528a9efa
C
61// ---------------------------------------------------------------------------
62
65fcc311
C
63export {
64 remoteVideosRouter
65}
528a9efa
C
66
67// ---------------------------------------------------------------------------
68
69818c93 69function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
528a9efa 70 const requests = req.body.data
4ff0d862 71 const fromPod = res.locals.secure.pod
528a9efa
C
72
73 // We need to process in the same order to keep consistency
74 // TODO: optimization
65fcc311 75 eachSeries(requests, function (request: any, callbackEach) {
55fa55a9 76 const data = request.data
528a9efa 77
62f4ef41
C
78 // Get the function we need to call in order to process the request
79 const fun = functionsHash[request.type]
80 if (fun === undefined) {
81 logger.error('Unkown remote request type %s.', request.type)
82 return callbackEach(null)
528a9efa 83 }
62f4ef41
C
84
85 fun.call(this, data, fromPod, callbackEach)
aaf61f38
C
86 }, function (err) {
87 if (err) logger.error('Error managing remote videos.', { error: err })
528a9efa
C
88 })
89
90 // We don't need to keep the other pod waiting
91 return res.type('json').status(204).end()
92}
93
69818c93 94function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
9e167724
C
95 const requests = req.body.data
96 const fromPod = res.locals.secure.pod
97
65fcc311 98 eachSeries(requests, function (request: any, callbackEach) {
9e167724
C
99 const videoData = request.data
100
101 quickAndDirtyUpdateVideoRetryWrapper(videoData, 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
69818c93 109function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
e4c87ec2
C
110 const requests = req.body.data
111 const fromPod = res.locals.secure.pod
112
65fcc311 113 eachSeries(requests, function (request: any, callbackEach) {
e4c87ec2
C
114 const eventData = request.data
115
116 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
117 }, function (err) {
118 if (err) logger.error('Error managing remote videos.', { error: err })
119 })
120
121 return res.type('json').status(204).end()
122}
123
69818c93 124function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
e4c87ec2
C
125 const options = {
126 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.'
128 }
129
65fcc311 130 retryTransactionWrapper(processVideosEvents, options, finalCallback)
e4c87ec2
C
131}
132
69818c93 133function processVideosEvents (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
e4c87ec2 134 waterfall([
65fcc311 135 startSerializableTransaction,
e4c87ec2
C
136
137 function findVideo (t, callback) {
138 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
139 return callback(err, t, videoInstance)
140 })
141 },
142
143 function updateVideoIntoDB (t, videoInstance, callback) {
144 const options = { transaction: t }
145
146 let columnToUpdate
d38b8281 147 let qaduType
e4c87ec2
C
148
149 switch (eventData.eventType) {
65fcc311 150 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
e4c87ec2 151 columnToUpdate = 'views'
65fcc311 152 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
e4c87ec2
C
153 break
154
65fcc311 155 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
e4c87ec2 156 columnToUpdate = 'likes'
65fcc311 157 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
e4c87ec2
C
158 break
159
65fcc311 160 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
e4c87ec2 161 columnToUpdate = 'dislikes'
65fcc311 162 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
e4c87ec2
C
163 break
164
165 default:
166 return callback(new Error('Unknown video event type.'))
167 }
168
169 const query = {}
170 query[columnToUpdate] = eventData.count
171
172 videoInstance.increment(query, options).asCallback(function (err) {
d38b8281
C
173 return callback(err, t, videoInstance, qaduType)
174 })
175 },
176
177 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
178 const qadusParams = [
179 {
180 videoId: videoInstance.id,
181 type: qaduType
182 }
183 ]
184
65fcc311 185 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
e4c87ec2
C
186 return callback(err, t)
187 })
188 },
189
65fcc311 190 commitTransaction
e4c87ec2 191
69818c93 192 ], function (err: Error, t: Sequelize.Transaction) {
e4c87ec2 193 if (err) {
e4c87ec2 194 logger.debug('Cannot process a video event.', { error: err })
65fcc311 195 return rollbackTransaction(err, t, finalCallback)
e4c87ec2
C
196 }
197
198 logger.info('Remote video event processed for video %s.', eventData.remoteId)
199 return finalCallback(null)
200 })
201}
202
69818c93 203function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
9e167724
C
204 const options = {
205 arguments: [ videoData, fromPod ],
206 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
207 }
208
65fcc311 209 retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
9e167724
C
210}
211
69818c93 212function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
f148e5ed
C
213 let videoName
214
9e167724 215 waterfall([
65fcc311 216 startSerializableTransaction,
9e167724
C
217
218 function findVideo (t, callback) {
e4c87ec2 219 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
9e167724
C
220 return callback(err, t, videoInstance)
221 })
222 },
223
224 function updateVideoIntoDB (t, videoInstance, callback) {
225 const options = { transaction: t }
226
f148e5ed
C
227 videoName = videoInstance.name
228
9e167724
C
229 if (videoData.views) {
230 videoInstance.set('views', videoData.views)
231 }
232
233 if (videoData.likes) {
234 videoInstance.set('likes', videoData.likes)
235 }
236
237 if (videoData.dislikes) {
238 videoInstance.set('dislikes', videoData.dislikes)
239 }
240
241 videoInstance.save(options).asCallback(function (err) {
242 return callback(err, t)
243 })
244 },
245
65fcc311 246 commitTransaction
9e167724 247
69818c93 248 ], function (err: Error, t: Sequelize.Transaction) {
9e167724
C
249 if (err) {
250 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
65fcc311 251 return rollbackTransaction(err, t, finalCallback)
9e167724
C
252 }
253
f148e5ed 254 logger.info('Remote video %s quick and dirty updated', videoName)
9e167724
C
255 return finalCallback(null)
256 })
257}
258
ed04d94f 259// Handle retries on fail
69818c93 260function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
d6a5b018
C
261 const options = {
262 arguments: [ videoToCreateData, fromPod ],
263 errorMessage: 'Cannot insert the remote video with many retries.'
264 }
ed04d94f 265
65fcc311 266 retryTransactionWrapper(addRemoteVideo, options, finalCallback)
ed04d94f
C
267}
268
69818c93 269function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
ed04d94f 270 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
6666aad4 271
feb4bdfd
C
272 waterfall([
273
65fcc311 274 startSerializableTransaction,
7920c273 275
cddadde8
C
276 function assertRemoteIdAndHostUnique (t, callback) {
277 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
278 if (err) return callback(err)
279
280 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
281
282 return callback(null, t)
283 })
284 },
285
4ff0d862
C
286 function findOrCreateAuthor (t, callback) {
287 const name = videoToCreateData.author
288 const podId = fromPod.id
289 // This author is from another pod so we do not associate a user
290 const userId = null
feb4bdfd 291
4ff0d862
C
292 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
293 return callback(err, t, authorInstance)
feb4bdfd
C
294 })
295 },
296
7920c273
C
297 function findOrCreateTags (t, author, callback) {
298 const tags = videoToCreateData.tags
7920c273 299
4ff0d862 300 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
7920c273
C
301 return callback(err, t, author, tagInstances)
302 })
303 },
304
305 function createVideoObject (t, author, tagInstances, callback) {
feb4bdfd
C
306 const videoData = {
307 name: videoToCreateData.name,
308 remoteId: videoToCreateData.remoteId,
309 extname: videoToCreateData.extname,
310 infoHash: videoToCreateData.infoHash,
6e07c3de 311 category: videoToCreateData.category,
6f0c39e2 312 licence: videoToCreateData.licence,
3092476e 313 language: videoToCreateData.language,
31b59b47 314 nsfw: videoToCreateData.nsfw,
feb4bdfd
C
315 description: videoToCreateData.description,
316 authorId: author.id,
124648d7 317 duration: videoToCreateData.duration,
79066fdf 318 createdAt: videoToCreateData.createdAt,
ed04d94f 319 // FIXME: updatedAt does not seems to be considered by Sequelize
d38b8281
C
320 updatedAt: videoToCreateData.updatedAt,
321 views: videoToCreateData.views,
322 likes: videoToCreateData.likes,
323 dislikes: videoToCreateData.dislikes
feb4bdfd
C
324 }
325
326 const video = db.Video.build(videoData)
327
7920c273 328 return callback(null, t, tagInstances, video)
feb4bdfd
C
329 },
330
7920c273 331 function generateThumbnail (t, tagInstances, video, callback) {
4d324488 332 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
feb4bdfd 333 if (err) {
4d324488 334 logger.error('Cannot generate thumbnail from data.', { error: err })
feb4bdfd
C
335 return callback(err)
336 }
337
7920c273 338 return callback(err, t, tagInstances, video)
feb4bdfd
C
339 })
340 },
341
7920c273
C
342 function insertVideoIntoDB (t, tagInstances, video, callback) {
343 const options = {
344 transaction: t
345 }
346
347 video.save(options).asCallback(function (err, videoCreated) {
348 return callback(err, t, tagInstances, videoCreated)
349 })
350 },
351
352 function associateTagsToVideo (t, tagInstances, video, callback) {
62f4ef41
C
353 const options = {
354 transaction: t
355 }
7920c273
C
356
357 video.setTags(tagInstances, options).asCallback(function (err) {
358 return callback(err, t)
359 })
4145c1c6
C
360 },
361
65fcc311 362 commitTransaction
c77fa067 363
69818c93 364 ], function (err: Error, t: Sequelize.Transaction) {
7920c273 365 if (err) {
ed04d94f
C
366 // This is just a debug because we will retry the insert
367 logger.debug('Cannot insert the remote video.', { error: err })
65fcc311 368 return rollbackTransaction(err, t, finalCallback)
7920c273
C
369 }
370
4145c1c6
C
371 logger.info('Remote video %s inserted.', videoToCreateData.name)
372 return finalCallback(null)
7920c273 373 })
528a9efa
C
374}
375
ed04d94f 376// Handle retries on fail
69818c93 377function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
d6a5b018 378 const options = {
fbc22d79 379 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
380 errorMessage: 'Cannot update the remote video with many retries'
381 }
ed04d94f 382
65fcc311 383 retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
ed04d94f
C
384}
385
69818c93 386function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) {
ed04d94f 387 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
feb4bdfd 388
3d118fb5
C
389 waterfall([
390
65fcc311 391 startSerializableTransaction,
3d118fb5
C
392
393 function findVideo (t, callback) {
e4c87ec2 394 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
55fa55a9 395 return callback(err, t, videoInstance)
3d118fb5
C
396 })
397 },
398
399 function findOrCreateTags (t, videoInstance, callback) {
400 const tags = videoAttributesToUpdate.tags
401
402 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
403 return callback(err, t, videoInstance, tagInstances)
404 })
405 },
406
407 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
408 const options = { transaction: t }
409
410 videoInstance.set('name', videoAttributesToUpdate.name)
6e07c3de 411 videoInstance.set('category', videoAttributesToUpdate.category)
6f0c39e2 412 videoInstance.set('licence', videoAttributesToUpdate.licence)
3092476e 413 videoInstance.set('language', videoAttributesToUpdate.language)
31b59b47 414 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
3d118fb5
C
415 videoInstance.set('description', videoAttributesToUpdate.description)
416 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
417 videoInstance.set('duration', videoAttributesToUpdate.duration)
418 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
79066fdf 419 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
3d118fb5 420 videoInstance.set('extname', videoAttributesToUpdate.extname)
d38b8281
C
421 videoInstance.set('views', videoAttributesToUpdate.views)
422 videoInstance.set('likes', videoAttributesToUpdate.likes)
423 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
3d118fb5
C
424
425 videoInstance.save(options).asCallback(function (err) {
426 return callback(err, t, videoInstance, tagInstances)
427 })
428 },
429
430 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
431 const options = { transaction: t }
432
433 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
434 return callback(err, t)
435 })
4145c1c6
C
436 },
437
65fcc311 438 commitTransaction
528a9efa 439
69818c93 440 ], function (err: Error, t: Sequelize.Transaction) {
3d118fb5 441 if (err) {
ed04d94f
C
442 // This is just a debug because we will retry the insert
443 logger.debug('Cannot update the remote video.', { error: err })
65fcc311 444 return rollbackTransaction(err, t, finalCallback)
6666aad4
C
445 }
446
4145c1c6
C
447 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
448 return finalCallback(null)
3d118fb5
C
449 })
450}
451
69818c93 452function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance, callback: (err: Error) => void) {
3d118fb5 453 // We need the instance because we have to remove some other stuffs (thumbnail etc)
e4c87ec2 454 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
d8cc063e
C
455 // Do not return the error, continue the process
456 if (err) return callback(null)
55fa55a9
C
457
458 logger.debug('Removing remote video %s.', video.remoteId)
d8cc063e
C
459 video.destroy().asCallback(function (err) {
460 // Do not return the error, continue the process
461 if (err) {
462 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
463 }
464
465 return callback(null)
466 })
55fa55a9
C
467 })
468}
469
69818c93 470function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance, callback: (err: Error) => void) {
e4c87ec2 471 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
3d118fb5 472 if (err || !video) {
55fa55a9
C
473 if (!err) err = new Error('video not found')
474
ed04d94f 475 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
d8cc063e
C
476 // Do not return the error, continue the process
477 return callback(null)
3d118fb5 478 }
6666aad4 479
55fa55a9
C
480 logger.debug('Reporting remote abuse for video %s.', video.id)
481
482 const videoAbuseData = {
483 reporterUsername: reportData.reporterUsername,
484 reason: reportData.reportReason,
485 reporterPodId: fromPod.id,
486 videoId: video.id
487 }
488
d8cc063e
C
489 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
490 if (err) {
491 logger.error('Cannot create remote abuse video.', { error: err })
492 }
493
494 return callback(null)
495 })
55fa55a9
C
496 })
497}
498
69818c93 499function fetchOwnedVideo (id: string, callback: (err: Error, video?: VideoInstance) => void) {
e4c87ec2
C
500 db.Video.load(id, function (err, video) {
501 if (err || !video) {
502 if (!err) err = new Error('video not found')
503
504 logger.error('Cannot load owned video from id.', { error: err, id })
505 return callback(err)
506 }
507
508 return callback(null, video)
509 })
510}
511
69818c93 512function fetchRemoteVideo (podHost: string, remoteId: string, callback: (err: Error, video?: VideoInstance) => void) {
55fa55a9
C
513 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
514 if (err || !video) {
515 if (!err) err = new Error('video not found')
516
ed04d94f 517 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
55fa55a9
C
518 return callback(err)
519 }
520
521 return callback(null, video)
528a9efa
C
522 })
523}