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 }])
51 validatorsPagination
.pagination
,
52 validatorsSort
.videoAbusesSort
,
53 sort
.setVideoAbusesSort
,
54 pagination
.setPagination
,
57 router
.post('/:id/abuse',
59 validatorsVideos
.videoAbuseReport
,
60 reportVideoAbuseRetryWrapper
63 router
.put('/:id/rate',
65 validatorsVideos
.videoRate
,
70 validatorsPagination
.pagination
,
71 validatorsSort
.videosSort
,
73 pagination
.setPagination
,
79 validatorsVideos
.videosUpdate
,
80 updateVideoRetryWrapper
85 validatorsVideos
.videosAdd
,
89 validatorsVideos
.videosGet
,
94 validatorsVideos
.videosRemove
,
97 router
.get('/search/:value',
98 validatorsVideos
.videosSearch
,
99 validatorsPagination
.pagination
,
100 validatorsSort
.videosSort
,
102 pagination
.setPagination
,
103 search
.setVideosSearch
,
107 // ---------------------------------------------------------------------------
109 module
.exports
= router
111 // ---------------------------------------------------------------------------
113 function rateVideoRetryWrapper (req
, res
, next
) {
115 arguments: [ req
, res
],
116 errorMessage: 'Cannot update the user video rate.'
119 databaseUtils
.retryTransactionWrapper(rateVideo
, options
, function (err
) {
120 if (err
) return next(err
)
122 return res
.type('json').status(204).end()
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
132 databaseUtils
.startSerializableTransaction
,
134 function findPreviousRate (t
, callback
) {
135 db
.UserVideoRate
.load(userInstance
.id
, videoInstance
.id
, t
, function (err
, previousRate
) {
136 return callback(err
, t
, previousRate
)
140 function insertUserRateIntoDB (t
, previousRate
, callback
) {
141 const options
= { transaction: t
}
143 let likesToIncrement
= 0
144 let dislikesToIncrement
= 0
146 if (rateType
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
++
147 else if (rateType
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
++
149 // There was a previous rate, update it
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
--
155 previousRate
.type
= rateType
157 previousRate
.save(options
).asCallback(function (err
) {
158 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
160 } else { // There was not a previous rate, insert a new one
162 userId: userInstance
.id
,
163 videoId: videoInstance
.id
,
167 db
.UserVideoRate
.create(query
, options
).asCallback(function (err
) {
168 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
173 function updateVideoAttributeDB (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
174 const options
= { transaction: t
}
175 const incrementQuery
= {
176 likes: likesToIncrement
,
177 dislikes: dislikesToIncrement
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
)
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
)
191 const eventsParams
= []
193 if (likesToIncrement
!== 0) {
195 videoId: videoInstance
.id
,
196 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES
,
197 count: likesToIncrement
201 if (dislikesToIncrement
!== 0) {
203 videoId: videoInstance
.id
,
204 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES
,
205 count: dislikesToIncrement
209 friends
.addEventsToRemoteVideo(eventsParams
, t
, function (err
) {
210 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
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
)
219 const qadusParams
= []
221 if (likesToIncrement
!== 0) {
223 videoId: videoInstance
.id
,
224 type: constants
.REQUEST_VIDEO_QADU_TYPES
.LIKES
228 if (dislikesToIncrement
!== 0) {
230 videoId: videoInstance
.id
,
231 type: constants
.REQUEST_VIDEO_QADU_TYPES
.DISLIKES
235 friends
.quickAndDirtyUpdatesVideoToFriends(qadusParams
, t
, function (err
) {
236 return callback(err
, t
)
240 databaseUtils
.commitTransaction
242 ], function (err
, t
) {
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
)
249 logger
.info('User video rate for video %s of user %s updated.', videoInstance
.name
, userInstance
.username
)
250 return finalCallback(null)
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
) {
258 arguments: [ req
, res
, req
.files
.videofile
[0] ],
259 errorMessage: 'Cannot insert the video with many retries.'
262 databaseUtils
.retryTransactionWrapper(addVideo
, options
, function (err
) {
263 if (err
) return next(err
)
265 // TODO : include Location of the new video -> 201
266 return res
.type('json').status(204).end()
270 function addVideo (req
, res
, videoFile
, finalCallback
) {
271 const videoInfos
= req
.body
275 databaseUtils
.startSerializableTransaction
,
277 function findOrCreateAuthor (t
, callback
) {
278 const user
= res
.locals
.oauth
.token
.User
280 const name
= user
.username
281 // null because it is OUR pod
283 const userId
= user
.id
285 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
286 return callback(err
, t
, authorInstance
)
290 function findOrCreateTags (t
, author
, callback
) {
291 const tags
= videoInfos
.tags
293 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
294 return callback(err
, t
, author
, tagInstances
)
298 function createVideoObject (t
, author
, tagInstances
, callback
) {
300 name: videoInfos
.name
,
302 extname: path
.extname(videoFile
.filename
),
303 description: videoInfos
.description
,
304 duration: videoFile
.duration
,
308 const video
= db
.Video
.build(videoData
)
310 return callback(null, t
, author
, tagInstances
, video
)
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())
319 fs
.rename(source
, destination
, function (err
) {
320 if (err
) return callback(err
)
322 // This is important in case if there is another attempt
323 videoFile
.filename
= video
.getVideoFilename()
324 return callback(null, t
, author
, tagInstances
, video
)
328 function insertVideoIntoDB (t
, author
, tagInstances
, video
, callback
) {
329 const options
= { transaction: t
}
331 // Add tags association
332 video
.save(options
).asCallback(function (err
, videoCreated
) {
333 if (err
) return callback(err
)
335 // Do not forget to add Author informations to the created video
336 videoCreated
.Author
= author
338 return callback(err
, t
, tagInstances
, videoCreated
)
342 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
343 const options
= { transaction: t
}
345 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
346 video
.Tags
= tagInstances
348 return callback(err
, t
, video
)
352 function sendToFriends (t
, video
, callback
) {
353 video
.toAddRemoteJSON(function (err
, remoteVideo
) {
354 if (err
) return callback(err
)
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
)
363 databaseUtils
.commitTransaction
365 ], function andFinally (err
, t
) {
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
)
372 logger
.info('Video with name %s created.', videoInfos
.name
)
373 return finalCallback(null)
377 function updateVideoRetryWrapper (req
, res
, next
) {
379 arguments: [ req
, res
],
380 errorMessage: 'Cannot update the video with many retries.'
383 databaseUtils
.retryTransactionWrapper(updateVideo
, options
, function (err
) {
384 if (err
) return next(err
)
386 // TODO : include Location of the new video -> 201
387 return res
.type('json').status(204).end()
391 function updateVideo (req
, res
, finalCallback
) {
392 const videoInstance
= res
.locals
.video
393 const videoFieldsSave
= videoInstance
.toJSON()
394 const videoInfosToUpdate
= req
.body
398 databaseUtils
.startSerializableTransaction
,
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
)
406 return callback(null, t
, null)
410 function updateVideoIntoDB (t
, tagInstances
, callback
) {
415 if (videoInfosToUpdate
.name
) videoInstance
.set('name', videoInfosToUpdate
.name
)
416 if (videoInfosToUpdate
.description
) videoInstance
.set('description', videoInfosToUpdate
.description
)
418 videoInstance
.save(options
).asCallback(function (err
) {
419 return callback(err
, t
, tagInstances
)
423 function associateTagsToVideo (t
, tagInstances
, callback
) {
425 const options
= { transaction: t
}
427 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
428 videoInstance
.Tags
= tagInstances
430 return callback(err
, t
)
433 return callback(null, t
)
437 function sendToFriends (t
, callback
) {
438 const json
= videoInstance
.toUpdateRemoteJSON()
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
)
446 databaseUtils
.commitTransaction
448 ], function andFinally (err
, t
) {
450 logger
.debug('Cannot update the video.', { error: err
})
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
)
460 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
463 logger
.info('Video with name %s updated.', videoInfosToUpdate
.name
)
464 return finalCallback(null)
468 function getVideo (req
, res
, next
) {
469 const videoInstance
= res
.locals
.video
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
) {
475 logger
.error('Cannot add view to video %d.', videoInstance
.id
)
479 // FIXME: make a real view system
480 // For example, only add a view when a user watch a video during 30s etc
482 videoId: videoInstance
.id
,
483 type: constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
485 friends
.quickAndDirtyUpdateVideoToFriends(qaduParams
)
488 // Just send the event to our friends
489 const eventParams
= {
490 videoId: videoInstance
.id
,
491 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS
493 friends
.addEventToRemoteVideo(eventParams
)
496 // Do not wait the view system
497 res
.json(videoInstance
.toFormatedJSON())
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
)
504 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
508 function removeVideo (req
, res
, next
) {
509 const videoInstance
= res
.locals
.video
511 videoInstance
.destroy().asCallback(function (err
) {
513 logger
.error('Errors when removed the video.', { error: err
})
517 return res
.type('json').status(204).end()
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
)
527 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
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
)
536 res
.json(utils
.getFormatedObjects(abusesList
, abusesTotal
))
540 function reportVideoAbuseRetryWrapper (req
, res
, next
) {
542 arguments: [ req
, res
],
543 errorMessage: 'Cannot report abuse to the video with many retries.'
546 databaseUtils
.retryTransactionWrapper(reportVideoAbuse
, options
, function (err
) {
547 if (err
) return next(err
)
549 return res
.type('json').status(204).end()
553 function reportVideoAbuse (req
, res
, finalCallback
) {
554 const videoInstance
= res
.locals
.video
555 const reporterUsername
= res
.locals
.oauth
.token
.User
.username
559 reason: req
.body
.reason
,
560 videoId: videoInstance
.id
,
561 reporterPodId: null // This is our pod that reported this abuse
566 databaseUtils
.startSerializableTransaction
,
568 function createAbuse (t
, callback
) {
569 db
.VideoAbuse
.create(abuse
).asCallback(function (err
, abuse
) {
570 return callback(err
, t
, abuse
)
574 function sendToFriendsIfNeeded (t
, abuse
, callback
) {
575 // We send the information to the destination pod
576 if (videoInstance
.isOwned() === false) {
579 reportReason: abuse
.reason
,
580 videoRemoteId: videoInstance
.remoteId
583 friends
.reportAbuseVideoToFriend(reportData
, videoInstance
)
586 return callback(null, t
)
589 databaseUtils
.commitTransaction
591 ], function andFinally (err
, t
) {
593 logger
.debug('Cannot update the video.', { error: err
})
594 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
597 logger
.info('Abuse report for video %s created.', videoInstance
.name
)
598 return finalCallback(null)