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
67 Promise.each(requests, (request: any) => {
68 const data = request.data
70 // Get the function we need to call in order to process the request
71 const fun = functionsHash[request.type]
72 if (fun === undefined) {
73 logger.error('Unkown remote request type %s.', request.type)
77 return fun.call(this, data, fromPod)
79 .catch(err => logger.error('Error managing remote videos.', err))
81 // Don't block the other pod
82 return res.type('json').status(204).end()
85 function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
86 const requests = req.body.data
87 const fromPod = res.locals.secure.pod
89 Promise.each(requests, (request: any) => {
90 const videoData = request.data
92 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
94 .catch(err => logger.error('Error managing remote videos.', err))
96 return res.type('json').status(204).end()
99 function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
100 const requests = req.body.data
101 const fromPod = res.locals.secure.pod
103 Promise.each(requests, (request: any) => {
104 const eventData = request.data
106 return processVideosEventsRetryWrapper(eventData, fromPod)
108 .catch(err => logger.error('Error managing remote videos.', err))
110 return res.type('json').status(204).end()
113 function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) {
115 arguments: [ eventData, fromPod ],
116 errorMessage: 'Cannot process videos events with many retries.'
119 return retryTransactionWrapper(processVideosEvents, options)
122 function processVideosEvents (eventData: any, fromPod: PodInstance) {
124 return db.sequelize.transaction(t => {
125 return fetchOwnedVideo(eventData.remoteId)
126 .then(videoInstance => {
127 const options = { transaction: t }
132 switch (eventData.eventType) {
133 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
134 columnToUpdate = 'views'
135 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
138 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
139 columnToUpdate = 'likes'
140 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
143 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
144 columnToUpdate = 'dislikes'
145 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
149 throw new Error('Unknown video event type.')
153 query[columnToUpdate] = eventData.count
155 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
157 .then(({ videoInstance, qaduType }) => {
158 const qadusParams = [
160 videoId: videoInstance.id,
165 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
168 .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId))
170 logger.debug('Cannot process a video event.', err)
175 function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) {
177 arguments: [ videoData, fromPod ],
178 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
181 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
184 function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) {
187 return db.sequelize.transaction(t => {
188 return fetchRemoteVideo(fromPod.host, videoData.remoteId)
189 .then(videoInstance => {
190 const options = { transaction: t }
192 videoName = videoInstance.name
194 if (videoData.views) {
195 videoInstance.set('views', videoData.views)
198 if (videoData.likes) {
199 videoInstance.set('likes', videoData.likes)
202 if (videoData.dislikes) {
203 videoInstance.set('dislikes', videoData.dislikes)
206 return videoInstance.save(options)
209 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
210 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', err))
213 // Handle retries on fail
214 function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) {
216 arguments: [ videoToCreateData, fromPod ],
217 errorMessage: 'Cannot insert the remote video with many retries.'
220 return retryTransactionWrapper(addRemoteVideo, options)
223 function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) {
224 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
226 return db.sequelize.transaction(t => {
227 return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId)
229 if (video) throw new Error('RemoteId and host pair is not unique.')
234 const name = videoToCreateData.author
235 const podId = fromPod.id
236 // This author is from another pod so we do not associate a user
239 return db.Author.findOrCreateAuthor(name, podId, userId, t)
242 const tags = videoToCreateData.tags
244 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
246 .then(({ author, tagInstances }) => {
248 name: videoToCreateData.name,
249 remoteId: videoToCreateData.remoteId,
250 extname: videoToCreateData.extname,
251 infoHash: videoToCreateData.infoHash,
252 category: videoToCreateData.category,
253 licence: videoToCreateData.licence,
254 language: videoToCreateData.language,
255 nsfw: videoToCreateData.nsfw,
256 description: videoToCreateData.description,
258 duration: videoToCreateData.duration,
259 createdAt: videoToCreateData.createdAt,
260 // FIXME: updatedAt does not seems to be considered by Sequelize
261 updatedAt: videoToCreateData.updatedAt,
262 views: videoToCreateData.views,
263 likes: videoToCreateData.likes,
264 dislikes: videoToCreateData.dislikes
267 const video = db.Video.build(videoData)
268 return { tagInstances, video }
270 .then(({ tagInstances, video }) => {
271 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
273 .then(({ tagInstances, video }) => {
278 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
280 .then(({ tagInstances, videoCreated }) => {
285 return videoCreated.setTags(tagInstances, options)
288 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
290 logger.debug('Cannot insert the remote video.', err)
295 // Handle retries on fail
296 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) {
298 arguments: [ videoAttributesToUpdate, fromPod ],
299 errorMessage: 'Cannot update the remote video with many retries'
302 return retryTransactionWrapper(updateRemoteVideo, options)
305 function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) {
306 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
308 return db.sequelize.transaction(t => {
309 return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId)
310 .then(videoInstance => {
311 const tags = videoAttributesToUpdate.tags
313 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
315 .then(({ videoInstance, tagInstances }) => {
316 const options = { transaction: t }
318 videoInstance.set('name', videoAttributesToUpdate.name)
319 videoInstance.set('category', videoAttributesToUpdate.category)
320 videoInstance.set('licence', videoAttributesToUpdate.licence)
321 videoInstance.set('language', videoAttributesToUpdate.language)
322 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
323 videoInstance.set('description', videoAttributesToUpdate.description)
324 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
325 videoInstance.set('duration', videoAttributesToUpdate.duration)
326 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
327 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
328 videoInstance.set('extname', videoAttributesToUpdate.extname)
329 videoInstance.set('views', videoAttributesToUpdate.views)
330 videoInstance.set('likes', videoAttributesToUpdate.likes)
331 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
333 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
335 .then(({ videoInstance, tagInstances }) => {
336 const options = { transaction: t }
338 return videoInstance.setTags(tagInstances, options)
341 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
343 // This is just a debug because we will retry the insert
344 logger.debug('Cannot update the remote video.', err)
349 function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance) {
350 // We need the instance because we have to remove some other stuffs (thumbnail etc)
351 return fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId)
353 logger.debug('Removing remote video %s.', video.remoteId)
354 return video.destroy()
357 logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err.stack })
361 function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance) {
362 return fetchOwnedVideo(reportData.videoRemoteId)
364 logger.debug('Reporting remote abuse for video %s.', video.id)
366 const videoAbuseData = {
367 reporterUsername: reportData.reporterUsername,
368 reason: reportData.reportReason,
369 reporterPodId: fromPod.id,
373 return db.VideoAbuse.create(videoAbuseData)
375 .catch(err => logger.error('Cannot create remote abuse video.', err))
378 function fetchOwnedVideo (id: string) {
379 return db.Video.load(id)
381 if (!video) throw new Error('Video not found')
386 logger.error('Cannot load owned video from id.', { error: err.stack, id })
391 function fetchRemoteVideo (podHost: string, remoteId: string) {
392 return db.Video.loadByHostAndRemoteId(podHost, remoteId)
394 if (!video) throw new Error('Video not found')
399 logger.error('Cannot load video from host and remote id.', { error: err.stack, podHost, remoteId })