]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
Server: add licence video 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
51 router.get('/abuse',
52 oAuth.authenticate,
53 admin.ensureIsAdmin,
54 validatorsPagination.pagination,
55 validatorsSort.videoAbusesSort,
56 sort.setVideoAbusesSort,
57 pagination.setPagination,
58 listVideoAbuses
59 )
60 router.post('/:id/abuse',
61 oAuth.authenticate,
62 validatorsVideos.videoAbuseReport,
63 reportVideoAbuseRetryWrapper
64 )
65
66 router.put('/:id/rate',
67 oAuth.authenticate,
68 validatorsVideos.videoRate,
69 rateVideoRetryWrapper
70 )
71
72 router.get('/',
73 validatorsPagination.pagination,
74 validatorsSort.videosSort,
75 sort.setVideosSort,
76 pagination.setPagination,
77 listVideos
78 )
79 router.put('/:id',
80 oAuth.authenticate,
81 reqFiles,
82 validatorsVideos.videosUpdate,
83 updateVideoRetryWrapper
84 )
85 router.post('/',
86 oAuth.authenticate,
87 reqFiles,
88 validatorsVideos.videosAdd,
89 addVideoRetryWrapper
90 )
91 router.get('/:id',
92 validatorsVideos.videosGet,
93 getVideo
94 )
95 router.delete('/:id',
96 oAuth.authenticate,
97 validatorsVideos.videosRemove,
98 removeVideo
99 )
100 router.get('/search/:value',
101 validatorsVideos.videosSearch,
102 validatorsPagination.pagination,
103 validatorsSort.videosSort,
104 sort.setVideosSort,
105 pagination.setPagination,
106 search.setVideosSearch,
107 searchVideos
108 )
109
110 // ---------------------------------------------------------------------------
111
112 module.exports = router
113
114 // ---------------------------------------------------------------------------
115
116 function listVideoCategories (req, res, next) {
117 res.json(constants.VIDEO_CATEGORIES)
118 }
119
120 function listVideoLicences (req, res, next) {
121 res.json(constants.VIDEO_LICENCES)
122 }
123
124 function 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
137 function 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
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
267 function addVideoRetryWrapper (req, res, next) {
268 const options = {
269 arguments: [ req, res, req.files.videofile[0] ],
270 errorMessage: 'Cannot insert the video with many retries.'
271 }
272
273 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
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 })
279 }
280
281 function addVideo (req, res, videoFile, finalCallback) {
282 const videoInfos = req.body
283
284 waterfall([
285
286 databaseUtils.startSerializableTransaction,
287
288 function findOrCreateAuthor (t, callback) {
289 const user = res.locals.oauth.token.User
290
291 const name = user.username
292 // null because it is OUR pod
293 const podId = null
294 const userId = user.id
295
296 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
297 return callback(err, t, authorInstance)
298 })
299 },
300
301 function findOrCreateTags (t, author, callback) {
302 const tags = videoInfos.tags
303
304 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
305 return callback(err, t, author, tagInstances)
306 })
307 },
308
309 function createVideoObject (t, author, tagInstances, callback) {
310 const videoData = {
311 name: videoInfos.name,
312 remoteId: null,
313 extname: path.extname(videoFile.filename),
314 category: videoInfos.category,
315 licence: videoInfos.licence,
316 description: videoInfos.description,
317 duration: videoFile.duration,
318 authorId: author.id
319 }
320
321 const video = db.Video.build(videoData)
322
323 return callback(null, t, author, tagInstances, video)
324 },
325
326 // Set the videoname the same as the id
327 function renameVideoFile (t, author, tagInstances, video, callback) {
328 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
329 const source = path.join(videoDir, videoFile.filename)
330 const destination = path.join(videoDir, video.getVideoFilename())
331
332 fs.rename(source, destination, function (err) {
333 if (err) return callback(err)
334
335 // This is important in case if there is another attempt
336 videoFile.filename = video.getVideoFilename()
337 return callback(null, t, author, tagInstances, video)
338 })
339 },
340
341 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
342 const options = { transaction: t }
343
344 // Add tags association
345 video.save(options).asCallback(function (err, videoCreated) {
346 if (err) return callback(err)
347
348 // Do not forget to add Author informations to the created video
349 videoCreated.Author = author
350
351 return callback(err, t, tagInstances, videoCreated)
352 })
353 },
354
355 function associateTagsToVideo (t, tagInstances, video, callback) {
356 const options = { transaction: t }
357
358 video.setTags(tagInstances, options).asCallback(function (err) {
359 video.Tags = tagInstances
360
361 return callback(err, t, video)
362 })
363 },
364
365 function sendToFriends (t, video, callback) {
366 video.toAddRemoteJSON(function (err, remoteVideo) {
367 if (err) return callback(err)
368
369 // Now we'll add the video's meta data to our friends
370 friends.addVideoToFriends(remoteVideo, t, function (err) {
371 return callback(err, t)
372 })
373 })
374 },
375
376 databaseUtils.commitTransaction
377
378 ], function andFinally (err, t) {
379 if (err) {
380 // This is just a debug because we will retry the insert
381 logger.debug('Cannot insert the video.', { error: err })
382 return databaseUtils.rollbackTransaction(err, t, finalCallback)
383 }
384
385 logger.info('Video with name %s created.', videoInfos.name)
386 return finalCallback(null)
387 })
388 }
389
390 function updateVideoRetryWrapper (req, res, next) {
391 const options = {
392 arguments: [ req, res ],
393 errorMessage: 'Cannot update the video with many retries.'
394 }
395
396 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
397 if (err) return next(err)
398
399 // TODO : include Location of the new video -> 201
400 return res.type('json').status(204).end()
401 })
402 }
403
404 function updateVideo (req, res, finalCallback) {
405 const videoInstance = res.locals.video
406 const videoFieldsSave = videoInstance.toJSON()
407 const videoInfosToUpdate = req.body
408
409 waterfall([
410
411 databaseUtils.startSerializableTransaction,
412
413 function findOrCreateTags (t, callback) {
414 if (videoInfosToUpdate.tags) {
415 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
416 return callback(err, t, tagInstances)
417 })
418 } else {
419 return callback(null, t, null)
420 }
421 },
422
423 function updateVideoIntoDB (t, tagInstances, callback) {
424 const options = {
425 transaction: t
426 }
427
428 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
429 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
430 if (videoInfosToUpdate.licence) videoInstance.set('licence', videoInfosToUpdate.licence)
431 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
432
433 videoInstance.save(options).asCallback(function (err) {
434 return callback(err, t, tagInstances)
435 })
436 },
437
438 function associateTagsToVideo (t, tagInstances, callback) {
439 if (tagInstances) {
440 const options = { transaction: t }
441
442 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
443 videoInstance.Tags = tagInstances
444
445 return callback(err, t)
446 })
447 } else {
448 return callback(null, t)
449 }
450 },
451
452 function sendToFriends (t, callback) {
453 const json = videoInstance.toUpdateRemoteJSON()
454
455 // Now we'll update the video's meta data to our friends
456 friends.updateVideoToFriends(json, t, function (err) {
457 return callback(err, t)
458 })
459 },
460
461 databaseUtils.commitTransaction
462
463 ], function andFinally (err, t) {
464 if (err) {
465 logger.debug('Cannot update the video.', { error: err })
466
467 // Force fields we want to update
468 // If the transaction is retried, sequelize will think the object has not changed
469 // So it will skip the SQL request, even if the last one was ROLLBACKed!
470 Object.keys(videoFieldsSave).forEach(function (key) {
471 const value = videoFieldsSave[key]
472 videoInstance.set(key, value)
473 })
474
475 return databaseUtils.rollbackTransaction(err, t, finalCallback)
476 }
477
478 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
479 return finalCallback(null)
480 })
481 }
482
483 function getVideo (req, res, next) {
484 const videoInstance = res.locals.video
485
486 if (videoInstance.isOwned()) {
487 // The increment is done directly in the database, not using the instance value
488 videoInstance.increment('views').asCallback(function (err) {
489 if (err) {
490 logger.error('Cannot add view to video %d.', videoInstance.id)
491 return
492 }
493
494 // FIXME: make a real view system
495 // For example, only add a view when a user watch a video during 30s etc
496 const qaduParams = {
497 videoId: videoInstance.id,
498 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
499 }
500 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
501 })
502 } else {
503 // Just send the event to our friends
504 const eventParams = {
505 videoId: videoInstance.id,
506 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
507 }
508 friends.addEventToRemoteVideo(eventParams)
509 }
510
511 // Do not wait the view system
512 res.json(videoInstance.toFormatedJSON())
513 }
514
515 function listVideos (req, res, next) {
516 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
517 if (err) return next(err)
518
519 res.json(utils.getFormatedObjects(videosList, videosTotal))
520 })
521 }
522
523 function removeVideo (req, res, next) {
524 const videoInstance = res.locals.video
525
526 videoInstance.destroy().asCallback(function (err) {
527 if (err) {
528 logger.error('Errors when removed the video.', { error: err })
529 return next(err)
530 }
531
532 return res.type('json').status(204).end()
533 })
534 }
535
536 function searchVideos (req, res, next) {
537 db.Video.searchAndPopulateAuthorAndPodAndTags(
538 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
539 function (err, videosList, videosTotal) {
540 if (err) return next(err)
541
542 res.json(utils.getFormatedObjects(videosList, videosTotal))
543 }
544 )
545 }
546
547 function listVideoAbuses (req, res, next) {
548 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
549 if (err) return next(err)
550
551 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
552 })
553 }
554
555 function reportVideoAbuseRetryWrapper (req, res, next) {
556 const options = {
557 arguments: [ req, res ],
558 errorMessage: 'Cannot report abuse to the video with many retries.'
559 }
560
561 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
562 if (err) return next(err)
563
564 return res.type('json').status(204).end()
565 })
566 }
567
568 function reportVideoAbuse (req, res, finalCallback) {
569 const videoInstance = res.locals.video
570 const reporterUsername = res.locals.oauth.token.User.username
571
572 const abuse = {
573 reporterUsername,
574 reason: req.body.reason,
575 videoId: videoInstance.id,
576 reporterPodId: null // This is our pod that reported this abuse
577 }
578
579 waterfall([
580
581 databaseUtils.startSerializableTransaction,
582
583 function createAbuse (t, callback) {
584 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
585 return callback(err, t, abuse)
586 })
587 },
588
589 function sendToFriendsIfNeeded (t, abuse, callback) {
590 // We send the information to the destination pod
591 if (videoInstance.isOwned() === false) {
592 const reportData = {
593 reporterUsername,
594 reportReason: abuse.reason,
595 videoRemoteId: videoInstance.remoteId
596 }
597
598 friends.reportAbuseVideoToFriend(reportData, videoInstance)
599 }
600
601 return callback(null, t)
602 },
603
604 databaseUtils.commitTransaction
605
606 ], function andFinally (err, t) {
607 if (err) {
608 logger.debug('Cannot update the video.', { error: err })
609 return databaseUtils.rollbackTransaction(err, t, finalCallback)
610 }
611
612 logger.info('Abuse report for video %s created.', videoInstance.name)
613 return finalCallback(null)
614 })
615 }