]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
5a67d1121fcb9c3f2f7df73ec548cb422b5e9f42
[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('/abuse',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsPagination.pagination,
52 validatorsSort.videoAbusesSort,
53 sort.setVideoAbusesSort,
54 pagination.setPagination,
55 listVideoAbuses
56 )
57 router.post('/:id/abuse',
58 oAuth.authenticate,
59 validatorsVideos.videoAbuseReport,
60 reportVideoAbuseRetryWrapper
61 )
62
63 router.get('/',
64 validatorsPagination.pagination,
65 validatorsSort.videosSort,
66 sort.setVideosSort,
67 pagination.setPagination,
68 listVideos
69 )
70 router.put('/:id',
71 oAuth.authenticate,
72 reqFiles,
73 validatorsVideos.videosUpdate,
74 updateVideoRetryWrapper
75 )
76 router.post('/',
77 oAuth.authenticate,
78 reqFiles,
79 validatorsVideos.videosAdd,
80 addVideoRetryWrapper
81 )
82 router.get('/:id',
83 validatorsVideos.videosGet,
84 getVideo
85 )
86 router.delete('/:id',
87 oAuth.authenticate,
88 validatorsVideos.videosRemove,
89 removeVideo
90 )
91 router.get('/search/:value',
92 validatorsVideos.videosSearch,
93 validatorsPagination.pagination,
94 validatorsSort.videosSort,
95 sort.setVideosSort,
96 pagination.setPagination,
97 search.setVideosSearch,
98 searchVideos
99 )
100
101 // ---------------------------------------------------------------------------
102
103 module.exports = router
104
105 // ---------------------------------------------------------------------------
106
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
109 function addVideoRetryWrapper (req, res, next) {
110 const options = {
111 arguments: [ req, res, req.files.videofile[0] ],
112 errorMessage: 'Cannot insert the video with many retries.'
113 }
114
115 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
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 })
121 }
122
123 function addVideo (req, res, videoFile, finalCallback) {
124 const videoInfos = req.body
125
126 waterfall([
127
128 databaseUtils.startSerializableTransaction,
129
130 function findOrCreateAuthor (t, callback) {
131 const user = res.locals.oauth.token.User
132
133 const name = user.username
134 // null because it is OUR pod
135 const podId = null
136 const userId = user.id
137
138 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
139 return callback(err, t, authorInstance)
140 })
141 },
142
143 function findOrCreateTags (t, author, callback) {
144 const tags = videoInfos.tags
145
146 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
147 return callback(err, t, author, tagInstances)
148 })
149 },
150
151 function createVideoObject (t, author, tagInstances, callback) {
152 const videoData = {
153 name: videoInfos.name,
154 remoteId: null,
155 extname: path.extname(videoFile.filename),
156 description: videoInfos.description,
157 duration: videoFile.duration,
158 authorId: author.id,
159 views: videoInfos.views
160 }
161
162 const video = db.Video.build(videoData)
163
164 return callback(null, t, author, tagInstances, video)
165 },
166
167 // Set the videoname the same as the id
168 function renameVideoFile (t, author, tagInstances, video, callback) {
169 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
170 const source = path.join(videoDir, videoFile.filename)
171 const destination = path.join(videoDir, video.getVideoFilename())
172
173 fs.rename(source, destination, function (err) {
174 if (err) return callback(err)
175
176 // This is important in case if there is another attempt
177 videoFile.filename = video.getVideoFilename()
178 return callback(null, t, author, tagInstances, video)
179 })
180 },
181
182 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
183 const options = { transaction: t }
184
185 // Add tags association
186 video.save(options).asCallback(function (err, videoCreated) {
187 if (err) return callback(err)
188
189 // Do not forget to add Author informations to the created video
190 videoCreated.Author = author
191
192 return callback(err, t, tagInstances, videoCreated)
193 })
194 },
195
196 function associateTagsToVideo (t, tagInstances, video, callback) {
197 const options = { transaction: t }
198
199 video.setTags(tagInstances, options).asCallback(function (err) {
200 video.Tags = tagInstances
201
202 return callback(err, t, video)
203 })
204 },
205
206 function sendToFriends (t, video, callback) {
207 video.toAddRemoteJSON(function (err, remoteVideo) {
208 if (err) return callback(err)
209
210 // Now we'll add the video's meta data to our friends
211 friends.addVideoToFriends(remoteVideo, t, function (err) {
212 return callback(err, t)
213 })
214 })
215 },
216
217 databaseUtils.commitTransaction
218
219 ], function andFinally (err, t) {
220 if (err) {
221 // This is just a debug because we will retry the insert
222 logger.debug('Cannot insert the video.', { error: err })
223 return databaseUtils.rollbackTransaction(err, t, finalCallback)
224 }
225
226 logger.info('Video with name %s created.', videoInfos.name)
227 return finalCallback(null)
228 })
229 }
230
231 function updateVideoRetryWrapper (req, res, next) {
232 const options = {
233 arguments: [ req, res ],
234 errorMessage: 'Cannot update the video with many retries.'
235 }
236
237 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
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 })
243 }
244
245 function updateVideo (req, res, finalCallback) {
246 const videoInstance = res.locals.video
247 const videoFieldsSave = videoInstance.toJSON()
248 const videoInfosToUpdate = req.body
249
250 waterfall([
251
252 databaseUtils.startSerializableTransaction,
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) {
265 const options = {
266 transaction: t
267 }
268
269 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
270 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
271
272 videoInstance.save(options).asCallback(function (err) {
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
295 friends.updateVideoToFriends(json, t, function (err) {
296 return callback(err, t)
297 })
298 },
299
300 databaseUtils.commitTransaction
301
302 ], function andFinally (err, t) {
303 if (err) {
304 logger.debug('Cannot update the video.', { error: err })
305
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
314 return databaseUtils.rollbackTransaction(err, t, finalCallback)
315 }
316
317 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
318 return finalCallback(null)
319 })
320 }
321
322 function getVideo (req, res, next) {
323 const videoInstance = res.locals.video
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 })
337 } else {
338 // Just send the event to our friends
339 friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
340 }
341
342 // Do not wait the view system
343 res.json(videoInstance.toFormatedJSON())
344 }
345
346 function listVideos (req, res, next) {
347 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
348 if (err) return next(err)
349
350 res.json(utils.getFormatedObjects(videosList, videosTotal))
351 })
352 }
353
354 function removeVideo (req, res, next) {
355 const videoInstance = res.locals.video
356
357 videoInstance.destroy().asCallback(function (err) {
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()
364 })
365 }
366
367 function searchVideos (req, res, next) {
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)
372
373 res.json(utils.getFormatedObjects(videosList, videosTotal))
374 }
375 )
376 }
377
378 function 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)
381
382 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
383 })
384 }
385
386 function reportVideoAbuseRetryWrapper (req, res, next) {
387 const options = {
388 arguments: [ req, res ],
389 errorMessage: 'Cannot report abuse to the video with many retries.'
390 }
391
392 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
393 if (err) return next(err)
394
395 return res.type('json').status(204).end()
396 })
397 }
398
399 function reportVideoAbuse (req, res, finalCallback) {
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
408 }
409
410 waterfall([
411
412 databaseUtils.startSerializableTransaction,
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 }
428
429 friends.reportAbuseVideoToFriend(reportData, videoInstance)
430 }
431
432 return callback(null, t)
433 },
434
435 databaseUtils.commitTransaction
436
437 ], function andFinally (err, t) {
438 if (err) {
439 logger.debug('Cannot update the video.', { error: err })
440 return databaseUtils.rollbackTransaction(err, t, finalCallback)
441 }
442
443 logger.info('Abuse report for video %s created.', videoInstance.name)
444 return finalCallback(null)
445 })
446 }