diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-26 10:55:40 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-03-18 11:17:59 +0100 |
commit | 418d092afa81e2c8fe8ac6838fc4b5eb0af6a782 (patch) | |
tree | 5e9bc5604fd5d66a006cfebb7acdbdd5486e5d1e /server/controllers/api/video-playlist.ts | |
parent | b427febb4d5cebf03b815bca2c59af6e82491569 (diff) | |
download | PeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.tar.gz PeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.tar.zst PeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.zip |
Playlist server API
Diffstat (limited to 'server/controllers/api/video-playlist.ts')
-rw-r--r-- | server/controllers/api/video-playlist.ts | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts new file mode 100644 index 000000000..709c58beb --- /dev/null +++ b/server/controllers/api/video-playlist.ts | |||
@@ -0,0 +1,415 @@ | |||
1 | import * as express from 'express' | ||
2 | import { getFormattedObjects, getServerActor } from '../../helpers/utils' | ||
3 | import { | ||
4 | asyncMiddleware, | ||
5 | asyncRetryTransactionMiddleware, | ||
6 | authenticate, | ||
7 | commonVideosFiltersValidator, | ||
8 | paginationValidator, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort | ||
11 | } from '../../middlewares' | ||
12 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
13 | import { videoPlaylistsSortValidator } from '../../middlewares/validators' | ||
14 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | ||
15 | import { CONFIG, MIMETYPES, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' | ||
16 | import { logger } from '../../helpers/logger' | ||
17 | import { resetSequelizeInstance } from '../../helpers/database-utils' | ||
18 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
19 | import { | ||
20 | videoPlaylistsAddValidator, | ||
21 | videoPlaylistsAddVideoValidator, | ||
22 | videoPlaylistsDeleteValidator, | ||
23 | videoPlaylistsGetValidator, | ||
24 | videoPlaylistsReorderVideosValidator, | ||
25 | videoPlaylistsUpdateOrRemoveVideoValidator, | ||
26 | videoPlaylistsUpdateValidator | ||
27 | } from '../../middlewares/validators/videos/video-playlists' | ||
28 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' | ||
29 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
30 | import { processImage } from '../../helpers/image-utils' | ||
31 | import { join } from 'path' | ||
32 | import { UserModel } from '../../models/account/user' | ||
33 | import { | ||
34 | getVideoPlaylistActivityPubUrl, | ||
35 | getVideoPlaylistElementActivityPubUrl, | ||
36 | sendCreateVideoPlaylist, | ||
37 | sendDeleteVideoPlaylist, | ||
38 | sendUpdateVideoPlaylist | ||
39 | } from '../../lib/activitypub' | ||
40 | import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' | ||
41 | import { VideoModel } from '../../models/video/video' | ||
42 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | ||
43 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' | ||
44 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' | ||
45 | import { copy, pathExists } from 'fs-extra' | ||
46 | |||
47 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) | ||
48 | |||
49 | const videoPlaylistRouter = express.Router() | ||
50 | |||
51 | videoPlaylistRouter.get('/', | ||
52 | paginationValidator, | ||
53 | videoPlaylistsSortValidator, | ||
54 | setDefaultSort, | ||
55 | setDefaultPagination, | ||
56 | asyncMiddleware(listVideoPlaylists) | ||
57 | ) | ||
58 | |||
59 | videoPlaylistRouter.get('/:playlistId', | ||
60 | asyncMiddleware(videoPlaylistsGetValidator), | ||
61 | getVideoPlaylist | ||
62 | ) | ||
63 | |||
64 | videoPlaylistRouter.post('/', | ||
65 | authenticate, | ||
66 | reqThumbnailFile, | ||
67 | asyncMiddleware(videoPlaylistsAddValidator), | ||
68 | asyncRetryTransactionMiddleware(addVideoPlaylist) | ||
69 | ) | ||
70 | |||
71 | videoPlaylistRouter.put('/:playlistId', | ||
72 | authenticate, | ||
73 | reqThumbnailFile, | ||
74 | asyncMiddleware(videoPlaylistsUpdateValidator), | ||
75 | asyncRetryTransactionMiddleware(updateVideoPlaylist) | ||
76 | ) | ||
77 | |||
78 | videoPlaylistRouter.delete('/:playlistId', | ||
79 | authenticate, | ||
80 | asyncMiddleware(videoPlaylistsDeleteValidator), | ||
81 | asyncRetryTransactionMiddleware(removeVideoPlaylist) | ||
82 | ) | ||
83 | |||
84 | videoPlaylistRouter.get('/:playlistId/videos', | ||
85 | asyncMiddleware(videoPlaylistsGetValidator), | ||
86 | paginationValidator, | ||
87 | setDefaultPagination, | ||
88 | commonVideosFiltersValidator, | ||
89 | asyncMiddleware(getVideoPlaylistVideos) | ||
90 | ) | ||
91 | |||
92 | videoPlaylistRouter.post('/:playlistId/videos', | ||
93 | authenticate, | ||
94 | asyncMiddleware(videoPlaylistsAddVideoValidator), | ||
95 | asyncRetryTransactionMiddleware(addVideoInPlaylist) | ||
96 | ) | ||
97 | |||
98 | videoPlaylistRouter.put('/:playlistId/videos', | ||
99 | authenticate, | ||
100 | asyncMiddleware(videoPlaylistsReorderVideosValidator), | ||
101 | asyncRetryTransactionMiddleware(reorderVideosPlaylist) | ||
102 | ) | ||
103 | |||
104 | videoPlaylistRouter.put('/:playlistId/videos/:videoId', | ||
105 | authenticate, | ||
106 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), | ||
107 | asyncRetryTransactionMiddleware(updateVideoPlaylistElement) | ||
108 | ) | ||
109 | |||
110 | videoPlaylistRouter.delete('/:playlistId/videos/:videoId', | ||
111 | authenticate, | ||
112 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), | ||
113 | asyncRetryTransactionMiddleware(removeVideoFromPlaylist) | ||
114 | ) | ||
115 | |||
116 | // --------------------------------------------------------------------------- | ||
117 | |||
118 | export { | ||
119 | videoPlaylistRouter | ||
120 | } | ||
121 | |||
122 | // --------------------------------------------------------------------------- | ||
123 | |||
124 | async function listVideoPlaylists (req: express.Request, res: express.Response) { | ||
125 | const serverActor = await getServerActor() | ||
126 | const resultList = await VideoPlaylistModel.listForApi({ | ||
127 | followerActorId: serverActor.id, | ||
128 | start: req.query.start, | ||
129 | count: req.query.count, | ||
130 | sort: req.query.sort | ||
131 | }) | ||
132 | |||
133 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
134 | } | ||
135 | |||
136 | function getVideoPlaylist (req: express.Request, res: express.Response) { | ||
137 | const videoPlaylist = res.locals.videoPlaylist as VideoPlaylistModel | ||
138 | |||
139 | return res.json(videoPlaylist.toFormattedJSON()) | ||
140 | } | ||
141 | |||
142 | async function addVideoPlaylist (req: express.Request, res: express.Response) { | ||
143 | const videoPlaylistInfo: VideoPlaylistCreate = req.body | ||
144 | const user: UserModel = res.locals.oauth.token.User | ||
145 | |||
146 | const videoPlaylist = new VideoPlaylistModel({ | ||
147 | name: videoPlaylistInfo.displayName, | ||
148 | description: videoPlaylistInfo.description, | ||
149 | privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE, | ||
150 | ownerAccountId: user.Account.id | ||
151 | }) | ||
152 | |||
153 | videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object | ||
154 | |||
155 | if (videoPlaylistInfo.videoChannelId !== undefined) { | ||
156 | const videoChannel = res.locals.videoChannel as VideoChannelModel | ||
157 | |||
158 | videoPlaylist.videoChannelId = videoChannel.id | ||
159 | videoPlaylist.VideoChannel = videoChannel | ||
160 | } | ||
161 | |||
162 | const thumbnailField = req.files['thumbnailfile'] | ||
163 | if (thumbnailField) { | ||
164 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | ||
165 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()), THUMBNAILS_SIZE) | ||
166 | } | ||
167 | |||
168 | const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { | ||
169 | const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) | ||
170 | |||
171 | await sendCreateVideoPlaylist(videoPlaylistCreated, t) | ||
172 | |||
173 | return videoPlaylistCreated | ||
174 | }) | ||
175 | |||
176 | logger.info('Video playlist with uuid %s created.', videoPlaylist.uuid) | ||
177 | |||
178 | return res.json({ | ||
179 | videoPlaylist: { | ||
180 | id: videoPlaylistCreated.id, | ||
181 | uuid: videoPlaylistCreated.uuid | ||
182 | } | ||
183 | }).end() | ||
184 | } | ||
185 | |||
186 | async function updateVideoPlaylist (req: express.Request, res: express.Response) { | ||
187 | const videoPlaylistInstance = res.locals.videoPlaylist as VideoPlaylistModel | ||
188 | const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON() | ||
189 | const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate | ||
190 | const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE | ||
191 | |||
192 | const thumbnailField = req.files['thumbnailfile'] | ||
193 | if (thumbnailField) { | ||
194 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | ||
195 | await processImage( | ||
196 | thumbnailPhysicalFile, | ||
197 | join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylistInstance.getThumbnailName()), | ||
198 | THUMBNAILS_SIZE | ||
199 | ) | ||
200 | } | ||
201 | |||
202 | try { | ||
203 | await sequelizeTypescript.transaction(async t => { | ||
204 | const sequelizeOptions = { | ||
205 | transaction: t | ||
206 | } | ||
207 | |||
208 | if (videoPlaylistInfoToUpdate.videoChannelId !== undefined) { | ||
209 | if (videoPlaylistInfoToUpdate.videoChannelId === null) { | ||
210 | videoPlaylistInstance.videoChannelId = null | ||
211 | } else { | ||
212 | const videoChannel = res.locals.videoChannel as VideoChannelModel | ||
213 | |||
214 | videoPlaylistInstance.videoChannelId = videoChannel.id | ||
215 | } | ||
216 | } | ||
217 | |||
218 | if (videoPlaylistInfoToUpdate.displayName !== undefined) videoPlaylistInstance.name = videoPlaylistInfoToUpdate.displayName | ||
219 | if (videoPlaylistInfoToUpdate.description !== undefined) videoPlaylistInstance.description = videoPlaylistInfoToUpdate.description | ||
220 | |||
221 | if (videoPlaylistInfoToUpdate.privacy !== undefined) { | ||
222 | videoPlaylistInstance.privacy = parseInt(videoPlaylistInfoToUpdate.privacy.toString(), 10) | ||
223 | } | ||
224 | |||
225 | const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) | ||
226 | |||
227 | const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE | ||
228 | |||
229 | if (isNewPlaylist) { | ||
230 | await sendCreateVideoPlaylist(playlistUpdated, t) | ||
231 | } else { | ||
232 | await sendUpdateVideoPlaylist(playlistUpdated, t) | ||
233 | } | ||
234 | |||
235 | logger.info('Video playlist %s updated.', videoPlaylistInstance.uuid) | ||
236 | |||
237 | return playlistUpdated | ||
238 | }) | ||
239 | } catch (err) { | ||
240 | logger.debug('Cannot update the video playlist.', { err }) | ||
241 | |||
242 | // Force fields we want to update | ||
243 | // If the transaction is retried, sequelize will think the object has not changed | ||
244 | // So it will skip the SQL request, even if the last one was ROLLBACKed! | ||
245 | resetSequelizeInstance(videoPlaylistInstance, videoPlaylistFieldsSave) | ||
246 | |||
247 | throw err | ||
248 | } | ||
249 | |||
250 | return res.type('json').status(204).end() | ||
251 | } | ||
252 | |||
253 | async function removeVideoPlaylist (req: express.Request, res: express.Response) { | ||
254 | const videoPlaylistInstance: VideoPlaylistModel = res.locals.videoPlaylist | ||
255 | |||
256 | await sequelizeTypescript.transaction(async t => { | ||
257 | await videoPlaylistInstance.destroy({ transaction: t }) | ||
258 | |||
259 | await sendDeleteVideoPlaylist(videoPlaylistInstance, t) | ||
260 | |||
261 | logger.info('Video playlist %s deleted.', videoPlaylistInstance.uuid) | ||
262 | }) | ||
263 | |||
264 | return res.type('json').status(204).end() | ||
265 | } | ||
266 | |||
267 | async function addVideoInPlaylist (req: express.Request, res: express.Response) { | ||
268 | const body: VideoPlaylistElementCreate = req.body | ||
269 | const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist | ||
270 | const video: VideoModel = res.locals.video | ||
271 | |||
272 | const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => { | ||
273 | const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t) | ||
274 | |||
275 | const playlistElement = await VideoPlaylistElementModel.create({ | ||
276 | url: getVideoPlaylistElementActivityPubUrl(videoPlaylist, video), | ||
277 | position, | ||
278 | startTimestamp: body.startTimestamp || null, | ||
279 | stopTimestamp: body.stopTimestamp || null, | ||
280 | videoPlaylistId: videoPlaylist.id, | ||
281 | videoId: video.id | ||
282 | }, { transaction: t }) | ||
283 | |||
284 | // If the user did not set a thumbnail, automatically take the video thumbnail | ||
285 | if (playlistElement.position === 1) { | ||
286 | const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()) | ||
287 | |||
288 | if (await pathExists(playlistThumbnailPath) === false) { | ||
289 | const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) | ||
290 | await copy(videoThumbnailPath, playlistThumbnailPath) | ||
291 | } | ||
292 | } | ||
293 | |||
294 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
295 | |||
296 | return playlistElement | ||
297 | }) | ||
298 | |||
299 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) | ||
300 | |||
301 | return res.json({ | ||
302 | videoPlaylistElement: { | ||
303 | id: playlistElement.id | ||
304 | } | ||
305 | }).end() | ||
306 | } | ||
307 | |||
308 | async function updateVideoPlaylistElement (req: express.Request, res: express.Response) { | ||
309 | const body: VideoPlaylistElementUpdate = req.body | ||
310 | const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist | ||
311 | const videoPlaylistElement: VideoPlaylistElementModel = res.locals.videoPlaylistElement | ||
312 | |||
313 | const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => { | ||
314 | if (body.startTimestamp !== undefined) videoPlaylistElement.startTimestamp = body.startTimestamp | ||
315 | if (body.stopTimestamp !== undefined) videoPlaylistElement.stopTimestamp = body.stopTimestamp | ||
316 | |||
317 | const element = await videoPlaylistElement.save({ transaction: t }) | ||
318 | |||
319 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
320 | |||
321 | return element | ||
322 | }) | ||
323 | |||
324 | logger.info('Element of position %d of playlist %s updated.', playlistElement.position, videoPlaylist.uuid) | ||
325 | |||
326 | return res.type('json').status(204).end() | ||
327 | } | ||
328 | |||
329 | async function removeVideoFromPlaylist (req: express.Request, res: express.Response) { | ||
330 | const videoPlaylistElement: VideoPlaylistElementModel = res.locals.videoPlaylistElement | ||
331 | const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist | ||
332 | const positionToDelete = videoPlaylistElement.position | ||
333 | |||
334 | await sequelizeTypescript.transaction(async t => { | ||
335 | await videoPlaylistElement.destroy({ transaction: t }) | ||
336 | |||
337 | // Decrease position of the next elements | ||
338 | await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t) | ||
339 | |||
340 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
341 | |||
342 | logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) | ||
343 | }) | ||
344 | |||
345 | return res.type('json').status(204).end() | ||
346 | } | ||
347 | |||
348 | async function reorderVideosPlaylist (req: express.Request, res: express.Response) { | ||
349 | const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist | ||
350 | |||
351 | const start: number = req.body.startPosition | ||
352 | const insertAfter: number = req.body.insertAfter | ||
353 | const reorderLength: number = req.body.reorderLength || 1 | ||
354 | |||
355 | if (start === insertAfter) { | ||
356 | return res.status(204).end() | ||
357 | } | ||
358 | |||
359 | // Example: if we reorder position 2 and insert after position 5 (so at position 6): # 1 2 3 4 5 6 7 8 9 | ||
360 | // * increase position when position > 5 # 1 2 3 4 5 7 8 9 10 | ||
361 | // * update position 2 -> position 6 # 1 3 4 5 6 7 8 9 10 | ||
362 | // * decrease position when position position > 2 # 1 2 3 4 5 6 7 8 9 | ||
363 | await sequelizeTypescript.transaction(async t => { | ||
364 | const newPosition = insertAfter + 1 | ||
365 | |||
366 | // Add space after the position when we want to insert our reordered elements (increase) | ||
367 | await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, newPosition, null, reorderLength, t) | ||
368 | |||
369 | let oldPosition = start | ||
370 | |||
371 | // We incremented the position of the elements we want to reorder | ||
372 | if (start >= newPosition) oldPosition += reorderLength | ||
373 | |||
374 | const endOldPosition = oldPosition + reorderLength - 1 | ||
375 | // Insert our reordered elements in their place (update) | ||
376 | await VideoPlaylistElementModel.reassignPositionOf(videoPlaylist.id, oldPosition, endOldPosition, newPosition, t) | ||
377 | |||
378 | // Decrease positions of elements after the old position of our ordered elements (decrease) | ||
379 | await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t) | ||
380 | |||
381 | await sendUpdateVideoPlaylist(videoPlaylist, t) | ||
382 | }) | ||
383 | |||
384 | logger.info( | ||
385 | 'Reordered playlist %s (inserted after %d elements %d - %d).', | ||
386 | videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1 | ||
387 | ) | ||
388 | |||
389 | return res.type('json').status(204).end() | ||
390 | } | ||
391 | |||
392 | async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { | ||
393 | const videoPlaylistInstance: VideoPlaylistModel = res.locals.videoPlaylist | ||
394 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | ||
395 | |||
396 | const resultList = await VideoModel.listForApi({ | ||
397 | followerActorId, | ||
398 | start: req.query.start, | ||
399 | count: req.query.count, | ||
400 | sort: 'VideoPlaylistElements.position', | ||
401 | includeLocalVideos: true, | ||
402 | categoryOneOf: req.query.categoryOneOf, | ||
403 | licenceOneOf: req.query.licenceOneOf, | ||
404 | languageOneOf: req.query.languageOneOf, | ||
405 | tagsOneOf: req.query.tagsOneOf, | ||
406 | tagsAllOf: req.query.tagsAllOf, | ||
407 | filter: req.query.filter, | ||
408 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
409 | withFiles: false, | ||
410 | videoPlaylistId: videoPlaylistInstance.id, | ||
411 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
412 | }) | ||
413 | |||
414 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
415 | } | ||