diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 3edcb1c63..7baf34ca2 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -28,12 +28,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
inputFilters: AdvancedInputFilter[] = [
{
- queryParams: { search: 'type:auto' },
- label: $localize`Automatic blocks`
- },
- {
- queryParams: { search: 'type:manual' },
- label: $localize`Manual blocks`
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ queryParams: { search: 'type:auto' },
+ label: $localize`Automatic blocks`
+ },
+ {
+ queryParams: { search: 'type:manual' },
+ label: $localize`Manual blocks`
+ }
+ ]
}
]
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
index c09ce7293..a60b228af 100644
--- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
+++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
@@ -44,12 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
inputFilters: AdvancedInputFilter[] = [
{
- queryParams: { search: 'local:true' },
- label: $localize`Local comments`
- },
- {
- queryParams: { search: 'local:false' },
- label: $localize`Remote comments`
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ queryParams: { search: 'local:true' },
+ label: $localize`Local comments`
+ },
+ {
+ queryParams: { search: 'local:false' },
+ label: $localize`Remote comments`
+ }
+ ]
}
]
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 1030759df..548e6e80f 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -36,8 +36,13 @@ export class UserListComponent extends RestTable implements OnInit {
inputFilters: AdvancedInputFilter[] = [
{
- queryParams: { search: 'banned:true' },
- label: $localize`Banned users`
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ queryParams: { search: 'banned:true' },
+ label: $localize`Banned users`
+ }
+ ]
}
]
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.ts b/client/src/app/+my-library/my-follows/my-followers.component.ts
index 413d524df..4a72b983f 100644
--- a/client/src/app/+my-library/my-follows/my-followers.component.ts
+++ b/client/src/app/+my-library/my-follows/my-followers.component.ts
@@ -37,12 +37,19 @@ export class MyFollowersComponent implements OnInit {
}
this.auth.userInformationLoaded.subscribe(() => {
- this.inputFilters = this.auth.getUser().videoChannels.map(c => {
+ const channelFilters = this.auth.getUser().videoChannels.map(c => {
return {
queryParams: { search: 'channel:' + c.name },
- label: $localize`Followers of ${c.name}`
+ label: c.name
}
})
+
+ this.inputFilters = [
+ {
+ title: $localize`Channel filters`,
+ children: channelFilters
+ }
+ ]
})
}
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index b1f3baf80..a117d0915 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -9,7 +9,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
-import { VideoSortField } from '@shared/models'
+import { VideoChannel, VideoSortField } from '@shared/models'
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
@Component({
@@ -47,16 +47,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
user: User
- inputFilters: AdvancedInputFilter[] = [
- {
- queryParams: { search: 'isLive:true' },
- label: $localize`Only live videos`
- }
- ]
+ inputFilters: AdvancedInputFilter[]
disabled = false
private search: string
+ private userChannels: VideoChannel[] = []
constructor (
protected router: Router,
@@ -79,6 +75,35 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
if (this.route.snapshot.queryParams['search']) {
this.search = this.route.snapshot.queryParams['search']
}
+
+ this.authService.userInformationLoaded.subscribe(() => {
+ this.user = this.authService.getUser()
+ this.userChannels = this.user.videoChannels
+
+ const channelFilters = this.userChannels.map(c => {
+ return {
+ queryParams: { search: 'channel:' + c.name },
+ label: c.name
+ }
+ })
+
+ this.inputFilters = [
+ {
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ queryParams: { search: 'isLive:true' },
+ label: $localize`Only live videos`
+ }
+ ]
+ },
+
+ {
+ title: $localize`Channel filters`,
+ children: channelFilters
+ }
+ ]
+ })
}
onSearch (search: string) {
@@ -105,7 +130,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
- return this.videoService.getMyVideos(newPagination, this.sort, this.search)
+ return this.videoService.getMyVideos({
+ videoPagination: newPagination,
+ sort: this.sort,
+ userChannels: this.userChannels,
+ search: this.search
+ })
.pipe(
tap(res => this.pagination.totalItems = res.total)
)
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 33e9fd8de..297993e39 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -39,24 +39,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
inputFilters: AdvancedInputFilter[] = [
{
- queryParams: { search: 'state:pending' },
- label: $localize`Unsolved reports`
- },
- {
- queryParams: { search: 'state:accepted' },
- label: $localize`Accepted reports`
- },
- {
- queryParams: { search: 'state:rejected' },
- label: $localize`Refused reports`
- },
- {
- queryParams: { search: 'videoIs:blacklisted' },
- label: $localize`Reports with blocked videos`
- },
- {
- queryParams: { search: 'videoIs:deleted' },
- label: $localize`Reports with deleted videos`
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ queryParams: { search: 'state:pending' },
+ label: $localize`Unsolved reports`
+ },
+ {
+ queryParams: { search: 'state:accepted' },
+ label: $localize`Accepted reports`
+ },
+ {
+ queryParams: { search: 'state:rejected' },
+ label: $localize`Refused reports`
+ },
+ {
+ queryParams: { search: 'videoIs:blacklisted' },
+ label: $localize`Reports with blocked videos`
+ },
+ {
+ queryParams: { search: 'videoIs:deleted' },
+ label: $localize`Reports with deleted videos`
+ }
+ ]
}
]
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
index 10d1296cf..c662b9bb6 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.html
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
@@ -5,11 +5,13 @@
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
index 8315662b4..a12dddf7a 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
@@ -5,8 +5,12 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@
import { ActivatedRoute, Params, Router } from '@angular/router'
export type AdvancedInputFilter = {
- label: string
- queryParams: Params
+ title: string
+
+ children: {
+ label: string
+ queryParams: Params
+ }[]
}
const logger = debug('peertube:AdvancedInputFilterComponent')
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 2f43f1b9d..7935569e7 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -13,6 +13,7 @@ import {
UserVideoRateType,
UserVideoRateUpdate,
Video as VideoServerModel,
+ VideoChannel as VideoChannelServerModel,
VideoConstant,
VideoDetails as VideoDetailsServerModel,
VideoFileMetadata,
@@ -122,7 +123,14 @@ export class VideoService {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
- getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable> {
+ getMyVideos (options: {
+ videoPagination: ComponentPaginationLight
+ sort: VideoSortField
+ userChannels?: VideoChannelServerModel[]
+ search?: string
+ }): Observable> {
+ const { videoPagination, sort, userChannels = [], search } = options
+
const pagination = this.restService.componentToRestPagination(videoPagination)
let params = new HttpParams()
@@ -133,6 +141,16 @@ export class VideoService {
isLive: {
prefix: 'isLive:',
isBoolean: true
+ },
+ channelId: {
+ prefix: 'channel:',
+ handler: (name: string) => {
+ const channel = userChannels.find(c => c.name === name)
+
+ if (channel) return channel.id
+
+ return undefined
+ }
}
})
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 83b774d3c..6bacdbbb6 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -25,7 +25,7 @@ import {
usersUpdateMeValidator,
usersVideoRatingValidator
} from '../../../middlewares'
-import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
+import { deleteMeValidator, usersVideosValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
@@ -69,6 +69,7 @@ meRouter.get('/me/videos',
videosSortValidator,
setDefaultVideosSort,
setDefaultPagination,
+ asyncMiddleware(usersVideosValidator),
asyncMiddleware(getUserVideos)
)
@@ -113,6 +114,7 @@ async function getUserVideos (req: express.Request, res: express.Response) {
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
+ channelId: res.locals.videoChannel?.id,
isLive: req.query.isLive
}, 'filter:api.user.me.videos.list.params')
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index c6eeeaf18..8f1a7801f 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -4,7 +4,7 @@ import { omit } from 'lodash'
import { Hooks } from '@server/lib/plugins/hooks'
import { MUserDefault } from '@server/types/models'
import { HttpStatusCode, UserRegister, UserRole } from '@shared/models'
-import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
+import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import {
isUserAdminFlagsValid,
@@ -31,7 +31,7 @@ import { Redis } from '../../lib/redis'
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
import { ActorModel } from '../../models/actor/actor'
import { UserModel } from '../../models/user/user'
-import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
+import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
const usersListValidator = [
query('blocked')
@@ -318,6 +318,28 @@ const usersVideoRatingValidator = [
}
]
+const usersVideosValidator = [
+ query('isLive')
+ .optional()
+ .customSanitizer(toBooleanOrNull)
+ .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
+
+ query('channelId')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isIdValid).withMessage('Should have a valid channel id'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking usersVideosValidator parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+
+ if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
+
+ return next()
+ }
+]
+
const ensureUserRegistrationAllowed = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const allowedParams = {
@@ -513,6 +535,7 @@ export {
ensureUserRegistrationAllowed,
ensureUserRegistrationAllowedForIP,
usersGetValidator,
+ usersVideosValidator,
usersAskResetPasswordValidator,
usersResetPasswordValidator,
usersAskSendVerifyEmailValidator,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index d2daf18ee..4044287ee 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -978,10 +978,12 @@ export class VideoModel extends Model>> {
start: number
count: number
sort: string
+
+ channelId?: number
isLive?: boolean
search?: string
}) {
- const { accountId, start, count, sort, search, isLive } = options
+ const { accountId, channelId, start, count, sort, search, isLive } = options
function buildBaseQuery (): FindOptions {
const where: WhereOptions = {}
@@ -996,6 +998,10 @@ export class VideoModel extends Model>> {
where.isLive = isLive
}
+ const channelWhere = channelId
+ ? { id: channelId }
+ : {}
+
const baseQuery = {
offset: start,
limit: count,
@@ -1005,6 +1011,7 @@ export class VideoModel extends Model>> {
{
model: VideoChannelModel,
required: true,
+ where: channelWhere,
include: [
{
model: AccountModel,
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index e11ca0c82..d02b6e156 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -119,6 +119,20 @@ describe('Test videos API validator', function () {
await checkBadSortPagination(server.url, path, server.accessToken)
})
+ it('Should fail with an invalid channel', async function () {
+ await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } })
+ })
+
+ it('Should fail with an unknown channel', async function () {
+ await makeGetRequest({
+ url: server.url,
+ token: server.accessToken,
+ path,
+ query: { channelId: 89898 },
+ expectedStatus: HttpStatusCode.NOT_FOUND_404
+ })
+ })
+
it('Should success with the correct parameters', async function () {
await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 })
})
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 085d9d870..6c41e7d56 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -318,6 +318,8 @@ describe('Test users', function () {
fixture: 'video_short.webm'
}
await server.videos.upload({ token: userToken, attributes })
+
+ await server.channels.create({ token: userToken, attributes: { name: 'other_channel' } })
})
it('Should have video quota updated', async function () {
@@ -340,6 +342,29 @@ describe('Test users', function () {
expect(video.previewPath).to.not.be.null
})
+ it('Should be able to filter by channel in my videos', async function () {
+ const myInfo = await server.users.getMyInfo({ token: userToken })
+ const mainChannel = myInfo.videoChannels.find(c => c.name !== 'other_channel')
+ const otherChannel = myInfo.videoChannels.find(c => c.name === 'other_channel')
+
+ {
+ const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: mainChannel.id })
+ expect(total).to.equal(1)
+ expect(data).to.have.lengthOf(1)
+
+ const video: Video = data[0]
+ expect(video.name).to.equal('super user video')
+ expect(video.thumbnailPath).to.not.be.null
+ expect(video.previewPath).to.not.be.null
+ }
+
+ {
+ const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: otherChannel.id })
+ expect(total).to.equal(0)
+ expect(data).to.have.lengthOf(0)
+ }
+ })
+
it('Should be able to search in my videos', async function () {
{
const { total, data } = await server.videos.listMyVideos({ token: userToken, sort: '-createdAt', search: 'user video' })
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
index 99f56a34c..c1a9ec806 100644
--- a/shared/extra-utils/videos/videos-command.ts
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -207,6 +207,7 @@ export class VideosCommand extends AbstractCommand {
sort?: string
search?: string
isLive?: boolean
+ channelId?: number
} = {}) {
const path = '/api/v1/users/me/videos'
@@ -214,7 +215,7 @@ export class VideosCommand extends AbstractCommand {
...options,
path,
- query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
+ query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})