]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Server: Fix video propagation with transcoding enabled
[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 95)
198b205c 96
fbf1134e 97router.delete('/:id',
69b0a27c 98 oAuth.authenticate,
fc51fde0 99 validatorsVideos.videosRemove,
fbf1134e
C
100 removeVideo
101)
198b205c 102
46246b5f 103router.get('/search/:value',
fc51fde0
C
104 validatorsVideos.videosSearch,
105 validatorsPagination.pagination,
106 validatorsSort.videosSort,
a877d5ac 107 sort.setVideosSort,
fbf1134e 108 pagination.setPagination,
46246b5f 109 search.setVideosSearch,
fbf1134e
C
110 searchVideos
111)
8c308c2b 112
198b205c
GS
113router.post('/:id/blacklist',
114 oAuth.authenticate,
115 admin.ensureIsAdmin,
116 validatorsVideos.videosBlacklist,
117 addVideoToBlacklist
118)
119
9f10b292 120// ---------------------------------------------------------------------------
c45f7f84 121
9f10b292 122module.exports = router
c45f7f84 123
9f10b292 124// ---------------------------------------------------------------------------
c45f7f84 125
6e07c3de
C
126function listVideoCategories (req, res, next) {
127 res.json(constants.VIDEO_CATEGORIES)
128}
129
6f0c39e2
C
130function listVideoLicences (req, res, next) {
131 res.json(constants.VIDEO_LICENCES)
132}
133
3092476e
C
134function listVideoLanguages (req, res, next) {
135 res.json(constants.VIDEO_LANGUAGES)
136}
137
d38b8281
C
138function 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
151function 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
ed04d94f
C
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
281function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
282 const options = {
283 arguments: [ req, res, req.files.videofile[0] ],
284 errorMessage: 'Cannot insert the video with many retries.'
285 }
ed04d94f 286
4df023f2 287 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
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 })
ed04d94f
C
293}
294
4145c1c6 295function addVideo (req, res, videoFile, finalCallback) {
bc503c2a 296 const videoInfos = req.body
9f10b292 297
1a42c9e2 298 waterfall([
807df9e6 299
4df023f2 300 databaseUtils.startSerializableTransaction,
7920c273 301
4145c1c6 302 function findOrCreateAuthor (t, callback) {
4712081f 303 const user = res.locals.oauth.token.User
feb4bdfd 304
4ff0d862
C
305 const name = user.username
306 // null because it is OUR pod
307 const podId = null
308 const userId = user.id
4712081f 309
4ff0d862 310 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
4145c1c6 311 return callback(err, t, authorInstance)
7920c273
C
312 })
313 },
314
4145c1c6 315 function findOrCreateTags (t, author, callback) {
7920c273 316 const tags = videoInfos.tags
4ff0d862
C
317
318 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
4145c1c6 319 return callback(err, t, author, tagInstances)
feb4bdfd
C
320 })
321 },
322
4145c1c6 323 function createVideoObject (t, author, tagInstances, callback) {
807df9e6
C
324 const videoData = {
325 name: videoInfos.name,
558d7c23
C
326 remoteId: null,
327 extname: path.extname(videoFile.filename),
6e07c3de 328 category: videoInfos.category,
6f0c39e2 329 licence: videoInfos.licence,
3092476e 330 language: videoInfos.language,
31b59b47 331 nsfw: videoInfos.nsfw,
807df9e6 332 description: videoInfos.description,
67100f1f 333 duration: videoFile.duration,
d38b8281 334 authorId: author.id
807df9e6
C
335 }
336
feb4bdfd 337 const video = db.Video.build(videoData)
558d7c23 338
4145c1c6 339 return callback(null, t, author, tagInstances, video)
558d7c23
C
340 },
341
feb4bdfd 342 // Set the videoname the same as the id
4145c1c6 343 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
344 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
345 const source = path.join(videoDir, videoFile.filename)
f285faa0 346 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
347
348 fs.rename(source, destination, function (err) {
4145c1c6 349 if (err) return callback(err)
ed04d94f
C
350
351 // This is important in case if there is another attempt
352 videoFile.filename = video.getVideoFilename()
4145c1c6 353 return callback(null, t, author, tagInstances, video)
558d7c23
C
354 })
355 },
356
4145c1c6 357 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
358 const options = { transaction: t }
359
360 // Add tags association
361 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 362 if (err) return callback(err)
7920c273 363
feb4bdfd
C
364 // Do not forget to add Author informations to the created video
365 videoCreated.Author = author
366
4145c1c6 367 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 368 })
807df9e6
C
369 },
370
4145c1c6 371 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
372 const options = { transaction: t }
373
374 video.setTags(tagInstances, options).asCallback(function (err) {
375 video.Tags = tagInstances
376
4145c1c6 377 return callback(err, t, video)
7920c273
C
378 })
379 },
380
4145c1c6 381 function sendToFriends (t, video, callback) {
62326afb
C
382 // Let transcoding job send the video to friends because the videofile extension might change
383 if (constants.CONFIG.TRANSCODING.ENABLED === true) return callback(null, t)
384
7b1f49de 385 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 386 if (err) return callback(err)
807df9e6 387
528a9efa 388 // Now we'll add the video's meta data to our friends
ed04d94f 389 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 390 return callback(err, t)
ed04d94f 391 })
528a9efa 392 })
4145c1c6
C
393 },
394
395 databaseUtils.commitTransaction
807df9e6 396
7b1f49de
C
397 ], function andFinally (err, t) {
398 if (err) {
ed04d94f
C
399 // This is just a debug because we will retry the insert
400 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 401 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
402 }
403
4145c1c6
C
404 logger.info('Video with name %s created.', videoInfos.name)
405 return finalCallback(null)
7b1f49de
C
406 })
407}
408
ed04d94f 409function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
410 const options = {
411 arguments: [ req, res ],
412 errorMessage: 'Cannot update the video with many retries.'
413 }
ed04d94f 414
4df023f2 415 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
416 if (err) return next(err)
417
418 // TODO : include Location of the new video -> 201
419 return res.type('json').status(204).end()
420 })
ed04d94f
C
421}
422
423function updateVideo (req, res, finalCallback) {
818f7987 424 const videoInstance = res.locals.video
7f4e7c36 425 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
426 const videoInfosToUpdate = req.body
427
428 waterfall([
429
4df023f2 430 databaseUtils.startSerializableTransaction,
7b1f49de
C
431
432 function findOrCreateTags (t, callback) {
433 if (videoInfosToUpdate.tags) {
434 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
435 return callback(err, t, tagInstances)
436 })
437 } else {
438 return callback(null, t, null)
439 }
440 },
441
442 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
443 const options = {
444 transaction: t
445 }
7b1f49de
C
446
447 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
6e07c3de 448 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
6f0c39e2 449 if (videoInfosToUpdate.licence) videoInstance.set('licence', videoInfosToUpdate.licence)
3092476e 450 if (videoInfosToUpdate.language) videoInstance.set('language', videoInfosToUpdate.language)
31b59b47 451 if (videoInfosToUpdate.nsfw) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
7b1f49de
C
452 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
453
7b1f49de 454 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
455 return callback(err, t, tagInstances)
456 })
457 },
458
459 function associateTagsToVideo (t, tagInstances, callback) {
460 if (tagInstances) {
461 const options = { transaction: t }
462
463 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
464 videoInstance.Tags = tagInstances
465
466 return callback(err, t)
467 })
468 } else {
469 return callback(null, t)
470 }
471 },
472
473 function sendToFriends (t, callback) {
474 const json = videoInstance.toUpdateRemoteJSON()
475
476 // Now we'll update the video's meta data to our friends
ed04d94f
C
477 friends.updateVideoToFriends(json, t, function (err) {
478 return callback(err, t)
479 })
4145c1c6
C
480 },
481
482 databaseUtils.commitTransaction
7b1f49de 483
7920c273 484 ], function andFinally (err, t) {
807df9e6 485 if (err) {
ed04d94f 486 logger.debug('Cannot update the video.', { error: err })
7920c273 487
7f4e7c36
C
488 // Force fields we want to update
489 // If the transaction is retried, sequelize will think the object has not changed
490 // So it will skip the SQL request, even if the last one was ROLLBACKed!
491 Object.keys(videoFieldsSave).forEach(function (key) {
492 const value = videoFieldsSave[key]
493 videoInstance.set(key, value)
494 })
495
4145c1c6 496 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
497 }
498
4145c1c6
C
499 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
500 return finalCallback(null)
9f10b292
C
501 })
502}
8c308c2b 503
68ce3ae0 504function getVideo (req, res, next) {
818f7987 505 const videoInstance = res.locals.video
9e167724
C
506
507 if (videoInstance.isOwned()) {
508 // The increment is done directly in the database, not using the instance value
509 videoInstance.increment('views').asCallback(function (err) {
510 if (err) {
511 logger.error('Cannot add view to video %d.', videoInstance.id)
512 return
513 }
514
515 // FIXME: make a real view system
516 // For example, only add a view when a user watch a video during 30s etc
d38b8281
C
517 const qaduParams = {
518 videoId: videoInstance.id,
519 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
520 }
521 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
9e167724 522 })
e4c87ec2
C
523 } else {
524 // Just send the event to our friends
d38b8281
C
525 const eventParams = {
526 videoId: videoInstance.id,
527 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
528 }
529 friends.addEventToRemoteVideo(eventParams)
9e167724
C
530 }
531
532 // Do not wait the view system
818f7987 533 res.json(videoInstance.toFormatedJSON())
9f10b292 534}
8c308c2b 535
9f10b292 536function listVideos (req, res, next) {
feb4bdfd 537 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 538 if (err) return next(err)
c45f7f84 539
55fa55a9 540 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
541 })
542}
c45f7f84 543
9f10b292 544function removeVideo (req, res, next) {
818f7987 545 const videoInstance = res.locals.video
8c308c2b 546
818f7987 547 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
548 if (err) {
549 logger.error('Errors when removed the video.', { error: err })
550 return next(err)
551 }
552
553 return res.type('json').status(204).end()
9f10b292
C
554 })
555}
8c308c2b 556
9f10b292 557function searchVideos (req, res, next) {
7920c273
C
558 db.Video.searchAndPopulateAuthorAndPodAndTags(
559 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
560 function (err, videosList, videosTotal) {
561 if (err) return next(err)
8c308c2b 562
55fa55a9 563 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
564 }
565 )
9f10b292 566}
c173e565 567
55fa55a9
C
568function listVideoAbuses (req, res, next) {
569 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
570 if (err) return next(err)
2df82d42 571
55fa55a9 572 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 573 })
55fa55a9 574}
2df82d42 575
bf4ff8fe 576function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
577 const options = {
578 arguments: [ req, res ],
579 errorMessage: 'Cannot report abuse to the video with many retries.'
580 }
bf4ff8fe 581
4df023f2
C
582 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
583 if (err) return next(err)
584
585 return res.type('json').status(204).end()
586 })
bf4ff8fe
C
587}
588
589function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
590 const videoInstance = res.locals.video
591 const reporterUsername = res.locals.oauth.token.User.username
592
593 const abuse = {
594 reporterUsername,
595 reason: req.body.reason,
596 videoId: videoInstance.id,
597 reporterPodId: null // This is our pod that reported this abuse
68ce3ae0 598 }
55fa55a9 599
bf4ff8fe
C
600 waterfall([
601
da691c46 602 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
603
604 function createAbuse (t, callback) {
605 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
606 return callback(err, t, abuse)
607 })
608 },
609
610 function sendToFriendsIfNeeded (t, abuse, callback) {
611 // We send the information to the destination pod
612 if (videoInstance.isOwned() === false) {
613 const reportData = {
614 reporterUsername,
615 reportReason: abuse.reason,
616 videoRemoteId: videoInstance.remoteId
617 }
55fa55a9 618
bf4ff8fe 619 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
620 }
621
bf4ff8fe 622 return callback(null, t)
4145c1c6
C
623 },
624
625 databaseUtils.commitTransaction
55fa55a9 626
bf4ff8fe
C
627 ], function andFinally (err, t) {
628 if (err) {
629 logger.debug('Cannot update the video.', { error: err })
4145c1c6 630 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
631 }
632
4145c1c6
C
633 logger.info('Abuse report for video %s created.', videoInstance.name)
634 return finalCallback(null)
55fa55a9 635 })
2df82d42 636}
198b205c
GS
637
638function addVideoToBlacklist (req, res, next) {
639 const videoInstance = res.locals.video
640
ab683a8e 641 const toCreate = {
198b205c 642 videoId: videoInstance.id
ab683a8e
C
643 }
644
645 db.BlacklistedVideo.create(toCreate).asCallback(function (err) {
198b205c
GS
646 if (err) {
647 logger.error('Errors when blacklisting video ', { error: err })
648 return next(err)
649 }
650
651 return res.type('json').status(204).end()
652 })
653}