]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.ts
Move to promises
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
1 import * as express from 'express'
2 import * as Promise from 'bluebird'
3
4 import { database as db } from '../../../initializers/database'
5 import {
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_ENDPOINTS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10 } from '../../../initializers'
11 import {
12 checkSignature,
13 signatureValidator,
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'
21
22 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
23
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
30
31 const remoteVideosRouter = express.Router()
32
33 remoteVideosRouter.post('/',
34 signatureValidator,
35 checkSignature,
36 remoteVideosValidator,
37 remoteVideos
38 )
39
40 remoteVideosRouter.post('/qadu',
41 signatureValidator,
42 checkSignature,
43 remoteQaduVideosValidator,
44 remoteVideosQadu
45 )
46
47 remoteVideosRouter.post('/events',
48 signatureValidator,
49 checkSignature,
50 remoteEventsVideosValidator,
51 remoteVideosEvents
52 )
53
54 // ---------------------------------------------------------------------------
55
56 export {
57 remoteVideosRouter
58 }
59
60 // ---------------------------------------------------------------------------
61
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
65
66 // We need to process in the same order to keep consistency
67 // TODO: optimization
68 Promise.mapSeries(requests, (request: any) => {
69 const data = request.data
70
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)
75 return
76 }
77
78 return fun.call(this, data, fromPod)
79 })
80 .catch(err => logger.error('Error managing remote videos.', { error: err }))
81
82 // We don't need to keep the other pod waiting
83 return res.type('json').status(204).end()
84 }
85
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
89
90 Promise.mapSeries(requests, (request: any) => {
91 const videoData = request.data
92
93 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
94 })
95 .catch(err => logger.error('Error managing remote videos.', { error: err }))
96
97 return res.type('json').status(204).end()
98 }
99
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
103
104 Promise.mapSeries(requests, (request: any) => {
105 const eventData = request.data
106
107 return processVideosEventsRetryWrapper(eventData, fromPod)
108 })
109 .catch(err => logger.error('Error managing remote videos.', { error: err }))
110
111 return res.type('json').status(204).end()
112 }
113
114 function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) {
115 const options = {
116 arguments: [ eventData, fromPod ],
117 errorMessage: 'Cannot process videos events with many retries.'
118 }
119
120 return retryTransactionWrapper(processVideosEvents, options)
121 }
122
123 function processVideosEvents (eventData: any, fromPod: PodInstance) {
124
125 return db.sequelize.transaction(t => {
126 return fetchOwnedVideo(eventData.remoteId)
127 .then(videoInstance => {
128 const options = { transaction: t }
129
130 let columnToUpdate
131 let qaduType
132
133 switch (eventData.eventType) {
134 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
135 columnToUpdate = 'views'
136 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
137 break
138
139 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
140 columnToUpdate = 'likes'
141 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
142 break
143
144 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
145 columnToUpdate = 'dislikes'
146 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
147 break
148
149 default:
150 throw new Error('Unknown video event type.')
151 }
152
153 const query = {}
154 query[columnToUpdate] = eventData.count
155
156 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
157 })
158 .then(({ videoInstance, qaduType }) => {
159 const qadusParams = [
160 {
161 videoId: videoInstance.id,
162 type: qaduType
163 }
164 ]
165
166 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
167 })
168 })
169 .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId))
170 .catch(err => {
171 logger.debug('Cannot process a video event.', { error: err })
172 throw err
173 })
174 }
175
176 function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) {
177 const options = {
178 arguments: [ videoData, fromPod ],
179 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
180 }
181
182 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
183 }
184
185 function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) {
186 let videoName
187
188 return db.sequelize.transaction(t => {
189 return fetchRemoteVideo(fromPod.host, videoData.remoteId)
190 .then(videoInstance => {
191 const options = { transaction: t }
192
193 videoName = videoInstance.name
194
195 if (videoData.views) {
196 videoInstance.set('views', videoData.views)
197 }
198
199 if (videoData.likes) {
200 videoInstance.set('likes', videoData.likes)
201 }
202
203 if (videoData.dislikes) {
204 videoInstance.set('dislikes', videoData.dislikes)
205 }
206
207 return videoInstance.save(options)
208 })
209 })
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 }))
212 }
213
214 // Handle retries on fail
215 function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) {
216 const options = {
217 arguments: [ videoToCreateData, fromPod ],
218 errorMessage: 'Cannot insert the remote video with many retries.'
219 }
220
221 return retryTransactionWrapper(addRemoteVideo, options)
222 }
223
224 function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) {
225 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
226
227 return db.sequelize.transaction(t => {
228 return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId)
229 .then(video => {
230 if (video) throw new Error('RemoteId and host pair is not unique.')
231
232 return undefined
233 })
234 .then(() => {
235 const name = videoToCreateData.author
236 const podId = fromPod.id
237 // This author is from another pod so we do not associate a user
238 const userId = null
239
240 return db.Author.findOrCreateAuthor(name, podId, userId, t)
241 })
242 .then(author => {
243 const tags = videoToCreateData.tags
244
245 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
246 })
247 .then(({ author, tagInstances }) => {
248 const videoData = {
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,
258 authorId: author.id,
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
266 }
267
268 const video = db.Video.build(videoData)
269 return { tagInstances, video }
270 })
271 .then(({ tagInstances, video }) => {
272 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
273 })
274 .then(({ tagInstances, video }) => {
275 const options = {
276 transaction: t
277 }
278
279 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
280 })
281 .then(({ tagInstances, videoCreated }) => {
282 const options = {
283 transaction: t
284 }
285
286 return videoCreated.setTags(tagInstances, options)
287 })
288 })
289 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
290 .catch(err => {
291 logger.debug('Cannot insert the remote video.', { error: err })
292 throw err
293 })
294 }
295
296 // Handle retries on fail
297 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) {
298 const options = {
299 arguments: [ videoAttributesToUpdate, fromPod ],
300 errorMessage: 'Cannot update the remote video with many retries'
301 }
302
303 return retryTransactionWrapper(updateRemoteVideo, options)
304 }
305
306 function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) {
307 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
308
309 return db.sequelize.transaction(t => {
310 return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId)
311 .then(videoInstance => {
312 const tags = videoAttributesToUpdate.tags
313
314 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
315 })
316 .then(({ videoInstance, tagInstances }) => {
317 const options = { transaction: t }
318
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)
333
334 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
335 })
336 .then(({ videoInstance, tagInstances }) => {
337 const options = { transaction: t }
338
339 return videoInstance.setTags(tagInstances, options)
340 })
341 })
342 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
343 .catch(err => {
344 // This is just a debug because we will retry the insert
345 logger.debug('Cannot update the remote video.', { error: err })
346 throw err
347 })
348 }
349
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)
353 .then(video => {
354 logger.debug('Removing remote video %s.', video.remoteId)
355 return video.destroy()
356 })
357 .catch(err => {
358 logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err })
359 })
360 }
361
362 function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance) {
363 return fetchOwnedVideo(reportData.videoRemoteId)
364 .then(video => {
365 logger.debug('Reporting remote abuse for video %s.', video.id)
366
367 const videoAbuseData = {
368 reporterUsername: reportData.reporterUsername,
369 reason: reportData.reportReason,
370 reporterPodId: fromPod.id,
371 videoId: video.id
372 }
373
374 return db.VideoAbuse.create(videoAbuseData)
375 })
376 .catch(err => logger.error('Cannot create remote abuse video.', { error: err }))
377 }
378
379 function fetchOwnedVideo (id: string) {
380 return db.Video.load(id)
381 .then(video => {
382 if (!video) throw new Error('Video not found')
383
384 return video
385 })
386 .catch(err => {
387 logger.error('Cannot load owned video from id.', { error: err, id })
388 throw err
389 })
390 }
391
392 function fetchRemoteVideo (podHost: string, remoteId: string) {
393 return db.Video.loadByHostAndRemoteId(podHost, remoteId)
394 .then(video => {
395 if (!video) throw new Error('Video not found')
396
397 return video
398 })
399 .catch(err => {
400 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
401 throw err
402 })
403 }