Add ability to filter my videos by live

pull/4042/head
Chocobozzz 2021-05-03 11:06:19 +02:00
parent dfcb6f50a6
commit 1fd61899ea
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
46 changed files with 569 additions and 336 deletions

View File

@ -3,4 +3,4 @@
<ng-container i18n>Reports</ng-container> <ng-container i18n>Reports</ng-container>
</h1> </h1>
<my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-table> <my-abuse-list-table viewType="admin"></my-abuse-list-table>

View File

@ -13,25 +13,7 @@
<ng-template pTemplate="caption"> <ng-template pTemplate="caption">
<div class="caption"> <div class="caption">
<div class="ml-auto"> <div class="ml-auto">
<div class="input-group has-feedback has-clear"> <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
<div class="input-group-text" ngbDropdownToggle>
<span class="caret" aria-haspopup="menu" role="button"></span>
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced block filters</h6>
<a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:auto' }" class="dropdown-item" i18n>Automatic blocks</a>
<a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:manual' }" class="dropdown-item" i18n>Manual blocks</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>

View File

@ -6,6 +6,7 @@ import { AfterViewInit, Component, OnInit } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser' import { DomSanitizer } from '@angular/platform-browser'
import { ActivatedRoute, Params, Router } from '@angular/router' import { ActivatedRoute, Params, Router } from '@angular/router'
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { VideoBlockService } from '@app/shared/shared-moderation' import { VideoBlockService } from '@app/shared/shared-moderation'
import { VideoBlacklist, VideoBlacklistType } from '@shared/models' import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
@ -24,6 +25,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = []
inputFilters: AdvancedInputFilter[] = [
{
queryParams: { 'search': 'type:auto' },
label: $localize`Automatic blocks`
},
{
queryParams: { 'search': 'type:manual' },
label: $localize`Manual blocks`
}
]
constructor ( constructor (
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected router: Router, protected router: Router,
@ -111,25 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
if (this.search) this.setTableFilter(this.search, false) if (this.search) this.setTableFilter(this.search, false)
} }
/* Table filter functions */
onBlockSearch (event: Event) {
this.onSearch(event)
this.setQueryParams((event.target as HTMLInputElement).value)
}
setQueryParams (search: string) {
const queryParams: Params = {}
if (search) Object.assign(queryParams, { search })
this.router.navigate([ '/admin/moderation/video-blocks/list' ], { queryParams })
}
resetTableFilter () {
this.setTableFilter('')
this.setQueryParams('')
this.resetSearch()
}
/* END Table filter functions */
getIdentifier () { getIdentifier () {
return 'VideoBlockListComponent' return 'VideoBlockListComponent'
} }

View File

@ -26,25 +26,7 @@
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
<div class="input-group has-feedback has-clear"> <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
<div class="input-group-text" ngbDropdownToggle>
<span class="caret" aria-haspopup="menu" role="button"></span>
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced comments filters</h6>
<a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:true' }" class="dropdown-item" i18n>Local comments</a>
<a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:false' }" class="dropdown-item" i18n>Remote comments</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>

View File

@ -2,6 +2,7 @@ import { SortMeta } from 'primeng/api'
import { AfterViewInit, Component, OnInit } from '@angular/core' import { AfterViewInit, Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction } from '@app/shared/shared-main' import { DropdownAction } from '@app/shared/shared-main'
import { BulkService } from '@app/shared/shared-moderation' import { BulkService } from '@app/shared/shared-moderation'
import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
@ -43,6 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
selectedComments: VideoCommentAdmin[] = [] selectedComments: VideoCommentAdmin[] = []
bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = []
inputFilters: AdvancedInputFilter[] = [
{
queryParams: { 'search': 'local:true' },
label: $localize`Local comments`
},
{
queryParams: { 'search': 'local:false' },
label: $localize`Remote comments`
}
]
get authUser () { get authUser () {
return this.auth.getUser() return this.auth.getUser()
} }

View File

@ -22,24 +22,7 @@
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
<div class="input-group has-feedback has-clear"> <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
<div class="input-group-text" ngbDropdownToggle>
<span class="caret" aria-haspopup="menu" role="button"></span>
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced user filters</h6>
<a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ tr.banned > td {
.table-email { .table-email {
@include disable-default-a-behaviour; @include disable-default-a-behaviour;
color: pvar(--mainForegroundColor); color: pvar(--mainForegroundColor);
} }
@ -28,14 +29,6 @@ tr.banned > td {
margin-left: 0.1rem; margin-left: 0.1rem;
} }
.caption {
justify-content: space-between;
input {
@include peertube-input-text(250px);
}
}
p-tableCheckbox { p-tableCheckbox {
position: relative; position: relative;
top: -2.5px; top: -2.5px;
@ -55,18 +48,7 @@ my-global-icon {
.progress { .progress {
@include progressbar($small: true); @include progressbar($small: true);
width: auto; width: auto;
max-width: 100%; max-width: 100%;
} }
.input-group {
@include peertube-input-group(300px);
input {
flex: 1;
}
.dropdown-toggle::after {
margin-left: 0;
}
}

View File

@ -1,8 +1,9 @@
import { SortMeta } from 'primeng/api' import { SortMeta } from 'primeng/api'
import { Component, OnInit, ViewChild } from '@angular/core' import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Params, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core'
import { Account, DropdownAction } from '@app/shared/shared-main' import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction } from '@app/shared/shared-main'
import { UserBanModalComponent } from '@app/shared/shared-moderation' import { UserBanModalComponent } from '@app/shared/shared-moderation'
import { ServerConfig, User, UserRole } from '@shared/models' import { ServerConfig, User, UserRole } from '@shared/models'
@ -18,19 +19,28 @@ type UserForList = User & {
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: [ './user-list.component.scss' ] styleUrls: [ './user-list.component.scss' ]
}) })
export class UserListComponent extends RestTable implements OnInit { export class UserListComponent extends RestTable implements OnInit, AfterViewInit {
@ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
users: User[] = [] users: User[] = []
totalRecords = 0 totalRecords = 0
sort: SortMeta = { field: 'createdAt', order: 1 } sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
highlightBannedUsers = false highlightBannedUsers = false
selectedUsers: User[] = [] selectedUsers: User[] = []
bulkUserActions: DropdownAction<User[]>[][] = [] bulkUserActions: DropdownAction<User[]>[][] = []
columns: { id: string, label: string }[] columns: { id: string, label: string }[]
inputFilters: AdvancedInputFilter[] = [
{
queryParams: { 'search': 'banned:true' },
label: $localize`Banned users`
}
]
private _selectedColumns: string[] private _selectedColumns: string[]
private serverConfig: ServerConfig private serverConfig: ServerConfig
@ -117,6 +127,10 @@ export class UserListComponent extends RestTable implements OnInit {
this.columns.push({ id: 'lastLoginDate', label: 'Last login' }) this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
} }
ngAfterViewInit () {
if (this.search) this.setTableFilter(this.search, false)
}
getIdentifier () { getIdentifier () {
return 'UserListComponent' return 'UserListComponent'
} }

View File

@ -3,4 +3,4 @@
<ng-container i18n>Reports</ng-container> <ng-container i18n>Reports</ng-container>
</h1> </h1>
<my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table> <my-abuse-list-table viewType="user"></my-abuse-list-table>

View File

@ -19,12 +19,7 @@
</h1> </h1>
<div class="videos-header d-flex justify-content-between"> <div class="videos-header d-flex justify-content-between">
<div class="has-feedback has-clear"> <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
<input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch"
(ngModelChange)="onVideosSearchChanged()" />
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
<div class="peertube-select-container peertube-select-button"> <div class="peertube-select-container peertube-select-button">
<select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">

View File

@ -1,10 +1,11 @@
import { concat, Observable, Subject } from 'rxjs' import { concat, Observable } from 'rxjs'
import { debounceTime, tap, toArray } from 'rxjs/operators' import { tap, toArray } from 'rxjs/operators'
import { Component, OnInit, ViewChild } from '@angular/core' import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' import { AuthService, ComponentPagination, ConfirmService, Notifier, RouteFilter, ScreenService, ServerService, User } from '@app/core'
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
import { immutableAssign } from '@app/helpers' import { immutableAssign } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
@ -15,7 +16,7 @@ import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.c
templateUrl: './my-videos.component.html', templateUrl: './my-videos.component.html',
styleUrls: [ './my-videos.component.scss' ] styleUrls: [ './my-videos.component.scss' ]
}) })
export class MyVideosComponent implements OnInit, DisableForReuseHook { export class MyVideosComponent extends RouteFilter implements OnInit, AfterViewInit, DisableForReuseHook {
@ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
@ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
@ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
@ -40,13 +41,18 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
videoActions: DropdownAction<{ video: Video }>[] = [] videoActions: DropdownAction<{ video: Video }>[] = []
videos: Video[] = [] videos: Video[] = []
videosSearch: string
videosSearchChanged = new Subject<string>()
getVideosObservableFunction = this.getVideosObservable.bind(this) getVideosObservableFunction = this.getVideosObservable.bind(this)
sort: VideoSortField = '-publishedAt' sort: VideoSortField = '-publishedAt'
user: User user: User
inputFilters: AdvancedInputFilter[] = [
{
queryParams: { 'search': 'isLive:true' },
label: $localize`Only live videos`
}
]
constructor ( constructor (
protected router: Router, protected router: Router,
protected serverService: ServerService, protected serverService: ServerService,
@ -57,6 +63,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
private confirmService: ConfirmService, private confirmService: ConfirmService,
private videoService: VideoService private videoService: VideoService
) { ) {
super()
this.titlePage = $localize`My videos` this.titlePage = $localize`My videos`
} }
@ -65,20 +73,16 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
this.user = this.authService.getUser() this.user = this.authService.getUser()
this.videosSearchChanged this.initSearch()
.pipe(debounceTime(500)) this.listenToSearchChange()
.subscribe(() => { }
ngAfterViewInit () {
if (this.search) this.setTableFilter(this.search, false)
}
loadData () {
this.videosSelection.reloadVideos() this.videosSelection.reloadVideos()
})
}
resetSearch () {
this.videosSearch = ''
this.onVideosSearchChanged()
}
onVideosSearchChanged () {
this.videosSearchChanged.next()
} }
onChangeSortColumn () { onChangeSortColumn () {
@ -96,7 +100,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
getVideosObservable (page: number) { getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page }) const newPagination = immutableAssign(this.pagination, { currentPage: page })
return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch) return this.videoService.getMyVideos(newPagination, this.sort, this.search)
.pipe( .pipe(
tap(res => this.pagination.totalItems = res.total) tap(res => this.pagination.totalItems = res.total)
) )

View File

@ -1,14 +1,14 @@
import * as debug from 'debug' import * as debug from 'debug'
import { LazyLoadEvent, SortMeta } from 'primeng/api' import { LazyLoadEvent, SortMeta } from 'primeng/api'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators' import { ActivatedRoute, Router } from '@angular/router'
import { ActivatedRoute, Params, Router } from '@angular/router'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { RouteFilter } from '../routing'
import { RestPagination } from './rest-pagination' import { RestPagination } from './rest-pagination'
const logger = debug('peertube:tables:RestTable') const logger = debug('peertube:tables:RestTable')
export abstract class RestTable { export abstract class RestTable extends RouteFilter {
abstract totalRecords: number abstract totalRecords: number
abstract sort: SortMeta abstract sort: SortMeta
@ -19,8 +19,6 @@ export abstract class RestTable {
rowsPerPage = this.rowsPerPageOptions[0] rowsPerPage = this.rowsPerPageOptions[0]
expandedRows = {} expandedRows = {}
baseRoute: string
protected searchStream: Subject<string> protected searchStream: Subject<string>
protected route: ActivatedRoute protected route: ActivatedRoute
@ -66,55 +64,6 @@ export abstract class RestTable {
peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort)) peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
} }
initSearch () {
this.searchStream = new Subject()
this.searchStream
.pipe(
debounceTime(400),
distinctUntilChanged()
)
.subscribe(search => {
this.search = search
logger('On search %s.', this.search)
this.loadData()
})
}
onSearch (event: Event) {
const target = event.target as HTMLInputElement
this.searchStream.next(target.value)
this.setQueryParams((event.target as HTMLInputElement).value)
}
setQueryParams (search: string) {
if (!this.baseRoute) return
const queryParams: Params = {}
if (search) Object.assign(queryParams, { search })
this.router.navigate([ this.baseRoute ], { queryParams })
}
resetTableFilter () {
this.setTableFilter('')
this.setQueryParams('')
this.resetSearch()
}
listenToSearchChange () {
this.route.queryParams
.subscribe(params => {
this.search = params.search || ''
// Primeng table will run an event to load data
this.setTableFilter(this.search)
})
}
onPage (event: { first: number, rows: number }) { onPage (event: { first: number, rows: number }) {
logger('On page %o.', event) logger('On page %o.', event)
@ -131,21 +80,6 @@ export abstract class RestTable {
this.expandedRows = {} this.expandedRows = {}
} }
setTableFilter (filter: string, triggerEvent = true) {
// FIXME: cannot use ViewChild, so create a component for the filter input
const filterInput = document.getElementById('table-filter') as HTMLInputElement
if (!filterInput) return
filterInput.value = filter
if (triggerEvent) filterInput.dispatchEvent(new Event('keyup'))
}
resetSearch () {
this.searchStream.next('')
this.setTableFilter('')
}
protected abstract loadData (): void protected abstract loadData (): void
private getSortLocalStorageKey () { private getSortLocalStorageKey () {

View File

@ -5,6 +5,7 @@ export * from './login-guard.service'
export * from './menu-guard.service' export * from './menu-guard.service'
export * from './preload-selected-modules-list' export * from './preload-selected-modules-list'
export * from './redirect.service' export * from './redirect.service'
export * from './route-filter'
export * from './server-config-resolver.service' export * from './server-config-resolver.service'
export * from './unlogged-guard.service' export * from './unlogged-guard.service'
export * from './user-right-guard.service' export * from './user-right-guard.service'

View File

@ -0,0 +1,79 @@
import * as debug from 'debug'
import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { ActivatedRoute, Params, Router } from '@angular/router'
const logger = debug('peertube:tables:RouteFilter')
export abstract class RouteFilter {
search: string
protected searchStream: Subject<string>
protected route: ActivatedRoute
protected router: Router
initSearch () {
this.searchStream = new Subject()
this.searchStream
.pipe(
debounceTime(400),
distinctUntilChanged()
)
.subscribe(search => {
this.search = search
logger('On search %s.', this.search)
this.loadData()
})
}
onSearch (event: Event) {
const target = event.target as HTMLInputElement
this.searchStream.next(target.value)
this.setQueryParams(target.value)
}
resetTableFilter () {
this.setTableFilter('')
this.setQueryParams('')
this.resetSearch()
}
resetSearch () {
this.searchStream.next('')
this.setTableFilter('')
}
listenToSearchChange () {
this.route.queryParams
.subscribe(params => {
this.search = params.search || ''
// Primeng table will run an event to load data
this.setTableFilter(this.search)
})
}
setTableFilter (filter: string, triggerEvent = true) {
// FIXME: cannot use ViewChild, so create a component for the filter input
const filterInput = document.getElementById('table-filter') as HTMLInputElement
if (!filterInput) return
filterInput.value = filter
if (triggerEvent) filterInput.dispatchEvent(new Event('keyup'))
}
protected abstract loadData (): void
private setQueryParams (search: string) {
const queryParams: Params = {}
if (search) Object.assign(queryParams, { search })
this.router.navigate([ ], { queryParams })
}
}

View File

@ -7,7 +7,7 @@
<span class="col-3 moderation-expanded-label" i18n>Reporter</span> <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
<span class="col-9 moderation-expanded-text"> <span class="col-9 moderation-expanded-text">
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
class="chip" class="chip"
> >
<my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
@ -16,7 +16,7 @@
</div> </div>
</a> </a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
class="ml-auto text-muted abuse-details-links" i18n class="ml-auto text-muted abuse-details-links" i18n
> >
{abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@ -27,7 +27,7 @@
<div class="d-flex" *ngIf="abuse.flaggedAccount"> <div class="d-flex" *ngIf="abuse.flaggedAccount">
<span class="col-3 moderation-expanded-label" i18n>Reportee</span> <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
<span class="col-9 moderation-expanded-text"> <span class="col-9 moderation-expanded-text">
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
class="chip" class="chip"
> >
<my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar> <my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar>
@ -36,7 +36,7 @@
</div> </div>
</a> </a>
<a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
class="ml-auto text-muted abuse-details-links" i18n class="ml-auto text-muted abuse-details-links" i18n
> >
{abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@ -53,7 +53,7 @@
<div class="mt-3 d-flex"> <div class="mt-3 d-flex">
<span class="col-3 moderation-expanded-label"> <span class="col-3 moderation-expanded-label">
<ng-container i18n>Report</ng-container> <ng-container i18n>Report</ng-container>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a> <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
</span> </span>
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
</div> </div>
@ -61,7 +61,7 @@
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
<span class="col-3"></span> <span class="col-3"></span>
<span class="col-9"> <span class="col-9">
<a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]" <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '.' ]"
[queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
> >
<div>{{ reason.label }}</div> <div>{{ reason.label }}</div>

View File

@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { durationToString } from '@app/helpers' import { durationToString } from '@app/helpers'
import { Account } from '@app/shared/shared-main'
import { AbusePredefinedReasonsString } from '@shared/models' import { AbusePredefinedReasonsString } from '@shared/models'
import { ProcessedAbuse } from './processed-abuse.model' import { ProcessedAbuse } from './processed-abuse.model'
@ -12,7 +11,6 @@ import { ProcessedAbuse } from './processed-abuse.model'
export class AbuseDetailsComponent { export class AbuseDetailsComponent {
@Input() abuse: ProcessedAbuse @Input() abuse: ProcessedAbuse
@Input() isAdminView: boolean @Input() isAdminView: boolean
@Input() baseRoute: string
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }

View File

@ -8,28 +8,7 @@
<ng-template pTemplate="caption"> <ng-template pTemplate="caption">
<div class="caption"> <div class="caption">
<div class="ml-auto"> <div class="ml-auto">
<div class="input-group has-feedback has-clear"> <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
<div class="input-group-text" ngbDropdownToggle>
<span class="caret" aria-haspopup="menu" role="button"></span>
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
@ -171,7 +150,7 @@
<ng-template pTemplate="rowexpansion" let-abuse> <ng-template pTemplate="rowexpansion" let-abuse>
<tr> <tr>
<td class="expand-cell" colspan="8"> <td class="expand-cell" colspan="8">
<my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details> <my-abuse-details [abuse]="abuse" [isAdminView]="isAdminView()"></my-abuse-details>
</td> </td>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -14,6 +14,7 @@ import { AbuseState, AdminAbuse } from '@shared/models'
import { AbuseMessageModalComponent } from './abuse-message-modal.component' import { AbuseMessageModalComponent } from './abuse-message-modal.component'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component' import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { ProcessedAbuse } from './processed-abuse.model' import { ProcessedAbuse } from './processed-abuse.model'
import { AdvancedInputFilter } from '../shared-forms'
const logger = debug('peertube:moderation:AbuseListTableComponent') const logger = debug('peertube:moderation:AbuseListTableComponent')
@ -24,7 +25,6 @@ const logger = debug('peertube:moderation:AbuseListTableComponent')
}) })
export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit { export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
@Input() viewType: 'admin' | 'user' @Input() viewType: 'admin' | 'user'
@Input() baseRoute: string
@ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
@ -36,6 +36,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
abuseActions: DropdownAction<ProcessedAbuse>[][] = [] abuseActions: DropdownAction<ProcessedAbuse>[][] = []
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`
}
]
constructor ( constructor (
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected router: Router, protected router: Router,

View File

@ -98,7 +98,7 @@ export class ActorAvatarComponent {
jkl: 'gray', jkl: 'gray',
mno: 'yellow', mno: 'yellow',
pqr: 'orange', pqr: 'orange',
stv: 'red', stvu: 'red',
wxyz: 'dark-blue' wxyz: 'dark-blue'
} }

View File

@ -0,0 +1,22 @@
<div class="input-group has-feedback has-clear">
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
<div class="input-group-text" ngbDropdownToggle>
<span class="caret" aria-haspopup="menu" role="button"></span>
</div>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced filters</h6>
<a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
{{ filter.label }}
</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>

View File

@ -0,0 +1,10 @@
@import '_variables';
@import '_mixins';
input {
@include peertube-input-text(250px);
}
.input-group-text {
background-color: transparent;
}

View File

@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { Params } from '@angular/router'
export type AdvancedInputFilter = {
label: string
queryParams: Params
}
@Component({
selector: 'my-advanced-input-filter',
templateUrl: './advanced-input-filter.component.html',
styleUrls: [ './advanced-input-filter.component.scss' ]
})
export class AdvancedInputFilterComponent {
@Input() filters: AdvancedInputFilter[] = []
@Output() resetTableFilter = new EventEmitter<void>()
@Output() search = new EventEmitter<Event>()
onSearch (event: Event) {
this.search.emit(event)
}
onResetTableFilter () {
this.resetTableFilter.emit()
}
}

View File

@ -1,12 +1,14 @@
export * from './form-validator.service' export * from './advanced-input-filter.component'
export * from './form-reactive' export * from './form-reactive'
export * from './select' export * from './form-validator.service'
export * from './input-toggle-hidden.component' export * from './form-validator.service'
export * from './input-switch.component' export * from './input-switch.component'
export * from './input-toggle-hidden.component'
export * from './markdown-textarea.component' export * from './markdown-textarea.component'
export * from './peertube-checkbox.component' export * from './peertube-checkbox.component'
export * from './preview-upload.component' export * from './preview-upload.component'
export * from './reactive-file.component' export * from './reactive-file.component'
export * from './select'
export * from './shared-form.module'
export * from './textarea-autoresize.directive' export * from './textarea-autoresize.directive'
export * from './timestamp-input.component' export * from './timestamp-input.component'
export * from './shared-form.module'

View File

@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { SharedGlobalIconModule } from '../shared-icons' import { SharedGlobalIconModule } from '../shared-icons'
import { SharedMainModule } from '../shared-main/shared-main.module' import { SharedMainModule } from '../shared-main/shared-main.module'
import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
import { DynamicFormFieldComponent } from './dynamic-form-field.component' import { DynamicFormFieldComponent } from './dynamic-form-field.component'
import { FormValidatorService } from './form-validator.service' import { FormValidatorService } from './form-validator.service'
import { InputSwitchComponent } from './input-switch.component' import { InputSwitchComponent } from './input-switch.component'
@ -52,7 +53,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
SelectCheckboxComponent, SelectCheckboxComponent,
SelectCustomValueComponent, SelectCustomValueComponent,
DynamicFormFieldComponent DynamicFormFieldComponent,
AdvancedInputFilterComponent
], ],
exports: [ exports: [
@ -78,7 +81,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
SelectCheckboxComponent, SelectCheckboxComponent,
SelectCustomValueComponent, SelectCustomValueComponent,
DynamicFormFieldComponent DynamicFormFieldComponent,
AdvancedInputFilterComponent
], ],
providers: [ providers: [

View File

@ -124,7 +124,23 @@ export class VideoService implements VideosProvider {
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) params = this.restService.addRestGetParams(params, pagination, sort)
params = this.restService.addObjectParams(params, { search })
if (search) {
const filters = this.restService.parseQueryStringFilter(search, {
isLive: {
prefix: 'isLive:',
isBoolean: true,
handler: v => {
if (v === 'true') return v
if (v === 'false') return v
return undefined
}
}
})
params = this.restService.addObjectParams(params, filters)
}
return this.authHttp return this.authHttp
.get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params }) .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })

View File

@ -1,4 +1,4 @@
import { NSFWQuery, SearchTargetType } from '@shared/models' import { BooleanBothQuery, SearchTargetType } from '@shared/models'
export class AdvancedSearch { export class AdvancedSearch {
startDate: string // ISO 8601 startDate: string // ISO 8601
@ -7,7 +7,7 @@ export class AdvancedSearch {
originallyPublishedStartDate: string // ISO 8601 originallyPublishedStartDate: string // ISO 8601
originallyPublishedEndDate: string // ISO 8601 originallyPublishedEndDate: string // ISO 8601
nsfw: NSFWQuery nsfw: BooleanBothQuery
categoryOneOf: string categoryOneOf: string
@ -33,7 +33,7 @@ export class AdvancedSearch {
endDate?: string endDate?: string
originallyPublishedStartDate?: string originallyPublishedStartDate?: string
originallyPublishedEndDate?: string originallyPublishedEndDate?: string
nsfw?: NSFWQuery nsfw?: BooleanBothQuery
categoryOneOf?: string categoryOneOf?: string
licenceOneOf?: string licenceOneOf?: string
languageOneOf?: string languageOneOf?: string

View File

@ -9,6 +9,10 @@ input[type=button] {
border-radius: inherit; border-radius: inherit;
} }
p-table .p-datatable-header .caption {
margin-bottom: 15px;
}
// Taken from old nova light theme // Taken from old nova light theme
body .p-disabled { body .p-disabled {
@ -512,10 +516,6 @@ p-table {
.left-buttons { .left-buttons {
padding-left: 15px; padding-left: 15px;
} }
.input-group-text {
background-color: transparent;
}
} }
} }

View File

@ -1,9 +1,10 @@
import * as express from 'express' import * as express from 'express'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { VideosWithSearchCommonQuery } from '@shared/models'
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
import { getFormattedObjects } from '../../helpers/utils' import { getFormattedObjects } from '../../helpers/utils'
import { Hooks } from '../../lib/plugins/hooks'
import { JobQueue } from '../../lib/job-queue' import { JobQueue } from '../../lib/job-queue'
import { Hooks } from '../../lib/plugins/hooks'
import { import {
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
const account = res.locals.account const account = res.locals.account
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const query = req.query as VideosWithSearchCommonQuery
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
followerActorId, followerActorId,
start: req.query.start, start: query.start,
count: req.query.count, count: query.count,
sort: req.query.sort, sort: query.sort,
includeLocalVideos: true, includeLocalVideos: true,
categoryOneOf: req.query.categoryOneOf, categoryOneOf: query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf, licenceOneOf: query.licenceOneOf,
languageOneOf: req.query.languageOneOf, languageOneOf: query.languageOneOf,
tagsOneOf: req.query.tagsOneOf, tagsOneOf: query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf, tagsAllOf: query.tagsAllOf,
filter: req.query.filter, filter: query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw), isLive: query.isLive,
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
accountId: account.id, accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined, user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos, countVideos,
search: req.query.search search: query.search
}, 'filter:api.accounts.videos.list.params') }, 'filter:api.accounts.videos.list.params')
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(

View File

@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) {
start: req.query.start, start: req.query.start,
count: req.query.count, count: req.query.count,
sort: req.query.sort, sort: req.query.sort,
search: req.query.search search: req.query.search,
isLive: req.query.isLive
}, 'filter:api.user.me.videos.list.params') }, 'filter:api.user.me.videos.list.params')
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(

View File

@ -2,8 +2,8 @@ import 'multer'
import * as express from 'express' import * as express from 'express'
import { sendUndoFollow } from '@server/lib/activitypub/send' import { sendUndoFollow } from '@server/lib/activitypub/send'
import { VideoChannelModel } from '@server/models/video/video-channel' import { VideoChannelModel } from '@server/models/video/video-channel'
import { VideosCommonQuery } from '@shared/models'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
import { getFormattedObjects } from '../../../helpers/utils' import { getFormattedObjects } from '../../../helpers/utils'
import { WEBSERVER } from '../../../initializers/constants' import { WEBSERVER } from '../../../initializers/constants'
@ -170,19 +170,20 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const query = req.query as VideosCommonQuery
const resultList = await VideoModel.listForApi({ const resultList = await VideoModel.listForApi({
start: req.query.start, start: query.start,
count: req.query.count, count: query.count,
sort: req.query.sort, sort: query.sort,
includeLocalVideos: false, includeLocalVideos: false,
categoryOneOf: req.query.categoryOneOf, categoryOneOf: query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf, licenceOneOf: query.licenceOneOf,
languageOneOf: req.query.languageOneOf, languageOneOf: query.languageOneOf,
tagsOneOf: req.query.tagsOneOf, tagsOneOf: query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf, tagsAllOf: query.tagsAllOf,
nsfw: buildNSFWFilter(res, req.query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
filter: req.query.filter as VideoFilter, filter: query.filter,
withFiles: false, withFiles: false,
followerActorId: user.Account.Actor.id, followerActorId: user.Account.Actor.id,
user, user,

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { MChannelBannerAccountDefault } from '@server/types/models' import { MChannelBannerAccountDefault } from '@server/types/models'
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
import { resetSequelizeInstance } from '../../helpers/database-utils' import { resetSequelizeInstance } from '../../helpers/database-utils'
@ -312,20 +312,21 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
const videoChannelInstance = res.locals.videoChannel const videoChannelInstance = res.locals.videoChannel
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const query = req.query as VideosCommonQuery
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
followerActorId, followerActorId,
start: req.query.start, start: query.start,
count: req.query.count, count: query.count,
sort: req.query.sort, sort: query.sort,
includeLocalVideos: true, includeLocalVideos: true,
categoryOneOf: req.query.categoryOneOf, categoryOneOf: query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf, licenceOneOf: query.licenceOneOf,
languageOneOf: req.query.languageOneOf, languageOneOf: query.languageOneOf,
tagsOneOf: req.query.tagsOneOf, tagsOneOf: query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf, tagsAllOf: query.tagsAllOf,
filter: req.query.filter, filter: query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
videoChannelId: videoChannelInstance.id, videoChannelId: videoChannelInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined, user: res.locals.oauth ? res.locals.oauth.token.User : undefined,

View File

@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
@ -494,20 +493,22 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
} }
async function listVideos (req: express.Request, res: express.Response) { async function listVideos (req: express.Request, res: express.Response) {
const query = req.query as VideosCommonQuery
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
start: req.query.start, start: query.start,
count: req.query.count, count: query.count,
sort: req.query.sort, sort: query.sort,
includeLocalVideos: true, includeLocalVideos: true,
categoryOneOf: req.query.categoryOneOf, categoryOneOf: query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf, licenceOneOf: query.licenceOneOf,
languageOneOf: req.query.languageOneOf, languageOneOf: query.languageOneOf,
tagsOneOf: req.query.tagsOneOf, tagsOneOf: query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf, tagsAllOf: query.tagsAllOf,
nsfw: buildNSFWFilter(res, req.query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
filter: req.query.filter as VideoFilter, isLive: query.isLive,
filter: query.filter,
withFiles: false, withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined, user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos countVideos

View File

@ -11,7 +11,7 @@ function isStringArray (value: any) {
return isArray(value) && value.every(v => typeof v === 'string') return isArray(value) && value.every(v => typeof v === 'string')
} }
function isNSFWQueryValid (value: any) { function isBooleanBothQueryValid (value: any) {
return value === 'true' || value === 'false' || value === 'both' return value === 'true' || value === 'false' || value === 'both'
} }
@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) {
export { export {
isNumberArray, isNumberArray,
isStringArray, isStringArray,
isNSFWQueryValid, isBooleanBothQueryValid,
isSearchTargetValid isSearchTargetValid
} }

View File

@ -20,7 +20,7 @@ import {
toIntOrNull, toIntOrNull,
toValueOrNull toValueOrNull
} from '../../../helpers/custom-validators/misc' } from '../../../helpers/custom-validators/misc'
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
import { import {
isScheduleVideoUpdatePrivacyValid, isScheduleVideoUpdatePrivacyValid,
@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [
.custom(isStringArray).withMessage('Should have a valid all of tags array'), .custom(isStringArray).withMessage('Should have a valid all of tags array'),
query('nsfw') query('nsfw')
.optional() .optional()
.custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
query('isLive')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid live boolean'),
query('filter') query('filter')
.optional() .optional()
.custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),

View File

@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = {
start: number start: number
sort: string sort: string
filter?: VideoFilter
categoryOneOf?: number[]
nsfw?: boolean nsfw?: boolean
filter?: VideoFilter
isLive?: boolean
categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
tagsOneOf?: string[] tagsOneOf?: string[]
@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
if (options.nsfw === true) { if (options.nsfw === true) {
and.push('"video"."nsfw" IS TRUE') and.push('"video"."nsfw" IS TRUE')
} else if (options.nsfw === false) {
and.push('"video"."nsfw" IS FALSE')
} }
if (options.nsfw === false) { if (options.isLive === true) {
and.push('"video"."nsfw" IS FALSE') and.push('"video"."isLive" IS TRUE')
} else if (options.isLive === false) {
and.push('"video"."isLive" IS FALSE')
} }
if (options.categoryOneOf) { if (options.categoryOneOf) {

View File

@ -1021,14 +1021,28 @@ export class VideoModel extends Model {
start: number start: number
count: number count: number
sort: string sort: string
isLive?: boolean
search?: string search?: string
}) { }) {
const { accountId, start, count, sort, search } = options const { accountId, start, count, sort, search, isLive } = options
function buildBaseQuery (): FindOptions { function buildBaseQuery (): FindOptions {
let baseQuery = { const where: WhereOptions = {}
if (search) {
where.name = {
[Op.iLike]: '%' + search + '%'
}
}
if (isLive) {
where.isLive = isLive
}
const baseQuery = {
offset: start, offset: start,
limit: count, limit: count,
where,
order: getVideoSort(sort), order: getVideoSort(sort),
include: [ include: [
{ {
@ -1047,16 +1061,6 @@ export class VideoModel extends Model {
] ]
} }
if (search) {
baseQuery = Object.assign(baseQuery, {
where: {
name: {
[Op.iLike]: '%' + search + '%'
}
}
})
}
return baseQuery return baseQuery
} }
@ -1084,23 +1088,34 @@ export class VideoModel extends Model {
start: number start: number
count: number count: number
sort: string sort: string
nsfw: boolean nsfw: boolean
filter?: VideoFilter
isLive?: boolean
includeLocalVideos: boolean includeLocalVideos: boolean
withFiles: boolean withFiles: boolean
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
tagsOneOf?: string[] tagsOneOf?: string[]
tagsAllOf?: string[] tagsAllOf?: string[]
filter?: VideoFilter
accountId?: number accountId?: number
videoChannelId?: number videoChannelId?: number
followerActorId?: number followerActorId?: number
videoPlaylistId?: number videoPlaylistId?: number
trendingDays?: number trendingDays?: number
user?: MUserAccountId user?: MUserAccountId
historyOfUser?: MUserId historyOfUser?: MUserId
countVideos?: boolean countVideos?: boolean
search?: string search?: string
}) { }) {
if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@ -1128,6 +1143,7 @@ export class VideoModel extends Model {
followerActorId, followerActorId,
serverAccountId: serverActor.Account.id, serverAccountId: serverActor.Account.id,
nsfw: options.nsfw, nsfw: options.nsfw,
isLive: options.isLive,
categoryOneOf: options.categoryOneOf, categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf, licenceOneOf: options.licenceOneOf,
languageOneOf: options.languageOneOf, languageOneOf: options.languageOneOf,
@ -1160,6 +1176,7 @@ export class VideoModel extends Model {
originallyPublishedStartDate?: string originallyPublishedStartDate?: string
originallyPublishedEndDate?: string originallyPublishedEndDate?: string
nsfw?: boolean nsfw?: boolean
isLive?: boolean
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
@ -1171,23 +1188,32 @@ export class VideoModel extends Model {
filter?: VideoFilter filter?: VideoFilter
}) { }) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const queryOptions = { const queryOptions = {
followerActorId: serverActor.id, followerActorId: serverActor.id,
serverAccountId: serverActor.Account.id, serverAccountId: serverActor.Account.id,
includeLocalVideos: options.includeLocalVideos, includeLocalVideos: options.includeLocalVideos,
nsfw: options.nsfw, nsfw: options.nsfw,
isLive: options.isLive,
categoryOneOf: options.categoryOneOf, categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf, licenceOneOf: options.licenceOneOf,
languageOneOf: options.languageOneOf, languageOneOf: options.languageOneOf,
tagsOneOf: options.tagsOneOf, tagsOneOf: options.tagsOneOf,
tagsAllOf: options.tagsAllOf, tagsAllOf: options.tagsAllOf,
user: options.user, user: options.user,
filter: options.filter, filter: options.filter,
start: options.start, start: options.start,
count: options.count, count: options.count,
sort: options.sort, sort: options.sort,
startDate: options.startDate, startDate: options.startDate,
endDate: options.endDate, endDate: options.endDate,
originallyPublishedStartDate: options.originallyPublishedStartDate, originallyPublishedStartDate: options.originallyPublishedStartDate,
originallyPublishedEndDate: options.originallyPublishedEndDate, originallyPublishedEndDate: options.originallyPublishedEndDate,

View File

@ -19,10 +19,12 @@ import {
doubleFollow, doubleFollow,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getLive, getLive,
getMyVideosWithFilter,
getPlaylist, getPlaylist,
getVideo, getVideo,
getVideoIdFromUUID, getVideoIdFromUUID,
getVideosList, getVideosList,
getVideosWithFilters,
killallServers, killallServers,
makeRawRequest, makeRawRequest,
removeVideo, removeVideo,
@ -37,6 +39,7 @@ import {
testImage, testImage,
updateCustomSubConfig, updateCustomSubConfig,
updateLive, updateLive,
uploadVideoAndGetId,
viewVideo, viewVideo,
wait, wait,
waitJobs, waitJobs,
@ -229,6 +232,68 @@ describe('Test live', function () {
}) })
}) })
describe('Live filters', function () {
let command: any
let liveVideoId: string
let vodVideoId: string
before(async function () {
this.timeout(120000)
vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid
const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id }
const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions)
liveVideoId = resLive.body.video.uuid
command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
await waitUntilLivePublishedOnAllServers(liveVideoId)
await waitJobs(servers)
})
it('Should only display lives', async function () {
const res = await getVideosWithFilters(servers[0].url, { isLive: true })
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
expect(res.body.data[0].name).to.equal('live')
})
it('Should not display lives', async function () {
const res = await getVideosWithFilters(servers[0].url, { isLive: false })
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
expect(res.body.data[0].name).to.equal('vod video')
})
it('Should display my lives', async function () {
this.timeout(60000)
await stopFfmpeg(command)
await waitJobs(servers)
const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true })
const videos = res.body.data as Video[]
const result = videos.every(v => v.isLive)
expect(result).to.be.true
})
it('Should not display my lives', async function () {
const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false })
const videos = res.body.data as Video[]
const result = videos.every(v => !v.isLive)
expect(result).to.be.true
})
after(async function () {
await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId)
await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId)
})
})
describe('Stream checks', function () { describe('Stream checks', function () {
let liveVideo: LiveVideo & VideoDetails let liveVideo: LiveVideo & VideoDetails
let rtmpUrl: string let rtmpUrl: string

View File

@ -1,17 +1,24 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import * as chai from 'chai'
import 'mocha' import 'mocha'
import * as chai from 'chai'
import { VideoPrivacy } from '@shared/models'
import { import {
advancedVideosSearch, advancedVideosSearch,
cleanupTests, cleanupTests,
createLive,
flushAndRunServer, flushAndRunServer,
immutableAssign, immutableAssign,
searchVideo, searchVideo,
sendRTMPStreamInVideo,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
setDefaultVideoChannel,
stopFfmpeg,
updateCustomSubConfig,
uploadVideo, uploadVideo,
wait wait,
waitUntilLivePublished
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
@ -28,6 +35,7 @@ describe('Test videos search', function () {
server = await flushAndRunServer(1) server = await flushAndRunServer(1)
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
{ {
const attributes1 = { const attributes1 = {
@ -449,6 +457,43 @@ describe('Test videos search', function () {
expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3') expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
}) })
it('Should search by live', async function () {
this.timeout(30000)
{
const options = {
search: {
searchIndex: { enabled: false }
},
live: { enabled: true }
}
await updateCustomSubConfig(server.url, server.accessToken, options)
}
{
const res = await advancedVideosSearch(server.url, { isLive: true })
expect(res.body.total).to.equal(0)
expect(res.body.data).to.have.lengthOf(0)
}
{
const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id }
const resLive = await createLive(server.url, server.accessToken, liveOptions)
const liveVideoId = resLive.body.video.uuid
const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId)
await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
const res = await advancedVideosSearch(server.url, { isLive: true })
expect(res.body.total).to.equal(1)
expect(res.body.data[0].name).to.equal('live')
await stopFfmpeg(command)
}
})
after(async function () { after(async function () {
await cleanupTests([ server ]) await cleanupTests([ server ])
}) })

View File

@ -387,11 +387,11 @@ describe('Test a single server', function () {
}) })
it('Should filter by tags and category', async function () { it('Should filter by tags and category', async function () {
const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 }) const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
expect(res1.body.total).to.equal(1) expect(res1.body.total).to.equal(1)
expect(res1.body.data[0].name).to.equal('my super video updated') expect(res1.body.data[0].name).to.equal('my super video updated')
const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 }) const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
expect(res2.body.total).to.equal(0) expect(res2.body.total).to.equal(0)
}) })

View File

@ -8,6 +8,7 @@ import * as request from 'supertest'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import validator from 'validator' import validator from 'validator'
import { HttpStatusCode } from '@shared/core-utils' import { HttpStatusCode } from '@shared/core-utils'
import { VideosCommonQuery } from '@shared/models'
import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
import { VideoDetails, VideoPrivacy } from '../../models/videos' import { VideoDetails, VideoPrivacy } from '../../models/videos'
import { import {
@ -195,6 +196,18 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
const path = '/api/v1/users/me/videos'
return makeGetRequest({
url,
path,
token: accessToken,
query,
statusCodeExpected: HttpStatusCode.OK_200
})
}
function getAccountVideos ( function getAccountVideos (
url: string, url: string,
accessToken: string, accessToken: string,
@ -295,7 +308,7 @@ function getVideosListSort (url: string, sort: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { function getVideosWithFilters (url: string, query: VideosCommonQuery) {
const path = '/api/v1/videos' const path = '/api/v1/videos'
return request(url) return request(url)
@ -751,6 +764,7 @@ export {
completeVideoCheck, completeVideoCheck,
checkVideoFilesWereRemoved, checkVideoFilesWereRemoved,
getPlaylistVideos, getPlaylistVideos,
getMyVideosWithFilter,
uploadVideoAndGetId, uploadVideoAndGetId,
getLocalIdByUUID, getLocalIdByUUID,
getVideoIdFromUUID getVideoIdFromUUID

View File

@ -0,0 +1 @@
export type BooleanBothQuery = 'true' | 'false' | 'both'

View File

@ -1,4 +1,5 @@
export * from './nsfw-query.model' export * from './boolean-both-query.model'
export * from './search-target-query.model' export * from './search-target-query.model'
export * from './videos-common-query.model'
export * from './videos-search-query.model' export * from './videos-search-query.model'
export * from './video-channels-search-query.model' export * from './video-channels-search-query.model'

View File

@ -1 +0,0 @@
export type NSFWQuery = 'true' | 'false' | 'both'

View File

@ -0,0 +1,28 @@
import { VideoFilter } from '../videos'
import { BooleanBothQuery } from './boolean-both-query.model'
// These query parameters can be used with any endpoint that list videos
export interface VideosCommonQuery {
start?: number
count?: number
sort?: string
nsfw?: BooleanBothQuery
isLive?: boolean
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
filter?: VideoFilter
}
export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
search?: string
}

View File

@ -1,33 +1,15 @@
import { VideoFilter } from '../videos'
import { NSFWQuery } from './nsfw-query.model'
import { SearchTargetQuery } from './search-target-query.model' import { SearchTargetQuery } from './search-target-query.model'
import { VideosCommonQuery } from './videos-common-query.model'
export interface VideosSearchQuery extends SearchTargetQuery { export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery {
search?: string search?: string
start?: number
count?: number
sort?: string
startDate?: string // ISO 8601 startDate?: string // ISO 8601
endDate?: string // ISO 8601 endDate?: string // ISO 8601
originallyPublishedStartDate?: string // ISO 8601 originallyPublishedStartDate?: string // ISO 8601
originallyPublishedEndDate?: string // ISO 8601 originallyPublishedEndDate?: string // ISO 8601
nsfw?: NSFWQuery
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
durationMin?: number // seconds durationMin?: number // seconds
durationMax?: number // seconds durationMax?: number // seconds
filter?: VideoFilter
} }

View File

@ -210,6 +210,7 @@ paths:
parameters: parameters:
- $ref: '#/components/parameters/name' - $ref: '#/components/parameters/name'
- $ref: '#/components/parameters/categoryOneOf' - $ref: '#/components/parameters/categoryOneOf'
- $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf' - $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf' - $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
@ -781,6 +782,7 @@ paths:
- Videos - Videos
parameters: parameters:
- $ref: '#/components/parameters/categoryOneOf' - $ref: '#/components/parameters/categoryOneOf'
- $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf' - $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf' - $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
@ -1086,6 +1088,7 @@ paths:
- Video - Video
parameters: parameters:
- $ref: '#/components/parameters/categoryOneOf' - $ref: '#/components/parameters/categoryOneOf'
- $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf' - $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf' - $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
@ -2194,6 +2197,7 @@ paths:
parameters: parameters:
- $ref: '#/components/parameters/channelHandle' - $ref: '#/components/parameters/channelHandle'
- $ref: '#/components/parameters/categoryOneOf' - $ref: '#/components/parameters/categoryOneOf'
- $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf' - $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf' - $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
@ -2841,6 +2845,7 @@ paths:
schema: schema:
type: string type: string
- $ref: '#/components/parameters/categoryOneOf' - $ref: '#/components/parameters/categoryOneOf'
- $ref: '#/components/parameters/isLive'
- $ref: '#/components/parameters/tagsOneOf' - $ref: '#/components/parameters/tagsOneOf'
- $ref: '#/components/parameters/tagsAllOf' - $ref: '#/components/parameters/tagsAllOf'
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
@ -3809,6 +3814,13 @@ components:
description: The comment id description: The comment id
schema: schema:
type: integer type: integer
isLive:
name: isLive
in: query
required: false
description: whether or not the video is a live
schema:
type: boolean
categoryOneOf: categoryOneOf:
name: categoryOneOf name: categoryOneOf
in: query in: query