]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.ts
Upgrade common server dependencies
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
CommitLineData
4d4e5cd4 1import * as express from 'express'
6fcd19ba 2import * as Promise from 'bluebird'
528a9efa 3
e02643f3 4import { database as db } from '../../../initializers/database'
65fcc311
C
5import {
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_ENDPOINTS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10} from '../../../initializers'
11import {
12 checkSignature,
13 signatureValidator,
14 remoteVideosValidator,
15 remoteQaduVideosValidator,
16 remoteEventsVideosValidator
17} from '../../../middlewares'
6fcd19ba 18import { logger, retryTransactionWrapper } from '../../../helpers'
65fcc311 19import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
556ddc31 20import { PodInstance } from '../../../models'
4771e000
C
21import {
22 RemoteVideoRequest,
23 RemoteVideoCreateData,
24 RemoteVideoUpdateData,
25 RemoteVideoRemoveData,
26 RemoteVideoReportAbuseData,
27 RemoteQaduVideoRequest,
28 RemoteQaduVideoData,
29 RemoteVideoEventRequest,
30 RemoteVideoEventData
31} from '../../../../shared'
65fcc311
C
32
33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
62f4ef41
C
34
35// Functions to call when processing a remote request
6fcd19ba 36const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
62f4ef41
C
37functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
38functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
39functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
40functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
41
65fcc311 42const remoteVideosRouter = express.Router()
528a9efa 43
65fcc311
C
44remoteVideosRouter.post('/',
45 signatureValidator,
46 checkSignature,
47 remoteVideosValidator,
528a9efa
C
48 remoteVideos
49)
50
65fcc311
C
51remoteVideosRouter.post('/qadu',
52 signatureValidator,
53 checkSignature,
54 remoteQaduVideosValidator,
9e167724
C
55 remoteVideosQadu
56)
57
65fcc311
C
58remoteVideosRouter.post('/events',
59 signatureValidator,
60 checkSignature,
61 remoteEventsVideosValidator,
e4c87ec2
C
62 remoteVideosEvents
63)
64
528a9efa
C
65// ---------------------------------------------------------------------------
66
65fcc311
C
67export {
68 remoteVideosRouter
69}
528a9efa
C
70
71// ---------------------------------------------------------------------------
72
69818c93 73function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 74 const requests: RemoteVideoRequest[] = req.body.data
4ff0d862 75 const fromPod = res.locals.secure.pod
528a9efa
C
76
77 // We need to process in the same order to keep consistency
4771e000 78 Promise.each(requests, request => {
55fa55a9 79 const data = request.data
528a9efa 80
62f4ef41
C
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)
6fcd19ba 85 return
528a9efa 86 }
62f4ef41 87
6fcd19ba 88 return fun.call(this, data, fromPod)
528a9efa 89 })
ad0997ad 90 .catch(err => logger.error('Error managing remote videos.', err))
528a9efa 91
709756b8 92 // Don't block the other pod
528a9efa
C
93 return res.type('json').status(204).end()
94}
95
69818c93 96function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 97 const requests: RemoteQaduVideoRequest[] = req.body.data
9e167724
C
98 const fromPod = res.locals.secure.pod
99
4771e000 100 Promise.each(requests, request => {
9e167724
C
101 const videoData = request.data
102
6fcd19ba 103 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
9e167724 104 })
ad0997ad 105 .catch(err => logger.error('Error managing remote videos.', err))
9e167724
C
106
107 return res.type('json').status(204).end()
108}
109
69818c93 110function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 111 const requests: RemoteVideoEventRequest[] = req.body.data
e4c87ec2
C
112 const fromPod = res.locals.secure.pod
113
4771e000 114 Promise.each(requests, request => {
e4c87ec2
C
115 const eventData = request.data
116
6fcd19ba 117 return processVideosEventsRetryWrapper(eventData, fromPod)
e4c87ec2 118 })
ad0997ad 119 .catch(err => logger.error('Error managing remote videos.', err))
e4c87ec2
C
120
121 return res.type('json').status(204).end()
122}
123
4771e000 124function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
e4c87ec2
C
125 const options = {
126 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.'
128 }
129
6fcd19ba 130 return retryTransactionWrapper(processVideosEvents, options)
e4c87ec2
C
131}
132
4771e000 133function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
e4c87ec2 134
6fcd19ba 135 return db.sequelize.transaction(t => {
0a6658fd 136 return fetchVideoByUUID(eventData.uuid)
6fcd19ba
C
137 .then(videoInstance => {
138 const options = { transaction: t }
e4c87ec2 139
6fcd19ba
C
140 let columnToUpdate
141 let qaduType
e4c87ec2 142
6fcd19ba
C
143 switch (eventData.eventType) {
144 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
145 columnToUpdate = 'views'
146 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
147 break
e4c87ec2 148
6fcd19ba
C
149 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
150 columnToUpdate = 'likes'
151 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
152 break
e4c87ec2 153
6fcd19ba
C
154 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
155 columnToUpdate = 'dislikes'
156 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
157 break
e4c87ec2 158
6fcd19ba
C
159 default:
160 throw new Error('Unknown video event type.')
161 }
e4c87ec2 162
6fcd19ba
C
163 const query = {}
164 query[columnToUpdate] = eventData.count
e4c87ec2 165
6fcd19ba 166 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
d38b8281 167 })
6fcd19ba
C
168 .then(({ videoInstance, qaduType }) => {
169 const qadusParams = [
170 {
171 videoId: videoInstance.id,
172 type: qaduType
173 }
174 ]
175
176 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
e4c87ec2 177 })
6fcd19ba 178 })
0a6658fd 179 .then(() => logger.info('Remote video event processed for video %s.', eventData.uuid))
6fcd19ba 180 .catch(err => {
ad0997ad 181 logger.debug('Cannot process a video event.', err)
6fcd19ba 182 throw err
e4c87ec2
C
183 })
184}
185
4771e000 186function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
9e167724
C
187 const options = {
188 arguments: [ videoData, fromPod ],
189 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
190 }
191
6fcd19ba 192 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
9e167724
C
193}
194
4771e000 195function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
f148e5ed
C
196 let videoName
197
6fcd19ba 198 return db.sequelize.transaction(t => {
0a6658fd 199 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid)
6fcd19ba
C
200 .then(videoInstance => {
201 const options = { transaction: t }
9e167724 202
6fcd19ba 203 videoName = videoInstance.name
f148e5ed 204
6fcd19ba
C
205 if (videoData.views) {
206 videoInstance.set('views', videoData.views)
207 }
9e167724 208
6fcd19ba
C
209 if (videoData.likes) {
210 videoInstance.set('likes', videoData.likes)
211 }
9e167724 212
6fcd19ba
C
213 if (videoData.dislikes) {
214 videoInstance.set('dislikes', videoData.dislikes)
215 }
9e167724 216
6fcd19ba 217 return videoInstance.save(options)
9e167724 218 })
9e167724 219 })
6fcd19ba 220 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
ad0997ad 221 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', err))
9e167724
C
222}
223
ed04d94f 224// Handle retries on fail
4771e000 225function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
d6a5b018
C
226 const options = {
227 arguments: [ videoToCreateData, fromPod ],
228 errorMessage: 'Cannot insert the remote video with many retries.'
229 }
ed04d94f 230
6fcd19ba 231 return retryTransactionWrapper(addRemoteVideo, options)
ed04d94f
C
232}
233
4771e000 234function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
0a6658fd 235 logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
6666aad4 236
6fcd19ba 237 return db.sequelize.transaction(t => {
0a6658fd 238 return db.Video.loadByUUID(videoToCreateData.uuid)
6fcd19ba 239 .then(video => {
0a6658fd 240 if (video) throw new Error('UUID already exists.')
cddadde8 241
6fcd19ba 242 return undefined
cddadde8 243 })
6fcd19ba
C
244 .then(() => {
245 const name = videoToCreateData.author
246 const podId = fromPod.id
247 // This author is from another pod so we do not associate a user
248 const userId = null
feb4bdfd 249
6fcd19ba 250 return db.Author.findOrCreateAuthor(name, podId, userId, t)
feb4bdfd 251 })
6fcd19ba
C
252 .then(author => {
253 const tags = videoToCreateData.tags
feb4bdfd 254
6fcd19ba 255 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
7920c273 256 })
6fcd19ba
C
257 .then(({ author, tagInstances }) => {
258 const videoData = {
259 name: videoToCreateData.name,
0a6658fd 260 uuid: videoToCreateData.uuid,
6fcd19ba
C
261 category: videoToCreateData.category,
262 licence: videoToCreateData.licence,
263 language: videoToCreateData.language,
264 nsfw: videoToCreateData.nsfw,
265 description: videoToCreateData.description,
266 authorId: author.id,
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,
0a6658fd
C
273 dislikes: videoToCreateData.dislikes,
274 remote: true
feb4bdfd
C
275 }
276
6fcd19ba
C
277 const video = db.Video.build(videoData)
278 return { tagInstances, video }
feb4bdfd 279 })
6fcd19ba
C
280 .then(({ tagInstances, video }) => {
281 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
7920c273 282 })
6fcd19ba
C
283 .then(({ tagInstances, video }) => {
284 const options = {
285 transaction: t
286 }
7920c273 287
6fcd19ba 288 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
7920c273 289 })
93e1258c
C
290 .then(({ tagInstances, videoCreated }) => {
291 const tasks = []
292 const options = {
293 transaction: t
294 }
295
296 videoToCreateData.files.forEach(fileData => {
297 const videoFileInstance = db.VideoFile.build({
298 extname: fileData.extname,
299 infoHash: fileData.infoHash,
300 resolution: fileData.resolution,
301 size: fileData.size,
302 videoId: videoCreated.id
303 })
304
305 tasks.push(videoFileInstance.save(options))
306 })
307
308 return Promise.all(tasks).then(() => ({ tagInstances, videoCreated }))
309 })
6fcd19ba
C
310 .then(({ tagInstances, videoCreated }) => {
311 const options = {
312 transaction: t
313 }
7920c273 314
6fcd19ba
C
315 return videoCreated.setTags(tagInstances, options)
316 })
317 })
318 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
319 .catch(err => {
ad0997ad 320 logger.debug('Cannot insert the remote video.', err)
6fcd19ba 321 throw err
7920c273 322 })
528a9efa
C
323}
324
ed04d94f 325// Handle retries on fail
4771e000 326function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
d6a5b018 327 const options = {
fbc22d79 328 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
329 errorMessage: 'Cannot update the remote video with many retries'
330 }
ed04d94f 331
6fcd19ba 332 return retryTransactionWrapper(updateRemoteVideo, options)
ed04d94f
C
333}
334
4771e000 335function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
0a6658fd 336 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
feb4bdfd 337
6fcd19ba 338 return db.sequelize.transaction(t => {
0a6658fd 339 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid)
6fcd19ba
C
340 .then(videoInstance => {
341 const tags = videoAttributesToUpdate.tags
3d118fb5 342
6fcd19ba 343 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
3d118fb5 344 })
6fcd19ba
C
345 .then(({ videoInstance, tagInstances }) => {
346 const options = { transaction: t }
347
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)
362
363 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
3d118fb5 364 })
93e1258c
C
365 .then(({ tagInstances, videoInstance }) => {
366 const tasks = []
367 const options = {
368 transaction: t
369 }
370
371 videoAttributesToUpdate.files.forEach(fileData => {
372 const videoFileInstance = db.VideoFile.build({
373 extname: fileData.extname,
374 infoHash: fileData.infoHash,
375 resolution: fileData.resolution,
376 size: fileData.size,
377 videoId: videoInstance.id
378 })
379
380 tasks.push(videoFileInstance.save(options))
381 })
382
383 return Promise.all(tasks).then(() => ({ tagInstances, videoInstance }))
384 })
6fcd19ba
C
385 .then(({ videoInstance, tagInstances }) => {
386 const options = { transaction: t }
3d118fb5 387
6fcd19ba 388 return videoInstance.setTags(tagInstances, options)
3d118fb5 389 })
6fcd19ba
C
390 })
391 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
392 .catch(err => {
393 // This is just a debug because we will retry the insert
ad0997ad 394 logger.debug('Cannot update the remote video.', err)
6fcd19ba 395 throw err
3d118fb5
C
396 })
397}
398
4771e000 399function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
3d118fb5 400 // We need the instance because we have to remove some other stuffs (thumbnail etc)
0a6658fd 401 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid)
6fcd19ba 402 .then(video => {
0a6658fd 403 logger.debug('Removing remote video %s.', video.uuid)
6fcd19ba
C
404 return video.destroy()
405 })
406 .catch(err => {
0a6658fd 407 logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack })
d8cc063e 408 })
55fa55a9
C
409}
410
4771e000 411function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
0a6658fd 412 return fetchVideoByUUID(reportData.videoUUID)
6fcd19ba
C
413 .then(video => {
414 logger.debug('Reporting remote abuse for video %s.', video.id)
55fa55a9 415
6fcd19ba
C
416 const videoAbuseData = {
417 reporterUsername: reportData.reporterUsername,
418 reason: reportData.reportReason,
419 reporterPodId: fromPod.id,
420 videoId: video.id
d8cc063e
C
421 }
422
6fcd19ba 423 return db.VideoAbuse.create(videoAbuseData)
d8cc063e 424 })
ad0997ad 425 .catch(err => logger.error('Cannot create remote abuse video.', err))
55fa55a9
C
426}
427
0a6658fd
C
428function fetchVideoByUUID (id: string) {
429 return db.Video.loadByUUID(id)
6fcd19ba
C
430 .then(video => {
431 if (!video) throw new Error('Video not found')
e4c87ec2 432
6fcd19ba
C
433 return video
434 })
435 .catch(err => {
ad0997ad 436 logger.error('Cannot load owned video from id.', { error: err.stack, id })
6fcd19ba
C
437 throw err
438 })
e4c87ec2
C
439}
440
0a6658fd
C
441function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
442 return db.Video.loadByHostAndUUID(podHost, uuid)
6fcd19ba
C
443 .then(video => {
444 if (!video) throw new Error('Video not found')
55fa55a9 445
6fcd19ba
C
446 return video
447 })
448 .catch(err => {
0a6658fd 449 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
6fcd19ba
C
450 throw err
451 })
528a9efa 452}