aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.html20
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts31
-rw-r--r--client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html20
-rw-r--r--client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts12
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html19
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss22
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts22
-rw-r--r--client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html2
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html7
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts40
-rw-r--r--client/src/app/core/rest/rest-table.ts72
-rw-r--r--client/src/app/core/routing/index.ts1
-rw-r--r--client/src/app/core/routing/route-filter.ts79
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html12
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.ts2
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html25
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts25
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar.component.ts2
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.html22
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.scss10
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.ts27
-rw-r--r--client/src/app/shared/shared-forms/index.ts10
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts9
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts18
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts6
-rw-r--r--client/src/sass/primeng-custom.scss8
-rw-r--r--server/controllers/api/accounts.ts27
-rw-r--r--server/controllers/api/users/me.ts3
-rw-r--r--server/controllers/api/users/my-subscriptions.ts23
-rw-r--r--server/controllers/api/video-channel.ts23
-rw-r--r--server/controllers/api/videos/index.ts25
-rw-r--r--server/helpers/custom-validators/search.ts4
-rw-r--r--server/middlewares/validators/videos/videos.ts8
-rw-r--r--server/models/video/video-query-builder.ts12
-rw-r--r--server/models/video/video.ts52
-rw-r--r--server/tests/api/live/live.ts65
-rw-r--r--server/tests/api/search/search-videos.ts49
-rw-r--r--server/tests/api/videos/single-server.ts4
-rw-r--r--shared/extra-utils/videos/videos.ts16
-rw-r--r--shared/models/search/boolean-both-query.model.ts1
-rw-r--r--shared/models/search/index.ts3
-rw-r--r--shared/models/search/nsfw-query.model.ts1
-rw-r--r--shared/models/search/videos-common-query.model.ts28
-rw-r--r--shared/models/search/videos-search-query.model.ts22
-rw-r--r--support/doc/api/openapi.yaml12
46 files changed, 568 insertions, 335 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 9a6c124e1..a9e0931f8 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -3,4 +3,4 @@
3 <ng-container i18n>Reports</ng-container> 3 <ng-container i18n>Reports</ng-container>
4</h1> 4</h1>
5 5
6<my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-table> 6<my-abuse-list-table viewType="admin"></my-abuse-list-table>
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
index c7275de1b..cf2466bdb 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
@@ -13,25 +13,7 @@
13 <ng-template pTemplate="caption"> 13 <ng-template pTemplate="caption">
14 <div class="caption"> 14 <div class="caption">
15 <div class="ml-auto"> 15 <div class="ml-auto">
16 <div class="input-group has-feedback has-clear"> 16 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
17 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
18 <div class="input-group-text" ngbDropdownToggle>
19 <span class="caret" aria-haspopup="menu" role="button"></span>
20 </div>
21
22 <div role="menu" ngbDropdownMenu>
23 <h6 class="dropdown-header" i18n>Advanced block filters</h6>
24 <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:auto' }" class="dropdown-item" i18n>Automatic blocks</a>
25 <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:manual' }" class="dropdown-item" i18n>Manual blocks</a>
26 </div>
27 </div>
28 <input
29 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
30 (keyup)="onSearch($event)"
31 >
32 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
33 <span class="sr-only" i18n>Clear filters</span>
34 </div>
35 </div> 17 </div>
36 </div> 18 </div>
37 </ng-template> 19 </ng-template>
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index d6aca10e7..dfd8dc745 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -6,6 +6,7 @@ import { AfterViewInit, Component, OnInit } from '@angular/core'
6import { DomSanitizer } from '@angular/platform-browser' 6import { DomSanitizer } from '@angular/platform-browser'
7import { ActivatedRoute, Params, Router } from '@angular/router' 7import { ActivatedRoute, Params, Router } from '@angular/router'
8import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 8import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
9import { AdvancedInputFilter } from '@app/shared/shared-forms'
9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { VideoBlockService } from '@app/shared/shared-moderation' 11import { VideoBlockService } from '@app/shared/shared-moderation'
11import { VideoBlacklist, VideoBlacklistType } from '@shared/models' 12import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
@@ -24,6 +25,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
24 25
25 videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] 26 videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = []
26 27
28 inputFilters: AdvancedInputFilter[] = [
29 {
30 queryParams: { 'search': 'type:auto' },
31 label: $localize`Automatic blocks`
32 },
33 {
34 queryParams: { 'search': 'type:manual' },
35 label: $localize`Manual blocks`
36 }
37 ]
38
27 constructor ( 39 constructor (
28 protected route: ActivatedRoute, 40 protected route: ActivatedRoute,
29 protected router: Router, 41 protected router: Router,
@@ -111,25 +123,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
111 if (this.search) this.setTableFilter(this.search, false) 123 if (this.search) this.setTableFilter(this.search, false)
112 } 124 }
113 125
114 /* Table filter functions */
115 onBlockSearch (event: Event) {
116 this.onSearch(event)
117 this.setQueryParams((event.target as HTMLInputElement).value)
118 }
119
120 setQueryParams (search: string) {
121 const queryParams: Params = {}
122 if (search) Object.assign(queryParams, { search })
123 this.router.navigate([ '/admin/moderation/video-blocks/list' ], { queryParams })
124 }
125
126 resetTableFilter () {
127 this.setTableFilter('')
128 this.setQueryParams('')
129 this.resetSearch()
130 }
131 /* END Table filter functions */
132
133 getIdentifier () { 126 getIdentifier () {
134 return 'VideoBlockListComponent' 127 return 'VideoBlockListComponent'
135 } 128 }
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
index b6cec9c51..5cc0ff137 100644
--- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
+++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
@@ -26,25 +26,7 @@
26 </div> 26 </div>
27 27
28 <div class="ml-auto"> 28 <div class="ml-auto">
29 <div class="input-group has-feedback has-clear"> 29 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
30 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
31 <div class="input-group-text" ngbDropdownToggle>
32 <span class="caret" aria-haspopup="menu" role="button"></span>
33 </div>
34
35 <div role="menu" ngbDropdownMenu>
36 <h6 class="dropdown-header" i18n>Advanced comments filters</h6>
37 <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:true' }" class="dropdown-item" i18n>Local comments</a>
38 <a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:false' }" class="dropdown-item" i18n>Remote comments</a>
39 </div>
40 </div>
41 <input
42 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
43 (keyup)="onSearch($event)"
44 >
45 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
46 <span class="sr-only" i18n>Clear filters</span>
47 </div>
48 </div> 30 </div>
49 </div> 31 </div>
50 </ng-template> 32 </ng-template>
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
index 63493d00d..ebbbddb43 100644
--- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
+++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts
@@ -2,6 +2,7 @@ import { SortMeta } from 'primeng/api'
2import { AfterViewInit, Component, OnInit } from '@angular/core' 2import { AfterViewInit, Component, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 4import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
5import { AdvancedInputFilter } from '@app/shared/shared-forms'
5import { DropdownAction } from '@app/shared/shared-main' 6import { DropdownAction } from '@app/shared/shared-main'
6import { BulkService } from '@app/shared/shared-moderation' 7import { BulkService } from '@app/shared/shared-moderation'
7import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
@@ -43,6 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte
43 selectedComments: VideoCommentAdmin[] = [] 44 selectedComments: VideoCommentAdmin[] = []
44 bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] 45 bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = []
45 46
47 inputFilters: AdvancedInputFilter[] = [
48 {
49 queryParams: { 'search': 'local:true' },
50 label: $localize`Local comments`
51 },
52 {
53 queryParams: { 'search': 'local:false' },
54 label: $localize`Remote comments`
55 }
56 ]
57
46 get authUser () { 58 get authUser () {
47 return this.auth.getUser() 59 return this.auth.getUser()
48 } 60 }
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index f84d3fd0c..7170d7019 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -22,24 +22,7 @@
22 </div> 22 </div>
23 23
24 <div class="ml-auto"> 24 <div class="ml-auto">
25 <div class="input-group has-feedback has-clear"> 25 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
26 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
27 <div class="input-group-text" ngbDropdownToggle>
28 <span class="caret" aria-haspopup="menu" role="button"></span>
29 </div>
30
31 <div role="menu" ngbDropdownMenu>
32 <h6 class="dropdown-header" i18n>Advanced user filters</h6>
33 <a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a>
34 </div>
35 </div>
36 <input
37 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
38 (keyup)="onSearch($event)"
39 >
40 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
41 <span class="sr-only" i18n>Clear filters</span>
42 </div>
43 </div> 26 </div>
44 27
45 </div> 28 </div>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index f18747ec3..db4979a51 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -11,6 +11,7 @@ tr.banned > td {
11 11
12.table-email { 12.table-email {
13 @include disable-default-a-behaviour; 13 @include disable-default-a-behaviour;
14
14 color: pvar(--mainForegroundColor); 15 color: pvar(--mainForegroundColor);
15} 16}
16 17
@@ -28,14 +29,6 @@ tr.banned > td {
28 margin-left: 0.1rem; 29 margin-left: 0.1rem;
29} 30}
30 31
31.caption {
32 justify-content: space-between;
33
34 input {
35 @include peertube-input-text(250px);
36 }
37}
38
39p-tableCheckbox { 32p-tableCheckbox {
40 position: relative; 33 position: relative;
41 top: -2.5px; 34 top: -2.5px;
@@ -55,18 +48,7 @@ my-global-icon {
55 48
56.progress { 49.progress {
57 @include progressbar($small: true); 50 @include progressbar($small: true);
51
58 width: auto; 52 width: auto;
59 max-width: 100%; 53 max-width: 100%;
60} 54}
61
62.input-group {
63 @include peertube-input-group(300px);
64
65 input {
66 flex: 1;
67 }
68
69 .dropdown-toggle::after {
70 margin-left: 0;
71 }
72}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 339e18206..435bc17d7 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -1,8 +1,9 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Params, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' 4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core'
5import { Account, DropdownAction } from '@app/shared/shared-main' 5import { AdvancedInputFilter } from '@app/shared/shared-forms'
6import { DropdownAction } from '@app/shared/shared-main'
6import { UserBanModalComponent } from '@app/shared/shared-moderation' 7import { UserBanModalComponent } from '@app/shared/shared-moderation'
7import { ServerConfig, User, UserRole } from '@shared/models' 8import { ServerConfig, User, UserRole } from '@shared/models'
8 9
@@ -18,19 +19,28 @@ type UserForList = User & {
18 templateUrl: './user-list.component.html', 19 templateUrl: './user-list.component.html',
19 styleUrls: [ './user-list.component.scss' ] 20 styleUrls: [ './user-list.component.scss' ]
20}) 21})
21export class UserListComponent extends RestTable implements OnInit { 22export class UserListComponent extends RestTable implements OnInit, AfterViewInit {
22 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent 23 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
23 24
24 users: User[] = [] 25 users: User[] = []
26
25 totalRecords = 0 27 totalRecords = 0
26 sort: SortMeta = { field: 'createdAt', order: 1 } 28 sort: SortMeta = { field: 'createdAt', order: 1 }
27 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 29 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
30
28 highlightBannedUsers = false 31 highlightBannedUsers = false
29 32
30 selectedUsers: User[] = [] 33 selectedUsers: User[] = []
31 bulkUserActions: DropdownAction<User[]>[][] = [] 34 bulkUserActions: DropdownAction<User[]>[][] = []
32 columns: { id: string, label: string }[] 35 columns: { id: string, label: string }[]
33 36
37 inputFilters: AdvancedInputFilter[] = [
38 {
39 queryParams: { 'search': 'banned:true' },
40 label: $localize`Banned users`
41 }
42 ]
43
34 private _selectedColumns: string[] 44 private _selectedColumns: string[]
35 private serverConfig: ServerConfig 45 private serverConfig: ServerConfig
36 46
@@ -117,6 +127,10 @@ export class UserListComponent extends RestTable implements OnInit {
117 this.columns.push({ id: 'lastLoginDate', label: 'Last login' }) 127 this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
118 } 128 }
119 129
130 ngAfterViewInit () {
131 if (this.search) this.setTableFilter(this.search, false)
132 }
133
120 getIdentifier () { 134 getIdentifier () {
121 return 'UserListComponent' 135 return 'UserListComponent'
122 } 136 }
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
index 59ca61be6..e83b59019 100644
--- a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
+++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
@@ -3,4 +3,4 @@
3 <ng-container i18n>Reports</ng-container> 3 <ng-container i18n>Reports</ng-container>
4</h1> 4</h1>
5 5
6<my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table> 6<my-abuse-list-table viewType="user"></my-abuse-list-table>
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index e9f436378..7c1cdb511 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -19,12 +19,7 @@
19</h1> 19</h1>
20 20
21<div class="videos-header d-flex justify-content-between"> 21<div class="videos-header d-flex justify-content-between">
22 <div class="has-feedback has-clear"> 22 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
23 <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch"
24 (ngModelChange)="onVideosSearchChanged()" />
25 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
26 <span class="sr-only" i18n>Clear filters</span>
27 </div>
28 23
29 <div class="peertube-select-container peertube-select-button"> 24 <div class="peertube-select-container peertube-select-button">
30 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> 25 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index 356e158d6..f9c1b32b0 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -1,10 +1,11 @@
1import { concat, Observable, Subject } from 'rxjs' 1import { concat, Observable } from 'rxjs'
2import { debounceTime, tap, toArray } from 'rxjs/operators' 2import { tap, toArray } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' 5import { AuthService, ComponentPagination, ConfirmService, Notifier, RouteFilter, ScreenService, ServerService, User } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers' 7import { immutableAssign } from '@app/helpers'
8import { AdvancedInputFilter } from '@app/shared/shared-forms'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
10import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' 11import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
@@ -15,7 +16,7 @@ import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.c
15 templateUrl: './my-videos.component.html', 16 templateUrl: './my-videos.component.html',
16 styleUrls: [ './my-videos.component.scss' ] 17 styleUrls: [ './my-videos.component.scss' ]
17}) 18})
18export class MyVideosComponent implements OnInit, DisableForReuseHook { 19export class MyVideosComponent extends RouteFilter implements OnInit, AfterViewInit, DisableForReuseHook {
19 @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent 20 @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
20 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent 21 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
21 @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent 22 @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
@@ -40,13 +41,18 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
40 videoActions: DropdownAction<{ video: Video }>[] = [] 41 videoActions: DropdownAction<{ video: Video }>[] = []
41 42
42 videos: Video[] = [] 43 videos: Video[] = []
43 videosSearch: string
44 videosSearchChanged = new Subject<string>()
45 getVideosObservableFunction = this.getVideosObservable.bind(this) 44 getVideosObservableFunction = this.getVideosObservable.bind(this)
46 sort: VideoSortField = '-publishedAt' 45 sort: VideoSortField = '-publishedAt'
47 46
48 user: User 47 user: User
49 48
49 inputFilters: AdvancedInputFilter[] = [
50 {
51 queryParams: { 'search': 'isLive:true' },
52 label: $localize`Only live videos`
53 }
54 ]
55
50 constructor ( 56 constructor (
51 protected router: Router, 57 protected router: Router,
52 protected serverService: ServerService, 58 protected serverService: ServerService,
@@ -57,6 +63,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
57 private confirmService: ConfirmService, 63 private confirmService: ConfirmService,
58 private videoService: VideoService 64 private videoService: VideoService
59 ) { 65 ) {
66 super()
67
60 this.titlePage = $localize`My videos` 68 this.titlePage = $localize`My videos`
61 } 69 }
62 70
@@ -65,20 +73,16 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
65 73
66 this.user = this.authService.getUser() 74 this.user = this.authService.getUser()
67 75
68 this.videosSearchChanged 76 this.initSearch()
69 .pipe(debounceTime(500)) 77 this.listenToSearchChange()
70 .subscribe(() => {
71 this.videosSelection.reloadVideos()
72 })
73 } 78 }
74 79
75 resetSearch () { 80 ngAfterViewInit () {
76 this.videosSearch = '' 81 if (this.search) this.setTableFilter(this.search, false)
77 this.onVideosSearchChanged()
78 } 82 }
79 83
80 onVideosSearchChanged () { 84 loadData () {
81 this.videosSearchChanged.next() 85 this.videosSelection.reloadVideos()
82 } 86 }
83 87
84 onChangeSortColumn () { 88 onChangeSortColumn () {
@@ -96,7 +100,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
96 getVideosObservable (page: number) { 100 getVideosObservable (page: number) {
97 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 101 const newPagination = immutableAssign(this.pagination, { currentPage: page })
98 102
99 return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch) 103 return this.videoService.getMyVideos(newPagination, this.sort, this.search)
100 .pipe( 104 .pipe(
101 tap(res => this.pagination.totalItems = res.total) 105 tap(res => this.pagination.totalItems = res.total)
102 ) 106 )
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts
index 32c1db446..9baab8a39 100644
--- a/client/src/app/core/rest/rest-table.ts
+++ b/client/src/app/core/rest/rest-table.ts
@@ -1,14 +1,14 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { LazyLoadEvent, SortMeta } from 'primeng/api' 2import { LazyLoadEvent, SortMeta } from 'primeng/api'
3import { Subject } from 'rxjs' 3import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 4import { ActivatedRoute, Router } from '@angular/router'
5import { ActivatedRoute, Params, Router } from '@angular/router'
6import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 5import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
6import { RouteFilter } from '../routing'
7import { RestPagination } from './rest-pagination' 7import { RestPagination } from './rest-pagination'
8 8
9const logger = debug('peertube:tables:RestTable') 9const logger = debug('peertube:tables:RestTable')
10 10
11export abstract class RestTable { 11export abstract class RestTable extends RouteFilter {
12 12
13 abstract totalRecords: number 13 abstract totalRecords: number
14 abstract sort: SortMeta 14 abstract sort: SortMeta
@@ -19,8 +19,6 @@ export abstract class RestTable {
19 rowsPerPage = this.rowsPerPageOptions[0] 19 rowsPerPage = this.rowsPerPageOptions[0]
20 expandedRows = {} 20 expandedRows = {}
21 21
22 baseRoute: string
23
24 protected searchStream: Subject<string> 22 protected searchStream: Subject<string>
25 23
26 protected route: ActivatedRoute 24 protected route: ActivatedRoute
@@ -66,55 +64,6 @@ export abstract class RestTable {
66 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort)) 64 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
67 } 65 }
68 66
69 initSearch () {
70 this.searchStream = new Subject()
71
72 this.searchStream
73 .pipe(
74 debounceTime(400),
75 distinctUntilChanged()
76 )
77 .subscribe(search => {
78 this.search = search
79
80 logger('On search %s.', this.search)
81
82 this.loadData()
83 })
84 }
85
86 onSearch (event: Event) {
87 const target = event.target as HTMLInputElement
88 this.searchStream.next(target.value)
89
90 this.setQueryParams((event.target as HTMLInputElement).value)
91 }
92
93 setQueryParams (search: string) {
94 if (!this.baseRoute) return
95
96 const queryParams: Params = {}
97
98 if (search) Object.assign(queryParams, { search })
99 this.router.navigate([ this.baseRoute ], { queryParams })
100 }
101
102 resetTableFilter () {
103 this.setTableFilter('')
104 this.setQueryParams('')
105 this.resetSearch()
106 }
107
108 listenToSearchChange () {
109 this.route.queryParams
110 .subscribe(params => {
111 this.search = params.search || ''
112
113 // Primeng table will run an event to load data
114 this.setTableFilter(this.search)
115 })
116 }
117
118 onPage (event: { first: number, rows: number }) { 67 onPage (event: { first: number, rows: number }) {
119 logger('On page %o.', event) 68 logger('On page %o.', event)
120 69
@@ -131,21 +80,6 @@ export abstract class RestTable {
131 this.expandedRows = {} 80 this.expandedRows = {}
132 } 81 }
133 82
134 setTableFilter (filter: string, triggerEvent = true) {
135 // FIXME: cannot use ViewChild, so create a component for the filter input
136 const filterInput = document.getElementById('table-filter') as HTMLInputElement
137 if (!filterInput) return
138
139 filterInput.value = filter
140
141 if (triggerEvent) filterInput.dispatchEvent(new Event('keyup'))
142 }
143
144 resetSearch () {
145 this.searchStream.next('')
146 this.setTableFilter('')
147 }
148
149 protected abstract loadData (): void 83 protected abstract loadData (): void
150 84
151 private getSortLocalStorageKey () { 85 private getSortLocalStorageKey () {
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index 239c27caf..d53a4ae2c 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -5,6 +5,7 @@ export * from './login-guard.service'
5export * from './menu-guard.service' 5export * from './menu-guard.service'
6export * from './preload-selected-modules-list' 6export * from './preload-selected-modules-list'
7export * from './redirect.service' 7export * from './redirect.service'
8export * from './route-filter'
8export * from './server-config-resolver.service' 9export * from './server-config-resolver.service'
9export * from './unlogged-guard.service' 10export * from './unlogged-guard.service'
10export * from './user-right-guard.service' 11export * from './user-right-guard.service'
diff --git a/client/src/app/core/routing/route-filter.ts b/client/src/app/core/routing/route-filter.ts
new file mode 100644
index 000000000..f783a0c40
--- /dev/null
+++ b/client/src/app/core/routing/route-filter.ts
@@ -0,0 +1,79 @@
1import * as debug from 'debug'
2import { Subject } from 'rxjs'
3import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
4import { ActivatedRoute, Params, Router } from '@angular/router'
5
6const logger = debug('peertube:tables:RouteFilter')
7
8export abstract class RouteFilter {
9 search: string
10
11 protected searchStream: Subject<string>
12
13 protected route: ActivatedRoute
14 protected router: Router
15
16 initSearch () {
17 this.searchStream = new Subject()
18
19 this.searchStream
20 .pipe(
21 debounceTime(400),
22 distinctUntilChanged()
23 )
24 .subscribe(search => {
25 this.search = search
26
27 logger('On search %s.', this.search)
28
29 this.loadData()
30 })
31 }
32
33 onSearch (event: Event) {
34 const target = event.target as HTMLInputElement
35 this.searchStream.next(target.value)
36
37 this.setQueryParams(target.value)
38 }
39
40 resetTableFilter () {
41 this.setTableFilter('')
42 this.setQueryParams('')
43 this.resetSearch()
44 }
45
46 resetSearch () {
47 this.searchStream.next('')
48 this.setTableFilter('')
49 }
50
51 listenToSearchChange () {
52 this.route.queryParams
53 .subscribe(params => {
54 this.search = params.search || ''
55
56 // Primeng table will run an event to load data
57 this.setTableFilter(this.search)
58 })
59 }
60
61 setTableFilter (filter: string, triggerEvent = true) {
62 // FIXME: cannot use ViewChild, so create a component for the filter input
63 const filterInput = document.getElementById('table-filter') as HTMLInputElement
64 if (!filterInput) return
65
66 filterInput.value = filter
67
68 if (triggerEvent) filterInput.dispatchEvent(new Event('keyup'))
69 }
70
71 protected abstract loadData (): void
72
73 private setQueryParams (search: string) {
74 const queryParams: Params = {}
75
76 if (search) Object.assign(queryParams, { search })
77 this.router.navigate([ ], { queryParams })
78 }
79}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index f2eaeb32f..110f574fa 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -7,7 +7,7 @@
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span> 7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8 8
9 <span class="col-9 moderation-expanded-text"> 9 <span class="col-9 moderation-expanded-text">
10 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 10 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip" 11 class="chip"
12 > 12 >
13 <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> 13 <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar>
@@ -16,7 +16,7 @@
16 </div> 16 </div>
17 </a> 17 </a>
18 18
19 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 19 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
20 class="ml-auto text-muted abuse-details-links" i18n 20 class="ml-auto text-muted abuse-details-links" i18n
21 > 21 >
22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@@ -27,7 +27,7 @@
27 <div class="d-flex" *ngIf="abuse.flaggedAccount"> 27 <div class="d-flex" *ngIf="abuse.flaggedAccount">
28 <span class="col-3 moderation-expanded-label" i18n>Reportee</span> 28 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
29 <span class="col-9 moderation-expanded-text"> 29 <span class="col-9 moderation-expanded-text">
30 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 30 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
31 class="chip" 31 class="chip"
32 > 32 >
33 <my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar> 33 <my-actor-avatar [account]="abuse.flaggedAccount"></my-actor-avatar>
@@ -36,7 +36,7 @@
36 </div> 36 </div>
37 </a> 37 </a>
38 38
39 <a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 39 <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
40 class="ml-auto text-muted abuse-details-links" i18n 40 class="ml-auto text-muted abuse-details-links" i18n
41 > 41 >
42 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 42 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@@ -53,7 +53,7 @@
53 <div class="mt-3 d-flex"> 53 <div class="mt-3 d-flex">
54 <span class="col-3 moderation-expanded-label"> 54 <span class="col-3 moderation-expanded-label">
55 <ng-container i18n>Report</ng-container> 55 <ng-container i18n>Report</ng-container>
56 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a> 56 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
57 </span> 57 </span>
58 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> 58 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
59 </div> 59 </div>
@@ -61,7 +61,7 @@
61 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> 61 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
62 <span class="col-3"></span> 62 <span class="col-3"></span>
63 <span class="col-9"> 63 <span class="col-9">
64 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]" 64 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '.' ]"
65 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" 65 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
66 > 66 >
67 <div>{{ reason.label }}</div> 67 <div>{{ reason.label }}</div>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
index e8ce7e678..14674c5f0 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
@@ -1,6 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { durationToString } from '@app/helpers' 2import { durationToString } from '@app/helpers'
3import { Account } from '@app/shared/shared-main'
4import { AbusePredefinedReasonsString } from '@shared/models' 3import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedAbuse } from './processed-abuse.model' 4import { ProcessedAbuse } from './processed-abuse.model'
6 5
@@ -12,7 +11,6 @@ import { ProcessedAbuse } from './processed-abuse.model'
12export class AbuseDetailsComponent { 11export class AbuseDetailsComponent {
13 @Input() abuse: ProcessedAbuse 12 @Input() abuse: ProcessedAbuse
14 @Input() isAdminView: boolean 13 @Input() isAdminView: boolean
15 @Input() baseRoute: string
16 14
17 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } 15 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
18 16
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index b41bc75d4..22f84a96e 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -8,28 +8,7 @@
8 <ng-template pTemplate="caption"> 8 <ng-template pTemplate="caption">
9 <div class="caption"> 9 <div class="caption">
10 <div class="ml-auto"> 10 <div class="ml-auto">
11 <div class="input-group has-feedback has-clear"> 11 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)" (resetTableFilter)="resetTableFilter()"></my-advanced-input-filter>
12 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
13 <div class="input-group-text" ngbDropdownToggle>
14 <span class="caret" aria-haspopup="menu" role="button"></span>
15 </div>
16
17 <div role="menu" ngbDropdownMenu>
18 <h6 class="dropdown-header" i18n>Advanced report filters</h6>
19 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
20 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
21 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
22 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
23 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
24 </div>
25 </div>
26 <input
27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
28 (keyup)="onSearch($event)"
29 >
30 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
31 <span class="sr-only" i18n>Clear filters</span>
32 </div>
33 </div> 12 </div>
34 </div> 13 </div>
35 </ng-template> 14 </ng-template>
@@ -171,7 +150,7 @@
171 <ng-template pTemplate="rowexpansion" let-abuse> 150 <ng-template pTemplate="rowexpansion" let-abuse>
172 <tr> 151 <tr>
173 <td class="expand-cell" colspan="8"> 152 <td class="expand-cell" colspan="8">
174 <my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details> 153 <my-abuse-details [abuse]="abuse" [isAdminView]="isAdminView()"></my-abuse-details>
175 </td> 154 </td>
176 </tr> 155 </tr>
177 </ng-template> 156 </ng-template>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 8b5771237..f393c0d1e 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -14,6 +14,7 @@ import { AbuseState, AdminAbuse } from '@shared/models'
14import { AbuseMessageModalComponent } from './abuse-message-modal.component' 14import { AbuseMessageModalComponent } from './abuse-message-modal.component'
15import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
16import { ProcessedAbuse } from './processed-abuse.model' 16import { ProcessedAbuse } from './processed-abuse.model'
17import { AdvancedInputFilter } from '../shared-forms'
17 18
18const logger = debug('peertube:moderation:AbuseListTableComponent') 19const logger = debug('peertube:moderation:AbuseListTableComponent')
19 20
@@ -24,7 +25,6 @@ const logger = debug('peertube:moderation:AbuseListTableComponent')
24}) 25})
25export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit { 26export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
26 @Input() viewType: 'admin' | 'user' 27 @Input() viewType: 'admin' | 'user'
27 @Input() baseRoute: string
28 28
29 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent 29 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
30 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent 30 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
@@ -36,6 +36,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
36 36
37 abuseActions: DropdownAction<ProcessedAbuse>[][] = [] 37 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
38 38
39 inputFilters: AdvancedInputFilter[] = [
40 {
41 queryParams: { 'search': 'state:pending' },
42 label: $localize`Unsolved reports`
43 },
44 {
45 queryParams: { 'search': 'state:accepted' },
46 label: $localize`Accepted reports`
47 },
48 {
49 queryParams: { 'search': 'state:rejected' },
50 label: $localize`Refused reports`
51 },
52 {
53 queryParams: { 'search': 'videoIs:blacklisted' },
54 label: $localize`Reports with blocked videos`
55 },
56 {
57 queryParams: { 'search': 'videoIs:deleted' },
58 label: $localize`Reports with deleted videos`
59 }
60 ]
61
39 constructor ( 62 constructor (
40 protected route: ActivatedRoute, 63 protected route: ActivatedRoute,
41 protected router: Router, 64 protected router: Router,
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
index e4efbe475..835e15110 100644
--- a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
+++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
@@ -98,7 +98,7 @@ export class ActorAvatarComponent {
98 jkl: 'gray', 98 jkl: 'gray',
99 mno: 'yellow', 99 mno: 'yellow',
100 pqr: 'orange', 100 pqr: 'orange',
101 stv: 'red', 101 stvu: 'red',
102 wxyz: 'dark-blue' 102 wxyz: 'dark-blue'
103 } 103 }
104 104
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
new file mode 100644
index 000000000..03c4f127b
--- /dev/null
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
@@ -0,0 +1,22 @@
1<div class="input-group has-feedback has-clear">
2 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
3 <div class="input-group-text" ngbDropdownToggle>
4 <span class="caret" aria-haspopup="menu" role="button"></span>
5 </div>
6
7 <div role="menu" ngbDropdownMenu>
8 <h6 class="dropdown-header" i18n>Advanced filters</h6>
9
10 <a *ngFor="let filter of filters" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item">
11 {{ filter.label }}
12 </a>
13
14 </div>
15 </div>
16 <input
17 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
18 (keyup)="onSearch($event)"
19 >
20 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a>
21 <span class="sr-only" i18n>Clear filters</span>
22</div>
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
new file mode 100644
index 000000000..7c2198927
--- /dev/null
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4input {
5 @include peertube-input-text(250px);
6}
7
8.input-group-text {
9 background-color: transparent;
10}
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
new file mode 100644
index 000000000..394090751
--- /dev/null
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts
@@ -0,0 +1,27 @@
1import { Component, EventEmitter, Input, Output } from '@angular/core'
2import { Params } from '@angular/router'
3
4export type AdvancedInputFilter = {
5 label: string
6 queryParams: Params
7}
8
9@Component({
10 selector: 'my-advanced-input-filter',
11 templateUrl: './advanced-input-filter.component.html',
12 styleUrls: [ './advanced-input-filter.component.scss' ]
13})
14export class AdvancedInputFilterComponent {
15 @Input() filters: AdvancedInputFilter[] = []
16
17 @Output() resetTableFilter = new EventEmitter<void>()
18 @Output() search = new EventEmitter<Event>()
19
20 onSearch (event: Event) {
21 this.search.emit(event)
22 }
23
24 onResetTableFilter () {
25 this.resetTableFilter.emit()
26 }
27}
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index 1d859b991..727416a40 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -1,12 +1,14 @@
1export * from './form-validator.service' 1export * from './advanced-input-filter.component'
2export * from './form-reactive' 2export * from './form-reactive'
3export * from './select' 3export * from './form-validator.service'
4export * from './input-toggle-hidden.component' 4export * from './form-validator.service'
5export * from './input-switch.component' 5export * from './input-switch.component'
6export * from './input-toggle-hidden.component'
6export * from './markdown-textarea.component' 7export * from './markdown-textarea.component'
7export * from './peertube-checkbox.component' 8export * from './peertube-checkbox.component'
8export * from './preview-upload.component' 9export * from './preview-upload.component'
9export * from './reactive-file.component' 10export * from './reactive-file.component'
11export * from './select'
12export * from './shared-form.module'
10export * from './textarea-autoresize.directive' 13export * from './textarea-autoresize.directive'
11export * from './timestamp-input.component' 14export * from './timestamp-input.component'
12export * from './shared-form.module'
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 9bdd138a1..5417f7342 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { NgSelectModule } from '@ng-select/ng-select' 5import { NgSelectModule } from '@ng-select/ng-select'
6import { SharedGlobalIconModule } from '../shared-icons' 6import { SharedGlobalIconModule } from '../shared-icons'
7import { SharedMainModule } from '../shared-main/shared-main.module' 7import { SharedMainModule } from '../shared-main/shared-main.module'
8import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
8import { DynamicFormFieldComponent } from './dynamic-form-field.component' 9import { DynamicFormFieldComponent } from './dynamic-form-field.component'
9import { FormValidatorService } from './form-validator.service' 10import { FormValidatorService } from './form-validator.service'
10import { InputSwitchComponent } from './input-switch.component' 11import { InputSwitchComponent } from './input-switch.component'
@@ -52,7 +53,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
52 SelectCheckboxComponent, 53 SelectCheckboxComponent,
53 SelectCustomValueComponent, 54 SelectCustomValueComponent,
54 55
55 DynamicFormFieldComponent 56 DynamicFormFieldComponent,
57
58 AdvancedInputFilterComponent
56 ], 59 ],
57 60
58 exports: [ 61 exports: [
@@ -78,7 +81,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
78 SelectCheckboxComponent, 81 SelectCheckboxComponent,
79 SelectCustomValueComponent, 82 SelectCustomValueComponent,
80 83
81 DynamicFormFieldComponent 84 DynamicFormFieldComponent,
85
86 AdvancedInputFilterComponent
82 ], 87 ],
83 88
84 providers: [ 89 providers: [
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 0b708b692..668e51f73 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -124,7 +124,23 @@ export class VideoService implements VideosProvider {
124 124
125 let params = new HttpParams() 125 let params = new HttpParams()
126 params = this.restService.addRestGetParams(params, pagination, sort) 126 params = this.restService.addRestGetParams(params, pagination, sort)
127 params = this.restService.addObjectParams(params, { search }) 127
128 if (search) {
129 const filters = this.restService.parseQueryStringFilter(search, {
130 isLive: {
131 prefix: 'isLive:',
132 isBoolean: true,
133 handler: v => {
134 if (v === 'true') return v
135 if (v === 'false') return v
136
137 return undefined
138 }
139 }
140 })
141
142 params = this.restService.addObjectParams(params, filters)
143 }
128 144
129 return this.authHttp 145 return this.authHttp
130 .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params }) 146 .get<ResultList<Video>>(UserService.BASE_USERS_URL + 'me/videos', { params })
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 30badc8fa..0e3924841 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -1,4 +1,4 @@
1import { NSFWQuery, SearchTargetType } from '@shared/models' 1import { BooleanBothQuery, SearchTargetType } from '@shared/models'
2 2
3export class AdvancedSearch { 3export class AdvancedSearch {
4 startDate: string // ISO 8601 4 startDate: string // ISO 8601
@@ -7,7 +7,7 @@ export class AdvancedSearch {
7 originallyPublishedStartDate: string // ISO 8601 7 originallyPublishedStartDate: string // ISO 8601
8 originallyPublishedEndDate: string // ISO 8601 8 originallyPublishedEndDate: string // ISO 8601
9 9
10 nsfw: NSFWQuery 10 nsfw: BooleanBothQuery
11 11
12 categoryOneOf: string 12 categoryOneOf: string
13 13
@@ -33,7 +33,7 @@ export class AdvancedSearch {
33 endDate?: string 33 endDate?: string
34 originallyPublishedStartDate?: string 34 originallyPublishedStartDate?: string
35 originallyPublishedEndDate?: string 35 originallyPublishedEndDate?: string
36 nsfw?: NSFWQuery 36 nsfw?: BooleanBothQuery
37 categoryOneOf?: string 37 categoryOneOf?: string
38 licenceOneOf?: string 38 licenceOneOf?: string
39 languageOneOf?: string 39 languageOneOf?: string
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index 1abcd30e4..6a4d89dff 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -9,6 +9,10 @@ input[type=button] {
9 border-radius: inherit; 9 border-radius: inherit;
10} 10}
11 11
12p-table .p-datatable-header .caption {
13 margin-bottom: 15px;
14}
15
12// Taken from old nova light theme 16// Taken from old nova light theme
13 17
14body .p-disabled { 18body .p-disabled {
@@ -512,10 +516,6 @@ p-table {
512 .left-buttons { 516 .left-buttons {
513 padding-left: 15px; 517 padding-left: 15px;
514 } 518 }
515
516 .input-group-text {
517 background-color: transparent;
518 }
519 } 519 }
520 } 520 }
521 521
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index e31924a94..49a8e3195 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,9 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
3import { VideosWithSearchCommonQuery } from '@shared/models'
3import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 4import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
4import { getFormattedObjects } from '../../helpers/utils' 5import { getFormattedObjects } from '../../helpers/utils'
5import { Hooks } from '../../lib/plugins/hooks'
6import { JobQueue } from '../../lib/job-queue' 6import { JobQueue } from '../../lib/job-queue'
7import { Hooks } from '../../lib/plugins/hooks'
7import { 8import {
8 asyncMiddleware, 9 asyncMiddleware,
9 authenticate, 10 authenticate,
@@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
158 const account = res.locals.account 159 const account = res.locals.account
159 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 160 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
160 const countVideos = getCountVideos(req) 161 const countVideos = getCountVideos(req)
162 const query = req.query as VideosWithSearchCommonQuery
161 163
162 const apiOptions = await Hooks.wrapObject({ 164 const apiOptions = await Hooks.wrapObject({
163 followerActorId, 165 followerActorId,
164 start: req.query.start, 166 start: query.start,
165 count: req.query.count, 167 count: query.count,
166 sort: req.query.sort, 168 sort: query.sort,
167 includeLocalVideos: true, 169 includeLocalVideos: true,
168 categoryOneOf: req.query.categoryOneOf, 170 categoryOneOf: query.categoryOneOf,
169 licenceOneOf: req.query.licenceOneOf, 171 licenceOneOf: query.licenceOneOf,
170 languageOneOf: req.query.languageOneOf, 172 languageOneOf: query.languageOneOf,
171 tagsOneOf: req.query.tagsOneOf, 173 tagsOneOf: query.tagsOneOf,
172 tagsAllOf: req.query.tagsAllOf, 174 tagsAllOf: query.tagsAllOf,
173 filter: req.query.filter, 175 filter: query.filter,
174 nsfw: buildNSFWFilter(res, req.query.nsfw), 176 isLive: query.isLive,
177 nsfw: buildNSFWFilter(res, query.nsfw),
175 withFiles: false, 178 withFiles: false,
176 accountId: account.id, 179 accountId: account.id,
177 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 180 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
178 countVideos, 181 countVideos,
179 search: req.query.search 182 search: query.search
180 }, 'filter:api.accounts.videos.list.params') 183 }, 'filter:api.accounts.videos.list.params')
181 184
182 const resultList = await Hooks.wrapPromiseFun( 185 const resultList = await Hooks.wrapPromiseFun(
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 9f9d2d77f..0763d1900 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) {
111 start: req.query.start, 111 start: req.query.start,
112 count: req.query.count, 112 count: req.query.count,
113 sort: req.query.sort, 113 sort: req.query.sort,
114 search: req.query.search 114 search: req.query.search,
115 isLive: req.query.isLive
115 }, 'filter:api.user.me.videos.list.params') 116 }, 'filter:api.user.me.videos.list.params')
116 117
117 const resultList = await Hooks.wrapPromiseFun( 118 const resultList = await Hooks.wrapPromiseFun(
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index e8949ee59..56b93276f 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -2,8 +2,8 @@ import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { sendUndoFollow } from '@server/lib/activitypub/send' 3import { sendUndoFollow } from '@server/lib/activitypub/send'
4import { VideoChannelModel } from '@server/models/video/video-channel' 4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { VideosCommonQuery } from '@shared/models'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
6import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
8import { getFormattedObjects } from '../../../helpers/utils' 8import { getFormattedObjects } from '../../../helpers/utils'
9import { WEBSERVER } from '../../../initializers/constants' 9import { WEBSERVER } from '../../../initializers/constants'
@@ -170,19 +170,20 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
170async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { 170async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
171 const user = res.locals.oauth.token.User 171 const user = res.locals.oauth.token.User
172 const countVideos = getCountVideos(req) 172 const countVideos = getCountVideos(req)
173 const query = req.query as VideosCommonQuery
173 174
174 const resultList = await VideoModel.listForApi({ 175 const resultList = await VideoModel.listForApi({
175 start: req.query.start, 176 start: query.start,
176 count: req.query.count, 177 count: query.count,
177 sort: req.query.sort, 178 sort: query.sort,
178 includeLocalVideos: false, 179 includeLocalVideos: false,
179 categoryOneOf: req.query.categoryOneOf, 180 categoryOneOf: query.categoryOneOf,
180 licenceOneOf: req.query.licenceOneOf, 181 licenceOneOf: query.licenceOneOf,
181 languageOneOf: req.query.languageOneOf, 182 languageOneOf: query.languageOneOf,
182 tagsOneOf: req.query.tagsOneOf, 183 tagsOneOf: query.tagsOneOf,
183 tagsAllOf: req.query.tagsAllOf, 184 tagsAllOf: query.tagsAllOf,
184 nsfw: buildNSFWFilter(res, req.query.nsfw), 185 nsfw: buildNSFWFilter(res, query.nsfw),
185 filter: req.query.filter as VideoFilter, 186 filter: query.filter,
186 withFiles: false, 187 withFiles: false,
187 followerActorId: user.Account.Actor.id, 188 followerActorId: user.Account.Actor.id,
188 user, 189 user,
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 149d6cfb4..a755d7e57 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks' 2import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { MChannelBannerAccountDefault } from '@server/types/models' 4import { MChannelBannerAccountDefault } from '@server/types/models'
5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
8import { resetSequelizeInstance } from '../../helpers/database-utils' 8import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -312,20 +312,21 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
312 const videoChannelInstance = res.locals.videoChannel 312 const videoChannelInstance = res.locals.videoChannel
313 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 313 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
314 const countVideos = getCountVideos(req) 314 const countVideos = getCountVideos(req)
315 const query = req.query as VideosCommonQuery
315 316
316 const apiOptions = await Hooks.wrapObject({ 317 const apiOptions = await Hooks.wrapObject({
317 followerActorId, 318 followerActorId,
318 start: req.query.start, 319 start: query.start,
319 count: req.query.count, 320 count: query.count,
320 sort: req.query.sort, 321 sort: query.sort,
321 includeLocalVideos: true, 322 includeLocalVideos: true,
322 categoryOneOf: req.query.categoryOneOf, 323 categoryOneOf: query.categoryOneOf,
323 licenceOneOf: req.query.licenceOneOf, 324 licenceOneOf: query.licenceOneOf,
324 languageOneOf: req.query.languageOneOf, 325 languageOneOf: query.languageOneOf,
325 tagsOneOf: req.query.tagsOneOf, 326 tagsOneOf: query.tagsOneOf,
326 tagsAllOf: req.query.tagsAllOf, 327 tagsAllOf: query.tagsAllOf,
327 filter: req.query.filter, 328 filter: query.filter,
328 nsfw: buildNSFWFilter(res, req.query.nsfw), 329 nsfw: buildNSFWFilter(res, query.nsfw),
329 withFiles: false, 330 withFiles: false,
330 videoChannelId: videoChannelInstance.id, 331 videoChannelId: videoChannelInstance.id,
331 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 332 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 7fee278f2..6ec6478e4 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
10import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 10import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
11import { getServerActor } from '@server/models/application/application' 11import { getServerActor } from '@server/models/application/application'
12import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 12import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
13import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' 13import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
14import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 14import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
15import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
16import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 15import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 16import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 17import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
@@ -494,20 +493,22 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
494} 493}
495 494
496async function listVideos (req: express.Request, res: express.Response) { 495async function listVideos (req: express.Request, res: express.Response) {
496 const query = req.query as VideosCommonQuery
497 const countVideos = getCountVideos(req) 497 const countVideos = getCountVideos(req)
498 498
499 const apiOptions = await Hooks.wrapObject({ 499 const apiOptions = await Hooks.wrapObject({
500 start: req.query.start, 500 start: query.start,
501 count: req.query.count, 501 count: query.count,
502 sort: req.query.sort, 502 sort: query.sort,
503 includeLocalVideos: true, 503 includeLocalVideos: true,
504 categoryOneOf: req.query.categoryOneOf, 504 categoryOneOf: query.categoryOneOf,
505 licenceOneOf: req.query.licenceOneOf, 505 licenceOneOf: query.licenceOneOf,
506 languageOneOf: req.query.languageOneOf, 506 languageOneOf: query.languageOneOf,
507 tagsOneOf: req.query.tagsOneOf, 507 tagsOneOf: query.tagsOneOf,
508 tagsAllOf: req.query.tagsAllOf, 508 tagsAllOf: query.tagsAllOf,
509 nsfw: buildNSFWFilter(res, req.query.nsfw), 509 nsfw: buildNSFWFilter(res, query.nsfw),
510 filter: req.query.filter as VideoFilter, 510 isLive: query.isLive,
511 filter: query.filter,
511 withFiles: false, 512 withFiles: false,
512 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 513 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
513 countVideos 514 countVideos
diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts
index 429fcafcf..a8f258838 100644
--- a/server/helpers/custom-validators/search.ts
+++ b/server/helpers/custom-validators/search.ts
@@ -11,7 +11,7 @@ function isStringArray (value: any) {
11 return isArray(value) && value.every(v => typeof v === 'string') 11 return isArray(value) && value.every(v => typeof v === 'string')
12} 12}
13 13
14function isNSFWQueryValid (value: any) { 14function isBooleanBothQueryValid (value: any) {
15 return value === 'true' || value === 'false' || value === 'both' 15 return value === 'true' || value === 'false' || value === 'both'
16} 16}
17 17
@@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) {
32export { 32export {
33 isNumberArray, 33 isNumberArray,
34 isStringArray, 34 isStringArray,
35 isNSFWQueryValid, 35 isBooleanBothQueryValid,
36 isSearchTargetValid 36 isSearchTargetValid
37} 37}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 4d31d3dcb..bb617d77c 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -20,7 +20,7 @@ import {
20 toIntOrNull, 20 toIntOrNull,
21 toValueOrNull 21 toValueOrNull
22} from '../../../helpers/custom-validators/misc' 22} from '../../../helpers/custom-validators/misc'
23import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 23import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
24import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' 24import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
25import { 25import {
26 isScheduleVideoUpdatePrivacyValid, 26 isScheduleVideoUpdatePrivacyValid,
@@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [
439 .custom(isStringArray).withMessage('Should have a valid all of tags array'), 439 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
440 query('nsfw') 440 query('nsfw')
441 .optional() 441 .optional()
442 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), 442 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
443 query('isLive')
444 .optional()
445 .customSanitizer(toBooleanOrNull)
446 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
443 query('filter') 447 query('filter')
444 .optional() 448 .optional()
445 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), 449 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 4d95ddee2..155afe64b 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = {
16 start: number 16 start: number
17 sort: string 17 sort: string
18 18
19 nsfw?: boolean
19 filter?: VideoFilter 20 filter?: VideoFilter
21 isLive?: boolean
22
20 categoryOneOf?: number[] 23 categoryOneOf?: number[]
21 nsfw?: boolean
22 licenceOneOf?: number[] 24 licenceOneOf?: number[]
23 languageOneOf?: string[] 25 languageOneOf?: string[]
24 tagsOneOf?: string[] 26 tagsOneOf?: string[]
@@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
199 201
200 if (options.nsfw === true) { 202 if (options.nsfw === true) {
201 and.push('"video"."nsfw" IS TRUE') 203 and.push('"video"."nsfw" IS TRUE')
204 } else if (options.nsfw === false) {
205 and.push('"video"."nsfw" IS FALSE')
202 } 206 }
203 207
204 if (options.nsfw === false) { 208 if (options.isLive === true) {
205 and.push('"video"."nsfw" IS FALSE') 209 and.push('"video"."isLive" IS TRUE')
210 } else if (options.isLive === false) {
211 and.push('"video"."isLive" IS FALSE')
206 } 212 }
207 213
208 if (options.categoryOneOf) { 214 if (options.categoryOneOf) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 422bf6deb..e55a21a6b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1021,14 +1021,28 @@ export class VideoModel extends Model {
1021 start: number 1021 start: number
1022 count: number 1022 count: number
1023 sort: string 1023 sort: string
1024 isLive?: boolean
1024 search?: string 1025 search?: string
1025 }) { 1026 }) {
1026 const { accountId, start, count, sort, search } = options 1027 const { accountId, start, count, sort, search, isLive } = options
1027 1028
1028 function buildBaseQuery (): FindOptions { 1029 function buildBaseQuery (): FindOptions {
1029 let baseQuery = { 1030 const where: WhereOptions = {}
1031
1032 if (search) {
1033 where.name = {
1034 [Op.iLike]: '%' + search + '%'
1035 }
1036 }
1037
1038 if (isLive) {
1039 where.isLive = isLive
1040 }
1041
1042 const baseQuery = {
1030 offset: start, 1043 offset: start,
1031 limit: count, 1044 limit: count,
1045 where,
1032 order: getVideoSort(sort), 1046 order: getVideoSort(sort),
1033 include: [ 1047 include: [
1034 { 1048 {
@@ -1047,16 +1061,6 @@ export class VideoModel extends Model {
1047 ] 1061 ]
1048 } 1062 }
1049 1063
1050 if (search) {
1051 baseQuery = Object.assign(baseQuery, {
1052 where: {
1053 name: {
1054 [Op.iLike]: '%' + search + '%'
1055 }
1056 }
1057 })
1058 }
1059
1060 return baseQuery 1064 return baseQuery
1061 } 1065 }
1062 1066
@@ -1084,23 +1088,34 @@ export class VideoModel extends Model {
1084 start: number 1088 start: number
1085 count: number 1089 count: number
1086 sort: string 1090 sort: string
1091
1087 nsfw: boolean 1092 nsfw: boolean
1093 filter?: VideoFilter
1094 isLive?: boolean
1095
1088 includeLocalVideos: boolean 1096 includeLocalVideos: boolean
1089 withFiles: boolean 1097 withFiles: boolean
1098
1090 categoryOneOf?: number[] 1099 categoryOneOf?: number[]
1091 licenceOneOf?: number[] 1100 licenceOneOf?: number[]
1092 languageOneOf?: string[] 1101 languageOneOf?: string[]
1093 tagsOneOf?: string[] 1102 tagsOneOf?: string[]
1094 tagsAllOf?: string[] 1103 tagsAllOf?: string[]
1095 filter?: VideoFilter 1104
1096 accountId?: number 1105 accountId?: number
1097 videoChannelId?: number 1106 videoChannelId?: number
1107
1098 followerActorId?: number 1108 followerActorId?: number
1109
1099 videoPlaylistId?: number 1110 videoPlaylistId?: number
1111
1100 trendingDays?: number 1112 trendingDays?: number
1113
1101 user?: MUserAccountId 1114 user?: MUserAccountId
1102 historyOfUser?: MUserId 1115 historyOfUser?: MUserId
1116
1103 countVideos?: boolean 1117 countVideos?: boolean
1118
1104 search?: string 1119 search?: string
1105 }) { 1120 }) {
1106 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1121 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@@ -1128,6 +1143,7 @@ export class VideoModel extends Model {
1128 followerActorId, 1143 followerActorId,
1129 serverAccountId: serverActor.Account.id, 1144 serverAccountId: serverActor.Account.id,
1130 nsfw: options.nsfw, 1145 nsfw: options.nsfw,
1146 isLive: options.isLive,
1131 categoryOneOf: options.categoryOneOf, 1147 categoryOneOf: options.categoryOneOf,
1132 licenceOneOf: options.licenceOneOf, 1148 licenceOneOf: options.licenceOneOf,
1133 languageOneOf: options.languageOneOf, 1149 languageOneOf: options.languageOneOf,
@@ -1160,6 +1176,7 @@ export class VideoModel extends Model {
1160 originallyPublishedStartDate?: string 1176 originallyPublishedStartDate?: string
1161 originallyPublishedEndDate?: string 1177 originallyPublishedEndDate?: string
1162 nsfw?: boolean 1178 nsfw?: boolean
1179 isLive?: boolean
1163 categoryOneOf?: number[] 1180 categoryOneOf?: number[]
1164 licenceOneOf?: number[] 1181 licenceOneOf?: number[]
1165 languageOneOf?: string[] 1182 languageOneOf?: string[]
@@ -1171,23 +1188,32 @@ export class VideoModel extends Model {
1171 filter?: VideoFilter 1188 filter?: VideoFilter
1172 }) { 1189 }) {
1173 const serverActor = await getServerActor() 1190 const serverActor = await getServerActor()
1191
1174 const queryOptions = { 1192 const queryOptions = {
1175 followerActorId: serverActor.id, 1193 followerActorId: serverActor.id,
1176 serverAccountId: serverActor.Account.id, 1194 serverAccountId: serverActor.Account.id,
1195
1177 includeLocalVideos: options.includeLocalVideos, 1196 includeLocalVideos: options.includeLocalVideos,
1178 nsfw: options.nsfw, 1197 nsfw: options.nsfw,
1198 isLive: options.isLive,
1199
1179 categoryOneOf: options.categoryOneOf, 1200 categoryOneOf: options.categoryOneOf,
1180 licenceOneOf: options.licenceOneOf, 1201 licenceOneOf: options.licenceOneOf,
1181 languageOneOf: options.languageOneOf, 1202 languageOneOf: options.languageOneOf,
1203
1182 tagsOneOf: options.tagsOneOf, 1204 tagsOneOf: options.tagsOneOf,
1183 tagsAllOf: options.tagsAllOf, 1205 tagsAllOf: options.tagsAllOf,
1206
1184 user: options.user, 1207 user: options.user,
1185 filter: options.filter, 1208 filter: options.filter,
1209
1186 start: options.start, 1210 start: options.start,
1187 count: options.count, 1211 count: options.count,
1188 sort: options.sort, 1212 sort: options.sort,
1213
1189 startDate: options.startDate, 1214 startDate: options.startDate,
1190 endDate: options.endDate, 1215 endDate: options.endDate,
1216
1191 originallyPublishedStartDate: options.originallyPublishedStartDate, 1217 originallyPublishedStartDate: options.originallyPublishedStartDate,
1192 originallyPublishedEndDate: options.originallyPublishedEndDate, 1218 originallyPublishedEndDate: options.originallyPublishedEndDate,
1193 1219
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index d48e2a8ee..57fb58150 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -19,10 +19,12 @@ import {
19 doubleFollow, 19 doubleFollow,
20 flushAndRunMultipleServers, 20 flushAndRunMultipleServers,
21 getLive, 21 getLive,
22 getMyVideosWithFilter,
22 getPlaylist, 23 getPlaylist,
23 getVideo, 24 getVideo,
24 getVideoIdFromUUID, 25 getVideoIdFromUUID,
25 getVideosList, 26 getVideosList,
27 getVideosWithFilters,
26 killallServers, 28 killallServers,
27 makeRawRequest, 29 makeRawRequest,
28 removeVideo, 30 removeVideo,
@@ -37,6 +39,7 @@ import {
37 testImage, 39 testImage,
38 updateCustomSubConfig, 40 updateCustomSubConfig,
39 updateLive, 41 updateLive,
42 uploadVideoAndGetId,
40 viewVideo, 43 viewVideo,
41 wait, 44 wait,
42 waitJobs, 45 waitJobs,
@@ -229,6 +232,68 @@ describe('Test live', function () {
229 }) 232 })
230 }) 233 })
231 234
235 describe('Live filters', function () {
236 let command: any
237 let liveVideoId: string
238 let vodVideoId: string
239
240 before(async function () {
241 this.timeout(120000)
242
243 vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid
244
245 const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id }
246 const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions)
247 liveVideoId = resLive.body.video.uuid
248
249 command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId)
250 await waitUntilLivePublishedOnAllServers(liveVideoId)
251 await waitJobs(servers)
252 })
253
254 it('Should only display lives', async function () {
255 const res = await getVideosWithFilters(servers[0].url, { isLive: true })
256
257 expect(res.body.total).to.equal(1)
258 expect(res.body.data).to.have.lengthOf(1)
259 expect(res.body.data[0].name).to.equal('live')
260 })
261
262 it('Should not display lives', async function () {
263 const res = await getVideosWithFilters(servers[0].url, { isLive: false })
264
265 expect(res.body.total).to.equal(1)
266 expect(res.body.data).to.have.lengthOf(1)
267 expect(res.body.data[0].name).to.equal('vod video')
268 })
269
270 it('Should display my lives', async function () {
271 this.timeout(60000)
272
273 await stopFfmpeg(command)
274 await waitJobs(servers)
275
276 const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true })
277 const videos = res.body.data as Video[]
278
279 const result = videos.every(v => v.isLive)
280 expect(result).to.be.true
281 })
282
283 it('Should not display my lives', async function () {
284 const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false })
285 const videos = res.body.data as Video[]
286
287 const result = videos.every(v => !v.isLive)
288 expect(result).to.be.true
289 })
290
291 after(async function () {
292 await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId)
293 await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId)
294 })
295 })
296
232 describe('Stream checks', function () { 297 describe('Stream checks', function () {
233 let liveVideo: LiveVideo & VideoDetails 298 let liveVideo: LiveVideo & VideoDetails
234 let rtmpUrl: string 299 let rtmpUrl: string
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index e05c3a269..5b8907961 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -1,17 +1,24 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { VideoPrivacy } from '@shared/models'
5import { 6import {
6 advancedVideosSearch, 7 advancedVideosSearch,
7 cleanupTests, 8 cleanupTests,
9 createLive,
8 flushAndRunServer, 10 flushAndRunServer,
9 immutableAssign, 11 immutableAssign,
10 searchVideo, 12 searchVideo,
13 sendRTMPStreamInVideo,
11 ServerInfo, 14 ServerInfo,
12 setAccessTokensToServers, 15 setAccessTokensToServers,
16 setDefaultVideoChannel,
17 stopFfmpeg,
18 updateCustomSubConfig,
13 uploadVideo, 19 uploadVideo,
14 wait 20 wait,
21 waitUntilLivePublished
15} from '../../../../shared/extra-utils' 22} from '../../../../shared/extra-utils'
16import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' 23import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
17 24
@@ -28,6 +35,7 @@ describe('Test videos search', function () {
28 server = await flushAndRunServer(1) 35 server = await flushAndRunServer(1)
29 36
30 await setAccessTokensToServers([ server ]) 37 await setAccessTokensToServers([ server ])
38 await setDefaultVideoChannel([ server ])
31 39
32 { 40 {
33 const attributes1 = { 41 const attributes1 = {
@@ -449,6 +457,43 @@ describe('Test videos search', function () {
449 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3') 457 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
450 }) 458 })
451 459
460 it('Should search by live', async function () {
461 this.timeout(30000)
462
463 {
464 const options = {
465 search: {
466 searchIndex: { enabled: false }
467 },
468 live: { enabled: true }
469 }
470 await updateCustomSubConfig(server.url, server.accessToken, options)
471 }
472
473 {
474 const res = await advancedVideosSearch(server.url, { isLive: true })
475
476 expect(res.body.total).to.equal(0)
477 expect(res.body.data).to.have.lengthOf(0)
478 }
479
480 {
481 const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id }
482 const resLive = await createLive(server.url, server.accessToken, liveOptions)
483 const liveVideoId = resLive.body.video.uuid
484
485 const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId)
486 await waitUntilLivePublished(server.url, server.accessToken, liveVideoId)
487
488 const res = await advancedVideosSearch(server.url, { isLive: true })
489
490 expect(res.body.total).to.equal(1)
491 expect(res.body.data[0].name).to.equal('live')
492
493 await stopFfmpeg(command)
494 }
495 })
496
452 after(async function () { 497 after(async function () {
453 await cleanupTests([ server ]) 498 await cleanupTests([ server ])
454 }) 499 })
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index da90223b8..a79648bf7 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -387,11 +387,11 @@ describe('Test a single server', function () {
387 }) 387 })
388 388
389 it('Should filter by tags and category', async function () { 389 it('Should filter by tags and category', async function () {
390 const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 }) 390 const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
391 expect(res1.body.total).to.equal(1) 391 expect(res1.body.total).to.equal(1)
392 expect(res1.body.data[0].name).to.equal('my super video updated') 392 expect(res1.body.data[0].name).to.equal('my super video updated')
393 393
394 const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 }) 394 const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
395 expect(res2.body.total).to.equal(0) 395 expect(res2.body.total).to.equal(0)
396 }) 396 })
397 397
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 67fe82d41..a0143b0ef 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -8,6 +8,7 @@ import * as request from 'supertest'
8import { v4 as uuidv4 } from 'uuid' 8import { v4 as uuidv4 } from 'uuid'
9import validator from 'validator' 9import validator from 'validator'
10import { HttpStatusCode } from '@shared/core-utils' 10import { HttpStatusCode } from '@shared/core-utils'
11import { VideosCommonQuery } from '@shared/models'
11import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 12import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
12import { VideoDetails, VideoPrivacy } from '../../models/videos' 13import { VideoDetails, VideoPrivacy } from '../../models/videos'
13import { 14import {
@@ -195,6 +196,18 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
195 .expect('Content-Type', /json/) 196 .expect('Content-Type', /json/)
196} 197}
197 198
199function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
200 const path = '/api/v1/users/me/videos'
201
202 return makeGetRequest({
203 url,
204 path,
205 token: accessToken,
206 query,
207 statusCodeExpected: HttpStatusCode.OK_200
208 })
209}
210
198function getAccountVideos ( 211function getAccountVideos (
199 url: string, 212 url: string,
200 accessToken: string, 213 accessToken: string,
@@ -295,7 +308,7 @@ function getVideosListSort (url: string, sort: string) {
295 .expect('Content-Type', /json/) 308 .expect('Content-Type', /json/)
296} 309}
297 310
298function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { 311function getVideosWithFilters (url: string, query: VideosCommonQuery) {
299 const path = '/api/v1/videos' 312 const path = '/api/v1/videos'
300 313
301 return request(url) 314 return request(url)
@@ -751,6 +764,7 @@ export {
751 completeVideoCheck, 764 completeVideoCheck,
752 checkVideoFilesWereRemoved, 765 checkVideoFilesWereRemoved,
753 getPlaylistVideos, 766 getPlaylistVideos,
767 getMyVideosWithFilter,
754 uploadVideoAndGetId, 768 uploadVideoAndGetId,
755 getLocalIdByUUID, 769 getLocalIdByUUID,
756 getVideoIdFromUUID 770 getVideoIdFromUUID
diff --git a/shared/models/search/boolean-both-query.model.ts b/shared/models/search/boolean-both-query.model.ts
new file mode 100644
index 000000000..57b0e8d44
--- /dev/null
+++ b/shared/models/search/boolean-both-query.model.ts
@@ -0,0 +1 @@
export type BooleanBothQuery = 'true' | 'false' | 'both'
diff --git a/shared/models/search/index.ts b/shared/models/search/index.ts
index e2d0ab620..697ceccb1 100644
--- a/shared/models/search/index.ts
+++ b/shared/models/search/index.ts
@@ -1,4 +1,5 @@
1export * from './nsfw-query.model' 1export * from './boolean-both-query.model'
2export * from './search-target-query.model' 2export * from './search-target-query.model'
3export * from './videos-common-query.model'
3export * from './videos-search-query.model' 4export * from './videos-search-query.model'
4export * from './video-channels-search-query.model' 5export * from './video-channels-search-query.model'
diff --git a/shared/models/search/nsfw-query.model.ts b/shared/models/search/nsfw-query.model.ts
deleted file mode 100644
index 6b6ad1991..000000000
--- a/shared/models/search/nsfw-query.model.ts
+++ /dev/null
@@ -1 +0,0 @@
1export type NSFWQuery = 'true' | 'false' | 'both'
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
new file mode 100644
index 000000000..bd02489ea
--- /dev/null
+++ b/shared/models/search/videos-common-query.model.ts
@@ -0,0 +1,28 @@
1import { VideoFilter } from '../videos'
2import { BooleanBothQuery } from './boolean-both-query.model'
3
4// These query parameters can be used with any endpoint that list videos
5export interface VideosCommonQuery {
6 start?: number
7 count?: number
8 sort?: string
9
10 nsfw?: BooleanBothQuery
11
12 isLive?: boolean
13
14 categoryOneOf?: number[]
15
16 licenceOneOf?: number[]
17
18 languageOneOf?: string[]
19
20 tagsOneOf?: string[]
21 tagsAllOf?: string[]
22
23 filter?: VideoFilter
24}
25
26export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
27 search?: string
28}
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts
index 3ce4ff73e..406f6cab2 100644
--- a/shared/models/search/videos-search-query.model.ts
+++ b/shared/models/search/videos-search-query.model.ts
@@ -1,33 +1,15 @@
1import { VideoFilter } from '../videos'
2import { NSFWQuery } from './nsfw-query.model'
3import { SearchTargetQuery } from './search-target-query.model' 1import { SearchTargetQuery } from './search-target-query.model'
2import { VideosCommonQuery } from './videos-common-query.model'
4 3
5export interface VideosSearchQuery extends SearchTargetQuery { 4export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery {
6 search?: string 5 search?: string
7 6
8 start?: number
9 count?: number
10 sort?: string
11
12 startDate?: string // ISO 8601 7 startDate?: string // ISO 8601
13 endDate?: string // ISO 8601 8 endDate?: string // ISO 8601
14 9
15 originallyPublishedStartDate?: string // ISO 8601 10 originallyPublishedStartDate?: string // ISO 8601
16 originallyPublishedEndDate?: string // ISO 8601 11 originallyPublishedEndDate?: string // ISO 8601
17 12
18 nsfw?: NSFWQuery
19
20 categoryOneOf?: number[]
21
22 licenceOneOf?: number[]
23
24 languageOneOf?: string[]
25
26 tagsOneOf?: string[]
27 tagsAllOf?: string[]
28
29 durationMin?: number // seconds 13 durationMin?: number // seconds
30 durationMax?: number // seconds 14 durationMax?: number // seconds
31
32 filter?: VideoFilter
33} 15}
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 1fffe7ddf..da51732ad 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -210,6 +210,7 @@ paths:
210 parameters: 210 parameters:
211 - $ref: '#/components/parameters/name' 211 - $ref: '#/components/parameters/name'
212 - $ref: '#/components/parameters/categoryOneOf' 212 - $ref: '#/components/parameters/categoryOneOf'
213 - $ref: '#/components/parameters/isLive'
213 - $ref: '#/components/parameters/tagsOneOf' 214 - $ref: '#/components/parameters/tagsOneOf'
214 - $ref: '#/components/parameters/tagsAllOf' 215 - $ref: '#/components/parameters/tagsAllOf'
215 - $ref: '#/components/parameters/licenceOneOf' 216 - $ref: '#/components/parameters/licenceOneOf'
@@ -781,6 +782,7 @@ paths:
781 - Videos 782 - Videos
782 parameters: 783 parameters:
783 - $ref: '#/components/parameters/categoryOneOf' 784 - $ref: '#/components/parameters/categoryOneOf'
785 - $ref: '#/components/parameters/isLive'
784 - $ref: '#/components/parameters/tagsOneOf' 786 - $ref: '#/components/parameters/tagsOneOf'
785 - $ref: '#/components/parameters/tagsAllOf' 787 - $ref: '#/components/parameters/tagsAllOf'
786 - $ref: '#/components/parameters/licenceOneOf' 788 - $ref: '#/components/parameters/licenceOneOf'
@@ -1086,6 +1088,7 @@ paths:
1086 - Video 1088 - Video
1087 parameters: 1089 parameters:
1088 - $ref: '#/components/parameters/categoryOneOf' 1090 - $ref: '#/components/parameters/categoryOneOf'
1091 - $ref: '#/components/parameters/isLive'
1089 - $ref: '#/components/parameters/tagsOneOf' 1092 - $ref: '#/components/parameters/tagsOneOf'
1090 - $ref: '#/components/parameters/tagsAllOf' 1093 - $ref: '#/components/parameters/tagsAllOf'
1091 - $ref: '#/components/parameters/licenceOneOf' 1094 - $ref: '#/components/parameters/licenceOneOf'
@@ -2194,6 +2197,7 @@ paths:
2194 parameters: 2197 parameters:
2195 - $ref: '#/components/parameters/channelHandle' 2198 - $ref: '#/components/parameters/channelHandle'
2196 - $ref: '#/components/parameters/categoryOneOf' 2199 - $ref: '#/components/parameters/categoryOneOf'
2200 - $ref: '#/components/parameters/isLive'
2197 - $ref: '#/components/parameters/tagsOneOf' 2201 - $ref: '#/components/parameters/tagsOneOf'
2198 - $ref: '#/components/parameters/tagsAllOf' 2202 - $ref: '#/components/parameters/tagsAllOf'
2199 - $ref: '#/components/parameters/licenceOneOf' 2203 - $ref: '#/components/parameters/licenceOneOf'
@@ -2841,6 +2845,7 @@ paths:
2841 schema: 2845 schema:
2842 type: string 2846 type: string
2843 - $ref: '#/components/parameters/categoryOneOf' 2847 - $ref: '#/components/parameters/categoryOneOf'
2848 - $ref: '#/components/parameters/isLive'
2844 - $ref: '#/components/parameters/tagsOneOf' 2849 - $ref: '#/components/parameters/tagsOneOf'
2845 - $ref: '#/components/parameters/tagsAllOf' 2850 - $ref: '#/components/parameters/tagsAllOf'
2846 - $ref: '#/components/parameters/licenceOneOf' 2851 - $ref: '#/components/parameters/licenceOneOf'
@@ -3809,6 +3814,13 @@ components:
3809 description: The comment id 3814 description: The comment id
3810 schema: 3815 schema:
3811 type: integer 3816 type: integer
3817 isLive:
3818 name: isLive
3819 in: query
3820 required: false
3821 description: whether or not the video is a live
3822 schema:
3823 type: boolean
3812 categoryOneOf: 3824 categoryOneOf:
3813 name: categoryOneOf 3825 name: categoryOneOf
3814 in: query 3826 in: query