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)