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