aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+admin/admin.component.html2
-rw-r--r--client/src/app/+admin/admin.module.ts13
-rw-r--r--client/src/app/+admin/follows/follows.component.html6
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts5
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts2
-rw-r--r--client/src/app/+admin/follows/shared/redundancy.service.ts28
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/index.ts1
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html82
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss37
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts178
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html24
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss8
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts11
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts15
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/shared/images/global-icon.component.ts1
-rw-r--r--client/src/app/shared/instance/instance-statistics.component.ts23
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/shared/video/redundancy.service.ts73
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts28
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts3
-rw-r--r--client/src/app/shared/video/video.model.ts4
23 files changed, 489 insertions, 65 deletions
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html
index 9a3d90c18..0d06aaedc 100644
--- a/client/src/app/+admin/admin.component.html
+++ b/client/src/app/+admin/admin.component.html
@@ -5,7 +5,7 @@
5 </a> 5 </a>
6 6
7 <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> 7 <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
8 Manage follows 8 Follows & redundancies
9 </a> 9 </a>
10 10
11 <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page"> 11 <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page">
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 9c56b5750..fdbe70314 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' 8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -16,7 +16,6 @@ import {
16} from './moderation' 16} from './moderation'
17import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 17import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
18import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 18import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
19import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
20import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 19import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
21import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' 20import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
22import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' 21import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system'
@@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin-
27import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' 26import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component'
28import { SelectButtonModule } from 'primeng/selectbutton' 27import { SelectButtonModule } from 'primeng/selectbutton'
29import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
30import { ChartModule } from 'primeng/chart'
30 31
31@NgModule({ 32@NgModule({
32 imports: [ 33 imports: [
33 AdminRoutingModule, 34 AdminRoutingModule,
35
36 SharedModule,
37
34 TableModule, 38 TableModule,
35 SelectButtonModule, 39 SelectButtonModule,
36 SharedModule 40 ChartModule
37 ], 41 ],
38 42
39 declarations: [ 43 declarations: [
@@ -44,6 +48,8 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
44 FollowersListComponent, 48 FollowersListComponent,
45 FollowingListComponent, 49 FollowingListComponent,
46 RedundancyCheckboxComponent, 50 RedundancyCheckboxComponent,
51 VideoRedundanciesListComponent,
52 VideoRedundancyInformationComponent,
47 53
48 UsersComponent, 54 UsersComponent,
49 UserCreateComponent, 55 UserCreateComponent,
@@ -78,7 +84,6 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
78 ], 84 ],
79 85
80 providers: [ 86 providers: [
81 RedundancyService,
82 JobService, 87 JobService,
83 LogsService, 88 LogsService,
84 DebugService, 89 DebugService,
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html
index 21d477132..46581daf9 100644
--- a/client/src/app/+admin/follows/follows.component.html
+++ b/client/src/app/+admin/follows/follows.component.html
@@ -1,5 +1,5 @@
1<div class="admin-sub-header"> 1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Manage follows</div> 2 <div i18n class="form-sub-title">Follows & redundancies</div>
3 3
4 <div class="admin-sub-nav"> 4 <div class="admin-sub-nav">
5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a> 5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a>
@@ -7,7 +7,9 @@
7 <a i18n routerLink="following-add" routerLinkActive="active">Follow</a> 7 <a i18n routerLink="following-add" routerLinkActive="active">Follow</a>
8 8
9 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> 9 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a>
10
11 <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a>
10 </div> 12 </div>
11</div> 13</div>
12 14
13<router-outlet></router-outlet> \ No newline at end of file 15<router-outlet></router-outlet>
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts
index e84c79e82..298733eb0 100644
--- a/client/src/app/+admin/follows/follows.routes.ts
+++ b/client/src/app/+admin/follows/follows.routes.ts
@@ -6,6 +6,7 @@ import { FollowingAddComponent } from './following-add'
6import { FollowersListComponent } from './followers-list' 6import { FollowersListComponent } from './followers-list'
7import { UserRight } from '../../../../../shared' 7import { UserRight } from '../../../../../shared'
8import { FollowingListComponent } from './following-list/following-list.component' 8import { FollowingListComponent } from './following-list/following-list.component'
9import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
9 10
10export const FollowsRoutes: Routes = [ 11export const FollowsRoutes: Routes = [
11 { 12 {
@@ -47,6 +48,10 @@ export const FollowsRoutes: Routes = [
47 title: 'Add follow' 48 title: 'Add follow'
48 } 49 }
49 } 50 }
51 },
52 {
53 path: 'video-redundancies-list',
54 component: VideoRedundanciesListComponent
50 } 55 }
51 ] 56 ]
52 } 57 }
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts
index e94f33710..4fcb35cb1 100644
--- a/client/src/app/+admin/follows/index.ts
+++ b/client/src/app/+admin/follows/index.ts
@@ -1,5 +1,6 @@
1export * from './following-add' 1export * from './following-add'
2export * from './followers-list' 2export * from './followers-list'
3export * from './following-list' 3export * from './following-list'
4export * from './video-redundancies-list'
4export * from './follows.component' 5export * from './follows.component'
5export * from './follows.routes' 6export * from './follows.routes'
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
index fa1da26bf..9d7883d97 100644
--- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
+++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
@@ -1,7 +1,7 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 4import { RedundancyService } from '@app/shared/video/redundancy.service'
5 5
6@Component({ 6@Component({
7 selector: 'my-redundancy-checkbox', 7 selector: 'my-redundancy-checkbox',
diff --git a/client/src/app/+admin/follows/shared/redundancy.service.ts b/client/src/app/+admin/follows/shared/redundancy.service.ts
deleted file mode 100644
index 87ae01c04..000000000
--- a/client/src/app/+admin/follows/shared/redundancy.service.ts
+++ /dev/null
@@ -1,28 +0,0 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/shared'
5import { environment } from '../../../../environments/environment'
6
7@Injectable()
8export class RedundancyService {
9 static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/server/redundancy'
10
11 constructor (
12 private authHttp: HttpClient,
13 private restExtractor: RestExtractor
14 ) { }
15
16 updateRedundancy (host: string, redundancyAllowed: boolean) {
17 const url = RedundancyService.BASE_USER_SUBSCRIPTIONS_URL + '/' + host
18
19 const body = { redundancyAllowed }
20
21 return this.authHttp.put(url, body)
22 .pipe(
23 map(this.restExtractor.extractDataBool),
24 catchError(err => this.restExtractor.handleError(err))
25 )
26 }
27
28}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/index.ts b/client/src/app/+admin/follows/video-redundancies-list/index.ts
new file mode 100644
index 000000000..6a7c7f483
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/index.ts
@@ -0,0 +1 @@
export * from './video-redundancies-list.component'
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
new file mode 100644
index 000000000..80c66ec60
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
@@ -0,0 +1,82 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Video redundancies list</div>
3
4 <div class="select-filter-block">
5 <label for="displayType" i18n>Display</label>
6
7 <div class="peertube-select-container">
8 <select id="displayType" name="displayType" [(ngModel)]="displayType" (ngModelChange)="onDisplayTypeChanged()">
9 <option value="my-videos">My videos duplicated by remote instances</option>
10 <option value="remote-videos">Remote videos duplicated by my instance</option>
11 </select>
12 </div>
13 </div>
14</div>
15
16<p-table
17 [value]="videoRedundancies" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
18 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
19>
20 <ng-template pTemplate="header">
21 <tr>
22 <th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
23 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
24 <th i18n>Video URL</th>
25 <th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
26 <th></th>
27 </tr>
28 </ng-template>
29
30 <ng-template pTemplate="body" let-redundancy>
31 <tr class="expander" [pRowToggler]="redundancy">
32 <td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td>
33
34 <td>{{ redundancy.name }}</td>
35
36 <td>
37 <a target="_blank" rel="noopener noreferrer" [href]="redundancy.url">{{ redundancy.url }}</a>
38 </td>
39
40 <td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td>
41
42 <td class="action-cell">
43 <my-delete-button (click)="removeRedundancy(redundancy)"></my-delete-button>
44 </td>
45 </tr>
46 </ng-template>
47
48 <ng-template pTemplate="rowexpansion" let-redundancy>
49 <tr>
50 <td colspan="2">
51 <div *ngFor="let file of redundancy.redundancies.files" class="expansion-block">
52 <my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information>
53 </div>
54 </td>
55 </tr>
56
57 <tr>
58 <td colspan="2">
59 <div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists">
60 <my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information>
61 </div>
62 </td>
63 </tr>
64 </ng-template>
65</p-table>
66
67
68<div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos()">
69 <div class="form-sub-title" i18n>Enabled strategies stats</div>
70
71 <div class="chart-blocks">
72
73 <div *ngIf="noRedundancies" i18n class="no-results">
74 No redundancy strategy is enabled on your instance.
75 </div>
76
77 <div class="chart-block" *ngFor="let r of redundanciesGraphsData">
78 <p-chart type="pie" [data]="r.graphData" [options]="r.options" width="300px" height="300px"></p-chart>
79 </div>
80
81 </div>
82</div>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss
new file mode 100644
index 000000000..05018c281
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss
@@ -0,0 +1,37 @@
1@import '_variables';
2@import '_mixins';
3
4.expansion-block {
5 margin-bottom: 20px;
6}
7
8.admin-sub-header {
9 align-items: flex-end;
10
11 .select-filter-block {
12 &:not(:last-child) {
13 margin-right: 10px;
14 }
15
16 label {
17 margin-bottom: 2px;
18 }
19
20 .peertube-select-container {
21 @include peertube-select-container(auto);
22 }
23 }
24}
25
26.redundancies-charts {
27 margin-top: 50px;
28
29 .chart-blocks {
30 display: flex;
31 justify-content: center;
32
33 .chart-block {
34 margin: 0 20px;
35 }
36 }
37}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
new file mode 100644
index 000000000..4b41d1d86
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
@@ -0,0 +1,178 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { SortMeta } from 'primeng/api'
4import { ConfirmService } from '../../../core/confirm/confirm.service'
5import { RestPagination, RestTable } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
8import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
9import { VideosRedundancyStats } from '@shared/models/server'
10import { BytesPipe } from 'ngx-pipes'
11import { RedundancyService } from '@app/shared/video/redundancy.service'
12
13@Component({
14 selector: 'my-video-redundancies-list',
15 templateUrl: './video-redundancies-list.component.html',
16 styleUrls: [ './video-redundancies-list.component.scss' ]
17})
18export class VideoRedundanciesListComponent extends RestTable implements OnInit {
19 private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type'
20
21 videoRedundancies: VideoRedundancy[] = []
22 totalRecords = 0
23 rowsPerPage = 10
24
25 sort: SortMeta = { field: 'name', order: 1 }
26 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
27 displayType: VideoRedundanciesTarget = 'my-videos'
28
29 redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = []
30
31 noRedundancies = false
32
33 private bytesPipe: BytesPipe
34
35 constructor (
36 private notifier: Notifier,
37 private confirmService: ConfirmService,
38 private redundancyService: RedundancyService,
39 private serverService: ServerService,
40 private i18n: I18n
41 ) {
42 super()
43
44 this.bytesPipe = new BytesPipe()
45 }
46
47 ngOnInit () {
48 this.loadSelectLocalStorage()
49
50 this.initialize()
51
52 this.serverService.getServerStats()
53 .subscribe(res => {
54 const redundancies = res.videosRedundancy
55
56 if (redundancies.length === 0) this.noRedundancies = true
57
58 for (const r of redundancies) {
59 this.buildPieData(r)
60 }
61 })
62 }
63
64 isDisplayingRemoteVideos () {
65 return this.displayType === 'remote-videos'
66 }
67
68 getTotalSize (redundancy: VideoRedundancy) {
69 return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) +
70 redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0)
71 }
72
73 onDisplayTypeChanged () {
74 this.pagination.start = 0
75 this.saveSelectLocalStorage()
76
77 this.loadData()
78 }
79
80 getRedundancyStrategy (redundancy: VideoRedundancy) {
81 if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy
82 if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy
83
84 return ''
85 }
86
87 buildPieData (stats: VideosRedundancyStats) {
88 const totalSize = stats.totalSize
89 ? stats.totalSize - stats.totalUsed
90 : stats.totalUsed
91
92 if (totalSize === 0) return
93
94 this.redundanciesGraphsData.push({
95 stats,
96 graphData: {
97 labels: [ this.i18n('Used'), this.i18n('Available') ],
98 datasets: [
99 {
100 data: [ stats.totalUsed, totalSize ],
101 backgroundColor: [
102 '#FF6384',
103 '#36A2EB'
104 ],
105 hoverBackgroundColor: [
106 '#FF6384',
107 '#36A2EB'
108 ]
109 }
110 ]
111 },
112 options: {
113 title: {
114 display: true,
115 text: stats.strategy
116 },
117
118 tooltips: {
119 callbacks: {
120 label: (tooltipItem: any, data: any) => {
121 const dataset = data.datasets[tooltipItem.datasetIndex]
122 let label = data.labels[tooltipItem.index]
123 if (label) label += ': '
124 else label = ''
125
126 label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1)
127 return label
128 }
129 }
130 }
131 }
132 })
133 }
134
135 async removeRedundancy (redundancy: VideoRedundancy) {
136 const message = this.i18n('Do you really want to remove this video redundancy?')
137 const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy'))
138 if (res === false) return
139
140 this.redundancyService.removeVideoRedundancies(redundancy)
141 .subscribe(
142 () => {
143 this.notifier.success(this.i18n('Video redundancies removed!'))
144 this.loadData()
145 },
146
147 err => this.notifier.error(err.message)
148 )
149
150 }
151
152 protected loadData () {
153 const options = {
154 pagination: this.pagination,
155 sort: this.sort,
156 target: this.displayType
157 }
158
159 this.redundancyService.listVideoRedundancies(options)
160 .subscribe(
161 resultList => {
162 this.videoRedundancies = resultList.data
163 this.totalRecords = resultList.total
164 },
165
166 err => this.notifier.error(err.message)
167 )
168 }
169
170 private loadSelectLocalStorage () {
171 const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE)
172 if (displayType) this.displayType = displayType as VideoRedundanciesTarget
173 }
174
175 private saveSelectLocalStorage () {
176 peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType)
177 }
178}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html
new file mode 100644
index 000000000..a379520e3
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html
@@ -0,0 +1,24 @@
1<div>
2 <span class="label">Url</span>
3 <a target="_blank" rel="noopener noreferrer" [href]="redundancyElement.fileUrl">{{ redundancyElement.fileUrl }}</a>
4</div>
5
6<div>
7 <span class="label">Created on</span>
8 <span>{{ redundancyElement.createdAt | date: 'medium' }}</span>
9</div>
10
11<div>
12 <span class="label">Expires on</span>
13 <span>{{ redundancyElement.expiresOn | date: 'medium' }}</span>
14</div>
15
16<div>
17 <span class="label">Size</span>
18 <span>{{ redundancyElement.size | bytes: 1 }}</span>
19</div>
20
21<div *ngIf="redundancyElement.strategy">
22 <span class="label">Strategy</span>
23 <span>{{ redundancyElement.strategy }}</span>
24</div>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss
new file mode 100644
index 000000000..6b09fbb01
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss
@@ -0,0 +1,8 @@
1@import '_variables';
2@import '_mixins';
3
4.label {
5 display: inline-block;
6 min-width: 100px;
7 font-weight: $font-semibold;
8}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts
new file mode 100644
index 000000000..6f3090c08
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models'
3
4@Component({
5 selector: 'my-video-redundancy-information',
6 templateUrl: './video-redundancy-information.component.html',
7 styleUrls: [ './video-redundancy-information.component.scss' ]
8})
9export class VideoRedundancyInformationComponent {
10 @Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation
11}
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index 20c8ea71a..bc40452cf 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -16,8 +16,8 @@ import { JobTypeClient } from '../../../../types/job-type-client.type'
16 styleUrls: [ './jobs.component.scss' ] 16 styleUrls: [ './jobs.component.scss' ]
17}) 17})
18export class JobsComponent extends RestTable implements OnInit { 18export class JobsComponent extends RestTable implements OnInit {
19 private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' 19 private static LOCAL_STORAGE_STATE = 'jobs-list-state'
20 private static JOB_STATE_LOCAL_STORAGE_TYPE = 'jobs-list-type' 20 private static LOCAL_STORAGE_TYPE = 'jobs-list-type'
21 21
22 jobState: JobStateClient = 'waiting' 22 jobState: JobStateClient = 'waiting'
23 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] 23 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ]
@@ -34,7 +34,8 @@ export class JobsComponent extends RestTable implements OnInit {
34 'video-file-import', 34 'video-file-import',
35 'video-import', 35 'video-import',
36 'videos-views', 36 'videos-views',
37 'activitypub-refresher' 37 'activitypub-refresher',
38 'video-redundancy'
38 ] 39 ]
39 40
40 jobs: Job[] = [] 41 jobs: Job[] = []
@@ -77,15 +78,15 @@ export class JobsComponent extends RestTable implements OnInit {
77 } 78 }
78 79
79 private loadJobStateAndType () { 80 private loadJobStateAndType () {
80 const state = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE) 81 const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE)
81 if (state) this.jobState = state as JobState 82 if (state) this.jobState = state as JobState
82 83
83 const type = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE) 84 const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE)
84 if (type) this.jobType = type as JobType 85 if (type) this.jobType = type as JobType
85 } 86 }
86 87
87 private saveJobStateAndType () { 88 private saveJobStateAndType () {
88 peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) 89 peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState)
89 peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE, this.jobType) 90 peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType)
90 } 91 }
91} 92}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index cdcbcb528..1f6cfb596 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -9,6 +9,7 @@ import { VideoConstant } from '../../../../../shared/models/videos'
9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' 9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
11import { sortBy } from '@app/shared/misc/utils' 11import { sortBy } from '@app/shared/misc/utils'
12import { ServerStats } from '@shared/models/server'
12 13
13@Injectable() 14@Injectable()
14export class ServerService { 15export class ServerService {
@@ -16,6 +17,8 @@ export class ServerService {
16 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 17 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
17 private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' 18 private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
18 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' 19 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
20 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
21
19 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' 22 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
20 23
21 configReloaded = new Subject<void>() 24 configReloaded = new Subject<void>()
@@ -235,6 +238,10 @@ export class ServerService {
235 return this.localeObservable.pipe(first()) 238 return this.localeObservable.pipe(first())
236 } 239 }
237 240
241 getServerStats () {
242 return this.http.get<ServerStats>(ServerService.BASE_STATS_URL)
243 }
244
238 private loadAttributeEnum <T extends string | number> ( 245 private loadAttributeEnum <T extends string | number> (
239 baseUrl: string, 246 baseUrl: string,
240 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 247 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index 806aca347..b6e641228 100644
--- a/client/src/app/shared/images/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
@@ -1,6 +1,5 @@
1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'
2import { HooksService } from '@app/core/plugins/hooks.service' 2import { HooksService } from '@app/core/plugins/hooks.service'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4 3
5const icons = { 4const icons = {
6 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), 5 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'),
diff --git a/client/src/app/shared/instance/instance-statistics.component.ts b/client/src/app/shared/instance/instance-statistics.component.ts
index 8ec728f05..40aa8a4c0 100644
--- a/client/src/app/shared/instance/instance-statistics.component.ts
+++ b/client/src/app/shared/instance/instance-statistics.component.ts
@@ -1,9 +1,6 @@
1import { map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { ServerStats } from '@shared/models/server' 2import { ServerStats } from '@shared/models/server'
6import { environment } from '../../../environments/environment' 3import { ServerService } from '@app/core'
7 4
8@Component({ 5@Component({
9 selector: 'my-instance-statistics', 6 selector: 'my-instance-statistics',
@@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment'
11 styleUrls: [ './instance-statistics.component.scss' ] 8 styleUrls: [ './instance-statistics.component.scss' ]
12}) 9})
13export class InstanceStatisticsComponent implements OnInit { 10export class InstanceStatisticsComponent implements OnInit {
14 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
15
16 serverStats: ServerStats = null 11 serverStats: ServerStats = null
17 12
18 constructor ( 13 constructor (
19 private http: HttpClient, 14 private serverService: ServerService
20 private i18n: I18n
21 ) { 15 ) {
22 } 16 }
23 17
24 ngOnInit () { 18 ngOnInit () {
25 this.getStats() 19 this.serverService.getServerStats()
26 .subscribe( 20 .subscribe(res => this.serverStats = res)
27 res => {
28 this.serverStats = res
29 }
30 )
31 }
32
33 getStats () {
34 return this.http
35 .get<ServerStats>(InstanceStatisticsComponent.BASE_STATS_URL)
36 } 21 }
37} 22}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b2eb13f73..d06d37d8c 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -98,6 +98,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
98import { MultiSelectModule } from 'primeng/multiselect' 98import { MultiSelectModule } from 'primeng/multiselect'
99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' 99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' 100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
101import { RedundancyService } from '@app/shared/video/redundancy.service'
101 102
102@NgModule({ 103@NgModule({
103 imports: [ 104 imports: [
@@ -300,6 +301,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
300 UserNotificationService, 301 UserNotificationService,
301 302
302 FollowService, 303 FollowService,
304 RedundancyService,
303 305
304 I18n 306 I18n
305 ] 307 ]
diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts
new file mode 100644
index 000000000..fb918d73b
--- /dev/null
+++ b/client/src/app/shared/video/redundancy.service.ts
@@ -0,0 +1,73 @@
1import { catchError, map, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, RestPagination, RestService } from '@app/shared/rest'
5import { SortMeta } from 'primeng/api'
6import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
7import { concat, Observable } from 'rxjs'
8import { environment } from '../../../environments/environment'
9
10@Injectable()
11export class RedundancyService {
12 static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) { }
19
20 updateRedundancy (host: string, redundancyAllowed: boolean) {
21 const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host
22
23 const body = { redundancyAllowed }
24
25 return this.authHttp.put(url, body)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 listVideoRedundancies (options: {
33 pagination: RestPagination,
34 sort: SortMeta,
35 target?: VideoRedundanciesTarget
36 }): Observable<ResultList<VideoRedundancy>> {
37 const { pagination, sort, target } = options
38
39 let params = new HttpParams()
40 params = this.restService.addRestGetParams(params, pagination, sort)
41
42 if (target) params = params.append('target', target)
43
44 return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params })
45 .pipe(
46 catchError(res => this.restExtractor.handleError(res))
47 )
48 }
49
50 addVideoRedundancy (video: Video) {
51 return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id })
52 .pipe(
53 catchError(res => this.restExtractor.handleError(res))
54 )
55 }
56
57 removeVideoRedundancies (redundancy: VideoRedundancy) {
58 const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id)
59 .concat(redundancy.redundancies.files.map(r => r.id))
60 .map(id => this.removeRedundancy(id))
61
62 return concat(...observables)
63 .pipe(toArray())
64 }
65
66 private removeRedundancy (redundancyId: number) {
67 return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId)
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 catchError(res => this.restExtractor.handleError(res))
71 )
72 }
73}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
index afdeab18d..390d74c52 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -14,6 +14,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
14import { VideoBlacklistService } from '@app/shared/video-blacklist' 14import { VideoBlacklistService } from '@app/shared/video-blacklist'
15import { ScreenService } from '@app/shared/misc/screen.service' 15import { ScreenService } from '@app/shared/misc/screen.service'
16import { VideoCaption } from '@shared/models' 16import { VideoCaption } from '@shared/models'
17import { RedundancyService } from '@app/shared/video/redundancy.service'
17 18
18export type VideoActionsDisplayType = { 19export type VideoActionsDisplayType = {
19 playlist?: boolean 20 playlist?: boolean
@@ -22,6 +23,7 @@ export type VideoActionsDisplayType = {
22 blacklist?: boolean 23 blacklist?: boolean
23 delete?: boolean 24 delete?: boolean
24 report?: boolean 25 report?: boolean
26 duplicate?: boolean
25} 27}
26 28
27@Component({ 29@Component({
@@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
46 update: true, 48 update: true,
47 blacklist: true, 49 blacklist: true,
48 delete: true, 50 delete: true,
49 report: true 51 report: true,
52 duplicate: true
50 } 53 }
51 @Input() placement = 'left' 54 @Input() placement = 'left'
52 55
@@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
74 private screenService: ScreenService, 77 private screenService: ScreenService,
75 private videoService: VideoService, 78 private videoService: VideoService,
76 private blocklistService: BlocklistService, 79 private blocklistService: BlocklistService,
80 private redundancyService: RedundancyService,
77 private i18n: I18n 81 private i18n: I18n
78 ) { } 82 ) { }
79 83
@@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
144 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled 148 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
145 } 149 }
146 150
151 canVideoBeDuplicated () {
152 return this.video.canBeDuplicatedBy(this.user)
153 }
154
147 /* Action handlers */ 155 /* Action handlers */
148 156
149 async unblacklistVideo () { 157 async unblacklistVideo () {
@@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges {
186 ) 194 )
187 } 195 }
188 196
197 duplicateVideo () {
198 this.redundancyService.addVideoRedundancy(this.video)
199 .subscribe(
200 () => {
201 const message = this.i18n('This video will be duplicated by your instance.')
202 this.notifier.success(message)
203 },
204
205 err => this.notifier.error(err.message)
206 )
207 }
208
189 onVideoBlacklisted () { 209 onVideoBlacklisted () {
190 this.videoBlacklisted.emit() 210 this.videoBlacklisted.emit()
191 } 211 }
@@ -234,6 +254,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
234 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() 254 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable()
235 }, 255 },
236 { 256 {
257 label: this.i18n('Duplicate (redundancy)'),
258 handler: () => this.duplicateVideo(),
259 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(),
260 iconName: 'cloud-download'
261 },
262 {
237 label: this.i18n('Delete'), 263 label: this.i18n('Delete'),
238 handler: () => this.removeVideo(), 264 handler: () => this.removeVideo(),
239 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), 265 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(),
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 598a7a983..1dfb3eec7 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -64,7 +64,8 @@ export class VideoMiniatureComponent implements OnInit {
64 update: true, 64 update: true,
65 blacklist: true, 65 blacklist: true,
66 delete: true, 66 delete: true,
67 report: true 67 report: true,
68 duplicate: false
68 } 69 }
69 showActions = false 70 showActions = false
70 serverConfig: ServerConfig 71 serverConfig: ServerConfig
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index fb98d5382..9eeaf41b0 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -152,4 +152,8 @@ export class Video implements VideoServerModel {
152 isUpdatableBy (user: AuthUser) { 152 isUpdatableBy (user: AuthUser) {
153 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) 153 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
154 } 154 }
155
156 canBeDuplicatedBy (user: AuthUser) {
157 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
158 }
155} 159}