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