]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Add link to wiki for production installation
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos.js
CommitLineData
9f10b292
C
1'use strict'
2
f0f5567b 3const express = require('express')
558d7c23 4const fs = require('fs')
f0f5567b 5const multer = require('multer')
558d7c23 6const path = require('path')
1a42c9e2 7const waterfall = require('async/waterfall')
f0f5567b 8
f253b1c1 9const constants = require('../../initializers/constants')
feb4bdfd 10const db = require('../../initializers/database')
f253b1c1
C
11const logger = require('../../helpers/logger')
12const friends = require('../../lib/friends')
13const middlewares = require('../../middlewares')
55fa55a9 14const admin = middlewares.admin
69b0a27c 15const oAuth = middlewares.oauth
fbf1134e 16const pagination = middlewares.pagination
fc51fde0
C
17const validators = middlewares.validators
18const validatorsPagination = validators.pagination
19const validatorsSort = validators.sort
20const validatorsVideos = validators.videos
46246b5f 21const search = middlewares.search
a877d5ac 22const sort = middlewares.sort
4df023f2 23const databaseUtils = require('../../helpers/database-utils')
f253b1c1 24const utils = require('../../helpers/utils')
f0f5567b
C
25
26const router = express.Router()
9f10b292
C
27
28// multer configuration
f0f5567b 29const storage = multer.diskStorage({
9f10b292 30 destination: function (req, file, cb) {
b3d92510 31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
9f10b292
C
32 },
33
34 filename: function (req, file, cb) {
f0f5567b 35 let extension = ''
9f10b292
C
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'
bc503c2a
C
39 utils.generateRandomString(16, function (err, randomString) {
40 const fieldname = err ? undefined : randomString
9f10b292
C
41 cb(null, fieldname + '.' + extension)
42 })
43 }
44})
45
8c9c1942 46const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
8c308c2b 47
6e07c3de 48router.get('/categories', listVideoCategories)
6f0c39e2 49router.get('/licences', listVideoLicences)
6e07c3de 50
55fa55a9
C
51router.get('/abuse',
52 oAuth.authenticate,
53 admin.ensureIsAdmin,
54 validatorsPagination.pagination,
55 validatorsSort.videoAbusesSort,
56 sort.setVideoAbusesSort,
57 pagination.setPagination,
58 listVideoAbuses
59)
60router.post('/:id/abuse',
61 oAuth.authenticate,
62 validatorsVideos.videoAbuseReport,
bf4ff8fe 63 reportVideoAbuseRetryWrapper
55fa55a9
C
64)
65
d38b8281
C
66router.put('/:id/rate',
67 oAuth.authenticate,
68 validatorsVideos.videoRate,
69 rateVideoRetryWrapper
70)
71
fbf1134e 72router.get('/',
fc51fde0
C
73 validatorsPagination.pagination,
74 validatorsSort.videosSort,
a877d5ac 75 sort.setVideosSort,
fbf1134e
C
76 pagination.setPagination,
77 listVideos
78)
7b1f49de
C
79router.put('/:id',
80 oAuth.authenticate,
81 reqFiles,
82 validatorsVideos.videosUpdate,
ed04d94f 83 updateVideoRetryWrapper
7b1f49de 84)
fbf1134e 85router.post('/',
69b0a27c 86 oAuth.authenticate,
fbf1134e 87 reqFiles,
fc51fde0 88 validatorsVideos.videosAdd,
ed04d94f 89 addVideoRetryWrapper
fbf1134e
C
90)
91router.get('/:id',
fc51fde0 92 validatorsVideos.videosGet,
68ce3ae0 93 getVideo
fbf1134e
C
94)
95router.delete('/:id',
69b0a27c 96 oAuth.authenticate,
fc51fde0 97 validatorsVideos.videosRemove,
fbf1134e
C
98 removeVideo
99)
46246b5f 100router.get('/search/:value',
fc51fde0
C
101 validatorsVideos.videosSearch,
102 validatorsPagination.pagination,
103 validatorsSort.videosSort,
a877d5ac 104 sort.setVideosSort,
fbf1134e 105 pagination.setPagination,
46246b5f 106 search.setVideosSearch,
fbf1134e
C
107 searchVideos
108)
8c308c2b 109
9f10b292 110// ---------------------------------------------------------------------------
c45f7f84 111
9f10b292 112module.exports = router
c45f7f84 113
9f10b292 114// ---------------------------------------------------------------------------
c45f7f84 115
6e07c3de
C
116function listVideoCategories (req, res, next) {
117 res.json(constants.VIDEO_CATEGORIES)
118}
119
6f0c39e2
C
120function listVideoLicences (req, res, next) {
121 res.json(constants.VIDEO_LICENCES)
122}
123
d38b8281
C
124function rateVideoRetryWrapper (req, res, next) {
125 const options = {
126 arguments: [ req, res ],
127 errorMessage: 'Cannot update the user video rate.'
128 }
129
130 databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
131 if (err) return next(err)
132
133 return res.type('json').status(204).end()
134 })
135}
136
137function rateVideo (req, res, finalCallback) {
138 const rateType = req.body.rating
139 const videoInstance = res.locals.video
140 const userInstance = res.locals.oauth.token.User
141
142 waterfall([
143 databaseUtils.startSerializableTransaction,
144
145 function findPreviousRate (t, callback) {
146 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
147 return callback(err, t, previousRate)
148 })
149 },
150
151 function insertUserRateIntoDB (t, previousRate, callback) {
152 const options = { transaction: t }
153
154 let likesToIncrement = 0
155 let dislikesToIncrement = 0
156
157 if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
158 else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
159
160 // There was a previous rate, update it
161 if (previousRate) {
162 // We will remove the previous rate, so we will need to remove it from the video attribute
163 if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
164 else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
165
166 previousRate.type = rateType
167
168 previousRate.save(options).asCallback(function (err) {
169 return callback(err, t, likesToIncrement, dislikesToIncrement)
170 })
171 } else { // There was not a previous rate, insert a new one
172 const query = {
173 userId: userInstance.id,
174 videoId: videoInstance.id,
175 type: rateType
176 }
177
178 db.UserVideoRate.create(query, options).asCallback(function (err) {
179 return callback(err, t, likesToIncrement, dislikesToIncrement)
180 })
181 }
182 },
183
184 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
185 const options = { transaction: t }
186 const incrementQuery = {
187 likes: likesToIncrement,
188 dislikes: dislikesToIncrement
189 }
190
191 // Even if we do not own the video we increment the attributes
192 // It is usefull for the user to have a feedback
193 videoInstance.increment(incrementQuery, options).asCallback(function (err) {
194 return callback(err, t, likesToIncrement, dislikesToIncrement)
195 })
196 },
197
198 function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
199 // No need for an event type, we own the video
200 if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
201
202 const eventsParams = []
203
204 if (likesToIncrement !== 0) {
205 eventsParams.push({
206 videoId: videoInstance.id,
207 type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
208 count: likesToIncrement
209 })
210 }
211
212 if (dislikesToIncrement !== 0) {
213 eventsParams.push({
214 videoId: videoInstance.id,
215 type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
216 count: dislikesToIncrement
217 })
218 }
219
220 friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
221 return callback(err, t, likesToIncrement, dislikesToIncrement)
222 })
223 },
224
225 function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
226 // We do not own the video, there is no need to send a quick and dirty update to friends
227 // Our rate was already sent by the addEvent function
228 if (videoInstance.isOwned() === false) return callback(null, t)
229
230 const qadusParams = []
231
232 if (likesToIncrement !== 0) {
233 qadusParams.push({
234 videoId: videoInstance.id,
235 type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
236 })
237 }
238
239 if (dislikesToIncrement !== 0) {
240 qadusParams.push({
241 videoId: videoInstance.id,
242 type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
243 })
244 }
245
246 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
247 return callback(err, t)
248 })
249 },
250
251 databaseUtils.commitTransaction
252
253 ], function (err, t) {
254 if (err) {
255 // This is just a debug because we will retry the insert
256 logger.debug('Cannot add the user video rate.', { error: err })
257 return databaseUtils.rollbackTransaction(err, t, finalCallback)
258 }
259
260 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
261 return finalCallback(null)
262 })
263}
264
ed04d94f
C
265// Wrapper to video add that retry the function if there is a database error
266// We need this because we run the transaction in SERIALIZABLE isolation that can fail
267function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
268 const options = {
269 arguments: [ req, res, req.files.videofile[0] ],
270 errorMessage: 'Cannot insert the video with many retries.'
271 }
ed04d94f 272
4df023f2 273 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
274 if (err) return next(err)
275
276 // TODO : include Location of the new video -> 201
277 return res.type('json').status(204).end()
278 })
ed04d94f
C
279}
280
4145c1c6 281function addVideo (req, res, videoFile, finalCallback) {
bc503c2a 282 const videoInfos = req.body
9f10b292 283
1a42c9e2 284 waterfall([
807df9e6 285
4df023f2 286 databaseUtils.startSerializableTransaction,
7920c273 287
4145c1c6 288 function findOrCreateAuthor (t, callback) {
4712081f 289 const user = res.locals.oauth.token.User
feb4bdfd 290
4ff0d862
C
291 const name = user.username
292 // null because it is OUR pod
293 const podId = null
294 const userId = user.id
4712081f 295
4ff0d862 296 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
4145c1c6 297 return callback(err, t, authorInstance)
7920c273
C
298 })
299 },
300
4145c1c6 301 function findOrCreateTags (t, author, callback) {
7920c273 302 const tags = videoInfos.tags
4ff0d862
C
303
304 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
4145c1c6 305 return callback(err, t, author, tagInstances)
feb4bdfd
C
306 })
307 },
308
4145c1c6 309 function createVideoObject (t, author, tagInstances, callback) {
807df9e6
C
310 const videoData = {
311 name: videoInfos.name,
558d7c23
C
312 remoteId: null,
313 extname: path.extname(videoFile.filename),
6e07c3de 314 category: videoInfos.category,
6f0c39e2 315 licence: videoInfos.licence,
31b59b47 316 nsfw: videoInfos.nsfw,
807df9e6 317 description: videoInfos.description,
67100f1f 318 duration: videoFile.duration,
d38b8281 319 authorId: author.id
807df9e6
C
320 }
321
feb4bdfd 322 const video = db.Video.build(videoData)
558d7c23 323
4145c1c6 324 return callback(null, t, author, tagInstances, video)
558d7c23
C
325 },
326
feb4bdfd 327 // Set the videoname the same as the id
4145c1c6 328 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
329 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
330 const source = path.join(videoDir, videoFile.filename)
f285faa0 331 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
332
333 fs.rename(source, destination, function (err) {
4145c1c6 334 if (err) return callback(err)
ed04d94f
C
335
336 // This is important in case if there is another attempt
337 videoFile.filename = video.getVideoFilename()
4145c1c6 338 return callback(null, t, author, tagInstances, video)
558d7c23
C
339 })
340 },
341
4145c1c6 342 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
343 const options = { transaction: t }
344
345 // Add tags association
346 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 347 if (err) return callback(err)
7920c273 348
feb4bdfd
C
349 // Do not forget to add Author informations to the created video
350 videoCreated.Author = author
351
4145c1c6 352 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 353 })
807df9e6
C
354 },
355
4145c1c6 356 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
357 const options = { transaction: t }
358
359 video.setTags(tagInstances, options).asCallback(function (err) {
360 video.Tags = tagInstances
361
4145c1c6 362 return callback(err, t, video)
7920c273
C
363 })
364 },
365
4145c1c6 366 function sendToFriends (t, video, callback) {
7b1f49de 367 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 368 if (err) return callback(err)
807df9e6 369
528a9efa 370 // Now we'll add the video's meta data to our friends
ed04d94f 371 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 372 return callback(err, t)
ed04d94f 373 })
528a9efa 374 })
4145c1c6
C
375 },
376
377 databaseUtils.commitTransaction
807df9e6 378
7b1f49de
C
379 ], function andFinally (err, t) {
380 if (err) {
ed04d94f
C
381 // This is just a debug because we will retry the insert
382 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 383 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
384 }
385
4145c1c6
C
386 logger.info('Video with name %s created.', videoInfos.name)
387 return finalCallback(null)
7b1f49de
C
388 })
389}
390
ed04d94f 391function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
392 const options = {
393 arguments: [ req, res ],
394 errorMessage: 'Cannot update the video with many retries.'
395 }
ed04d94f 396
4df023f2 397 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
398 if (err) return next(err)
399
400 // TODO : include Location of the new video -> 201
401 return res.type('json').status(204).end()
402 })
ed04d94f
C
403}
404
405function updateVideo (req, res, finalCallback) {
818f7987 406 const videoInstance = res.locals.video
7f4e7c36 407 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
408 const videoInfosToUpdate = req.body
409
410 waterfall([
411
4df023f2 412 databaseUtils.startSerializableTransaction,
7b1f49de
C
413
414 function findOrCreateTags (t, callback) {
415 if (videoInfosToUpdate.tags) {
416 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
417 return callback(err, t, tagInstances)
418 })
419 } else {
420 return callback(null, t, null)
421 }
422 },
423
424 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
425 const options = {
426 transaction: t
427 }
7b1f49de
C
428
429 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
6e07c3de 430 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
6f0c39e2 431 if (videoInfosToUpdate.licence) videoInstance.set('licence', videoInfosToUpdate.licence)
31b59b47 432 if (videoInfosToUpdate.nsfw) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
7b1f49de
C
433 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
434
7b1f49de 435 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
436 return callback(err, t, tagInstances)
437 })
438 },
439
440 function associateTagsToVideo (t, tagInstances, callback) {
441 if (tagInstances) {
442 const options = { transaction: t }
443
444 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
445 videoInstance.Tags = tagInstances
446
447 return callback(err, t)
448 })
449 } else {
450 return callback(null, t)
451 }
452 },
453
454 function sendToFriends (t, callback) {
455 const json = videoInstance.toUpdateRemoteJSON()
456
457 // Now we'll update the video's meta data to our friends
ed04d94f
C
458 friends.updateVideoToFriends(json, t, function (err) {
459 return callback(err, t)
460 })
4145c1c6
C
461 },
462
463 databaseUtils.commitTransaction
7b1f49de 464
7920c273 465 ], function andFinally (err, t) {
807df9e6 466 if (err) {
ed04d94f 467 logger.debug('Cannot update the video.', { error: err })
7920c273 468
7f4e7c36
C
469 // Force fields we want to update
470 // If the transaction is retried, sequelize will think the object has not changed
471 // So it will skip the SQL request, even if the last one was ROLLBACKed!
472 Object.keys(videoFieldsSave).forEach(function (key) {
473 const value = videoFieldsSave[key]
474 videoInstance.set(key, value)
475 })
476
4145c1c6 477 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
478 }
479
4145c1c6
C
480 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
481 return finalCallback(null)
9f10b292
C
482 })
483}
8c308c2b 484
68ce3ae0 485function getVideo (req, res, next) {
818f7987 486 const videoInstance = res.locals.video
9e167724
C
487
488 if (videoInstance.isOwned()) {
489 // The increment is done directly in the database, not using the instance value
490 videoInstance.increment('views').asCallback(function (err) {
491 if (err) {
492 logger.error('Cannot add view to video %d.', videoInstance.id)
493 return
494 }
495
496 // FIXME: make a real view system
497 // For example, only add a view when a user watch a video during 30s etc
d38b8281
C
498 const qaduParams = {
499 videoId: videoInstance.id,
500 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
501 }
502 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
9e167724 503 })
e4c87ec2
C
504 } else {
505 // Just send the event to our friends
d38b8281
C
506 const eventParams = {
507 videoId: videoInstance.id,
508 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
509 }
510 friends.addEventToRemoteVideo(eventParams)
9e167724
C
511 }
512
513 // Do not wait the view system
818f7987 514 res.json(videoInstance.toFormatedJSON())
9f10b292 515}
8c308c2b 516
9f10b292 517function listVideos (req, res, next) {
feb4bdfd 518 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 519 if (err) return next(err)
c45f7f84 520
55fa55a9 521 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
522 })
523}
c45f7f84 524
9f10b292 525function removeVideo (req, res, next) {
818f7987 526 const videoInstance = res.locals.video
8c308c2b 527
818f7987 528 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
529 if (err) {
530 logger.error('Errors when removed the video.', { error: err })
531 return next(err)
532 }
533
534 return res.type('json').status(204).end()
9f10b292
C
535 })
536}
8c308c2b 537
9f10b292 538function searchVideos (req, res, next) {
7920c273
C
539 db.Video.searchAndPopulateAuthorAndPodAndTags(
540 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
541 function (err, videosList, videosTotal) {
542 if (err) return next(err)
8c308c2b 543
55fa55a9 544 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
545 }
546 )
9f10b292 547}
c173e565 548
55fa55a9
C
549function listVideoAbuses (req, res, next) {
550 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
551 if (err) return next(err)
2df82d42 552
55fa55a9 553 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 554 })
55fa55a9 555}
2df82d42 556
bf4ff8fe 557function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
558 const options = {
559 arguments: [ req, res ],
560 errorMessage: 'Cannot report abuse to the video with many retries.'
561 }
bf4ff8fe 562
4df023f2
C
563 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
564 if (err) return next(err)
565
566 return res.type('json').status(204).end()
567 })
bf4ff8fe
C
568}
569
570function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
571 const videoInstance = res.locals.video
572 const reporterUsername = res.locals.oauth.token.User.username
573
574 const abuse = {
575 reporterUsername,
576 reason: req.body.reason,
577 videoId: videoInstance.id,
578 reporterPodId: null // This is our pod that reported this abuse
68ce3ae0 579 }
55fa55a9 580
bf4ff8fe
C
581 waterfall([
582
da691c46 583 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
584
585 function createAbuse (t, callback) {
586 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
587 return callback(err, t, abuse)
588 })
589 },
590
591 function sendToFriendsIfNeeded (t, abuse, callback) {
592 // We send the information to the destination pod
593 if (videoInstance.isOwned() === false) {
594 const reportData = {
595 reporterUsername,
596 reportReason: abuse.reason,
597 videoRemoteId: videoInstance.remoteId
598 }
55fa55a9 599
bf4ff8fe 600 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
601 }
602
bf4ff8fe 603 return callback(null, t)
4145c1c6
C
604 },
605
606 databaseUtils.commitTransaction
55fa55a9 607
bf4ff8fe
C
608 ], function andFinally (err, t) {
609 if (err) {
610 logger.debug('Cannot update the video.', { error: err })
4145c1c6 611 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
612 }
613
4145c1c6
C
614 logger.info('Abuse report for video %s created.', videoInstance.name)
615 return finalCallback(null)
55fa55a9 616 })
2df82d42 617}