]>
Commit | Line | Data |
---|---|---|
9f10b292 C |
1 | 'use strict' |
2 | ||
f0f5567b | 3 | const express = require('express') |
558d7c23 | 4 | const fs = require('fs') |
f0f5567b | 5 | const multer = require('multer') |
558d7c23 | 6 | const path = require('path') |
1a42c9e2 | 7 | const waterfall = require('async/waterfall') |
f0f5567b | 8 | |
d33242b0 C |
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') | |
69b0a27c | 14 | const oAuth = middlewares.oauth |
fbf1134e | 15 | const pagination = middlewares.pagination |
fc51fde0 C |
16 | const validators = middlewares.validators |
17 | const validatorsPagination = validators.pagination | |
18 | const validatorsSort = validators.sort | |
19 | const validatorsVideos = validators.videos | |
46246b5f | 20 | const search = middlewares.search |
a877d5ac | 21 | const sort = middlewares.sort |
d33242b0 C |
22 | const databaseUtils = require('../../../helpers/database-utils') |
23 | const utils = require('../../../helpers/utils') | |
24 | ||
25 | const abuseController = require('./abuse') | |
26 | const blacklistController = require('./blacklist') | |
27 | const rateController = require('./rate') | |
f0f5567b C |
28 | |
29 | const router = express.Router() | |
9f10b292 C |
30 | |
31 | // multer configuration | |
f0f5567b | 32 | const storage = multer.diskStorage({ |
9f10b292 | 33 | destination: function (req, file, cb) { |
b3d92510 | 34 | cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR) |
9f10b292 C |
35 | }, |
36 | ||
37 | filename: function (req, file, cb) { | |
f0f5567b | 38 | let extension = '' |
9f10b292 C |
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' | |
bc503c2a C |
42 | utils.generateRandomString(16, function (err, randomString) { |
43 | const fieldname = err ? undefined : randomString | |
9f10b292 C |
44 | cb(null, fieldname + '.' + extension) |
45 | }) | |
46 | } | |
47 | }) | |
48 | ||
8c9c1942 | 49 | const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) |
8c308c2b | 50 | |
d33242b0 C |
51 | router.use('/', abuseController) |
52 | router.use('/', blacklistController) | |
53 | router.use('/', rateController) | |
54 | ||
6e07c3de | 55 | router.get('/categories', listVideoCategories) |
6f0c39e2 | 56 | router.get('/licences', listVideoLicences) |
3092476e | 57 | router.get('/languages', listVideoLanguages) |
6e07c3de | 58 | |
fbf1134e | 59 | router.get('/', |
fc51fde0 C |
60 | validatorsPagination.pagination, |
61 | validatorsSort.videosSort, | |
a877d5ac | 62 | sort.setVideosSort, |
fbf1134e C |
63 | pagination.setPagination, |
64 | listVideos | |
65 | ) | |
7b1f49de C |
66 | router.put('/:id', |
67 | oAuth.authenticate, | |
68 | reqFiles, | |
69 | validatorsVideos.videosUpdate, | |
ed04d94f | 70 | updateVideoRetryWrapper |
7b1f49de | 71 | ) |
fbf1134e | 72 | router.post('/', |
69b0a27c | 73 | oAuth.authenticate, |
fbf1134e | 74 | reqFiles, |
fc51fde0 | 75 | validatorsVideos.videosAdd, |
ed04d94f | 76 | addVideoRetryWrapper |
fbf1134e C |
77 | ) |
78 | router.get('/:id', | |
fc51fde0 | 79 | validatorsVideos.videosGet, |
68ce3ae0 | 80 | getVideo |
fbf1134e | 81 | ) |
198b205c | 82 | |
fbf1134e | 83 | router.delete('/:id', |
69b0a27c | 84 | oAuth.authenticate, |
fc51fde0 | 85 | validatorsVideos.videosRemove, |
fbf1134e C |
86 | removeVideo |
87 | ) | |
198b205c | 88 | |
46246b5f | 89 | router.get('/search/:value', |
fc51fde0 C |
90 | validatorsVideos.videosSearch, |
91 | validatorsPagination.pagination, | |
92 | validatorsSort.videosSort, | |
a877d5ac | 93 | sort.setVideosSort, |
fbf1134e | 94 | pagination.setPagination, |
46246b5f | 95 | search.setVideosSearch, |
fbf1134e C |
96 | searchVideos |
97 | ) | |
8c308c2b | 98 | |
9f10b292 | 99 | // --------------------------------------------------------------------------- |
c45f7f84 | 100 | |
9f10b292 | 101 | module.exports = router |
c45f7f84 | 102 | |
9f10b292 | 103 | // --------------------------------------------------------------------------- |
c45f7f84 | 104 | |
6e07c3de C |
105 | function listVideoCategories (req, res, next) { |
106 | res.json(constants.VIDEO_CATEGORIES) | |
107 | } | |
108 | ||
6f0c39e2 C |
109 | function listVideoLicences (req, res, next) { |
110 | res.json(constants.VIDEO_LICENCES) | |
111 | } | |
112 | ||
3092476e C |
113 | function listVideoLanguages (req, res, next) { |
114 | res.json(constants.VIDEO_LANGUAGES) | |
115 | } | |
116 | ||
ed04d94f C |
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) { | |
d6a5b018 C |
120 | const options = { |
121 | arguments: [ req, res, req.files.videofile[0] ], | |
122 | errorMessage: 'Cannot insert the video with many retries.' | |
123 | } | |
ed04d94f | 124 | |
4df023f2 | 125 | databaseUtils.retryTransactionWrapper(addVideo, options, function (err) { |
d6a5b018 C |
126 | if (err) return next(err) |
127 | ||
128 | // TODO : include Location of the new video -> 201 | |
129 | return res.type('json').status(204).end() | |
130 | }) | |
ed04d94f C |
131 | } |
132 | ||
4145c1c6 | 133 | function addVideo (req, res, videoFile, finalCallback) { |
bc503c2a | 134 | const videoInfos = req.body |
9f10b292 | 135 | |
1a42c9e2 | 136 | waterfall([ |
807df9e6 | 137 | |
4df023f2 | 138 | databaseUtils.startSerializableTransaction, |
7920c273 | 139 | |
4145c1c6 | 140 | function findOrCreateAuthor (t, callback) { |
4712081f | 141 | const user = res.locals.oauth.token.User |
feb4bdfd | 142 | |
4ff0d862 C |
143 | const name = user.username |
144 | // null because it is OUR pod | |
145 | const podId = null | |
146 | const userId = user.id | |
4712081f | 147 | |
4ff0d862 | 148 | db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { |
4145c1c6 | 149 | return callback(err, t, authorInstance) |
7920c273 C |
150 | }) |
151 | }, | |
152 | ||
4145c1c6 | 153 | function findOrCreateTags (t, author, callback) { |
7920c273 | 154 | const tags = videoInfos.tags |
4ff0d862 C |
155 | |
156 | db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { | |
4145c1c6 | 157 | return callback(err, t, author, tagInstances) |
feb4bdfd C |
158 | }) |
159 | }, | |
160 | ||
4145c1c6 | 161 | function createVideoObject (t, author, tagInstances, callback) { |
807df9e6 C |
162 | const videoData = { |
163 | name: videoInfos.name, | |
558d7c23 C |
164 | remoteId: null, |
165 | extname: path.extname(videoFile.filename), | |
6e07c3de | 166 | category: videoInfos.category, |
6f0c39e2 | 167 | licence: videoInfos.licence, |
3092476e | 168 | language: videoInfos.language, |
31b59b47 | 169 | nsfw: videoInfos.nsfw, |
807df9e6 | 170 | description: videoInfos.description, |
67100f1f | 171 | duration: videoFile.duration, |
d38b8281 | 172 | authorId: author.id |
807df9e6 C |
173 | } |
174 | ||
feb4bdfd | 175 | const video = db.Video.build(videoData) |
558d7c23 | 176 | |
4145c1c6 | 177 | return callback(null, t, author, tagInstances, video) |
558d7c23 C |
178 | }, |
179 | ||
feb4bdfd | 180 | // Set the videoname the same as the id |
4145c1c6 | 181 | function renameVideoFile (t, author, tagInstances, video, callback) { |
558d7c23 C |
182 | const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR |
183 | const source = path.join(videoDir, videoFile.filename) | |
f285faa0 | 184 | const destination = path.join(videoDir, video.getVideoFilename()) |
558d7c23 C |
185 | |
186 | fs.rename(source, destination, function (err) { | |
4145c1c6 | 187 | if (err) return callback(err) |
ed04d94f C |
188 | |
189 | // This is important in case if there is another attempt | |
190 | videoFile.filename = video.getVideoFilename() | |
4145c1c6 | 191 | return callback(null, t, author, tagInstances, video) |
558d7c23 C |
192 | }) |
193 | }, | |
194 | ||
4145c1c6 | 195 | function insertVideoIntoDB (t, author, tagInstances, video, callback) { |
7920c273 C |
196 | const options = { transaction: t } |
197 | ||
198 | // Add tags association | |
199 | video.save(options).asCallback(function (err, videoCreated) { | |
4145c1c6 | 200 | if (err) return callback(err) |
7920c273 | 201 | |
feb4bdfd C |
202 | // Do not forget to add Author informations to the created video |
203 | videoCreated.Author = author | |
204 | ||
4145c1c6 | 205 | return callback(err, t, tagInstances, videoCreated) |
3a8a8b51 | 206 | }) |
807df9e6 C |
207 | }, |
208 | ||
4145c1c6 | 209 | function associateTagsToVideo (t, tagInstances, video, callback) { |
7920c273 C |
210 | const options = { transaction: t } |
211 | ||
212 | video.setTags(tagInstances, options).asCallback(function (err) { | |
213 | video.Tags = tagInstances | |
214 | ||
4145c1c6 | 215 | return callback(err, t, video) |
7920c273 C |
216 | }) |
217 | }, | |
218 | ||
4145c1c6 | 219 | function sendToFriends (t, video, callback) { |
62326afb C |
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) | |
222 | ||
7b1f49de | 223 | video.toAddRemoteJSON(function (err, remoteVideo) { |
4145c1c6 | 224 | if (err) return callback(err) |
807df9e6 | 225 | |
528a9efa | 226 | // Now we'll add the video's meta data to our friends |
ed04d94f | 227 | friends.addVideoToFriends(remoteVideo, t, function (err) { |
4145c1c6 | 228 | return callback(err, t) |
ed04d94f | 229 | }) |
528a9efa | 230 | }) |
4145c1c6 C |
231 | }, |
232 | ||
233 | databaseUtils.commitTransaction | |
807df9e6 | 234 | |
7b1f49de C |
235 | ], function andFinally (err, t) { |
236 | if (err) { | |
ed04d94f C |
237 | // This is just a debug because we will retry the insert |
238 | logger.debug('Cannot insert the video.', { error: err }) | |
4145c1c6 | 239 | return databaseUtils.rollbackTransaction(err, t, finalCallback) |
7b1f49de C |
240 | } |
241 | ||
4145c1c6 C |
242 | logger.info('Video with name %s created.', videoInfos.name) |
243 | return finalCallback(null) | |
7b1f49de C |
244 | }) |
245 | } | |
246 | ||
ed04d94f | 247 | function updateVideoRetryWrapper (req, res, next) { |
d6a5b018 C |
248 | const options = { |
249 | arguments: [ req, res ], | |
250 | errorMessage: 'Cannot update the video with many retries.' | |
251 | } | |
ed04d94f | 252 | |
4df023f2 | 253 | databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) { |
d6a5b018 C |
254 | if (err) return next(err) |
255 | ||
256 | // TODO : include Location of the new video -> 201 | |
257 | return res.type('json').status(204).end() | |
258 | }) | |
ed04d94f C |
259 | } |
260 | ||
261 | function updateVideo (req, res, finalCallback) { | |
818f7987 | 262 | const videoInstance = res.locals.video |
7f4e7c36 | 263 | const videoFieldsSave = videoInstance.toJSON() |
7b1f49de C |
264 | const videoInfosToUpdate = req.body |
265 | ||
266 | waterfall([ | |
267 | ||
4df023f2 | 268 | databaseUtils.startSerializableTransaction, |
7b1f49de C |
269 | |
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) | |
274 | }) | |
275 | } else { | |
276 | return callback(null, t, null) | |
277 | } | |
278 | }, | |
279 | ||
280 | function updateVideoIntoDB (t, tagInstances, callback) { | |
7f4e7c36 C |
281 | const options = { |
282 | transaction: t | |
283 | } | |
7b1f49de | 284 | |
c24ac1c1 C |
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) | |
7b1f49de | 291 | |
7b1f49de | 292 | videoInstance.save(options).asCallback(function (err) { |
7b1f49de C |
293 | return callback(err, t, tagInstances) |
294 | }) | |
295 | }, | |
296 | ||
297 | function associateTagsToVideo (t, tagInstances, callback) { | |
298 | if (tagInstances) { | |
299 | const options = { transaction: t } | |
300 | ||
301 | videoInstance.setTags(tagInstances, options).asCallback(function (err) { | |
302 | videoInstance.Tags = tagInstances | |
303 | ||
304 | return callback(err, t) | |
305 | }) | |
306 | } else { | |
307 | return callback(null, t) | |
308 | } | |
309 | }, | |
310 | ||
311 | function sendToFriends (t, callback) { | |
312 | const json = videoInstance.toUpdateRemoteJSON() | |
313 | ||
314 | // Now we'll update the video's meta data to our friends | |
ed04d94f C |
315 | friends.updateVideoToFriends(json, t, function (err) { |
316 | return callback(err, t) | |
317 | }) | |
4145c1c6 C |
318 | }, |
319 | ||
320 | databaseUtils.commitTransaction | |
7b1f49de | 321 | |
7920c273 | 322 | ], function andFinally (err, t) { |
807df9e6 | 323 | if (err) { |
ed04d94f | 324 | logger.debug('Cannot update the video.', { error: err }) |
7920c273 | 325 | |
7f4e7c36 C |
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) | |
332 | }) | |
333 | ||
4145c1c6 | 334 | return databaseUtils.rollbackTransaction(err, t, finalCallback) |
807df9e6 C |
335 | } |
336 | ||
4145c1c6 C |
337 | logger.info('Video with name %s updated.', videoInfosToUpdate.name) |
338 | return finalCallback(null) | |
9f10b292 C |
339 | }) |
340 | } | |
8c308c2b | 341 | |
68ce3ae0 | 342 | function getVideo (req, res, next) { |
818f7987 | 343 | const videoInstance = res.locals.video |
9e167724 C |
344 | |
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) { | |
348 | if (err) { | |
349 | logger.error('Cannot add view to video %d.', videoInstance.id) | |
350 | return | |
351 | } | |
352 | ||
353 | // FIXME: make a real view system | |
354 | // For example, only add a view when a user watch a video during 30s etc | |
d38b8281 C |
355 | const qaduParams = { |
356 | videoId: videoInstance.id, | |
357 | type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS | |
358 | } | |
359 | friends.quickAndDirtyUpdateVideoToFriends(qaduParams) | |
9e167724 | 360 | }) |
e4c87ec2 C |
361 | } else { |
362 | // Just send the event to our friends | |
d38b8281 C |
363 | const eventParams = { |
364 | videoId: videoInstance.id, | |
365 | type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS | |
366 | } | |
367 | friends.addEventToRemoteVideo(eventParams) | |
9e167724 C |
368 | } |
369 | ||
370 | // Do not wait the view system | |
818f7987 | 371 | res.json(videoInstance.toFormatedJSON()) |
9f10b292 | 372 | } |
8c308c2b | 373 | |
9f10b292 | 374 | function listVideos (req, res, next) { |
feb4bdfd | 375 | db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { |
9f10b292 | 376 | if (err) return next(err) |
c45f7f84 | 377 | |
55fa55a9 | 378 | res.json(utils.getFormatedObjects(videosList, videosTotal)) |
9f10b292 C |
379 | }) |
380 | } | |
c45f7f84 | 381 | |
9f10b292 | 382 | function removeVideo (req, res, next) { |
818f7987 | 383 | const videoInstance = res.locals.video |
8c308c2b | 384 | |
818f7987 | 385 | videoInstance.destroy().asCallback(function (err) { |
807df9e6 C |
386 | if (err) { |
387 | logger.error('Errors when removed the video.', { error: err }) | |
388 | return next(err) | |
389 | } | |
390 | ||
391 | return res.type('json').status(204).end() | |
9f10b292 C |
392 | }) |
393 | } | |
8c308c2b | 394 | |
9f10b292 | 395 | function searchVideos (req, res, next) { |
7920c273 C |
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) | |
8c308c2b | 400 | |
55fa55a9 | 401 | res.json(utils.getFormatedObjects(videosList, videosTotal)) |
7920c273 C |
402 | } |
403 | ) | |
9f10b292 | 404 | } |