Add ability to filter my videos by live
This commit is contained in:
parent
dfcb6f50a6
commit
1fd61899ea
46 changed files with 569 additions and 336 deletions
|
@ -3,4 +3,4 @@
|
|||
<ng-container i18n>Reports</ng-container>
|
||||
</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>
|
||||
|
|
|
@ -13,25 +13,7 @@
|
|||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="ml-auto">
|
||||
<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 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>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { AfterViewInit, Component, OnInit } from '@angular/core'
|
|||
import { DomSanitizer } from '@angular/platform-browser'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
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 { VideoBlockService } from '@app/shared/shared-moderation'
|
||||
import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
|
||||
|
@ -24,6 +25,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
|
|||
|
||||
videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = []
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { 'search': 'type:auto' },
|
||||
label: $localize`Automatic blocks`
|
||||
},
|
||||
{
|
||||
queryParams: { 'search': 'type:manual' },
|
||||
label: $localize`Manual blocks`
|
||||
}
|
||||
]
|
||||
|
||||
constructor (
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
|
@ -111,25 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
|
|||
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 () {
|
||||
return 'VideoBlockListComponent'
|
||||
}
|
||||
|
|
|
@ -26,25 +26,7 @@
|
|||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<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 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>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { SortMeta } from 'primeng/api'
|
|||
import { AfterViewInit, Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
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 { BulkService } from '@app/shared/shared-moderation'
|
||||
import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
|
||||
|
@ -43,6 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
|
|||
selectedComments: VideoCommentAdmin[] = []
|
||||
bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = []
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { 'search': 'local:true' },
|
||||
label: $localize`Local comments`
|
||||
},
|
||||
{
|
||||
queryParams: { 'search': 'local:false' },
|
||||
label: $localize`Remote comments`
|
||||
}
|
||||
]
|
||||
|
||||
get authUser () {
|
||||
return this.auth.getUser()
|
||||
}
|
||||
|
|
|
@ -22,24 +22,7 @@
|
|||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<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 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>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ tr.banned > td {
|
|||
|
||||
.table-email {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
color: pvar(--mainForegroundColor);
|
||||
}
|
||||
|
||||
|
@ -28,14 +29,6 @@ tr.banned > td {
|
|||
margin-left: 0.1rem;
|
||||
}
|
||||
|
||||
.caption {
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
}
|
||||
|
||||
p-tableCheckbox {
|
||||
position: relative;
|
||||
top: -2.5px;
|
||||
|
@ -55,18 +48,7 @@ my-global-icon {
|
|||
|
||||
.progress {
|
||||
@include progressbar($small: true);
|
||||
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@include peertube-input-group(300px);
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
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 { ServerConfig, User, UserRole } from '@shared/models'
|
||||
|
||||
|
@ -18,19 +19,28 @@ type UserForList = User & {
|
|||
templateUrl: './user-list.component.html',
|
||||
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
|
||||
|
||||
users: User[] = []
|
||||
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
highlightBannedUsers = false
|
||||
|
||||
selectedUsers: User[] = []
|
||||
bulkUserActions: DropdownAction<User[]>[][] = []
|
||||
columns: { id: string, label: string }[]
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { 'search': 'banned:true' },
|
||||
label: $localize`Banned users`
|
||||
}
|
||||
]
|
||||
|
||||
private _selectedColumns: string[]
|
||||
private serverConfig: ServerConfig
|
||||
|
||||
|
@ -117,6 +127,10 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
if (this.search) this.setTableFilter(this.search, false)
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'UserListComponent'
|
||||
}
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
<ng-container i18n>Reports</ng-container>
|
||||
</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>
|
||||
|
|
|
@ -19,12 +19,7 @@
|
|||
</h1>
|
||||
|
||||
<div class="videos-header d-flex justify-content-between">
|
||||
<div class="has-feedback has-clear">
|
||||
<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>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
|
||||
|
||||
<div class="peertube-select-container peertube-select-button">
|
||||
<select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { concat, Observable, Subject } from 'rxjs'
|
||||
import { debounceTime, tap, toArray } from 'rxjs/operators'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { concat, Observable } from 'rxjs'
|
||||
import { tap, toArray } from 'rxjs/operators'
|
||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
|
||||
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 { immutableAssign } from '@app/helpers'
|
||||
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'
|
||||
|
@ -15,7 +16,7 @@ import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.c
|
|||
templateUrl: './my-videos.component.html',
|
||||
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('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
||||
@ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
|
||||
|
@ -40,13 +41,18 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
videoActions: DropdownAction<{ video: Video }>[] = []
|
||||
|
||||
videos: Video[] = []
|
||||
videosSearch: string
|
||||
videosSearchChanged = new Subject<string>()
|
||||
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
||||
sort: VideoSortField = '-publishedAt'
|
||||
|
||||
user: User
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = [
|
||||
{
|
||||
queryParams: { 'search': 'isLive:true' },
|
||||
label: $localize`Only live videos`
|
||||
}
|
||||
]
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
protected serverService: ServerService,
|
||||
|
@ -57,6 +63,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
private confirmService: ConfirmService,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.titlePage = $localize`My videos`
|
||||
}
|
||||
|
||||
|
@ -65,20 +73,16 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
|
||||
this.user = this.authService.getUser()
|
||||
|
||||
this.videosSearchChanged
|
||||
.pipe(debounceTime(500))
|
||||
.subscribe(() => {
|
||||
this.videosSelection.reloadVideos()
|
||||
})
|
||||
this.initSearch()
|
||||
this.listenToSearchChange()
|
||||
}
|
||||
|
||||
resetSearch () {
|
||||
this.videosSearch = ''
|
||||
this.onVideosSearchChanged()
|
||||
ngAfterViewInit () {
|
||||
if (this.search) this.setTableFilter(this.search, false)
|
||||
}
|
||||
|
||||
onVideosSearchChanged () {
|
||||
this.videosSearchChanged.next()
|
||||
loadData () {
|
||||
this.videosSelection.reloadVideos()
|
||||
}
|
||||
|
||||
onChangeSortColumn () {
|
||||
|
@ -96,7 +100,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
getVideosObservable (page: number) {
|
||||
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(
|
||||
tap(res => this.pagination.totalItems = res.total)
|
||||
)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import * as debug from 'debug'
|
||||
import { LazyLoadEvent, SortMeta } from 'primeng/api'
|
||||
import { Subject } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { RouteFilter } from '../routing'
|
||||
import { RestPagination } from './rest-pagination'
|
||||
|
||||
const logger = debug('peertube:tables:RestTable')
|
||||
|
||||
export abstract class RestTable {
|
||||
export abstract class RestTable extends RouteFilter {
|
||||
|
||||
abstract totalRecords: number
|
||||
abstract sort: SortMeta
|
||||
|
@ -19,8 +19,6 @@ export abstract class RestTable {
|
|||
rowsPerPage = this.rowsPerPageOptions[0]
|
||||
expandedRows = {}
|
||||
|
||||
baseRoute: string
|
||||
|
||||
protected searchStream: Subject<string>
|
||||
|
||||
protected route: ActivatedRoute
|
||||
|
@ -66,55 +64,6 @@ export abstract class RestTable {
|
|||
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 }) {
|
||||
logger('On page %o.', event)
|
||||
|
||||
|
@ -131,21 +80,6 @@ export abstract class RestTable {
|
|||
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
|
||||
|
||||
private getSortLocalStorageKey () {
|
||||
|
|
|
@ -5,6 +5,7 @@ export * from './login-guard.service'
|
|||
export * from './menu-guard.service'
|
||||
export * from './preload-selected-modules-list'
|
||||
export * from './redirect.service'
|
||||
export * from './route-filter'
|
||||
export * from './server-config-resolver.service'
|
||||
export * from './unlogged-guard.service'
|
||||
export * from './user-right-guard.service'
|
||||
|
|
79
client/src/app/core/routing/route-filter.ts
Normal file
79
client/src/app/core/routing/route-filter.ts
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
||||
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||
class="chip"
|
||||
>
|
||||
<my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||
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>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<div class="d-flex" *ngIf="abuse.flaggedAccount">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||
class="chip"
|
||||
>
|
||||
<my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar>
|
||||
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||
<a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||
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>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<div class="mt-3 d-flex">
|
||||
<span class="col-3 moderation-expanded-label">
|
||||
<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 class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
||||
<span class="col-3"></span>
|
||||
<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"
|
||||
>
|
||||
<div>{{ reason.label }}</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { durationToString } from '@app/helpers'
|
||||
import { Account } from '@app/shared/shared-main'
|
||||
import { AbusePredefinedReasonsString } from '@shared/models'
|
||||
import { ProcessedAbuse } from './processed-abuse.model'
|
||||
|
||||
|
@ -12,7 +11,6 @@ import { ProcessedAbuse } from './processed-abuse.model'
|
|||
export class AbuseDetailsComponent {
|
||||
@Input() abuse: ProcessedAbuse
|
||||
@Input() isAdminView: boolean
|
||||
@Input() baseRoute: string
|
||||
|
||||
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
|
||||
|
||||
|
|
|
@ -8,28 +8,7 @@
|
|||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="ml-auto">
|
||||
<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 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>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -171,7 +150,7 @@
|
|||
<ng-template pTemplate="rowexpansion" let-abuse>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { AbuseState, AdminAbuse } from '@shared/models'
|
|||
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
|
||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||
import { ProcessedAbuse } from './processed-abuse.model'
|
||||
import { AdvancedInputFilter } from '../shared-forms'
|
||||
|
||||
const logger = debug('peertube:moderation:AbuseListTableComponent')
|
||||
|
||||
|
@ -24,7 +25,6 @@ const logger = debug('peertube:moderation:AbuseListTableComponent')
|
|||
})
|
||||
export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
|
||||
@Input() viewType: 'admin' | 'user'
|
||||
@Input() baseRoute: string
|
||||
|
||||
@ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
|
||||
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
||||
|
@ -36,6 +36,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
|
|||
|
||||
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 (
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
|
|
|
@ -98,7 +98,7 @@ export class ActorAvatarComponent {
|
|||
jkl: 'gray',
|
||||
mno: 'yellow',
|
||||
pqr: 'orange',
|
||||
stv: 'red',
|
||||
stvu: 'red',
|
||||
wxyz: 'dark-blue'
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: transparent;
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
export * from './form-validator.service'
|
||||
export * from './advanced-input-filter.component'
|
||||
export * from './form-reactive'
|
||||
export * from './select'
|
||||
export * from './input-toggle-hidden.component'
|
||||
export * from './form-validator.service'
|
||||
export * from './form-validator.service'
|
||||
export * from './input-switch.component'
|
||||
export * from './input-toggle-hidden.component'
|
||||
export * from './markdown-textarea.component'
|
||||
export * from './peertube-checkbox.component'
|
||||
export * from './preview-upload.component'
|
||||
export * from './reactive-file.component'
|
||||
export * from './select'
|
||||
export * from './shared-form.module'
|
||||
export * from './textarea-autoresize.directive'
|
||||
export * from './timestamp-input.component'
|
||||
export * from './shared-form.module'
|
||||
|
|
|
@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { SharedGlobalIconModule } from '../shared-icons'
|
||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||
import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
|
||||
import { DynamicFormFieldComponent } from './dynamic-form-field.component'
|
||||
import { FormValidatorService } from './form-validator.service'
|
||||
import { InputSwitchComponent } from './input-switch.component'
|
||||
|
@ -52,7 +53,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
|
|||
SelectCheckboxComponent,
|
||||
SelectCustomValueComponent,
|
||||
|
||||
DynamicFormFieldComponent
|
||||
DynamicFormFieldComponent,
|
||||
|
||||
AdvancedInputFilterComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -78,7 +81,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
|
|||
SelectCheckboxComponent,
|
||||
SelectCustomValueComponent,
|
||||
|
||||
DynamicFormFieldComponent
|
||||
DynamicFormFieldComponent,
|
||||
|
||||
AdvancedInputFilterComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -124,7 +124,23 @@ export class VideoService implements VideosProvider {
|
|||
|
||||
let params = new HttpParams()
|
||||
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
|
||||
.get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NSFWQuery, SearchTargetType } from '@shared/models'
|
||||
import { BooleanBothQuery, SearchTargetType } from '@shared/models'
|
||||
|
||||
export class AdvancedSearch {
|
||||
startDate: string // ISO 8601
|
||||
|
@ -7,7 +7,7 @@ export class AdvancedSearch {
|
|||
originallyPublishedStartDate: string // ISO 8601
|
||||
originallyPublishedEndDate: string // ISO 8601
|
||||
|
||||
nsfw: NSFWQuery
|
||||
nsfw: BooleanBothQuery
|
||||
|
||||
categoryOneOf: string
|
||||
|
||||
|
@ -33,7 +33,7 @@ export class AdvancedSearch {
|
|||
endDate?: string
|
||||
originallyPublishedStartDate?: string
|
||||
originallyPublishedEndDate?: string
|
||||
nsfw?: NSFWQuery
|
||||
nsfw?: BooleanBothQuery
|
||||
categoryOneOf?: string
|
||||
licenceOneOf?: string
|
||||
languageOneOf?: string
|
||||
|
|
|
@ -9,6 +9,10 @@ input[type=button] {
|
|||
border-radius: inherit;
|
||||
}
|
||||
|
||||
p-table .p-datatable-header .caption {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
// Taken from old nova light theme
|
||||
|
||||
body .p-disabled {
|
||||
|
@ -512,10 +516,6 @@ p-table {
|
|||
.left-buttons {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as express from 'express'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { VideosWithSearchCommonQuery } from '@shared/models'
|
||||
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
||||
import { getFormattedObjects } from '../../helpers/utils'
|
||||
import { Hooks } from '../../lib/plugins/hooks'
|
||||
import { JobQueue } from '../../lib/job-queue'
|
||||
import { Hooks } from '../../lib/plugins/hooks'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
authenticate,
|
||||
|
@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
|
|||
const account = res.locals.account
|
||||
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
|
||||
const countVideos = getCountVideos(req)
|
||||
const query = req.query as VideosWithSearchCommonQuery
|
||||
|
||||
const apiOptions = await Hooks.wrapObject({
|
||||
followerActorId,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
start: query.start,
|
||||
count: query.count,
|
||||
sort: query.sort,
|
||||
includeLocalVideos: true,
|
||||
categoryOneOf: req.query.categoryOneOf,
|
||||
licenceOneOf: req.query.licenceOneOf,
|
||||
languageOneOf: req.query.languageOneOf,
|
||||
tagsOneOf: req.query.tagsOneOf,
|
||||
tagsAllOf: req.query.tagsAllOf,
|
||||
filter: req.query.filter,
|
||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||
categoryOneOf: query.categoryOneOf,
|
||||
licenceOneOf: query.licenceOneOf,
|
||||
languageOneOf: query.languageOneOf,
|
||||
tagsOneOf: query.tagsOneOf,
|
||||
tagsAllOf: query.tagsAllOf,
|
||||
filter: query.filter,
|
||||
isLive: query.isLive,
|
||||
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||
withFiles: false,
|
||||
accountId: account.id,
|
||||
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
|
||||
countVideos,
|
||||
search: req.query.search
|
||||
search: query.search
|
||||
}, 'filter:api.accounts.videos.list.params')
|
||||
|
||||
const resultList = await Hooks.wrapPromiseFun(
|
||||
|
|
|
@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) {
|
|||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
search: req.query.search
|
||||
search: req.query.search,
|
||||
isLive: req.query.isLive
|
||||
}, 'filter:api.user.me.videos.list.params')
|
||||
|
||||
const resultList = await Hooks.wrapPromiseFun(
|
||||
|
|
|
@ -2,8 +2,8 @@ import 'multer'
|
|||
import * as express from 'express'
|
||||
import { sendUndoFollow } from '@server/lib/activitypub/send'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
||||
import { VideosCommonQuery } from '@shared/models'
|
||||
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 { getFormattedObjects } from '../../../helpers/utils'
|
||||
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) {
|
||||
const user = res.locals.oauth.token.User
|
||||
const countVideos = getCountVideos(req)
|
||||
const query = req.query as VideosCommonQuery
|
||||
|
||||
const resultList = await VideoModel.listForApi({
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
start: query.start,
|
||||
count: query.count,
|
||||
sort: query.sort,
|
||||
includeLocalVideos: false,
|
||||
categoryOneOf: req.query.categoryOneOf,
|
||||
licenceOneOf: req.query.licenceOneOf,
|
||||
languageOneOf: req.query.languageOneOf,
|
||||
tagsOneOf: req.query.tagsOneOf,
|
||||
tagsAllOf: req.query.tagsAllOf,
|
||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||
filter: req.query.filter as VideoFilter,
|
||||
categoryOneOf: query.categoryOneOf,
|
||||
licenceOneOf: query.licenceOneOf,
|
||||
languageOneOf: query.languageOneOf,
|
||||
tagsOneOf: query.tagsOneOf,
|
||||
tagsAllOf: query.tagsAllOf,
|
||||
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||
filter: query.filter,
|
||||
withFiles: false,
|
||||
followerActorId: user.Account.Actor.id,
|
||||
user,
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as express from 'express'
|
|||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
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 { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
|
||||
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 followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
|
||||
const countVideos = getCountVideos(req)
|
||||
const query = req.query as VideosCommonQuery
|
||||
|
||||
const apiOptions = await Hooks.wrapObject({
|
||||
followerActorId,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
start: query.start,
|
||||
count: query.count,
|
||||
sort: query.sort,
|
||||
includeLocalVideos: true,
|
||||
categoryOneOf: req.query.categoryOneOf,
|
||||
licenceOneOf: req.query.licenceOneOf,
|
||||
languageOneOf: req.query.languageOneOf,
|
||||
tagsOneOf: req.query.tagsOneOf,
|
||||
tagsAllOf: req.query.tagsAllOf,
|
||||
filter: req.query.filter,
|
||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||
categoryOneOf: query.categoryOneOf,
|
||||
licenceOneOf: query.licenceOneOf,
|
||||
languageOneOf: query.languageOneOf,
|
||||
tagsOneOf: query.tagsOneOf,
|
||||
tagsAllOf: query.tagsAllOf,
|
||||
filter: query.filter,
|
||||
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||
withFiles: false,
|
||||
videoChannelId: videoChannelInstance.id,
|
||||
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
|
||||
|
|
|
@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
|
|||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
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 { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-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) {
|
||||
const query = req.query as VideosCommonQuery
|
||||
const countVideos = getCountVideos(req)
|
||||
|
||||
const apiOptions = await Hooks.wrapObject({
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
start: query.start,
|
||||
count: query.count,
|
||||
sort: query.sort,
|
||||
includeLocalVideos: true,
|
||||
categoryOneOf: req.query.categoryOneOf,
|
||||
licenceOneOf: req.query.licenceOneOf,
|
||||
languageOneOf: req.query.languageOneOf,
|
||||
tagsOneOf: req.query.tagsOneOf,
|
||||
tagsAllOf: req.query.tagsAllOf,
|
||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||
filter: req.query.filter as VideoFilter,
|
||||
categoryOneOf: query.categoryOneOf,
|
||||
licenceOneOf: query.licenceOneOf,
|
||||
languageOneOf: query.languageOneOf,
|
||||
tagsOneOf: query.tagsOneOf,
|
||||
tagsAllOf: query.tagsAllOf,
|
||||
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||
isLive: query.isLive,
|
||||
filter: query.filter,
|
||||
withFiles: false,
|
||||
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
|
||||
countVideos
|
||||
|
|
|
@ -11,7 +11,7 @@ function isStringArray (value: any) {
|
|||
return isArray(value) && value.every(v => typeof v === 'string')
|
||||
}
|
||||
|
||||
function isNSFWQueryValid (value: any) {
|
||||
function isBooleanBothQueryValid (value: any) {
|
||||
return value === 'true' || value === 'false' || value === 'both'
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) {
|
|||
export {
|
||||
isNumberArray,
|
||||
isStringArray,
|
||||
isNSFWQueryValid,
|
||||
isBooleanBothQueryValid,
|
||||
isSearchTargetValid
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
toIntOrNull,
|
||||
toValueOrNull
|
||||
} 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 {
|
||||
isScheduleVideoUpdatePrivacyValid,
|
||||
|
@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [
|
|||
.custom(isStringArray).withMessage('Should have a valid all of tags array'),
|
||||
query('nsfw')
|
||||
.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')
|
||||
.optional()
|
||||
.custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
|
||||
|
|
|
@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = {
|
|||
start: number
|
||||
sort: string
|
||||
|
||||
filter?: VideoFilter
|
||||
categoryOneOf?: number[]
|
||||
nsfw?: boolean
|
||||
filter?: VideoFilter
|
||||
isLive?: boolean
|
||||
|
||||
categoryOneOf?: number[]
|
||||
licenceOneOf?: number[]
|
||||
languageOneOf?: string[]
|
||||
tagsOneOf?: string[]
|
||||
|
@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
|
|||
|
||||
if (options.nsfw === true) {
|
||||
and.push('"video"."nsfw" IS TRUE')
|
||||
} else if (options.nsfw === false) {
|
||||
and.push('"video"."nsfw" IS FALSE')
|
||||
}
|
||||
|
||||
if (options.nsfw === false) {
|
||||
and.push('"video"."nsfw" IS FALSE')
|
||||
if (options.isLive === true) {
|
||||
and.push('"video"."isLive" IS TRUE')
|
||||
} else if (options.isLive === false) {
|
||||
and.push('"video"."isLive" IS FALSE')
|
||||
}
|
||||
|
||||
if (options.categoryOneOf) {
|
||||
|
|
|
@ -1021,14 +1021,28 @@ export class VideoModel extends Model {
|
|||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
isLive?: boolean
|
||||
search?: string
|
||||
}) {
|
||||
const { accountId, start, count, sort, search } = options
|
||||
const { accountId, start, count, sort, search, isLive } = options
|
||||
|
||||
function buildBaseQuery (): FindOptions {
|
||||
let baseQuery = {
|
||||
const where: WhereOptions = {}
|
||||
|
||||
if (search) {
|
||||
where.name = {
|
||||
[Op.iLike]: '%' + search + '%'
|
||||
}
|
||||
}
|
||||
|
||||
if (isLive) {
|
||||
where.isLive = isLive
|
||||
}
|
||||
|
||||
const baseQuery = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
where,
|
||||
order: getVideoSort(sort),
|
||||
include: [
|
||||
{
|
||||
|
@ -1047,16 +1061,6 @@ export class VideoModel extends Model {
|
|||
]
|
||||
}
|
||||
|
||||
if (search) {
|
||||
baseQuery = Object.assign(baseQuery, {
|
||||
where: {
|
||||
name: {
|
||||
[Op.iLike]: '%' + search + '%'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return baseQuery
|
||||
}
|
||||
|
||||
|
@ -1084,23 +1088,34 @@ export class VideoModel extends Model {
|
|||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
|
||||
nsfw: boolean
|
||||
filter?: VideoFilter
|
||||
isLive?: boolean
|
||||
|
||||
includeLocalVideos: boolean
|
||||
withFiles: boolean
|
||||
|
||||
categoryOneOf?: number[]
|
||||
licenceOneOf?: number[]
|
||||
languageOneOf?: string[]
|
||||
tagsOneOf?: string[]
|
||||
tagsAllOf?: string[]
|
||||
filter?: VideoFilter
|
||||
|
||||
accountId?: number
|
||||
videoChannelId?: number
|
||||
|
||||
followerActorId?: number
|
||||
|
||||
videoPlaylistId?: number
|
||||
|
||||
trendingDays?: number
|
||||
|
||||
user?: MUserAccountId
|
||||
historyOfUser?: MUserId
|
||||
|
||||
countVideos?: boolean
|
||||
|
||||
search?: string
|
||||
}) {
|
||||
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,
|
||||
serverAccountId: serverActor.Account.id,
|
||||
nsfw: options.nsfw,
|
||||
isLive: options.isLive,
|
||||
categoryOneOf: options.categoryOneOf,
|
||||
licenceOneOf: options.licenceOneOf,
|
||||
languageOneOf: options.languageOneOf,
|
||||
|
@ -1160,6 +1176,7 @@ export class VideoModel extends Model {
|
|||
originallyPublishedStartDate?: string
|
||||
originallyPublishedEndDate?: string
|
||||
nsfw?: boolean
|
||||
isLive?: boolean
|
||||
categoryOneOf?: number[]
|
||||
licenceOneOf?: number[]
|
||||
languageOneOf?: string[]
|
||||
|
@ -1171,23 +1188,32 @@ export class VideoModel extends Model {
|
|||
filter?: VideoFilter
|
||||
}) {
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const queryOptions = {
|
||||
followerActorId: serverActor.id,
|
||||
serverAccountId: serverActor.Account.id,
|
||||
|
||||
includeLocalVideos: options.includeLocalVideos,
|
||||
nsfw: options.nsfw,
|
||||
isLive: options.isLive,
|
||||
|
||||
categoryOneOf: options.categoryOneOf,
|
||||
licenceOneOf: options.licenceOneOf,
|
||||
languageOneOf: options.languageOneOf,
|
||||
|
||||
tagsOneOf: options.tagsOneOf,
|
||||
tagsAllOf: options.tagsAllOf,
|
||||
|
||||
user: options.user,
|
||||
filter: options.filter,
|
||||
|
||||
start: options.start,
|
||||
count: options.count,
|
||||
sort: options.sort,
|
||||
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
|
||||
originallyPublishedStartDate: options.originallyPublishedStartDate,
|
||||
originallyPublishedEndDate: options.originallyPublishedEndDate,
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@ import {
|
|||
doubleFollow,
|
||||
flushAndRunMultipleServers,
|
||||
getLive,
|
||||
getMyVideosWithFilter,
|
||||
getPlaylist,
|
||||
getVideo,
|
||||
getVideoIdFromUUID,
|
||||
getVideosList,
|
||||
getVideosWithFilters,
|
||||
killallServers,
|
||||
makeRawRequest,
|
||||
removeVideo,
|
||||
|
@ -37,6 +39,7 @@ import {
|
|||
testImage,
|
||||
updateCustomSubConfig,
|
||||
updateLive,
|
||||
uploadVideoAndGetId,
|
||||
viewVideo,
|
||||
wait,
|
||||
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 () {
|
||||
let liveVideo: LiveVideo & VideoDetails
|
||||
let rtmpUrl: string
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import * as chai from 'chai'
|
||||
import 'mocha'
|
||||
import * as chai from 'chai'
|
||||
import { VideoPrivacy } from '@shared/models'
|
||||
import {
|
||||
advancedVideosSearch,
|
||||
cleanupTests,
|
||||
createLive,
|
||||
flushAndRunServer,
|
||||
immutableAssign,
|
||||
searchVideo,
|
||||
sendRTMPStreamInVideo,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
updateCustomSubConfig,
|
||||
uploadVideo,
|
||||
wait
|
||||
wait,
|
||||
waitUntilLivePublished
|
||||
} from '../../../../shared/extra-utils'
|
||||
import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
|
||||
|
||||
|
@ -28,6 +35,7 @@ describe('Test videos search', function () {
|
|||
server = await flushAndRunServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
{
|
||||
const attributes1 = {
|
||||
|
@ -449,6 +457,43 @@ describe('Test videos search', function () {
|
|||
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 () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -387,11 +387,11 @@ describe('Test a single server', 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.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)
|
||||
})
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import * as request from 'supertest'
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
import validator from 'validator'
|
||||
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 { VideoDetails, VideoPrivacy } from '../../models/videos'
|
||||
import {
|
||||
|
@ -195,6 +196,18 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
|
|||
.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 (
|
||||
url: string,
|
||||
accessToken: string,
|
||||
|
@ -295,7 +308,7 @@ function getVideosListSort (url: string, sort: string) {
|
|||
.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'
|
||||
|
||||
return request(url)
|
||||
|
@ -751,6 +764,7 @@ export {
|
|||
completeVideoCheck,
|
||||
checkVideoFilesWereRemoved,
|
||||
getPlaylistVideos,
|
||||
getMyVideosWithFilter,
|
||||
uploadVideoAndGetId,
|
||||
getLocalIdByUUID,
|
||||
getVideoIdFromUUID
|
||||
|
|
1
shared/models/search/boolean-both-query.model.ts
Normal file
1
shared/models/search/boolean-both-query.model.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type BooleanBothQuery = 'true' | 'false' | 'both'
|
|
@ -1,4 +1,5 @@
|
|||
export * from './nsfw-query.model'
|
||||
export * from './boolean-both-query.model'
|
||||
export * from './search-target-query.model'
|
||||
export * from './videos-common-query.model'
|
||||
export * from './videos-search-query.model'
|
||||
export * from './video-channels-search-query.model'
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export type NSFWQuery = 'true' | 'false' | 'both'
|
28
shared/models/search/videos-common-query.model.ts
Normal file
28
shared/models/search/videos-common-query.model.ts
Normal 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
|
||||
}
|
|
@ -1,33 +1,15 @@
|
|||
import { VideoFilter } from '../videos'
|
||||
import { NSFWQuery } from './nsfw-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
|
||||
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
|
||||
startDate?: string // ISO 8601
|
||||
endDate?: string // ISO 8601
|
||||
|
||||
originallyPublishedStartDate?: string // ISO 8601
|
||||
originallyPublishedEndDate?: string // ISO 8601
|
||||
|
||||
nsfw?: NSFWQuery
|
||||
|
||||
categoryOneOf?: number[]
|
||||
|
||||
licenceOneOf?: number[]
|
||||
|
||||
languageOneOf?: string[]
|
||||
|
||||
tagsOneOf?: string[]
|
||||
tagsAllOf?: string[]
|
||||
|
||||
durationMin?: number // seconds
|
||||
durationMax?: number // seconds
|
||||
|
||||
filter?: VideoFilter
|
||||
}
|
||||
|
|
|
@ -210,6 +210,7 @@ paths:
|
|||
parameters:
|
||||
- $ref: '#/components/parameters/name'
|
||||
- $ref: '#/components/parameters/categoryOneOf'
|
||||
- $ref: '#/components/parameters/isLive'
|
||||
- $ref: '#/components/parameters/tagsOneOf'
|
||||
- $ref: '#/components/parameters/tagsAllOf'
|
||||
- $ref: '#/components/parameters/licenceOneOf'
|
||||
|
@ -781,6 +782,7 @@ paths:
|
|||
- Videos
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/categoryOneOf'
|
||||
- $ref: '#/components/parameters/isLive'
|
||||
- $ref: '#/components/parameters/tagsOneOf'
|
||||
- $ref: '#/components/parameters/tagsAllOf'
|
||||
- $ref: '#/components/parameters/licenceOneOf'
|
||||
|
@ -1086,6 +1088,7 @@ paths:
|
|||
- Video
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/categoryOneOf'
|
||||
- $ref: '#/components/parameters/isLive'
|
||||
- $ref: '#/components/parameters/tagsOneOf'
|
||||
- $ref: '#/components/parameters/tagsAllOf'
|
||||
- $ref: '#/components/parameters/licenceOneOf'
|
||||
|
@ -2194,6 +2197,7 @@ paths:
|
|||
parameters:
|
||||
- $ref: '#/components/parameters/channelHandle'
|
||||
- $ref: '#/components/parameters/categoryOneOf'
|
||||
- $ref: '#/components/parameters/isLive'
|
||||
- $ref: '#/components/parameters/tagsOneOf'
|
||||
- $ref: '#/components/parameters/tagsAllOf'
|
||||
- $ref: '#/components/parameters/licenceOneOf'
|
||||
|
@ -2841,6 +2845,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
- $ref: '#/components/parameters/categoryOneOf'
|
||||
- $ref: '#/components/parameters/isLive'
|
||||
- $ref: '#/components/parameters/tagsOneOf'
|
||||
- $ref: '#/components/parameters/tagsAllOf'
|
||||
- $ref: '#/components/parameters/licenceOneOf'
|
||||
|
@ -3809,6 +3814,13 @@ components:
|
|||
description: The comment id
|
||||
schema:
|
||||
type: integer
|
||||
isLive:
|
||||
name: isLive
|
||||
in: query
|
||||
required: false
|
||||
description: whether or not the video is a live
|
||||
schema:
|
||||
type: boolean
|
||||
categoryOneOf:
|
||||
name: categoryOneOf
|
||||
in: query
|
||||
|
|
Loading…
Reference in a new issue