]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Server: typo
[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,
feb4bdfd 158 authorId: author.id
807df9e6
C
159 }
160
feb4bdfd 161 const video = db.Video.build(videoData)
558d7c23 162
4145c1c6 163 return callback(null, t, author, tagInstances, video)
558d7c23
C
164 },
165
feb4bdfd 166 // Set the videoname the same as the id
4145c1c6 167 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
168 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
169 const source = path.join(videoDir, videoFile.filename)
f285faa0 170 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
171
172 fs.rename(source, destination, function (err) {
4145c1c6 173 if (err) return callback(err)
ed04d94f
C
174
175 // This is important in case if there is another attempt
176 videoFile.filename = video.getVideoFilename()
4145c1c6 177 return callback(null, t, author, tagInstances, video)
558d7c23
C
178 })
179 },
180
4145c1c6 181 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
182 const options = { transaction: t }
183
184 // Add tags association
185 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 186 if (err) return callback(err)
7920c273 187
feb4bdfd
C
188 // Do not forget to add Author informations to the created video
189 videoCreated.Author = author
190
4145c1c6 191 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 192 })
807df9e6
C
193 },
194
4145c1c6 195 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
196 const options = { transaction: t }
197
198 video.setTags(tagInstances, options).asCallback(function (err) {
199 video.Tags = tagInstances
200
4145c1c6 201 return callback(err, t, video)
7920c273
C
202 })
203 },
204
4145c1c6 205 function sendToFriends (t, video, callback) {
7b1f49de 206 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 207 if (err) return callback(err)
807df9e6 208
528a9efa 209 // Now we'll add the video's meta data to our friends
ed04d94f 210 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 211 return callback(err, t)
ed04d94f 212 })
528a9efa 213 })
4145c1c6
C
214 },
215
216 databaseUtils.commitTransaction
807df9e6 217
7b1f49de
C
218 ], function andFinally (err, t) {
219 if (err) {
ed04d94f
C
220 // This is just a debug because we will retry the insert
221 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 222 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
223 }
224
4145c1c6
C
225 logger.info('Video with name %s created.', videoInfos.name)
226 return finalCallback(null)
7b1f49de
C
227 })
228}
229
ed04d94f 230function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
231 const options = {
232 arguments: [ req, res ],
233 errorMessage: 'Cannot update the video with many retries.'
234 }
ed04d94f 235
4df023f2 236 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
237 if (err) return next(err)
238
239 // TODO : include Location of the new video -> 201
240 return res.type('json').status(204).end()
241 })
ed04d94f
C
242}
243
244function updateVideo (req, res, finalCallback) {
818f7987 245 const videoInstance = res.locals.video
7f4e7c36 246 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
247 const videoInfosToUpdate = req.body
248
249 waterfall([
250
4df023f2 251 databaseUtils.startSerializableTransaction,
7b1f49de
C
252
253 function findOrCreateTags (t, callback) {
254 if (videoInfosToUpdate.tags) {
255 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
256 return callback(err, t, tagInstances)
257 })
258 } else {
259 return callback(null, t, null)
260 }
261 },
262
263 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
264 const options = {
265 transaction: t
266 }
7b1f49de
C
267
268 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
269 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
270
7b1f49de 271 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
272 return callback(err, t, tagInstances)
273 })
274 },
275
276 function associateTagsToVideo (t, tagInstances, callback) {
277 if (tagInstances) {
278 const options = { transaction: t }
279
280 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
281 videoInstance.Tags = tagInstances
282
283 return callback(err, t)
284 })
285 } else {
286 return callback(null, t)
287 }
288 },
289
290 function sendToFriends (t, callback) {
291 const json = videoInstance.toUpdateRemoteJSON()
292
293 // Now we'll update the video's meta data to our friends
ed04d94f
C
294 friends.updateVideoToFriends(json, t, function (err) {
295 return callback(err, t)
296 })
4145c1c6
C
297 },
298
299 databaseUtils.commitTransaction
7b1f49de 300
7920c273 301 ], function andFinally (err, t) {
807df9e6 302 if (err) {
ed04d94f 303 logger.debug('Cannot update the video.', { error: err })
7920c273 304
7f4e7c36
C
305 // Force fields we want to update
306 // If the transaction is retried, sequelize will think the object has not changed
307 // So it will skip the SQL request, even if the last one was ROLLBACKed!
308 Object.keys(videoFieldsSave).forEach(function (key) {
309 const value = videoFieldsSave[key]
310 videoInstance.set(key, value)
311 })
312
4145c1c6 313 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
314 }
315
4145c1c6
C
316 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
317 return finalCallback(null)
9f10b292
C
318 })
319}
8c308c2b 320
68ce3ae0 321function getVideo (req, res, next) {
818f7987 322 const videoInstance = res.locals.video
9e167724
C
323
324 if (videoInstance.isOwned()) {
325 // The increment is done directly in the database, not using the instance value
326 videoInstance.increment('views').asCallback(function (err) {
327 if (err) {
328 logger.error('Cannot add view to video %d.', videoInstance.id)
329 return
330 }
331
332 // FIXME: make a real view system
333 // For example, only add a view when a user watch a video during 30s etc
334 friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS)
335 })
e4c87ec2
C
336 } else {
337 // Just send the event to our friends
338 friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
9e167724
C
339 }
340
341 // Do not wait the view system
818f7987 342 res.json(videoInstance.toFormatedJSON())
9f10b292 343}
8c308c2b 344
9f10b292 345function listVideos (req, res, next) {
feb4bdfd 346 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 347 if (err) return next(err)
c45f7f84 348
55fa55a9 349 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
350 })
351}
c45f7f84 352
9f10b292 353function removeVideo (req, res, next) {
818f7987 354 const videoInstance = res.locals.video
8c308c2b 355
818f7987 356 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
357 if (err) {
358 logger.error('Errors when removed the video.', { error: err })
359 return next(err)
360 }
361
362 return res.type('json').status(204).end()
9f10b292
C
363 })
364}
8c308c2b 365
9f10b292 366function searchVideos (req, res, next) {
7920c273
C
367 db.Video.searchAndPopulateAuthorAndPodAndTags(
368 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
369 function (err, videosList, videosTotal) {
370 if (err) return next(err)
8c308c2b 371
55fa55a9 372 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
373 }
374 )
9f10b292 375}
c173e565 376
55fa55a9
C
377function listVideoAbuses (req, res, next) {
378 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
379 if (err) return next(err)
2df82d42 380
55fa55a9 381 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 382 })
55fa55a9 383}
2df82d42 384
bf4ff8fe 385function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
386 const options = {
387 arguments: [ req, res ],
388 errorMessage: 'Cannot report abuse to the video with many retries.'
389 }
bf4ff8fe 390
4df023f2
C
391 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
392 if (err) return next(err)
393
394 return res.type('json').status(204).end()
395 })
bf4ff8fe
C
396}
397
398function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
399 const videoInstance = res.locals.video
400 const reporterUsername = res.locals.oauth.token.User.username
401
402 const abuse = {
403 reporterUsername,
404 reason: req.body.reason,
405 videoId: videoInstance.id,
406 reporterPodId: null // This is our pod that reported this abuse
68ce3ae0 407 }
55fa55a9 408
bf4ff8fe
C
409 waterfall([
410
da691c46 411 databaseUtils.startSerializableTransaction,
bf4ff8fe
C
412
413 function createAbuse (t, callback) {
414 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
415 return callback(err, t, abuse)
416 })
417 },
418
419 function sendToFriendsIfNeeded (t, abuse, callback) {
420 // We send the information to the destination pod
421 if (videoInstance.isOwned() === false) {
422 const reportData = {
423 reporterUsername,
424 reportReason: abuse.reason,
425 videoRemoteId: videoInstance.remoteId
426 }
55fa55a9 427
bf4ff8fe 428 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
429 }
430
bf4ff8fe 431 return callback(null, t)
4145c1c6
C
432 },
433
434 databaseUtils.commitTransaction
55fa55a9 435
bf4ff8fe
C
436 ], function andFinally (err, t) {
437 if (err) {
438 logger.debug('Cannot update the video.', { error: err })
4145c1c6 439 return databaseUtils.rollbackTransaction(err, t, finalCallback)
bf4ff8fe
C
440 }
441
4145c1c6
C
442 logger.info('Abuse report for video %s created.', videoInstance.name)
443 return finalCallback(null)
55fa55a9 444 })
2df82d42 445}