]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Add ability for an administrator to remove any video (#61)
[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) {
7b1f49de 382 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 383 if (err) return callback(err)
807df9e6 384
528a9efa 385 // Now we'll add the video's meta data to our friends
ed04d94f 386 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 387 return callback(err, t)
ed04d94f 388 })
528a9efa 389 })
4145c1c6
C
390 },
391
392 databaseUtils.commitTransaction
807df9e6 393
7b1f49de
C
394 ], function andFinally (err, t) {
395 if (err) {
ed04d94f
C
396 // This is just a debug because we will retry the insert
397 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 398 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
399 }
400
4145c1c6
C
401 logger.info('Video with name %s created.', videoInfos.name)
402 return finalCallback(null)
7b1f49de
C
403 })
404}
405
ed04d94f 406function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
407 const options = {
408 arguments: [ req, res ],
409 errorMessage: 'Cannot update the video with many retries.'
410 }
ed04d94f 411
4df023f2 412 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
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 })
ed04d94f
C
418}
419
420function updateVideo (req, res, finalCallback) {
818f7987 421 const videoInstance = res.locals.video
7f4e7c36 422 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
423 const videoInfosToUpdate = req.body
424
425 waterfall([
426
4df023f2 427 databaseUtils.startSerializableTransaction,
7b1f49de
C
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) {
7f4e7c36
C
440 const options = {
441 transaction: t
442 }
7b1f49de
C
443
444 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
6e07c3de 445 if (videoInfosToUpdate.category) videoInstance.set('category', videoInfosToUpdate.category)
6f0c39e2 446 if (videoInfosToUpdate.licence) videoInstance.set('licence', videoInfosToUpdate.licence)
3092476e 447 if (videoInfosToUpdate.language) videoInstance.set('language', videoInfosToUpdate.language)
31b59b47 448 if (videoInfosToUpdate.nsfw) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
7b1f49de
C
449 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
450
7b1f49de 451 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
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
ed04d94f
C
474 friends.updateVideoToFriends(json, t, function (err) {
475 return callback(err, t)
476 })
4145c1c6
C
477 },
478
479 databaseUtils.commitTransaction
7b1f49de 480
7920c273 481 ], function andFinally (err, t) {
807df9e6 482 if (err) {
ed04d94f 483 logger.debug('Cannot update the video.', { error: err })
7920c273 484
7f4e7c36
C
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
4145c1c6 493 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
494 }
495
4145c1c6
C
496 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
497 return finalCallback(null)
9f10b292
C
498 })
499}
8c308c2b 500
68ce3ae0 501function getVideo (req, res, next) {
818f7987 502 const videoInstance = res.locals.video
9e167724
C
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
d38b8281
C
514 const qaduParams = {
515 videoId: videoInstance.id,
516 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
517 }
518 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
9e167724 519 })
e4c87ec2
C
520 } else {
521 // Just send the event to our friends
d38b8281
C
522 const eventParams = {
523 videoId: videoInstance.id,
524 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
525 }
526 friends.addEventToRemoteVideo(eventParams)
9e167724
C
527 }
528
529 // Do not wait the view system
818f7987 530 res.json(videoInstance.toFormatedJSON())
9f10b292 531}
8c308c2b 532
9f10b292 533function listVideos (req, res, next) {
feb4bdfd 534 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 535 if (err) return next(err)
c45f7f84 536
55fa55a9 537 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
538 })
539}
c45f7f84 540
9f10b292 541function removeVideo (req, res, next) {
818f7987 542 const videoInstance = res.locals.video
8c308c2b 543
818f7987 544 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
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()
9f10b292
C
551 })
552}
8c308c2b 553
9f10b292 554function searchVideos (req, res, next) {
7920c273
C
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)
8c308c2b 559
55fa55a9 560 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
561 }
562 )
9f10b292 563}
c173e565 564
55fa55a9
C
565function 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)
2df82d42 568
55fa55a9 569 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 570 })
55fa55a9 571}
2df82d42 572
bf4ff8fe 573function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
574 const options = {
575 arguments: [ req, res ],
576 errorMessage: 'Cannot report abuse to the video with many retries.'
577 }
bf4ff8fe 578
4df023f2
C
579 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
580 if (err) return next(err)
581
582 return res.type('json').status(204).end()
583 })
bf4ff8fe
C
584}
585
586function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
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
68ce3ae0 595 }
55fa55a9 596
bf4ff8fe
C
597 waterfall([
598
da691c46 599 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
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 }
55fa55a9 615
bf4ff8fe 616 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
617 }
618
bf4ff8fe 619 return callback(null, t)
4145c1c6
C
620 },
621
622 databaseUtils.commitTransaction
55fa55a9 623
bf4ff8fe
C
624 ], function andFinally (err, t) {
625 if (err) {
626 logger.debug('Cannot update the video.', { error: err })
4145c1c6 627 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
628 }
629
4145c1c6
C
630 logger.info('Abuse report for video %s created.', videoInstance.name)
631 return finalCallback(null)
55fa55a9 632 })
2df82d42 633}
198b205c
GS
634
635function addVideoToBlacklist (req, res, next) {
636 const videoInstance = res.locals.video
637
638 db.BlacklistedVideo.create({
639 videoId: videoInstance.id
640 })
641 .asCallback(function (err) {
642 if (err) {
643 logger.error('Errors when blacklisting video ', { error: err })
644 return next(err)
645 }
646
647 return res.type('json').status(204).end()
648 })
649}