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 oAuth
= middlewares
.oauth
15 const pagination
= middlewares
.pagination
16 const validators
= middlewares
.validators
17 const validatorsPagination
= validators
.pagination
18 const validatorsSort
= validators
.sort
19 const validatorsVideos
= validators
.videos
20 const search
= middlewares
.search
21 const sort
= middlewares
.sort
22 const databaseUtils
= require('../../../helpers/database-utils')
23 const utils
= require('../../../helpers/utils')
25 const abuseController
= require('./abuse')
26 const blacklistController
= require('./blacklist')
27 const rateController
= require('./rate')
29 const router
= express
.Router()
31 // multer configuration
32 const storage
= multer
.diskStorage({
33 destination: function (req
, file
, cb
) {
34 cb(null, constants
.CONFIG
.STORAGE
.VIDEOS_DIR
)
37 filename: function (req
, file
, cb
) {
39 if (file
.mimetype
=== 'video/webm') extension
= 'webm'
40 else if (file
.mimetype
=== 'video/mp4') extension
= 'mp4'
41 else if (file
.mimetype
=== 'video/ogg') extension
= 'ogv'
42 utils
.generateRandomString(16, function (err
, randomString
) {
43 const fieldname
= err
? undefined : randomString
44 cb(null, fieldname
+ '.' + extension
)
49 const reqFiles
= multer({ storage: storage
}).fields([{ name: 'videofile', maxCount: 1 }])
51 router
.use('/', abuseController
)
52 router
.use('/', blacklistController
)
53 router
.use('/', rateController
)
55 router
.get('/categories', listVideoCategories
)
56 router
.get('/licences', listVideoLicences
)
57 router
.get('/languages', listVideoLanguages
)
60 validatorsPagination
.pagination
,
61 validatorsSort
.videosSort
,
63 pagination
.setPagination
,
69 validatorsVideos
.videosUpdate
,
70 updateVideoRetryWrapper
75 validatorsVideos
.videosAdd
,
79 validatorsVideos
.videosGet
,
85 validatorsVideos
.videosRemove
,
89 router
.get('/search/:value',
90 validatorsVideos
.videosSearch
,
91 validatorsPagination
.pagination
,
92 validatorsSort
.videosSort
,
94 pagination
.setPagination
,
95 search
.setVideosSearch
,
99 // ---------------------------------------------------------------------------
101 module
.exports
= router
103 // ---------------------------------------------------------------------------
105 function listVideoCategories (req
, res
, next
) {
106 res
.json(constants
.VIDEO_CATEGORIES
)
109 function listVideoLicences (req
, res
, next
) {
110 res
.json(constants
.VIDEO_LICENCES
)
113 function listVideoLanguages (req
, res
, next
) {
114 res
.json(constants
.VIDEO_LANGUAGES
)
117 // Wrapper to video add that retry the function if there is a database error
118 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
119 function addVideoRetryWrapper (req
, res
, next
) {
121 arguments: [ req
, res
, req
.files
.videofile
[0] ],
122 errorMessage: 'Cannot insert the video with many retries.'
125 databaseUtils
.retryTransactionWrapper(addVideo
, options
, function (err
) {
126 if (err
) return next(err
)
128 // TODO : include Location of the new video -> 201
129 return res
.type('json').status(204).end()
133 function addVideo (req
, res
, videoFile
, finalCallback
) {
134 const videoInfos
= req
.body
138 databaseUtils
.startSerializableTransaction
,
140 function findOrCreateAuthor (t
, callback
) {
141 const user
= res
.locals
.oauth
.token
.User
143 const name
= user
.username
144 // null because it is OUR pod
146 const userId
= user
.id
148 db
.Author
.findOrCreateAuthor(name
, podId
, userId
, t
, function (err
, authorInstance
) {
149 return callback(err
, t
, authorInstance
)
153 function findOrCreateTags (t
, author
, callback
) {
154 const tags
= videoInfos
.tags
156 db
.Tag
.findOrCreateTags(tags
, t
, function (err
, tagInstances
) {
157 return callback(err
, t
, author
, tagInstances
)
161 function createVideoObject (t
, author
, tagInstances
, callback
) {
163 name: videoInfos
.name
,
165 extname: path
.extname(videoFile
.filename
),
166 category: videoInfos
.category
,
167 licence: videoInfos
.licence
,
168 language: videoInfos
.language
,
169 nsfw: videoInfos
.nsfw
,
170 description: videoInfos
.description
,
171 duration: videoFile
.duration
,
175 const video
= db
.Video
.build(videoData
)
177 return callback(null, t
, author
, tagInstances
, video
)
180 // Set the videoname the same as the id
181 function renameVideoFile (t
, author
, tagInstances
, video
, callback
) {
182 const videoDir
= constants
.CONFIG
.STORAGE
.VIDEOS_DIR
183 const source
= path
.join(videoDir
, videoFile
.filename
)
184 const destination
= path
.join(videoDir
, video
.getVideoFilename())
186 fs
.rename(source
, destination
, function (err
) {
187 if (err
) return callback(err
)
189 // This is important in case if there is another attempt
190 videoFile
.filename
= video
.getVideoFilename()
191 return callback(null, t
, author
, tagInstances
, video
)
195 function insertVideoIntoDB (t
, author
, tagInstances
, video
, callback
) {
196 const options
= { transaction: t
}
198 // Add tags association
199 video
.save(options
).asCallback(function (err
, videoCreated
) {
200 if (err
) return callback(err
)
202 // Do not forget to add Author informations to the created video
203 videoCreated
.Author
= author
205 return callback(err
, t
, tagInstances
, videoCreated
)
209 function associateTagsToVideo (t
, tagInstances
, video
, callback
) {
210 const options
= { transaction: t
}
212 video
.setTags(tagInstances
, options
).asCallback(function (err
) {
213 video
.Tags
= tagInstances
215 return callback(err
, t
, video
)
219 function sendToFriends (t
, video
, callback
) {
220 // Let transcoding job send the video to friends because the videofile extension might change
221 if (constants
.CONFIG
.TRANSCODING
.ENABLED
=== true) return callback(null, t
)
223 video
.toAddRemoteJSON(function (err
, remoteVideo
) {
224 if (err
) return callback(err
)
226 // Now we'll add the video's meta data to our friends
227 friends
.addVideoToFriends(remoteVideo
, t
, function (err
) {
228 return callback(err
, t
)
233 databaseUtils
.commitTransaction
235 ], function andFinally (err
, t
) {
237 // This is just a debug because we will retry the insert
238 logger
.debug('Cannot insert the video.', { error: err
})
239 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
242 logger
.info('Video with name %s created.', videoInfos
.name
)
243 return finalCallback(null)
247 function updateVideoRetryWrapper (req
, res
, next
) {
249 arguments: [ req
, res
],
250 errorMessage: 'Cannot update the video with many retries.'
253 databaseUtils
.retryTransactionWrapper(updateVideo
, options
, function (err
) {
254 if (err
) return next(err
)
256 // TODO : include Location of the new video -> 201
257 return res
.type('json').status(204).end()
261 function updateVideo (req
, res
, finalCallback
) {
262 const videoInstance
= res
.locals
.video
263 const videoFieldsSave
= videoInstance
.toJSON()
264 const videoInfosToUpdate
= req
.body
268 databaseUtils
.startSerializableTransaction
,
270 function findOrCreateTags (t
, callback
) {
271 if (videoInfosToUpdate
.tags
) {
272 db
.Tag
.findOrCreateTags(videoInfosToUpdate
.tags
, t
, function (err
, tagInstances
) {
273 return callback(err
, t
, tagInstances
)
276 return callback(null, t
, null)
280 function updateVideoIntoDB (t
, tagInstances
, callback
) {
285 if (videoInfosToUpdate
.name
!== undefined) videoInstance
.set('name', videoInfosToUpdate
.name
)
286 if (videoInfosToUpdate
.category
!== undefined) videoInstance
.set('category', videoInfosToUpdate
.category
)
287 if (videoInfosToUpdate
.licence
!== undefined) videoInstance
.set('licence', videoInfosToUpdate
.licence
)
288 if (videoInfosToUpdate
.language
!== undefined) videoInstance
.set('language', videoInfosToUpdate
.language
)
289 if (videoInfosToUpdate
.nsfw
!== undefined) videoInstance
.set('nsfw', videoInfosToUpdate
.nsfw
)
290 if (videoInfosToUpdate
.description
!== undefined) videoInstance
.set('description', videoInfosToUpdate
.description
)
292 videoInstance
.save(options
).asCallback(function (err
) {
293 return callback(err
, t
, tagInstances
)
297 function associateTagsToVideo (t
, tagInstances
, callback
) {
299 const options
= { transaction: t
}
301 videoInstance
.setTags(tagInstances
, options
).asCallback(function (err
) {
302 videoInstance
.Tags
= tagInstances
304 return callback(err
, t
)
307 return callback(null, t
)
311 function sendToFriends (t
, callback
) {
312 const json
= videoInstance
.toUpdateRemoteJSON()
314 // Now we'll update the video's meta data to our friends
315 friends
.updateVideoToFriends(json
, t
, function (err
) {
316 return callback(err
, t
)
320 databaseUtils
.commitTransaction
322 ], function andFinally (err
, t
) {
324 logger
.debug('Cannot update the video.', { error: err
})
326 // Force fields we want to update
327 // If the transaction is retried, sequelize will think the object has not changed
328 // So it will skip the SQL request, even if the last one was ROLLBACKed!
329 Object
.keys(videoFieldsSave
).forEach(function (key
) {
330 const value
= videoFieldsSave
[key
]
331 videoInstance
.set(key
, value
)
334 return databaseUtils
.rollbackTransaction(err
, t
, finalCallback
)
337 logger
.info('Video with name %s updated.', videoInfosToUpdate
.name
)
338 return finalCallback(null)
342 function getVideo (req
, res
, next
) {
343 const videoInstance
= res
.locals
.video
345 if (videoInstance
.isOwned()) {
346 // The increment is done directly in the database, not using the instance value
347 videoInstance
.increment('views').asCallback(function (err
) {
349 logger
.error('Cannot add view to video %d.', videoInstance
.id
)
353 // FIXME: make a real view system
354 // For example, only add a view when a user watch a video during 30s etc
356 videoId: videoInstance
.id
,
357 type: constants
.REQUEST_VIDEO_QADU_TYPES
.VIEWS
359 friends
.quickAndDirtyUpdateVideoToFriends(qaduParams
)
362 // Just send the event to our friends
363 const eventParams
= {
364 videoId: videoInstance
.id
,
365 type: constants
.REQUEST_VIDEO_EVENT_TYPES
.VIEWS
367 friends
.addEventToRemoteVideo(eventParams
)
370 // Do not wait the view system
371 res
.json(videoInstance
.toFormatedJSON())
374 function listVideos (req
, res
, next
) {
375 db
.Video
.listForApi(req
.query
.start
, req
.query
.count
, req
.query
.sort
, function (err
, videosList
, videosTotal
) {
376 if (err
) return next(err
)
378 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))
382 function removeVideo (req
, res
, next
) {
383 const videoInstance
= res
.locals
.video
385 videoInstance
.destroy().asCallback(function (err
) {
387 logger
.error('Errors when removed the video.', { error: err
})
391 return res
.type('json').status(204).end()
395 function searchVideos (req
, res
, next
) {
396 db
.Video
.searchAndPopulateAuthorAndPodAndTags(
397 req
.params
.value
, req
.query
.field
, req
.query
.start
, req
.query
.count
, req
.query
.sort
,
398 function (err
, videosList
, videosTotal
) {
399 if (err
) return next(err
)
401 res
.json(utils
.getFormatedObjects(videosList
, videosTotal
))