From 48dce1c90dff4e90a4bcffefaecf157336cf904b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 24 Apr 2018 17:05:32 +0200 Subject: [PATCH] Update video channel routes --- CHANGELOG.md | 3 + server/controllers/api/accounts.ts | 209 ++++++- server/controllers/api/index.ts | 2 + server/controllers/api/video-channel.ts | 34 ++ server/controllers/api/videos/channel.ts | 177 ------ server/controllers/api/videos/index.ts | 17 +- server/controllers/feeds.ts | 14 +- server/middlewares/validators/follows.ts | 2 +- .../middlewares/validators/video-channels.ts | 8 + server/models/video/video.ts | 92 ++- .../tests/api/check-params/video-channels.ts | 101 +++- server/tests/api/videos/multiple-servers.ts | 21 +- server/tests/api/videos/video-channels.ts | 15 +- server/tests/utils/videos/video-channels.ts | 35 +- support/doc/api/html/index.html | 543 ++++++++++++------ support/doc/api/openapi.yaml | 72 ++- 16 files changed, 859 insertions(+), 486 deletions(-) create mode 100644 server/controllers/api/video-channel.ts delete mode 100644 server/controllers/api/videos/channel.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 470b4f295..ac3f61735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### BREAKING CHANGES * Hide by default NSFW videos. Update the `instance.default_nsfw_policy` configuration to `blur` to keep the old behaviour + * Move video channels routes: + * `/videos/channels` routes to `/accounts/{accountId}/video-channels` + * `/videos/accounts/{accountId}/channels` route to `/accounts/{accountId}/video-channels` * PeerTube now listen on 127.0.0.1 by default * Use ISO 639 for language (*en*, *es*, *fr*...) * Tools (`import-videos`...) need the language ISO639 code instead of a number diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 06ab04033..04c5897c5 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts @@ -1,11 +1,30 @@ import * as express from 'express' -import { getFormattedObjects } from '../../helpers/utils' -import { asyncMiddleware, optionalAuthenticate, paginationValidator, setDefaultPagination, setDefaultSort } from '../../middlewares' +import { getFormattedObjects, resetSequelizeInstance } from '../../helpers/utils' +import { + asyncMiddleware, + authenticate, + listVideoAccountChannelsValidator, + optionalAuthenticate, + paginationValidator, + setDefaultPagination, + setDefaultSort, + videoChannelsAddValidator, + videoChannelsGetValidator, + videoChannelsRemoveValidator, + videoChannelsUpdateValidator +} from '../../middlewares' import { accountsGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' import { AccountModel } from '../../models/account/account' import { VideoModel } from '../../models/video/video' -import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type' import { isNSFWHidden } from '../../helpers/express-utils' +import { VideoChannelModel } from '../../models/video/video-channel' +import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' +import { sendUpdateActor } from '../../lib/activitypub/send' +import { createVideoChannel } from '../../lib/video-channel' +import { setAsyncActorKeys } from '../../lib/activitypub' +import { sequelizeTypescript } from '../../initializers' +import { logger } from '../../helpers/logger' +import { retryTransactionWrapper } from '../../helpers/database-utils' const accountsRouter = express.Router() @@ -29,7 +48,45 @@ accountsRouter.get('/:id/videos', setDefaultSort, setDefaultPagination, optionalAuthenticate, - asyncMiddleware(getAccountVideos) + asyncMiddleware(listAccountVideos) +) + +accountsRouter.get('/:accountId/video-channels', + asyncMiddleware(listVideoAccountChannelsValidator), + asyncMiddleware(listVideoAccountChannels) +) + +accountsRouter.post('/:accountId/video-channels', + authenticate, + videoChannelsAddValidator, + asyncMiddleware(addVideoChannelRetryWrapper) +) + +accountsRouter.put('/:accountId/video-channels/:id', + authenticate, + asyncMiddleware(videoChannelsUpdateValidator), + updateVideoChannelRetryWrapper +) + +accountsRouter.delete('/:accountId/video-channels/:id', + authenticate, + asyncMiddleware(videoChannelsRemoveValidator), + asyncMiddleware(removeVideoChannelRetryWrapper) +) + +accountsRouter.get('/:accountId/video-channels/:id', + asyncMiddleware(videoChannelsGetValidator), + asyncMiddleware(getVideoChannel) +) + +accountsRouter.get('/:accountId/video-channels/:id/videos', + asyncMiddleware(videoChannelsGetValidator), + paginationValidator, + videosSortValidator, + setDefaultSort, + setDefaultPagination, + optionalAuthenticate, + asyncMiddleware(listVideoChannelVideos) ) // --------------------------------------------------------------------------- @@ -52,18 +109,142 @@ async function listAccounts (req: express.Request, res: express.Response, next: return res.json(getFormattedObjects(resultList.data, resultList.total)) } -async function getAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { +async function listVideoAccountChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +// Wrapper to video channel add that retry the async function if there is a database error +// We need this because we run the transaction in SERIALIZABLE isolation that can fail +async function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot insert the video video channel with many retries.' + } + + const videoChannel = await retryTransactionWrapper(addVideoChannel, options) + return res.json({ + videoChannel: { + id: videoChannel.id + } + }).end() +} + +async function addVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInfo: VideoChannelCreate = req.body + const account: AccountModel = res.locals.oauth.token.User.Account + + const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { + return createVideoChannel(videoChannelInfo, account, t) + }) + + setAsyncActorKeys(videoChannelCreated.Actor) + .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) + + logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) + + return videoChannelCreated +} + +async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the video with many retries.' + } + + await retryTransactionWrapper(updateVideoChannel, options) + + return res.type('json').status(204).end() +} + +async function updateVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance = res.locals.videoChannel as VideoChannelModel + const videoChannelFieldsSave = videoChannelInstance.toJSON() + const videoChannelInfoToUpdate = req.body as VideoChannelUpdate + + try { + await sequelizeTypescript.transaction(async t => { + const sequelizeOptions = { + transaction: t + } + + if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) + if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) + if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support) + + const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) + await sendUpdateActor(videoChannelInstanceUpdated, t) + }) + + logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + } catch (err) { + logger.debug('Cannot update the video channel.', { err }) + + // Force fields we want to update + // If the transaction is retried, sequelize will think the object has not changed + // So it will skip the SQL request, even if the last one was ROLLBACKed! + resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave) + + throw err + } +} + +async function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot remove the video channel with many retries.' + } + + await retryTransactionWrapper(removeVideoChannel, options) + + return res.type('json').status(204).end() +} + +async function removeVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance: VideoChannelModel = res.locals.videoChannel + + return sequelizeTypescript.transaction(async t => { + await videoChannelInstance.destroy({ transaction: t }) + + logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + }) + +} + +async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { + const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) + + return res.json(videoChannelWithVideos.toFormattedJSON()) +} + +async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { + const videoChannelInstance: VideoChannelModel = res.locals.videoChannel + + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + withFiles: false, + videoChannelId: videoChannelInstance.id + }) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + + +async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { const account: AccountModel = res.locals.account - const resultList = await VideoModel.listForApi( - req.query.start as number, - req.query.count as number, - req.query.sort as VideoSortField, - isNSFWHidden(res), - null, - false, - account.id - ) + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + withFiles: false, + accountId: account.id + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 964d5d04c..8f63b9535 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts @@ -7,6 +7,7 @@ import { usersRouter } from './users' import { accountsRouter } from './accounts' import { videosRouter } from './videos' import { badRequest } from '../../helpers/express-utils' +import { videoChannelRouter } from './video-channel' const apiRouter = express.Router() @@ -15,6 +16,7 @@ apiRouter.use('/oauth-clients', oauthClientsRouter) apiRouter.use('/config', configRouter) apiRouter.use('/users', usersRouter) apiRouter.use('/accounts', accountsRouter) +apiRouter.use('/video-channels', videoChannelRouter) apiRouter.use('/videos', videosRouter) apiRouter.use('/jobs', jobsRouter) apiRouter.use('/ping', pong) diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts new file mode 100644 index 000000000..d57273747 --- /dev/null +++ b/server/controllers/api/video-channel.ts @@ -0,0 +1,34 @@ +import * as express from 'express' +import { getFormattedObjects } from '../../helpers/utils' +import { + asyncMiddleware, + paginationValidator, + setDefaultPagination, + setDefaultSort, + videoChannelsSortValidator +} from '../../middlewares' +import { VideoChannelModel } from '../../models/video/video-channel' + +const videoChannelRouter = express.Router() + +videoChannelRouter.get('/', + paginationValidator, + videoChannelsSortValidator, + setDefaultSort, + setDefaultPagination, + asyncMiddleware(listVideoChannels) +) + +// --------------------------------------------------------------------------- + +export { + videoChannelRouter +} + +// --------------------------------------------------------------------------- + +async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + const resultList = await VideoChannelModel.listForApi(req.query.start, req.query.count, req.query.sort) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts deleted file mode 100644 index e547d375f..000000000 --- a/server/controllers/api/videos/channel.ts +++ /dev/null @@ -1,177 +0,0 @@ -import * as express from 'express' -import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared' -import { retryTransactionWrapper } from '../../../helpers/database-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects, resetSequelizeInstance } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers' -import { setAsyncActorKeys } from '../../../lib/activitypub' -import { sendUpdateActor } from '../../../lib/activitypub/send' -import { createVideoChannel } from '../../../lib/video-channel' -import { - asyncMiddleware, authenticate, listVideoAccountChannelsValidator, paginationValidator, setDefaultSort, setDefaultPagination, - videoChannelsAddValidator, videoChannelsGetValidator, videoChannelsRemoveValidator, videoChannelsSortValidator, - videoChannelsUpdateValidator -} from '../../../middlewares' -import { AccountModel } from '../../../models/account/account' -import { VideoChannelModel } from '../../../models/video/video-channel' - -const videoChannelRouter = express.Router() - -videoChannelRouter.get('/channels', - paginationValidator, - videoChannelsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listVideoChannels) -) - -videoChannelRouter.get('/accounts/:accountId/channels', - asyncMiddleware(listVideoAccountChannelsValidator), - asyncMiddleware(listVideoAccountChannels) -) - -videoChannelRouter.post('/channels', - authenticate, - videoChannelsAddValidator, - asyncMiddleware(addVideoChannelRetryWrapper) -) - -videoChannelRouter.put('/channels/:id', - authenticate, - asyncMiddleware(videoChannelsUpdateValidator), - updateVideoChannelRetryWrapper -) - -videoChannelRouter.delete('/channels/:id', - authenticate, - asyncMiddleware(videoChannelsRemoveValidator), - asyncMiddleware(removeVideoChannelRetryWrapper) -) - -videoChannelRouter.get('/channels/:id', - asyncMiddleware(videoChannelsGetValidator), - asyncMiddleware(getVideoChannel) -) - -// --------------------------------------------------------------------------- - -export { - videoChannelRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoChannelModel.listForApi(req.query.start, req.query.count, req.query.sort) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listVideoAccountChannels (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -// Wrapper to video channel add that retry the async function if there is a database error -// We need this because we run the transaction in SERIALIZABLE isolation that can fail -async function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot insert the video video channel with many retries.' - } - - const videoChannel = await retryTransactionWrapper(addVideoChannel, options) - return res.json({ - videoChannel: { - id: videoChannel.id - } - }).end() -} - -async function addVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInfo: VideoChannelCreate = req.body - const account: AccountModel = res.locals.oauth.token.User.Account - - const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { - return createVideoChannel(videoChannelInfo, account, t) - }) - - setAsyncActorKeys(videoChannelCreated.Actor) - .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) - - logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) - - return videoChannelCreated -} - -async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the video with many retries.' - } - - await retryTransactionWrapper(updateVideoChannel, options) - - return res.type('json').status(204).end() -} - -async function updateVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance = res.locals.videoChannel as VideoChannelModel - const videoChannelFieldsSave = videoChannelInstance.toJSON() - const videoChannelInfoToUpdate = req.body as VideoChannelUpdate - - try { - await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { - transaction: t - } - - if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) - if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) - if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support) - - const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) - await sendUpdateActor(videoChannelInstanceUpdated, t) - }) - - logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) - } catch (err) { - logger.debug('Cannot update the video channel.', { err }) - - // Force fields we want to update - // If the transaction is retried, sequelize will think the object has not changed - // So it will skip the SQL request, even if the last one was ROLLBACKed! - resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave) - - throw err - } -} - -async function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot remove the video channel with many retries.' - } - - await retryTransactionWrapper(removeVideoChannel, options) - - return res.type('json').status(204).end() -} - -async function removeVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance: VideoChannelModel = res.locals.videoChannel - - return sequelizeTypescript.transaction(async t => { - await videoChannelInstance.destroy({ transaction: t }) - - logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) - }) - -} - -async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { - const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) - - return res.json(videoChannelWithVideos.toFormattedJSON()) -} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 61b6c5826..4b3198a74 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -42,7 +42,6 @@ import { VideoModel } from '../../../models/video/video' import { VideoFileModel } from '../../../models/video/video-file' import { abuseVideoRouter } from './abuse' import { blacklistRouter } from './blacklist' -import { videoChannelRouter } from './channel' import { videoCommentRouter } from './comment' import { rateVideoRouter } from './rate' import { VideoFilter } from '../../../../shared/models/videos/video-query.type' @@ -72,7 +71,6 @@ const reqVideoFileUpdate = createReqFiles( videosRouter.use('/', abuseVideoRouter) videosRouter.use('/', blacklistRouter) videosRouter.use('/', rateVideoRouter) -videosRouter.use('/', videoChannelRouter) videosRouter.use('/', videoCommentRouter) videosRouter.get('/categories', listVideoCategories) @@ -397,13 +395,14 @@ async function getVideoDescription (req: express.Request, res: express.Response) } async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoModel.listForApi( - req.query.start as number, - req.query.count as number, - req.query.sort as VideoSortField, - isNSFWHidden(res), - req.query.filter as VideoFilter - ) + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + filter: req.query.filter as VideoFilter, + withFiles: false + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 6a6af3e09..7dcaf7004 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -33,15 +33,15 @@ async function generateFeed (req: express.Request, res: express.Response, next: const account: AccountModel = res.locals.account const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' - const resultList = await VideoModel.listForApi( + const resultList = await VideoModel.listForApi({ start, - FEEDS.COUNT, - req.query.sort as VideoSortField, + count: FEEDS.COUNT, + sort: req.query.sort, hideNSFW, - req.query.filter, - true, - account ? account.id : null - ) + filter: req.query.filter, + withFiles: true, + accountId: account ? account.id : null + }) // Adding video items to the feed, one at a time resultList.data.forEach(video => { diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 991a2e175..bdf39eb9c 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -16,7 +16,7 @@ const followValidator = [ if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') { return res.status(400) .json({ - error: 'Cannot follow non HTTPS web server.' + error: 'Cannot follow on a non HTTPS web server.' }) .end() } diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index fe42105e8..e3a11a41b 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -26,6 +26,7 @@ const listVideoAccountChannelsValidator = [ ] const videoChannelsAddValidator = [ + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'), body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), @@ -41,6 +42,7 @@ const videoChannelsAddValidator = [ const videoChannelsUpdateValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'), body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), @@ -49,6 +51,7 @@ const videoChannelsUpdateValidator = [ logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) if (areValidationErrors(req, res)) return + if (!await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return // We need to make additional checks @@ -70,11 +73,13 @@ const videoChannelsUpdateValidator = [ const videoChannelsRemoveValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return + if (!await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return // Check if the user who did the request is able to delete the video @@ -87,11 +92,14 @@ const videoChannelsRemoveValidator = [ const videoChannelsGetValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').optional().custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoChannelsGet parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return + // On some routes, accountId is optional (for example in the ActivityPub route) + if (req.params.accountId && !await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return return next() diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 2ad9c00dd..7ababbf23 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -95,7 +95,14 @@ enum ScopeNames { } @Scopes({ - [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean, accountId?: number) => { + [ScopeNames.AVAILABLE_FOR_LIST]: (options: { + actorId: number, + hideNSFW: boolean, + filter?: VideoFilter, + withFiles?: boolean, + accountId?: number, + videoChannelId?: number + }) => { const accountInclude = { attributes: [ 'name' ], model: AccountModel.unscoped(), @@ -106,7 +113,7 @@ enum ScopeNames { attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ], model: ActorModel.unscoped(), required: true, - where: VideoModel.buildActorWhereWithFilter(filter), + where: VideoModel.buildActorWhereWithFilter(options.filter), include: [ { attributes: [ 'host' ], @@ -122,6 +129,18 @@ enum ScopeNames { ] } + const videoChannelInclude = { + attributes: [ 'name', 'description' ], + model: VideoChannelModel.unscoped(), + required: true, + where: {}, + include: [ + accountInclude + ] + } + + // Force actorId to be a number to avoid SQL injections + const actorIdNumber = parseInt(options.actorId.toString(), 10) const query: IFindOptions = { where: { id: { @@ -132,32 +151,23 @@ enum ScopeNames { '(' + 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + - 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + + 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + ' UNION ' + 'SELECT "video"."id" AS "id" FROM "video" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + 'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + - 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + + 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + actorIdNumber + ')' ) }, privacy: VideoPrivacy.PUBLIC }, - include: [ - { - attributes: [ 'name', 'description' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - accountInclude - ] - } - ] + include: [ videoChannelInclude ] } - if (withFiles === true) { + if (options.withFiles === true) { query.include.push({ model: VideoFileModel.unscoped(), required: true @@ -165,13 +175,19 @@ enum ScopeNames { } // Hide nsfw videos? - if (hideNSFW === true) { + if (options.hideNSFW === true) { query.where['nsfw'] = false } - if (accountId) { + if (options.accountId) { accountInclude.where = { - id: accountId + id: options.accountId + } + } + + if (options.videoChannelId) { + videoChannelInclude.where = { + id: options.videoChannelId } } @@ -697,23 +713,37 @@ export class VideoModel extends Model { }) } - static async listForApi ( + static async listForApi (options: { start: number, count: number, sort: string, hideNSFW: boolean, + withFiles: boolean, filter?: VideoFilter, - withFiles = false, - accountId?: number - ) { + accountId?: number, + videoChannelId?: number + }) { const query = { - offset: start, - limit: count, - order: getSort(sort) + offset: options.start, + limit: options.count, + order: getSort(options.sort) } const serverActor = await getServerActor() - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles, accountId ] }) + const scopes = { + method: [ + ScopeNames.AVAILABLE_FOR_LIST, { + actorId: serverActor.id, + hideNSFW: options.hideNSFW, + filter: options.filter, + withFiles: options.withFiles, + accountId: options.accountId, + videoChannelId: options.videoChannelId + } + ] + } + + return VideoModel.scope(scopes) .findAndCountAll(query) .then(({ rows, count }) => { return { @@ -750,8 +780,16 @@ export class VideoModel extends Model { } const serverActor = await getServerActor() + const scopes = { + method: [ + ScopeNames.AVAILABLE_FOR_LIST, { + actorId: serverActor.id, + hideNSFW + } + ] + } - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] }) + return VideoModel.scope(scopes) .findAndCountAll(query) .then(({ rows, count }) => { return { diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 43c5462ee..acb6bdd57 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts @@ -4,15 +4,29 @@ import * as chai from 'chai' import { omit } from 'lodash' import 'mocha' import { - createUser, deleteVideoChannel, flushTests, getAccountVideoChannelsList, getVideoChannelsList, immutableAssign, killallServers, - makeGetRequest, makePostBodyRequest, makePutBodyRequest, runServer, ServerInfo, setAccessTokensToServers, userLogin + createUser, + deleteVideoChannel, + flushTests, + getAccountVideoChannelsList, + getVideoChannelsList, + immutableAssign, + killallServers, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + runServer, + ServerInfo, + setAccessTokensToServers, + userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect describe('Test videos API validator', function () { - const path = '/api/v1/videos/channels' + const videoChannelPath = '/api/v1/video-channels' + const accountPath = '/api/v1/accounts/' let server: ServerInfo let accessTokenUser: string @@ -37,15 +51,15 @@ describe('Test videos API validator', function () { describe('When listing a video channels', function () { it('Should fail with a bad start pagination', async function () { - await checkBadStartPagination(server.url, path, server.accessToken) + await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) }) it('Should fail with a bad count pagination', async function () { - await checkBadCountPagination(server.url, path, server.accessToken) + await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) }) it('Should fail with an incorrect sort', async function () { - await checkBadSortPagination(server.url, path, server.accessToken) + await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) }) }) @@ -60,12 +74,20 @@ describe('Test videos API validator', function () { }) describe('When adding a video channel', function () { + let path: string + const baseCorrectParams = { name: 'hello', description: 'super description', support: 'super support text' } + before(async function () { + const res = await getAccountsList(server.url) + const accountId = res.body.data[0].id + path = accountPath + accountId + '/video-channels' + }) + it('Should fail with a non authenticated user', async function () { await makePostBodyRequest({ url: server.url, path, token: 'none', fields: baseCorrectParams, statusCodeExpected: 401 }) }) @@ -107,22 +129,27 @@ describe('Test videos API validator', function () { }) describe('When updating a video channel', function () { + let path: string + const baseCorrectParams = { name: 'hello', description: 'super description' } - let videoChannelId - before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + const videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + const accountId = res2.body.data[0].id + + path = accountPath + accountId + '/video-channels/' + videoChannelId }) it('Should fail with a non authenticated user', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: 'hi', fields: baseCorrectParams, statusCodeExpected: 401 @@ -132,7 +159,7 @@ describe('Test videos API validator', function () { it('Should fail with another authenticated user', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: accessTokenUser, fields: baseCorrectParams, statusCodeExpected: 403 @@ -141,23 +168,23 @@ describe('Test videos API validator', function () { it('Should fail with a long name', async function () { const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(25) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a long description', async function () { const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(60) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a long support text', async function () { const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(70) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should succeed with the correct parameters', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: server.accessToken, fields: baseCorrectParams, statusCodeExpected: 204 @@ -166,17 +193,23 @@ describe('Test videos API validator', function () { }) describe('When getting a video channel', function () { + let basePath: string let videoChannelId: number before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + const accountId = res2.body.data[0].id + + basePath = accountPath + accountId + '/video-channels' }) it('Should return the list of the video channels with nothing', async function () { const res = await makeGetRequest({ url: server.url, - path, + path: basePath, statusCodeExpected: 200 }) @@ -186,7 +219,7 @@ describe('Test videos API validator', function () { it('Should fail without a correct uuid', async function () { await makeGetRequest({ url: server.url, - path: path + '/coucou', + path: basePath + '/coucou', statusCodeExpected: 400 }) }) @@ -194,7 +227,7 @@ describe('Test videos API validator', function () { it('Should return 404 with an incorrect video channel', async function () { await makeGetRequest({ url: server.url, - path: path + '/4da6fde3-88f7-4d16-b119-108df5630b06', + path: basePath + '/4da6fde3-88f7-4d16-b119-108df5630b06', statusCodeExpected: 404 }) }) @@ -202,7 +235,7 @@ describe('Test videos API validator', function () { it('Should succeed with the correct parameters', async function () { await makeGetRequest({ url: server.url, - path: path + '/' + videoChannelId, + path: basePath + '/' + videoChannelId, statusCodeExpected: 200 }) }) @@ -210,33 +243,41 @@ describe('Test videos API validator', function () { describe('When deleting a video channel', function () { let videoChannelId: number + let accountId: number before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + accountId = res2.body.data[0].id }) it('Should fail with a non authenticated user', async function () { - await deleteVideoChannel(server.url, 'coucou', videoChannelId, 401) + await deleteVideoChannel(server.url, 'coucou', accountId, videoChannelId, 401) }) it('Should fail with another authenticated user', async function () { - await deleteVideoChannel(server.url, accessTokenUser, videoChannelId, 403) + await deleteVideoChannel(server.url, accessTokenUser, accountId, videoChannelId, 403) + }) + + it('Should fail with an unknown account id', async function () { + await deleteVideoChannel(server.url, server.accessToken, 454554,videoChannelId, 404) }) - it('Should fail with an unknown id', async function () { - await deleteVideoChannel(server.url, server.accessToken, 454554, 404) + it('Should fail with an unknown video channel id', async function () { + await deleteVideoChannel(server.url, server.accessToken, accountId,454554, 404) }) it('Should succeed with the correct parameters', async function () { - await deleteVideoChannel(server.url, server.accessToken, videoChannelId) + await deleteVideoChannel(server.url, server.accessToken, accountId, videoChannelId) }) it('Should fail to delete the last user video channel', async function () { const res = await getVideoChannelsList(server.url, 0, 1) videoChannelId = res.body.data[0].id - await deleteVideoChannel(server.url, server.accessToken, videoChannelId, 409) + await deleteVideoChannel(server.url, server.accessToken, accountId, videoChannelId, 409) }) }) diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 2563939ec..6238cdc08 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -39,6 +39,7 @@ import { getVideoCommentThreads, getVideoThreadComments } from '../../utils/videos/video-comments' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect @@ -46,6 +47,7 @@ describe('Test multiple servers', function () { let servers: ServerInfo[] = [] const toRemove = [] let videoUUID = '' + let accountId: number let videoChannelId: number before(async function () { @@ -56,13 +58,20 @@ describe('Test multiple servers', function () { // Get the access tokens await setAccessTokensToServers(servers) - const videoChannel = { - name: 'my channel', - description: 'super channel' + { + const res = await getAccountsList(servers[0].url) + accountId = res.body.data[0].id + } + + { + const videoChannel = { + name: 'my channel', + description: 'super channel' + } + await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, accountId, videoChannel) + const channelRes = await getVideoChannelsList(servers[ 0 ].url, 0, 1) + videoChannelId = channelRes.body.data[ 0 ].id } - await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) - const channelRes = await getVideoChannelsList(servers[0].url, 0, 1) - videoChannelId = channelRes.body.data[0].id // Server 1 and server 2 follow each other await doubleFollow(servers[0], servers[1]) diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index b9c9bbf3c..a7552a83a 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts @@ -17,12 +17,14 @@ import { setAccessTokensToServers, updateVideoChannel } from '../../utils/index' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect describe('Test video channels', function () { let servers: ServerInfo[] let userInfo: User + let accountId: number let videoChannelId: number before(async function () { @@ -35,6 +37,11 @@ describe('Test video channels', function () { await setAccessTokensToServers(servers) await doubleFollow(servers[0], servers[1]) + { + const res = await getAccountsList(servers[0].url) + accountId = res.body.data[0].id + } + await wait(5000) }) @@ -54,7 +61,7 @@ describe('Test video channels', function () { description: 'super video channel description', support: 'super video channel support text' } - const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) + const res = await addVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannel) videoChannelId = res.body.videoChannel.id // The channel is 1 is propagated to servers 2 @@ -120,7 +127,7 @@ describe('Test video channels', function () { support: 'video channel support text updated' } - await updateVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId, videoChannelAttributes) + await updateVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannelId, videoChannelAttributes) await wait(3000) }) @@ -139,7 +146,7 @@ describe('Test video channels', function () { }) it('Should get video channel', async function () { - const res = await getVideoChannel(servers[0].url, videoChannelId) + const res = await getVideoChannel(servers[0].url, accountId, videoChannelId) const videoChannel = res.body expect(videoChannel.displayName).to.equal('video channel updated') @@ -148,7 +155,7 @@ describe('Test video channels', function () { }) it('Should delete video channel', async function () { - await deleteVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId) + await deleteVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannelId) }) it('Should have video channel deleted', async function () { diff --git a/server/tests/utils/videos/video-channels.ts b/server/tests/utils/videos/video-channels.ts index 2d095d8ab..cfc541431 100644 --- a/server/tests/utils/videos/video-channels.ts +++ b/server/tests/utils/videos/video-channels.ts @@ -7,7 +7,7 @@ type VideoChannelAttributes = { } function getVideoChannelsList (url: string, start: number, count: number, sort?: string) { - const path = '/api/v1/videos/channels' + const path = '/api/v1/video-channels' const req = request(url) .get(path) @@ -22,7 +22,7 @@ function getVideoChannelsList (url: string, start: number, count: number, sort?: } function getAccountVideoChannelsList (url: string, accountId: number | string, specialStatus = 200) { - const path = '/api/v1/videos/accounts/' + accountId + '/channels' + const path = '/api/v1/accounts/' + accountId + '/video-channels' return request(url) .get(path) @@ -31,8 +31,14 @@ function getAccountVideoChannelsList (url: string, accountId: number | string, s .expect('Content-Type', /json/) } -function addVideoChannel (url: string, token: string, videoChannelAttributesArg: VideoChannelAttributes, expectedStatus = 200) { - const path = '/api/v1/videos/channels' +function addVideoChannel ( + url: string, + token: string, + accountId: number, + videoChannelAttributesArg: VideoChannelAttributes, + expectedStatus = 200 +) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' // Default attributes let attributes = { @@ -50,9 +56,16 @@ function addVideoChannel (url: string, token: string, videoChannelAttributesArg: .expect(expectedStatus) } -function updateVideoChannel (url: string, token: string, channelId: number, attributes: VideoChannelAttributes, expectedStatus = 204) { +function updateVideoChannel ( + url: string, + token: string, + accountId: number, + channelId: number, + attributes: VideoChannelAttributes, + expectedStatus = 204 +) { const body = {} - const path = '/api/v1/videos/channels/' + channelId + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId if (attributes.name) body['name'] = attributes.name if (attributes.description) body['description'] = attributes.description @@ -66,18 +79,18 @@ function updateVideoChannel (url: string, token: string, channelId: number, attr .expect(expectedStatus) } -function deleteVideoChannel (url: string, token: string, channelId: number, expectedStatus = 204) { - const path = '/api/v1/videos/channels/' +function deleteVideoChannel (url: string, token: string, accountId: number, channelId: number, expectedStatus = 204) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId return request(url) - .delete(path + channelId) + .delete(path) .set('Accept', 'application/json') .set('Authorization', 'Bearer ' + token) .expect(expectedStatus) } -function getVideoChannel (url: string, channelId: number) { - const path = '/api/v1/videos/channels/' + channelId +function getVideoChannel (url: string, accountId: number, channelId: number) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId return request(url) .get(path) diff --git a/support/doc/api/html/index.html b/support/doc/api/html/index.html index bf9430e79..23162c307 100644 --- a/support/doc/api/html/index.html +++ b/support/doc/api/html/index.html @@ -40,6 +40,14 @@ +
+ Feeds + +
Job
@@ -224,7 +232,8 @@
Schema Definitions
- VideoConstant + VideoConstantNumber + VideoConstantString VideoPrivacy Video VideoAbuse @@ -605,6 +614,95 @@ +

Feeds

+
+ + +
+ Feeds + +
+ + +

+ + GET + /feeds/videos.{format} + +

+
+
+
+
+
+
format
+ +
in path
+
+ string + + xml, + atom, + json + + + xml +
+
+
+

The format expected (xml defaults to RSS 2.0, atom to ATOM 1.0 and json to JSON FEED 1.0

+
+
+
+
+
accountId
+
in query
+
+ number + +
+
+
+

The id of the local account to filter to (beware, users IDs and not actors IDs which will return empty feeds

+
+
+
+
+
accountName
+
in query
+
+ string + +
+
+
+

The name of the local account to filter to

+
+
+
+
+
+
+
+
+
+
+
+
200 OK
+
+
+

successful operation

+
+
+
+
+
+
Response Content-Types: + application/atom+xml, application/rss+xml, application/json +
+
+
+

Job

@@ -1518,7 +1616,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -1770,7 +1868,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -2047,7 +2145,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -2488,7 +2586,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -2825,7 +2923,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -3206,7 +3304,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -3329,7 +3427,7 @@
language
in formData
- number + string
@@ -3478,7 +3576,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -3611,7 +3709,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -3942,7 +4040,7 @@
language
in formData
- number + string
@@ -4665,7 +4763,7 @@

VideoChannel

-
+
@@ -4677,7 +4775,7 @@

GET - /videos/channels + /video-channels

@@ -4789,9 +4887,144 @@ "label": "string" }, "language": { + "id": "string", + "label": "string" + }, + "privacy": "string", + "description": "string", + "duration": "number", + "isLocal": "boolean", + "name": "string", + "thumbnailPath": "string", + "previewPath": "string", + "embedPath": "string", + "views": "number", + "likes": "number", + "dislikes": "number", + "nsfw": "boolean", + "account": { + "name": "string", + "displayName": "string", + "url": "string", + "host": "string", + "avatar": { + "path": "string", + "createdAt": "string", + "updatedAt": "string" + } + } + } + ] + } +] + + + +
+
+
+
+ + +
+ VideoChannel + +
+ + +

+ + GET + /accounts/{accountId}/video-channels + +

+
+
+
+
+
+
accountId
+ +
in path
+
+ string + +
+
+
+

The account id

+
+
+
+
+
+
+
+
+
+
+
+
200 OK
+
+ + VideoChannel + +
+ +
+
+

successful operation

+
+
+
+
type
+
+ + + VideoChannel + + + +
+
+
+
+
+
Response Content-Types: + application/json +
+
+
Response Example + (200 OK) +
+
[
+  {
+    "displayName": "string",
+    "description": "string",
+    "isLocal": "boolean",
+    "owner": {
+      "name": "string",
+      "uuid": "string"
+    },
+    "videos": [
+      {
+        "id": "number",
+        "uuid": "string",
+        "createdAt": "string",
+        "publishedAt": "string",
+        "updatedAt": "string",
+        "category": {
           "id": "number",
           "label": "string"
         },
+        "licence": {
+          "id": "number",
+          "label": "string"
+        },
+        "language": {
+          "id": "string",
+          "label": "string"
+        },
         "privacy": "string",
         "description": "string",
         "duration": "number",
@@ -4825,7 +5058,7 @@
               
-
+
@@ -4837,7 +5070,7 @@

POST - /videos/channels + /accounts/{accountId}/video-channels

@@ -4858,6 +5091,22 @@
+
+
+
+
accountId
+ +
in path
+
+ string + +
+
+
+

The account id

+
+
+
@@ -4916,7 +5165,7 @@
-
+
@@ -4928,12 +5177,26 @@

GET - /videos/channels/{id} + /account/{accountId}/video-channels/{id}

+
+
+
accountId
+ +
in path
+
+ string + +
+
+
+

The account id

+
+
id
@@ -4945,7 +5208,7 @@
-

The video id

+

The video channel id

@@ -5003,7 +5266,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -5038,7 +5301,7 @@
-
+
@@ -5050,7 +5313,7 @@

PUT - /videos/channels/{id} + /account/{accountId}/video-channels/{id}

@@ -5072,6 +5335,20 @@
+
+
+
accountId
+ +
in path
+
+ string + +
+
+
+

The account id

+
+
id
@@ -5083,7 +5360,7 @@
-

The video id

+

The video channel id

@@ -5145,7 +5422,7 @@
-
+
@@ -5157,12 +5434,26 @@

DELETE - /videos/channels/{id} + /account/{accountId}/video-channels/{id}

+
+
+
accountId
+ +
in path
+
+ string + +
+
+
+

The account id

+
+
id
@@ -5174,7 +5465,7 @@
-

The video id

+

The video channel id

@@ -5223,141 +5514,6 @@
-
- - -
- VideoChannel - -
- - -

- - GET - /videos/accounts/{accountId}/channels - -

-
-
-
-
-
-
accountId
- -
in path
-
- string - -
-
-
-

The account id

-
-
-
-
-
-
-
-
-
-
-
-
200 OK
-
- - VideoChannel - -
- -
-
-

successful operation

-
-
-
-
type
-
- - - VideoChannel - - - -
-
-
-
-
-
Response Content-Types: - application/json -
-
-
Response Example - (200 OK) -
-
[
-  {
-    "displayName": "string",
-    "description": "string",
-    "isLocal": "boolean",
-    "owner": {
-      "name": "string",
-      "uuid": "string"
-    },
-    "videos": [
-      {
-        "id": "number",
-        "uuid": "string",
-        "createdAt": "string",
-        "publishedAt": "string",
-        "updatedAt": "string",
-        "category": {
-          "id": "number",
-          "label": "string"
-        },
-        "licence": {
-          "id": "number",
-          "label": "string"
-        },
-        "language": {
-          "id": "number",
-          "label": "string"
-        },
-        "privacy": "string",
-        "description": "string",
-        "duration": "number",
-        "isLocal": "boolean",
-        "name": "string",
-        "thumbnailPath": "string",
-        "previewPath": "string",
-        "embedPath": "string",
-        "views": "number",
-        "likes": "number",
-        "dislikes": "number",
-        "nsfw": "boolean",
-        "account": {
-          "name": "string",
-          "displayName": "string",
-          "url": "string",
-          "host": "string",
-          "avatar": {
-            "path": "string",
-            "createdAt": "string",
-            "updatedAt": "string"
-          }
-        }
-      }
-    ]
-  }
-]
-
- -
-
-
-

VideoComment

@@ -6104,9 +6260,9 @@

Schema Definitions

-
+

- VideoConstant: + VideoConstantNumber: + +

+
+ +
+

+ VideoConstantString: + +

+
+
+
+
+
+ id: + string + +
+
+ label: + string + +
+
+
+
+
+
+
Example
+
{
+  "id": "string",
+  "label": "string"
+}
 
@@ -6210,7 +6405,7 @@ category: - VideoConstant + VideoConstantNumber @@ -6219,7 +6414,7 @@ licence: - VideoConstant + VideoConstantNumber @@ -6228,7 +6423,7 @@ language: - VideoConstant + VideoConstantString @@ -6323,7 +6518,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -6609,7 +6804,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", @@ -7219,7 +7414,7 @@ "label": "string" }, "language": { - "id": "number", + "id": "string", "label": "string" }, "privacy": "string", diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 10f60175d..4a1f06d00 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -91,7 +91,7 @@ paths: in: path required: true type: string - enum: ['xml', 'atom' 'json'] + enum: [ 'xml', 'atom', 'json'] default: 'xml' description: 'The format expected (xml defaults to RSS 2.0, atom to ATOM 1.0 and json to JSON FEED 1.0' - name: accountId @@ -967,7 +967,7 @@ paths: type: array items: $ref: '#/definitions/VideoBlacklist' - /videos/channels: + /video-channels: get: tags: - VideoChannel @@ -998,6 +998,27 @@ paths: type: array items: $ref: '#/definitions/VideoChannel' + /accounts/{accountId}/video-channels: + get: + tags: + - VideoChannel + consumes: + - application/json + produces: + - application/json + parameters: + - name: accountId + in: path + required: true + type: string + description: 'The account id ' + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/VideoChannel' post: security: - OAuth2: [ ] @@ -1008,6 +1029,11 @@ paths: produces: - application/json parameters: + - name: accountId + in: path + required: true + type: string + description: 'The account id ' - in: body name: body schema: @@ -1015,7 +1041,7 @@ paths: responses: '204': description: successful operation - "/videos/channels/{id}": + "/account/{accountId}/video-channels/{id}": get: tags: - VideoChannel @@ -1024,11 +1050,16 @@ paths: produces: - application/json parameters: + - name: accountId + in: path + required: true + type: string + description: 'The account id ' - name: id in: path required: true type: string - description: 'The video id ' + description: 'The video channel id ' responses: '200': description: successful operation @@ -1044,11 +1075,16 @@ paths: produces: - application/json parameters: + - name: accountId + in: path + required: true + type: string + description: 'The account id ' - name: id in: path required: true type: string - description: 'The video id ' + description: 'The video channel id ' - in: body name: body schema: @@ -1066,35 +1102,19 @@ paths: produces: - application/json parameters: - - name: id + - name: accountId in: path required: true type: string - description: 'The video id ' - responses: - '204': - description: successful operation - /videos/accounts/{accountId}/channels: - get: - tags: - - VideoChannel - consumes: - - application/json - produces: - - application/json - parameters: - - name: accountId + description: 'The account id ' + - name: id in: path required: true type: string - description: 'The account id ' + description: 'The video channel id ' responses: - '200': + '204': description: successful operation - schema: - type: array - items: - $ref: '#/definitions/VideoChannel' "/videos/{videoId}/comment-threads": get: tags: -- 2.41.0