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