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