]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Server: add views attribute when sending videos to friends
[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
55fa55a9
C
48router.get('/abuse',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsPagination.pagination,
52 validatorsSort.videoAbusesSort,
53 sort.setVideoAbusesSort,
54 pagination.setPagination,
55 listVideoAbuses
56)
57router.post('/:id/abuse',
58 oAuth.authenticate,
59 validatorsVideos.videoAbuseReport,
bf4ff8fe 60 reportVideoAbuseRetryWrapper
55fa55a9
C
61)
62
fbf1134e 63router.get('/',
fc51fde0
C
64 validatorsPagination.pagination,
65 validatorsSort.videosSort,
a877d5ac 66 sort.setVideosSort,
fbf1134e
C
67 pagination.setPagination,
68 listVideos
69)
7b1f49de
C
70router.put('/:id',
71 oAuth.authenticate,
72 reqFiles,
73 validatorsVideos.videosUpdate,
ed04d94f 74 updateVideoRetryWrapper
7b1f49de 75)
fbf1134e 76router.post('/',
69b0a27c 77 oAuth.authenticate,
fbf1134e 78 reqFiles,
fc51fde0 79 validatorsVideos.videosAdd,
ed04d94f 80 addVideoRetryWrapper
fbf1134e
C
81)
82router.get('/:id',
fc51fde0 83 validatorsVideos.videosGet,
68ce3ae0 84 getVideo
fbf1134e
C
85)
86router.delete('/:id',
69b0a27c 87 oAuth.authenticate,
fc51fde0 88 validatorsVideos.videosRemove,
fbf1134e
C
89 removeVideo
90)
46246b5f 91router.get('/search/:value',
fc51fde0
C
92 validatorsVideos.videosSearch,
93 validatorsPagination.pagination,
94 validatorsSort.videosSort,
a877d5ac 95 sort.setVideosSort,
fbf1134e 96 pagination.setPagination,
46246b5f 97 search.setVideosSearch,
fbf1134e
C
98 searchVideos
99)
8c308c2b 100
9f10b292 101// ---------------------------------------------------------------------------
c45f7f84 102
9f10b292 103module.exports = router
c45f7f84 104
9f10b292 105// ---------------------------------------------------------------------------
c45f7f84 106
ed04d94f
C
107// Wrapper to video add that retry the function if there is a database error
108// We need this because we run the transaction in SERIALIZABLE isolation that can fail
109function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
110 const options = {
111 arguments: [ req, res, req.files.videofile[0] ],
112 errorMessage: 'Cannot insert the video with many retries.'
113 }
ed04d94f 114
4df023f2 115 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
116 if (err) return next(err)
117
118 // TODO : include Location of the new video -> 201
119 return res.type('json').status(204).end()
120 })
ed04d94f
C
121}
122
4145c1c6 123function addVideo (req, res, videoFile, finalCallback) {
bc503c2a 124 const videoInfos = req.body
9f10b292 125
1a42c9e2 126 waterfall([
807df9e6 127
4df023f2 128 databaseUtils.startSerializableTransaction,
7920c273 129
4145c1c6 130 function findOrCreateAuthor (t, callback) {
4712081f 131 const user = res.locals.oauth.token.User
feb4bdfd 132
4ff0d862
C
133 const name = user.username
134 // null because it is OUR pod
135 const podId = null
136 const userId = user.id
4712081f 137
4ff0d862 138 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
4145c1c6 139 return callback(err, t, authorInstance)
7920c273
C
140 })
141 },
142
4145c1c6 143 function findOrCreateTags (t, author, callback) {
7920c273 144 const tags = videoInfos.tags
4ff0d862
C
145
146 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
4145c1c6 147 return callback(err, t, author, tagInstances)
feb4bdfd
C
148 })
149 },
150
4145c1c6 151 function createVideoObject (t, author, tagInstances, callback) {
807df9e6
C
152 const videoData = {
153 name: videoInfos.name,
558d7c23
C
154 remoteId: null,
155 extname: path.extname(videoFile.filename),
807df9e6 156 description: videoInfos.description,
67100f1f 157 duration: videoFile.duration,
e3d156b3
C
158 authorId: author.id,
159 views: videoInfos.views
807df9e6
C
160 }
161
feb4bdfd 162 const video = db.Video.build(videoData)
558d7c23 163
4145c1c6 164 return callback(null, t, author, tagInstances, video)
558d7c23
C
165 },
166
feb4bdfd 167 // Set the videoname the same as the id
4145c1c6 168 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
169 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
170 const source = path.join(videoDir, videoFile.filename)
f285faa0 171 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
172
173 fs.rename(source, destination, function (err) {
4145c1c6 174 if (err) return callback(err)
ed04d94f
C
175
176 // This is important in case if there is another attempt
177 videoFile.filename = video.getVideoFilename()
4145c1c6 178 return callback(null, t, author, tagInstances, video)
558d7c23
C
179 })
180 },
181
4145c1c6 182 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
183 const options = { transaction: t }
184
185 // Add tags association
186 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 187 if (err) return callback(err)
7920c273 188
feb4bdfd
C
189 // Do not forget to add Author informations to the created video
190 videoCreated.Author = author
191
4145c1c6 192 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 193 })
807df9e6
C
194 },
195
4145c1c6 196 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
197 const options = { transaction: t }
198
199 video.setTags(tagInstances, options).asCallback(function (err) {
200 video.Tags = tagInstances
201
4145c1c6 202 return callback(err, t, video)
7920c273
C
203 })
204 },
205
4145c1c6 206 function sendToFriends (t, video, callback) {
7b1f49de 207 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 208 if (err) return callback(err)
807df9e6 209
528a9efa 210 // Now we'll add the video's meta data to our friends
ed04d94f 211 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 212 return callback(err, t)
ed04d94f 213 })
528a9efa 214 })
4145c1c6
C
215 },
216
217 databaseUtils.commitTransaction
807df9e6 218
7b1f49de
C
219 ], function andFinally (err, t) {
220 if (err) {
ed04d94f
C
221 // This is just a debug because we will retry the insert
222 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 223 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
224 }
225
4145c1c6
C
226 logger.info('Video with name %s created.', videoInfos.name)
227 return finalCallback(null)
7b1f49de
C
228 })
229}
230
ed04d94f 231function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
232 const options = {
233 arguments: [ req, res ],
234 errorMessage: 'Cannot update the video with many retries.'
235 }
ed04d94f 236
4df023f2 237 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
238 if (err) return next(err)
239
240 // TODO : include Location of the new video -> 201
241 return res.type('json').status(204).end()
242 })
ed04d94f
C
243}
244
245function updateVideo (req, res, finalCallback) {
818f7987 246 const videoInstance = res.locals.video
7f4e7c36 247 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
248 const videoInfosToUpdate = req.body
249
250 waterfall([
251
4df023f2 252 databaseUtils.startSerializableTransaction,
7b1f49de
C
253
254 function findOrCreateTags (t, callback) {
255 if (videoInfosToUpdate.tags) {
256 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
257 return callback(err, t, tagInstances)
258 })
259 } else {
260 return callback(null, t, null)
261 }
262 },
263
264 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
265 const options = {
266 transaction: t
267 }
7b1f49de
C
268
269 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
270 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
271
7b1f49de 272 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
273 return callback(err, t, tagInstances)
274 })
275 },
276
277 function associateTagsToVideo (t, tagInstances, callback) {
278 if (tagInstances) {
279 const options = { transaction: t }
280
281 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
282 videoInstance.Tags = tagInstances
283
284 return callback(err, t)
285 })
286 } else {
287 return callback(null, t)
288 }
289 },
290
291 function sendToFriends (t, callback) {
292 const json = videoInstance.toUpdateRemoteJSON()
293
294 // Now we'll update the video's meta data to our friends
ed04d94f
C
295 friends.updateVideoToFriends(json, t, function (err) {
296 return callback(err, t)
297 })
4145c1c6
C
298 },
299
300 databaseUtils.commitTransaction
7b1f49de 301
7920c273 302 ], function andFinally (err, t) {
807df9e6 303 if (err) {
ed04d94f 304 logger.debug('Cannot update the video.', { error: err })
7920c273 305
7f4e7c36
C
306 // Force fields we want to update
307 // If the transaction is retried, sequelize will think the object has not changed
308 // So it will skip the SQL request, even if the last one was ROLLBACKed!
309 Object.keys(videoFieldsSave).forEach(function (key) {
310 const value = videoFieldsSave[key]
311 videoInstance.set(key, value)
312 })
313
4145c1c6 314 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
315 }
316
4145c1c6
C
317 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
318 return finalCallback(null)
9f10b292
C
319 })
320}
8c308c2b 321
68ce3ae0 322function getVideo (req, res, next) {
818f7987 323 const videoInstance = res.locals.video
9e167724
C
324
325 if (videoInstance.isOwned()) {
326 // The increment is done directly in the database, not using the instance value
327 videoInstance.increment('views').asCallback(function (err) {
328 if (err) {
329 logger.error('Cannot add view to video %d.', videoInstance.id)
330 return
331 }
332
333 // FIXME: make a real view system
334 // For example, only add a view when a user watch a video during 30s etc
335 friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS)
336 })
e4c87ec2
C
337 } else {
338 // Just send the event to our friends
339 friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
9e167724
C
340 }
341
342 // Do not wait the view system
818f7987 343 res.json(videoInstance.toFormatedJSON())
9f10b292 344}
8c308c2b 345
9f10b292 346function listVideos (req, res, next) {
feb4bdfd 347 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 348 if (err) return next(err)
c45f7f84 349
55fa55a9 350 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
351 })
352}
c45f7f84 353
9f10b292 354function removeVideo (req, res, next) {
818f7987 355 const videoInstance = res.locals.video
8c308c2b 356
818f7987 357 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
358 if (err) {
359 logger.error('Errors when removed the video.', { error: err })
360 return next(err)
361 }
362
363 return res.type('json').status(204).end()
9f10b292
C
364 })
365}
8c308c2b 366
9f10b292 367function searchVideos (req, res, next) {
7920c273
C
368 db.Video.searchAndPopulateAuthorAndPodAndTags(
369 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
370 function (err, videosList, videosTotal) {
371 if (err) return next(err)
8c308c2b 372
55fa55a9 373 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
374 }
375 )
9f10b292 376}
c173e565 377
55fa55a9
C
378function listVideoAbuses (req, res, next) {
379 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
380 if (err) return next(err)
2df82d42 381
55fa55a9 382 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 383 })
55fa55a9 384}
2df82d42 385
bf4ff8fe 386function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
387 const options = {
388 arguments: [ req, res ],
389 errorMessage: 'Cannot report abuse to the video with many retries.'
390 }
bf4ff8fe 391
4df023f2
C
392 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
393 if (err) return next(err)
394
395 return res.type('json').status(204).end()
396 })
bf4ff8fe
C
397}
398
399function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
400 const videoInstance = res.locals.video
401 const reporterUsername = res.locals.oauth.token.User.username
402
403 const abuse = {
404 reporterUsername,
405 reason: req.body.reason,
406 videoId: videoInstance.id,
407 reporterPodId: null // This is our pod that reported this abuse
68ce3ae0 408 }
55fa55a9 409
bf4ff8fe
C
410 waterfall([
411
da691c46 412 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
413
414 function createAbuse (t, callback) {
415 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
416 return callback(err, t, abuse)
417 })
418 },
419
420 function sendToFriendsIfNeeded (t, abuse, callback) {
421 // We send the information to the destination pod
422 if (videoInstance.isOwned() === false) {
423 const reportData = {
424 reporterUsername,
425 reportReason: abuse.reason,
426 videoRemoteId: videoInstance.remoteId
427 }
55fa55a9 428
bf4ff8fe 429 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
430 }
431
bf4ff8fe 432 return callback(null, t)
4145c1c6
C
433 },
434
435 databaseUtils.commitTransaction
55fa55a9 436
bf4ff8fe
C
437 ], function andFinally (err, t) {
438 if (err) {
439 logger.debug('Cannot update the video.', { error: err })
4145c1c6 440 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
441 }
442
4145c1c6
C
443 logger.info('Abuse report for video %s created.', videoInstance.name)
444 return finalCallback(null)
55fa55a9 445 })
2df82d42 446}