]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/controllers/api/video-channel.ts
Fix retrying update on sql serialization conflict
[github/Chocobozzz/PeerTube.git] / server / controllers / api / video-channel.ts
... / ...
CommitLineData
1import express from 'express'
2import { pickCommonVideoQuery } from '@server/helpers/query'
3import { getBiggestActorImage } from '@server/lib/actor-image'
4import { Hooks } from '@server/lib/plugins/hooks'
5import { ActorFollowModel } from '@server/models/actor/actor-follow'
6import { getServerActor } from '@server/models/application/application'
7import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
8import { MChannelBannerAccountDefault } from '@server/types/models'
9import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models'
10import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
11import { resetSequelizeInstance } from '../../helpers/database-utils'
12import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
13import { logger } from '../../helpers/logger'
14import { getFormattedObjects } from '../../helpers/utils'
15import { MIMETYPES } from '../../initializers/constants'
16import { sequelizeTypescript } from '../../initializers/database'
17import { sendUpdateActor } from '../../lib/activitypub/send'
18import { JobQueue } from '../../lib/job-queue'
19import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor'
20import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
21import {
22 asyncMiddleware,
23 asyncRetryTransactionMiddleware,
24 authenticate,
25 commonVideosFiltersValidator,
26 ensureCanManageChannelOrAccount,
27 optionalAuthenticate,
28 paginationValidator,
29 setDefaultPagination,
30 setDefaultSort,
31 setDefaultVideosSort,
32 videoChannelsAddValidator,
33 videoChannelsRemoveValidator,
34 videoChannelsSortValidator,
35 videoChannelsUpdateValidator,
36 videoPlaylistsSortValidator
37} from '../../middlewares'
38import {
39 ensureChannelOwnerCanUpload,
40 ensureIsLocalChannel,
41 videoChannelImportVideosValidator,
42 videoChannelsFollowersSortValidator,
43 videoChannelsListValidator,
44 videoChannelsNameWithHostValidator,
45 videosSortValidator
46} from '../../middlewares/validators'
47import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
48import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
49import { AccountModel } from '../../models/account/account'
50import { VideoModel } from '../../models/video/video'
51import { VideoChannelModel } from '../../models/video/video-channel'
52import { VideoPlaylistModel } from '../../models/video/video-playlist'
53
54const auditLogger = auditLoggerFactory('channels')
55const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
56const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
57
58const videoChannelRouter = express.Router()
59
60videoChannelRouter.get('/',
61 paginationValidator,
62 videoChannelsSortValidator,
63 setDefaultSort,
64 setDefaultPagination,
65 videoChannelsListValidator,
66 asyncMiddleware(listVideoChannels)
67)
68
69videoChannelRouter.post('/',
70 authenticate,
71 asyncMiddleware(videoChannelsAddValidator),
72 asyncRetryTransactionMiddleware(addVideoChannel)
73)
74
75videoChannelRouter.post('/:nameWithHost/avatar/pick',
76 authenticate,
77 reqAvatarFile,
78 asyncMiddleware(videoChannelsNameWithHostValidator),
79 ensureIsLocalChannel,
80 ensureCanManageChannelOrAccount,
81 updateAvatarValidator,
82 asyncMiddleware(updateVideoChannelAvatar)
83)
84
85videoChannelRouter.post('/:nameWithHost/banner/pick',
86 authenticate,
87 reqBannerFile,
88 asyncMiddleware(videoChannelsNameWithHostValidator),
89 ensureIsLocalChannel,
90 ensureCanManageChannelOrAccount,
91 updateBannerValidator,
92 asyncMiddleware(updateVideoChannelBanner)
93)
94
95videoChannelRouter.delete('/:nameWithHost/avatar',
96 authenticate,
97 asyncMiddleware(videoChannelsNameWithHostValidator),
98 ensureIsLocalChannel,
99 ensureCanManageChannelOrAccount,
100 asyncMiddleware(deleteVideoChannelAvatar)
101)
102
103videoChannelRouter.delete('/:nameWithHost/banner',
104 authenticate,
105 asyncMiddleware(videoChannelsNameWithHostValidator),
106 ensureIsLocalChannel,
107 ensureCanManageChannelOrAccount,
108 asyncMiddleware(deleteVideoChannelBanner)
109)
110
111videoChannelRouter.put('/:nameWithHost',
112 authenticate,
113 asyncMiddleware(videoChannelsNameWithHostValidator),
114 ensureIsLocalChannel,
115 ensureCanManageChannelOrAccount,
116 videoChannelsUpdateValidator,
117 asyncRetryTransactionMiddleware(updateVideoChannel)
118)
119
120videoChannelRouter.delete('/:nameWithHost',
121 authenticate,
122 asyncMiddleware(videoChannelsNameWithHostValidator),
123 ensureIsLocalChannel,
124 ensureCanManageChannelOrAccount,
125 asyncMiddleware(videoChannelsRemoveValidator),
126 asyncRetryTransactionMiddleware(removeVideoChannel)
127)
128
129videoChannelRouter.get('/:nameWithHost',
130 asyncMiddleware(videoChannelsNameWithHostValidator),
131 asyncMiddleware(getVideoChannel)
132)
133
134videoChannelRouter.get('/:nameWithHost/video-playlists',
135 asyncMiddleware(videoChannelsNameWithHostValidator),
136 paginationValidator,
137 videoPlaylistsSortValidator,
138 setDefaultSort,
139 setDefaultPagination,
140 commonVideoPlaylistFiltersValidator,
141 asyncMiddleware(listVideoChannelPlaylists)
142)
143
144videoChannelRouter.get('/:nameWithHost/videos',
145 asyncMiddleware(videoChannelsNameWithHostValidator),
146 paginationValidator,
147 videosSortValidator,
148 setDefaultVideosSort,
149 setDefaultPagination,
150 optionalAuthenticate,
151 commonVideosFiltersValidator,
152 asyncMiddleware(listVideoChannelVideos)
153)
154
155videoChannelRouter.get('/:nameWithHost/followers',
156 authenticate,
157 asyncMiddleware(videoChannelsNameWithHostValidator),
158 ensureCanManageChannelOrAccount,
159 paginationValidator,
160 videoChannelsFollowersSortValidator,
161 setDefaultSort,
162 setDefaultPagination,
163 asyncMiddleware(listVideoChannelFollowers)
164)
165
166videoChannelRouter.post('/:nameWithHost/import-videos',
167 authenticate,
168 asyncMiddleware(videoChannelsNameWithHostValidator),
169 asyncMiddleware(videoChannelImportVideosValidator),
170 ensureIsLocalChannel,
171 ensureCanManageChannelOrAccount,
172 asyncMiddleware(ensureChannelOwnerCanUpload),
173 asyncMiddleware(importVideosInChannel)
174)
175
176// ---------------------------------------------------------------------------
177
178export {
179 videoChannelRouter
180}
181
182// ---------------------------------------------------------------------------
183
184async function listVideoChannels (req: express.Request, res: express.Response) {
185 const serverActor = await getServerActor()
186
187 const apiOptions = await Hooks.wrapObject({
188 actorId: serverActor.id,
189 start: req.query.start,
190 count: req.query.count,
191 sort: req.query.sort
192 }, 'filter:api.video-channels.list.params')
193
194 const resultList = await Hooks.wrapPromiseFun(
195 VideoChannelModel.listForApi,
196 apiOptions,
197 'filter:api.video-channels.list.result'
198 )
199
200 return res.json(getFormattedObjects(resultList.data, resultList.total))
201}
202
203async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
204 const bannerPhysicalFile = req.files['bannerfile'][0]
205 const videoChannel = res.locals.videoChannel
206 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
207
208 const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
209
210 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
211
212 return res.json({
213 // TODO: remove, deprecated in 4.2
214 banner: getBiggestActorImage(banners).toFormattedJSON(),
215 banners: banners.map(b => b.toFormattedJSON())
216 })
217}
218
219async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
220 const avatarPhysicalFile = req.files['avatarfile'][0]
221 const videoChannel = res.locals.videoChannel
222 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
223
224 const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
225 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
226
227 return res.json({
228 // TODO: remove, deprecated in 4.2
229 avatar: getBiggestActorImage(avatars).toFormattedJSON(),
230 avatars: avatars.map(a => a.toFormattedJSON())
231 })
232}
233
234async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
235 const videoChannel = res.locals.videoChannel
236
237 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
238
239 return res.status(HttpStatusCode.NO_CONTENT_204).end()
240}
241
242async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
243 const videoChannel = res.locals.videoChannel
244
245 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
246
247 return res.status(HttpStatusCode.NO_CONTENT_204).end()
248}
249
250async function addVideoChannel (req: express.Request, res: express.Response) {
251 const videoChannelInfo: VideoChannelCreate = req.body
252
253 const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
254 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
255
256 return createLocalVideoChannel(videoChannelInfo, account, t)
257 })
258
259 const payload = { actorId: videoChannelCreated.actorId }
260 await JobQueue.Instance.createJob({ type: 'actor-keys', payload })
261
262 auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()))
263 logger.info('Video channel %s created.', videoChannelCreated.Actor.url)
264
265 Hooks.runAction('action:api.video-channel.created', { videoChannel: videoChannelCreated, req, res })
266
267 return res.json({
268 videoChannel: {
269 id: videoChannelCreated.id
270 }
271 })
272}
273
274async function updateVideoChannel (req: express.Request, res: express.Response) {
275 const videoChannelInstance = res.locals.videoChannel
276 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
277 const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
278 let doBulkVideoUpdate = false
279
280 try {
281 await sequelizeTypescript.transaction(async t => {
282 if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
283 if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
284
285 if (videoChannelInfoToUpdate.support !== undefined) {
286 const oldSupportField = videoChannelInstance.support
287 videoChannelInstance.support = videoChannelInfoToUpdate.support
288
289 if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) {
290 doBulkVideoUpdate = true
291 await VideoModel.bulkUpdateSupportField(videoChannelInstance, t)
292 }
293 }
294
295 const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault
296 await sendUpdateActor(videoChannelInstanceUpdated, t)
297
298 auditLogger.update(
299 getAuditIdFromRes(res),
300 new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
301 oldVideoChannelAuditKeys
302 )
303
304 Hooks.runAction('action:api.video-channel.updated', { videoChannel: videoChannelInstanceUpdated, req, res })
305
306 logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
307 })
308 } catch (err) {
309 logger.debug('Cannot update the video channel.', { err })
310
311 // If the transaction is retried, sequelize will think the object has not changed
312 // So we need to restore the previous fields
313 resetSequelizeInstance(videoChannelInstance)
314
315 throw err
316 }
317
318 res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
319
320 // Don't process in a transaction, and after the response because it could be long
321 if (doBulkVideoUpdate) {
322 await federateAllVideosOfChannel(videoChannelInstance)
323 }
324}
325
326async function removeVideoChannel (req: express.Request, res: express.Response) {
327 const videoChannelInstance = res.locals.videoChannel
328
329 await sequelizeTypescript.transaction(async t => {
330 await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
331
332 await videoChannelInstance.destroy({ transaction: t })
333
334 Hooks.runAction('action:api.video-channel.deleted', { videoChannel: videoChannelInstance, req, res })
335
336 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
337 logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url)
338 })
339
340 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
341}
342
343async function getVideoChannel (req: express.Request, res: express.Response) {
344 const id = res.locals.videoChannel.id
345 const videoChannel = await Hooks.wrapObject(res.locals.videoChannel, 'filter:api.video-channel.get.result', { id })
346
347 if (videoChannel.isOutdated()) {
348 JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
349 }
350
351 return res.json(videoChannel.toFormattedJSON())
352}
353
354async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
355 const serverActor = await getServerActor()
356
357 const resultList = await VideoPlaylistModel.listForApi({
358 followerActorId: serverActor.id,
359 start: req.query.start,
360 count: req.query.count,
361 sort: req.query.sort,
362 videoChannelId: res.locals.videoChannel.id,
363 type: req.query.playlistType
364 })
365
366 return res.json(getFormattedObjects(resultList.data, resultList.total))
367}
368
369async function listVideoChannelVideos (req: express.Request, res: express.Response) {
370 const serverActor = await getServerActor()
371
372 const videoChannelInstance = res.locals.videoChannel
373
374 const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
375 ? null
376 : {
377 actorId: serverActor.id,
378 orLocalVideos: true
379 }
380
381 const countVideos = getCountVideos(req)
382 const query = pickCommonVideoQuery(req.query)
383
384 const apiOptions = await Hooks.wrapObject({
385 ...query,
386
387 displayOnlyForFollower,
388 nsfw: buildNSFWFilter(res, query.nsfw),
389 videoChannelId: videoChannelInstance.id,
390 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
391 countVideos
392 }, 'filter:api.video-channels.videos.list.params')
393
394 const resultList = await Hooks.wrapPromiseFun(
395 VideoModel.listForApi,
396 apiOptions,
397 'filter:api.video-channels.videos.list.result'
398 )
399
400 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
401}
402
403async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
404 const channel = res.locals.videoChannel
405
406 const resultList = await ActorFollowModel.listFollowersForApi({
407 actorIds: [ channel.actorId ],
408 start: req.query.start,
409 count: req.query.count,
410 sort: req.query.sort,
411 search: req.query.search,
412 state: 'accepted'
413 })
414
415 return res.json(getFormattedObjects(resultList.data, resultList.total))
416}
417
418async function importVideosInChannel (req: express.Request, res: express.Response) {
419 const { externalChannelUrl } = req.body as VideosImportInChannelCreate
420
421 await JobQueue.Instance.createJob({
422 type: 'video-channel-import',
423 payload: {
424 externalChannelUrl,
425 videoChannelId: res.locals.videoChannel.id,
426 partOfChannelSyncId: res.locals.videoChannelSync?.id
427 }
428 })
429
430 logger.info('Video import job for channel "%s" with url "%s" created.', res.locals.videoChannel.name, externalChannelUrl)
431
432 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
433}