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