Playlist server API
[oweals/peertube.git] / server / controllers / api / video-channel.ts
1 import * as express from 'express'
2 import { getFormattedObjects, getServerActor } from '../../helpers/utils'
3 import {
4   asyncMiddleware,
5   asyncRetryTransactionMiddleware,
6   authenticate,
7   commonVideosFiltersValidator,
8   optionalAuthenticate,
9   paginationValidator,
10   setDefaultPagination,
11   setDefaultSort,
12   videoChannelsAddValidator,
13   videoChannelsRemoveValidator,
14   videoChannelsSortValidator,
15   videoChannelsUpdateValidator,
16   videoPlaylistsSortValidator
17 } from '../../middlewares'
18 import { VideoChannelModel } from '../../models/video/video-channel'
19 import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
20 import { sendUpdateActor } from '../../lib/activitypub/send'
21 import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
22 import { createVideoChannel } from '../../lib/video-channel'
23 import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
24 import { setAsyncActorKeys } from '../../lib/activitypub'
25 import { AccountModel } from '../../models/account/account'
26 import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
27 import { logger } from '../../helpers/logger'
28 import { VideoModel } from '../../models/video/video'
29 import { updateAvatarValidator } from '../../middlewares/validators/avatar'
30 import { updateActorAvatarFile } from '../../lib/avatar'
31 import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
32 import { resetSequelizeInstance } from '../../helpers/database-utils'
33 import { UserModel } from '../../models/account/user'
34 import { JobQueue } from '../../lib/job-queue'
35 import { VideoPlaylistModel } from '../../models/video/video-playlist'
36
37 const auditLogger = auditLoggerFactory('channels')
38 const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
39
40 const videoChannelRouter = express.Router()
41
42 videoChannelRouter.get('/',
43   paginationValidator,
44   videoChannelsSortValidator,
45   setDefaultSort,
46   setDefaultPagination,
47   asyncMiddleware(listVideoChannels)
48 )
49
50 videoChannelRouter.post('/',
51   authenticate,
52   asyncMiddleware(videoChannelsAddValidator),
53   asyncRetryTransactionMiddleware(addVideoChannel)
54 )
55
56 videoChannelRouter.post('/:nameWithHost/avatar/pick',
57   authenticate,
58   reqAvatarFile,
59   // Check the rights
60   asyncMiddleware(videoChannelsUpdateValidator),
61   updateAvatarValidator,
62   asyncMiddleware(updateVideoChannelAvatar)
63 )
64
65 videoChannelRouter.put('/:nameWithHost',
66   authenticate,
67   asyncMiddleware(videoChannelsUpdateValidator),
68   asyncRetryTransactionMiddleware(updateVideoChannel)
69 )
70
71 videoChannelRouter.delete('/:nameWithHost',
72   authenticate,
73   asyncMiddleware(videoChannelsRemoveValidator),
74   asyncRetryTransactionMiddleware(removeVideoChannel)
75 )
76
77 videoChannelRouter.get('/:nameWithHost',
78   asyncMiddleware(videoChannelsNameWithHostValidator),
79   asyncMiddleware(getVideoChannel)
80 )
81
82 videoChannelRouter.get('/:nameWithHost/video-playlists',
83   asyncMiddleware(videoChannelsNameWithHostValidator),
84   paginationValidator,
85   videoPlaylistsSortValidator,
86   setDefaultSort,
87   setDefaultPagination,
88   asyncMiddleware(listVideoChannelPlaylists)
89 )
90
91 videoChannelRouter.get('/:nameWithHost/videos',
92   asyncMiddleware(videoChannelsNameWithHostValidator),
93   paginationValidator,
94   videosSortValidator,
95   setDefaultSort,
96   setDefaultPagination,
97   optionalAuthenticate,
98   commonVideosFiltersValidator,
99   asyncMiddleware(listVideoChannelVideos)
100 )
101
102 // ---------------------------------------------------------------------------
103
104 export {
105   videoChannelRouter
106 }
107
108 // ---------------------------------------------------------------------------
109
110 async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) {
111   const serverActor = await getServerActor()
112   const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
113
114   return res.json(getFormattedObjects(resultList.data, resultList.total))
115 }
116
117 async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
118   const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
119   const videoChannel = res.locals.videoChannel as VideoChannelModel
120   const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
121
122   const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel)
123
124   auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
125
126   return res
127     .json({
128       avatar: avatar.toFormattedJSON()
129     })
130     .end()
131 }
132
133 async function addVideoChannel (req: express.Request, res: express.Response) {
134   const videoChannelInfo: VideoChannelCreate = req.body
135
136   const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => {
137     const account = await AccountModel.load((res.locals.oauth.token.User as UserModel).Account.id, t)
138
139     return createVideoChannel(videoChannelInfo, account, t)
140   })
141
142   setAsyncActorKeys(videoChannelCreated.Actor)
143     .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err }))
144
145   auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()))
146   logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid)
147
148   return res.json({
149     videoChannel: {
150       id: videoChannelCreated.id,
151       uuid: videoChannelCreated.Actor.uuid
152     }
153   }).end()
154 }
155
156 async function updateVideoChannel (req: express.Request, res: express.Response) {
157   const videoChannelInstance = res.locals.videoChannel as VideoChannelModel
158   const videoChannelFieldsSave = videoChannelInstance.toJSON()
159   const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
160   const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
161
162   try {
163     await sequelizeTypescript.transaction(async t => {
164       const sequelizeOptions = {
165         transaction: t
166       }
167
168       if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.displayName)
169       if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
170       if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support)
171
172       const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
173       await sendUpdateActor(videoChannelInstanceUpdated, t)
174
175       auditLogger.update(
176         getAuditIdFromRes(res),
177         new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
178         oldVideoChannelAuditKeys
179       )
180       logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
181     })
182   } catch (err) {
183     logger.debug('Cannot update the video channel.', { err })
184
185     // Force fields we want to update
186     // If the transaction is retried, sequelize will think the object has not changed
187     // So it will skip the SQL request, even if the last one was ROLLBACKed!
188     resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave)
189
190     throw err
191   }
192
193   return res.type('json').status(204).end()
194 }
195
196 async function removeVideoChannel (req: express.Request, res: express.Response) {
197   const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
198
199   await sequelizeTypescript.transaction(async t => {
200     await videoChannelInstance.destroy({ transaction: t })
201
202     auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
203     logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
204   })
205
206   return res.type('json').status(204).end()
207 }
208
209 async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) {
210   const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id)
211
212   if (videoChannelWithVideos.isOutdated()) {
213     JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } })
214             .catch(err => logger.error('Cannot create AP refresher job for actor %s.', videoChannelWithVideos.Actor.url, { err }))
215   }
216
217   return res.json(videoChannelWithVideos.toFormattedJSON())
218 }
219
220 async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
221   const serverActor = await getServerActor()
222
223   const resultList = await VideoPlaylistModel.listForApi({
224     followerActorId: serverActor.id,
225     start: req.query.start,
226     count: req.query.count,
227     sort: req.query.sort,
228     videoChannelId: res.locals.videoChannel.id
229   })
230
231   return res.json(getFormattedObjects(resultList.data, resultList.total))
232 }
233
234 async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
235   const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
236   const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
237
238   const resultList = await VideoModel.listForApi({
239     followerActorId,
240     start: req.query.start,
241     count: req.query.count,
242     sort: req.query.sort,
243     includeLocalVideos: true,
244     categoryOneOf: req.query.categoryOneOf,
245     licenceOneOf: req.query.licenceOneOf,
246     languageOneOf: req.query.languageOneOf,
247     tagsOneOf: req.query.tagsOneOf,
248     tagsAllOf: req.query.tagsAllOf,
249     filter: req.query.filter,
250     nsfw: buildNSFWFilter(res, req.query.nsfw),
251     withFiles: false,
252     videoChannelId: videoChannelInstance.id,
253     user: res.locals.oauth ? res.locals.oauth.token.User : undefined
254   })
255
256   return res.json(getFormattedObjects(resultList.data, resultList.total))
257 }