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')
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')
26 const router = express.Router()
28 // multer configuration
29 const storage = multer.diskStorage({
30 destination: function (req, file, cb) {
31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
34 filename: function (req, file, cb) {
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)
46 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
48 router.get('/categories', listVideoCategories)
53 validatorsPagination.pagination,
54 validatorsSort.videoAbusesSort,
55 sort.setVideoAbusesSort,
56 pagination.setPagination,
59 router.post('/:id/abuse',
61 validatorsVideos.videoAbuseReport,
62 reportVideoAbuseRetryWrapper
65 router.put('/:id/rate',
67 validatorsVideos.videoRate,
72 validatorsPagination.pagination,
73 validatorsSort.videosSort,
75 pagination.setPagination,
81 validatorsVideos.videosUpdate,
82 updateVideoRetryWrapper
87 validatorsVideos.videosAdd,
91 validatorsVideos.videosGet,
96 validatorsVideos.videosRemove,
99 router.get('/search/:value',
100 validatorsVideos.videosSearch,
101 validatorsPagination.pagination,
102 validatorsSort.videosSort,
104 pagination.setPagination,
105 search.setVideosSearch,
109 // ---------------------------------------------------------------------------
111 module.exports = router
113 // ---------------------------------------------------------------------------
115 function listVideoCategories (req, res, next) {
116 res.json(constants.VIDEO_CATEGORIES)
119 function rateVideoRetryWrapper (req, res, next) {
121 arguments: [ req, res ],
122 errorMessage: 'Cannot update the user video rate.'
125 databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
126 if (err) return next(err)
128 return res.type('json').status(204).end()
132 function rateVideo (req, res, finalCallback) {
133 const rateType = req.body.rating
134 const videoInstance = res.locals.video
135 const userInstance = res.locals.oauth.token.User
138 databaseUtils.startSerializableTransaction,
140 function findPreviousRate (t, callback) {
141 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
142 return callback(err, t, previousRate)
146 function insertUserRateIntoDB (t, previousRate, callback) {
147 const options = { transaction: t }
149 let likesToIncrement = 0
150 let dislikesToIncrement = 0
152 if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
153 else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
155 // There was a previous rate, update it
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--
161 previousRate.type = rateType
163 previousRate.save(options).asCallback(function (err) {
164 return callback(err, t, likesToIncrement, dislikesToIncrement)
166 } else { // There was not a previous rate, insert a new one
168 userId: userInstance.id,
169 videoId: videoInstance.id,
173 db.UserVideoRate.create(query, options).asCallback(function (err) {
174 return callback(err, t, likesToIncrement, dislikesToIncrement)
179 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
180 const options = { transaction: t }
181 const incrementQuery = {
182 likes: likesToIncrement,
183 dislikes: dislikesToIncrement
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)
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)
197 const eventsParams = []
199 if (likesToIncrement !== 0) {
201 videoId: videoInstance.id,
202 type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
203 count: likesToIncrement
207 if (dislikesToIncrement !== 0) {
209 videoId: videoInstance.id,
210 type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
211 count: dislikesToIncrement
215 friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
216 return callback(err, t, likesToIncrement, dislikesToIncrement)
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)
225 const qadusParams = []
227 if (likesToIncrement !== 0) {
229 videoId: videoInstance.id,
230 type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
234 if (dislikesToIncrement !== 0) {
236 videoId: videoInstance.id,
237 type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
241 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
242 return callback(err, t)
246 databaseUtils.commitTransaction
248 ], function (err, t) {
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)
255 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
256 return finalCallback(null)
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
262 function addVideoRetryWrapper (req, res, next) {
264 arguments: [ req, res, req.files.videofile[0] ],
265 errorMessage: 'Cannot insert the video with many retries.'
268 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
269 if (err) return next(err)
271 // TODO : include Location of the new video -> 201
272 return res.type('json').status(204).end()
276 function addVideo (req, res, videoFile, finalCallback) {
277 const videoInfos = req.body
281 databaseUtils.startSerializableTransaction,
283 function findOrCreateAuthor (t, callback) {
284 const user = res.locals.oauth.token.User
286 const name = user.username
287 // null because it is OUR pod
289 const userId = user.id
291 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
292 return callback(err, t, authorInstance)
296 function findOrCreateTags (t, author, callback) {
297 const tags = videoInfos.tags
299 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
300 return callback(err, t, author, tagInstances)
304 function createVideoObject (t, author, tagInstances, callback) {
306 name: videoInfos.name,
308 extname: path.extname(videoFile.filename),
309 category: videoInfos.category,
310 description: videoInfos.description,
311 duration: videoFile.duration,
315 const video = db.Video.build(videoData)
317 return callback(null, t, author, tagInstances, video)
320 // Set the videoname the same as the id
321 function renameVideoFile (t, author, tagInstances, video, callback) {
322 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
323 const source = path.join(videoDir, videoFile.filename)
324 const destination = path.join(videoDir, video.getVideoFilename())
326 fs.rename(source, destination, function (err) {
327 if (err) return callback(err)
329 // This is important in case if there is another attempt
330 videoFile.filename = video.getVideoFilename()
331 return callback(null, t, author, tagInstances, video)
335 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
336 const options = { transaction: t }
338 // Add tags association
339 video.save(options).asCallback(function (err, videoCreated) {
340 if (err) return callback(err)
342 // Do not forget to add Author informations to the created video
343 videoCreated.Author = author
345 return callback(err, t, tagInstances, videoCreated)
349 function associateTagsToVideo (t, tagInstances, video, callback) {
350 const options = { transaction: t }
352 video.setTags(tagInstances, options).asCallback(function (err) {
353 video.Tags = tagInstances
355 return callback(err, t, video)
359 function sendToFriends (t, video, callback) {
360 video.toAddRemoteJSON(function (err, remoteVideo) {
361 if (err) return callback(err)
363 // Now we'll add the video's meta data to our friends
364 friends.addVideoToFriends(remoteVideo, t, function (err) {
365 return callback(err, t)
370 databaseUtils.commitTransaction
372 ], function andFinally (err, t) {
374 // This is just a debug because we will retry the insert
375 logger.debug('Cannot insert the video.', { error: err })
376 return databaseUtils.rollbackTransaction(err, t, finalCallback)
379 logger.info('Video with name %s created.', videoInfos.name)
380 return finalCallback(null)
384 function updateVideoRetryWrapper (req, res, next) {
386 arguments: [ req, res ],
387 errorMessage: 'Cannot update the video with many retries.'
390 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
391 if (err) return next(err)
393 // TODO : include Location of the new video -> 201
394 return res.type('json').status(204).end()
398 function updateVideo (req, res, finalCallback) {
399 const videoInstance = res.locals.video
400 const videoFieldsSave = videoInstance.toJSON()
401 const videoInfosToUpdate = req.body
405 databaseUtils.startSerializableTransaction,
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)
413 return callback(null, t, null)
417 function updateVideoIntoDB (t, tagInstances, callback) {
422 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
423 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
424 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
426 videoInstance.save(options).asCallback(function (err) {
427 return callback(err, t, tagInstances)
431 function associateTagsToVideo (t, tagInstances, callback) {
433 const options = { transaction: t }
435 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
436 videoInstance.Tags = tagInstances
438 return callback(err, t)
441 return callback(null, t)
445 function sendToFriends (t, callback) {
446 const json = videoInstance.toUpdateRemoteJSON()
448 // Now we'll update the video's meta data to our friends
449 friends.updateVideoToFriends(json, t, function (err) {
450 return callback(err, t)
454 databaseUtils.commitTransaction
456 ], function andFinally (err, t) {
458 logger.debug('Cannot update the video.', { error: err })
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)
468 return databaseUtils.rollbackTransaction(err, t, finalCallback)
471 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
472 return finalCallback(null)
476 function getVideo (req, res, next) {
477 const videoInstance = res.locals.video
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) {
483 logger.error('Cannot add view to video %d.', videoInstance.id)
487 // FIXME: make a real view system
488 // For example, only add a view when a user watch a video during 30s etc
490 videoId: videoInstance.id,
491 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
493 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
496 // Just send the event to our friends
497 const eventParams = {
498 videoId: videoInstance.id,
499 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
501 friends.addEventToRemoteVideo(eventParams)
504 // Do not wait the view system
505 res.json(videoInstance.toFormatedJSON())
508 function listVideos (req, res, next) {
509 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
510 if (err) return next(err)
512 res.json(utils.getFormatedObjects(videosList, videosTotal))
516 function removeVideo (req, res, next) {
517 const videoInstance = res.locals.video
519 videoInstance.destroy().asCallback(function (err) {
521 logger.error('Errors when removed the video.', { error: err })
525 return res.type('json').status(204).end()
529 function searchVideos (req, res, next) {
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)
535 res.json(utils.getFormatedObjects(videosList, videosTotal))
540 function 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)
544 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
548 function reportVideoAbuseRetryWrapper (req, res, next) {
550 arguments: [ req, res ],
551 errorMessage: 'Cannot report abuse to the video with many retries.'
554 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
555 if (err) return next(err)
557 return res.type('json').status(204).end()
561 function reportVideoAbuse (req, res, finalCallback) {
562 const videoInstance = res.locals.video
563 const reporterUsername = res.locals.oauth.token.User.username
567 reason: req.body.reason,
568 videoId: videoInstance.id,
569 reporterPodId: null // This is our pod that reported this abuse
574 databaseUtils.startSerializableTransaction,
576 function createAbuse (t, callback) {
577 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
578 return callback(err, t, abuse)
582 function sendToFriendsIfNeeded (t, abuse, callback) {
583 // We send the information to the destination pod
584 if (videoInstance.isOwned() === false) {
587 reportReason: abuse.reason,
588 videoRemoteId: videoInstance.remoteId
591 friends.reportAbuseVideoToFriend(reportData, videoInstance)
594 return callback(null, t)
597 databaseUtils.commitTransaction
599 ], function andFinally (err, t) {
601 logger.debug('Cannot update the video.', { error: err })
602 return databaseUtils.rollbackTransaction(err, t, finalCallback)
605 logger.info('Abuse report for video %s created.', videoInstance.name)
606 return finalCallback(null)