]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos/index.js
Server: split videos controller
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos / index.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
d33242b0
C
9const constants = require('../../../initializers/constants')
10const db = require('../../../initializers/database')
11const logger = require('../../../helpers/logger')
12const friends = require('../../../lib/friends')
13const middlewares = require('../../../middlewares')
69b0a27c 14const oAuth = middlewares.oauth
fbf1134e 15const pagination = middlewares.pagination
fc51fde0
C
16const validators = middlewares.validators
17const validatorsPagination = validators.pagination
18const validatorsSort = validators.sort
19const validatorsVideos = validators.videos
46246b5f 20const search = middlewares.search
a877d5ac 21const sort = middlewares.sort
d33242b0
C
22const databaseUtils = require('../../../helpers/database-utils')
23const utils = require('../../../helpers/utils')
24
25const abuseController = require('./abuse')
26const blacklistController = require('./blacklist')
27const rateController = require('./rate')
f0f5567b
C
28
29const router = express.Router()
9f10b292
C
30
31// multer configuration
f0f5567b 32const storage = multer.diskStorage({
9f10b292 33 destination: function (req, file, cb) {
b3d92510 34 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
9f10b292
C
35 },
36
37 filename: function (req, file, cb) {
f0f5567b 38 let extension = ''
9f10b292
C
39 if (file.mimetype === 'video/webm') extension = 'webm'
40 else if (file.mimetype === 'video/mp4') extension = 'mp4'
41 else if (file.mimetype === 'video/ogg') extension = 'ogv'
bc503c2a
C
42 utils.generateRandomString(16, function (err, randomString) {
43 const fieldname = err ? undefined : randomString
9f10b292
C
44 cb(null, fieldname + '.' + extension)
45 })
46 }
47})
48
8c9c1942 49const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
8c308c2b 50
d33242b0
C
51router.use('/', abuseController)
52router.use('/', blacklistController)
53router.use('/', rateController)
54
6e07c3de 55router.get('/categories', listVideoCategories)
6f0c39e2 56router.get('/licences', listVideoLicences)
3092476e 57router.get('/languages', listVideoLanguages)
6e07c3de 58
fbf1134e 59router.get('/',
fc51fde0
C
60 validatorsPagination.pagination,
61 validatorsSort.videosSort,
a877d5ac 62 sort.setVideosSort,
fbf1134e
C
63 pagination.setPagination,
64 listVideos
65)
7b1f49de
C
66router.put('/:id',
67 oAuth.authenticate,
68 reqFiles,
69 validatorsVideos.videosUpdate,
ed04d94f 70 updateVideoRetryWrapper
7b1f49de 71)
fbf1134e 72router.post('/',
69b0a27c 73 oAuth.authenticate,
fbf1134e 74 reqFiles,
fc51fde0 75 validatorsVideos.videosAdd,
ed04d94f 76 addVideoRetryWrapper
fbf1134e
C
77)
78router.get('/:id',
fc51fde0 79 validatorsVideos.videosGet,
68ce3ae0 80 getVideo
fbf1134e 81)
198b205c 82
fbf1134e 83router.delete('/:id',
69b0a27c 84 oAuth.authenticate,
fc51fde0 85 validatorsVideos.videosRemove,
fbf1134e
C
86 removeVideo
87)
198b205c 88
46246b5f 89router.get('/search/:value',
fc51fde0
C
90 validatorsVideos.videosSearch,
91 validatorsPagination.pagination,
92 validatorsSort.videosSort,
a877d5ac 93 sort.setVideosSort,
fbf1134e 94 pagination.setPagination,
46246b5f 95 search.setVideosSearch,
fbf1134e
C
96 searchVideos
97)
8c308c2b 98
9f10b292 99// ---------------------------------------------------------------------------
c45f7f84 100
9f10b292 101module.exports = router
c45f7f84 102
9f10b292 103// ---------------------------------------------------------------------------
c45f7f84 104
6e07c3de
C
105function listVideoCategories (req, res, next) {
106 res.json(constants.VIDEO_CATEGORIES)
107}
108
6f0c39e2
C
109function listVideoLicences (req, res, next) {
110 res.json(constants.VIDEO_LICENCES)
111}
112
3092476e
C
113function listVideoLanguages (req, res, next) {
114 res.json(constants.VIDEO_LANGUAGES)
115}
116
ed04d94f
C
117// Wrapper to video add that retry the function if there is a database error
118// We need this because we run the transaction in SERIALIZABLE isolation that can fail
119function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
120 const options = {
121 arguments: [ req, res, req.files.videofile[0] ],
122 errorMessage: 'Cannot insert the video with many retries.'
123 }
ed04d94f 124
4df023f2 125 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
126 if (err) return next(err)
127
128 // TODO : include Location of the new video -> 201
129 return res.type('json').status(204).end()
130 })
ed04d94f
C
131}
132
4145c1c6 133function addVideo (req, res, videoFile, finalCallback) {
bc503c2a 134 const videoInfos = req.body
9f10b292 135
1a42c9e2 136 waterfall([
807df9e6 137
4df023f2 138 databaseUtils.startSerializableTransaction,
7920c273 139
4145c1c6 140 function findOrCreateAuthor (t, callback) {
4712081f 141 const user = res.locals.oauth.token.User
feb4bdfd 142
4ff0d862
C
143 const name = user.username
144 // null because it is OUR pod
145 const podId = null
146 const userId = user.id
4712081f 147
4ff0d862 148 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
4145c1c6 149 return callback(err, t, authorInstance)
7920c273
C
150 })
151 },
152
4145c1c6 153 function findOrCreateTags (t, author, callback) {
7920c273 154 const tags = videoInfos.tags
4ff0d862
C
155
156 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
4145c1c6 157 return callback(err, t, author, tagInstances)
feb4bdfd
C
158 })
159 },
160
4145c1c6 161 function createVideoObject (t, author, tagInstances, callback) {
807df9e6
C
162 const videoData = {
163 name: videoInfos.name,
558d7c23
C
164 remoteId: null,
165 extname: path.extname(videoFile.filename),
6e07c3de 166 category: videoInfos.category,
6f0c39e2 167 licence: videoInfos.licence,
3092476e 168 language: videoInfos.language,
31b59b47 169 nsfw: videoInfos.nsfw,
807df9e6 170 description: videoInfos.description,
67100f1f 171 duration: videoFile.duration,
d38b8281 172 authorId: author.id
807df9e6
C
173 }
174
feb4bdfd 175 const video = db.Video.build(videoData)
558d7c23 176
4145c1c6 177 return callback(null, t, author, tagInstances, video)
558d7c23
C
178 },
179
feb4bdfd 180 // Set the videoname the same as the id
4145c1c6 181 function renameVideoFile (t, author, tagInstances, video, callback) {
558d7c23
C
182 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
183 const source = path.join(videoDir, videoFile.filename)
f285faa0 184 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
185
186 fs.rename(source, destination, function (err) {
4145c1c6 187 if (err) return callback(err)
ed04d94f
C
188
189 // This is important in case if there is another attempt
190 videoFile.filename = video.getVideoFilename()
4145c1c6 191 return callback(null, t, author, tagInstances, video)
558d7c23
C
192 })
193 },
194
4145c1c6 195 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
7920c273
C
196 const options = { transaction: t }
197
198 // Add tags association
199 video.save(options).asCallback(function (err, videoCreated) {
4145c1c6 200 if (err) return callback(err)
7920c273 201
feb4bdfd
C
202 // Do not forget to add Author informations to the created video
203 videoCreated.Author = author
204
4145c1c6 205 return callback(err, t, tagInstances, videoCreated)
3a8a8b51 206 })
807df9e6
C
207 },
208
4145c1c6 209 function associateTagsToVideo (t, tagInstances, video, callback) {
7920c273
C
210 const options = { transaction: t }
211
212 video.setTags(tagInstances, options).asCallback(function (err) {
213 video.Tags = tagInstances
214
4145c1c6 215 return callback(err, t, video)
7920c273
C
216 })
217 },
218
4145c1c6 219 function sendToFriends (t, video, callback) {
62326afb
C
220 // Let transcoding job send the video to friends because the videofile extension might change
221 if (constants.CONFIG.TRANSCODING.ENABLED === true) return callback(null, t)
222
7b1f49de 223 video.toAddRemoteJSON(function (err, remoteVideo) {
4145c1c6 224 if (err) return callback(err)
807df9e6 225
528a9efa 226 // Now we'll add the video's meta data to our friends
ed04d94f 227 friends.addVideoToFriends(remoteVideo, t, function (err) {
4145c1c6 228 return callback(err, t)
ed04d94f 229 })
528a9efa 230 })
4145c1c6
C
231 },
232
233 databaseUtils.commitTransaction
807df9e6 234
7b1f49de
C
235 ], function andFinally (err, t) {
236 if (err) {
ed04d94f
C
237 // This is just a debug because we will retry the insert
238 logger.debug('Cannot insert the video.', { error: err })
4145c1c6 239 return databaseUtils.rollbackTransaction(err, t, finalCallback)
7b1f49de
C
240 }
241
4145c1c6
C
242 logger.info('Video with name %s created.', videoInfos.name)
243 return finalCallback(null)
7b1f49de
C
244 })
245}
246
ed04d94f 247function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
248 const options = {
249 arguments: [ req, res ],
250 errorMessage: 'Cannot update the video with many retries.'
251 }
ed04d94f 252
4df023f2 253 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
254 if (err) return next(err)
255
256 // TODO : include Location of the new video -> 201
257 return res.type('json').status(204).end()
258 })
ed04d94f
C
259}
260
261function updateVideo (req, res, finalCallback) {
818f7987 262 const videoInstance = res.locals.video
7f4e7c36 263 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
264 const videoInfosToUpdate = req.body
265
266 waterfall([
267
4df023f2 268 databaseUtils.startSerializableTransaction,
7b1f49de
C
269
270 function findOrCreateTags (t, callback) {
271 if (videoInfosToUpdate.tags) {
272 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
273 return callback(err, t, tagInstances)
274 })
275 } else {
276 return callback(null, t, null)
277 }
278 },
279
280 function updateVideoIntoDB (t, tagInstances, callback) {
7f4e7c36
C
281 const options = {
282 transaction: t
283 }
7b1f49de 284
c24ac1c1
C
285 if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name)
286 if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category)
287 if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence)
288 if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language)
289 if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
290 if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description)
7b1f49de 291
7b1f49de 292 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
293 return callback(err, t, tagInstances)
294 })
295 },
296
297 function associateTagsToVideo (t, tagInstances, callback) {
298 if (tagInstances) {
299 const options = { transaction: t }
300
301 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
302 videoInstance.Tags = tagInstances
303
304 return callback(err, t)
305 })
306 } else {
307 return callback(null, t)
308 }
309 },
310
311 function sendToFriends (t, callback) {
312 const json = videoInstance.toUpdateRemoteJSON()
313
314 // Now we'll update the video's meta data to our friends
ed04d94f
C
315 friends.updateVideoToFriends(json, t, function (err) {
316 return callback(err, t)
317 })
4145c1c6
C
318 },
319
320 databaseUtils.commitTransaction
7b1f49de 321
7920c273 322 ], function andFinally (err, t) {
807df9e6 323 if (err) {
ed04d94f 324 logger.debug('Cannot update the video.', { error: err })
7920c273 325
7f4e7c36
C
326 // Force fields we want to update
327 // If the transaction is retried, sequelize will think the object has not changed
328 // So it will skip the SQL request, even if the last one was ROLLBACKed!
329 Object.keys(videoFieldsSave).forEach(function (key) {
330 const value = videoFieldsSave[key]
331 videoInstance.set(key, value)
332 })
333
4145c1c6 334 return databaseUtils.rollbackTransaction(err, t, finalCallback)
807df9e6
C
335 }
336
4145c1c6
C
337 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
338 return finalCallback(null)
9f10b292
C
339 })
340}
8c308c2b 341
68ce3ae0 342function getVideo (req, res, next) {
818f7987 343 const videoInstance = res.locals.video
9e167724
C
344
345 if (videoInstance.isOwned()) {
346 // The increment is done directly in the database, not using the instance value
347 videoInstance.increment('views').asCallback(function (err) {
348 if (err) {
349 logger.error('Cannot add view to video %d.', videoInstance.id)
350 return
351 }
352
353 // FIXME: make a real view system
354 // For example, only add a view when a user watch a video during 30s etc
d38b8281
C
355 const qaduParams = {
356 videoId: videoInstance.id,
357 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
358 }
359 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
9e167724 360 })
e4c87ec2
C
361 } else {
362 // Just send the event to our friends
d38b8281
C
363 const eventParams = {
364 videoId: videoInstance.id,
365 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
366 }
367 friends.addEventToRemoteVideo(eventParams)
9e167724
C
368 }
369
370 // Do not wait the view system
818f7987 371 res.json(videoInstance.toFormatedJSON())
9f10b292 372}
8c308c2b 373
9f10b292 374function listVideos (req, res, next) {
feb4bdfd 375 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 376 if (err) return next(err)
c45f7f84 377
55fa55a9 378 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
379 })
380}
c45f7f84 381
9f10b292 382function removeVideo (req, res, next) {
818f7987 383 const videoInstance = res.locals.video
8c308c2b 384
818f7987 385 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
386 if (err) {
387 logger.error('Errors when removed the video.', { error: err })
388 return next(err)
389 }
390
391 return res.type('json').status(204).end()
9f10b292
C
392 })
393}
8c308c2b 394
9f10b292 395function searchVideos (req, res, next) {
7920c273
C
396 db.Video.searchAndPopulateAuthorAndPodAndTags(
397 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
398 function (err, videosList, videosTotal) {
399 if (err) return next(err)
8c308c2b 400
55fa55a9 401 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
402 }
403 )
9f10b292 404}