1 import * as express from 'express'
2 import * as Promise from 'bluebird'
4 import { database as db } from '../../../initializers/database'
6 REQUEST_ENDPOINT_ACTIONS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10 } from '../../../initializers'
14 remoteVideosValidator,
15 remoteQaduVideosValidator,
16 remoteEventsVideosValidator
17 } from '../../../middlewares'
18 import { logger, retryTransactionWrapper } from '../../../helpers'
19 import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
20 import { PodInstance, VideoInstance } from '../../../models'
22 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
24 // Functions to call when processing a remote request
25 const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
26 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
27 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
28 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
29 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
31 const remoteVideosRouter = express.Router()
33 remoteVideosRouter.post('/',
36 remoteVideosValidator,
40 remoteVideosRouter.post('/qadu',
43 remoteQaduVideosValidator,
47 remoteVideosRouter.post('/events',
50 remoteEventsVideosValidator,
54 // ---------------------------------------------------------------------------
60 // ---------------------------------------------------------------------------
62 function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
63 const requests = req.body.data
64 const fromPod = res.locals.secure.pod
66 // We need to process in the same order to keep consistency
68 Promise.mapSeries(requests, (request: any) => {
69 const data = request.data
71 // Get the function we need to call in order to process the request
72 const fun = functionsHash[request.type]
73 if (fun === undefined) {
74 logger.error('Unkown remote request type %s.', request.type)
78 return fun.call(this, data, fromPod)
80 .catch(err => logger.error('Error managing remote videos.', { error: err }))
82 // We don't need to keep the other pod waiting
83 return res.type('json').status(204).end()
86 function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const requests = req.body.data
88 const fromPod = res.locals.secure.pod
90 Promise.mapSeries(requests, (request: any) => {
91 const videoData = request.data
93 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
95 .catch(err => logger.error('Error managing remote videos.', { error: err }))
97 return res.type('json').status(204).end()
100 function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
101 const requests = req.body.data
102 const fromPod = res.locals.secure.pod
104 Promise.mapSeries(requests, (request: any) => {
105 const eventData = request.data
107 return processVideosEventsRetryWrapper(eventData, fromPod)
109 .catch(err => logger.error('Error managing remote videos.', { error: err }))
111 return res.type('json').status(204).end()
114 function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) {
116 arguments: [ eventData, fromPod ],
117 errorMessage: 'Cannot process videos events with many retries.'
120 return retryTransactionWrapper(processVideosEvents, options)
123 function processVideosEvents (eventData: any, fromPod: PodInstance) {
125 return db.sequelize.transaction(t => {
126 return fetchOwnedVideo(eventData.remoteId)
127 .then(videoInstance => {
128 const options = { transaction: t }
133 switch (eventData.eventType) {
134 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
135 columnToUpdate = 'views'
136 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
139 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
140 columnToUpdate = 'likes'
141 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
144 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
145 columnToUpdate = 'dislikes'
146 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
150 throw new Error('Unknown video event type.')
154 query[columnToUpdate] = eventData.count
156 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
158 .then(({ videoInstance, qaduType }) => {
159 const qadusParams = [
161 videoId: videoInstance.id,
166 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
169 .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId))
171 logger.debug('Cannot process a video event.', { error: err })
176 function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) {
178 arguments: [ videoData, fromPod ],
179 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
182 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
185 function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) {
188 return db.sequelize.transaction(t => {
189 return fetchRemoteVideo(fromPod.host, videoData.remoteId)
190 .then(videoInstance => {
191 const options = { transaction: t }
193 videoName = videoInstance.name
195 if (videoData.views) {
196 videoInstance.set('views', videoData.views)
199 if (videoData.likes) {
200 videoInstance.set('likes', videoData.likes)
203 if (videoData.dislikes) {
204 videoInstance.set('dislikes', videoData.dislikes)
207 return videoInstance.save(options)
210 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
211 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', { error: err }))
214 // Handle retries on fail
215 function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) {
217 arguments: [ videoToCreateData, fromPod ],
218 errorMessage: 'Cannot insert the remote video with many retries.'
221 return retryTransactionWrapper(addRemoteVideo, options)
224 function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) {
225 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
227 return db.sequelize.transaction(t => {
228 return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId)
230 if (video) throw new Error('RemoteId and host pair is not unique.')
235 const name = videoToCreateData.author
236 const podId = fromPod.id
237 // This author is from another pod so we do not associate a user
240 return db.Author.findOrCreateAuthor(name, podId, userId, t)
243 const tags = videoToCreateData.tags
245 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
247 .then(({ author, tagInstances }) => {
249 name: videoToCreateData.name,
250 remoteId: videoToCreateData.remoteId,
251 extname: videoToCreateData.extname,
252 infoHash: videoToCreateData.infoHash,
253 category: videoToCreateData.category,
254 licence: videoToCreateData.licence,
255 language: videoToCreateData.language,
256 nsfw: videoToCreateData.nsfw,
257 description: videoToCreateData.description,
259 duration: videoToCreateData.duration,
260 createdAt: videoToCreateData.createdAt,
261 // FIXME: updatedAt does not seems to be considered by Sequelize
262 updatedAt: videoToCreateData.updatedAt,
263 views: videoToCreateData.views,
264 likes: videoToCreateData.likes,
265 dislikes: videoToCreateData.dislikes
268 const video = db.Video.build(videoData)
269 return { tagInstances, video }
271 .then(({ tagInstances, video }) => {
272 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
274 .then(({ tagInstances, video }) => {
279 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
281 .then(({ tagInstances, videoCreated }) => {
286 return videoCreated.setTags(tagInstances, options)
289 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
291 logger.debug('Cannot insert the remote video.', { error: err })
296 // Handle retries on fail
297 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) {
299 arguments: [ videoAttributesToUpdate, fromPod ],
300 errorMessage: 'Cannot update the remote video with many retries'
303 return retryTransactionWrapper(updateRemoteVideo, options)
306 function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) {
307 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
309 return db.sequelize.transaction(t => {
310 return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId)
311 .then(videoInstance => {
312 const tags = videoAttributesToUpdate.tags
314 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
316 .then(({ videoInstance, tagInstances }) => {
317 const options = { transaction: t }
319 videoInstance.set('name', videoAttributesToUpdate.name)
320 videoInstance.set('category', videoAttributesToUpdate.category)
321 videoInstance.set('licence', videoAttributesToUpdate.licence)
322 videoInstance.set('language', videoAttributesToUpdate.language)
323 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
324 videoInstance.set('description', videoAttributesToUpdate.description)
325 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
326 videoInstance.set('duration', videoAttributesToUpdate.duration)
327 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
328 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
329 videoInstance.set('extname', videoAttributesToUpdate.extname)
330 videoInstance.set('views', videoAttributesToUpdate.views)
331 videoInstance.set('likes', videoAttributesToUpdate.likes)
332 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
334 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
336 .then(({ videoInstance, tagInstances }) => {
337 const options = { transaction: t }
339 return videoInstance.setTags(tagInstances, options)
342 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
344 // This is just a debug because we will retry the insert
345 logger.debug('Cannot update the remote video.', { error: err })
350 function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance) {
351 // We need the instance because we have to remove some other stuffs (thumbnail etc)
352 return fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId)
354 logger.debug('Removing remote video %s.', video.remoteId)
355 return video.destroy()
358 logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err })
362 function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance) {
363 return fetchOwnedVideo(reportData.videoRemoteId)
365 logger.debug('Reporting remote abuse for video %s.', video.id)
367 const videoAbuseData = {
368 reporterUsername: reportData.reporterUsername,
369 reason: reportData.reportReason,
370 reporterPodId: fromPod.id,
374 return db.VideoAbuse.create(videoAbuseData)
376 .catch(err => logger.error('Cannot create remote abuse video.', { error: err }))
379 function fetchOwnedVideo (id: string) {
380 return db.Video.load(id)
382 if (!video) throw new Error('Video not found')
387 logger.error('Cannot load owned video from id.', { error: err, id })
392 function fetchRemoteVideo (podHost: string, remoteId: string) {
393 return db.Video.loadByHostAndRemoteId(podHost, remoteId)
395 if (!video) throw new Error('Video not found')
400 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })