]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos/index.ts
Type models
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos / index.ts
1 import express = require('express')
2 import fs = require('fs')
3 import multer = require('multer')
4 import path = require('path')
5 import { waterfall } from 'async'
6
7 import { database as db } from '../../../initializers/database'
8 import {
9 CONFIG,
10 REQUEST_VIDEO_QADU_TYPES,
11 REQUEST_VIDEO_EVENT_TYPES,
12 VIDEO_CATEGORIES,
13 VIDEO_LICENCES,
14 VIDEO_LANGUAGES
15 } from '../../../initializers'
16 import {
17 addEventToRemoteVideo,
18 quickAndDirtyUpdateVideoToFriends,
19 addVideoToFriends,
20 updateVideoToFriends
21 } from '../../../lib'
22 import {
23 authenticate,
24 paginationValidator,
25 videosSortValidator,
26 setVideosSort,
27 setPagination,
28 setVideosSearch,
29 videosUpdateValidator,
30 videosSearchValidator,
31 videosAddValidator,
32 videosGetValidator,
33 videosRemoveValidator
34 } from '../../../middlewares'
35 import {
36 logger,
37 commitTransaction,
38 retryTransactionWrapper,
39 rollbackTransaction,
40 startSerializableTransaction,
41 generateRandomString,
42 getFormatedObjects
43 } from '../../../helpers'
44
45 import { abuseVideoRouter } from './abuse'
46 import { blacklistRouter } from './blacklist'
47 import { rateVideoRouter } from './rate'
48
49 const videosRouter = express.Router()
50
51 // multer configuration
52 const storage = multer.diskStorage({
53 destination: function (req, file, cb) {
54 cb(null, CONFIG.STORAGE.VIDEOS_DIR)
55 },
56
57 filename: function (req, file, cb) {
58 let extension = ''
59 if (file.mimetype === 'video/webm') extension = 'webm'
60 else if (file.mimetype === 'video/mp4') extension = 'mp4'
61 else if (file.mimetype === 'video/ogg') extension = 'ogv'
62 generateRandomString(16, function (err, randomString) {
63 const fieldname = err ? undefined : randomString
64 cb(null, fieldname + '.' + extension)
65 })
66 }
67 })
68
69 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
70
71 videosRouter.use('/', abuseVideoRouter)
72 videosRouter.use('/', blacklistRouter)
73 videosRouter.use('/', rateVideoRouter)
74
75 videosRouter.get('/categories', listVideoCategories)
76 videosRouter.get('/licences', listVideoLicences)
77 videosRouter.get('/languages', listVideoLanguages)
78
79 videosRouter.get('/',
80 paginationValidator,
81 videosSortValidator,
82 setVideosSort,
83 setPagination,
84 listVideos
85 )
86 videosRouter.put('/:id',
87 authenticate,
88 reqFiles,
89 videosUpdateValidator,
90 updateVideoRetryWrapper
91 )
92 videosRouter.post('/',
93 authenticate,
94 reqFiles,
95 videosAddValidator,
96 addVideoRetryWrapper
97 )
98 videosRouter.get('/:id',
99 videosGetValidator,
100 getVideo
101 )
102
103 videosRouter.delete('/:id',
104 authenticate,
105 videosRemoveValidator,
106 removeVideo
107 )
108
109 videosRouter.get('/search/:value',
110 videosSearchValidator,
111 paginationValidator,
112 videosSortValidator,
113 setVideosSort,
114 setPagination,
115 setVideosSearch,
116 searchVideos
117 )
118
119 // ---------------------------------------------------------------------------
120
121 export {
122 videosRouter
123 }
124
125 // ---------------------------------------------------------------------------
126
127 function listVideoCategories (req, res, next) {
128 res.json(VIDEO_CATEGORIES)
129 }
130
131 function listVideoLicences (req, res, next) {
132 res.json(VIDEO_LICENCES)
133 }
134
135 function listVideoLanguages (req, res, next) {
136 res.json(VIDEO_LANGUAGES)
137 }
138
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
141 function addVideoRetryWrapper (req, res, next) {
142 const options = {
143 arguments: [ req, res, req.files.videofile[0] ],
144 errorMessage: 'Cannot insert the video with many retries.'
145 }
146
147 retryTransactionWrapper(addVideo, options, function (err) {
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 })
153 }
154
155 function addVideo (req, res, videoFile, finalCallback) {
156 const videoInfos = req.body
157
158 waterfall([
159
160 startSerializableTransaction,
161
162 function findOrCreateAuthor (t, callback) {
163 const user = res.locals.oauth.token.User
164
165 const name = user.username
166 // null because it is OUR pod
167 const podId = null
168 const userId = user.id
169
170 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
171 return callback(err, t, authorInstance)
172 })
173 },
174
175 function findOrCreateTags (t, author, callback) {
176 const tags = videoInfos.tags
177
178 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
179 return callback(err, t, author, tagInstances)
180 })
181 },
182
183 function createVideoObject (t, author, tagInstances, callback) {
184 const videoData = {
185 name: videoInfos.name,
186 remoteId: null,
187 extname: path.extname(videoFile.filename),
188 category: videoInfos.category,
189 licence: videoInfos.licence,
190 language: videoInfos.language,
191 nsfw: videoInfos.nsfw,
192 description: videoInfos.description,
193 duration: videoFile.duration,
194 authorId: author.id
195 }
196
197 const video = db.Video.build(videoData)
198
199 return callback(null, t, author, tagInstances, video)
200 },
201
202 // Set the videoname the same as the id
203 function renameVideoFile (t, author, tagInstances, video, callback) {
204 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
205 const source = path.join(videoDir, videoFile.filename)
206 const destination = path.join(videoDir, video.getVideoFilename())
207
208 fs.rename(source, destination, function (err) {
209 if (err) return callback(err)
210
211 // This is important in case if there is another attempt
212 videoFile.filename = video.getVideoFilename()
213 return callback(null, t, author, tagInstances, video)
214 })
215 },
216
217 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
218 const options = { transaction: t }
219
220 // Add tags association
221 video.save(options).asCallback(function (err, videoCreated) {
222 if (err) return callback(err)
223
224 // Do not forget to add Author informations to the created video
225 videoCreated.Author = author
226
227 return callback(err, t, tagInstances, videoCreated)
228 })
229 },
230
231 function associateTagsToVideo (t, tagInstances, video, callback) {
232 const options = { transaction: t }
233
234 video.setTags(tagInstances, options).asCallback(function (err) {
235 video.Tags = tagInstances
236
237 return callback(err, t, video)
238 })
239 },
240
241 function sendToFriends (t, video, callback) {
242 // Let transcoding job send the video to friends because the videofile extension might change
243 if (CONFIG.TRANSCODING.ENABLED === true) return callback(null, t)
244
245 video.toAddRemoteJSON(function (err, remoteVideo) {
246 if (err) return callback(err)
247
248 // Now we'll add the video's meta data to our friends
249 addVideoToFriends(remoteVideo, t, function (err) {
250 return callback(err, t)
251 })
252 })
253 },
254
255 commitTransaction
256
257 ], function andFinally (err, t) {
258 if (err) {
259 // This is just a debug because we will retry the insert
260 logger.debug('Cannot insert the video.', { error: err })
261 return rollbackTransaction(err, t, finalCallback)
262 }
263
264 logger.info('Video with name %s created.', videoInfos.name)
265 return finalCallback(null)
266 })
267 }
268
269 function updateVideoRetryWrapper (req, res, next) {
270 const options = {
271 arguments: [ req, res ],
272 errorMessage: 'Cannot update the video with many retries.'
273 }
274
275 retryTransactionWrapper(updateVideo, options, function (err) {
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 })
281 }
282
283 function updateVideo (req, res, finalCallback) {
284 const videoInstance = res.locals.video
285 const videoFieldsSave = videoInstance.toJSON()
286 const videoInfosToUpdate = req.body
287
288 waterfall([
289
290 startSerializableTransaction,
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) {
303 const options = {
304 transaction: t
305 }
306
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)
313
314 videoInstance.save(options).asCallback(function (err) {
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
337 updateVideoToFriends(json, t, function (err) {
338 return callback(err, t)
339 })
340 },
341
342 commitTransaction
343
344 ], function andFinally (err, t) {
345 if (err) {
346 logger.debug('Cannot update the video.', { error: err })
347
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
356 return rollbackTransaction(err, t, finalCallback)
357 }
358
359 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
360 return finalCallback(null)
361 })
362 }
363
364 function getVideo (req, res, next) {
365 const videoInstance = res.locals.video
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
377 const qaduParams = {
378 videoId: videoInstance.id,
379 type: REQUEST_VIDEO_QADU_TYPES.VIEWS
380 }
381 quickAndDirtyUpdateVideoToFriends(qaduParams)
382 })
383 } else {
384 // Just send the event to our friends
385 const eventParams = {
386 videoId: videoInstance.id,
387 type: REQUEST_VIDEO_EVENT_TYPES.VIEWS
388 }
389 addEventToRemoteVideo(eventParams)
390 }
391
392 // Do not wait the view system
393 res.json(videoInstance.toFormatedJSON())
394 }
395
396 function listVideos (req, res, next) {
397 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
398 if (err) return next(err)
399
400 res.json(getFormatedObjects(videosList, videosTotal))
401 })
402 }
403
404 function removeVideo (req, res, next) {
405 const videoInstance = res.locals.video
406
407 videoInstance.destroy().asCallback(function (err) {
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()
414 })
415 }
416
417 function searchVideos (req, res, next) {
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)
422
423 res.json(getFormatedObjects(videosList, videosTotal))
424 }
425 )
426 }