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 } from '../../../models'
23 RemoteVideoCreateData,
24 RemoteVideoUpdateData,
25 RemoteVideoRemoveData,
26 RemoteVideoReportAbuseData,
27 RemoteQaduVideoRequest,
29 RemoteVideoEventRequest,
31 } from '../../../../shared'
33 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
35 // Functions to call when processing a remote request
36 const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
37 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
38 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
39 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
40 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
42 const remoteVideosRouter = express.Router()
44 remoteVideosRouter.post('/',
47 remoteVideosValidator,
51 remoteVideosRouter.post('/qadu',
54 remoteQaduVideosValidator,
58 remoteVideosRouter.post('/events',
61 remoteEventsVideosValidator,
65 // ---------------------------------------------------------------------------
71 // ---------------------------------------------------------------------------
73 function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
74 const requests: RemoteVideoRequest[] = req.body.data
75 const fromPod = res.locals.secure.pod
77 // We need to process in the same order to keep consistency
78 Promise.each(requests, request => {
79 const data = request.data
81 // Get the function we need to call in order to process the request
82 const fun = functionsHash[request.type]
83 if (fun === undefined) {
84 logger.error('Unkown remote request type %s.', request.type)
88 return fun.call(this, data, fromPod)
90 .catch(err => logger.error('Error managing remote videos.', err))
92 // Don't block the other pod
93 return res.type('json').status(204).end()
96 function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
97 const requests: RemoteQaduVideoRequest[] = req.body.data
98 const fromPod = res.locals.secure.pod
100 Promise.each(requests, request => {
101 const videoData = request.data
103 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
105 .catch(err => logger.error('Error managing remote videos.', err))
107 return res.type('json').status(204).end()
110 function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
111 const requests: RemoteVideoEventRequest[] = req.body.data
112 const fromPod = res.locals.secure.pod
114 Promise.each(requests, request => {
115 const eventData = request.data
117 return processVideosEventsRetryWrapper(eventData, fromPod)
119 .catch(err => logger.error('Error managing remote videos.', err))
121 return res.type('json').status(204).end()
124 function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
126 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.'
130 return retryTransactionWrapper(processVideosEvents, options)
133 function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
135 return db.sequelize.transaction(t => {
136 return fetchVideoByUUID(eventData.uuid)
137 .then(videoInstance => {
138 const options = { transaction: t }
143 switch (eventData.eventType) {
144 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
145 columnToUpdate = 'views'
146 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
149 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
150 columnToUpdate = 'likes'
151 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
154 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
155 columnToUpdate = 'dislikes'
156 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
160 throw new Error('Unknown video event type.')
164 query[columnToUpdate] = eventData.count
166 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
168 .then(({ videoInstance, qaduType }) => {
169 const qadusParams = [
171 videoId: videoInstance.id,
176 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
179 .then(() => logger.info('Remote video event processed for video %s.', eventData.uuid))
181 logger.debug('Cannot process a video event.', err)
186 function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
188 arguments: [ videoData, fromPod ],
189 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
192 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
195 function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
198 return db.sequelize.transaction(t => {
199 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid)
200 .then(videoInstance => {
201 const options = { transaction: t }
203 videoName = videoInstance.name
205 if (videoData.views) {
206 videoInstance.set('views', videoData.views)
209 if (videoData.likes) {
210 videoInstance.set('likes', videoData.likes)
213 if (videoData.dislikes) {
214 videoInstance.set('dislikes', videoData.dislikes)
217 return videoInstance.save(options)
220 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
221 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', err))
224 // Handle retries on fail
225 function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
227 arguments: [ videoToCreateData, fromPod ],
228 errorMessage: 'Cannot insert the remote video with many retries.'
231 return retryTransactionWrapper(addRemoteVideo, options)
234 function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
235 logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
237 return db.sequelize.transaction(t => {
238 return db.Video.loadByUUID(videoToCreateData.uuid)
240 if (video) throw new Error('UUID already exists.')
245 const name = videoToCreateData.author
246 const podId = fromPod.id
247 // This author is from another pod so we do not associate a user
250 return db.Author.findOrCreateAuthor(name, podId, userId, t)
253 const tags = videoToCreateData.tags
255 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
257 .then(({ author, tagInstances }) => {
259 name: videoToCreateData.name,
260 uuid: videoToCreateData.uuid,
261 category: videoToCreateData.category,
262 licence: videoToCreateData.licence,
263 language: videoToCreateData.language,
264 nsfw: videoToCreateData.nsfw,
265 description: videoToCreateData.description,
267 duration: videoToCreateData.duration,
268 createdAt: videoToCreateData.createdAt,
269 // FIXME: updatedAt does not seems to be considered by Sequelize
270 updatedAt: videoToCreateData.updatedAt,
271 views: videoToCreateData.views,
272 likes: videoToCreateData.likes,
273 dislikes: videoToCreateData.dislikes,
277 const video = db.Video.build(videoData)
278 return { tagInstances, video }
280 .then(({ tagInstances, video }) => {
281 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
283 .then(({ tagInstances, video }) => {
288 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
290 .then(({ tagInstances, videoCreated }) => {
296 videoToCreateData.files.forEach(fileData => {
297 const videoFileInstance = db.VideoFile.build({
298 extname: fileData.extname,
299 infoHash: fileData.infoHash,
300 resolution: fileData.resolution,
302 videoId: videoCreated.id
305 tasks.push(videoFileInstance.save(options))
308 return Promise.all(tasks).then(() => ({ tagInstances, videoCreated }))
310 .then(({ tagInstances, videoCreated }) => {
315 return videoCreated.setTags(tagInstances, options)
318 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
320 logger.debug('Cannot insert the remote video.', err)
325 // Handle retries on fail
326 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
328 arguments: [ videoAttributesToUpdate, fromPod ],
329 errorMessage: 'Cannot update the remote video with many retries'
332 return retryTransactionWrapper(updateRemoteVideo, options)
335 function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
336 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
338 return db.sequelize.transaction(t => {
339 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid)
340 .then(videoInstance => {
341 const tags = videoAttributesToUpdate.tags
343 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
345 .then(({ videoInstance, tagInstances }) => {
346 const options = { transaction: t }
348 videoInstance.set('name', videoAttributesToUpdate.name)
349 videoInstance.set('category', videoAttributesToUpdate.category)
350 videoInstance.set('licence', videoAttributesToUpdate.licence)
351 videoInstance.set('language', videoAttributesToUpdate.language)
352 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
353 videoInstance.set('description', videoAttributesToUpdate.description)
354 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
355 videoInstance.set('duration', videoAttributesToUpdate.duration)
356 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
357 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
358 videoInstance.set('extname', videoAttributesToUpdate.extname)
359 videoInstance.set('views', videoAttributesToUpdate.views)
360 videoInstance.set('likes', videoAttributesToUpdate.likes)
361 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
363 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
365 .then(({ tagInstances, videoInstance }) => {
371 videoAttributesToUpdate.files.forEach(fileData => {
372 const videoFileInstance = db.VideoFile.build({
373 extname: fileData.extname,
374 infoHash: fileData.infoHash,
375 resolution: fileData.resolution,
377 videoId: videoInstance.id
380 tasks.push(videoFileInstance.save(options))
383 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
385 .then(({ videoInstance, tagInstances }) => {
386 const options = { transaction: t }
388 return videoInstance.setTags(tagInstances, options)
391 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
393 // This is just a debug because we will retry the insert
394 logger.debug('Cannot update the remote video.', err)
399 function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
400 // We need the instance because we have to remove some other stuffs (thumbnail etc)
401 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid)
403 logger.debug('Removing remote video %s.', video.uuid)
404 return video.destroy()
407 logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack })
411 function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
412 return fetchVideoByUUID(reportData.videoUUID)
414 logger.debug('Reporting remote abuse for video %s.', video.id)
416 const videoAbuseData = {
417 reporterUsername: reportData.reporterUsername,
418 reason: reportData.reportReason,
419 reporterPodId: fromPod.id,
423 return db.VideoAbuse.create(videoAbuseData)
425 .catch(err => logger.error('Cannot create remote abuse video.', err))
428 function fetchVideoByUUID (id: string) {
429 return db.Video.loadByUUID(id)
431 if (!video) throw new Error('Video not found')
436 logger.error('Cannot load owned video from id.', { error: err.stack, id })
441 function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
442 return db.Video.loadByHostAndUUID(podHost, uuid)
444 if (!video) throw new Error('Video not found')
449 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })