]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.ts
Fix tests
[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'
69818c93 20import { PodInstance, VideoInstance } 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 extname: videoToCreateData.extname,
262 infoHash: videoToCreateData.infoHash,
263 category: videoToCreateData.category,
264 licence: videoToCreateData.licence,
265 language: videoToCreateData.language,
266 nsfw: videoToCreateData.nsfw,
267 description: videoToCreateData.description,
268 authorId: author.id,
269 duration: videoToCreateData.duration,
270 createdAt: videoToCreateData.createdAt,
271 // FIXME: updatedAt does not seems to be considered by Sequelize
272 updatedAt: videoToCreateData.updatedAt,
273 views: videoToCreateData.views,
274 likes: videoToCreateData.likes,
0a6658fd
C
275 dislikes: videoToCreateData.dislikes,
276 remote: true
feb4bdfd
C
277 }
278
6fcd19ba
C
279 const video = db.Video.build(videoData)
280 return { tagInstances, video }
feb4bdfd 281 })
6fcd19ba
C
282 .then(({ tagInstances, video }) => {
283 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
7920c273 284 })
6fcd19ba
C
285 .then(({ tagInstances, video }) => {
286 const options = {
287 transaction: t
288 }
7920c273 289
6fcd19ba 290 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
7920c273 291 })
6fcd19ba
C
292 .then(({ tagInstances, videoCreated }) => {
293 const options = {
294 transaction: t
295 }
7920c273 296
6fcd19ba
C
297 return videoCreated.setTags(tagInstances, options)
298 })
299 })
300 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
301 .catch(err => {
ad0997ad 302 logger.debug('Cannot insert the remote video.', err)
6fcd19ba 303 throw err
7920c273 304 })
528a9efa
C
305}
306
ed04d94f 307// Handle retries on fail
4771e000 308function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
d6a5b018 309 const options = {
fbc22d79 310 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
311 errorMessage: 'Cannot update the remote video with many retries'
312 }
ed04d94f 313
6fcd19ba 314 return retryTransactionWrapper(updateRemoteVideo, options)
ed04d94f
C
315}
316
4771e000 317function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
0a6658fd 318 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
feb4bdfd 319
6fcd19ba 320 return db.sequelize.transaction(t => {
0a6658fd 321 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid)
6fcd19ba
C
322 .then(videoInstance => {
323 const tags = videoAttributesToUpdate.tags
3d118fb5 324
6fcd19ba 325 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
3d118fb5 326 })
6fcd19ba
C
327 .then(({ videoInstance, tagInstances }) => {
328 const options = { transaction: t }
329
330 videoInstance.set('name', videoAttributesToUpdate.name)
331 videoInstance.set('category', videoAttributesToUpdate.category)
332 videoInstance.set('licence', videoAttributesToUpdate.licence)
333 videoInstance.set('language', videoAttributesToUpdate.language)
334 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
335 videoInstance.set('description', videoAttributesToUpdate.description)
336 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
337 videoInstance.set('duration', videoAttributesToUpdate.duration)
338 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
339 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
340 videoInstance.set('extname', videoAttributesToUpdate.extname)
341 videoInstance.set('views', videoAttributesToUpdate.views)
342 videoInstance.set('likes', videoAttributesToUpdate.likes)
343 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
344
345 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
3d118fb5 346 })
6fcd19ba
C
347 .then(({ videoInstance, tagInstances }) => {
348 const options = { transaction: t }
3d118fb5 349
6fcd19ba 350 return videoInstance.setTags(tagInstances, options)
3d118fb5 351 })
6fcd19ba
C
352 })
353 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
354 .catch(err => {
355 // This is just a debug because we will retry the insert
ad0997ad 356 logger.debug('Cannot update the remote video.', err)
6fcd19ba 357 throw err
3d118fb5
C
358 })
359}
360
4771e000 361function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
3d118fb5 362 // We need the instance because we have to remove some other stuffs (thumbnail etc)
0a6658fd 363 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid)
6fcd19ba 364 .then(video => {
0a6658fd 365 logger.debug('Removing remote video %s.', video.uuid)
6fcd19ba
C
366 return video.destroy()
367 })
368 .catch(err => {
0a6658fd 369 logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack })
d8cc063e 370 })
55fa55a9
C
371}
372
4771e000 373function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
0a6658fd 374 return fetchVideoByUUID(reportData.videoUUID)
6fcd19ba
C
375 .then(video => {
376 logger.debug('Reporting remote abuse for video %s.', video.id)
55fa55a9 377
6fcd19ba
C
378 const videoAbuseData = {
379 reporterUsername: reportData.reporterUsername,
380 reason: reportData.reportReason,
381 reporterPodId: fromPod.id,
382 videoId: video.id
d8cc063e
C
383 }
384
6fcd19ba 385 return db.VideoAbuse.create(videoAbuseData)
d8cc063e 386 })
ad0997ad 387 .catch(err => logger.error('Cannot create remote abuse video.', err))
55fa55a9
C
388}
389
0a6658fd
C
390function fetchVideoByUUID (id: string) {
391 return db.Video.loadByUUID(id)
6fcd19ba
C
392 .then(video => {
393 if (!video) throw new Error('Video not found')
e4c87ec2 394
6fcd19ba
C
395 return video
396 })
397 .catch(err => {
ad0997ad 398 logger.error('Cannot load owned video from id.', { error: err.stack, id })
6fcd19ba
C
399 throw err
400 })
e4c87ec2
C
401}
402
0a6658fd
C
403function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
404 return db.Video.loadByHostAndUUID(podHost, uuid)
6fcd19ba
C
405 .then(video => {
406 if (!video) throw new Error('Video not found')
55fa55a9 407
6fcd19ba
C
408 return video
409 })
410 .catch(err => {
0a6658fd 411 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
6fcd19ba
C
412 throw err
413 })
528a9efa 414}