Fix lint
[oweals/peertube.git] / server / controllers / api / videos / index.ts
1 import * as express from 'express'
2 import * as multer from 'multer'
3 import { extname, join } from 'path'
4 import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared'
5 import {
6   generateRandomString,
7   getFormattedObjects,
8   getVideoFileHeight,
9   logger,
10   renamePromise,
11   resetSequelizeInstance,
12   retryTransactionWrapper
13 } from '../../../helpers'
14 import { getServerAccount } from '../../../helpers/utils'
15 import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers'
16 import { database as db } from '../../../initializers/database'
17 import { sendAddVideo } from '../../../lib/activitypub/send/send-add'
18 import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update'
19 import { shareVideoByServer } from '../../../lib/activitypub/share'
20 import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
21 import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
22 import { sendCreateViewToVideoFollowers } from '../../../lib/index'
23 import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler'
24 import {
25   asyncMiddleware,
26   authenticate,
27   paginationValidator,
28   setPagination,
29   setVideosSearch,
30   setVideosSort,
31   videosAddValidator,
32   videosGetValidator,
33   videosRemoveValidator,
34   videosSearchValidator,
35   videosSortValidator,
36   videosUpdateValidator
37 } from '../../../middlewares'
38 import { VideoInstance } from '../../../models'
39 import { abuseVideoRouter } from './abuse'
40 import { blacklistRouter } from './blacklist'
41 import { videoChannelRouter } from './channel'
42 import { rateVideoRouter } from './rate'
43 import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
44
45 const videosRouter = express.Router()
46
47 // multer configuration
48 const storage = multer.diskStorage({
49   destination: (req, file, cb) => {
50     cb(null, CONFIG.STORAGE.VIDEOS_DIR)
51   },
52
53   filename: async (req, file, cb) => {
54     const extension = VIDEO_MIMETYPE_EXT[file.mimetype]
55     let randomString = ''
56
57     try {
58       randomString = await generateRandomString(16)
59     } catch (err) {
60       logger.error('Cannot generate random string for file name.', err)
61       randomString = 'fake-random-string'
62     }
63
64     cb(null, randomString + extension)
65   }
66 })
67
68 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
69
70 videosRouter.use('/', abuseVideoRouter)
71 videosRouter.use('/', blacklistRouter)
72 videosRouter.use('/', rateVideoRouter)
73 videosRouter.use('/', videoChannelRouter)
74
75 videosRouter.get('/categories', listVideoCategories)
76 videosRouter.get('/licences', listVideoLicences)
77 videosRouter.get('/languages', listVideoLanguages)
78 videosRouter.get('/privacies', listVideoPrivacies)
79
80 videosRouter.get('/',
81   paginationValidator,
82   videosSortValidator,
83   setVideosSort,
84   setPagination,
85   asyncMiddleware(listVideos)
86 )
87 videosRouter.put('/:id',
88   authenticate,
89   videosUpdateValidator,
90   asyncMiddleware(updateVideoRetryWrapper)
91 )
92 videosRouter.post('/upload',
93   authenticate,
94   reqFiles,
95   videosAddValidator,
96   asyncMiddleware(addVideoRetryWrapper)
97 )
98
99 videosRouter.get('/:id/description',
100   videosGetValidator,
101   asyncMiddleware(getVideoDescription)
102 )
103 videosRouter.get('/:id',
104   videosGetValidator,
105   getVideo
106 )
107
108 videosRouter.delete('/:id',
109   authenticate,
110   videosRemoveValidator,
111   asyncMiddleware(removeVideoRetryWrapper)
112 )
113
114 videosRouter.get('/search/:value',
115   videosSearchValidator,
116   paginationValidator,
117   videosSortValidator,
118   setVideosSort,
119   setPagination,
120   setVideosSearch,
121   asyncMiddleware(searchVideos)
122 )
123
124 // ---------------------------------------------------------------------------
125
126 export {
127   videosRouter
128 }
129
130 // ---------------------------------------------------------------------------
131
132 function listVideoCategories (req: express.Request, res: express.Response) {
133   res.json(VIDEO_CATEGORIES)
134 }
135
136 function listVideoLicences (req: express.Request, res: express.Response) {
137   res.json(VIDEO_LICENCES)
138 }
139
140 function listVideoLanguages (req: express.Request, res: express.Response) {
141   res.json(VIDEO_LANGUAGES)
142 }
143
144 function listVideoPrivacies (req: express.Request, res: express.Response) {
145   res.json(VIDEO_PRIVACIES)
146 }
147
148 // Wrapper to video add that retry the function if there is a database error
149 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
150 async function addVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
151   const options = {
152     arguments: [ req, res, req.files['videofile'][0] ],
153     errorMessage: 'Cannot insert the video with many retries.'
154   }
155
156   await retryTransactionWrapper(addVideo, options)
157
158   // TODO : include Location of the new video -> 201
159   res.type('json').status(204).end()
160 }
161
162 async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
163   const videoInfo: VideoCreate = req.body
164   let videoUUID = ''
165
166   await db.sequelize.transaction(async t => {
167     const sequelizeOptions = { transaction: t }
168
169     const videoData = {
170       name: videoInfo.name,
171       remote: false,
172       extname: extname(videoPhysicalFile.filename),
173       category: videoInfo.category,
174       licence: videoInfo.licence,
175       language: videoInfo.language,
176       nsfw: videoInfo.nsfw,
177       description: videoInfo.description,
178       privacy: videoInfo.privacy,
179       duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
180       channelId: res.locals.videoChannel.id
181     }
182     const video = db.Video.build(videoData)
183     video.url = getVideoActivityPubUrl(video)
184
185     const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
186     const videoFileHeight = await getVideoFileHeight(videoFilePath)
187
188     const videoFileData = {
189       extname: extname(videoPhysicalFile.filename),
190       resolution: videoFileHeight,
191       size: videoPhysicalFile.size
192     }
193     const videoFile = db.VideoFile.build(videoFileData)
194     const videoDir = CONFIG.STORAGE.VIDEOS_DIR
195     const source = join(videoDir, videoPhysicalFile.filename)
196     const destination = join(videoDir, video.getVideoFilename(videoFile))
197
198     await renamePromise(source, destination)
199     // This is important in case if there is another attempt in the retry process
200     videoPhysicalFile.filename = video.getVideoFilename(videoFile)
201
202     const tasks = []
203
204     tasks.push(
205       video.createTorrentAndSetInfoHash(videoFile),
206       video.createThumbnail(videoFile),
207       video.createPreview(videoFile)
208     )
209
210     if (CONFIG.TRANSCODING.ENABLED === true) {
211       // Put uuid because we don't have id auto incremented for now
212       const dataInput = {
213         videoUUID: video.uuid
214       }
215
216       tasks.push(
217         transcodingJobScheduler.createJob(t, 'videoFileOptimizer', dataInput)
218       )
219     }
220     await Promise.all(tasks)
221
222     const videoCreated = await video.save(sequelizeOptions)
223     // Do not forget to add video channel information to the created video
224     videoCreated.VideoChannel = res.locals.videoChannel
225     videoUUID = videoCreated.uuid
226
227     videoFile.videoId = video.id
228
229     await videoFile.save(sequelizeOptions)
230     video.VideoFiles = [videoFile]
231
232     if (videoInfo.tags) {
233       const tagInstances = await db.Tag.findOrCreateTags(videoInfo.tags, t)
234
235       await video.setTags(tagInstances, sequelizeOptions)
236       video.Tags = tagInstances
237     }
238
239     // Let transcoding job send the video to friends because the video file extension might change
240     if (CONFIG.TRANSCODING.ENABLED === true) return undefined
241     // Don't send video to remote servers, it is private
242     if (video.privacy === VideoPrivacy.PRIVATE) return undefined
243
244     await sendAddVideo(video, t)
245     await shareVideoByServer(video, t)
246   })
247
248   logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoUUID)
249 }
250
251 async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
252   const options = {
253     arguments: [ req, res ],
254     errorMessage: 'Cannot update the video with many retries.'
255   }
256
257   await retryTransactionWrapper(updateVideo, options)
258
259   return res.type('json').status(204).end()
260 }
261
262 async function updateVideo (req: express.Request, res: express.Response) {
263   const videoInstance: VideoInstance = res.locals.video
264   const videoFieldsSave = videoInstance.toJSON()
265   const videoInfoToUpdate: VideoUpdate = req.body
266   const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
267
268   try {
269     await db.sequelize.transaction(async t => {
270       const sequelizeOptions = {
271         transaction: t
272       }
273
274       if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name)
275       if (videoInfoToUpdate.category !== undefined) videoInstance.set('category', videoInfoToUpdate.category)
276       if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
277       if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
278       if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
279       if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy)
280       if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
281
282       const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
283
284       if (videoInfoToUpdate.tags) {
285         const tagInstances = await db.Tag.findOrCreateTags(videoInfoToUpdate.tags, t)
286
287         await videoInstance.setTags(tagInstances, sequelizeOptions)
288         videoInstance.Tags = tagInstances
289       }
290
291       // Now we'll update the video's meta data to our friends
292       if (wasPrivateVideo === false) {
293         await sendUpdateVideo(videoInstanceUpdated, t)
294       }
295
296       // Video is not private anymore, send a create action to remote servers
297       if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) {
298         await sendAddVideo(videoInstance, t)
299         await shareVideoByServer(videoInstance, t)
300       }
301     })
302
303     logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
304   } catch (err) {
305     // Force fields we want to update
306     // If the transaction is retried, sequelize will think the object has not changed
307     // So it will skip the SQL request, even if the last one was ROLLBACKed!
308     resetSequelizeInstance(videoInstance, videoFieldsSave)
309
310     throw err
311   }
312 }
313
314 async function getVideo (req: express.Request, res: express.Response) {
315   const videoInstance = res.locals.video
316
317   const baseIncrementPromise = videoInstance.increment('views')
318     .then(() => getServerAccount())
319
320   if (videoInstance.isOwned()) {
321     // The increment is done directly in the database, not using the instance value
322     baseIncrementPromise
323       .then(serverAccount => sendCreateViewToVideoFollowers(serverAccount, videoInstance, undefined))
324       .catch(err => logger.error('Cannot add view to video/send view to followers for %s.', videoInstance.uuid, err))
325   } else {
326     baseIncrementPromise
327       .then(serverAccount => sendCreateViewToOrigin(serverAccount, videoInstance, undefined))
328       .catch(err => logger.error('Cannot send view to origin server for %s.', videoInstance.uuid, err))
329   }
330
331   // Do not wait the view system
332   return res.json(videoInstance.toFormattedDetailsJSON())
333 }
334
335 async function getVideoDescription (req: express.Request, res: express.Response) {
336   const videoInstance = res.locals.video
337   let description = ''
338
339   if (videoInstance.isOwned()) {
340     description = videoInstance.description
341   } else {
342     description = await fetchRemoteVideoDescription(videoInstance)
343   }
344
345   return res.json({ description })
346 }
347
348 async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
349   const resultList = await db.Video.listForApi(req.query.start, req.query.count, req.query.sort)
350
351   return res.json(getFormattedObjects(resultList.data, resultList.total))
352 }
353
354 async function removeVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
355   const options = {
356     arguments: [ req, res ],
357     errorMessage: 'Cannot remove the video with many retries.'
358   }
359
360   await retryTransactionWrapper(removeVideo, options)
361
362   return res.type('json').status(204).end()
363 }
364
365 async function removeVideo (req: express.Request, res: express.Response) {
366   const videoInstance: VideoInstance = res.locals.video
367
368   await db.sequelize.transaction(async t => {
369     await videoInstance.destroy({ transaction: t })
370   })
371
372   logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
373 }
374
375 async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
376   const resultList = await db.Video.searchAndPopulateAccountAndServerAndTags(
377     req.params.value,
378     req.query.field,
379     req.query.start,
380     req.query.count,
381     req.query.sort
382   )
383
384   return res.json(getFormattedObjects(resultList.data, resultList.total))
385 }