]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.ts
bf442c6e58627ba66c73507cfbc24285cd7a719c
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
1 import * as express from 'express'
2 import * as Bluebird from 'bluebird'
3 import * as Sequelize from 'sequelize'
4
5 import { database as db } from '../../../initializers/database'
6 import {
7 REQUEST_ENDPOINT_ACTIONS,
8 REQUEST_ENDPOINTS,
9 REQUEST_VIDEO_EVENT_TYPES,
10 REQUEST_VIDEO_QADU_TYPES
11 } from '../../../initializers'
12 import {
13 checkSignature,
14 signatureValidator,
15 remoteVideosValidator,
16 remoteQaduVideosValidator,
17 remoteEventsVideosValidator
18 } from '../../../middlewares'
19 import { logger, retryTransactionWrapper } from '../../../helpers'
20 import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib'
21 import { PodInstance, VideoFileInstance } from '../../../models'
22 import {
23 RemoteVideoRequest,
24 RemoteVideoCreateData,
25 RemoteVideoUpdateData,
26 RemoteVideoRemoveData,
27 RemoteVideoReportAbuseData,
28 RemoteQaduVideoRequest,
29 RemoteQaduVideoData,
30 RemoteVideoEventRequest,
31 RemoteVideoEventData,
32 RemoteVideoChannelCreateData,
33 RemoteVideoChannelUpdateData,
34 RemoteVideoChannelRemoveData,
35 RemoteVideoAuthorRemoveData,
36 RemoteVideoAuthorCreateData
37 } from '../../../../shared'
38
39 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
40
41 // Functions to call when processing a remote request
42 // FIXME: use RemoteVideoRequestType as id type
43 const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
44 functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper
45 functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper
46 functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper
47 functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper
48 functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper
49 functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper
50 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper
51 functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper
52 functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper
53
54 const remoteVideosRouter = express.Router()
55
56 remoteVideosRouter.post('/',
57 signatureValidator,
58 checkSignature,
59 remoteVideosValidator,
60 remoteVideos
61 )
62
63 remoteVideosRouter.post('/qadu',
64 signatureValidator,
65 checkSignature,
66 remoteQaduVideosValidator,
67 remoteVideosQadu
68 )
69
70 remoteVideosRouter.post('/events',
71 signatureValidator,
72 checkSignature,
73 remoteEventsVideosValidator,
74 remoteVideosEvents
75 )
76
77 // ---------------------------------------------------------------------------
78
79 export {
80 remoteVideosRouter
81 }
82
83 // ---------------------------------------------------------------------------
84
85 function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
86 const requests: RemoteVideoRequest[] = req.body.data
87 const fromPod = res.locals.secure.pod
88
89 // We need to process in the same order to keep consistency
90 Bluebird.each(requests, request => {
91 const data = request.data
92
93 // Get the function we need to call in order to process the request
94 const fun = functionsHash[request.type]
95 if (fun === undefined) {
96 logger.error('Unknown remote request type %s.', request.type)
97 return
98 }
99
100 return fun.call(this, data, fromPod)
101 })
102 .catch(err => logger.error('Error managing remote videos.', err))
103
104 // Don't block the other pod
105 return res.type('json').status(204).end()
106 }
107
108 function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
109 const requests: RemoteQaduVideoRequest[] = req.body.data
110 const fromPod = res.locals.secure.pod
111
112 Bluebird.each(requests, request => {
113 const videoData = request.data
114
115 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
116 })
117 .catch(err => logger.error('Error managing remote videos.', err))
118
119 return res.type('json').status(204).end()
120 }
121
122 function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
123 const requests: RemoteVideoEventRequest[] = req.body.data
124 const fromPod = res.locals.secure.pod
125
126 Bluebird.each(requests, request => {
127 const eventData = request.data
128
129 return processVideosEventsRetryWrapper(eventData, fromPod)
130 })
131 .catch(err => logger.error('Error managing remote videos.', err))
132
133 return res.type('json').status(204).end()
134 }
135
136 async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
137 const options = {
138 arguments: [ eventData, fromPod ],
139 errorMessage: 'Cannot process videos events with many retries.'
140 }
141
142 await retryTransactionWrapper(processVideosEvents, options)
143 }
144
145 async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
146 await db.sequelize.transaction(async t => {
147 const sequelizeOptions = { transaction: t }
148 const videoInstance = await fetchVideoByUUID(eventData.uuid, t)
149
150 let columnToUpdate
151 let qaduType
152
153 switch (eventData.eventType) {
154 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
155 columnToUpdate = 'views'
156 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
157 break
158
159 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
160 columnToUpdate = 'likes'
161 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
162 break
163
164 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
165 columnToUpdate = 'dislikes'
166 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
167 break
168
169 default:
170 throw new Error('Unknown video event type.')
171 }
172
173 const query = {}
174 query[columnToUpdate] = eventData.count
175
176 await videoInstance.increment(query, sequelizeOptions)
177
178 const qadusParams = [
179 {
180 videoId: videoInstance.id,
181 type: qaduType
182 }
183 ]
184 await quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
185 })
186
187 logger.info('Remote video event processed for video with uuid %s.', eventData.uuid)
188 }
189
190 async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
191 const options = {
192 arguments: [ videoData, fromPod ],
193 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
194 }
195
196 await retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
197 }
198
199 async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
200 let videoUUID = ''
201
202 await db.sequelize.transaction(async t => {
203 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
204 const sequelizeOptions = { transaction: t }
205
206 videoUUID = videoInstance.uuid
207
208 if (videoData.views) {
209 videoInstance.set('views', videoData.views)
210 }
211
212 if (videoData.likes) {
213 videoInstance.set('likes', videoData.likes)
214 }
215
216 if (videoData.dislikes) {
217 videoInstance.set('dislikes', videoData.dislikes)
218 }
219
220 await videoInstance.save(sequelizeOptions)
221 })
222
223 logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
224 }
225
226 // Handle retries on fail
227 async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
228 const options = {
229 arguments: [ videoToCreateData, fromPod ],
230 errorMessage: 'Cannot insert the remote video with many retries.'
231 }
232
233 await retryTransactionWrapper(addRemoteVideo, options)
234 }
235
236 async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
237 logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
238
239 await db.sequelize.transaction(async t => {
240 const sequelizeOptions = {
241 transaction: t
242 }
243
244 const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid)
245 if (videoFromDatabase) throw new Error('UUID already exists.')
246
247 const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
248 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
249
250 const tags = videoToCreateData.tags
251 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
252
253 const videoData = {
254 name: videoToCreateData.name,
255 uuid: videoToCreateData.uuid,
256 category: videoToCreateData.category,
257 licence: videoToCreateData.licence,
258 language: videoToCreateData.language,
259 nsfw: videoToCreateData.nsfw,
260 description: videoToCreateData.description,
261 channelId: videoChannel.id,
262 duration: videoToCreateData.duration,
263 createdAt: videoToCreateData.createdAt,
264 // FIXME: updatedAt does not seems to be considered by Sequelize
265 updatedAt: videoToCreateData.updatedAt,
266 views: videoToCreateData.views,
267 likes: videoToCreateData.likes,
268 dislikes: videoToCreateData.dislikes,
269 remote: true
270 }
271
272 const video = db.Video.build(videoData)
273 await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
274 const videoCreated = await video.save(sequelizeOptions)
275
276 const tasks = []
277 for (const fileData of videoToCreateData.files) {
278 const videoFileInstance = db.VideoFile.build({
279 extname: fileData.extname,
280 infoHash: fileData.infoHash,
281 resolution: fileData.resolution,
282 size: fileData.size,
283 videoId: videoCreated.id
284 })
285
286 tasks.push(videoFileInstance.save(sequelizeOptions))
287 }
288
289 await Promise.all(tasks)
290
291 await videoCreated.setTags(tagInstances, sequelizeOptions)
292 })
293
294 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
295 }
296
297 // Handle retries on fail
298 async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
299 const options = {
300 arguments: [ videoAttributesToUpdate, fromPod ],
301 errorMessage: 'Cannot update the remote video with many retries'
302 }
303
304 await retryTransactionWrapper(updateRemoteVideo, options)
305 }
306
307 async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
308 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
309
310 try {
311 await db.sequelize.transaction(async t => {
312 const sequelizeOptions = {
313 transaction: t
314 }
315
316 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t)
317 const tags = videoAttributesToUpdate.tags
318
319 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
320
321 videoInstance.set('name', videoAttributesToUpdate.name)
322 videoInstance.set('category', videoAttributesToUpdate.category)
323 videoInstance.set('licence', videoAttributesToUpdate.licence)
324 videoInstance.set('language', videoAttributesToUpdate.language)
325 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
326 videoInstance.set('description', videoAttributesToUpdate.description)
327 videoInstance.set('duration', videoAttributesToUpdate.duration)
328 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
329 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
330 videoInstance.set('views', videoAttributesToUpdate.views)
331 videoInstance.set('likes', videoAttributesToUpdate.likes)
332 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
333
334 await videoInstance.save(sequelizeOptions)
335
336 // Remove old video files
337 const videoFileDestroyTasks: Bluebird<void>[] = []
338 for (const videoFile of videoInstance.VideoFiles) {
339 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
340 }
341 await Promise.all(videoFileDestroyTasks)
342
343 const videoFileCreateTasks: Bluebird<VideoFileInstance>[] = []
344 for (const fileData of videoAttributesToUpdate.files) {
345 const videoFileInstance = db.VideoFile.build({
346 extname: fileData.extname,
347 infoHash: fileData.infoHash,
348 resolution: fileData.resolution,
349 size: fileData.size,
350 videoId: videoInstance.id
351 })
352
353 videoFileCreateTasks.push(videoFileInstance.save(sequelizeOptions))
354 }
355
356 await Promise.all(videoFileCreateTasks)
357
358 await videoInstance.setTags(tagInstances, sequelizeOptions)
359 })
360
361 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
362 } catch (err) {
363 // This is just a debug because we will retry the insert
364 logger.debug('Cannot update the remote video.', err)
365 throw err
366 }
367 }
368
369 async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
370 const options = {
371 arguments: [ videoToRemoveData, fromPod ],
372 errorMessage: 'Cannot remove the remote video channel with many retries.'
373 }
374
375 await retryTransactionWrapper(removeRemoteVideo, options)
376 }
377
378 async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
379 logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
380
381 await db.sequelize.transaction(async t => {
382 // We need the instance because we have to remove some other stuffs (thumbnail etc)
383 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
384 await videoInstance.destroy({ transaction: t })
385 })
386
387 logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
388 }
389
390 async function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
391 const options = {
392 arguments: [ authorToCreateData, fromPod ],
393 errorMessage: 'Cannot insert the remote video author with many retries.'
394 }
395
396 await retryTransactionWrapper(addRemoteVideoAuthor, options)
397 }
398
399 async function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
400 logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
401
402 await db.sequelize.transaction(async t => {
403 const authorInDatabase = await db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
404 if (authorInDatabase) throw new Error('Author with UUID ' + authorToCreateData.uuid + ' already exists.')
405
406 const videoAuthorData = {
407 name: authorToCreateData.name,
408 uuid: authorToCreateData.uuid,
409 userId: null, // Not on our pod
410 podId: fromPod.id
411 }
412
413 const author = db.Author.build(videoAuthorData)
414 await author.save({ transaction: t })
415 })
416
417 logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid)
418 }
419
420 async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
421 const options = {
422 arguments: [ authorAttributesToRemove, fromPod ],
423 errorMessage: 'Cannot remove the remote video author with many retries.'
424 }
425
426 await retryTransactionWrapper(removeRemoteVideoAuthor, options)
427 }
428
429 async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
430 logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
431
432 await db.sequelize.transaction(async t => {
433 const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
434 await videoAuthor.destroy({ transaction: t })
435 })
436
437 logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)
438 }
439
440 async function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
441 const options = {
442 arguments: [ videoChannelToCreateData, fromPod ],
443 errorMessage: 'Cannot insert the remote video channel with many retries.'
444 }
445
446 await retryTransactionWrapper(addRemoteVideoChannel, options)
447 }
448
449 async function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
450 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
451
452 await db.sequelize.transaction(async t => {
453 const videoChannelInDatabase = await db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
454 if (videoChannelInDatabase) {
455 throw new Error('Video channel with UUID ' + videoChannelToCreateData.uuid + ' already exists.')
456 }
457
458 const authorUUID = videoChannelToCreateData.ownerUUID
459 const podId = fromPod.id
460
461 const author = await db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
462 if (!author) throw new Error('Unknown author UUID' + authorUUID + '.')
463
464 const videoChannelData = {
465 name: videoChannelToCreateData.name,
466 description: videoChannelToCreateData.description,
467 uuid: videoChannelToCreateData.uuid,
468 createdAt: videoChannelToCreateData.createdAt,
469 updatedAt: videoChannelToCreateData.updatedAt,
470 remote: true,
471 authorId: author.id
472 }
473
474 const videoChannel = db.VideoChannel.build(videoChannelData)
475 await videoChannel.save({ transaction: t })
476 })
477
478 logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
479 }
480
481 async function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
482 const options = {
483 arguments: [ videoChannelAttributesToUpdate, fromPod ],
484 errorMessage: 'Cannot update the remote video channel with many retries.'
485 }
486
487 await retryTransactionWrapper(updateRemoteVideoChannel, options)
488 }
489
490 async function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
491 logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
492
493 await db.sequelize.transaction(async t => {
494 const sequelizeOptions = { transaction: t }
495
496 const videoChannelInstance = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
497 videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
498 videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
499 videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
500 videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
501
502 await videoChannelInstance.save(sequelizeOptions)
503 })
504
505 logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid)
506 }
507
508 async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
509 const options = {
510 arguments: [ videoChannelAttributesToRemove, fromPod ],
511 errorMessage: 'Cannot remove the remote video channel with many retries.'
512 }
513
514 await retryTransactionWrapper(removeRemoteVideoChannel, options)
515 }
516
517 async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
518 logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
519
520 await db.sequelize.transaction(async t => {
521 const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
522 await videoChannel.destroy({ transaction: t })
523 })
524
525 logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
526 }
527
528 async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
529 const options = {
530 arguments: [ reportData, fromPod ],
531 errorMessage: 'Cannot create remote abuse video with many retries.'
532 }
533
534 await retryTransactionWrapper(reportAbuseRemoteVideo, options)
535 }
536
537 async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
538 logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
539
540 await db.sequelize.transaction(async t => {
541 const videoInstance = await fetchVideoByUUID(reportData.videoUUID, t)
542 const videoAbuseData = {
543 reporterUsername: reportData.reporterUsername,
544 reason: reportData.reportReason,
545 reporterPodId: fromPod.id,
546 videoId: videoInstance.id
547 }
548
549 await db.VideoAbuse.create(videoAbuseData)
550
551 })
552
553 logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)
554 }
555
556 async function fetchVideoByUUID (id: string, t: Sequelize.Transaction) {
557 try {
558 const video = await db.Video.loadByUUID(id, t)
559
560 if (!video) throw new Error('Video ' + id + ' not found')
561
562 return video
563 } catch (err) {
564 logger.error('Cannot load owned video from id.', { error: err.stack, id })
565 throw err
566 }
567 }
568
569 async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
570 try {
571 const video = await db.Video.loadByHostAndUUID(podHost, uuid, t)
572 if (!video) throw new Error('Video not found')
573
574 return video
575 } catch (err) {
576 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
577 throw err
578 }
579 }