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
64 validatorsPagination.pagination,
65 validatorsSort.videosSort,
67 pagination.setPagination,
73 validatorsVideos.videosUpdate,
74 updateVideoRetryWrapper
79 validatorsVideos.videosAdd,
83 validatorsVideos.videosGet,
88 validatorsVideos.videosRemove,
91 router.get('/search/:value',
92 validatorsVideos.videosSearch,
93 validatorsPagination.pagination,
94 validatorsSort.videosSort,
96 pagination.setPagination,
97 search.setVideosSearch,
101 // ---------------------------------------------------------------------------
103 module.exports = router
105 // ---------------------------------------------------------------------------
107 // Wrapper to video add that retry the function if there is a database error
108 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
109 function addVideoRetryWrapper (req, res, next) {
111 arguments: [ req, res, req.files.videofile[0] ],
112 errorMessage: 'Cannot insert the video with many retries.'
115 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
116 if (err) return next(err)
118 // TODO : include Location of the new video -> 201
119 return res.type('json').status(204).end()
123 function addVideo (req, res, videoFile, finalCallback) {
124 const videoInfos = req.body
128 databaseUtils.startSerializableTransaction,
130 function findOrCreateAuthor (t, callback) {
131 const user = res.locals.oauth.token.User
133 const name = user.username
134 // null because it is OUR pod
136 const userId = user.id
138 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
139 return callback(err, t, authorInstance)
143 function findOrCreateTags (t, author, callback) {
144 const tags = videoInfos.tags
146 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
147 return callback(err, t, author, tagInstances)
151 function createVideoObject (t, author, tagInstances, callback) {
153 name: videoInfos.name,
155 extname: path.extname(videoFile.filename),
156 description: videoInfos.description,
157 duration: videoFile.duration,
161 const video = db.Video.build(videoData)
163 return callback(null, t, author, tagInstances, video)
166 // Set the videoname the same as the id
167 function renameVideoFile (t, author, tagInstances, video, callback) {
168 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
169 const source = path.join(videoDir, videoFile.filename)
170 const destination = path.join(videoDir, video.getVideoFilename())
172 fs.rename(source, destination, function (err) {
173 if (err) return callback(err)
175 // This is important in case if there is another attempt
176 videoFile.filename = video.getVideoFilename()
177 return callback(null, t, author, tagInstances, video)
181 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
182 const options = { transaction: t }
184 // Add tags association
185 video.save(options).asCallback(function (err, videoCreated) {
186 if (err) return callback(err)
188 // Do not forget to add Author informations to the created video
189 videoCreated.Author = author
191 return callback(err, t, tagInstances, videoCreated)
195 function associateTagsToVideo (t, tagInstances, video, callback) {
196 const options = { transaction: t }
198 video.setTags(tagInstances, options).asCallback(function (err) {
199 video.Tags = tagInstances
201 return callback(err, t, video)
205 function sendToFriends (t, video, callback) {
206 video.toAddRemoteJSON(function (err, remoteVideo) {
207 if (err) return callback(err)
209 // Now we'll add the video's meta data to our friends
210 friends.addVideoToFriends(remoteVideo, t, function (err) {
211 return callback(err, t)
216 databaseUtils.commitTransaction
218 ], function andFinally (err, t) {
220 // This is just a debug because we will retry the insert
221 logger.debug('Cannot insert the video.', { error: err })
222 return databaseUtils.rollbackTransaction(err, t, finalCallback)
225 logger.info('Video with name %s created.', videoInfos.name)
226 return finalCallback(null)
230 function updateVideoRetryWrapper (req, res, next) {
232 arguments: [ req, res ],
233 errorMessage: 'Cannot update the video with many retries.'
236 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
237 if (err) return next(err)
239 // TODO : include Location of the new video -> 201
240 return res.type('json').status(204).end()
244 function updateVideo (req, res, finalCallback) {
245 const videoInstance = res.locals.video
246 const videoFieldsSave = videoInstance.toJSON()
247 const videoInfosToUpdate = req.body
251 databaseUtils.startSerializableTransaction,
253 function findOrCreateTags (t, callback) {
254 if (videoInfosToUpdate.tags) {
255 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
256 return callback(err, t, tagInstances)
259 return callback(null, t, null)
263 function updateVideoIntoDB (t, tagInstances, callback) {
268 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
269 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
271 videoInstance.save(options).asCallback(function (err) {
272 return callback(err, t, tagInstances)
276 function associateTagsToVideo (t, tagInstances, callback) {
278 const options = { transaction: t }
280 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
281 videoInstance.Tags = tagInstances
283 return callback(err, t)
286 return callback(null, t)
290 function sendToFriends (t, callback) {
291 const json = videoInstance.toUpdateRemoteJSON()
293 // Now we'll update the video's meta data to our friends
294 friends.updateVideoToFriends(json, t, function (err) {
295 return callback(err, t)
299 databaseUtils.commitTransaction
301 ], function andFinally (err, t) {
303 logger.debug('Cannot update the video.', { error: err })
305 // Force fields we want to update
306 // If the transaction is retried, sequelize will think the object has not changed
307 // So it will skip the SQL request, even if the last one was ROLLBACKed!
308 Object.keys(videoFieldsSave).forEach(function (key) {
309 const value = videoFieldsSave[key]
310 videoInstance.set(key, value)
313 return databaseUtils.rollbackTransaction(err, t, finalCallback)
316 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
317 return finalCallback(null)
321 function getVideo (req, res, next) {
322 const videoInstance = res.locals.video
324 if (videoInstance.isOwned()) {
325 // The increment is done directly in the database, not using the instance value
326 videoInstance.increment('views').asCallback(function (err) {
328 logger.error('Cannot add view to video %d.', videoInstance.id)
332 // FIXME: make a real view system
333 // For example, only add a view when a user watch a video during 30s etc
334 friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS)
337 // Just send the event to our friends
338 friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
341 // Do not wait the view system
342 res.json(videoInstance.toFormatedJSON())
345 function listVideos (req, res, next) {
346 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
347 if (err) return next(err)
349 res.json(utils.getFormatedObjects(videosList, videosTotal))
353 function removeVideo (req, res, next) {
354 const videoInstance = res.locals.video
356 videoInstance.destroy().asCallback(function (err) {
358 logger.error('Errors when removed the video.', { error: err })
362 return res.type('json').status(204).end()
366 function searchVideos (req, res, next) {
367 db.Video.searchAndPopulateAuthorAndPodAndTags(
368 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
369 function (err, videosList, videosTotal) {
370 if (err) return next(err)
372 res.json(utils.getFormatedObjects(videosList, videosTotal))
377 function listVideoAbuses (req, res, next) {
378 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
379 if (err) return next(err)
381 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
385 function reportVideoAbuseRetryWrapper (req, res, next) {
387 arguments: [ req, res ],
388 errorMessage: 'Cannot report abuse to the video with many retries.'
391 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
392 if (err) return next(err)
394 return res.type('json').status(204).end()
398 function reportVideoAbuse (req, res, finalCallback) {
399 const videoInstance = res.locals.video
400 const reporterUsername = res.locals.oauth.token.User.username
404 reason: req.body.reason,
405 videoId: videoInstance.id,
406 reporterPodId: null // This is our pod that reported this abuse
411 databaseUtils.startSerializableTransaction,
413 function createAbuse (t, callback) {
414 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
415 return callback(err, t, abuse)
419 function sendToFriendsIfNeeded (t, abuse, callback) {
420 // We send the information to the destination pod
421 if (videoInstance.isOwned() === false) {
424 reportReason: abuse.reason,
425 videoRemoteId: videoInstance.remoteId
428 friends.reportAbuseVideoToFriend(reportData, videoInstance)
431 return callback(null, t)
434 databaseUtils.commitTransaction
436 ], function andFinally (err, t) {
438 logger.debug('Cannot update the video.', { error: err })
439 return databaseUtils.rollbackTransaction(err, t, finalCallback)
442 logger.info('Abuse report for video %s created.', videoInstance.name)
443 return finalCallback(null)