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
)
49 router
.get('/licences', listVideoLicences
)
54 validatorsPagination
.pagination
,
55 validatorsSort
.videoAbusesSort
,
56 sort
.setVideoAbusesSort
,
57 pagination
.setPagination
,
60 router
.post('/:id/abuse',
62 validatorsVideos
.videoAbuseReport
,
63 reportVideoAbuseRetryWrapper
66 router
.put('/:id/rate',
68 validatorsVideos
.videoRate
,
73 validatorsPagination
.pagination
,
74 validatorsSort
.videosSort
,
76 pagination
.setPagination
,
82 validatorsVideos
.videosUpdate
,
83 updateVideoRetryWrapper
88 validatorsVideos
.videosAdd
,
92 validatorsVideos
.videosGet
,
97 validatorsVideos
.videosRemove
,
100 router
.get('/search/:value',
101 validatorsVideos
.videosSearch
,
102 validatorsPagination
.pagination
,
103 validatorsSort
.videosSort
,
105 pagination
.setPagination
,
106 search
.setVideosSearch
,
110 // ---------------------------------------------------------------------------
112 module
.exports
= router
114 // ---------------------------------------------------------------------------
116 function listVideoCategories (req
, res
, next
) {
117 res
.json(constants
.VIDEO_CATEGORIES
)
120 function listVideoLicences (req
, res
, next
) {
121 res
.json(constants
.VIDEO_LICENCES
)
124 function rateVideoRetryWrapper (req
, res
, next
) {
126 arguments: [ req
, res
],
127 errorMessage: 'Cannot update the user video rate.'
130 databaseUtils
.retryTransactionWrapper(rateVideo
, options
, function (err
) {
131 if (err
) return next(err
)
133 return res
.type('json').status(204).end()
137 function rateVideo (req
, res
, finalCallback
) {
138 const rateType
= req
.body
.rating
139 const videoInstance
= res
.locals
.video
140 const userInstance
= res
.locals
.oauth
.token
.User
143 databaseUtils
.startSerializableTransaction
,
145 function findPreviousRate (t
, callback
) {
146 db
.UserVideoRate
.load(userInstance
.id
, videoInstance
.id
, t
, function (err
, previousRate
) {
147 return callback(err
, t
, previousRate
)
151 function insertUserRateIntoDB (t
, previousRate
, callback
) {
152 const options
= { transaction: t
}
154 let likesToIncrement
= 0
155 let dislikesToIncrement
= 0
157 if (rateType
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
++
158 else if (rateType
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
++
160 // There was a previous rate, update it
162 // We will remove the previous rate, so we will need to remove it from the video attribute
163 if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
--
164 else if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
--
166 previousRate
.type
= rateType
168 previousRate
.save(options
).asCallback(function (err
) {
169 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
171 } else { // There was not a previous rate, insert a new one
173 userId: userInstance
.id
,
174 videoId: videoInstance
.id
,
178 db
.UserVideoRate
.create(query
, options
).asCallback(function (err
) {
179 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
184 function updateVideoAttributeDB (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
185 const options
= { transaction: t
}
186 const incrementQuery
= {
187 likes: likesToIncrement
,
188 dislikes: dislikesToIncrement
191 // Even if we do not own the video we increment the attributes
192 // It is usefull for the user to have a feedback
193 videoInstance
.increment(incrementQuery
, options
).asCallback(function (err
) {
194 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
198 function sendEventsToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
199 // No need for an event type, we own the video
200 if (videoInstance
.isOwned()) return callback(null, t
, likesToIncrement
, dislikesToIncrement
)
202 const eventsParams
= []
204 if (likesToIncrement
!== 0) {
206 videoId: videoInstance
.id
,
207 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES
,
208 count: likesToIncrement
212 if (dislikesToIncrement
!== 0) {
214 videoId: videoInstance
.id
,
215 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES
,
216 count: dislikesToIncrement
220 friends
.addEventsToRemoteVideo(eventsParams
, t
, function (err
) {
221 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
225 function sendQaduToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
226 // We do not own the video, there is no need to send a quick and dirty update to friends
227 // Our rate was already sent by the addEvent function
228 if (videoInstance
.isOwned() === false) return callback(null, t
)
230 const qadusParams
= []
232 if (likesToIncrement
!== 0) {
234 videoId: videoInstance
.id
,
235 type: constants
.REQUEST_VIDEO_QADU_TYPES
.LIKES
239 if (dislikesToIncrement
!== 0) {
241 videoId: videoInstance
.id
,
242 type: constants
.REQUEST_VIDEO_QADU_TYPES
.DISLIKES
246 friends
.quickAndDirtyUpdatesVideoToFriends(qadusParams
, t
, function (err
) {
247 return callback(err
, t
)
251 databaseUtils
.commitTransaction
253 ], function (err
, t
) {
255 // This is just a debug because we will retry the insert
256 logger
.debug('Cannot add the user video rate.', { error: err
})
257 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
260 logger
.info('User video rate for video %s of user %s updated.', videoInstance
.name
, userInstance
.username
)
261 return finalCallback(null)
265 // Wrapper to video add that retry the function if there is a database error
266 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
267 function addVideoRetryWrapper (req
, res
, next
) {
269 arguments: [ req
, res
, req
.files
.videofile
[0] ],
270 errorMessage: 'Cannot insert the video with many retries.'
273 databaseUtils
.retryTransactionWrapper(addVideo
, options
, function (err
) {
274 if (err
) return next(err
)
276 // TODO : include Location of the new video -> 201
277 return res
.type('json').status(204).end()
281 function addVideo (req
, res
, videoFile
, finalCallback
) {
282 const videoInfos
= req
.body
286 databaseUtils
.startSerializableTransaction
,
288 function findOrCreateAuthor (t
, callback
) {
289 const user
= res
.locals
.oauth
.token
.User
291 const name
= user
.username
292 // null because it is OUR pod
294 const userId
= user
.id
296 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
297 return callback(err
, t
, authorInstance
)
301 function findOrCreateTags (t
, author
, callback
) {
302 const tags
= videoInfos
.tags
304 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
305 return callback(err
, t
, author
, tagInstances
)
309 function createVideoObject (t
, author
, tagInstances
, callback
) {
311 name: videoInfos
.name
,
313 extname: path
.extname(videoFile
.filename
),
314 category: videoInfos
.category
,
315 licence: videoInfos
.licence
,
316 nsfw: videoInfos
.nsfw
,
317 description: videoInfos
.description
,
318 duration: videoFile
.duration
,
322 const video
= db
.Video
.build(videoData
)
324 return callback(null, t
, author
, tagInstances
, video
)
327 // Set the videoname the same as the id
328 function renameVideoFile (t
, author
, tagInstances
, video
, callback
) {
329 const videoDir
= constants
.CONFIG
.STORAGE
.VIDEOS_DIR
330 const source
= path
.join(videoDir
, videoFile
.filename
)
331 const destination
= path
.join(videoDir
, video
.getVideoFilename())
333 fs
.rename(source
, destination
, function (err
) {
334 if (err
) return callback(err
)
336 // This is important in case if there is another attempt
337 videoFile
.filename
= video
.getVideoFilename()
338 return callback(null, t
, author
, tagInstances
, video
)
342 function insertVideoIntoDB (t
, author
, tagInstances
, video
, callback
) {
343 const options
= { transaction: t
}
345 // Add tags association
346 video
.save(options
).asCallback(function (err
, videoCreated
) {
347 if (err
) return callback(err
)
349 // Do not forget to add Author informations to the created video
350 videoCreated
.Author
= author
352 return callback(err
, t
, tagInstances
, videoCreated
)
356 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
357 const options
= { transaction: t
}
359 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
360 video
.Tags
= tagInstances
362 return callback(err
, t
, video
)
366 function sendToFriends (t
, video
, callback
) {
367 video
.toAddRemoteJSON(function (err
, remoteVideo
) {
368 if (err
) return callback(err
)
370 // Now we'll add the video's meta data to our friends
371 friends
.addVideoToFriends(remoteVideo
, t
, function (err
) {
372 return callback(err
, t
)
377 databaseUtils
.commitTransaction
379 ], function andFinally (err
, t
) {
381 // This is just a debug because we will retry the insert
382 logger
.debug('Cannot insert the video.', { error: err
})
383 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
386 logger
.info('Video with name %s created.', videoInfos
.name
)
387 return finalCallback(null)
391 function updateVideoRetryWrapper (req
, res
, next
) {
393 arguments: [ req
, res
],
394 errorMessage: 'Cannot update the video with many retries.'
397 databaseUtils
.retryTransactionWrapper(updateVideo
, options
, function (err
) {
398 if (err
) return next(err
)
400 // TODO : include Location of the new video -> 201
401 return res
.type('json').status(204).end()
405 function updateVideo (req
, res
, finalCallback
) {
406 const videoInstance
= res
.locals
.video
407 const videoFieldsSave
= videoInstance
.toJSON()
408 const videoInfosToUpdate
= req
.body
412 databaseUtils
.startSerializableTransaction
,
414 function findOrCreateTags (t
, callback
) {
415 if (videoInfosToUpdate
.tags
) {
416 db
.Tag
.findOrCreateTags(videoInfosToUpdate
.tags
, t
, function (err
, tagInstances
) {
417 return callback(err
, t
, tagInstances
)
420 return callback(null, t
, null)
424 function updateVideoIntoDB (t
, tagInstances
, callback
) {
429 if (videoInfosToUpdate
.name
) videoInstance
.set('name', videoInfosToUpdate
.name
)
430 if (videoInfosToUpdate
.category
) videoInstance
.set('category', videoInfosToUpdate
.category
)
431 if (videoInfosToUpdate
.licence
) videoInstance
.set('licence', videoInfosToUpdate
.licence
)
432 if (videoInfosToUpdate
.nsfw
) videoInstance
.set('nsfw', videoInfosToUpdate
.nsfw
)
433 if (videoInfosToUpdate
.description
) videoInstance
.set('description', videoInfosToUpdate
.description
)
435 videoInstance
.save(options
).asCallback(function (err
) {
436 return callback(err
, t
, tagInstances
)
440 function associateTagsToVideo (t
, tagInstances
, callback
) {
442 const options
= { transaction: t
}
444 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
445 videoInstance
.Tags
= tagInstances
447 return callback(err
, t
)
450 return callback(null, t
)
454 function sendToFriends (t
, callback
) {
455 const json
= videoInstance
.toUpdateRemoteJSON()
457 // Now we'll update the video's meta data to our friends
458 friends
.updateVideoToFriends(json
, t
, function (err
) {
459 return callback(err
, t
)
463 databaseUtils
.commitTransaction
465 ], function andFinally (err
, t
) {
467 logger
.debug('Cannot update the video.', { error: err
})
469 // Force fields we want to update
470 // If the transaction is retried, sequelize will think the object has not changed
471 // So it will skip the SQL request, even if the last one was ROLLBACKed!
472 Object
.keys(videoFieldsSave
).forEach(function (key
) {
473 const value
= videoFieldsSave
[key
]
474 videoInstance
.set(key
, value
)
477 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
480 logger
.info('Video with name %s updated.', videoInfosToUpdate
.name
)
481 return finalCallback(null)
485 function getVideo (req
, res
, next
) {
486 const videoInstance
= res
.locals
.video
488 if (videoInstance
.isOwned()) {
489 // The increment is done directly in the database, not using the instance value
490 videoInstance
.increment('views').asCallback(function (err
) {
492 logger
.error('Cannot add view to video %d.', videoInstance
.id
)
496 // FIXME: make a real view system
497 // For example, only add a view when a user watch a video during 30s etc
499 videoId: videoInstance
.id
,
500 type: constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
502 friends
.quickAndDirtyUpdateVideoToFriends(qaduParams
)
505 // Just send the event to our friends
506 const eventParams
= {
507 videoId: videoInstance
.id
,
508 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS
510 friends
.addEventToRemoteVideo(eventParams
)
513 // Do not wait the view system
514 res
.json(videoInstance
.toFormatedJSON())
517 function listVideos (req
, res
, next
) {
518 db
.Video
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, videosList
, videosTotal
) {
519 if (err
) return next(err
)
521 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
525 function removeVideo (req
, res
, next
) {
526 const videoInstance
= res
.locals
.video
528 videoInstance
.destroy().asCallback(function (err
) {
530 logger
.error('Errors when removed the video.', { error: err
})
534 return res
.type('json').status(204).end()
538 function searchVideos (req
, res
, next
) {
539 db
.Video
.searchAndPopulateAuthorAndPodAndTags(
540 req
.params
.value
, req
.query
.field
, req
.query
.start
, req
.query
.count
, req
.query
.sort
,
541 function (err
, videosList
, videosTotal
) {
542 if (err
) return next(err
)
544 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
549 function listVideoAbuses (req
, res
, next
) {
550 db
.VideoAbuse
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, abusesList
, abusesTotal
) {
551 if (err
) return next(err
)
553 res
.json(utils
.getFormatedObjects(abusesList
, abusesTotal
))
557 function reportVideoAbuseRetryWrapper (req
, res
, next
) {
559 arguments: [ req
, res
],
560 errorMessage: 'Cannot report abuse to the video with many retries.'
563 databaseUtils
.retryTransactionWrapper(reportVideoAbuse
, options
, function (err
) {
564 if (err
) return next(err
)
566 return res
.type('json').status(204).end()
570 function reportVideoAbuse (req
, res
, finalCallback
) {
571 const videoInstance
= res
.locals
.video
572 const reporterUsername
= res
.locals
.oauth
.token
.User
.username
576 reason: req
.body
.reason
,
577 videoId: videoInstance
.id
,
578 reporterPodId: null // This is our pod that reported this abuse
583 databaseUtils
.startSerializableTransaction
,
585 function createAbuse (t
, callback
) {
586 db
.VideoAbuse
.create(abuse
).asCallback(function (err
, abuse
) {
587 return callback(err
, t
, abuse
)
591 function sendToFriendsIfNeeded (t
, abuse
, callback
) {
592 // We send the information to the destination pod
593 if (videoInstance
.isOwned() === false) {
596 reportReason: abuse
.reason
,
597 videoRemoteId: videoInstance
.remoteId
600 friends
.reportAbuseVideoToFriend(reportData
, videoInstance
)
603 return callback(null, t
)
606 databaseUtils
.commitTransaction
608 ], function andFinally (err
, t
) {
610 logger
.debug('Cannot update the video.', { error: err
})
611 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
614 logger
.info('Abuse report for video %s created.', videoInstance
.name
)
615 return finalCallback(null)