1 import * as express from 'express'
2 import * as Promise from 'bluebird'
3 import * as Sequelize from 'sequelize'
5 import { database as db } from '../../../initializers/database'
7 REQUEST_ENDPOINT_ACTIONS,
9 REQUEST_VIDEO_EVENT_TYPES,
10 REQUEST_VIDEO_QADU_TYPES
11 } from '../../../initializers'
15 remoteVideosValidator,
16 remoteQaduVideosValidator,
17 remoteEventsVideosValidator
18 } from '../../../middlewares'
19 import { logger, retryTransactionWrapper } from '../../../helpers'
20 import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
21 import { PodInstance, VideoFileInstance } from '../../../models'
24 RemoteVideoCreateData,
25 RemoteVideoUpdateData,
26 RemoteVideoRemoveData,
27 RemoteVideoReportAbuseData,
28 RemoteQaduVideoRequest,
30 RemoteVideoEventRequest,
32 RemoteVideoChannelCreateData,
33 RemoteVideoChannelUpdateData,
34 RemoteVideoChannelRemoveData,
35 RemoteVideoAuthorRemoveData,
36 RemoteVideoAuthorCreateData
37 } from '../../../../shared'
39 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
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
54 const remoteVideosRouter = express.Router()
56 remoteVideosRouter.post('/',
59 remoteVideosValidator,
63 remoteVideosRouter.post('/qadu',
66 remoteQaduVideosValidator,
70 remoteVideosRouter.post('/events',
73 remoteEventsVideosValidator,
77 // ---------------------------------------------------------------------------
83 // ---------------------------------------------------------------------------
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
89 // We need to process in the same order to keep consistency
90 Promise.each(requests, request => {
91 const data = request.data
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)
100 return fun.call(this, data, fromPod)
102 .catch(err => logger.error('Error managing remote videos.', err))
104 // Don't block the other pod
105 return res.type('json').status(204).end()
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
112 Promise.each(requests, request => {
113 const videoData = request.data
115 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
117 .catch(err => logger.error('Error managing remote videos.', err))
119 return res.type('json').status(204).end()
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
126 Promise.each(requests, request => {
127 const eventData = request.data
129 return processVideosEventsRetryWrapper(eventData, fromPod)
131 .catch(err => logger.error('Error managing remote videos.', err))
133 return res.type('json').status(204).end()
136 function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
138 arguments: [ eventData, fromPod ],
139 errorMessage: 'Cannot process videos events with many retries.'
142 return retryTransactionWrapper(processVideosEvents, options)
145 function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
147 return db.sequelize.transaction(t => {
148 return fetchVideoByUUID(eventData.uuid, t)
149 .then(videoInstance => {
150 const options = { transaction: t }
155 switch (eventData.eventType) {
156 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
157 columnToUpdate = 'views'
158 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
161 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
162 columnToUpdate = 'likes'
163 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
166 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
167 columnToUpdate = 'dislikes'
168 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
172 throw new Error('Unknown video event type.')
176 query[columnToUpdate] = eventData.count
178 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
180 .then(({ videoInstance, qaduType }) => {
181 const qadusParams = [
183 videoId: videoInstance.id,
188 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
191 .then(() => logger.info('Remote video event processed for video with uuid %s.', eventData.uuid))
193 logger.debug('Cannot process a video event.', err)
198 function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
200 arguments: [ videoData, fromPod ],
201 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
204 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
207 function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
210 return db.sequelize.transaction(t => {
211 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
212 .then(videoInstance => {
213 const options = { transaction: t }
215 videoUUID = videoInstance.uuid
217 if (videoData.views) {
218 videoInstance.set('views', videoData.views)
221 if (videoData.likes) {
222 videoInstance.set('likes', videoData.likes)
225 if (videoData.dislikes) {
226 videoInstance.set('dislikes', videoData.dislikes)
229 return videoInstance.save(options)
232 .then(() => logger.info('Remote video with uuid %s quick and dirty updated', videoUUID))
233 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', err))
236 // Handle retries on fail
237 function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
239 arguments: [ videoToCreateData, fromPod ],
240 errorMessage: 'Cannot insert the remote video with many retries.'
243 return retryTransactionWrapper(addRemoteVideo, options)
246 function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
247 logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
249 return db.sequelize.transaction(t => {
250 return db.Video.loadByUUID(videoToCreateData.uuid)
252 if (video) throw new Error('UUID already exists.')
254 return db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
256 .then(videoChannel => {
257 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
259 const tags = videoToCreateData.tags
261 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoChannel, tagInstances }))
263 .then(({ videoChannel, tagInstances }) => {
265 name: videoToCreateData.name,
266 uuid: videoToCreateData.uuid,
267 category: videoToCreateData.category,
268 licence: videoToCreateData.licence,
269 language: videoToCreateData.language,
270 nsfw: videoToCreateData.nsfw,
271 description: videoToCreateData.description,
272 channelId: videoChannel.id,
273 duration: videoToCreateData.duration,
274 createdAt: videoToCreateData.createdAt,
275 // FIXME: updatedAt does not seems to be considered by Sequelize
276 updatedAt: videoToCreateData.updatedAt,
277 views: videoToCreateData.views,
278 likes: videoToCreateData.likes,
279 dislikes: videoToCreateData.dislikes,
283 const video = db.Video.build(videoData)
284 return { tagInstances, video }
286 .then(({ tagInstances, video }) => {
287 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
289 .then(({ tagInstances, video }) => {
294 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
296 .then(({ tagInstances, videoCreated }) => {
302 videoToCreateData.files.forEach(fileData => {
303 const videoFileInstance = db.VideoFile.build({
304 extname: fileData.extname,
305 infoHash: fileData.infoHash,
306 resolution: fileData.resolution,
308 videoId: videoCreated.id
311 tasks.push(videoFileInstance.save(options))
314 return Promise.all(tasks).then(() => ({ tagInstances, videoCreated }))
316 .then(({ tagInstances, videoCreated }) => {
321 return videoCreated.setTags(tagInstances, options)
324 .then(() => logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid))
326 logger.debug('Cannot insert the remote video.', err)
331 // Handle retries on fail
332 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
334 arguments: [ videoAttributesToUpdate, fromPod ],
335 errorMessage: 'Cannot update the remote video with many retries'
338 return retryTransactionWrapper(updateRemoteVideo, options)
341 function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
342 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
344 return db.sequelize.transaction(t => {
345 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t)
346 .then(videoInstance => {
347 const tags = videoAttributesToUpdate.tags
349 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
351 .then(({ videoInstance, tagInstances }) => {
352 const options = { transaction: t }
354 videoInstance.set('name', videoAttributesToUpdate.name)
355 videoInstance.set('category', videoAttributesToUpdate.category)
356 videoInstance.set('licence', videoAttributesToUpdate.licence)
357 videoInstance.set('language', videoAttributesToUpdate.language)
358 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
359 videoInstance.set('description', videoAttributesToUpdate.description)
360 videoInstance.set('duration', videoAttributesToUpdate.duration)
361 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
362 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
363 videoInstance.set('views', videoAttributesToUpdate.views)
364 videoInstance.set('likes', videoAttributesToUpdate.likes)
365 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
367 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
369 .then(({ tagInstances, videoInstance }) => {
370 const tasks: Promise<void>[] = []
372 // Remove old video files
373 videoInstance.VideoFiles.forEach(videoFile => {
374 tasks.push(videoFile.destroy({ transaction: t }))
377 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
379 .then(({ tagInstances, videoInstance }) => {
380 const tasks: Promise<VideoFileInstance>[] = []
385 videoAttributesToUpdate.files.forEach(fileData => {
386 const videoFileInstance = db.VideoFile.build({
387 extname: fileData.extname,
388 infoHash: fileData.infoHash,
389 resolution: fileData.resolution,
391 videoId: videoInstance.id
394 tasks.push(videoFileInstance.save(options))
397 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
399 .then(({ videoInstance, tagInstances }) => {
400 const options = { transaction: t }
402 return videoInstance.setTags(tagInstances, options)
405 .then(() => logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid))
407 // This is just a debug because we will retry the insert
408 logger.debug('Cannot update the remote video.', err)
413 function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
415 arguments: [ videoToRemoveData, fromPod ],
416 errorMessage: 'Cannot remove the remote video channel with many retries.'
419 return retryTransactionWrapper(removeRemoteVideo, options)
422 function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
423 logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
425 return db.sequelize.transaction(t => {
426 // We need the instance because we have to remove some other stuffs (thumbnail etc)
427 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
428 .then(video => video.destroy({ transaction: t }))
430 .then(() => logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid))
432 logger.debug('Cannot remove the remote video.', err)
437 function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
439 arguments: [ authorToCreateData, fromPod ],
440 errorMessage: 'Cannot insert the remote video author with many retries.'
443 return retryTransactionWrapper(addRemoteVideoAuthor, options)
446 function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
447 logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
449 return db.sequelize.transaction(t => {
450 return db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
452 if (author) throw new Error('UUID already exists.')
457 const videoAuthorData = {
458 name: authorToCreateData.name,
459 uuid: authorToCreateData.uuid,
460 userId: null, // Not on our pod
464 const author = db.Author.build(videoAuthorData)
465 return author.save({ transaction: t })
468 .then(() => logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid))
470 logger.debug('Cannot insert the remote video author.', err)
475 function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
477 arguments: [ authorAttributesToRemove, fromPod ],
478 errorMessage: 'Cannot remove the remote video author with many retries.'
481 return retryTransactionWrapper(removeRemoteVideoAuthor, options)
484 function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
485 logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
487 return db.sequelize.transaction(t => {
488 return db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
489 .then(videoAuthor => videoAuthor.destroy({ transaction: t }))
491 .then(() => logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid))
493 logger.debug('Cannot remove the remote video author.', err)
498 function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
500 arguments: [ videoChannelToCreateData, fromPod ],
501 errorMessage: 'Cannot insert the remote video channel with many retries.'
504 return retryTransactionWrapper(addRemoteVideoChannel, options)
507 function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
508 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
510 return db.sequelize.transaction(t => {
511 return db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
512 .then(videoChannel => {
513 if (videoChannel) throw new Error('UUID already exists.')
518 const authorUUID = videoChannelToCreateData.ownerUUID
519 const podId = fromPod.id
521 return db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
524 if (!author) throw new Error('Unknown author UUID.')
526 const videoChannelData = {
527 name: videoChannelToCreateData.name,
528 description: videoChannelToCreateData.description,
529 uuid: videoChannelToCreateData.uuid,
530 createdAt: videoChannelToCreateData.createdAt,
531 updatedAt: videoChannelToCreateData.updatedAt,
536 const videoChannel = db.VideoChannel.build(videoChannelData)
537 return videoChannel.save({ transaction: t })
540 .then(() => logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid))
542 logger.debug('Cannot insert the remote video channel.', err)
547 function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
549 arguments: [ videoChannelAttributesToUpdate, fromPod ],
550 errorMessage: 'Cannot update the remote video channel with many retries.'
553 return retryTransactionWrapper(updateRemoteVideoChannel, options)
556 function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
557 logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
559 return db.sequelize.transaction(t => {
560 return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
561 .then(videoChannelInstance => {
562 const options = { transaction: t }
564 videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
565 videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
566 videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
567 videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
569 return videoChannelInstance.save(options)
572 .then(() => logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid))
574 // This is just a debug because we will retry the insert
575 logger.debug('Cannot update the remote video channel.', err)
580 function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
582 arguments: [ videoChannelAttributesToRemove, fromPod ],
583 errorMessage: 'Cannot remove the remote video channel with many retries.'
586 return retryTransactionWrapper(removeRemoteVideoChannel, options)
589 function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
590 logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
592 return db.sequelize.transaction(t => {
593 return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
594 .then(videoChannel => videoChannel.destroy({ transaction: t }))
596 .then(() => logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid))
598 logger.debug('Cannot remove the remote video channel.', err)
603 function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
605 arguments: [ reportData, fromPod ],
606 errorMessage: 'Cannot create remote abuse video with many retries.'
609 return retryTransactionWrapper(reportAbuseRemoteVideo, options)
612 function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
613 logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
615 return db.sequelize.transaction(t => {
616 return fetchVideoByUUID(reportData.videoUUID, t)
618 const videoAbuseData = {
619 reporterUsername: reportData.reporterUsername,
620 reason: reportData.reportReason,
621 reporterPodId: fromPod.id,
625 return db.VideoAbuse.create(videoAbuseData)
628 .then(() => logger.info('Remote abuse for video uuid %s created', reportData.videoUUID))
630 // This is just a debug because we will retry the insert
631 logger.debug('Cannot create remote abuse video', err)
636 function fetchVideoByUUID (id: string, t: Sequelize.Transaction) {
637 return db.Video.loadByUUID(id, t)
639 if (!video) throw new Error('Video not found')
644 logger.error('Cannot load owned video from id.', { error: err.stack, id })
649 function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
650 return db.Video.loadByHostAndUUID(podHost, uuid, t)
652 if (!video) throw new Error('Video not found')
657 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
662 function fetchVideoChannelByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
663 return db.VideoChannel.loadByHostAndUUID(podHost, uuid, t)
664 .then(videoChannel => {
665 if (!videoChannel) throw new Error('Video channel not found')
670 logger.error('Cannot load video channel from host and uuid.', { error: err.stack, podHost, uuid })