1
0
Fork 0

Add ability to filter my videos by live

This commit is contained in:
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>
</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">
<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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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-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"
>
<my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
@ -16,7 +16,7 @@
</div>
</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
>
{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:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
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:&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
>
{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>

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ export class ActorAvatarComponent {
jkl: 'gray',
mno: 'yellow',
pqr: 'orange',
stv: 'red',
stvu: 'red',
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 './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'

View file

@ -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: [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 './videos-common-query.model'
export * from './videos-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 { 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
}

View file

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