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
)
50 router
.get('/languages', listVideoLanguages
)
55 validatorsPagination
.pagination
,
56 validatorsSort
.videoAbusesSort
,
57 sort
.setVideoAbusesSort
,
58 pagination
.setPagination
,
61 router
.post('/:id/abuse',
63 validatorsVideos
.videoAbuseReport
,
64 reportVideoAbuseRetryWrapper
67 router
.put('/:id/rate',
69 validatorsVideos
.videoRate
,
74 validatorsPagination
.pagination
,
75 validatorsSort
.videosSort
,
77 pagination
.setPagination
,
83 validatorsVideos
.videosUpdate
,
84 updateVideoRetryWrapper
89 validatorsVideos
.videosAdd
,
93 validatorsVideos
.videosGet
,
98 validatorsVideos
.videosRemove
,
101 router
.get('/search/:value',
102 validatorsVideos
.videosSearch
,
103 validatorsPagination
.pagination
,
104 validatorsSort
.videosSort
,
106 pagination
.setPagination
,
107 search
.setVideosSearch
,
111 // ---------------------------------------------------------------------------
113 module
.exports
= router
115 // ---------------------------------------------------------------------------
117 function listVideoCategories (req
, res
, next
) {
118 res
.json(constants
.VIDEO_CATEGORIES
)
121 function listVideoLicences (req
, res
, next
) {
122 res
.json(constants
.VIDEO_LICENCES
)
125 function listVideoLanguages (req
, res
, next
) {
126 res
.json(constants
.VIDEO_LANGUAGES
)
129 function rateVideoRetryWrapper (req
, res
, next
) {
131 arguments: [ req
, res
],
132 errorMessage: 'Cannot update the user video rate.'
135 databaseUtils
.retryTransactionWrapper(rateVideo
, options
, function (err
) {
136 if (err
) return next(err
)
138 return res
.type('json').status(204).end()
142 function rateVideo (req
, res
, finalCallback
) {
143 const rateType
= req
.body
.rating
144 const videoInstance
= res
.locals
.video
145 const userInstance
= res
.locals
.oauth
.token
.User
148 databaseUtils
.startSerializableTransaction
,
150 function findPreviousRate (t
, callback
) {
151 db
.UserVideoRate
.load(userInstance
.id
, videoInstance
.id
, t
, function (err
, previousRate
) {
152 return callback(err
, t
, previousRate
)
156 function insertUserRateIntoDB (t
, previousRate
, callback
) {
157 const options
= { transaction: t
}
159 let likesToIncrement
= 0
160 let dislikesToIncrement
= 0
162 if (rateType
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
++
163 else if (rateType
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
++
165 // There was a previous rate, update it
167 // We will remove the previous rate, so we will need to remove it from the video attribute
168 if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
--
169 else if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
--
171 previousRate
.type
= rateType
173 previousRate
.save(options
).asCallback(function (err
) {
174 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
176 } else { // There was not a previous rate, insert a new one
178 userId: userInstance
.id
,
179 videoId: videoInstance
.id
,
183 db
.UserVideoRate
.create(query
, options
).asCallback(function (err
) {
184 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
189 function updateVideoAttributeDB (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
190 const options
= { transaction: t
}
191 const incrementQuery
= {
192 likes: likesToIncrement
,
193 dislikes: dislikesToIncrement
196 // Even if we do not own the video we increment the attributes
197 // It is usefull for the user to have a feedback
198 videoInstance
.increment(incrementQuery
, options
).asCallback(function (err
) {
199 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
203 function sendEventsToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
204 // No need for an event type, we own the video
205 if (videoInstance
.isOwned()) return callback(null, t
, likesToIncrement
, dislikesToIncrement
)
207 const eventsParams
= []
209 if (likesToIncrement
!== 0) {
211 videoId: videoInstance
.id
,
212 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES
,
213 count: likesToIncrement
217 if (dislikesToIncrement
!== 0) {
219 videoId: videoInstance
.id
,
220 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES
,
221 count: dislikesToIncrement
225 friends
.addEventsToRemoteVideo(eventsParams
, t
, function (err
) {
226 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
230 function sendQaduToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
231 // We do not own the video, there is no need to send a quick and dirty update to friends
232 // Our rate was already sent by the addEvent function
233 if (videoInstance
.isOwned() === false) return callback(null, t
)
235 const qadusParams
= []
237 if (likesToIncrement
!== 0) {
239 videoId: videoInstance
.id
,
240 type: constants
.REQUEST_VIDEO_QADU_TYPES
.LIKES
244 if (dislikesToIncrement
!== 0) {
246 videoId: videoInstance
.id
,
247 type: constants
.REQUEST_VIDEO_QADU_TYPES
.DISLIKES
251 friends
.quickAndDirtyUpdatesVideoToFriends(qadusParams
, t
, function (err
) {
252 return callback(err
, t
)
256 databaseUtils
.commitTransaction
258 ], function (err
, t
) {
260 // This is just a debug because we will retry the insert
261 logger
.debug('Cannot add the user video rate.', { error: err
})
262 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
265 logger
.info('User video rate for video %s of user %s updated.', videoInstance
.name
, userInstance
.username
)
266 return finalCallback(null)
270 // Wrapper to video add that retry the function if there is a database error
271 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
272 function addVideoRetryWrapper (req
, res
, next
) {
274 arguments: [ req
, res
, req
.files
.videofile
[0] ],
275 errorMessage: 'Cannot insert the video with many retries.'
278 databaseUtils
.retryTransactionWrapper(addVideo
, options
, function (err
) {
279 if (err
) return next(err
)
281 // TODO : include Location of the new video -> 201
282 return res
.type('json').status(204).end()
286 function addVideo (req
, res
, videoFile
, finalCallback
) {
287 const videoInfos
= req
.body
291 databaseUtils
.startSerializableTransaction
,
293 function findOrCreateAuthor (t
, callback
) {
294 const user
= res
.locals
.oauth
.token
.User
296 const name
= user
.username
297 // null because it is OUR pod
299 const userId
= user
.id
301 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
302 return callback(err
, t
, authorInstance
)
306 function findOrCreateTags (t
, author
, callback
) {
307 const tags
= videoInfos
.tags
309 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
310 return callback(err
, t
, author
, tagInstances
)
314 function createVideoObject (t
, author
, tagInstances
, callback
) {
316 name: videoInfos
.name
,
318 extname: path
.extname(videoFile
.filename
),
319 category: videoInfos
.category
,
320 licence: videoInfos
.licence
,
321 language: videoInfos
.language
,
322 nsfw: videoInfos
.nsfw
,
323 description: videoInfos
.description
,
324 duration: videoFile
.duration
,
328 const video
= db
.Video
.build(videoData
)
330 return callback(null, t
, author
, tagInstances
, video
)
333 // Set the videoname the same as the id
334 function renameVideoFile (t
, author
, tagInstances
, video
, callback
) {
335 const videoDir
= constants
.CONFIG
.STORAGE
.VIDEOS_DIR
336 const source
= path
.join(videoDir
, videoFile
.filename
)
337 const destination
= path
.join(videoDir
, video
.getVideoFilename())
339 fs
.rename(source
, destination
, function (err
) {
340 if (err
) return callback(err
)
342 // This is important in case if there is another attempt
343 videoFile
.filename
= video
.getVideoFilename()
344 return callback(null, t
, author
, tagInstances
, video
)
348 function insertVideoIntoDB (t
, author
, tagInstances
, video
, callback
) {
349 const options
= { transaction: t
}
351 // Add tags association
352 video
.save(options
).asCallback(function (err
, videoCreated
) {
353 if (err
) return callback(err
)
355 // Do not forget to add Author informations to the created video
356 videoCreated
.Author
= author
358 return callback(err
, t
, tagInstances
, videoCreated
)
362 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
363 const options
= { transaction: t
}
365 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
366 video
.Tags
= tagInstances
368 return callback(err
, t
, video
)
372 function sendToFriends (t
, video
, callback
) {
373 video
.toAddRemoteJSON(function (err
, remoteVideo
) {
374 if (err
) return callback(err
)
376 // Now we'll add the video's meta data to our friends
377 friends
.addVideoToFriends(remoteVideo
, t
, function (err
) {
378 return callback(err
, t
)
383 databaseUtils
.commitTransaction
385 ], function andFinally (err
, t
) {
387 // This is just a debug because we will retry the insert
388 logger
.debug('Cannot insert the video.', { error: err
})
389 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
392 logger
.info('Video with name %s created.', videoInfos
.name
)
393 return finalCallback(null)
397 function updateVideoRetryWrapper (req
, res
, next
) {
399 arguments: [ req
, res
],
400 errorMessage: 'Cannot update the video with many retries.'
403 databaseUtils
.retryTransactionWrapper(updateVideo
, options
, function (err
) {
404 if (err
) return next(err
)
406 // TODO : include Location of the new video -> 201
407 return res
.type('json').status(204).end()
411 function updateVideo (req
, res
, finalCallback
) {
412 const videoInstance
= res
.locals
.video
413 const videoFieldsSave
= videoInstance
.toJSON()
414 const videoInfosToUpdate
= req
.body
418 databaseUtils
.startSerializableTransaction
,
420 function findOrCreateTags (t
, callback
) {
421 if (videoInfosToUpdate
.tags
) {
422 db
.Tag
.findOrCreateTags(videoInfosToUpdate
.tags
, t
, function (err
, tagInstances
) {
423 return callback(err
, t
, tagInstances
)
426 return callback(null, t
, null)
430 function updateVideoIntoDB (t
, tagInstances
, callback
) {
435 if (videoInfosToUpdate
.name
) videoInstance
.set('name', videoInfosToUpdate
.name
)
436 if (videoInfosToUpdate
.category
) videoInstance
.set('category', videoInfosToUpdate
.category
)
437 if (videoInfosToUpdate
.licence
) videoInstance
.set('licence', videoInfosToUpdate
.licence
)
438 if (videoInfosToUpdate
.language
) videoInstance
.set('language', videoInfosToUpdate
.language
)
439 if (videoInfosToUpdate
.nsfw
) videoInstance
.set('nsfw', videoInfosToUpdate
.nsfw
)
440 if (videoInfosToUpdate
.description
) videoInstance
.set('description', videoInfosToUpdate
.description
)
442 videoInstance
.save(options
).asCallback(function (err
) {
443 return callback(err
, t
, tagInstances
)
447 function associateTagsToVideo (t
, tagInstances
, callback
) {
449 const options
= { transaction: t
}
451 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
452 videoInstance
.Tags
= tagInstances
454 return callback(err
, t
)
457 return callback(null, t
)
461 function sendToFriends (t
, callback
) {
462 const json
= videoInstance
.toUpdateRemoteJSON()
464 // Now we'll update the video's meta data to our friends
465 friends
.updateVideoToFriends(json
, t
, function (err
) {
466 return callback(err
, t
)
470 databaseUtils
.commitTransaction
472 ], function andFinally (err
, t
) {
474 logger
.debug('Cannot update the video.', { error: err
})
476 // Force fields we want to update
477 // If the transaction is retried, sequelize will think the object has not changed
478 // So it will skip the SQL request, even if the last one was ROLLBACKed!
479 Object
.keys(videoFieldsSave
).forEach(function (key
) {
480 const value
= videoFieldsSave
[key
]
481 videoInstance
.set(key
, value
)
484 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
487 logger
.info('Video with name %s updated.', videoInfosToUpdate
.name
)
488 return finalCallback(null)
492 function getVideo (req
, res
, next
) {
493 const videoInstance
= res
.locals
.video
495 if (videoInstance
.isOwned()) {
496 // The increment is done directly in the database, not using the instance value
497 videoInstance
.increment('views').asCallback(function (err
) {
499 logger
.error('Cannot add view to video %d.', videoInstance
.id
)
503 // FIXME: make a real view system
504 // For example, only add a view when a user watch a video during 30s etc
506 videoId: videoInstance
.id
,
507 type: constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
509 friends
.quickAndDirtyUpdateVideoToFriends(qaduParams
)
512 // Just send the event to our friends
513 const eventParams
= {
514 videoId: videoInstance
.id
,
515 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS
517 friends
.addEventToRemoteVideo(eventParams
)
520 // Do not wait the view system
521 res
.json(videoInstance
.toFormatedJSON())
524 function listVideos (req
, res
, next
) {
525 db
.Video
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, videosList
, videosTotal
) {
526 if (err
) return next(err
)
528 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
532 function removeVideo (req
, res
, next
) {
533 const videoInstance
= res
.locals
.video
535 videoInstance
.destroy().asCallback(function (err
) {
537 logger
.error('Errors when removed the video.', { error: err
})
541 return res
.type('json').status(204).end()
545 function searchVideos (req
, res
, next
) {
546 db
.Video
.searchAndPopulateAuthorAndPodAndTags(
547 req
.params
.value
, req
.query
.field
, req
.query
.start
, req
.query
.count
, req
.query
.sort
,
548 function (err
, videosList
, videosTotal
) {
549 if (err
) return next(err
)
551 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
556 function listVideoAbuses (req
, res
, next
) {
557 db
.VideoAbuse
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, abusesList
, abusesTotal
) {
558 if (err
) return next(err
)
560 res
.json(utils
.getFormatedObjects(abusesList
, abusesTotal
))
564 function reportVideoAbuseRetryWrapper (req
, res
, next
) {
566 arguments: [ req
, res
],
567 errorMessage: 'Cannot report abuse to the video with many retries.'
570 databaseUtils
.retryTransactionWrapper(reportVideoAbuse
, options
, function (err
) {
571 if (err
) return next(err
)
573 return res
.type('json').status(204).end()
577 function reportVideoAbuse (req
, res
, finalCallback
) {
578 const videoInstance
= res
.locals
.video
579 const reporterUsername
= res
.locals
.oauth
.token
.User
.username
583 reason: req
.body
.reason
,
584 videoId: videoInstance
.id
,
585 reporterPodId: null // This is our pod that reported this abuse
590 databaseUtils
.startSerializableTransaction
,
592 function createAbuse (t
, callback
) {
593 db
.VideoAbuse
.create(abuse
).asCallback(function (err
, abuse
) {
594 return callback(err
, t
, abuse
)
598 function sendToFriendsIfNeeded (t
, abuse
, callback
) {
599 // We send the information to the destination pod
600 if (videoInstance
.isOwned() === false) {
603 reportReason: abuse
.reason
,
604 videoRemoteId: videoInstance
.remoteId
607 friends
.reportAbuseVideoToFriend(reportData
, videoInstance
)
610 return callback(null, t
)
613 databaseUtils
.commitTransaction
615 ], function andFinally (err
, t
) {
617 logger
.debug('Cannot update the video.', { error: err
})
618 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
621 logger
.info('Abuse report for video %s created.', videoInstance
.name
)
622 return finalCallback(null)