]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
Add like/dislike system for videos
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos.js
1 'use strict'
2
3 const express = require('express')
4 const fs = require('fs')
5 const multer = require('multer')
6 const path = require('path')
7 const waterfall = require('async/waterfall')
8
9 const constants = require('../../initializers/constants')
10 const db = require('../../initializers/database')
11 const logger = require('../../helpers/logger')
12 const friends = require('../../lib/friends')
13 const middlewares = require('../../middlewares')
14 const admin = middlewares.admin
15 const oAuth = middlewares.oauth
16 const pagination = middlewares.pagination
17 const validators = middlewares.validators
18 const validatorsPagination = validators.pagination
19 const validatorsSort = validators.sort
20 const validatorsVideos = validators.videos
21 const search = middlewares.search
22 const sort = middlewares.sort
23 const databaseUtils = require('../../helpers/database-utils')
24 const utils = require('../../helpers/utils')
25
26 const router = express.Router()
27
28 // multer configuration
29 const storage = multer.diskStorage({
30 destination: function (req, file, cb) {
31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
32 },
33
34 filename: function (req, file, cb) {
35 let extension = ''
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'
39 utils.generateRandomString(16, function (err, randomString) {
40 const fieldname = err ? undefined : randomString
41 cb(null, fieldname + '.' + extension)
42 })
43 }
44 })
45
46 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
47
48 router.get('/abuse',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsPagination.pagination,
52 validatorsSort.videoAbusesSort,
53 sort.setVideoAbusesSort,
54 pagination.setPagination,
55 listVideoAbuses
56 )
57 router.post('/:id/abuse',
58 oAuth.authenticate,
59 validatorsVideos.videoAbuseReport,
60 reportVideoAbuseRetryWrapper
61 )
62
63 router.put('/:id/rate',
64 oAuth.authenticate,
65 validatorsVideos.videoRate,
66 rateVideoRetryWrapper
67 )
68
69 router.get('/',
70 validatorsPagination.pagination,
71 validatorsSort.videosSort,
72 sort.setVideosSort,
73 pagination.setPagination,
74 listVideos
75 )
76 router.put('/:id',
77 oAuth.authenticate,
78 reqFiles,
79 validatorsVideos.videosUpdate,
80 updateVideoRetryWrapper
81 )
82 router.post('/',
83 oAuth.authenticate,
84 reqFiles,
85 validatorsVideos.videosAdd,
86 addVideoRetryWrapper
87 )
88 router.get('/:id',
89 validatorsVideos.videosGet,
90 getVideo
91 )
92 router.delete('/:id',
93 oAuth.authenticate,
94 validatorsVideos.videosRemove,
95 removeVideo
96 )
97 router.get('/search/:value',
98 validatorsVideos.videosSearch,
99 validatorsPagination.pagination,
100 validatorsSort.videosSort,
101 sort.setVideosSort,
102 pagination.setPagination,
103 search.setVideosSearch,
104 searchVideos
105 )
106
107 // ---------------------------------------------------------------------------
108
109 module.exports = router
110
111 // ---------------------------------------------------------------------------
112
113 function 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
126 function 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
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
256 function addVideoRetryWrapper (req, res, next) {
257 const options = {
258 arguments: [ req, res, req.files.videofile[0] ],
259 errorMessage: 'Cannot insert the video with many retries.'
260 }
261
262 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
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 })
268 }
269
270 function addVideo (req, res, videoFile, finalCallback) {
271 const videoInfos = req.body
272
273 waterfall([
274
275 databaseUtils.startSerializableTransaction,
276
277 function findOrCreateAuthor (t, callback) {
278 const user = res.locals.oauth.token.User
279
280 const name = user.username
281 // null because it is OUR pod
282 const podId = null
283 const userId = user.id
284
285 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
286 return callback(err, t, authorInstance)
287 })
288 },
289
290 function findOrCreateTags (t, author, callback) {
291 const tags = videoInfos.tags
292
293 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
294 return callback(err, t, author, tagInstances)
295 })
296 },
297
298 function createVideoObject (t, author, tagInstances, callback) {
299 const videoData = {
300 name: videoInfos.name,
301 remoteId: null,
302 extname: path.extname(videoFile.filename),
303 description: videoInfos.description,
304 duration: videoFile.duration,
305 authorId: author.id
306 }
307
308 const video = db.Video.build(videoData)
309
310 return callback(null, t, author, tagInstances, video)
311 },
312
313 // Set the videoname the same as the id
314 function renameVideoFile (t, author, tagInstances, video, callback) {
315 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
316 const source = path.join(videoDir, videoFile.filename)
317 const destination = path.join(videoDir, video.getVideoFilename())
318
319 fs.rename(source, destination, function (err) {
320 if (err) return callback(err)
321
322 // This is important in case if there is another attempt
323 videoFile.filename = video.getVideoFilename()
324 return callback(null, t, author, tagInstances, video)
325 })
326 },
327
328 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
329 const options = { transaction: t }
330
331 // Add tags association
332 video.save(options).asCallback(function (err, videoCreated) {
333 if (err) return callback(err)
334
335 // Do not forget to add Author informations to the created video
336 videoCreated.Author = author
337
338 return callback(err, t, tagInstances, videoCreated)
339 })
340 },
341
342 function associateTagsToVideo (t, tagInstances, video, callback) {
343 const options = { transaction: t }
344
345 video.setTags(tagInstances, options).asCallback(function (err) {
346 video.Tags = tagInstances
347
348 return callback(err, t, video)
349 })
350 },
351
352 function sendToFriends (t, video, callback) {
353 video.toAddRemoteJSON(function (err, remoteVideo) {
354 if (err) return callback(err)
355
356 // Now we'll add the video's meta data to our friends
357 friends.addVideoToFriends(remoteVideo, t, function (err) {
358 return callback(err, t)
359 })
360 })
361 },
362
363 databaseUtils.commitTransaction
364
365 ], function andFinally (err, t) {
366 if (err) {
367 // This is just a debug because we will retry the insert
368 logger.debug('Cannot insert the video.', { error: err })
369 return databaseUtils.rollbackTransaction(err, t, finalCallback)
370 }
371
372 logger.info('Video with name %s created.', videoInfos.name)
373 return finalCallback(null)
374 })
375 }
376
377 function updateVideoRetryWrapper (req, res, next) {
378 const options = {
379 arguments: [ req, res ],
380 errorMessage: 'Cannot update the video with many retries.'
381 }
382
383 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
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 })
389 }
390
391 function updateVideo (req, res, finalCallback) {
392 const videoInstance = res.locals.video
393 const videoFieldsSave = videoInstance.toJSON()
394 const videoInfosToUpdate = req.body
395
396 waterfall([
397
398 databaseUtils.startSerializableTransaction,
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) {
411 const options = {
412 transaction: t
413 }
414
415 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
416 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
417
418 videoInstance.save(options).asCallback(function (err) {
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
441 friends.updateVideoToFriends(json, t, function (err) {
442 return callback(err, t)
443 })
444 },
445
446 databaseUtils.commitTransaction
447
448 ], function andFinally (err, t) {
449 if (err) {
450 logger.debug('Cannot update the video.', { error: err })
451
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
460 return databaseUtils.rollbackTransaction(err, t, finalCallback)
461 }
462
463 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
464 return finalCallback(null)
465 })
466 }
467
468 function getVideo (req, res, next) {
469 const videoInstance = res.locals.video
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
481 const qaduParams = {
482 videoId: videoInstance.id,
483 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
484 }
485 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
486 })
487 } else {
488 // Just send the event to our friends
489 const eventParams = {
490 videoId: videoInstance.id,
491 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
492 }
493 friends.addEventToRemoteVideo(eventParams)
494 }
495
496 // Do not wait the view system
497 res.json(videoInstance.toFormatedJSON())
498 }
499
500 function listVideos (req, res, next) {
501 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
502 if (err) return next(err)
503
504 res.json(utils.getFormatedObjects(videosList, videosTotal))
505 })
506 }
507
508 function removeVideo (req, res, next) {
509 const videoInstance = res.locals.video
510
511 videoInstance.destroy().asCallback(function (err) {
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()
518 })
519 }
520
521 function searchVideos (req, res, next) {
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)
526
527 res.json(utils.getFormatedObjects(videosList, videosTotal))
528 }
529 )
530 }
531
532 function 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)
535
536 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
537 })
538 }
539
540 function reportVideoAbuseRetryWrapper (req, res, next) {
541 const options = {
542 arguments: [ req, res ],
543 errorMessage: 'Cannot report abuse to the video with many retries.'
544 }
545
546 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
547 if (err) return next(err)
548
549 return res.type('json').status(204).end()
550 })
551 }
552
553 function reportVideoAbuse (req, res, finalCallback) {
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
562 }
563
564 waterfall([
565
566 databaseUtils.startSerializableTransaction,
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 }
582
583 friends.reportAbuseVideoToFriend(reportData, videoInstance)
584 }
585
586 return callback(null, t)
587 },
588
589 databaseUtils.commitTransaction
590
591 ], function andFinally (err, t) {
592 if (err) {
593 logger.debug('Cannot update the video.', { error: err })
594 return databaseUtils.rollbackTransaction(err, t, finalCallback)
595 }
596
597 logger.info('Abuse report for video %s created.', videoInstance.name)
598 return finalCallback(null)
599 })
600 }