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
,
99 validatorsVideos
.videosRemove
,
103 router
.get('/search/:value',
104 validatorsVideos
.videosSearch
,
105 validatorsPagination
.pagination
,
106 validatorsSort
.videosSort
,
108 pagination
.setPagination
,
109 search
.setVideosSearch
,
113 router
.post('/:id/blacklist',
116 validatorsVideos
.videosBlacklist
,
120 // ---------------------------------------------------------------------------
122 module
.exports
= router
124 // ---------------------------------------------------------------------------
126 function listVideoCategories (req
, res
, next
) {
127 res
.json(constants
.VIDEO_CATEGORIES
)
130 function listVideoLicences (req
, res
, next
) {
131 res
.json(constants
.VIDEO_LICENCES
)
134 function listVideoLanguages (req
, res
, next
) {
135 res
.json(constants
.VIDEO_LANGUAGES
)
138 function rateVideoRetryWrapper (req
, res
, next
) {
140 arguments: [ req
, res
],
141 errorMessage: 'Cannot update the user video rate.'
144 databaseUtils
.retryTransactionWrapper(rateVideo
, options
, function (err
) {
145 if (err
) return next(err
)
147 return res
.type('json').status(204).end()
151 function rateVideo (req
, res
, finalCallback
) {
152 const rateType
= req
.body
.rating
153 const videoInstance
= res
.locals
.video
154 const userInstance
= res
.locals
.oauth
.token
.User
157 databaseUtils
.startSerializableTransaction
,
159 function findPreviousRate (t
, callback
) {
160 db
.UserVideoRate
.load(userInstance
.id
, videoInstance
.id
, t
, function (err
, previousRate
) {
161 return callback(err
, t
, previousRate
)
165 function insertUserRateIntoDB (t
, previousRate
, callback
) {
166 const options
= { transaction: t
}
168 let likesToIncrement
= 0
169 let dislikesToIncrement
= 0
171 if (rateType
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
++
172 else if (rateType
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
++
174 // There was a previous rate, update it
176 // We will remove the previous rate, so we will need to remove it from the video attribute
177 if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.LIKE
) likesToIncrement
--
178 else if (previousRate
.type
=== constants
.VIDEO_RATE_TYPES
.DISLIKE
) dislikesToIncrement
--
180 previousRate
.type
= rateType
182 previousRate
.save(options
).asCallback(function (err
) {
183 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
185 } else { // There was not a previous rate, insert a new one
187 userId: userInstance
.id
,
188 videoId: videoInstance
.id
,
192 db
.UserVideoRate
.create(query
, options
).asCallback(function (err
) {
193 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
198 function updateVideoAttributeDB (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
199 const options
= { transaction: t
}
200 const incrementQuery
= {
201 likes: likesToIncrement
,
202 dislikes: dislikesToIncrement
205 // Even if we do not own the video we increment the attributes
206 // It is usefull for the user to have a feedback
207 videoInstance
.increment(incrementQuery
, options
).asCallback(function (err
) {
208 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
212 function sendEventsToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
213 // No need for an event type, we own the video
214 if (videoInstance
.isOwned()) return callback(null, t
, likesToIncrement
, dislikesToIncrement
)
216 const eventsParams
= []
218 if (likesToIncrement
!== 0) {
220 videoId: videoInstance
.id
,
221 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.LIKES
,
222 count: likesToIncrement
226 if (dislikesToIncrement
!== 0) {
228 videoId: videoInstance
.id
,
229 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.DISLIKES
,
230 count: dislikesToIncrement
234 friends
.addEventsToRemoteVideo(eventsParams
, t
, function (err
) {
235 return callback(err
, t
, likesToIncrement
, dislikesToIncrement
)
239 function sendQaduToFriendsIfNeeded (t
, likesToIncrement
, dislikesToIncrement
, callback
) {
240 // We do not own the video, there is no need to send a quick and dirty update to friends
241 // Our rate was already sent by the addEvent function
242 if (videoInstance
.isOwned() === false) return callback(null, t
)
244 const qadusParams
= []
246 if (likesToIncrement
!== 0) {
248 videoId: videoInstance
.id
,
249 type: constants
.REQUEST_VIDEO_QADU_TYPES
.LIKES
253 if (dislikesToIncrement
!== 0) {
255 videoId: videoInstance
.id
,
256 type: constants
.REQUEST_VIDEO_QADU_TYPES
.DISLIKES
260 friends
.quickAndDirtyUpdatesVideoToFriends(qadusParams
, t
, function (err
) {
261 return callback(err
, t
)
265 databaseUtils
.commitTransaction
267 ], function (err
, t
) {
269 // This is just a debug because we will retry the insert
270 logger
.debug('Cannot add the user video rate.', { error: err
})
271 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
274 logger
.info('User video rate for video %s of user %s updated.', videoInstance
.name
, userInstance
.username
)
275 return finalCallback(null)
279 // Wrapper to video add that retry the function if there is a database error
280 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
281 function addVideoRetryWrapper (req
, res
, next
) {
283 arguments: [ req
, res
, req
.files
.videofile
[0] ],
284 errorMessage: 'Cannot insert the video with many retries.'
287 databaseUtils
.retryTransactionWrapper(addVideo
, options
, function (err
) {
288 if (err
) return next(err
)
290 // TODO : include Location of the new video -> 201
291 return res
.type('json').status(204).end()
295 function addVideo (req
, res
, videoFile
, finalCallback
) {
296 const videoInfos
= req
.body
300 databaseUtils
.startSerializableTransaction
,
302 function findOrCreateAuthor (t
, callback
) {
303 const user
= res
.locals
.oauth
.token
.User
305 const name
= user
.username
306 // null because it is OUR pod
308 const userId
= user
.id
310 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
311 return callback(err
, t
, authorInstance
)
315 function findOrCreateTags (t
, author
, callback
) {
316 const tags
= videoInfos
.tags
318 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
319 return callback(err
, t
, author
, tagInstances
)
323 function createVideoObject (t
, author
, tagInstances
, callback
) {
325 name: videoInfos
.name
,
327 extname: path
.extname(videoFile
.filename
),
328 category: videoInfos
.category
,
329 licence: videoInfos
.licence
,
330 language: videoInfos
.language
,
331 nsfw: videoInfos
.nsfw
,
332 description: videoInfos
.description
,
333 duration: videoFile
.duration
,
337 const video
= db
.Video
.build(videoData
)
339 return callback(null, t
, author
, tagInstances
, video
)
342 // Set the videoname the same as the id
343 function renameVideoFile (t
, author
, tagInstances
, video
, callback
) {
344 const videoDir
= constants
.CONFIG
.STORAGE
.VIDEOS_DIR
345 const source
= path
.join(videoDir
, videoFile
.filename
)
346 const destination
= path
.join(videoDir
, video
.getVideoFilename())
348 fs
.rename(source
, destination
, function (err
) {
349 if (err
) return callback(err
)
351 // This is important in case if there is another attempt
352 videoFile
.filename
= video
.getVideoFilename()
353 return callback(null, t
, author
, tagInstances
, video
)
357 function insertVideoIntoDB (t
, author
, tagInstances
, video
, callback
) {
358 const options
= { transaction: t
}
360 // Add tags association
361 video
.save(options
).asCallback(function (err
, videoCreated
) {
362 if (err
) return callback(err
)
364 // Do not forget to add Author informations to the created video
365 videoCreated
.Author
= author
367 return callback(err
, t
, tagInstances
, videoCreated
)
371 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
372 const options
= { transaction: t
}
374 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
375 video
.Tags
= tagInstances
377 return callback(err
, t
, video
)
381 function sendToFriends (t
, video
, callback
) {
382 // Let transcoding job send the video to friends because the videofile extension might change
383 if (constants
.CONFIG
.TRANSCODING
.ENABLED
=== true) return callback(null, t
)
385 video
.toAddRemoteJSON(function (err
, remoteVideo
) {
386 if (err
) return callback(err
)
388 // Now we'll add the video's meta data to our friends
389 friends
.addVideoToFriends(remoteVideo
, t
, function (err
) {
390 return callback(err
, t
)
395 databaseUtils
.commitTransaction
397 ], function andFinally (err
, t
) {
399 // This is just a debug because we will retry the insert
400 logger
.debug('Cannot insert the video.', { error: err
})
401 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
404 logger
.info('Video with name %s created.', videoInfos
.name
)
405 return finalCallback(null)
409 function updateVideoRetryWrapper (req
, res
, next
) {
411 arguments: [ req
, res
],
412 errorMessage: 'Cannot update the video with many retries.'
415 databaseUtils
.retryTransactionWrapper(updateVideo
, options
, function (err
) {
416 if (err
) return next(err
)
418 // TODO : include Location of the new video -> 201
419 return res
.type('json').status(204).end()
423 function updateVideo (req
, res
, finalCallback
) {
424 const videoInstance
= res
.locals
.video
425 const videoFieldsSave
= videoInstance
.toJSON()
426 const videoInfosToUpdate
= req
.body
430 databaseUtils
.startSerializableTransaction
,
432 function findOrCreateTags (t
, callback
) {
433 if (videoInfosToUpdate
.tags
) {
434 db
.Tag
.findOrCreateTags(videoInfosToUpdate
.tags
, t
, function (err
, tagInstances
) {
435 return callback(err
, t
, tagInstances
)
438 return callback(null, t
, null)
442 function updateVideoIntoDB (t
, tagInstances
, callback
) {
447 if (videoInfosToUpdate
.name
) videoInstance
.set('name', videoInfosToUpdate
.name
)
448 if (videoInfosToUpdate
.category
) videoInstance
.set('category', videoInfosToUpdate
.category
)
449 if (videoInfosToUpdate
.licence
) videoInstance
.set('licence', videoInfosToUpdate
.licence
)
450 if (videoInfosToUpdate
.language
) videoInstance
.set('language', videoInfosToUpdate
.language
)
451 if (videoInfosToUpdate
.nsfw
) videoInstance
.set('nsfw', videoInfosToUpdate
.nsfw
)
452 if (videoInfosToUpdate
.description
) videoInstance
.set('description', videoInfosToUpdate
.description
)
454 videoInstance
.save(options
).asCallback(function (err
) {
455 return callback(err
, t
, tagInstances
)
459 function associateTagsToVideo (t
, tagInstances
, callback
) {
461 const options
= { transaction: t
}
463 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
464 videoInstance
.Tags
= tagInstances
466 return callback(err
, t
)
469 return callback(null, t
)
473 function sendToFriends (t
, callback
) {
474 const json
= videoInstance
.toUpdateRemoteJSON()
476 // Now we'll update the video's meta data to our friends
477 friends
.updateVideoToFriends(json
, t
, function (err
) {
478 return callback(err
, t
)
482 databaseUtils
.commitTransaction
484 ], function andFinally (err
, t
) {
486 logger
.debug('Cannot update the video.', { error: err
})
488 // Force fields we want to update
489 // If the transaction is retried, sequelize will think the object has not changed
490 // So it will skip the SQL request, even if the last one was ROLLBACKed!
491 Object
.keys(videoFieldsSave
).forEach(function (key
) {
492 const value
= videoFieldsSave
[key
]
493 videoInstance
.set(key
, value
)
496 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
499 logger
.info('Video with name %s updated.', videoInfosToUpdate
.name
)
500 return finalCallback(null)
504 function getVideo (req
, res
, next
) {
505 const videoInstance
= res
.locals
.video
507 if (videoInstance
.isOwned()) {
508 // The increment is done directly in the database, not using the instance value
509 videoInstance
.increment('views').asCallback(function (err
) {
511 logger
.error('Cannot add view to video %d.', videoInstance
.id
)
515 // FIXME: make a real view system
516 // For example, only add a view when a user watch a video during 30s etc
518 videoId: videoInstance
.id
,
519 type: constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
521 friends
.quickAndDirtyUpdateVideoToFriends(qaduParams
)
524 // Just send the event to our friends
525 const eventParams
= {
526 videoId: videoInstance
.id
,
527 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS
529 friends
.addEventToRemoteVideo(eventParams
)
532 // Do not wait the view system
533 res
.json(videoInstance
.toFormatedJSON())
536 function listVideos (req
, res
, next
) {
537 db
.Video
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, videosList
, videosTotal
) {
538 if (err
) return next(err
)
540 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
544 function removeVideo (req
, res
, next
) {
545 const videoInstance
= res
.locals
.video
547 videoInstance
.destroy().asCallback(function (err
) {
549 logger
.error('Errors when removed the video.', { error: err
})
553 return res
.type('json').status(204).end()
557 function searchVideos (req
, res
, next
) {
558 db
.Video
.searchAndPopulateAuthorAndPodAndTags(
559 req
.params
.value
, req
.query
.field
, req
.query
.start
, req
.query
.count
, req
.query
.sort
,
560 function (err
, videosList
, videosTotal
) {
561 if (err
) return next(err
)
563 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
568 function listVideoAbuses (req
, res
, next
) {
569 db
.VideoAbuse
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, abusesList
, abusesTotal
) {
570 if (err
) return next(err
)
572 res
.json(utils
.getFormatedObjects(abusesList
, abusesTotal
))
576 function reportVideoAbuseRetryWrapper (req
, res
, next
) {
578 arguments: [ req
, res
],
579 errorMessage: 'Cannot report abuse to the video with many retries.'
582 databaseUtils
.retryTransactionWrapper(reportVideoAbuse
, options
, function (err
) {
583 if (err
) return next(err
)
585 return res
.type('json').status(204).end()
589 function reportVideoAbuse (req
, res
, finalCallback
) {
590 const videoInstance
= res
.locals
.video
591 const reporterUsername
= res
.locals
.oauth
.token
.User
.username
595 reason: req
.body
.reason
,
596 videoId: videoInstance
.id
,
597 reporterPodId: null // This is our pod that reported this abuse
602 databaseUtils
.startSerializableTransaction
,
604 function createAbuse (t
, callback
) {
605 db
.VideoAbuse
.create(abuse
).asCallback(function (err
, abuse
) {
606 return callback(err
, t
, abuse
)
610 function sendToFriendsIfNeeded (t
, abuse
, callback
) {
611 // We send the information to the destination pod
612 if (videoInstance
.isOwned() === false) {
615 reportReason: abuse
.reason
,
616 videoRemoteId: videoInstance
.remoteId
619 friends
.reportAbuseVideoToFriend(reportData
, videoInstance
)
622 return callback(null, t
)
625 databaseUtils
.commitTransaction
627 ], function andFinally (err
, t
) {
629 logger
.debug('Cannot update the video.', { error: err
})
630 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
633 logger
.info('Abuse report for video %s created.', videoInstance
.name
)
634 return finalCallback(null)
638 function addVideoToBlacklist (req
, res
, next
) {
639 const videoInstance
= res
.locals
.video
642 videoId: videoInstance
.id
645 db
.BlacklistedVideo
.create(toCreate
).asCallback(function (err
) {
647 logger
.error('Errors when blacklisting video ', { error: err
})
651 return res
.type('json').status(204).end()