Add channel filters for my videos/followers

pull/4477/head
Chocobozzz 2021-10-20 09:05:43 +02:00
parent 7e76cc3800
commit 978c87e7f5
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
15 changed files with 207 additions and 54 deletions

View File

@ -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`
}
]
}
]

View File

@ -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`
}
]
}
]

View File

@ -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`
}
]
}
]

View File

@ -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
}
]
})
}

View File

@ -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)
)

View File

@ -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`
}
]
}
]

View File

@ -5,11 +5,13 @@
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced filters</h6>
<ng-container *ngFor="let group of filters">
<h6 class="dropdown-header">{{ group.title }}</h6>
<a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
{{ filter.label }}
</a>
<a *ngFor="let filter of group.children" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
{{ filter.label }}
</a>
</ng-container>
</div>
</div>

View File

@ -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')

View File

@ -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<ResultList<Video>> {
getMyVideos (options: {
videoPagination: ComponentPaginationLight
sort: VideoSortField
userChannels?: VideoChannelServerModel[]
search?: string
}): Observable<ResultList<Video>> {
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
}
}
})

View File

@ -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')

View File

@ -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,

View File

@ -978,10 +978,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
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<Partial<AttributesOnly<VideoModel>>> {
where.isLive = isLive
}
const channelWhere = channelId
? { id: channelId }
: {}
const baseQuery = {
offset: start,
limit: count,
@ -1005,6 +1011,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
{
model: VideoChannelModel,
required: true,
where: channelWhere,
include: [
{
model: AccountModel,

View File

@ -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 })
})

View File

@ -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' })

View File

@ -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
})