]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Client: fix prod compilation
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos.js
CommitLineData
9f10b292
C
1'use strict'
2
f0f5567b 3const express = require('express')
558d7c23 4const fs = require('fs')
f0f5567b 5const multer = require('multer')
558d7c23 6const path = require('path')
1a42c9e2 7const waterfall = require('async/waterfall')
f0f5567b 8
f253b1c1 9const constants = require('../../initializers/constants')
feb4bdfd 10const db = require('../../initializers/database')
f253b1c1
C
11const logger = require('../../helpers/logger')
12const friends = require('../../lib/friends')
13const middlewares = require('../../middlewares')
55fa55a9 14const admin = middlewares.admin
69b0a27c 15const oAuth = middlewares.oauth
fbf1134e 16const pagination = middlewares.pagination
fc51fde0
C
17const validators = middlewares.validators
18const validatorsPagination = validators.pagination
19const validatorsSort = validators.sort
20const validatorsVideos = validators.videos
46246b5f 21const search = middlewares.search
a877d5ac 22const sort = middlewares.sort
4df023f2 23const databaseUtils = require('../../helpers/database-utils')
f253b1c1 24const utils = require('../../helpers/utils')
f0f5567b
C
25
26const router = express.Router()
9f10b292
C
27
28// multer configuration
f0f5567b 29const storage = multer.diskStorage({
9f10b292 30 destination: function (req, file, cb) {
b3d92510 31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
9f10b292
C
32 },
33
34 filename: function (req, file, cb) {
f0f5567b 35 let extension = ''
9f10b292
C
36 if (file.mimetype === 'video/webm') extension = 'webm'
37 else if (file.mimetype === 'video/mp4') extension = 'mp4'
38 else if (file.mimetype === 'video/ogg') extension = 'ogv'
bc503c2a
C
39 utils.generateRandomString(16, function (err, randomString) {
40 const fieldname = err ? undefined : randomString
9f10b292
C
41 cb(null, fieldname + '.' + extension)
42 })
43 }
44})
45
8c9c1942 46const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
8c308c2b 47
55fa55a9
C
48router.get('/abuse',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsPagination.pagination,
52 validatorsSort.videoAbusesSort,
53 sort.setVideoAbusesSort,
54 pagination.setPagination,
55 listVideoAbuses
56)
57router.post('/:id/abuse',
58 oAuth.authenticate,
59 validatorsVideos.videoAbuseReport,
bf4ff8fe 60 reportVideoAbuseRetryWrapper
55fa55a9
C
61)
62
d38b8281
C
63router.put('/:id/rate',
64 oAuth.authenticate,
65 validatorsVideos.videoRate,
66 rateVideoRetryWrapper
67)
68
fbf1134e 69router.get('/',
fc51fde0
C
70 validatorsPagination.pagination,
71 validatorsSort.videosSort,
a877d5ac 72 sort.setVideosSort,
fbf1134e
C
73 pagination.setPagination,
74 listVideos
75)
7b1f49de
C
76router.put('/:id',
77 oAuth.authenticate,
78 reqFiles,
79 validatorsVideos.videosUpdate,
ed04d94f 80 updateVideoRetryWrapper
7b1f49de 81)
fbf1134e 82router.post('/',
69b0a27c 83 oAuth.authenticate,
fbf1134e 84 reqFiles,
fc51fde0 85 validatorsVideos.videosAdd,
ed04d94f 86 addVideoRetryWrapper
fbf1134e
C
87)
88router.get('/:id',
fc51fde0 89 validatorsVideos.videosGet,
68ce3ae0 90 getVideo
fbf1134e
C
91)
92router.delete('/:id',
69b0a27c 93 oAuth.authenticate,
fc51fde0 94 validatorsVideos.videosRemove,
fbf1134e
C
95 removeVideo
96)
46246b5f 97router.get('/search/:value',
fc51fde0
C
98 validatorsVideos.videosSearch,
99 validatorsPagination.pagination,
100 validatorsSort.videosSort,
a877d5ac 101 sort.setVideosSort,
fbf1134e 102 pagination.setPagination,
46246b5f 103 search.setVideosSearch,
fbf1134e
C
104 searchVideos
105)
8c308c2b 106
9f10b292 107// ---------------------------------------------------------------------------
c45f7f84 108
9f10b292 109module.exports = router
c45f7f84 110
9f10b292 111// ---------------------------------------------------------------------------
c45f7f84 112
d38b8281
C
113function rateVideoRetryWrapper (req, res, next) {
114 const options = {
115 arguments: [ req, res ],
116 errorMessage: 'Cannot update the user video rate.'
117 }
118
119 databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
120 if (err) return next(err)
121
122 return res.type('json').status(204).end()
123 })
124}
125
126function rateVideo (req, res, finalCallback) {
127 const rateType = req.body.rating
128 const videoInstance = res.locals.video
129 const userInstance = res.locals.oauth.token.User
130
131 waterfall([
132 databaseUtils.startSerializableTransaction,
133
134 function findPreviousRate (t, callback) {
135 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
136 return callback(err, t, previousRate)
137 })
138 },
139
140 function insertUserRateIntoDB (t, previousRate, callback) {
141 const options = { transaction: t }
142
143 let likesToIncrement = 0
144 let dislikesToIncrement = 0
145
146 if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
147 else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
148
149 // There was a previous rate, update it
150 if (previousRate) {
151 // We will remove the previous rate, so we will need to remove it from the video attribute
152 if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
153 else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
154
155 previousRate.type = rateType
156
157 previousRate.save(options).asCallback(function (err) {
158 return callback(err, t, likesToIncrement, dislikesToIncrement)
159 })
160 } else { // There was not a previous rate, insert a new one
161 const query = {
162 userId: userInstance.id,
163 videoId: videoInstance.id,
164 type: rateType
165 }
166
167 db.UserVideoRate.create(query, options).asCallback(function (err) {
168 return callback(err, t, likesToIncrement, dislikesToIncrement)
169 })
170 }
171 },
172
173 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
174 const options = { transaction: t }
175 const incrementQuery = {
176 likes: likesToIncrement,
177 dislikes: dislikesToIncrement
178 }
179
180 // Even if we do not own the video we increment the attributes
181 // It is usefull for the user to have a feedback
182 videoInstance.increment(incrementQuery, options).asCallback(function (err) {
183 return callback(err, t, likesToIncrement, dislikesToIncrement)
184 })
185 },
186
187 function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
188 // No need for an event type, we own the video
189 if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
190
191 const eventsParams = []
192
193 if (likesToIncrement !== 0) {
194 eventsParams.push({
195 videoId: videoInstance.id,
196 type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
197 count: likesToIncrement
198 })
199 }
200
201 if (dislikesToIncrement !== 0) {
202 eventsParams.push({
203 videoId: videoInstance.id,
204 type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
205 count: dislikesToIncrement
206 })
207 }
208
209 friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
210 return callback(err, t, likesToIncrement, dislikesToIncrement)
211 })
212 },
213
214 function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
215 // We do not own the video, there is no need to send a quick and dirty update to friends
216 // Our rate was already sent by the addEvent function
217 if (videoInstance.isOwned() === false) return callback(null, t)
218
219 const qadusParams = []
220
221 if (likesToIncrement !== 0) {
222 qadusParams.push({
223 videoId: videoInstance.id,
224 type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
225 })
226 }
227
228 if (dislikesToIncrement !== 0) {
229 qadusParams.push({
230 videoId: videoInstance.id,
231 type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
232 })
233 }
234
235 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
236 return callback(err, t)
237 })
238 },
239
240 databaseUtils.commitTransaction
241
242 ], function (err, t) {
243 if (err) {
244 // This is just a debug because we will retry the insert
245 logger.debug('Cannot add the user video rate.', { error: err })
246 return databaseUtils.rollbackTransaction(err, t, finalCallback)
247 }
248
249 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
250 return finalCallback(null)
251 })
252}
253
ed04d94f
C
254// Wrapper to video add that retry the function if there is a database error
255// We need this because we run the transaction in SERIALIZABLE isolation that can fail
256function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
257 const options = {
258 arguments: [ req, res, req.files.videofile[0] ],
259 errorMessage: 'Cannot insert the video with many retries.'
260 }
ed04d94f 261
4df023f2 262 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
263 if (err) return next(err)
264
265 // TODO : include Location of the new video -> 201
266 return res.type('json').status(204).end()
267 })
ed04d94f
C
268}
269
4145c1c6 270function addVideo (req, res, videoFile, finalCallback) {
bc503c2a 271 const videoInfos = req.body
9f10b292 272
1a42c9e2 273 waterfall([
807df9e6 274
4df023f2 275 databaseUtils.startSerializableTransaction,
7920c273 276
4145c1c6 277 function findOrCreateAuthor (t, callback) {
4712081f 278 const user = res.locals.oauth.token.User
feb4bdfd 279
4ff0d862
C
280 const name = user.username
281 // null because it is OUR pod
282 const podId = null
283 const userId = user.id
4712081f 284
4ff0d862 285 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
4145c1c6 286 return callback(err, t, authorInstance)
7920c273
C
287 })
288 },
289
4145c1c6 290 function findOrCreateTags (t, author, callback) {
7920c273 291 const tags = videoInfos.tags
4ff0d862
C
292
293 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
4145c1c6 294 return callback(err, t, author, tagInstances)
feb4bdfd
C
295 })
296 },
297
4145c1c6 298 function createVideoObject (t, author, tagInstances, callback) {
807df9e6
C
299 const videoData = {
300 name: videoInfos.name,
558d7c23
C
301 remoteId: null,
302 extname: path.extname(videoFile.filename),
807df9e6 303 description: videoInfos.description,
67100f1f 304 duration: videoFile.duration,
d38b8281 305 authorId: author.id
807df9e6
C
306 }
307
feb4bdfd 308 const video = db.Video.build(videoData)
558d7c23 309
4145c1c6 310 return callback(null, t, author, tagInstances, video)
558d7c23
C
311 },
312
feb4bdfd 313 // Set the videoname the same as the id
4145c1c6 314 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
315 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
316 const source = path.join(videoDir, videoFile.filename)
f285faa0 317 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
318
319 fs.rename(source, destination, function (err) {
4145c1c6 320 if (err) return callback(err)
ed04d94f
C
321
322 // This is important in case if there is another attempt
323 videoFile.filename = video.getVideoFilename()
4145c1c6 324 return callback(null, t, author, tagInstances, video)
558d7c23
C
325 })
326 },
327
4145c1c6 328 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
329 const options = { transaction: t }
330
331 // Add tags association
332 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 333 if (err) return callback(err)
7920c273 334
feb4bdfd
C
335 // Do not forget to add Author informations to the created video
336 videoCreated.Author = author
337
4145c1c6 338 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 339 })
807df9e6
C
340 },
341
4145c1c6 342 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
343 const options = { transaction: t }
344
345 video.setTags(tagInstances, options).asCallback(function (err) {
346 video.Tags = tagInstances
347
4145c1c6 348 return callback(err, t, video)
7920c273
C
349 })
350 },
351
4145c1c6 352 function sendToFriends (t, video, callback) {
7b1f49de 353 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 354 if (err) return callback(err)
807df9e6 355
528a9efa 356 // Now we'll add the video's meta data to our friends
ed04d94f 357 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 358 return callback(err, t)
ed04d94f 359 })
528a9efa 360 })
4145c1c6
C
361 },
362
363 databaseUtils.commitTransaction
807df9e6 364
7b1f49de
C
365 ], function andFinally (err, t) {
366 if (err) {
ed04d94f
C
367 // This is just a debug because we will retry the insert
368 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 369 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
370 }
371
4145c1c6
C
372 logger.info('Video with name %s created.', videoInfos.name)
373 return finalCallback(null)
7b1f49de
C
374 })
375}
376
ed04d94f 377function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
378 const options = {
379 arguments: [ req, res ],
380 errorMessage: 'Cannot update the video with many retries.'
381 }
ed04d94f 382
4df023f2 383 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
384 if (err) return next(err)
385
386 // TODO : include Location of the new video -> 201
387 return res.type('json').status(204).end()
388 })
ed04d94f
C
389}
390
391function updateVideo (req, res, finalCallback) {
818f7987 392 const videoInstance = res.locals.video
7f4e7c36 393 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
394 const videoInfosToUpdate = req.body
395
396 waterfall([
397
4df023f2 398 databaseUtils.startSerializableTransaction,
7b1f49de
C
399
400 function findOrCreateTags (t, callback) {
401 if (videoInfosToUpdate.tags) {
402 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
403 return callback(err, t, tagInstances)
404 })
405 } else {
406 return callback(null, t, null)
407 }
408 },
409
410 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
411 const options = {
412 transaction: t
413 }
7b1f49de
C
414
415 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
416 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
417
7b1f49de 418 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
419 return callback(err, t, tagInstances)
420 })
421 },
422
423 function associateTagsToVideo (t, tagInstances, callback) {
424 if (tagInstances) {
425 const options = { transaction: t }
426
427 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
428 videoInstance.Tags = tagInstances
429
430 return callback(err, t)
431 })
432 } else {
433 return callback(null, t)
434 }
435 },
436
437 function sendToFriends (t, callback) {
438 const json = videoInstance.toUpdateRemoteJSON()
439
440 // Now we'll update the video's meta data to our friends
ed04d94f
C
441 friends.updateVideoToFriends(json, t, function (err) {
442 return callback(err, t)
443 })
4145c1c6
C
444 },
445
446 databaseUtils.commitTransaction
7b1f49de 447
7920c273 448 ], function andFinally (err, t) {
807df9e6 449 if (err) {
ed04d94f 450 logger.debug('Cannot update the video.', { error: err })
7920c273 451
7f4e7c36
C
452 // Force fields we want to update
453 // If the transaction is retried, sequelize will think the object has not changed
454 // So it will skip the SQL request, even if the last one was ROLLBACKed!
455 Object.keys(videoFieldsSave).forEach(function (key) {
456 const value = videoFieldsSave[key]
457 videoInstance.set(key, value)
458 })
459
4145c1c6 460 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
461 }
462
4145c1c6
C
463 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
464 return finalCallback(null)
9f10b292
C
465 })
466}
8c308c2b 467
68ce3ae0 468function getVideo (req, res, next) {
818f7987 469 const videoInstance = res.locals.video
9e167724
C
470
471 if (videoInstance.isOwned()) {
472 // The increment is done directly in the database, not using the instance value
473 videoInstance.increment('views').asCallback(function (err) {
474 if (err) {
475 logger.error('Cannot add view to video %d.', videoInstance.id)
476 return
477 }
478
479 // FIXME: make a real view system
480 // For example, only add a view when a user watch a video during 30s etc
d38b8281
C
481 const qaduParams = {
482 videoId: videoInstance.id,
483 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
484 }
485 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
9e167724 486 })
e4c87ec2
C
487 } else {
488 // Just send the event to our friends
d38b8281
C
489 const eventParams = {
490 videoId: videoInstance.id,
491 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
492 }
493 friends.addEventToRemoteVideo(eventParams)
9e167724
C
494 }
495
496 // Do not wait the view system
818f7987 497 res.json(videoInstance.toFormatedJSON())
9f10b292 498}
8c308c2b 499
9f10b292 500function listVideos (req, res, next) {
feb4bdfd 501 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 502 if (err) return next(err)
c45f7f84 503
55fa55a9 504 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
505 })
506}
c45f7f84 507
9f10b292 508function removeVideo (req, res, next) {
818f7987 509 const videoInstance = res.locals.video
8c308c2b 510
818f7987 511 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
512 if (err) {
513 logger.error('Errors when removed the video.', { error: err })
514 return next(err)
515 }
516
517 return res.type('json').status(204).end()
9f10b292
C
518 })
519}
8c308c2b 520
9f10b292 521function searchVideos (req, res, next) {
7920c273
C
522 db.Video.searchAndPopulateAuthorAndPodAndTags(
523 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
524 function (err, videosList, videosTotal) {
525 if (err) return next(err)
8c308c2b 526
55fa55a9 527 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
528 }
529 )
9f10b292 530}
c173e565 531
55fa55a9
C
532function listVideoAbuses (req, res, next) {
533 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
534 if (err) return next(err)
2df82d42 535
55fa55a9 536 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 537 })
55fa55a9 538}
2df82d42 539
bf4ff8fe 540function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
541 const options = {
542 arguments: [ req, res ],
543 errorMessage: 'Cannot report abuse to the video with many retries.'
544 }
bf4ff8fe 545
4df023f2
C
546 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
547 if (err) return next(err)
548
549 return res.type('json').status(204).end()
550 })
bf4ff8fe
C
551}
552
553function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
554 const videoInstance = res.locals.video
555 const reporterUsername = res.locals.oauth.token.User.username
556
557 const abuse = {
558 reporterUsername,
559 reason: req.body.reason,
560 videoId: videoInstance.id,
561 reporterPodId: null // This is our pod that reported this abuse
68ce3ae0 562 }
55fa55a9 563
bf4ff8fe
C
564 waterfall([
565
da691c46 566 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
567
568 function createAbuse (t, callback) {
569 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
570 return callback(err, t, abuse)
571 })
572 },
573
574 function sendToFriendsIfNeeded (t, abuse, callback) {
575 // We send the information to the destination pod
576 if (videoInstance.isOwned() === false) {
577 const reportData = {
578 reporterUsername,
579 reportReason: abuse.reason,
580 videoRemoteId: videoInstance.remoteId
581 }
55fa55a9 582
bf4ff8fe 583 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
584 }
585
bf4ff8fe 586 return callback(null, t)
4145c1c6
C
587 },
588
589 databaseUtils.commitTransaction
55fa55a9 590
bf4ff8fe
C
591 ], function andFinally (err, t) {
592 if (err) {
593 logger.debug('Cannot update the video.', { error: err })
4145c1c6 594 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
595 }
596
4145c1c6
C
597 logger.info('Abuse report for video %s created.', videoInstance.name)
598 return finalCallback(null)
55fa55a9 599 })
2df82d42 600}