From b764380ac23f4e9d4677d08acdc3474c2931a16d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 10 Jan 2020 10:11:28 +0100 Subject: Add ability to list redundancies --- client/src/app/+admin/admin.component.html | 2 +- client/src/app/+admin/admin.module.ts | 13 +- .../src/app/+admin/follows/follows.component.html | 6 +- client/src/app/+admin/follows/follows.routes.ts | 5 + client/src/app/+admin/follows/index.ts | 1 + .../shared/redundancy-checkbox.component.ts | 2 +- .../+admin/follows/shared/redundancy.service.ts | 28 ---- .../follows/video-redundancies-list/index.ts | 1 + .../video-redundancies-list.component.html | 82 ++++++++++ .../video-redundancies-list.component.scss | 37 +++++ .../video-redundancies-list.component.ts | 178 +++++++++++++++++++++ .../video-redundancy-information.component.html | 24 +++ .../video-redundancy-information.component.scss | 8 + .../video-redundancy-information.component.ts | 11 ++ .../src/app/+admin/system/jobs/jobs.component.ts | 15 +- client/src/app/core/server/server.service.ts | 7 + .../src/app/shared/images/global-icon.component.ts | 1 - .../instance/instance-statistics.component.ts | 23 +-- client/src/app/shared/shared.module.ts | 2 + client/src/app/shared/video/redundancy.service.ts | 73 +++++++++ .../video/video-actions-dropdown.component.ts | 28 +++- .../app/shared/video/video-miniature.component.ts | 3 +- client/src/app/shared/video/video.model.ts | 4 + 23 files changed, 489 insertions(+), 65 deletions(-) delete mode 100644 client/src/app/+admin/follows/shared/redundancy.service.ts create mode 100644 client/src/app/+admin/follows/video-redundancies-list/index.ts create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss create mode 100644 client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts create mode 100644 client/src/app/shared/video/redundancy.service.ts (limited to 'client/src') 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 @@ - Manage follows + Follows & redundancies 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' import { SharedModule } from '../shared' import { AdminRoutingModule } from './admin-routing.module' import { AdminComponent } from './admin.component' -import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' +import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' import { FollowingListComponent } from './follows/following-list/following-list.component' import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' import { @@ -16,7 +16,6 @@ import { } from './moderation' import { ModerationComponent } from '@app/+admin/moderation/moderation.component' import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' -import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' @@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin- import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' import { SelectButtonModule } from 'primeng/selectbutton' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' +import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' +import { ChartModule } from 'primeng/chart' @NgModule({ imports: [ AdminRoutingModule, + + SharedModule, + TableModule, SelectButtonModule, - SharedModule + ChartModule ], declarations: [ @@ -44,6 +48,8 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' FollowersListComponent, FollowingListComponent, RedundancyCheckboxComponent, + VideoRedundanciesListComponent, + VideoRedundancyInformationComponent, UsersComponent, UserCreateComponent, @@ -78,7 +84,6 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' ], providers: [ - RedundancyService, JobService, LogsService, 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 @@
-
Manage follows
+
Follows & redundancies
- \ No newline at end of file + 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' import { FollowersListComponent } from './followers-list' import { UserRight } from '../../../../../shared' import { FollowingListComponent } from './following-list/following-list.component' +import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list' export const FollowsRoutes: Routes = [ { @@ -47,6 +48,10 @@ export const FollowsRoutes: Routes = [ title: 'Add follow' } } + }, + { + path: 'video-redundancies-list', + component: VideoRedundanciesListComponent } ] } 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 @@ export * from './following-add' export * from './followers-list' export * from './following-list' +export * from './video-redundancies-list' export * from './follows.component' export * 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 @@ import { Component, Input } from '@angular/core' import { Notifier } from '@app/core' import { I18n } from '@ngx-translate/i18n-polyfill' -import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' +import { RedundancyService } from '@app/shared/video/redundancy.service' @Component({ 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 @@ -import { catchError, map } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { RestExtractor } from '@app/shared' -import { environment } from '../../../../environments/environment' - -@Injectable() -export class RedundancyService { - static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/server/redundancy' - - constructor ( - private authHttp: HttpClient, - private restExtractor: RestExtractor - ) { } - - updateRedundancy (host: string, redundancyAllowed: boolean) { - const url = RedundancyService.BASE_USER_SUBSCRIPTIONS_URL + '/' + host - - const body = { redundancyAllowed } - - return this.authHttp.put(url, body) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) - } - -} 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 @@ +
+
Video redundancies list
+ +
+ + +
+ +
+
+
+ + + + + Strategy + Video name + Video URL + Total size + + + + + + + {{ getRedundancyStrategy(redundancy) }} + + {{ redundancy.name }} + + + {{ redundancy.url }} + + + {{ getTotalSize(redundancy) | bytes: 1 }} + + + + + + + + + + +
+ +
+ + + + + +
+ +
+ + +
+
+ + +
+
Enabled strategies stats
+ +
+ +
+ No redundancy strategy is enabled on your instance. +
+ +
+ +
+ +
+
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 @@ +@import '_variables'; +@import '_mixins'; + +.expansion-block { + margin-bottom: 20px; +} + +.admin-sub-header { + align-items: flex-end; + + .select-filter-block { + &:not(:last-child) { + margin-right: 10px; + } + + label { + margin-bottom: 2px; + } + + .peertube-select-container { + @include peertube-select-container(auto); + } + } +} + +.redundancies-charts { + margin-top: 50px; + + .chart-blocks { + display: flex; + justify-content: center; + + .chart-block { + margin: 0 20px; + } + } +} 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 @@ +import { Component, OnInit } from '@angular/core' +import { Notifier, ServerService } from '@app/core' +import { SortMeta } from 'primeng/api' +import { ConfirmService } from '../../../core/confirm/confirm.service' +import { RestPagination, RestTable } from '../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' +import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' +import { VideosRedundancyStats } from '@shared/models/server' +import { BytesPipe } from 'ngx-pipes' +import { RedundancyService } from '@app/shared/video/redundancy.service' + +@Component({ + selector: 'my-video-redundancies-list', + templateUrl: './video-redundancies-list.component.html', + styleUrls: [ './video-redundancies-list.component.scss' ] +}) +export class VideoRedundanciesListComponent extends RestTable implements OnInit { + private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type' + + videoRedundancies: VideoRedundancy[] = [] + totalRecords = 0 + rowsPerPage = 10 + + sort: SortMeta = { field: 'name', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + displayType: VideoRedundanciesTarget = 'my-videos' + + redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = [] + + noRedundancies = false + + private bytesPipe: BytesPipe + + constructor ( + private notifier: Notifier, + private confirmService: ConfirmService, + private redundancyService: RedundancyService, + private serverService: ServerService, + private i18n: I18n + ) { + super() + + this.bytesPipe = new BytesPipe() + } + + ngOnInit () { + this.loadSelectLocalStorage() + + this.initialize() + + this.serverService.getServerStats() + .subscribe(res => { + const redundancies = res.videosRedundancy + + if (redundancies.length === 0) this.noRedundancies = true + + for (const r of redundancies) { + this.buildPieData(r) + } + }) + } + + isDisplayingRemoteVideos () { + return this.displayType === 'remote-videos' + } + + getTotalSize (redundancy: VideoRedundancy) { + return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) + + redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0) + } + + onDisplayTypeChanged () { + this.pagination.start = 0 + this.saveSelectLocalStorage() + + this.loadData() + } + + getRedundancyStrategy (redundancy: VideoRedundancy) { + if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy + if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy + + return '' + } + + buildPieData (stats: VideosRedundancyStats) { + const totalSize = stats.totalSize + ? stats.totalSize - stats.totalUsed + : stats.totalUsed + + if (totalSize === 0) return + + this.redundanciesGraphsData.push({ + stats, + graphData: { + labels: [ this.i18n('Used'), this.i18n('Available') ], + datasets: [ + { + data: [ stats.totalUsed, totalSize ], + backgroundColor: [ + '#FF6384', + '#36A2EB' + ], + hoverBackgroundColor: [ + '#FF6384', + '#36A2EB' + ] + } + ] + }, + options: { + title: { + display: true, + text: stats.strategy + }, + + tooltips: { + callbacks: { + label: (tooltipItem: any, data: any) => { + const dataset = data.datasets[tooltipItem.datasetIndex] + let label = data.labels[tooltipItem.index] + if (label) label += ': ' + else label = '' + + label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1) + return label + } + } + } + } + }) + } + + async removeRedundancy (redundancy: VideoRedundancy) { + const message = this.i18n('Do you really want to remove this video redundancy?') + const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy')) + if (res === false) return + + this.redundancyService.removeVideoRedundancies(redundancy) + .subscribe( + () => { + this.notifier.success(this.i18n('Video redundancies removed!')) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + + } + + protected loadData () { + const options = { + pagination: this.pagination, + sort: this.sort, + target: this.displayType + } + + this.redundancyService.listVideoRedundancies(options) + .subscribe( + resultList => { + this.videoRedundancies = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notifier.error(err.message) + ) + } + + private loadSelectLocalStorage () { + const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE) + if (displayType) this.displayType = displayType as VideoRedundanciesTarget + } + + private saveSelectLocalStorage () { + peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType) + } +} 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 @@ +
+ Url + {{ redundancyElement.fileUrl }} +
+ +
+ Created on + {{ redundancyElement.createdAt | date: 'medium' }} +
+ +
+ Expires on + {{ redundancyElement.expiresOn | date: 'medium' }} +
+ +
+ Size + {{ redundancyElement.size | bytes: 1 }} +
+ +
+ Strategy + {{ redundancyElement.strategy }} +
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 @@ +@import '_variables'; +@import '_mixins'; + +.label { + display: inline-block; + min-width: 100px; + font-weight: $font-semibold; +} 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 @@ +import { Component, Input } from '@angular/core' +import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models' + +@Component({ + selector: 'my-video-redundancy-information', + templateUrl: './video-redundancy-information.component.html', + styleUrls: [ './video-redundancy-information.component.scss' ] +}) +export class VideoRedundancyInformationComponent { + @Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation +} 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' styleUrls: [ './jobs.component.scss' ] }) export class JobsComponent extends RestTable implements OnInit { - private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' - private static JOB_STATE_LOCAL_STORAGE_TYPE = 'jobs-list-type' + private static LOCAL_STORAGE_STATE = 'jobs-list-state' + private static LOCAL_STORAGE_TYPE = 'jobs-list-type' jobState: JobStateClient = 'waiting' jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] @@ -34,7 +34,8 @@ export class JobsComponent extends RestTable implements OnInit { 'video-file-import', 'video-import', 'videos-views', - 'activitypub-refresher' + 'activitypub-refresher', + 'video-redundancy' ] jobs: Job[] = [] @@ -77,15 +78,15 @@ export class JobsComponent extends RestTable implements OnInit { } private loadJobStateAndType () { - const state = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE) + const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE) if (state) this.jobState = state as JobState - const type = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE) + const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE) if (type) this.jobType = type as JobType } private saveJobStateAndType () { - peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) - peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE, this.jobType) + peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState) + peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType) } } 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' import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' import { sortBy } from '@app/shared/misc/utils' +import { ServerStats } from '@shared/models/server' @Injectable() export class ServerService { @@ -16,6 +17,8 @@ export class ServerService { private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' + private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' + private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' configReloaded = new Subject() @@ -235,6 +238,10 @@ export class ServerService { return this.localeObservable.pipe(first()) } + getServerStats () { + return this.http.get(ServerService.BASE_STATS_URL) + } + private loadAttributeEnum ( baseUrl: string, 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 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' import { HooksService } from '@app/core/plugins/hooks.service' -import { I18n } from '@ngx-translate/i18n-polyfill' const icons = { '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 @@ -import { map } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' import { Component, OnInit } from '@angular/core' -import { I18n } from '@ngx-translate/i18n-polyfill' import { ServerStats } from '@shared/models/server' -import { environment } from '../../../environments/environment' +import { ServerService } from '@app/core' @Component({ selector: 'my-instance-statistics', @@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment' styleUrls: [ './instance-statistics.component.scss' ] }) export class InstanceStatisticsComponent implements OnInit { - private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' - serverStats: ServerStats = null constructor ( - private http: HttpClient, - private i18n: I18n + private serverService: ServerService ) { } ngOnInit () { - this.getStats() - .subscribe( - res => { - this.serverStats = res - } - ) - } - - getStats () { - return this.http - .get(InstanceStatisticsComponent.BASE_STATS_URL) + this.serverService.getServerStats() + .subscribe(res => this.serverStats = res) } } 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' import { MultiSelectModule } from 'primeng/multiselect' import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' +import { RedundancyService } from '@app/shared/video/redundancy.service' @NgModule({ imports: [ @@ -300,6 +301,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop UserNotificationService, FollowService, + RedundancyService, I18n ] 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 @@ +import { catchError, map, toArray } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/shared/rest' +import { SortMeta } from 'primeng/api' +import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' +import { concat, Observable } from 'rxjs' +import { environment } from '../../../environments/environment' + +@Injectable() +export class RedundancyService { + static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) { } + + updateRedundancy (host: string, redundancyAllowed: boolean) { + const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host + + const body = { redundancyAllowed } + + return this.authHttp.put(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + listVideoRedundancies (options: { + pagination: RestPagination, + sort: SortMeta, + target?: VideoRedundanciesTarget + }): Observable> { + const { pagination, sort, target } = options + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (target) params = params.append('target', target) + + return this.authHttp.get>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + addVideoRedundancy (video: Video) { + return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + removeVideoRedundancies (redundancy: VideoRedundancy) { + const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id) + .concat(redundancy.redundancies.files.map(r => r.id)) + .map(id => this.removeRedundancy(id)) + + return concat(...observables) + .pipe(toArray()) + } + + private removeRedundancy (redundancyId: number) { + return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } +} 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 import { VideoBlacklistService } from '@app/shared/video-blacklist' import { ScreenService } from '@app/shared/misc/screen.service' import { VideoCaption } from '@shared/models' +import { RedundancyService } from '@app/shared/video/redundancy.service' export type VideoActionsDisplayType = { playlist?: boolean @@ -22,6 +23,7 @@ export type VideoActionsDisplayType = { blacklist?: boolean delete?: boolean report?: boolean + duplicate?: boolean } @Component({ @@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges { update: true, blacklist: true, delete: true, - report: true + report: true, + duplicate: true } @Input() placement = 'left' @@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges { private screenService: ScreenService, private videoService: VideoService, private blocklistService: BlocklistService, + private redundancyService: RedundancyService, private i18n: I18n ) { } @@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges { return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled } + canVideoBeDuplicated () { + return this.video.canBeDuplicatedBy(this.user) + } + /* Action handlers */ async unblacklistVideo () { @@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges { ) } + duplicateVideo () { + this.redundancyService.addVideoRedundancy(this.video) + .subscribe( + () => { + const message = this.i18n('This video will be duplicated by your instance.') + this.notifier.success(message) + }, + + err => this.notifier.error(err.message) + ) + } + onVideoBlacklisted () { this.videoBlacklisted.emit() } @@ -233,6 +253,12 @@ export class VideoActionsDropdownComponent implements OnChanges { iconName: 'undo', isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() }, + { + label: this.i18n('Duplicate (redundancy)'), + handler: () => this.duplicateVideo(), + isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(), + iconName: 'cloud-download' + }, { label: this.i18n('Delete'), handler: () => this.removeVideo(), 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 { update: true, blacklist: true, delete: true, - report: true + report: true, + duplicate: false } showActions = false 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 { isUpdatableBy (user: AuthUser) { return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) } + + canBeDuplicatedBy (user: AuthUser) { + return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) + } } -- cgit v1.2.3