]>
Commit | Line | Data |
---|---|---|
4d4e5cd4 C |
1 | import * as express from 'express' |
2 | import * as fs from 'fs' | |
3 | import * as multer from 'multer' | |
4 | import * as path from 'path' | |
65fcc311 | 5 | import { waterfall } from 'async' |
9f10b292 | 6 | |
e02643f3 | 7 | import { database as db } from '../../../initializers/database' |
65fcc311 C |
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() | |
9f10b292 C |
50 | |
51 | // multer configuration | |
f0f5567b | 52 | const storage = multer.diskStorage({ |
9f10b292 | 53 | destination: function (req, file, cb) { |
65fcc311 | 54 | cb(null, CONFIG.STORAGE.VIDEOS_DIR) |
9f10b292 C |
55 | }, |
56 | ||
57 | filename: function (req, file, cb) { | |
f0f5567b | 58 | let extension = '' |
9f10b292 C |
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' | |
65fcc311 | 62 | generateRandomString(16, function (err, randomString) { |
bc503c2a | 63 | const fieldname = err ? undefined : randomString |
9f10b292 C |
64 | cb(null, fieldname + '.' + extension) |
65 | }) | |
66 | } | |
67 | }) | |
68 | ||
8c9c1942 | 69 | const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) |
8c308c2b | 70 | |
65fcc311 C |
71 | videosRouter.use('/', abuseVideoRouter) |
72 | videosRouter.use('/', blacklistRouter) | |
73 | videosRouter.use('/', rateVideoRouter) | |
d33242b0 | 74 | |
65fcc311 C |
75 | videosRouter.get('/categories', listVideoCategories) |
76 | videosRouter.get('/licences', listVideoLicences) | |
77 | videosRouter.get('/languages', listVideoLanguages) | |
6e07c3de | 78 | |
65fcc311 C |
79 | videosRouter.get('/', |
80 | paginationValidator, | |
81 | videosSortValidator, | |
82 | setVideosSort, | |
83 | setPagination, | |
fbf1134e C |
84 | listVideos |
85 | ) | |
65fcc311 C |
86 | videosRouter.put('/:id', |
87 | authenticate, | |
7b1f49de | 88 | reqFiles, |
65fcc311 | 89 | videosUpdateValidator, |
ed04d94f | 90 | updateVideoRetryWrapper |
7b1f49de | 91 | ) |
65fcc311 C |
92 | videosRouter.post('/', |
93 | authenticate, | |
fbf1134e | 94 | reqFiles, |
65fcc311 | 95 | videosAddValidator, |
ed04d94f | 96 | addVideoRetryWrapper |
fbf1134e | 97 | ) |
65fcc311 C |
98 | videosRouter.get('/:id', |
99 | videosGetValidator, | |
68ce3ae0 | 100 | getVideo |
fbf1134e | 101 | ) |
198b205c | 102 | |
65fcc311 C |
103 | videosRouter.delete('/:id', |
104 | authenticate, | |
105 | videosRemoveValidator, | |
fbf1134e C |
106 | removeVideo |
107 | ) | |
198b205c | 108 | |
65fcc311 C |
109 | videosRouter.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 |
121 | export { |
122 | videosRouter | |
123 | } | |
c45f7f84 | 124 | |
9f10b292 | 125 | // --------------------------------------------------------------------------- |
c45f7f84 | 126 | |
6e07c3de | 127 | function listVideoCategories (req, res, next) { |
65fcc311 | 128 | res.json(VIDEO_CATEGORIES) |
6e07c3de C |
129 | } |
130 | ||
6f0c39e2 | 131 | function listVideoLicences (req, res, next) { |
65fcc311 | 132 | res.json(VIDEO_LICENCES) |
6f0c39e2 C |
133 | } |
134 | ||
3092476e | 135 | function listVideoLanguages (req, res, next) { |
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 | |
141 | function addVideoRetryWrapper (req, res, next) { | |
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 | ||
4145c1c6 | 155 | function addVideo (req, res, videoFile, finalCallback) { |
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, |
67100f1f | 193 | duration: videoFile.duration, |
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 | |
7b1f49de C |
257 | ], function andFinally (err, t) { |
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 | ||
ed04d94f | 269 | function updateVideoRetryWrapper (req, res, next) { |
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 | ||
283 | function updateVideo (req, res, finalCallback) { | |
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 | |
7920c273 | 344 | ], function andFinally (err, t) { |
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 | ||
4145c1c6 C |
359 | logger.info('Video with name %s updated.', videoInfosToUpdate.name) |
360 | return finalCallback(null) | |
9f10b292 C |
361 | }) |
362 | } | |
8c308c2b | 363 | |
68ce3ae0 | 364 | function getVideo (req, res, next) { |
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 | |
9f10b292 | 396 | function listVideos (req, res, next) { |
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 | |
9f10b292 | 404 | function removeVideo (req, res, next) { |
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 | |
9f10b292 | 417 | function searchVideos (req, res, next) { |
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 | } |