]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
Server: add video language attribute
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos.js
1 'use strict'
2
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')
8
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')
25
26 const router = express.Router()
27
28 // multer configuration
29 const storage = multer.diskStorage({
30 destination: function (req, file, cb) {
31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
32 },
33
34 filename: function (req, file, cb) {
35 let extension = ''
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)
42 })
43 }
44 })
45
46 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
47
48 router.get('/categories', listVideoCategories)
49 router.get('/licences', listVideoLicences)
50 router.get('/languages', listVideoLanguages)
51
52 router.get('/abuse',
53 oAuth.authenticate,
54 admin.ensureIsAdmin,
55 validatorsPagination.pagination,
56 validatorsSort.videoAbusesSort,
57 sort.setVideoAbusesSort,
58 pagination.setPagination,
59 listVideoAbuses
60 )
61 router.post('/:id/abuse',
62 oAuth.authenticate,
63 validatorsVideos.videoAbuseReport,
64 reportVideoAbuseRetryWrapper
65 )
66
67 router.put('/:id/rate',
68 oAuth.authenticate,
69 validatorsVideos.videoRate,
70 rateVideoRetryWrapper
71 )
72
73 router.get('/',
74 validatorsPagination.pagination,
75 validatorsSort.videosSort,
76 sort.setVideosSort,
77 pagination.setPagination,
78 listVideos
79 )
80 router.put('/:id',
81 oAuth.authenticate,
82 reqFiles,
83 validatorsVideos.videosUpdate,
84 updateVideoRetryWrapper
85 )
86 router.post('/',
87 oAuth.authenticate,
88 reqFiles,
89 validatorsVideos.videosAdd,
90 addVideoRetryWrapper
91 )
92 router.get('/:id',
93 validatorsVideos.videosGet,
94 getVideo
95 )
96 router.delete('/:id',
97 oAuth.authenticate,
98 validatorsVideos.videosRemove,
99 removeVideo
100 )
101 router.get('/search/:value',
102 validatorsVideos.videosSearch,
103 validatorsPagination.pagination,
104 validatorsSort.videosSort,
105 sort.setVideosSort,
106 pagination.setPagination,
107 search.setVideosSearch,
108 searchVideos
109 )
110
111 // ---------------------------------------------------------------------------
112
113 module.exports = router
114
115 // ---------------------------------------------------------------------------
116
117 function listVideoCategories (req, res, next) {
118 res.json(constants.VIDEO_CATEGORIES)
119 }
120
121 function listVideoLicences (req, res, next) {
122 res.json(constants.VIDEO_LICENCES)
123 }
124
125 function listVideoLanguages (req, res, next) {
126 res.json(constants.VIDEO_LANGUAGES)
127 }
128
129 function 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
142 function 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
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
272 function addVideoRetryWrapper (req, res, next) {
273 const options = {
274 arguments: [ req, res, req.files.videofile[0] ],
275 errorMessage: 'Cannot insert the video with many retries.'
276 }
277
278 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
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 })
284 }
285
286 function addVideo (req, res, videoFile, finalCallback) {
287 const videoInfos = req.body
288
289 waterfall([
290
291 databaseUtils.startSerializableTransaction,
292
293 function findOrCreateAuthor (t, callback) {
294 const user = res.locals.oauth.token.User
295
296 const name = user.username
297 // null because it is OUR pod
298 const podId = null
299 const userId = user.id
300
301 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
302 return callback(err, t, authorInstance)
303 })
304 },
305
306 function findOrCreateTags (t, author, callback) {
307 const tags = videoInfos.tags
308
309 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
310 return callback(err, t, author, tagInstances)
311 })
312 },
313
314 function createVideoObject (t, author, tagInstances, callback) {
315 const videoData = {
316 name: videoInfos.name,
317 remoteId: null,
318 extname: path.extname(videoFile.filename),
319 category: videoInfos.category,
320 licence: videoInfos.licence,
321 language: videoInfos.language,
322 nsfw: videoInfos.nsfw,
323 description: videoInfos.description,
324 duration: videoFile.duration,
325 authorId: author.id
326 }
327
328 const video = db.Video.build(videoData)
329
330 return callback(null, t, author, tagInstances, video)
331 },
332
333 // Set the videoname the same as the id
334 function renameVideoFile (t, author, tagInstances, video, callback) {
335 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
336 const source = path.join(videoDir, videoFile.filename)
337 const destination = path.join(videoDir, video.getVideoFilename())
338
339 fs.rename(source, destination, function (err) {
340 if (err) return callback(err)
341
342 // This is important in case if there is another attempt
343 videoFile.filename = video.getVideoFilename()
344 return callback(null, t, author, tagInstances, video)
345 })
346 },
347
348 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
349 const options = { transaction: t }
350
351 // Add tags association
352 video.save(options).asCallback(function (err, videoCreated) {
353 if (err) return callback(err)
354
355 // Do not forget to add Author informations to the created video
356 videoCreated.Author = author
357
358 return callback(err, t, tagInstances, videoCreated)
359 })
360 },
361
362 function associateTagsToVideo (t, tagInstances, video, callback) {
363 const options = { transaction: t }
364
365 video.setTags(tagInstances, options).asCallback(function (err) {
366 video.Tags = tagInstances
367
368 return callback(err, t, video)
369 })
370 },
371
372 function sendToFriends (t, video, callback) {
373 video.toAddRemoteJSON(function (err, remoteVideo) {
374 if (err) return callback(err)
375
376 // Now we'll add the video's meta data to our friends
377 friends.addVideoToFriends(remoteVideo, t, function (err) {
378 return callback(err, t)
379 })
380 })
381 },
382
383 databaseUtils.commitTransaction
384
385 ], function andFinally (err, t) {
386 if (err) {
387 // This is just a debug because we will retry the insert
388 logger.debug('Cannot insert the video.', { error: err })
389 return databaseUtils.rollbackTransaction(err, t, finalCallback)
390 }
391
392 logger.info('Video with name %s created.', videoInfos.name)
393 return finalCallback(null)
394 })
395 }
396
397 function updateVideoRetryWrapper (req, res, next) {
398 const options = {
399 arguments: [ req, res ],
400 errorMessage: 'Cannot update the video with many retries.'
401 }
402
403 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
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 })
409 }
410
411 function updateVideo (req, res, finalCallback) {
412 const videoInstance = res.locals.video
413 const videoFieldsSave = videoInstance.toJSON()
414 const videoInfosToUpdate = req.body
415
416 waterfall([
417
418 databaseUtils.startSerializableTransaction,
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) {
431 const options = {
432 transaction: t
433 }
434
435 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
436 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
437 if (videoInfosToUpdate.licence) videoInstance.set('licence', videoInfosToUpdate.licence)
438 if (videoInfosToUpdate.language) videoInstance.set('language', videoInfosToUpdate.language)
439 if (videoInfosToUpdate.nsfw) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
440 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
441
442 videoInstance.save(options).asCallback(function (err) {
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
465 friends.updateVideoToFriends(json, t, function (err) {
466 return callback(err, t)
467 })
468 },
469
470 databaseUtils.commitTransaction
471
472 ], function andFinally (err, t) {
473 if (err) {
474 logger.debug('Cannot update the video.', { error: err })
475
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
484 return databaseUtils.rollbackTransaction(err, t, finalCallback)
485 }
486
487 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
488 return finalCallback(null)
489 })
490 }
491
492 function getVideo (req, res, next) {
493 const videoInstance = res.locals.video
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
505 const qaduParams = {
506 videoId: videoInstance.id,
507 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
508 }
509 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
510 })
511 } else {
512 // Just send the event to our friends
513 const eventParams = {
514 videoId: videoInstance.id,
515 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
516 }
517 friends.addEventToRemoteVideo(eventParams)
518 }
519
520 // Do not wait the view system
521 res.json(videoInstance.toFormatedJSON())
522 }
523
524 function listVideos (req, res, next) {
525 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
526 if (err) return next(err)
527
528 res.json(utils.getFormatedObjects(videosList, videosTotal))
529 })
530 }
531
532 function removeVideo (req, res, next) {
533 const videoInstance = res.locals.video
534
535 videoInstance.destroy().asCallback(function (err) {
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()
542 })
543 }
544
545 function searchVideos (req, res, next) {
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)
550
551 res.json(utils.getFormatedObjects(videosList, videosTotal))
552 }
553 )
554 }
555
556 function 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)
559
560 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
561 })
562 }
563
564 function reportVideoAbuseRetryWrapper (req, res, next) {
565 const options = {
566 arguments: [ req, res ],
567 errorMessage: 'Cannot report abuse to the video with many retries.'
568 }
569
570 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
571 if (err) return next(err)
572
573 return res.type('json').status(204).end()
574 })
575 }
576
577 function reportVideoAbuse (req, res, finalCallback) {
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
586 }
587
588 waterfall([
589
590 databaseUtils.startSerializableTransaction,
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 }
606
607 friends.reportAbuseVideoToFriend(reportData, videoInstance)
608 }
609
610 return callback(null, t)
611 },
612
613 databaseUtils.commitTransaction
614
615 ], function andFinally (err, t) {
616 if (err) {
617 logger.debug('Cannot update the video.', { error: err })
618 return databaseUtils.rollbackTransaction(err, t, finalCallback)
619 }
620
621 logger.info('Abuse report for video %s created.', videoInstance.name)
622 return finalCallback(null)
623 })
624 }