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