diff options
122 files changed, 3082 insertions, 994 deletions
diff --git a/client/package.json b/client/package.json index cd0a82aa4..e9c74787c 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -69,7 +69,7 @@ | |||
69 | "@types/node": "^10.9.2", | 69 | "@types/node": "^10.9.2", |
70 | "@types/sanitize-html": "1.18.0", | 70 | "@types/sanitize-html": "1.18.0", |
71 | "@types/socket.io-client": "^1.4.32", | 71 | "@types/socket.io-client": "^1.4.32", |
72 | "@types/video.js": "^7.2.5", | 72 | "@types/video.js": "^7.3.3", |
73 | "@types/webtorrent": "^0.107.0", | 73 | "@types/webtorrent": "^0.107.0", |
74 | "angular2-hotkeys": "^2.1.2", | 74 | "angular2-hotkeys": "^2.1.2", |
75 | "angularx-qrcode": "1.6.4", | 75 | "angularx-qrcode": "1.6.4", |
@@ -77,6 +77,7 @@ | |||
77 | "bootstrap": "^4.1.3", | 77 | "bootstrap": "^4.1.3", |
78 | "buffer": "^5.1.0", | 78 | "buffer": "^5.1.0", |
79 | "cache-chunk-store": "^3.0.0", | 79 | "cache-chunk-store": "^3.0.0", |
80 | "chart.js": "^2.9.3", | ||
80 | "codelyzer": "^5.0.1", | 81 | "codelyzer": "^5.0.1", |
81 | "core-js": "^3.1.4", | 82 | "core-js": "^3.1.4", |
82 | "css-loader": "^3.1.0", | 83 | "css-loader": "^3.1.0", |
@@ -132,6 +133,7 @@ | |||
132 | "videojs-dock": "^2.0.2", | 133 | "videojs-dock": "^2.0.2", |
133 | "videojs-hotkeys": "^0.2.21", | 134 | "videojs-hotkeys": "^0.2.21", |
134 | "videostream": "~3.2.1", | 135 | "videostream": "~3.2.1", |
136 | "vtt.js": "^0.13.0", | ||
135 | "webpack-bundle-analyzer": "^3.0.2", | 137 | "webpack-bundle-analyzer": "^3.0.2", |
136 | "webpack-cli": "^3.0.8", | 138 | "webpack-cli": "^3.0.8", |
137 | "webtorrent": "^0.107.16", | 139 | "webtorrent": "^0.107.16", |
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' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AdminRoutingModule } from './admin-routing.module' | 6 | import { AdminRoutingModule } from './admin-routing.module' |
7 | import { AdminComponent } from './admin.component' | 7 | import { AdminComponent } from './admin.component' |
8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' | 8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' |
9 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 9 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' | 10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' |
11 | import { | 11 | import { |
@@ -16,7 +16,6 @@ import { | |||
16 | } from './moderation' | 16 | } from './moderation' |
17 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 17 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
18 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 18 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
19 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | ||
20 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | 19 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' |
21 | import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' | 20 | import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' |
22 | import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' | 21 | import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' |
@@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin- | |||
27 | import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' | 26 | import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' |
28 | import { SelectButtonModule } from 'primeng/selectbutton' | 27 | import { SelectButtonModule } from 'primeng/selectbutton' |
29 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | 28 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' |
29 | import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' | ||
30 | import { 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' | |||
6 | import { FollowersListComponent } from './followers-list' | 6 | import { FollowersListComponent } from './followers-list' |
7 | import { UserRight } from '../../../../../shared' | 7 | import { UserRight } from '../../../../../shared' |
8 | import { FollowingListComponent } from './following-list/following-list.component' | 8 | import { FollowingListComponent } from './following-list/following-list.component' |
9 | import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list' | ||
9 | 10 | ||
10 | export const FollowsRoutes: Routes = [ | 11 | export 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 @@ | |||
1 | export * from './following-add' | 1 | export * from './following-add' |
2 | export * from './followers-list' | 2 | export * from './followers-list' |
3 | export * from './following-list' | 3 | export * from './following-list' |
4 | export * from './video-redundancies-list' | ||
4 | export * from './follows.component' | 5 | export * from './follows.component' |
5 | export * from './follows.routes' | 6 | 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | 4 | import { 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 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/shared' | ||
5 | import { environment } from '../../../../environments/environment' | ||
6 | |||
7 | @Injectable() | ||
8 | export 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { SortMeta } from 'primeng/api' | ||
4 | import { ConfirmService } from '../../../core/confirm/confirm.service' | ||
5 | import { RestPagination, RestTable } from '../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
8 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | ||
9 | import { VideosRedundancyStats } from '@shared/models/server' | ||
10 | import { BytesPipe } from 'ngx-pipes' | ||
11 | import { 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 | }) | ||
18 | export 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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 | }) |
18 | export class JobsComponent extends RestTable implements OnInit { | 18 | export 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' | |||
9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' | 9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' |
10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
11 | import { sortBy } from '@app/shared/misc/utils' | 11 | import { sortBy } from '@app/shared/misc/utils' |
12 | import { ServerStats } from '@shared/models/server' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class ServerService { | 15 | export 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 @@ | |||
1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' |
2 | import { HooksService } from '@app/core/plugins/hooks.service' | 2 | import { HooksService } from '@app/core/plugins/hooks.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | 3 | ||
5 | const icons = { | 4 | const 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 @@ | |||
1 | import { map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { ServerStats } from '@shared/models/server' | 2 | import { ServerStats } from '@shared/models/server' |
6 | import { environment } from '../../../environments/environment' | 3 | import { 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 | }) |
13 | export class InstanceStatisticsComponent implements OnInit { | 10 | export 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' | |||
98 | import { MultiSelectModule } from 'primeng/multiselect' | 98 | import { MultiSelectModule } from 'primeng/multiselect' |
99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' | 99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' |
100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' | 100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' |
101 | import { 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 @@ | |||
1 | import { catchError, map, toArray } from 'rxjs/operators' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor, RestPagination, RestService } from '@app/shared/rest' | ||
5 | import { SortMeta } from 'primeng/api' | ||
6 | import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
7 | import { concat, Observable } from 'rxjs' | ||
8 | import { environment } from '../../../environments/environment' | ||
9 | |||
10 | @Injectable() | ||
11 | export 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 | |||
14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' | 14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' |
15 | import { ScreenService } from '@app/shared/misc/screen.service' | 15 | import { ScreenService } from '@app/shared/misc/screen.service' |
16 | import { VideoCaption } from '@shared/models' | 16 | import { VideoCaption } from '@shared/models' |
17 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
17 | 18 | ||
18 | export type VideoActionsDisplayType = { | 19 | export 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..546518cca 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -42,6 +42,9 @@ export class Video implements VideoServerModel { | |||
42 | dislikes: number | 42 | dislikes: number |
43 | nsfw: boolean | 43 | nsfw: boolean |
44 | 44 | ||
45 | originInstanceUrl: string | ||
46 | originInstanceHost: string | ||
47 | |||
45 | waitTranscoding?: boolean | 48 | waitTranscoding?: boolean |
46 | state?: VideoConstant<VideoState> | 49 | state?: VideoConstant<VideoState> |
47 | scheduledUpdate?: VideoScheduleUpdate | 50 | scheduledUpdate?: VideoScheduleUpdate |
@@ -86,22 +89,31 @@ export class Video implements VideoServerModel { | |||
86 | this.waitTranscoding = hash.waitTranscoding | 89 | this.waitTranscoding = hash.waitTranscoding |
87 | this.state = hash.state | 90 | this.state = hash.state |
88 | this.description = hash.description | 91 | this.description = hash.description |
92 | |||
89 | this.duration = hash.duration | 93 | this.duration = hash.duration |
90 | this.durationLabel = durationToString(hash.duration) | 94 | this.durationLabel = durationToString(hash.duration) |
95 | |||
91 | this.id = hash.id | 96 | this.id = hash.id |
92 | this.uuid = hash.uuid | 97 | this.uuid = hash.uuid |
98 | |||
93 | this.isLocal = hash.isLocal | 99 | this.isLocal = hash.isLocal |
94 | this.name = hash.name | 100 | this.name = hash.name |
101 | |||
95 | this.thumbnailPath = hash.thumbnailPath | 102 | this.thumbnailPath = hash.thumbnailPath |
96 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath | 103 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath |
104 | |||
97 | this.previewPath = hash.previewPath | 105 | this.previewPath = hash.previewPath |
98 | this.previewUrl = absoluteAPIUrl + hash.previewPath | 106 | this.previewUrl = absoluteAPIUrl + hash.previewPath |
107 | |||
99 | this.embedPath = hash.embedPath | 108 | this.embedPath = hash.embedPath |
100 | this.embedUrl = absoluteAPIUrl + hash.embedPath | 109 | this.embedUrl = absoluteAPIUrl + hash.embedPath |
110 | |||
101 | this.views = hash.views | 111 | this.views = hash.views |
102 | this.likes = hash.likes | 112 | this.likes = hash.likes |
103 | this.dislikes = hash.dislikes | 113 | this.dislikes = hash.dislikes |
114 | |||
104 | this.nsfw = hash.nsfw | 115 | this.nsfw = hash.nsfw |
116 | |||
105 | this.account = hash.account | 117 | this.account = hash.account |
106 | this.channel = hash.channel | 118 | this.channel = hash.channel |
107 | 119 | ||
@@ -124,6 +136,9 @@ export class Video implements VideoServerModel { | |||
124 | this.blacklistedReason = hash.blacklistedReason | 136 | this.blacklistedReason = hash.blacklistedReason |
125 | 137 | ||
126 | this.userHistory = hash.userHistory | 138 | this.userHistory = hash.userHistory |
139 | |||
140 | this.originInstanceHost = this.account.host | ||
141 | this.originInstanceUrl = 'https://' + this.originInstanceHost | ||
127 | } | 142 | } |
128 | 143 | ||
129 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { | 144 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { |
@@ -152,4 +167,8 @@ export class Video implements VideoServerModel { | |||
152 | isUpdatableBy (user: AuthUser) { | 167 | isUpdatableBy (user: AuthUser) { |
153 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) | 168 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) |
154 | } | 169 | } |
170 | |||
171 | canBeDuplicatedBy (user: AuthUser) { | ||
172 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) | ||
173 | } | ||
155 | } | 174 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index bc3a3ffdd..a382777f5 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -188,6 +188,11 @@ | |||
188 | <span class="video-attribute-value">{{ video.privacy.label }}</span> | 188 | <span class="video-attribute-value">{{ video.privacy.label }}</span> |
189 | </div> | 189 | </div> |
190 | 190 | ||
191 | <div *ngIf="video.isLocal === false" class="video-attribute"> | ||
192 | <span i18n class="video-attribute-label">Origin instance</span> | ||
193 | <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="video.originInstanceUrl">{{ video.originInstanceHost }}</a> | ||
194 | </div> | ||
195 | |||
191 | <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> | 196 | <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> |
192 | <span i18n class="video-attribute-label">Originally published</span> | 197 | <span i18n class="video-attribute-label">Originally published</span> |
193 | <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> | 198 | <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> |
diff --git a/client/src/assets/player/bezels/bezels-plugin.ts b/client/src/assets/player/bezels/bezels-plugin.ts index c2c251961..499177526 100644 --- a/client/src/assets/player/bezels/bezels-plugin.ts +++ b/client/src/assets/player/bezels/bezels-plugin.ts | |||
@@ -1,85 +1,12 @@ | |||
1 | // @ts-ignore | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | import * as videojs from 'video.js' | 2 | import './pause-bezel' |
3 | import { VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
4 | 3 | ||
5 | function getPauseBezel () { | 4 | const Plugin = videojs.getPlugin('plugin') |
6 | return ` | ||
7 | <div class="vjs-bezels-pause"> | ||
8 | <div class="vjs-bezel" role="status" aria-label="Pause"> | ||
9 | <div class="vjs-bezel-icon"> | ||
10 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
11 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use> | ||
12 | <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path> | ||
13 | </svg> | ||
14 | </div> | ||
15 | </div> | ||
16 | </div> | ||
17 | ` | ||
18 | } | ||
19 | |||
20 | function getPlayBezel () { | ||
21 | return ` | ||
22 | <div class="vjs-bezels-play"> | ||
23 | <div class="vjs-bezel" role="status" aria-label="Play"> | ||
24 | <div class="vjs-bezel-icon"> | ||
25 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
26 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use> | ||
27 | <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path> | ||
28 | </svg> | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
32 | ` | ||
33 | } | ||
34 | |||
35 | // @ts-ignore-start | ||
36 | const Component = videojs.getComponent('Component') | ||
37 | class PauseBezel extends Component { | ||
38 | options_: any | ||
39 | container: HTMLBodyElement | ||
40 | |||
41 | constructor (player: videojs.Player, options: any) { | ||
42 | super(player, options) | ||
43 | this.options_ = options | ||
44 | |||
45 | player.on('pause', (_: any) => { | ||
46 | if (player.seeking() || player.ended()) return | ||
47 | this.container.innerHTML = getPauseBezel() | ||
48 | this.showBezel() | ||
49 | }) | ||
50 | |||
51 | player.on('play', (_: any) => { | ||
52 | if (player.seeking()) return | ||
53 | this.container.innerHTML = getPlayBezel() | ||
54 | this.showBezel() | ||
55 | }) | ||
56 | } | ||
57 | 5 | ||
58 | createEl () { | ||
59 | const container = super.createEl('div', { | ||
60 | className: 'vjs-bezels-content' | ||
61 | }) | ||
62 | this.container = container | ||
63 | container.style.display = 'none' | ||
64 | |||
65 | return container | ||
66 | } | ||
67 | |||
68 | showBezel () { | ||
69 | this.container.style.display = 'inherit' | ||
70 | setTimeout(() => { | ||
71 | this.container.style.display = 'none' | ||
72 | }, 500) // matching the animation duration | ||
73 | } | ||
74 | } | ||
75 | // @ts-ignore-end | ||
76 | |||
77 | videojs.registerComponent('PauseBezel', PauseBezel) | ||
78 | |||
79 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
80 | class BezelsPlugin extends Plugin { | 6 | class BezelsPlugin extends Plugin { |
81 | constructor (player: videojs.Player, options: any = {}) { | 7 | |
82 | super(player, options) | 8 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
9 | super(player) | ||
83 | 10 | ||
84 | this.player.ready(() => { | 11 | this.player.ready(() => { |
85 | player.addClass('vjs-bezels') | 12 | player.addClass('vjs-bezels') |
@@ -90,4 +17,5 @@ class BezelsPlugin extends Plugin { | |||
90 | } | 17 | } |
91 | 18 | ||
92 | videojs.registerPlugin('bezels', BezelsPlugin) | 19 | videojs.registerPlugin('bezels', BezelsPlugin) |
20 | |||
93 | export { BezelsPlugin } | 21 | export { BezelsPlugin } |
diff --git a/client/src/assets/player/bezels/pause-bezel.ts b/client/src/assets/player/bezels/pause-bezel.ts new file mode 100644 index 000000000..98eb12099 --- /dev/null +++ b/client/src/assets/player/bezels/pause-bezel.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | function getPauseBezel () { | ||
4 | return ` | ||
5 | <div class="vjs-bezels-pause"> | ||
6 | <div class="vjs-bezel" role="status" aria-label="Pause"> | ||
7 | <div class="vjs-bezel-icon"> | ||
8 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
9 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use> | ||
10 | <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path> | ||
11 | </svg> | ||
12 | </div> | ||
13 | </div> | ||
14 | </div> | ||
15 | ` | ||
16 | } | ||
17 | |||
18 | function getPlayBezel () { | ||
19 | return ` | ||
20 | <div class="vjs-bezels-play"> | ||
21 | <div class="vjs-bezel" role="status" aria-label="Play"> | ||
22 | <div class="vjs-bezel-icon"> | ||
23 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
24 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use> | ||
25 | <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path> | ||
26 | </svg> | ||
27 | </div> | ||
28 | </div> | ||
29 | </div> | ||
30 | ` | ||
31 | } | ||
32 | |||
33 | const Component = videojs.getComponent('Component') | ||
34 | class PauseBezel extends Component { | ||
35 | container: HTMLDivElement | ||
36 | |||
37 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
38 | super(player, options) | ||
39 | |||
40 | player.on('pause', (_: any) => { | ||
41 | if (player.seeking() || player.ended()) return | ||
42 | this.container.innerHTML = getPauseBezel() | ||
43 | this.showBezel() | ||
44 | }) | ||
45 | |||
46 | player.on('play', (_: any) => { | ||
47 | if (player.seeking()) return | ||
48 | this.container.innerHTML = getPlayBezel() | ||
49 | this.showBezel() | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | createEl () { | ||
54 | this.container = super.createEl('div', { | ||
55 | className: 'vjs-bezels-content' | ||
56 | }) as HTMLDivElement | ||
57 | |||
58 | this.container.style.display = 'none' | ||
59 | |||
60 | return this.container | ||
61 | } | ||
62 | |||
63 | showBezel () { | ||
64 | this.container.style.display = 'inherit' | ||
65 | |||
66 | setTimeout(() => { | ||
67 | this.container.style.display = 'none' | ||
68 | }, 500) // matching the animation duration | ||
69 | } | ||
70 | } | ||
71 | |||
72 | videojs.registerComponent('PauseBezel', PauseBezel) | ||
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index c3f863f72..512054ae6 100644 --- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -1,7 +1,5 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | 2 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' |
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | 3 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' |
6 | import { Events, Segment } from 'p2p-media-loader-core' | 4 | import { Events, Segment } from 'p2p-media-loader-core' |
7 | import { timeToInt } from '../utils' | 5 | import { timeToInt } from '../utils' |
@@ -10,7 +8,7 @@ import { timeToInt } from '../utils' | |||
10 | window['videojs'] = videojs | 8 | window['videojs'] = videojs |
11 | require('@streamroot/videojs-hlsjs-plugin') | 9 | require('@streamroot/videojs-hlsjs-plugin') |
12 | 10 | ||
13 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 11 | const Plugin = videojs.getPlugin('plugin') |
14 | class P2pMediaLoaderPlugin extends Plugin { | 12 | class P2pMediaLoaderPlugin extends Plugin { |
15 | 13 | ||
16 | private readonly CONSTANTS = { | 14 | private readonly CONSTANTS = { |
@@ -37,12 +35,13 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
37 | 35 | ||
38 | private networkInfoInterval: any | 36 | private networkInfoInterval: any |
39 | 37 | ||
40 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | 38 | constructor (player: VideoJsPlayer, options?: P2PMediaLoaderPluginOptions) { |
41 | super(player, options) | 39 | super(player) |
42 | 40 | ||
43 | this.options = options | 41 | this.options = options |
44 | 42 | ||
45 | if (!videojs.Html5Hlsjs) { | 43 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
44 | if (!(videojs as any).Html5Hlsjs) { | ||
46 | const message = 'HLS.js does not seem to be supported.' | 45 | const message = 'HLS.js does not seem to be supported.' |
47 | console.warn(message) | 46 | console.warn(message) |
48 | 47 | ||
@@ -50,7 +49,8 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
50 | return | 49 | return |
51 | } | 50 | } |
52 | 51 | ||
53 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | 52 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
53 | (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
54 | this.hlsjs = hlsjs | 54 | this.hlsjs = hlsjs |
55 | }) | 55 | }) |
56 | 56 | ||
@@ -84,8 +84,9 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
84 | private initialize () { | 84 | private initialize () { |
85 | initHlsJsPlayer(this.hlsjs) | 85 | initHlsJsPlayer(this.hlsjs) |
86 | 86 | ||
87 | const tech = this.player.tech_ | 87 | // FIXME: typings |
88 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | 88 | const options = this.player.tech(true).options_ as any |
89 | this.p2pEngine = options.hlsjsConfig.loader.getEngine() | ||
89 | 90 | ||
90 | // Avoid using constants to not import hls.hs | 91 | // Avoid using constants to not import hls.hs |
91 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | 92 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index d9e02cd7d..4e6387a53 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -1,21 +1,26 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | 1 | import { VideoFile } from '../../../../shared/models/videos' |
2 | // @ts-ignore | 2 | import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' |
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | 3 | import 'videojs-hotkeys' |
5 | import 'videojs-dock' | 4 | import 'videojs-dock' |
6 | import 'videojs-contextmenu-ui' | 5 | import 'videojs-contextmenu-ui' |
7 | import 'videojs-contrib-quality-levels' | 6 | import 'videojs-contrib-quality-levels' |
7 | import './upnext/end-card' | ||
8 | import './upnext/upnext-plugin' | 8 | import './upnext/upnext-plugin' |
9 | import './bezels/bezels-plugin' | 9 | import './bezels/bezels-plugin' |
10 | import './peertube-plugin' | 10 | import './peertube-plugin' |
11 | import './videojs-components/next-video-button' | 11 | import './videojs-components/next-video-button' |
12 | import './videojs-components/p2p-info-button' | ||
12 | import './videojs-components/peertube-link-button' | 13 | import './videojs-components/peertube-link-button' |
14 | import './videojs-components/peertube-load-progress-bar' | ||
13 | import './videojs-components/resolution-menu-button' | 15 | import './videojs-components/resolution-menu-button' |
16 | import './videojs-components/resolution-menu-item' | ||
17 | import './videojs-components/settings-dialog' | ||
14 | import './videojs-components/settings-menu-button' | 18 | import './videojs-components/settings-menu-button' |
15 | import './videojs-components/p2p-info-button' | 19 | import './videojs-components/settings-menu-item' |
16 | import './videojs-components/peertube-load-progress-bar' | 20 | import './videojs-components/settings-panel' |
21 | import './videojs-components/settings-panel-child' | ||
17 | import './videojs-components/theater-button' | 22 | import './videojs-components/theater-button' |
18 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | 23 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings' |
19 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | 24 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' |
20 | import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' | 25 | import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' |
21 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | 26 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' |
@@ -24,12 +29,17 @@ import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | |||
24 | import { getStoredP2PEnabled } from './peertube-player-local-storage' | 29 | import { getStoredP2PEnabled } from './peertube-player-local-storage' |
25 | import { TranslationsManager } from './translations-manager' | 30 | import { TranslationsManager } from './translations-manager' |
26 | 31 | ||
32 | // For VideoJS | ||
33 | (window as any).WebVTT = require('vtt.js/lib/vtt.js').WebVTT; | ||
34 | |||
27 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 35 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
28 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | 36 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' |
37 | |||
38 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
29 | // Change Captions to Subtitles/CC | 39 | // Change Captions to Subtitles/CC |
30 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | 40 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' |
31 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | 41 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) |
32 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | 42 | CaptionsButton.prototype.label_ = ' ' |
33 | 43 | ||
34 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 44 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' |
35 | 45 | ||
@@ -92,9 +102,9 @@ export type PeertubePlayerManagerOptions = { | |||
92 | 102 | ||
93 | export class PeertubePlayerManager { | 103 | export class PeertubePlayerManager { |
94 | private static playerElementClassName: string | 104 | private static playerElementClassName: string |
95 | private static onPlayerChange: (player: any) => void | 105 | private static onPlayerChange: (player: VideoJsPlayer) => void |
96 | 106 | ||
97 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { | 107 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: VideoJsPlayer) => void) { |
98 | let p2pMediaLoader: any | 108 | let p2pMediaLoader: any |
99 | 109 | ||
100 | this.onPlayerChange = onPlayerChange | 110 | this.onPlayerChange = onPlayerChange |
@@ -114,12 +124,12 @@ export class PeertubePlayerManager { | |||
114 | 124 | ||
115 | const self = this | 125 | const self = this |
116 | return new Promise(res => { | 126 | return new Promise(res => { |
117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | 127 | videojs(options.common.playerElement, videojsOptions, function (this: VideoJsPlayer) { |
118 | const player = this | 128 | const player = this |
119 | 129 | ||
120 | let alreadyFallback = false | 130 | let alreadyFallback = false |
121 | 131 | ||
122 | player.tech_.one('error', () => { | 132 | player.tech(true).one('error', () => { |
123 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | 133 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) |
124 | alreadyFallback = true | 134 | alreadyFallback = true |
125 | }) | 135 | }) |
@@ -164,7 +174,7 @@ export class PeertubePlayerManager { | |||
164 | const videojsOptions = this.getVideojsOptions(mode, options) | 174 | const videojsOptions = this.getVideojsOptions(mode, options) |
165 | 175 | ||
166 | const self = this | 176 | const self = this |
167 | videojs(newVideoElement, videojsOptions, function (this: any) { | 177 | videojs(newVideoElement, videojsOptions, function (this: VideoJsPlayer) { |
168 | const player = this | 178 | const player = this |
169 | 179 | ||
170 | self.addContextMenu(mode, player, options.common.embedUrl) | 180 | self.addContextMenu(mode, player, options.common.embedUrl) |
@@ -173,7 +183,11 @@ export class PeertubePlayerManager { | |||
173 | }) | 183 | }) |
174 | } | 184 | } |
175 | 185 | ||
176 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { | 186 | private static getVideojsOptions ( |
187 | mode: PlayerMode, | ||
188 | options: PeertubePlayerManagerOptions, | ||
189 | p2pMediaLoaderModule?: any | ||
190 | ): VideoJsPlayerOptions { | ||
177 | const commonOptions = options.common | 191 | const commonOptions = options.common |
178 | 192 | ||
179 | let autoplay = commonOptions.autoplay | 193 | let autoplay = commonOptions.autoplay |
@@ -213,7 +227,7 @@ export class PeertubePlayerManager { | |||
213 | html5, | 227 | html5, |
214 | 228 | ||
215 | // We don't use text track settings for now | 229 | // We don't use text track settings for now |
216 | textTrackSettings: false, | 230 | textTrackSettings: false as any, // FIXME: typings |
217 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | 231 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, |
218 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | 232 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, |
219 | 233 | ||
@@ -237,7 +251,7 @@ export class PeertubePlayerManager { | |||
237 | peertubeLink: commonOptions.peertubeLink, | 251 | peertubeLink: commonOptions.peertubeLink, |
238 | theaterButton: commonOptions.theaterButton, | 252 | theaterButton: commonOptions.theaterButton, |
239 | nextVideo: commonOptions.nextVideo | 253 | nextVideo: commonOptions.nextVideo |
240 | }) | 254 | }) as any // FIXME: typings |
241 | } | 255 | } |
242 | } | 256 | } |
243 | 257 | ||
@@ -406,7 +420,7 @@ export class PeertubePlayerManager { | |||
406 | return children | 420 | return children |
407 | } | 421 | } |
408 | 422 | ||
409 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | 423 | private static addContextMenu (mode: PlayerMode, player: VideoJsPlayer, videoEmbedUrl: string) { |
410 | const content = [ | 424 | const content = [ |
411 | { | 425 | { |
412 | label: player.localize('Copy the video URL'), | 426 | label: player.localize('Copy the video URL'), |
@@ -416,9 +430,8 @@ export class PeertubePlayerManager { | |||
416 | }, | 430 | }, |
417 | { | 431 | { |
418 | label: player.localize('Copy the video URL at the current time'), | 432 | label: player.localize('Copy the video URL at the current time'), |
419 | listener: function () { | 433 | listener: function (this: VideoJsPlayer) { |
420 | const player = this as videojs.Player | 434 | copyToClipboard(buildVideoLink({ startTime: this.currentTime() })) |
421 | copyToClipboard(buildVideoLink({ startTime: player.currentTime() })) | ||
422 | } | 435 | } |
423 | }, | 436 | }, |
424 | { | 437 | { |
@@ -432,9 +445,8 @@ export class PeertubePlayerManager { | |||
432 | if (mode === 'webtorrent') { | 445 | if (mode === 'webtorrent') { |
433 | content.push({ | 446 | content.push({ |
434 | label: player.localize('Copy magnet URI'), | 447 | label: player.localize('Copy magnet URI'), |
435 | listener: function () { | 448 | listener: function (this: VideoJsPlayer) { |
436 | const player = this as videojs.Player | 449 | copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri) |
437 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
438 | } | 450 | } |
439 | }) | 451 | }) |
440 | } | 452 | } |
@@ -472,7 +484,8 @@ export class PeertubePlayerManager { | |||
472 | return event.key === '>' | 484 | return event.key === '>' |
473 | }, | 485 | }, |
474 | handler: function (player: videojs.Player) { | 486 | handler: function (player: videojs.Player) { |
475 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | 487 | const newValue = Math.min(player.playbackRate() + 0.1, 5) |
488 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
476 | } | 489 | } |
477 | }, | 490 | }, |
478 | decreasePlaybackRateKey: { | 491 | decreasePlaybackRateKey: { |
@@ -480,7 +493,8 @@ export class PeertubePlayerManager { | |||
480 | return event.key === '<' | 493 | return event.key === '<' |
481 | }, | 494 | }, |
482 | handler: function (player: videojs.Player) { | 495 | handler: function (player: videojs.Player) { |
483 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | 496 | const newValue = Math.max(player.playbackRate() - 0.1, 0.10) |
497 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
484 | } | 498 | } |
485 | }, | 499 | }, |
486 | frameByFrame: { | 500 | frameByFrame: { |
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index 9824c43b5..19d104676 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -1,14 +1,10 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | 2 | import './videojs-components/settings-menu-button' |
5 | import { | 3 | import { |
6 | PeerTubePluginOptions, | 4 | PeerTubePluginOptions, |
7 | ResolutionUpdateData, | 5 | ResolutionUpdateData, |
8 | UserWatching, | 6 | UserWatching, |
9 | VideoJSCaption, | 7 | VideoJSCaption |
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | 8 | } from './peertube-videojs-typings' |
13 | import { isMobile, timeToInt } from './utils' | 9 | import { isMobile, timeToInt } from './utils' |
14 | import { | 10 | import { |
@@ -20,7 +16,8 @@ import { | |||
20 | saveVolumeInStore | 16 | saveVolumeInStore |
21 | } from './peertube-player-local-storage' | 17 | } from './peertube-player-local-storage' |
22 | 18 | ||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 19 | const Plugin = videojs.getPlugin('plugin') |
20 | |||
24 | class PeerTubePlugin extends Plugin { | 21 | class PeerTubePlugin extends Plugin { |
25 | private readonly videoViewUrl: string | 22 | private readonly videoViewUrl: string |
26 | private readonly videoDuration: number | 23 | private readonly videoDuration: number |
@@ -28,7 +25,6 @@ class PeerTubePlugin extends Plugin { | |||
28 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | 25 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video |
29 | } | 26 | } |
30 | 27 | ||
31 | private player: any | ||
32 | private videoCaptions: VideoJSCaption[] | 28 | private videoCaptions: VideoJSCaption[] |
33 | private defaultSubtitle: string | 29 | private defaultSubtitle: string |
34 | 30 | ||
@@ -40,8 +36,8 @@ class PeerTubePlugin extends Plugin { | |||
40 | private mouseInControlBar = false | 36 | private mouseInControlBar = false |
41 | private readonly savedInactivityTimeout: number | 37 | private readonly savedInactivityTimeout: number |
42 | 38 | ||
43 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | 39 | constructor (player: VideoJsPlayer, options?: PeerTubePluginOptions) { |
44 | super(player, options) | 40 | super(player) |
45 | 41 | ||
46 | this.videoViewUrl = options.videoViewUrl | 42 | this.videoViewUrl = options.videoViewUrl |
47 | this.videoDuration = options.videoDuration | 43 | this.videoDuration = options.videoDuration |
@@ -67,7 +63,7 @@ class PeerTubePlugin extends Plugin { | |||
67 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | 63 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) |
68 | } | 64 | } |
69 | 65 | ||
70 | this.player.tech_.on('loadedqualitydata', () => { | 66 | this.player.tech(true).on('loadedqualitydata', () => { |
71 | setTimeout(() => { | 67 | setTimeout(() => { |
72 | // Replay a resolution change, now we loaded all quality data | 68 | // Replay a resolution change, now we loaded all quality data |
73 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) | 69 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) |
@@ -102,7 +98,7 @@ class PeerTubePlugin extends Plugin { | |||
102 | } | 98 | } |
103 | 99 | ||
104 | this.player.textTracks().on('change', () => { | 100 | this.player.textTracks().on('change', () => { |
105 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | 101 | const showing = this.player.textTracks().tracks_.find(t => { |
106 | return t.kind === 'captions' && t.mode === 'showing' | 102 | return t.kind === 'captions' && t.mode === 'showing' |
107 | }) | 103 | }) |
108 | 104 | ||
@@ -262,7 +258,7 @@ class PeerTubePlugin extends Plugin { | |||
262 | 258 | ||
263 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 259 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
264 | private initSmoothProgressBar () { | 260 | private initSmoothProgressBar () { |
265 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 261 | const SeekBar = videojs.getComponent('SeekBar') as any |
266 | SeekBar.prototype.getPercent = function getPercent () { | 262 | SeekBar.prototype.getPercent = function getPercent () { |
267 | // Allows for smooth scrubbing, when player can't keep up. | 263 | // Allows for smooth scrubbing, when player can't keep up. |
268 | // const time = (this.player_.scrubbing()) ? | 264 | // const time = (this.player_.scrubbing()) ? |
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index aad4dbb4f..e45722661 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -1,7 +1,4 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { PeerTubePlugin } from './peertube-plugin' | 2 | import { PeerTubePlugin } from './peertube-plugin' |
6 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | 3 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' |
7 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | 4 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' |
@@ -9,20 +6,45 @@ import { PlayerMode } from './peertube-player-manager' | |||
9 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 6 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
10 | import { VideoFile } from '@shared/models' | 7 | import { VideoFile } from '@shared/models' |
11 | 8 | ||
12 | declare namespace videojs { | 9 | declare module 'video.js' { |
13 | interface Player { | 10 | export interface VideoJsPlayer { |
11 | theaterEnabled: boolean | ||
12 | |||
13 | // FIXME: add it to upstream typings | ||
14 | posterImage: { | ||
15 | show (): void | ||
16 | hide (): void | ||
17 | } | ||
18 | |||
19 | handleTechSeeked_ (): void | ||
20 | |||
21 | // Plugins | ||
22 | |||
14 | peertube (): PeerTubePlugin | 23 | peertube (): PeerTubePlugin |
15 | webtorrent (): WebTorrentPlugin | 24 | webtorrent (): WebTorrentPlugin |
16 | p2pMediaLoader (): P2pMediaLoaderPlugin | 25 | p2pMediaLoader (): P2pMediaLoaderPlugin |
17 | } | ||
18 | } | ||
19 | 26 | ||
20 | interface VideoJSComponentInterface { | 27 | contextmenuUI (options: any): any |
21 | _player: videojs.Player | 28 | |
29 | bezels (): void | ||
30 | |||
31 | qualityLevels (): { height: number, id: number }[] & { | ||
32 | selectedIndex: number | ||
33 | selectedIndex_: number | ||
22 | 34 | ||
23 | new (player: videojs.Player, options?: any): any | 35 | addQualityLevel (representation: { |
36 | id: number | ||
37 | label: string | ||
38 | height: number | ||
39 | _enabled: boolean | ||
40 | }): void | ||
41 | } | ||
24 | 42 | ||
25 | registerComponent (name: string, obj: any): any | 43 | textTracks (): TextTrackList & { |
44 | on: Function | ||
45 | tracks_: { kind: string, mode: string, language: string }[] | ||
46 | } | ||
47 | } | ||
26 | } | 48 | } |
27 | 49 | ||
28 | type VideoJSCaption = { | 50 | type VideoJSCaption = { |
@@ -78,9 +100,6 @@ type VideoJSPluginOptions = { | |||
78 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | 100 | p2pMediaLoader?: P2PMediaLoaderPluginOptions |
79 | } | 101 | } |
80 | 102 | ||
81 | // videojs typings don't have some method we need | ||
82 | const videojsUntyped = videojs as any | ||
83 | |||
84 | type LoadedQualityData = { | 103 | type LoadedQualityData = { |
85 | qualitySwitchCallback: Function, | 104 | qualitySwitchCallback: Function, |
86 | qualityData: { | 105 | qualityData: { |
@@ -123,8 +142,6 @@ export { | |||
123 | PlayerNetworkInfo, | 142 | PlayerNetworkInfo, |
124 | ResolutionUpdateData, | 143 | ResolutionUpdateData, |
125 | AutoResolutionUpdateData, | 144 | AutoResolutionUpdateData, |
126 | VideoJSComponentInterface, | ||
127 | videojsUntyped, | ||
128 | VideoJSCaption, | 145 | VideoJSCaption, |
129 | UserWatching, | 146 | UserWatching, |
130 | PeerTubePluginOptions, | 147 | PeerTubePluginOptions, |
diff --git a/client/src/assets/player/upnext/end-card.ts b/client/src/assets/player/upnext/end-card.ts new file mode 100644 index 000000000..d121a83a9 --- /dev/null +++ b/client/src/assets/player/upnext/end-card.ts | |||
@@ -0,0 +1,155 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | function getMainTemplate (options: any) { | ||
4 | return ` | ||
5 | <div class="vjs-upnext-top"> | ||
6 | <span class="vjs-upnext-headtext">${options.headText}</span> | ||
7 | <div class="vjs-upnext-title"></div> | ||
8 | </div> | ||
9 | <div class="vjs-upnext-autoplay-icon"> | ||
10 | <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%"> | ||
11 | <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle> | ||
12 | <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle> | ||
13 | <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg> | ||
14 | </div> | ||
15 | <span class="vjs-upnext-bottom"> | ||
16 | <span class="vjs-upnext-cancel"> | ||
17 | <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button> | ||
18 | </span> | ||
19 | <span class="vjs-upnext-suspended">${options.suspendedText}</span> | ||
20 | </span> | ||
21 | ` | ||
22 | } | ||
23 | |||
24 | export interface EndCardOptions extends videojs.ComponentOptions { | ||
25 | next: Function, | ||
26 | getTitle: () => string | ||
27 | timeout: number | ||
28 | cancelText: string | ||
29 | headText: string | ||
30 | suspendedText: string | ||
31 | condition: () => boolean | ||
32 | suspended: () => boolean | ||
33 | } | ||
34 | |||
35 | const Component = videojs.getComponent('Component') | ||
36 | class EndCard extends Component { | ||
37 | options_: EndCardOptions | ||
38 | |||
39 | dashOffsetTotal = 586 | ||
40 | dashOffsetStart = 293 | ||
41 | interval = 50 | ||
42 | upNextEvents = new videojs.EventTarget() | ||
43 | ticks = 0 | ||
44 | totalTicks: number | ||
45 | |||
46 | container: HTMLDivElement | ||
47 | title: HTMLElement | ||
48 | autoplayRing: HTMLElement | ||
49 | cancelButton: HTMLElement | ||
50 | suspendedMessage: HTMLElement | ||
51 | nextButton: HTMLElement | ||
52 | |||
53 | constructor (player: VideoJsPlayer, options: EndCardOptions) { | ||
54 | super(player, options) | ||
55 | |||
56 | this.totalTicks = this.options_.timeout / this.interval | ||
57 | |||
58 | player.on('ended', (_: any) => { | ||
59 | if (!this.options_.condition()) return | ||
60 | |||
61 | player.addClass('vjs-upnext--showing') | ||
62 | this.showCard((canceled: boolean) => { | ||
63 | player.removeClass('vjs-upnext--showing') | ||
64 | this.container.style.display = 'none' | ||
65 | if (!canceled) { | ||
66 | this.options_.next() | ||
67 | } | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | player.on('playing', () => { | ||
72 | this.upNextEvents.trigger('playing') | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | createEl () { | ||
77 | const container = super.createEl('div', { | ||
78 | className: 'vjs-upnext-content', | ||
79 | innerHTML: getMainTemplate(this.options_) | ||
80 | }) as HTMLDivElement | ||
81 | |||
82 | this.container = container | ||
83 | container.style.display = 'none' | ||
84 | |||
85 | this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] as HTMLElement | ||
86 | this.title = container.getElementsByClassName('vjs-upnext-title')[0] as HTMLElement | ||
87 | this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] as HTMLElement | ||
88 | this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] as HTMLElement | ||
89 | this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] as HTMLElement | ||
90 | |||
91 | this.cancelButton.onclick = () => { | ||
92 | this.upNextEvents.trigger('cancel') | ||
93 | } | ||
94 | |||
95 | this.nextButton.onclick = () => { | ||
96 | this.upNextEvents.trigger('next') | ||
97 | } | ||
98 | |||
99 | return container | ||
100 | } | ||
101 | |||
102 | showCard (cb: Function) { | ||
103 | let timeout: any | ||
104 | |||
105 | this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart) | ||
106 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart) | ||
107 | |||
108 | this.title.innerHTML = this.options_.getTitle() | ||
109 | |||
110 | this.upNextEvents.one('cancel', () => { | ||
111 | clearTimeout(timeout) | ||
112 | cb(true) | ||
113 | }) | ||
114 | |||
115 | this.upNextEvents.one('playing', () => { | ||
116 | clearTimeout(timeout) | ||
117 | cb(true) | ||
118 | }) | ||
119 | |||
120 | this.upNextEvents.one('next', () => { | ||
121 | clearTimeout(timeout) | ||
122 | cb(false) | ||
123 | }) | ||
124 | |||
125 | const goToPercent = (percent: number) => { | ||
126 | const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100) | ||
127 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset) | ||
128 | } | ||
129 | |||
130 | const tick = () => { | ||
131 | goToPercent((this.ticks++) * 100 / this.totalTicks) | ||
132 | } | ||
133 | |||
134 | const update = () => { | ||
135 | if (this.options_.suspended()) { | ||
136 | this.suspendedMessage.innerText = this.options_.suspendedText | ||
137 | goToPercent(0) | ||
138 | this.ticks = 0 | ||
139 | timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer | ||
140 | } else if (this.ticks >= this.totalTicks) { | ||
141 | clearTimeout(timeout) | ||
142 | cb(false) | ||
143 | } else { | ||
144 | this.suspendedMessage.innerText = '' | ||
145 | tick() | ||
146 | timeout = setTimeout(update.bind(this), this.interval) | ||
147 | } | ||
148 | } | ||
149 | |||
150 | this.container.style.display = 'block' | ||
151 | timeout = setTimeout(update.bind(this), this.interval) | ||
152 | } | ||
153 | } | ||
154 | |||
155 | videojs.registerComponent('EndCard', EndCard) | ||
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts index a3747b25f..6512fec2c 100644 --- a/client/src/assets/player/upnext/upnext-plugin.ts +++ b/client/src/assets/player/upnext/upnext-plugin.ts | |||
@@ -1,154 +1,11 @@ | |||
1 | // @ts-ignore | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | import * as videojs from 'video.js' | 2 | import { EndCardOptions } from './end-card' |
3 | import { VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
4 | 3 | ||
5 | function getMainTemplate (options: any) { | 4 | const Plugin = videojs.getPlugin('plugin') |
6 | return ` | ||
7 | <div class="vjs-upnext-top"> | ||
8 | <span class="vjs-upnext-headtext">${options.headText}</span> | ||
9 | <div class="vjs-upnext-title"></div> | ||
10 | </div> | ||
11 | <div class="vjs-upnext-autoplay-icon"> | ||
12 | <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%"> | ||
13 | <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle> | ||
14 | <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle> | ||
15 | <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg> | ||
16 | </div> | ||
17 | <span class="vjs-upnext-bottom"> | ||
18 | <span class="vjs-upnext-cancel"> | ||
19 | <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button> | ||
20 | </span> | ||
21 | <span class="vjs-upnext-suspended">${options.suspendedText}</span> | ||
22 | </span> | ||
23 | ` | ||
24 | } | ||
25 | |||
26 | // @ts-ignore-start | ||
27 | const Component = videojs.getComponent('Component') | ||
28 | class EndCard extends Component { | ||
29 | options_: any | ||
30 | dashOffsetTotal = 586 | ||
31 | dashOffsetStart = 293 | ||
32 | interval = 50 | ||
33 | upNextEvents = new videojs.EventTarget() | ||
34 | ticks = 0 | ||
35 | totalTicks: number | ||
36 | |||
37 | container: HTMLElement | ||
38 | title: HTMLElement | ||
39 | autoplayRing: HTMLElement | ||
40 | cancelButton: HTMLElement | ||
41 | suspendedMessage: HTMLElement | ||
42 | nextButton: HTMLElement | ||
43 | |||
44 | constructor (player: videojs.Player, options: any) { | ||
45 | super(player, options) | ||
46 | |||
47 | this.totalTicks = this.options_.timeout / this.interval | ||
48 | |||
49 | player.on('ended', (_: any) => { | ||
50 | if (!this.options_.condition()) return | ||
51 | |||
52 | player.addClass('vjs-upnext--showing') | ||
53 | this.showCard((canceled: boolean) => { | ||
54 | player.removeClass('vjs-upnext--showing') | ||
55 | this.container.style.display = 'none' | ||
56 | if (!canceled) { | ||
57 | this.options_.next() | ||
58 | } | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | player.on('playing', () => { | ||
63 | this.upNextEvents.trigger('playing') | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | createEl () { | ||
68 | const container = super.createEl('div', { | ||
69 | className: 'vjs-upnext-content', | ||
70 | innerHTML: getMainTemplate(this.options_) | ||
71 | }) | ||
72 | |||
73 | this.container = container | ||
74 | container.style.display = 'none' | ||
75 | |||
76 | this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] | ||
77 | this.title = container.getElementsByClassName('vjs-upnext-title')[0] | ||
78 | this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] | ||
79 | this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] | ||
80 | this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] | ||
81 | |||
82 | this.cancelButton.onclick = () => { | ||
83 | this.upNextEvents.trigger('cancel') | ||
84 | } | ||
85 | |||
86 | this.nextButton.onclick = () => { | ||
87 | this.upNextEvents.trigger('next') | ||
88 | } | ||
89 | |||
90 | return container | ||
91 | } | ||
92 | 5 | ||
93 | showCard (cb: Function) { | ||
94 | let timeout: any | ||
95 | |||
96 | this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart) | ||
97 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart) | ||
98 | |||
99 | this.title.innerHTML = this.options_.getTitle() | ||
100 | |||
101 | this.upNextEvents.one('cancel', () => { | ||
102 | clearTimeout(timeout) | ||
103 | cb(true) | ||
104 | }) | ||
105 | |||
106 | this.upNextEvents.one('playing', () => { | ||
107 | clearTimeout(timeout) | ||
108 | cb(true) | ||
109 | }) | ||
110 | |||
111 | this.upNextEvents.one('next', () => { | ||
112 | clearTimeout(timeout) | ||
113 | cb(false) | ||
114 | }) | ||
115 | |||
116 | const goToPercent = (percent: number) => { | ||
117 | const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100) | ||
118 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset) | ||
119 | } | ||
120 | |||
121 | const tick = () => { | ||
122 | goToPercent((this.ticks++) * 100 / this.totalTicks) | ||
123 | } | ||
124 | |||
125 | const update = () => { | ||
126 | if (this.options_.suspended()) { | ||
127 | this.suspendedMessage.innerText = this.options_.suspendedText | ||
128 | goToPercent(0) | ||
129 | this.ticks = 0 | ||
130 | timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer | ||
131 | } else if (this.ticks >= this.totalTicks) { | ||
132 | clearTimeout(timeout) | ||
133 | cb(false) | ||
134 | } else { | ||
135 | this.suspendedMessage.innerText = '' | ||
136 | tick() | ||
137 | timeout = setTimeout(update.bind(this), this.interval) | ||
138 | } | ||
139 | } | ||
140 | |||
141 | this.container.style.display = 'block' | ||
142 | timeout = setTimeout(update.bind(this), this.interval) | ||
143 | } | ||
144 | } | ||
145 | // @ts-ignore-end | ||
146 | |||
147 | videojs.registerComponent('EndCard', EndCard) | ||
148 | |||
149 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
150 | class UpNextPlugin extends Plugin { | 6 | class UpNextPlugin extends Plugin { |
151 | constructor (player: videojs.Player, options: any = {}) { | 7 | |
8 | constructor (player: VideoJsPlayer, options: Partial<EndCardOptions> = {}) { | ||
152 | const settings = { | 9 | const settings = { |
153 | next: options.next, | 10 | next: options.next, |
154 | getTitle: options.getTitle, | 11 | getTitle: options.getTitle, |
@@ -160,7 +17,7 @@ class UpNextPlugin extends Plugin { | |||
160 | suspended: options.suspended | 17 | suspended: options.suspended |
161 | } | 18 | } |
162 | 19 | ||
163 | super(player, settings) | 20 | super(player) |
164 | 21 | ||
165 | this.player.ready(() => { | 22 | this.player.ready(() => { |
166 | player.addClass('vjs-upnext') | 23 | player.addClass('vjs-upnext') |
diff --git a/client/src/assets/player/videojs-components/next-video-button.ts b/client/src/assets/player/videojs-components/next-video-button.ts index bf5c1aba4..bdb245dcc 100644 --- a/client/src/assets/player/videojs-components/next-video-button.ts +++ b/client/src/assets/player/videojs-components/next-video-button.ts | |||
@@ -1,21 +1,25 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
5 | 2 | ||
6 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 3 | const Button = videojs.getComponent('Button') |
4 | |||
5 | export interface NextVideoButtonOptions extends videojs.ComponentOptions { | ||
6 | handler: Function | ||
7 | } | ||
7 | 8 | ||
8 | class NextVideoButton extends Button { | 9 | class NextVideoButton extends Button { |
10 | private readonly nextVideoButtonOptions: NextVideoButtonOptions | ||
9 | 11 | ||
10 | constructor (player: Player, options: any) { | 12 | constructor (player: VideoJsPlayer, options?: NextVideoButtonOptions) { |
11 | super(player, options) | 13 | super(player, options) |
14 | |||
15 | this.nextVideoButtonOptions = options | ||
12 | } | 16 | } |
13 | 17 | ||
14 | createEl () { | 18 | createEl () { |
15 | const button = videojsUntyped.dom.createEl('button', { | 19 | const button = videojs.dom.createEl('button', { |
16 | className: 'vjs-next-video' | 20 | className: 'vjs-next-video' |
17 | }) | 21 | }) as HTMLButtonElement |
18 | const nextIcon = videojsUntyped.dom.createEl('span', { | 22 | const nextIcon = videojs.dom.createEl('span', { |
19 | className: 'icon icon-next' | 23 | className: 'icon icon-next' |
20 | }) | 24 | }) |
21 | button.appendChild(nextIcon) | 25 | button.appendChild(nextIcon) |
@@ -26,11 +30,8 @@ class NextVideoButton extends Button { | |||
26 | } | 30 | } |
27 | 31 | ||
28 | handleClick () { | 32 | handleClick () { |
29 | this.options_.handler() | 33 | this.nextVideoButtonOptions.handler() |
30 | } | 34 | } |
31 | |||
32 | } | 35 | } |
33 | 36 | ||
34 | NextVideoButton.prototype.controlText_ = 'Next video' | 37 | videojs.registerComponent('NextVideoButton', NextVideoButton) |
35 | |||
36 | NextVideoButton.registerComponent('NextVideoButton', NextVideoButton) | ||
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index 6424787b2..db6806fed 100644 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -1,63 +1,64 @@ | |||
1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import { PlayerNetworkInfo } from '../peertube-videojs-typings' |
2 | import videojs from 'video.js' | ||
2 | import { bytes } from '../utils' | 3 | import { bytes } from '../utils' |
3 | 4 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
5 | class P2pInfoButton extends Button { | 6 | class P2pInfoButton extends Button { |
6 | 7 | ||
7 | createEl () { | 8 | createEl () { |
8 | const div = videojsUntyped.dom.createEl('div', { | 9 | const div = videojs.dom.createEl('div', { |
9 | className: 'vjs-peertube' | 10 | className: 'vjs-peertube' |
10 | }) | 11 | }) |
11 | const subDivWebtorrent = videojsUntyped.dom.createEl('div', { | 12 | const subDivWebtorrent = videojs.dom.createEl('div', { |
12 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info | 13 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info |
13 | }) | 14 | }) as HTMLDivElement |
14 | div.appendChild(subDivWebtorrent) | 15 | div.appendChild(subDivWebtorrent) |
15 | 16 | ||
16 | const downloadIcon = videojsUntyped.dom.createEl('span', { | 17 | const downloadIcon = videojs.dom.createEl('span', { |
17 | className: 'icon icon-download' | 18 | className: 'icon icon-download' |
18 | }) | 19 | }) |
19 | subDivWebtorrent.appendChild(downloadIcon) | 20 | subDivWebtorrent.appendChild(downloadIcon) |
20 | 21 | ||
21 | const downloadSpeedText = videojsUntyped.dom.createEl('span', { | 22 | const downloadSpeedText = videojs.dom.createEl('span', { |
22 | className: 'download-speed-text' | 23 | className: 'download-speed-text' |
23 | }) | 24 | }) |
24 | const downloadSpeedNumber = videojsUntyped.dom.createEl('span', { | 25 | const downloadSpeedNumber = videojs.dom.createEl('span', { |
25 | className: 'download-speed-number' | 26 | className: 'download-speed-number' |
26 | }) | 27 | }) |
27 | const downloadSpeedUnit = videojsUntyped.dom.createEl('span') | 28 | const downloadSpeedUnit = videojs.dom.createEl('span') |
28 | downloadSpeedText.appendChild(downloadSpeedNumber) | 29 | downloadSpeedText.appendChild(downloadSpeedNumber) |
29 | downloadSpeedText.appendChild(downloadSpeedUnit) | 30 | downloadSpeedText.appendChild(downloadSpeedUnit) |
30 | subDivWebtorrent.appendChild(downloadSpeedText) | 31 | subDivWebtorrent.appendChild(downloadSpeedText) |
31 | 32 | ||
32 | const uploadIcon = videojsUntyped.dom.createEl('span', { | 33 | const uploadIcon = videojs.dom.createEl('span', { |
33 | className: 'icon icon-upload' | 34 | className: 'icon icon-upload' |
34 | }) | 35 | }) |
35 | subDivWebtorrent.appendChild(uploadIcon) | 36 | subDivWebtorrent.appendChild(uploadIcon) |
36 | 37 | ||
37 | const uploadSpeedText = videojsUntyped.dom.createEl('span', { | 38 | const uploadSpeedText = videojs.dom.createEl('span', { |
38 | className: 'upload-speed-text' | 39 | className: 'upload-speed-text' |
39 | }) | 40 | }) |
40 | const uploadSpeedNumber = videojsUntyped.dom.createEl('span', { | 41 | const uploadSpeedNumber = videojs.dom.createEl('span', { |
41 | className: 'upload-speed-number' | 42 | className: 'upload-speed-number' |
42 | }) | 43 | }) |
43 | const uploadSpeedUnit = videojsUntyped.dom.createEl('span') | 44 | const uploadSpeedUnit = videojs.dom.createEl('span') |
44 | uploadSpeedText.appendChild(uploadSpeedNumber) | 45 | uploadSpeedText.appendChild(uploadSpeedNumber) |
45 | uploadSpeedText.appendChild(uploadSpeedUnit) | 46 | uploadSpeedText.appendChild(uploadSpeedUnit) |
46 | subDivWebtorrent.appendChild(uploadSpeedText) | 47 | subDivWebtorrent.appendChild(uploadSpeedText) |
47 | 48 | ||
48 | const peersText = videojsUntyped.dom.createEl('span', { | 49 | const peersText = videojs.dom.createEl('span', { |
49 | className: 'peers-text' | 50 | className: 'peers-text' |
50 | }) | 51 | }) |
51 | const peersNumber = videojsUntyped.dom.createEl('span', { | 52 | const peersNumber = videojs.dom.createEl('span', { |
52 | className: 'peers-number' | 53 | className: 'peers-number' |
53 | }) | 54 | }) |
54 | subDivWebtorrent.appendChild(peersNumber) | 55 | subDivWebtorrent.appendChild(peersNumber) |
55 | subDivWebtorrent.appendChild(peersText) | 56 | subDivWebtorrent.appendChild(peersText) |
56 | 57 | ||
57 | const subDivHttp = videojsUntyped.dom.createEl('div', { | 58 | const subDivHttp = videojs.dom.createEl('div', { |
58 | className: 'vjs-peertube-hidden' | 59 | className: 'vjs-peertube-hidden' |
59 | }) | 60 | }) |
60 | const subDivHttpText = videojsUntyped.dom.createEl('span', { | 61 | const subDivHttpText = videojs.dom.createEl('span', { |
61 | className: 'http-fallback', | 62 | className: 'http-fallback', |
62 | textContent: 'HTTP' | 63 | textContent: 'HTTP' |
63 | }) | 64 | }) |
@@ -83,8 +84,8 @@ class P2pInfoButton extends Button { | |||
83 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) | 84 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) |
84 | const numPeers = p2pStats.numPeers | 85 | const numPeers = p2pStats.numPeers |
85 | 86 | ||
86 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + | 87 | subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + |
87 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) | 88 | this.player().localize('Total uploaded: ' + totalUploaded.join(' ')) |
88 | 89 | ||
89 | downloadSpeedNumber.textContent = downloadSpeed[ 0 ] | 90 | downloadSpeedNumber.textContent = downloadSpeed[ 0 ] |
90 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] | 91 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] |
@@ -92,14 +93,15 @@ class P2pInfoButton extends Button { | |||
92 | uploadSpeedNumber.textContent = uploadSpeed[ 0 ] | 93 | uploadSpeedNumber.textContent = uploadSpeed[ 0 ] |
93 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | 94 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] |
94 | 95 | ||
95 | peersNumber.textContent = numPeers | 96 | peersNumber.textContent = numPeers.toString() |
96 | peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) | 97 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) |
97 | 98 | ||
98 | subDivHttp.className = 'vjs-peertube-hidden' | 99 | subDivHttp.className = 'vjs-peertube-hidden' |
99 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 100 | subDivWebtorrent.className = 'vjs-peertube-displayed' |
100 | }) | 101 | }) |
101 | 102 | ||
102 | return div | 103 | return div as HTMLButtonElement |
103 | } | 104 | } |
104 | } | 105 | } |
105 | Button.registerComponent('P2PInfoButton', P2pInfoButton) | 106 | |
107 | videojs.registerComponent('P2PInfoButton', P2pInfoButton) | ||
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index 4d0ea37f5..0db9762a5 100644 --- a/client/src/assets/player/videojs-components/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,13 +1,10 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
2 | import { buildVideoLink } from '../utils' | 1 | import { buildVideoLink } from '../utils' |
3 | // FIXME: something weird with our path definition in tsconfig and typings | 2 | import videojs, { VideoJsPlayer } from 'video.js' |
4 | // @ts-ignore | ||
5 | import { Player } from 'video.js' | ||
6 | 3 | ||
7 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
8 | class PeerTubeLinkButton extends Button { | 5 | class PeerTubeLinkButton extends Button { |
9 | 6 | ||
10 | constructor (player: Player, options: any) { | 7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
11 | super(player, options) | 8 | super(player, options) |
12 | } | 9 | } |
13 | 10 | ||
@@ -20,21 +17,22 @@ class PeerTubeLinkButton extends Button { | |||
20 | } | 17 | } |
21 | 18 | ||
22 | handleClick () { | 19 | handleClick () { |
23 | this.player_.pause() | 20 | this.player().pause() |
24 | } | 21 | } |
25 | 22 | ||
26 | private buildElement () { | 23 | private buildElement () { |
27 | const el = videojsUntyped.dom.createEl('a', { | 24 | const el = videojs.dom.createEl('a', { |
28 | href: buildVideoLink(), | 25 | href: buildVideoLink(), |
29 | innerHTML: 'PeerTube', | 26 | innerHTML: 'PeerTube', |
30 | title: this.player_.localize('Go to the video page'), | 27 | title: this.player().localize('Go to the video page'), |
31 | className: 'vjs-peertube-link', | 28 | className: 'vjs-peertube-link', |
32 | target: '_blank' | 29 | target: '_blank' |
33 | }) | 30 | }) |
34 | 31 | ||
35 | el.addEventListener('mouseenter', () => this.updateHref()) | 32 | el.addEventListener('mouseenter', () => this.updateHref()) |
36 | 33 | ||
37 | return el | 34 | return el as HTMLButtonElement |
38 | } | 35 | } |
39 | } | 36 | } |
40 | Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | 37 | |
38 | videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | ||
diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index b594fc1c5..8168e8f2d 100644 --- a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts | |||
@@ -1,16 +1,12 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
5 | 2 | ||
6 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 3 | const Component = videojs.getComponent('Component') |
7 | 4 | ||
8 | class PeerTubeLoadProgressBar extends Component { | 5 | class PeerTubeLoadProgressBar extends Component { |
9 | partEls_: any[] | ||
10 | 6 | ||
11 | constructor (player: Player, options: any) { | 7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
12 | super(player, options) | 8 | super(player, options) |
13 | this.partEls_ = [] | 9 | |
14 | this.on(player, 'progress', this.update) | 10 | this.on(player, 'progress', this.update) |
15 | } | 11 | } |
16 | 12 | ||
@@ -22,8 +18,6 @@ class PeerTubeLoadProgressBar extends Component { | |||
22 | } | 18 | } |
23 | 19 | ||
24 | dispose () { | 20 | dispose () { |
25 | this.partEls_ = null | ||
26 | |||
27 | super.dispose() | 21 | super.dispose() |
28 | } | 22 | } |
29 | 23 | ||
@@ -31,7 +25,8 @@ class PeerTubeLoadProgressBar extends Component { | |||
31 | const torrent = this.player().webtorrent().getTorrent() | 25 | const torrent = this.player().webtorrent().getTorrent() |
32 | if (!torrent) return | 26 | if (!torrent) return |
33 | 27 | ||
34 | this.el_.style.width = (torrent.progress * 100) + '%' | 28 | // FIXME: typings |
29 | (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' | ||
35 | } | 30 | } |
36 | 31 | ||
37 | } | 32 | } |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index 2de3ece19..0fa6272e7 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -1,22 +1,19 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | 2 | ||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 3 | import { LoadedQualityData } from '../peertube-videojs-typings' |
6 | import { ResolutionMenuItem } from './resolution-menu-item' | 4 | import { ResolutionMenuItem } from './resolution-menu-item' |
7 | 5 | ||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 6 | const Menu = videojs.getComponent('Menu') |
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | 7 | const MenuButton = videojs.getComponent('MenuButton') |
10 | class ResolutionMenuButton extends MenuButton { | 8 | class ResolutionMenuButton extends MenuButton { |
11 | label: HTMLElement | 9 | labelEl_: HTMLElement |
12 | labelEl_: any | ||
13 | player: Player | ||
14 | 10 | ||
15 | constructor (player: Player, options: any) { | 11 | constructor (player: VideoJsPlayer, options?: videojs.MenuButtonOptions) { |
16 | super(player, options) | 12 | super(player, options) |
17 | this.player = player | ||
18 | 13 | ||
19 | player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | 14 | this.controlText('Quality') |
15 | |||
16 | player.tech(true).on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | ||
20 | 17 | ||
21 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) | 18 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) |
22 | } | 19 | } |
@@ -24,9 +21,9 @@ class ResolutionMenuButton extends MenuButton { | |||
24 | createEl () { | 21 | createEl () { |
25 | const el = super.createEl() | 22 | const el = super.createEl() |
26 | 23 | ||
27 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | 24 | this.labelEl_ = videojs.dom.createEl('div', { |
28 | className: 'vjs-resolution-value' | 25 | className: 'vjs-resolution-value' |
29 | }) | 26 | }) as HTMLElement |
30 | 27 | ||
31 | el.appendChild(this.labelEl_) | 28 | el.appendChild(this.labelEl_) |
32 | 29 | ||
@@ -55,7 +52,7 @@ class ResolutionMenuButton extends MenuButton { | |||
55 | 52 | ||
56 | for (const child of children) { | 53 | for (const child of children) { |
57 | if (component !== child) { | 54 | if (component !== child) { |
58 | child.selected(false) | 55 | (child as videojs.MenuItem).selected(false) |
59 | } | 56 | } |
60 | } | 57 | } |
61 | }) | 58 | }) |
@@ -76,7 +73,7 @@ class ResolutionMenuButton extends MenuButton { | |||
76 | if (d.id === -1) continue | 73 | if (d.id === -1) continue |
77 | 74 | ||
78 | const label = d.label === '0p' | 75 | const label = d.label === '0p' |
79 | ? this.player.localize('Audio-only') | 76 | ? this.player().localize('Audio-only') |
80 | : d.label | 77 | : d.label |
81 | 78 | ||
82 | this.menu.addChild(new ResolutionMenuItem( | 79 | this.menu.addChild(new ResolutionMenuItem( |
@@ -110,6 +107,5 @@ class ResolutionMenuButton extends MenuButton { | |||
110 | this.trigger('menuChanged') | 107 | this.trigger('menuChanged') |
111 | } | 108 | } |
112 | } | 109 | } |
113 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
114 | 110 | ||
115 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 111 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts index 6c42fefd2..b039c4572 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-item.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts | |||
@@ -1,12 +1,16 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | 2 | import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings' |
3 | import { Player } from 'video.js' | ||
4 | 3 | ||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 4 | const MenuItem = videojs.getComponent('MenuItem') |
5 | |||
6 | export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { | ||
7 | labels?: { [id: number]: string } | ||
8 | id: number | ||
9 | callback: Function | ||
10 | } | ||
6 | 11 | ||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | 12 | class ResolutionMenuItem extends MenuItem { |
9 | private readonly id: number | 13 | private readonly resolutionId: number |
10 | private readonly label: string | 14 | private readonly label: string |
11 | // Only used for the automatic item | 15 | // Only used for the automatic item |
12 | private readonly labels: { [id: number]: string } | 16 | private readonly labels: { [id: number]: string } |
@@ -15,7 +19,7 @@ class ResolutionMenuItem extends MenuItem { | |||
15 | private autoResolutionPossible: boolean | 19 | private autoResolutionPossible: boolean |
16 | private currentResolutionLabel: string | 20 | private currentResolutionLabel: string |
17 | 21 | ||
18 | constructor (player: Player, options: any) { | 22 | constructor (player: VideoJsPlayer, options?: ResolutionMenuItemOptions) { |
19 | options.selectable = true | 23 | options.selectable = true |
20 | 24 | ||
21 | super(player, options) | 25 | super(player, options) |
@@ -23,40 +27,40 @@ class ResolutionMenuItem extends MenuItem { | |||
23 | this.autoResolutionPossible = true | 27 | this.autoResolutionPossible = true |
24 | this.currentResolutionLabel = '' | 28 | this.currentResolutionLabel = '' |
25 | 29 | ||
30 | this.resolutionId = options.id | ||
26 | this.label = options.label | 31 | this.label = options.label |
27 | this.labels = options.labels | 32 | this.labels = options.labels |
28 | this.id = options.id | ||
29 | this.callback = options.callback | 33 | this.callback = options.callback |
30 | 34 | ||
31 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) | 35 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) |
32 | 36 | ||
33 | // We only want to disable the "Auto" item | 37 | // We only want to disable the "Auto" item |
34 | if (this.id === -1) { | 38 | if (this.resolutionId === -1) { |
35 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) | 39 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) |
36 | } | 40 | } |
37 | } | 41 | } |
38 | 42 | ||
39 | handleClick (event: any) { | 43 | handleClick (event: any) { |
40 | // Auto button disabled? | 44 | // Auto button disabled? |
41 | if (this.autoResolutionPossible === false && this.id === -1) return | 45 | if (this.autoResolutionPossible === false && this.resolutionId === -1) return |
42 | 46 | ||
43 | super.handleClick(event) | 47 | super.handleClick(event) |
44 | 48 | ||
45 | this.callback(this.id, 'video') | 49 | this.callback(this.resolutionId, 'video') |
46 | } | 50 | } |
47 | 51 | ||
48 | updateSelection (data: ResolutionUpdateData) { | 52 | updateSelection (data: ResolutionUpdateData) { |
49 | if (this.id === -1) { | 53 | if (this.resolutionId === -1) { |
50 | this.currentResolutionLabel = this.labels[data.id] | 54 | this.currentResolutionLabel = this.labels[data.id] |
51 | } | 55 | } |
52 | 56 | ||
53 | // Automatic resolution only | 57 | // Automatic resolution only |
54 | if (data.auto === true) { | 58 | if (data.auto === true) { |
55 | this.selected(this.id === -1) | 59 | this.selected(this.resolutionId === -1) |
56 | return | 60 | return |
57 | } | 61 | } |
58 | 62 | ||
59 | this.selected(this.id === data.id) | 63 | this.selected(this.resolutionId === data.id) |
60 | } | 64 | } |
61 | 65 | ||
62 | updateAutoResolution (data: AutoResolutionUpdateData) { | 66 | updateAutoResolution (data: AutoResolutionUpdateData) { |
@@ -71,13 +75,13 @@ class ResolutionMenuItem extends MenuItem { | |||
71 | } | 75 | } |
72 | 76 | ||
73 | getLabel () { | 77 | getLabel () { |
74 | if (this.id === -1) { | 78 | if (this.resolutionId === -1) { |
75 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' | 79 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' |
76 | } | 80 | } |
77 | 81 | ||
78 | return this.label | 82 | return this.label |
79 | } | 83 | } |
80 | } | 84 | } |
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | 85 | videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem) |
82 | 86 | ||
83 | export { ResolutionMenuItem } | 87 | export { ResolutionMenuItem } |
diff --git a/client/src/assets/player/videojs-components/settings-dialog.ts b/client/src/assets/player/videojs-components/settings-dialog.ts new file mode 100644 index 000000000..dd0b1e472 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-dialog.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsDialog extends Component { | ||
6 | constructor (player: VideoJsPlayer) { | ||
7 | super(player) | ||
8 | |||
9 | this.hide() | ||
10 | } | ||
11 | |||
12 | /** | ||
13 | * Create the component's DOM element | ||
14 | * | ||
15 | * @return {Element} | ||
16 | * @method createEl | ||
17 | */ | ||
18 | createEl () { | ||
19 | const uniqueId = this.id() | ||
20 | const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId | ||
21 | const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId | ||
22 | |||
23 | return super.createEl('div', { | ||
24 | className: 'vjs-settings-dialog vjs-modal-overlay', | ||
25 | innerHTML: '', | ||
26 | tabIndex: -1 | ||
27 | }, { | ||
28 | 'role': 'dialog', | ||
29 | 'aria-labelledby': dialogLabelId, | ||
30 | 'aria-describedby': dialogDescriptionId | ||
31 | }) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | Component.registerComponent('SettingsDialog', SettingsDialog) | ||
36 | |||
37 | export { SettingsDialog } | ||
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts index b700f4be6..eae628e7d 100644 --- a/client/src/assets/player/videojs-components/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -1,43 +1,52 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | ||
3 | |||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
6 | import * as videojs from 'video.js' | ||
7 | |||
8 | import { SettingsMenuItem } from './settings-menu-item' | 2 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
10 | import { toTitleCase } from '../utils' | 3 | import { toTitleCase } from '../utils' |
4 | import videojs, { VideoJsPlayer } from 'video.js' | ||
5 | |||
6 | import { SettingsDialog } from './settings-dialog' | ||
7 | import { SettingsPanel } from './settings-panel' | ||
8 | import { SettingsPanelChild } from './settings-panel-child' | ||
11 | 9 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 10 | const Button = videojs.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 11 | const Menu = videojs.getComponent('Menu') |
14 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const Component = videojs.getComponent('Component') |
13 | |||
14 | export interface SettingsButtonOptions extends videojs.ComponentOptions { | ||
15 | entries: any[] | ||
16 | setup?: { | ||
17 | maxHeightOffset: number | ||
18 | } | ||
19 | } | ||
15 | 20 | ||
16 | class SettingsButton extends Button { | 21 | class SettingsButton extends Button { |
17 | playerComponent = videojs.Player | 22 | dialog: SettingsDialog |
18 | dialog: any | 23 | dialogEl: HTMLElement |
19 | dialogEl: any | 24 | menu: videojs.Menu |
20 | menu: any | 25 | panel: SettingsPanel |
21 | panel: any | 26 | panelChild: SettingsPanelChild |
22 | panelChild: any | 27 | |
23 | 28 | addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem | |
24 | addSettingsItemHandler: Function | 29 | disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem |
25 | disposeSettingsItemHandler: Function | 30 | playerClickHandler: typeof SettingsButton.prototype.onPlayerClick |
26 | playerClickHandler: Function | 31 | userInactiveHandler: typeof SettingsButton.prototype.onUserInactive |
27 | userInactiveHandler: Function | 32 | |
28 | 33 | private settingsButtonOptions: SettingsButtonOptions | |
29 | constructor (player: videojs.Player, options: any) { | 34 | |
35 | constructor (player: VideoJsPlayer, options?: SettingsButtonOptions) { | ||
30 | super(player, options) | 36 | super(player, options) |
31 | 37 | ||
32 | this.playerComponent = player | 38 | this.settingsButtonOptions = options |
33 | this.dialog = this.playerComponent.addChild('settingsDialog') | 39 | |
34 | this.dialogEl = this.dialog.el_ | 40 | this.controlText('Settings') |
41 | |||
42 | this.dialog = this.player().addChild('settingsDialog') | ||
43 | this.dialogEl = this.dialog.el() as HTMLElement | ||
35 | this.menu = null | 44 | this.menu = null |
36 | this.panel = this.dialog.addChild('settingsPanel') | 45 | this.panel = this.dialog.addChild('settingsPanel') |
37 | this.panelChild = this.panel.addChild('settingsPanelChild') | 46 | this.panelChild = this.panel.addChild('settingsPanelChild') |
38 | 47 | ||
39 | this.addClass('vjs-settings') | 48 | this.addClass('vjs-settings') |
40 | this.el_.setAttribute('aria-label', 'Settings Button') | 49 | this.el().setAttribute('aria-label', 'Settings Button') |
41 | 50 | ||
42 | // Event handlers | 51 | // Event handlers |
43 | this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) | 52 | this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) |
@@ -84,7 +93,7 @@ class SettingsButton extends Button { | |||
84 | 93 | ||
85 | this.hideDialog() | 94 | this.hideDialog() |
86 | 95 | ||
87 | if (this.options_.entries.length === 0) { | 96 | if (this.settingsButtonOptions.entries.length === 0) { |
88 | this.addClass('vjs-hidden') | 97 | this.addClass('vjs-hidden') |
89 | } | 98 | } |
90 | } | 99 | } |
@@ -103,10 +112,10 @@ class SettingsButton extends Button { | |||
103 | } | 112 | } |
104 | 113 | ||
105 | bindEvents () { | 114 | bindEvents () { |
106 | this.playerComponent.on('click', this.playerClickHandler) | 115 | this.player().on('click', this.playerClickHandler) |
107 | this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) | 116 | this.player().on('addsettingsitem', this.addSettingsItemHandler) |
108 | this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) | 117 | this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) |
109 | this.playerComponent.on('userinactive', this.userInactiveHandler) | 118 | this.player().on('userinactive', this.userInactiveHandler) |
110 | } | 119 | } |
111 | 120 | ||
112 | buildCSSClass () { | 121 | buildCSSClass () { |
@@ -122,9 +131,9 @@ class SettingsButton extends Button { | |||
122 | } | 131 | } |
123 | 132 | ||
124 | showDialog () { | 133 | showDialog () { |
125 | this.player_.peertube().onMenuOpen() | 134 | this.player().peertube().onMenuOpen(); |
126 | 135 | ||
127 | this.menu.el_.style.opacity = '1' | 136 | (this.menu.el() as HTMLElement).style.opacity = '1' |
128 | this.dialog.show() | 137 | this.dialog.show() |
129 | 138 | ||
130 | this.setDialogSize(this.getComponentSize(this.menu)) | 139 | this.setDialogSize(this.getComponentSize(this.menu)) |
@@ -134,23 +143,24 @@ class SettingsButton extends Button { | |||
134 | this.player_.peertube().onMenuClosed() | 143 | this.player_.peertube().onMenuClosed() |
135 | 144 | ||
136 | this.dialog.hide() | 145 | this.dialog.hide() |
137 | this.setDialogSize(this.getComponentSize(this.menu)) | 146 | this.setDialogSize(this.getComponentSize(this.menu)); |
138 | this.menu.el_.style.opacity = '1' | 147 | (this.menu.el() as HTMLElement).style.opacity = '1' |
139 | this.resetChildren() | 148 | this.resetChildren() |
140 | } | 149 | } |
141 | 150 | ||
142 | getComponentSize (element: any) { | 151 | getComponentSize (element: videojs.Component | HTMLElement) { |
143 | let width: number = null | 152 | let width: number = null |
144 | let height: number = null | 153 | let height: number = null |
145 | 154 | ||
146 | // Could be component or just DOM element | 155 | // Could be component or just DOM element |
147 | if (element instanceof Component) { | 156 | if (element instanceof Component) { |
148 | width = element.el_.offsetWidth | 157 | const el = element.el() as HTMLElement |
149 | height = element.el_.offsetHeight | 158 | |
159 | width = el.offsetWidth | ||
160 | height = el.offsetHeight; | ||
150 | 161 | ||
151 | // keep width/height as properties for direct use | 162 | (element as any).width = width; |
152 | element.width = width | 163 | (element as any).height = height |
153 | element.height = height | ||
154 | } else { | 164 | } else { |
155 | width = element.offsetWidth | 165 | width = element.offsetWidth |
156 | height = element.offsetHeight | 166 | height = element.offsetHeight |
@@ -164,15 +174,17 @@ class SettingsButton extends Button { | |||
164 | return | 174 | return |
165 | } | 175 | } |
166 | 176 | ||
167 | const offset = this.options_.setup.maxHeightOffset | 177 | const offset = this.settingsButtonOptions.setup.maxHeightOffset |
168 | const maxHeight = this.playerComponent.el_.offsetHeight - offset | 178 | const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset // FIXME: typings |
179 | |||
180 | const panelEl = this.panel.el() as HTMLElement | ||
169 | 181 | ||
170 | if (height > maxHeight) { | 182 | if (height > maxHeight) { |
171 | height = maxHeight | 183 | height = maxHeight |
172 | width += 17 | 184 | width += 17 |
173 | this.panel.el_.style.maxHeight = `${height}px` | 185 | panelEl.style.maxHeight = `${height}px` |
174 | } else if (this.panel.el_.style.maxHeight !== '') { | 186 | } else if (panelEl.style.maxHeight !== '') { |
175 | this.panel.el_.style.maxHeight = '' | 187 | panelEl.style.maxHeight = '' |
176 | } | 188 | } |
177 | 189 | ||
178 | this.dialogEl.style.width = `${width}px` | 190 | this.dialogEl.style.width = `${width}px` |
@@ -182,7 +194,7 @@ class SettingsButton extends Button { | |||
182 | buildMenu () { | 194 | buildMenu () { |
183 | this.menu = new Menu(this.player()) | 195 | this.menu = new Menu(this.player()) |
184 | this.menu.addClass('vjs-main-menu') | 196 | this.menu.addClass('vjs-main-menu') |
185 | const entries = this.options_.entries | 197 | const entries = this.settingsButtonOptions.entries |
186 | 198 | ||
187 | if (entries.length === 0) { | 199 | if (entries.length === 0) { |
188 | this.addClass('vjs-hidden') | 200 | this.addClass('vjs-hidden') |
@@ -191,7 +203,7 @@ class SettingsButton extends Button { | |||
191 | } | 203 | } |
192 | 204 | ||
193 | for (const entry of entries) { | 205 | for (const entry of entries) { |
194 | this.addMenuItem(entry, this.options_) | 206 | this.addMenuItem(entry, this.settingsButtonOptions) |
195 | } | 207 | } |
196 | 208 | ||
197 | this.panelChild.addChild(this.menu) | 209 | this.panelChild.addChild(this.menu) |
@@ -199,15 +211,17 @@ class SettingsButton extends Button { | |||
199 | 211 | ||
200 | addMenuItem (entry: any, options: any) { | 212 | addMenuItem (entry: any, options: any) { |
201 | const openSubMenu = function (this: any) { | 213 | const openSubMenu = function (this: any) { |
202 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | 214 | if (videojs.dom.hasClass(this.el_, 'open')) { |
203 | videojsUntyped.dom.removeClass(this.el_, 'open') | 215 | videojs.dom.removeClass(this.el_, 'open') |
204 | } else { | 216 | } else { |
205 | videojsUntyped.dom.addClass(this.el_, 'open') | 217 | videojs.dom.addClass(this.el_, 'open') |
206 | } | 218 | } |
207 | } | 219 | } |
208 | 220 | ||
209 | options.name = toTitleCase(entry) | 221 | options.name = toTitleCase(entry) |
210 | const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) | 222 | |
223 | const newOptions = Object.assign({}, options, { entry, menuButton: this }) | ||
224 | const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) | ||
211 | 225 | ||
212 | this.menu.addChild(settingsMenuItem) | 226 | this.menu.addChild(settingsMenuItem) |
213 | 227 | ||
@@ -221,7 +235,7 @@ class SettingsButton extends Button { | |||
221 | 235 | ||
222 | resetChildren () { | 236 | resetChildren () { |
223 | for (const menuChild of this.menu.children()) { | 237 | for (const menuChild of this.menu.children()) { |
224 | menuChild.reset() | 238 | (menuChild as SettingsMenuItem).reset() |
225 | } | 239 | } |
226 | } | 240 | } |
227 | 241 | ||
@@ -230,75 +244,12 @@ class SettingsButton extends Button { | |||
230 | */ | 244 | */ |
231 | hideChildren () { | 245 | hideChildren () { |
232 | for (const menuChild of this.menu.children()) { | 246 | for (const menuChild of this.menu.children()) { |
233 | menuChild.hideSubMenu() | 247 | (menuChild as SettingsMenuItem).hideSubMenu() |
234 | } | 248 | } |
235 | } | 249 | } |
236 | 250 | ||
237 | } | 251 | } |
238 | 252 | ||
239 | class SettingsPanel extends Component { | ||
240 | constructor (player: videojs.Player, options: any) { | ||
241 | super(player, options) | ||
242 | } | ||
243 | |||
244 | createEl () { | ||
245 | return super.createEl('div', { | ||
246 | className: 'vjs-settings-panel', | ||
247 | innerHTML: '', | ||
248 | tabIndex: -1 | ||
249 | }) | ||
250 | } | ||
251 | } | ||
252 | |||
253 | class SettingsPanelChild extends Component { | ||
254 | constructor (player: videojs.Player, options: any) { | ||
255 | super(player, options) | ||
256 | } | ||
257 | |||
258 | createEl () { | ||
259 | return super.createEl('div', { | ||
260 | className: 'vjs-settings-panel-child', | ||
261 | innerHTML: '', | ||
262 | tabIndex: -1 | ||
263 | }) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | class SettingsDialog extends Component { | ||
268 | constructor (player: videojs.Player, options: any) { | ||
269 | super(player, options) | ||
270 | this.hide() | ||
271 | } | ||
272 | |||
273 | /** | ||
274 | * Create the component's DOM element | ||
275 | * | ||
276 | * @return {Element} | ||
277 | * @method createEl | ||
278 | */ | ||
279 | createEl () { | ||
280 | const uniqueId = this.id_ | ||
281 | const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId | ||
282 | const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId | ||
283 | |||
284 | return super.createEl('div', { | ||
285 | className: 'vjs-settings-dialog vjs-modal-overlay', | ||
286 | innerHTML: '', | ||
287 | tabIndex: -1 | ||
288 | }, { | ||
289 | 'role': 'dialog', | ||
290 | 'aria-labelledby': dialogLabelId, | ||
291 | 'aria-describedby': dialogDescriptionId | ||
292 | }) | ||
293 | } | ||
294 | |||
295 | } | ||
296 | |||
297 | SettingsButton.prototype.controlText_ = 'Settings' | ||
298 | |||
299 | Component.registerComponent('SettingsButton', SettingsButton) | 253 | Component.registerComponent('SettingsButton', SettingsButton) |
300 | Component.registerComponent('SettingsDialog', SettingsDialog) | ||
301 | Component.registerComponent('SettingsPanel', SettingsPanel) | ||
302 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | ||
303 | 254 | ||
304 | export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } | 255 | export { SettingsButton } |
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 84d394c0e..f5671f49d 100644 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -1,57 +1,63 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | ||
3 | |||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
6 | import * as videojs from 'video.js' | ||
7 | |||
8 | import { toTitleCase } from '../utils' | 2 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 3 | import videojs, { VideoJsPlayer } from 'video.js' |
10 | 4 | import { SettingsButton } from './settings-menu-button' | |
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 5 | import { SettingsDialog } from './settings-dialog' |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 6 | import { SettingsPanel } from './settings-panel' |
7 | import { SettingsPanelChild } from './settings-panel-child' | ||
8 | |||
9 | const MenuItem = videojs.getComponent('MenuItem') | ||
10 | const component = videojs.getComponent('Component') | ||
11 | |||
12 | export interface SettingsMenuItemOptions extends videojs.MenuItemOptions { | ||
13 | entry: string | ||
14 | menuButton: SettingsButton | ||
15 | } | ||
13 | 16 | ||
14 | class SettingsMenuItem extends MenuItem { | 17 | class SettingsMenuItem extends MenuItem { |
15 | settingsButton: any | 18 | settingsButton: SettingsButton |
16 | dialog: any | 19 | dialog: SettingsDialog |
17 | mainMenu: any | 20 | mainMenu: videojs.Menu |
18 | panel: any | 21 | panel: SettingsPanel |
19 | panelChild: any | 22 | panelChild: SettingsPanelChild |
20 | panelChildEl: any | 23 | panelChildEl: HTMLElement |
21 | size: any | 24 | size: number[] |
22 | menuToLoad: string | 25 | menuToLoad: string |
23 | subMenu: any | 26 | subMenu: SettingsButton |
24 | 27 | ||
25 | submenuClickHandler: Function | 28 | submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick |
26 | transitionEndHandler: Function | 29 | transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd |
27 | 30 | ||
28 | settingsSubMenuTitleEl_: any | 31 | settingsSubMenuTitleEl_: HTMLElement |
29 | settingsSubMenuValueEl_: any | 32 | settingsSubMenuValueEl_: HTMLElement |
30 | settingsSubMenuEl_: any | 33 | settingsSubMenuEl_: HTMLElement |
31 | 34 | ||
32 | constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { | 35 | constructor (player: VideoJsPlayer, options?: SettingsMenuItemOptions) { |
33 | super(player, options) | 36 | super(player, options) |
34 | 37 | ||
35 | this.settingsButton = menuButton | 38 | this.settingsButton = options.menuButton |
36 | this.dialog = this.settingsButton.dialog | 39 | this.dialog = this.settingsButton.dialog |
37 | this.mainMenu = this.settingsButton.menu | 40 | this.mainMenu = this.settingsButton.menu |
38 | this.panel = this.dialog.getChild('settingsPanel') | 41 | this.panel = this.dialog.getChild('settingsPanel') |
39 | this.panelChild = this.panel.getChild('settingsPanelChild') | 42 | this.panelChild = this.panel.getChild('settingsPanelChild') |
40 | this.panelChildEl = this.panelChild.el_ | 43 | this.panelChildEl = this.panelChild.el() as HTMLElement |
41 | 44 | ||
42 | this.size = null | 45 | this.size = null |
43 | 46 | ||
44 | // keep state of what menu type is loading next | 47 | // keep state of what menu type is loading next |
45 | this.menuToLoad = 'mainmenu' | 48 | this.menuToLoad = 'mainmenu' |
46 | 49 | ||
47 | const subMenuName = toTitleCase(entry) | 50 | const subMenuName = toTitleCase(options.entry) |
48 | const SubMenuComponent = videojsUntyped.getComponent(subMenuName) | 51 | const SubMenuComponent = videojs.getComponent(subMenuName) |
49 | 52 | ||
50 | if (!SubMenuComponent) { | 53 | if (!SubMenuComponent) { |
51 | throw new Error(`Component ${subMenuName} does not exist`) | 54 | throw new Error(`Component ${subMenuName} does not exist`) |
52 | } | 55 | } |
53 | this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) | 56 | |
54 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] | 57 | const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this }) |
58 | |||
59 | this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings | ||
60 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[ 0 ] | ||
55 | this.settingsSubMenuEl_.className += ' ' + subMenuClass | 61 | this.settingsSubMenuEl_.className += ' ' + subMenuClass |
56 | 62 | ||
57 | this.eventHandlers() | 63 | this.eventHandlers() |
@@ -72,7 +78,7 @@ class SettingsMenuItem extends MenuItem { | |||
72 | player.on('captionsChanged', () => { | 78 | player.on('captionsChanged', () => { |
73 | setTimeout(() => { | 79 | setTimeout(() => { |
74 | this.settingsSubMenuEl_.innerHTML = '' | 80 | this.settingsSubMenuEl_.innerHTML = '' |
75 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 81 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) |
76 | this.update() | 82 | this.update() |
77 | this.bindClickEvents() | 83 | this.bindClickEvents() |
78 | }, 0) | 84 | }, 0) |
@@ -119,27 +125,27 @@ class SettingsMenuItem extends MenuItem { | |||
119 | * @method createEl | 125 | * @method createEl |
120 | */ | 126 | */ |
121 | createEl () { | 127 | createEl () { |
122 | const el = videojsUntyped.dom.createEl('li', { | 128 | const el = videojs.dom.createEl('li', { |
123 | className: 'vjs-menu-item' | 129 | className: 'vjs-menu-item' |
124 | }) | 130 | }) |
125 | 131 | ||
126 | this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { | 132 | this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', { |
127 | className: 'vjs-settings-sub-menu-title' | 133 | className: 'vjs-settings-sub-menu-title' |
128 | }) | 134 | }) as HTMLElement |
129 | 135 | ||
130 | el.appendChild(this.settingsSubMenuTitleEl_) | 136 | el.appendChild(this.settingsSubMenuTitleEl_) |
131 | 137 | ||
132 | this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { | 138 | this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', { |
133 | className: 'vjs-settings-sub-menu-value' | 139 | className: 'vjs-settings-sub-menu-value' |
134 | }) | 140 | }) as HTMLElement |
135 | 141 | ||
136 | el.appendChild(this.settingsSubMenuValueEl_) | 142 | el.appendChild(this.settingsSubMenuValueEl_) |
137 | 143 | ||
138 | this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { | 144 | this.settingsSubMenuEl_ = videojs.dom.createEl('div', { |
139 | className: 'vjs-settings-sub-menu' | 145 | className: 'vjs-settings-sub-menu' |
140 | }) | 146 | }) as HTMLElement |
141 | 147 | ||
142 | return el | 148 | return el as HTMLLIElement |
143 | } | 149 | } |
144 | 150 | ||
145 | /** | 151 | /** |
@@ -147,17 +153,17 @@ class SettingsMenuItem extends MenuItem { | |||
147 | * | 153 | * |
148 | * @method handleClick | 154 | * @method handleClick |
149 | */ | 155 | */ |
150 | handleClick () { | 156 | handleClick (event: videojs.EventTarget.Event) { |
151 | this.menuToLoad = 'submenu' | 157 | this.menuToLoad = 'submenu' |
152 | // Remove open class to ensure only the open submenu gets this class | 158 | // Remove open class to ensure only the open submenu gets this class |
153 | videojsUntyped.dom.removeClass(this.el_, 'open') | 159 | videojs.dom.removeClass(this.el(), 'open') |
154 | 160 | ||
155 | super.handleClick() | 161 | super.handleClick(event); |
156 | 162 | ||
157 | this.mainMenu.el_.style.opacity = '0' | 163 | (this.mainMenu.el() as HTMLElement).style.opacity = '0' |
158 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element | 164 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element |
159 | if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { | 165 | if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { |
160 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | 166 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') |
161 | 167 | ||
162 | // animation not played without timeout | 168 | // animation not played without timeout |
163 | setTimeout(() => { | 169 | setTimeout(() => { |
@@ -167,7 +173,7 @@ class SettingsMenuItem extends MenuItem { | |||
167 | 173 | ||
168 | this.settingsButton.setDialogSize(this.size) | 174 | this.settingsButton.setDialogSize(this.size) |
169 | } else { | 175 | } else { |
170 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 176 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
171 | } | 177 | } |
172 | } | 178 | } |
173 | 179 | ||
@@ -178,9 +184,9 @@ class SettingsMenuItem extends MenuItem { | |||
178 | */ | 184 | */ |
179 | createBackButton () { | 185 | createBackButton () { |
180 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) | 186 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) |
181 | button.name_ = 'BackButton' | 187 | |
182 | button.addClass('vjs-back-button') | 188 | button.addClass('vjs-back-button'); |
183 | button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 189 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) |
184 | } | 190 | } |
185 | 191 | ||
186 | /** | 192 | /** |
@@ -189,17 +195,17 @@ class SettingsMenuItem extends MenuItem { | |||
189 | * @method PrefixedEvent | 195 | * @method PrefixedEvent |
190 | */ | 196 | */ |
191 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { | 197 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { |
192 | const prefix = ['webkit', 'moz', 'MS', 'o', ''] | 198 | const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] |
193 | 199 | ||
194 | for (let p = 0; p < prefix.length; p++) { | 200 | for (let p = 0; p < prefix.length; p++) { |
195 | if (!prefix[p]) { | 201 | if (!prefix[ p ]) { |
196 | type = type.toLowerCase() | 202 | type = type.toLowerCase() |
197 | } | 203 | } |
198 | 204 | ||
199 | if (action === 'addEvent') { | 205 | if (action === 'addEvent') { |
200 | element.addEventListener(prefix[p] + type, callback, false) | 206 | element.addEventListener(prefix[ p ] + type, callback, false) |
201 | } else if (action === 'removeEvent') { | 207 | } else if (action === 'removeEvent') { |
202 | element.removeEventListener(prefix[p] + type, callback, false) | 208 | element.removeEventListener(prefix[ p ] + type, callback, false) |
203 | } | 209 | } |
204 | } | 210 | } |
205 | } | 211 | } |
@@ -211,7 +217,7 @@ class SettingsMenuItem extends MenuItem { | |||
211 | 217 | ||
212 | if (this.menuToLoad === 'mainmenu') { | 218 | if (this.menuToLoad === 'mainmenu') { |
213 | // hide submenu | 219 | // hide submenu |
214 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 220 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
215 | 221 | ||
216 | // reset opacity to 0 | 222 | // reset opacity to 0 |
217 | this.settingsSubMenuEl_.style.opacity = '0' | 223 | this.settingsSubMenuEl_.style.opacity = '0' |
@@ -219,25 +225,27 @@ class SettingsMenuItem extends MenuItem { | |||
219 | } | 225 | } |
220 | 226 | ||
221 | reset () { | 227 | reset () { |
222 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 228 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
223 | this.settingsSubMenuEl_.style.opacity = '0' | 229 | this.settingsSubMenuEl_.style.opacity = '0' |
224 | this.setMargin() | 230 | this.setMargin() |
225 | } | 231 | } |
226 | 232 | ||
227 | loadMainMenu () { | 233 | loadMainMenu () { |
234 | const mainMenuEl = this.mainMenu.el() as HTMLElement | ||
228 | this.menuToLoad = 'mainmenu' | 235 | this.menuToLoad = 'mainmenu' |
229 | this.mainMenu.show() | 236 | this.mainMenu.show() |
230 | this.mainMenu.el_.style.opacity = '0' | 237 | mainMenuEl.style.opacity = '0' |
231 | 238 | ||
232 | // back button will always take you to main menu, so set dialog sizes | 239 | // back button will always take you to main menu, so set dialog sizes |
233 | this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) | 240 | const mainMenuAny = this.mainMenu as any |
241 | this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ]) | ||
234 | 242 | ||
235 | // animation not triggered without timeout (some async stuff ?!?) | 243 | // animation not triggered without timeout (some async stuff ?!?) |
236 | setTimeout(() => { | 244 | setTimeout(() => { |
237 | // animate margin and opacity before hiding the submenu | 245 | // animate margin and opacity before hiding the submenu |
238 | // this triggers CSS Transition event | 246 | // this triggers CSS Transition event |
239 | this.setMargin() | 247 | this.setMargin() |
240 | this.mainMenu.el_.style.opacity = '1' | 248 | mainMenuEl.style.opacity = '1' |
241 | }, 0) | 249 | }, 0) |
242 | } | 250 | } |
243 | 251 | ||
@@ -251,8 +259,8 @@ class SettingsMenuItem extends MenuItem { | |||
251 | this.update() | 259 | this.update() |
252 | }) | 260 | }) |
253 | 261 | ||
254 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 262 | this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText()) |
255 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 263 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) |
256 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) | 264 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) |
257 | this.update() | 265 | this.update() |
258 | 266 | ||
@@ -283,7 +291,8 @@ class SettingsMenuItem extends MenuItem { | |||
283 | // or sets options_['selected'] on the selected playback rate. | 291 | // or sets options_['selected'] on the selected playback rate. |
284 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | 292 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton |
285 | if (subMenu === 'PlaybackRateMenuButton') { | 293 | if (subMenu === 'PlaybackRateMenuButton') { |
286 | setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) | 294 | const html = (this.subMenu as any).labelEl_.innerHTML |
295 | setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = html, 250) | ||
287 | } else { | 296 | } else { |
288 | // Loop trough the submenu items to find the selected child | 297 | // Loop trough the submenu items to find the selected child |
289 | for (const subMenuItem of this.subMenu.menu.children_) { | 298 | for (const subMenuItem of this.subMenu.menu.children_) { |
@@ -292,13 +301,15 @@ class SettingsMenuItem extends MenuItem { | |||
292 | } | 301 | } |
293 | 302 | ||
294 | if (subMenuItem.hasClass('vjs-selected')) { | 303 | if (subMenuItem.hasClass('vjs-selected')) { |
304 | const subMenuItemUntyped = subMenuItem as any | ||
305 | |||
295 | // Prefer to use the function | 306 | // Prefer to use the function |
296 | if (typeof subMenuItem.getLabel === 'function') { | 307 | if (typeof subMenuItemUntyped.getLabel === 'function') { |
297 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() | 308 | this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel() |
298 | break | 309 | break |
299 | } | 310 | } |
300 | 311 | ||
301 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label | 312 | this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label |
302 | } | 313 | } |
303 | } | 314 | } |
304 | } | 315 | } |
@@ -313,7 +324,7 @@ class SettingsMenuItem extends MenuItem { | |||
313 | if (!(item instanceof component)) { | 324 | if (!(item instanceof component)) { |
314 | continue | 325 | continue |
315 | } | 326 | } |
316 | item.on(['tap', 'click'], this.submenuClickHandler) | 327 | item.on([ 'tap', 'click' ], this.submenuClickHandler) |
317 | } | 328 | } |
318 | } | 329 | } |
319 | 330 | ||
@@ -321,11 +332,11 @@ class SettingsMenuItem extends MenuItem { | |||
321 | // if number of submenu items change dynamically more logic will be needed | 332 | // if number of submenu items change dynamically more logic will be needed |
322 | setSize () { | 333 | setSize () { |
323 | this.dialog.removeClass('vjs-hidden') | 334 | this.dialog.removeClass('vjs-hidden') |
324 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | 335 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') |
325 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | 336 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
326 | this.setMargin() | 337 | this.setMargin() |
327 | this.dialog.addClass('vjs-hidden') | 338 | this.dialog.addClass('vjs-hidden') |
328 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 339 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
329 | } | 340 | } |
330 | 341 | ||
331 | setMargin () { | 342 | setMargin () { |
@@ -341,19 +352,19 @@ class SettingsMenuItem extends MenuItem { | |||
341 | */ | 352 | */ |
342 | hideSubMenu () { | 353 | hideSubMenu () { |
343 | // after removing settings item this.el_ === null | 354 | // after removing settings item this.el_ === null |
344 | if (!this.el_) { | 355 | if (!this.el()) { |
345 | return | 356 | return |
346 | } | 357 | } |
347 | 358 | ||
348 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | 359 | if (videojs.dom.hasClass(this.el(), 'open')) { |
349 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 360 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
350 | videojsUntyped.dom.removeClass(this.el_, 'open') | 361 | videojs.dom.removeClass(this.el(), 'open') |
351 | } | 362 | } |
352 | } | 363 | } |
353 | 364 | ||
354 | } | 365 | } |
355 | 366 | ||
356 | SettingsMenuItem.prototype.contentElType = 'button' | 367 | (SettingsMenuItem as any).prototype.contentElType = 'button' |
357 | videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) | 368 | videojs.registerComponent('SettingsMenuItem', SettingsMenuItem) |
358 | 369 | ||
359 | export { SettingsMenuItem } | 370 | export { SettingsMenuItem } |
diff --git a/client/src/assets/player/videojs-components/settings-panel-child.ts b/client/src/assets/player/videojs-components/settings-panel-child.ts new file mode 100644 index 000000000..d12e8218a --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-panel-child.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanelChild extends Component { | ||
6 | |||
7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | } | ||
10 | |||
11 | createEl () { | ||
12 | return super.createEl('div', { | ||
13 | className: 'vjs-settings-panel-child', | ||
14 | innerHTML: '', | ||
15 | tabIndex: -1 | ||
16 | }) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | ||
21 | |||
22 | export { SettingsPanelChild } | ||
diff --git a/client/src/assets/player/videojs-components/settings-panel.ts b/client/src/assets/player/videojs-components/settings-panel.ts new file mode 100644 index 000000000..2090abf45 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-panel.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanel extends Component { | ||
6 | |||
7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | } | ||
10 | |||
11 | createEl () { | ||
12 | return super.createEl('div', { | ||
13 | className: 'vjs-settings-panel', | ||
14 | innerHTML: '', | ||
15 | tabIndex: -1 | ||
16 | }) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | Component.registerComponent('SettingsPanel', SettingsPanel) | ||
21 | |||
22 | export { SettingsPanel } | ||
diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index bf383cf34..1c8c9f154 100644 --- a/client/src/assets/player/videojs-components/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -1,26 +1,24 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' | 2 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 3 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
9 | class TheaterButton extends Button { | 5 | class TheaterButton extends Button { |
10 | 6 | ||
11 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' | 7 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' |
12 | 8 | ||
13 | constructor (player: videojs.Player, options: any) { | 9 | constructor (player: VideoJsPlayer, options: videojs.ComponentOptions) { |
14 | super(player, options) | 10 | super(player, options) |
15 | 11 | ||
16 | const enabled = getStoredTheater() | 12 | const enabled = getStoredTheater() |
17 | if (enabled === true) { | 13 | if (enabled === true) { |
18 | this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) | 14 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) |
19 | 15 | ||
20 | this.handleTheaterChange() | 16 | this.handleTheaterChange() |
21 | } | 17 | } |
22 | 18 | ||
23 | this.player_.theaterEnabled = enabled | 19 | this.controlText('Theater mode') |
20 | |||
21 | this.player().theaterEnabled = enabled | ||
24 | } | 22 | } |
25 | 23 | ||
26 | buildCSSClass () { | 24 | buildCSSClass () { |
@@ -52,6 +50,4 @@ class TheaterButton extends Button { | |||
52 | } | 50 | } |
53 | } | 51 | } |
54 | 52 | ||
55 | TheaterButton.prototype.controlText_ = 'Theater mode' | 53 | videojs.registerComponent('TheaterButton', TheaterButton) |
56 | |||
57 | TheaterButton.registerComponent('TheaterButton', TheaterButton) | ||
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 35cf85c99..d26fc38fa 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -1,17 +1,15 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | 2 | ||
5 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
6 | import { renderVideo } from './video-renderer' | 4 | import { renderVideo } from './video-renderer' |
7 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' | 5 | import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
8 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' | 6 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
9 | import { PeertubeChunkStore } from './peertube-chunk-store' | 7 | import { PeertubeChunkStore } from './peertube-chunk-store' |
10 | import { | 8 | import { |
11 | getAverageBandwidthInStore, | 9 | getAverageBandwidthInStore, |
12 | getStoredMute, | 10 | getStoredMute, |
13 | getStoredVolume, | ||
14 | getStoredP2PEnabled, | 11 | getStoredP2PEnabled, |
12 | getStoredVolume, | ||
15 | saveAverageBandwidth | 13 | saveAverageBandwidth |
16 | } from '../peertube-player-local-storage' | 14 | } from '../peertube-player-local-storage' |
17 | import { VideoFile } from '@shared/models' | 15 | import { VideoFile } from '@shared/models' |
@@ -24,13 +22,14 @@ type PlayOptions = { | |||
24 | delay?: number | 22 | delay?: number |
25 | } | 23 | } |
26 | 24 | ||
27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 25 | const Plugin = videojs.getPlugin('plugin') |
26 | |||
28 | class WebTorrentPlugin extends Plugin { | 27 | class WebTorrentPlugin extends Plugin { |
29 | private readonly playerElement: HTMLVideoElement | 28 | private readonly playerElement: HTMLVideoElement |
30 | 29 | ||
31 | private readonly autoplay: boolean = false | 30 | private readonly autoplay: boolean = false |
32 | private readonly startTime: number = 0 | 31 | private readonly startTime: number = 0 |
33 | private readonly savePlayerSrcFunction: Function | 32 | private readonly savePlayerSrcFunction: VideoJsPlayer['src'] |
34 | private readonly videoFiles: VideoFile[] | 33 | private readonly videoFiles: VideoFile[] |
35 | private readonly videoDuration: number | 34 | private readonly videoDuration: number |
36 | private readonly CONSTANTS = { | 35 | private readonly CONSTANTS = { |
@@ -49,7 +48,6 @@ class WebTorrentPlugin extends Plugin { | |||
49 | dht: false | 48 | dht: false |
50 | }) | 49 | }) |
51 | 50 | ||
52 | private player: any | ||
53 | private currentVideoFile: VideoFile | 51 | private currentVideoFile: VideoFile |
54 | private torrent: WebTorrent.Torrent | 52 | private torrent: WebTorrent.Torrent |
55 | 53 | ||
@@ -70,8 +68,8 @@ class WebTorrentPlugin extends Plugin { | |||
70 | 68 | ||
71 | private downloadSpeeds: number[] = [] | 69 | private downloadSpeeds: number[] = [] |
72 | 70 | ||
73 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { | 71 | constructor (player: VideoJsPlayer, options?: WebtorrentPluginOptions) { |
74 | super(player, options) | 72 | super(player) |
75 | 73 | ||
76 | this.startTime = timeToInt(options.startTime) | 74 | this.startTime = timeToInt(options.startTime) |
77 | 75 | ||
@@ -147,12 +145,12 @@ class WebTorrentPlugin extends Plugin { | |||
147 | } | 145 | } |
148 | 146 | ||
149 | // Do not display error to user because we will have multiple fallback | 147 | // Do not display error to user because we will have multiple fallback |
150 | this.disableErrorDisplay() | 148 | this.disableErrorDisplay(); |
151 | 149 | ||
152 | // Hack to "simulate" src link in video.js >= 6 | 150 | // Hack to "simulate" src link in video.js >= 6 |
153 | // Without this, we can't play the video after pausing it | 151 | // Without this, we can't play the video after pausing it |
154 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 | 152 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 |
155 | this.player.src = () => true | 153 | (this.player as any).src = () => true |
156 | const oldPlaybackRate = this.player.playbackRate() | 154 | const oldPlaybackRate = this.player.playbackRate() |
157 | 155 | ||
158 | const previousVideoFile = this.currentVideoFile | 156 | const previousVideoFile = this.currentVideoFile |
@@ -333,7 +331,7 @@ class WebTorrentPlugin extends Plugin { | |||
333 | 331 | ||
334 | const playPromise = this.player.play() | 332 | const playPromise = this.player.play() |
335 | if (playPromise !== undefined) { | 333 | if (playPromise !== undefined) { |
336 | return playPromise.then(done) | 334 | return playPromise.then(() => done()) |
337 | .catch((err: Error) => { | 335 | .catch((err: Error) => { |
338 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { | 336 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { |
339 | return | 337 | return |
@@ -426,8 +424,8 @@ class WebTorrentPlugin extends Plugin { | |||
426 | } | 424 | } |
427 | 425 | ||
428 | // Proxy first play | 426 | // Proxy first play |
429 | const oldPlay = this.player.play.bind(this.player) | 427 | const oldPlay = this.player.play.bind(this.player); |
430 | this.player.play = () => { | 428 | (this.player as any).play = () => { |
431 | this.player.addClass('vjs-has-big-play-button-clicked') | 429 | this.player.addClass('vjs-has-big-play-button-clicked') |
432 | this.player.play = oldPlay | 430 | this.player.play = oldPlay |
433 | 431 | ||
@@ -619,7 +617,7 @@ class WebTorrentPlugin extends Plugin { | |||
619 | video: qualityLevelsPayload | 617 | video: qualityLevelsPayload |
620 | } | 618 | } |
621 | } | 619 | } |
622 | this.player.tech_.trigger('loadedqualitydata', payload) | 620 | this.player.tech(true).trigger('loadedqualitydata', payload) |
623 | } | 621 | } |
624 | 622 | ||
625 | private buildQualityLabel (file: VideoFile) { | 623 | private buildQualityLabel (file: VideoFile) { |
@@ -651,9 +649,9 @@ class WebTorrentPlugin extends Plugin { | |||
651 | return | 649 | return |
652 | } | 650 | } |
653 | 651 | ||
654 | for (let i = 0; i < qualityLevels; i++) { | 652 | for (let i = 0; i < qualityLevels.length; i++) { |
655 | const q = this.player.qualityLevels[i] | 653 | const q = qualityLevels[i] |
656 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | 654 | if (q.height === resolutionId) qualityLevels.selectedIndex_ = i |
657 | } | 655 | } |
658 | } | 656 | } |
659 | } | 657 | } |
diff --git a/client/tsconfig.json b/client/tsconfig.json index 8824c4f7c..c4f2d6a6a 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json | |||
@@ -26,7 +26,7 @@ | |||
26 | "paths": { | 26 | "paths": { |
27 | "@app/*": [ "src/app/*" ], | 27 | "@app/*": [ "src/app/*" ], |
28 | "@shared/*": [ "../shared/*" ], | 28 | "@shared/*": [ "../shared/*" ], |
29 | "video.js": [ "node_modules/video.js/dist/alt/video.core.js" ], | 29 | "video.js": [ "node_modules/video.js/dist/alt/video.core.novtt" ], |
30 | "fs": [ "src/shims/noop" ], | 30 | "fs": [ "src/shims/noop" ], |
31 | "http": [ "src/shims/http" ], | 31 | "http": [ "src/shims/http" ], |
32 | "https": [ "src/shims/https" ], | 32 | "https": [ "src/shims/https" ], |
diff --git a/client/webpack/webpack.video-embed.js b/client/webpack/webpack.video-embed.js index 909048cca..f6d532556 100644 --- a/client/webpack/webpack.video-embed.js +++ b/client/webpack/webpack.video-embed.js | |||
@@ -27,7 +27,7 @@ module.exports = function () { | |||
27 | modules: [ helpers.root('src'), helpers.root('node_modules') ], | 27 | modules: [ helpers.root('src'), helpers.root('node_modules') ], |
28 | 28 | ||
29 | alias: { | 29 | alias: { |
30 | 'video.js$': path.resolve('node_modules/video.js/dist/alt/video.core.js') | 30 | 'video.js$': path.resolve('node_modules/video.js/dist/alt/video.core.novtt.js') |
31 | } | 31 | } |
32 | }, | 32 | }, |
33 | 33 | ||
diff --git a/client/yarn.lock b/client/yarn.lock index 0855a2570..20ff5c3c8 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -1157,10 +1157,10 @@ | |||
1157 | resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" | 1157 | resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" |
1158 | integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== | 1158 | integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== |
1159 | 1159 | ||
1160 | "@types/video.js@^7.2.5": | 1160 | "@types/video.js@^7.3.3": |
1161 | version "7.2.15" | 1161 | version "7.3.3" |
1162 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.2.15.tgz#03d950f01c985a5082ead4d1b73064455a1c8c6f" | 1162 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.3.tgz#b6870d954473dfd694e10b55a90c0f3be8522da3" |
1163 | integrity sha512-NsojVfvTwdVqDe0+vJaoHOO2iuLm0sp/u8jEsZeLGsM3gNfg5WIOFd6NC0cQR9JHUuDPPSPF70jxdklGWm5jhQ== | 1163 | integrity sha512-yAb46+4A0dKFxOQRVLoLyfC/S/BmHLE10MxPXt/t88+7R4GWLHosHelVtYpKBRykjptdkqfQXNRXoQzDeKm6MA== |
1164 | 1164 | ||
1165 | "@types/webpack-sources@^0.1.5": | 1165 | "@types/webpack-sources@^0.1.5": |
1166 | version "0.1.5" | 1166 | version "0.1.5" |
@@ -2586,6 +2586,29 @@ chardet@^0.7.0: | |||
2586 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" | 2586 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" |
2587 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== | 2587 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== |
2588 | 2588 | ||
2589 | chart.js@^2.9.3: | ||
2590 | version "2.9.3" | ||
2591 | resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7" | ||
2592 | integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw== | ||
2593 | dependencies: | ||
2594 | chartjs-color "^2.1.0" | ||
2595 | moment "^2.10.2" | ||
2596 | |||
2597 | chartjs-color-string@^0.6.0: | ||
2598 | version "0.6.0" | ||
2599 | resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71" | ||
2600 | integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A== | ||
2601 | dependencies: | ||
2602 | color-name "^1.0.0" | ||
2603 | |||
2604 | chartjs-color@^2.1.0: | ||
2605 | version "2.4.1" | ||
2606 | resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0" | ||
2607 | integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w== | ||
2608 | dependencies: | ||
2609 | chartjs-color-string "^0.6.0" | ||
2610 | color-convert "^1.9.3" | ||
2611 | |||
2589 | check-types@^8.0.3: | 2612 | check-types@^8.0.3: |
2590 | version "8.0.3" | 2613 | version "8.0.3" |
2591 | resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" | 2614 | resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" |
@@ -2800,7 +2823,7 @@ collection-visit@^1.0.0: | |||
2800 | map-visit "^1.0.0" | 2823 | map-visit "^1.0.0" |
2801 | object-visit "^1.0.0" | 2824 | object-visit "^1.0.0" |
2802 | 2825 | ||
2803 | color-convert@^1.9.0: | 2826 | color-convert@^1.9.0, color-convert@^1.9.3: |
2804 | version "1.9.3" | 2827 | version "1.9.3" |
2805 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" | 2828 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" |
2806 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== | 2829 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== |
@@ -2812,6 +2835,11 @@ color-name@1.1.3: | |||
2812 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" | 2835 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" |
2813 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= | 2836 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= |
2814 | 2837 | ||
2838 | color-name@^1.0.0: | ||
2839 | version "1.1.4" | ||
2840 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" | ||
2841 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | ||
2842 | |||
2815 | colors@1.1.2: | 2843 | colors@1.1.2: |
2816 | version "1.1.2" | 2844 | version "1.1.2" |
2817 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" | 2845 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" |
@@ -6941,6 +6969,11 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: | |||
6941 | dependencies: | 6969 | dependencies: |
6942 | minimist "0.0.8" | 6970 | minimist "0.0.8" |
6943 | 6971 | ||
6972 | moment@^2.10.2: | ||
6973 | version "2.24.0" | ||
6974 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" | ||
6975 | integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== | ||
6976 | |||
6944 | mousetrap@^1.6.0: | 6977 | mousetrap@^1.6.0: |
6945 | version "1.6.3" | 6978 | version "1.6.3" |
6946 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" | 6979 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" |
@@ -10809,6 +10842,11 @@ void-elements@^2.0.0: | |||
10809 | resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" | 10842 | resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" |
10810 | integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= | 10843 | integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= |
10811 | 10844 | ||
10845 | vtt.js@^0.13.0: | ||
10846 | version "0.13.0" | ||
10847 | resolved "https://registry.yarnpkg.com/vtt.js/-/vtt.js-0.13.0.tgz#955c667b34d5325b2012cb9e8ba9bad6e0b11ff8" | ||
10848 | integrity sha1-lVxmezTVMlsgEsuei6m61uCxH/g= | ||
10849 | |||
10812 | watchpack@^1.6.0: | 10850 | watchpack@^1.6.0: |
10813 | version "1.6.0" | 10851 | version "1.6.0" |
10814 | resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" | 10852 | resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" |
diff --git a/config/test.yaml b/config/test.yaml index 5758c1887..74979f3a7 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -40,18 +40,18 @@ contact_form: | |||
40 | 40 | ||
41 | redundancy: | 41 | redundancy: |
42 | videos: | 42 | videos: |
43 | check_interval: '10 minutes' | 43 | check_interval: '1 minute' |
44 | strategies: | 44 | strategies: |
45 | - | 45 | - |
46 | size: '10MB' | 46 | size: '1000MB' |
47 | min_lifetime: '10 minutes' | 47 | min_lifetime: '10 minutes' |
48 | strategy: 'most-views' | 48 | strategy: 'most-views' |
49 | - | 49 | - |
50 | size: '10MB' | 50 | size: '1000MB' |
51 | min_lifetime: '10 minutes' | 51 | min_lifetime: '10 minutes' |
52 | strategy: 'trending' | 52 | strategy: 'trending' |
53 | - | 53 | - |
54 | size: '10MB' | 54 | size: '1000MB' |
55 | min_lifetime: '10 minutes' | 55 | min_lifetime: '10 minutes' |
56 | strategy: 'recently-added' | 56 | strategy: 'recently-added' |
57 | min_views: 1 | 57 | min_views: 1 |
diff --git a/package.json b/package.json index d4858725a..b416e9501 100644 --- a/package.json +++ b/package.json | |||
@@ -5,7 +5,7 @@ | |||
5 | "private": true, | 5 | "private": true, |
6 | "licence": "AGPL-3.0", | 6 | "licence": "AGPL-3.0", |
7 | "engines": { | 7 | "engines": { |
8 | "node": ">=8.x" | 8 | "node": ">=10.x" |
9 | }, | 9 | }, |
10 | "bin": { | 10 | "bin": { |
11 | "peertube": "dist/server/tools/peertube.js" | 11 | "peertube": "dist/server/tools/peertube.js" |
@@ -41,7 +41,7 @@ | |||
41 | "i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js", | 41 | "i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js", |
42 | "reset-password": "node ./dist/scripts/reset-password.js", | 42 | "reset-password": "node ./dist/scripts/reset-password.js", |
43 | "play": "scripty", | 43 | "play": "scripty", |
44 | "dev": "scripty", | 44 | "dev": "sh ./scripts/dev/index.sh", |
45 | "dev:server": "sh ./scripts/dev/server.sh", | 45 | "dev:server": "sh ./scripts/dev/server.sh", |
46 | "dev:embed": "scripty", | 46 | "dev:embed": "scripty", |
47 | "dev:client": "sh ./scripts/dev/client.sh", | 47 | "dev:client": "sh ./scripts/dev/client.sh", |
diff --git a/scripts/ci.sh b/scripts/ci.sh index d111b7447..4063a6241 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh | |||
@@ -12,7 +12,7 @@ killall -q peertube || true | |||
12 | perl -0777 -i -pe 's#proxy:(\n\s+)enabled: false\n\s+url: ""#proxy:$1enabled: true$1url: "http://188.165.225.149:7899"#' config/test.yaml | 12 | perl -0777 -i -pe 's#proxy:(\n\s+)enabled: false\n\s+url: ""#proxy:$1enabled: true$1url: "http://188.165.225.149:7899"#' config/test.yaml |
13 | 13 | ||
14 | if [ "$1" = "misc" ]; then | 14 | if [ "$1" = "misc" ]; then |
15 | npm run build -- --light-fr | 15 | npm run build -- --light |
16 | mocha --timeout 5000 --exit --require ts-node/register --require tsconfig-paths/register --bail server/tests/client.ts \ | 16 | mocha --timeout 5000 --exit --require ts-node/register --require tsconfig-paths/register --bail server/tests/client.ts \ |
17 | server/tests/feeds/index.ts \ | 17 | server/tests/feeds/index.ts \ |
18 | server/tests/misc-endpoints.ts \ | 18 | server/tests/misc-endpoints.ts \ |
diff --git a/scripts/dev/index.sh b/scripts/dev/index.sh index d221d2fc8..9e89516b8 100755 --- a/scripts/dev/index.sh +++ b/scripts/dev/index.sh | |||
@@ -3,5 +3,5 @@ | |||
3 | set -eu | 3 | set -eu |
4 | 4 | ||
5 | NODE_ENV=test npm run concurrently -- -k \ | 5 | NODE_ENV=test npm run concurrently -- -k \ |
6 | "npm run dev:client -- --skip-server" \ | 6 | "sh scripts/dev/client.sh --skip-server" \ |
7 | "npm run dev:server" | 7 | "sh scripts/dev/server.sh" |
@@ -1,10 +1,6 @@ | |||
1 | import { registerTSPaths } from './server/helpers/register-ts-paths' | 1 | import { registerTSPaths } from './server/helpers/register-ts-paths' |
2 | |||
3 | registerTSPaths() | 2 | registerTSPaths() |
4 | 3 | ||
5 | // FIXME: https://github.com/nodejs/node/pull/16853 | ||
6 | require('tls').DEFAULT_ECDH_CURVE = 'auto' | ||
7 | |||
8 | import { isTestInstance } from './server/helpers/core-utils' | 4 | import { isTestInstance } from './server/helpers/core-utils' |
9 | if (isTestInstance()) { | 5 | if (isTestInstance()) { |
10 | require('source-map-support').install() | 6 | require('source-map-support').install() |
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 29a403a43..c69de21fb 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -24,7 +24,7 @@ import { | |||
24 | } from '../../../middlewares/validators' | 24 | } from '../../../middlewares/validators' |
25 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 25 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
26 | import { JobQueue } from '../../../lib/job-queue' | 26 | import { JobQueue } from '../../../lib/job-queue' |
27 | import { removeRedundancyOf } from '../../../lib/redundancy' | 27 | import { removeRedundanciesOfServer } from '../../../lib/redundancy' |
28 | import { sequelizeTypescript } from '../../../initializers/database' | 28 | import { sequelizeTypescript } from '../../../initializers/database' |
29 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | 29 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' |
30 | 30 | ||
@@ -153,7 +153,7 @@ async function removeFollowing (req: express.Request, res: express.Response) { | |||
153 | await server.save({ transaction: t }) | 153 | await server.save({ transaction: t }) |
154 | 154 | ||
155 | // Async, could be long | 155 | // Async, could be long |
156 | removeRedundancyOf(server.id) | 156 | removeRedundanciesOfServer(server.id) |
157 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, err)) | 157 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, err)) |
158 | 158 | ||
159 | await follow.destroy({ transaction: t }) | 159 | await follow.destroy({ transaction: t }) |
diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts index 4ea6164a3..a11c1a74f 100644 --- a/server/controllers/api/server/redundancy.ts +++ b/server/controllers/api/server/redundancy.ts | |||
@@ -1,9 +1,24 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserRight } from '../../../../shared/models/users' | 2 | import { UserRight } from '../../../../shared/models/users' |
3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' | 3 | import { |
4 | import { updateServerRedundancyValidator } from '../../../middlewares/validators/redundancy' | 4 | asyncMiddleware, |
5 | import { removeRedundancyOf } from '../../../lib/redundancy' | 5 | authenticate, |
6 | ensureUserHasRight, | ||
7 | paginationValidator, | ||
8 | setDefaultPagination, | ||
9 | setDefaultVideoRedundanciesSort, | ||
10 | videoRedundanciesSortValidator | ||
11 | } from '../../../middlewares' | ||
12 | import { | ||
13 | listVideoRedundanciesValidator, | ||
14 | updateServerRedundancyValidator, | ||
15 | addVideoRedundancyValidator, | ||
16 | removeVideoRedundancyValidator | ||
17 | } from '../../../middlewares/validators/redundancy' | ||
18 | import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy' | ||
6 | import { logger } from '../../../helpers/logger' | 19 | import { logger } from '../../../helpers/logger' |
20 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | ||
21 | import { JobQueue } from '@server/lib/job-queue' | ||
7 | 22 | ||
8 | const serverRedundancyRouter = express.Router() | 23 | const serverRedundancyRouter = express.Router() |
9 | 24 | ||
@@ -14,6 +29,31 @@ serverRedundancyRouter.put('/redundancy/:host', | |||
14 | asyncMiddleware(updateRedundancy) | 29 | asyncMiddleware(updateRedundancy) |
15 | ) | 30 | ) |
16 | 31 | ||
32 | serverRedundancyRouter.get('/redundancy/videos', | ||
33 | authenticate, | ||
34 | ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), | ||
35 | listVideoRedundanciesValidator, | ||
36 | paginationValidator, | ||
37 | videoRedundanciesSortValidator, | ||
38 | setDefaultVideoRedundanciesSort, | ||
39 | setDefaultPagination, | ||
40 | asyncMiddleware(listVideoRedundancies) | ||
41 | ) | ||
42 | |||
43 | serverRedundancyRouter.post('/redundancy/videos', | ||
44 | authenticate, | ||
45 | ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), | ||
46 | addVideoRedundancyValidator, | ||
47 | asyncMiddleware(addVideoRedundancy) | ||
48 | ) | ||
49 | |||
50 | serverRedundancyRouter.delete('/redundancy/videos/:redundancyId', | ||
51 | authenticate, | ||
52 | ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), | ||
53 | removeVideoRedundancyValidator, | ||
54 | asyncMiddleware(removeVideoRedundancyController) | ||
55 | ) | ||
56 | |||
17 | // --------------------------------------------------------------------------- | 57 | // --------------------------------------------------------------------------- |
18 | 58 | ||
19 | export { | 59 | export { |
@@ -22,6 +62,42 @@ export { | |||
22 | 62 | ||
23 | // --------------------------------------------------------------------------- | 63 | // --------------------------------------------------------------------------- |
24 | 64 | ||
65 | async function listVideoRedundancies (req: express.Request, res: express.Response) { | ||
66 | const resultList = await VideoRedundancyModel.listForApi({ | ||
67 | start: req.query.start, | ||
68 | count: req.query.count, | ||
69 | sort: req.query.sort, | ||
70 | target: req.query.target, | ||
71 | strategy: req.query.strategy | ||
72 | }) | ||
73 | |||
74 | const result = { | ||
75 | total: resultList.total, | ||
76 | data: resultList.data.map(r => VideoRedundancyModel.toFormattedJSONStatic(r)) | ||
77 | } | ||
78 | |||
79 | return res.json(result) | ||
80 | } | ||
81 | |||
82 | async function addVideoRedundancy (req: express.Request, res: express.Response) { | ||
83 | const payload = { | ||
84 | videoId: res.locals.onlyVideo.id | ||
85 | } | ||
86 | |||
87 | await JobQueue.Instance.createJob({ | ||
88 | type: 'video-redundancy', | ||
89 | payload | ||
90 | }) | ||
91 | |||
92 | return res.sendStatus(204) | ||
93 | } | ||
94 | |||
95 | async function removeVideoRedundancyController (req: express.Request, res: express.Response) { | ||
96 | await removeVideoRedundancy(res.locals.videoRedundancy) | ||
97 | |||
98 | return res.sendStatus(204) | ||
99 | } | ||
100 | |||
25 | async function updateRedundancy (req: express.Request, res: express.Response) { | 101 | async function updateRedundancy (req: express.Request, res: express.Response) { |
26 | const server = res.locals.server | 102 | const server = res.locals.server |
27 | 103 | ||
@@ -30,7 +106,7 @@ async function updateRedundancy (req: express.Request, res: express.Response) { | |||
30 | await server.save() | 106 | await server.save() |
31 | 107 | ||
32 | // Async, could be long | 108 | // Async, could be long |
33 | removeRedundancyOf(server.id) | 109 | removeRedundanciesOfServer(server.id) |
34 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) | 110 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) |
35 | 111 | ||
36 | return res.sendStatus(204) | 112 | return res.sendStatus(204) |
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts index 3616c074d..6d508a481 100644 --- a/server/controllers/api/server/stats.ts +++ b/server/controllers/api/server/stats.ts | |||
@@ -10,6 +10,7 @@ import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' | |||
10 | import { cacheRoute } from '../../../middlewares/cache' | 10 | import { cacheRoute } from '../../../middlewares/cache' |
11 | import { VideoFileModel } from '../../../models/video/video-file' | 11 | import { VideoFileModel } from '../../../models/video/video-file' |
12 | import { CONFIG } from '../../../initializers/config' | 12 | import { CONFIG } from '../../../initializers/config' |
13 | import { VideoRedundancyStrategyWithManual } from '@shared/models' | ||
13 | 14 | ||
14 | const statsRouter = express.Router() | 15 | const statsRouter = express.Router() |
15 | 16 | ||
@@ -25,8 +26,15 @@ async function getStats (req: express.Request, res: express.Response) { | |||
25 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 26 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
26 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | 27 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() |
27 | 28 | ||
29 | const strategies: { strategy: VideoRedundancyStrategyWithManual, size: number }[] = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES | ||
30 | .map(r => ({ | ||
31 | strategy: r.strategy, | ||
32 | size: r.size | ||
33 | })) | ||
34 | strategies.push({ strategy: 'manual', size: null }) | ||
35 | |||
28 | const videosRedundancyStats = await Promise.all( | 36 | const videosRedundancyStats = await Promise.all( |
29 | CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { | 37 | strategies.map(r => { |
30 | return VideoRedundancyModel.getStats(r.strategy) | 38 | return VideoRedundancyModel.getStats(r.strategy) |
31 | .then(stats => Object.assign(stats, { strategy: r.strategy, totalSize: r.size })) | 39 | .then(stats => Object.assign(stats, { strategy: r.strategy, totalSize: r.size })) |
32 | }) | 40 | }) |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 8d4ff07eb..a593f7076 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -12,7 +12,8 @@ import { | |||
12 | VIDEO_CATEGORIES, | 12 | VIDEO_CATEGORIES, |
13 | VIDEO_LANGUAGES, | 13 | VIDEO_LANGUAGES, |
14 | VIDEO_LICENCES, | 14 | VIDEO_LICENCES, |
15 | VIDEO_PRIVACIES | 15 | VIDEO_PRIVACIES, |
16 | VIDEO_TRANSCODING_FPS | ||
16 | } from '../../../initializers/constants' | 17 | } from '../../../initializers/constants' |
17 | import { | 18 | import { |
18 | changeVideoChannelShare, | 19 | changeVideoChannelShare, |
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts index fc9575160..77c851880 100644 --- a/server/controllers/webfinger.ts +++ b/server/controllers/webfinger.ts | |||
@@ -18,7 +18,7 @@ export { | |||
18 | // --------------------------------------------------------------------------- | 18 | // --------------------------------------------------------------------------- |
19 | 19 | ||
20 | function webfingerController (req: express.Request, res: express.Response) { | 20 | function webfingerController (req: express.Request, res: express.Response) { |
21 | const actor = res.locals.actorFull | 21 | const actor = res.locals.actorUrl |
22 | 22 | ||
23 | const json = { | 23 | const json = { |
24 | subject: req.query.resource, | 24 | subject: req.query.resource, |
@@ -32,5 +32,5 @@ function webfingerController (req: express.Request, res: express.Response) { | |||
32 | ] | 32 | ] |
33 | } | 33 | } |
34 | 34 | ||
35 | return res.json(json).end() | 35 | return res.json(json) |
36 | } | 36 | } |
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index 21d5c53ca..c5b3b4d9f 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -6,7 +6,7 @@ import { CacheFileObject } from '../../../../shared/models/activitypub/objects' | |||
6 | function isCacheFileObjectValid (object: CacheFileObject) { | 6 | function isCacheFileObjectValid (object: CacheFileObject) { |
7 | return exists(object) && | 7 | return exists(object) && |
8 | object.type === 'CacheFile' && | 8 | object.type === 'CacheFile' && |
9 | isDateValid(object.expires) && | 9 | (object.expires === null || isDateValid(object.expires)) && |
10 | isActivityPubUrlValid(object.object) && | 10 | isActivityPubUrlValid(object.object) && |
11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) | 11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) |
12 | } | 12 | } |
diff --git a/server/helpers/custom-validators/video-redundancies.ts b/server/helpers/custom-validators/video-redundancies.ts new file mode 100644 index 000000000..50a559c4f --- /dev/null +++ b/server/helpers/custom-validators/video-redundancies.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { exists } from './misc' | ||
2 | |||
3 | function isVideoRedundancyTarget (value: any) { | ||
4 | return exists(value) && | ||
5 | (value === 'my-videos' || value === 'remote-videos') | ||
6 | } | ||
7 | |||
8 | // --------------------------------------------------------------------------- | ||
9 | |||
10 | export { | ||
11 | isVideoRedundancyTarget | ||
12 | } | ||
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 00c32e99a..63dc5b6a3 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -263,6 +263,10 @@ async function canDoQuickTranscode (path: string): Promise<boolean> { | |||
263 | return true | 263 | return true |
264 | } | 264 | } |
265 | 265 | ||
266 | function getClosestFramerateStandard (fps: number, hd = false): number { | ||
267 | return VIDEO_TRANSCODING_FPS[hd ? 'HD_STANDARD' : 'STANDARD'].slice(0).sort((a, b) => fps % a - fps % b)[0] | ||
268 | } | ||
269 | |||
266 | // --------------------------------------------------------------------------- | 270 | // --------------------------------------------------------------------------- |
267 | 271 | ||
268 | export { | 272 | export { |
@@ -286,13 +290,16 @@ export { | |||
286 | 290 | ||
287 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 291 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { |
288 | let fps = await getVideoFileFPS(options.inputPath) | 292 | let fps = await getVideoFileFPS(options.inputPath) |
289 | // On small/medium resolutions, limit FPS | ||
290 | if ( | 293 | if ( |
294 | // On small/medium resolutions, limit FPS | ||
291 | options.resolution !== undefined && | 295 | options.resolution !== undefined && |
292 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 296 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
293 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 297 | fps > VIDEO_TRANSCODING_FPS.AVERAGE || |
298 | // If the video is doesn't match hd standard | ||
299 | !VIDEO_TRANSCODING_FPS.HD_STANDARD.some(value => fps % value === 0) | ||
294 | ) { | 300 | ) { |
295 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 301 | // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value |
302 | fps = getClosestFramerateStandard(fps) | ||
296 | } | 303 | } |
297 | 304 | ||
298 | command = await presetH264(command, options.inputPath, options.resolution, fps) | 305 | command = await presetH264(command, options.inputPath, options.resolution, fps) |
@@ -305,7 +312,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
305 | 312 | ||
306 | if (fps) { | 313 | if (fps) { |
307 | // Hard FPS limits | 314 | // Hard FPS limits |
308 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 315 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, true) |
309 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | 316 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN |
310 | 317 | ||
311 | command = command.withFPS(fps) | 318 | command = command.withFPS(fps) |
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index 395417612..fd2988ad0 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -5,7 +5,7 @@ import * as winston from 'winston' | |||
5 | import { FileTransportOptions } from 'winston/lib/winston/transports' | 5 | import { FileTransportOptions } from 'winston/lib/winston/transports' |
6 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
7 | import { omit } from 'lodash' | 7 | import { omit } from 'lodash' |
8 | import { LOG_FILENAME } from '@server/initializers/constants' | 8 | import { LOG_FILENAME } from '../initializers/constants' |
9 | 9 | ||
10 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 10 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
11 | 11 | ||
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index 3a99518c6..8a5d030df 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -9,12 +9,12 @@ import { promisify2 } from './core-utils' | |||
9 | import { MVideo } from '@server/typings/models/video/video' | 9 | import { MVideo } from '@server/typings/models/video/video' |
10 | import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file' | 10 | import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file' |
11 | import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist' | 11 | import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist' |
12 | import { STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' | 12 | import { WEBSERVER } from '@server/initializers/constants' |
13 | import * as parseTorrent from 'parse-torrent' | 13 | import * as parseTorrent from 'parse-torrent' |
14 | import * as magnetUtil from 'magnet-uri' | 14 | import * as magnetUtil from 'magnet-uri' |
15 | import { isArray } from '@server/helpers/custom-validators/misc' | 15 | import { isArray } from '@server/helpers/custom-validators/misc' |
16 | import { extractVideo } from '@server/lib/videos' | 16 | import { extractVideo } from '@server/lib/videos' |
17 | import { getTorrentFileName, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 17 | import { getTorrentFileName, getVideoFilePath } from '@server/lib/video-paths' |
18 | 18 | ||
19 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) | 19 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) |
20 | 20 | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 7fd77f3e8..fd8bf09fc 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { IConfig } from 'config' | 1 | import { IConfig } from 'config' |
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { VideosRedundancy } from '../../shared/models' | 3 | import { VideosRedundancyStrategy } from '../../shared/models' |
4 | // Do not use barrels, remain constants as independent as possible | 4 | // Do not use barrels, remain constants as independent as possible |
5 | import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' | 5 | import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' |
6 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 6 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
@@ -304,7 +304,7 @@ function getLocalConfigFilePath () { | |||
304 | return join(dirname(configSources[ 0 ].name), filename + '.json') | 304 | return join(dirname(configSources[ 0 ].name), filename + '.json') |
305 | } | 305 | } |
306 | 306 | ||
307 | function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { | 307 | function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { |
308 | if (!objs) return [] | 308 | if (!objs) return [] |
309 | 309 | ||
310 | if (!Array.isArray(objs)) return objs | 310 | if (!Array.isArray(objs)) return objs |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 032f63c8f..64803b1db 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 470 | 17 | const LAST_MIGRATION_VERSION = 475 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
@@ -73,7 +73,9 @@ const SORTABLE_COLUMNS = { | |||
73 | 73 | ||
74 | PLUGINS: [ 'name', 'createdAt', 'updatedAt' ], | 74 | PLUGINS: [ 'name', 'createdAt', 'updatedAt' ], |
75 | 75 | ||
76 | AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ] | 76 | AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ], |
77 | |||
78 | VIDEO_REDUNDANCIES: [ 'name' ] | ||
77 | } | 79 | } |
78 | 80 | ||
79 | const OAUTH_LIFETIME = { | 81 | const OAUTH_LIFETIME = { |
@@ -117,45 +119,44 @@ const REMOTE_SCHEME = { | |||
117 | WS: 'wss' | 119 | WS: 'wss' |
118 | } | 120 | } |
119 | 121 | ||
120 | // TODO: remove 'video-file' | 122 | const JOB_ATTEMPTS: { [id in JobType]: number } = { |
121 | const JOB_ATTEMPTS: { [id in (JobType | 'video-file')]: number } = { | ||
122 | 'activitypub-http-broadcast': 5, | 123 | 'activitypub-http-broadcast': 5, |
123 | 'activitypub-http-unicast': 5, | 124 | 'activitypub-http-unicast': 5, |
124 | 'activitypub-http-fetcher': 5, | 125 | 'activitypub-http-fetcher': 5, |
125 | 'activitypub-follow': 5, | 126 | 'activitypub-follow': 5, |
126 | 'video-file-import': 1, | 127 | 'video-file-import': 1, |
127 | 'video-transcoding': 1, | 128 | 'video-transcoding': 1, |
128 | 'video-file': 1, | ||
129 | 'video-import': 1, | 129 | 'video-import': 1, |
130 | 'email': 5, | 130 | 'email': 5, |
131 | 'videos-views': 1, | 131 | 'videos-views': 1, |
132 | 'activitypub-refresher': 1 | 132 | 'activitypub-refresher': 1, |
133 | 'video-redundancy': 1 | ||
133 | } | 134 | } |
134 | const JOB_CONCURRENCY: { [id in (JobType | 'video-file')]: number } = { | 135 | const JOB_CONCURRENCY: { [id in JobType]: number } = { |
135 | 'activitypub-http-broadcast': 1, | 136 | 'activitypub-http-broadcast': 1, |
136 | 'activitypub-http-unicast': 5, | 137 | 'activitypub-http-unicast': 5, |
137 | 'activitypub-http-fetcher': 1, | 138 | 'activitypub-http-fetcher': 1, |
138 | 'activitypub-follow': 1, | 139 | 'activitypub-follow': 1, |
139 | 'video-file-import': 1, | 140 | 'video-file-import': 1, |
140 | 'video-transcoding': 1, | 141 | 'video-transcoding': 1, |
141 | 'video-file': 1, | ||
142 | 'video-import': 1, | 142 | 'video-import': 1, |
143 | 'email': 5, | 143 | 'email': 5, |
144 | 'videos-views': 1, | 144 | 'videos-views': 1, |
145 | 'activitypub-refresher': 1 | 145 | 'activitypub-refresher': 1, |
146 | 'video-redundancy': 1 | ||
146 | } | 147 | } |
147 | const JOB_TTL: { [id in (JobType | 'video-file')]: number } = { | 148 | const JOB_TTL: { [id in JobType]: number } = { |
148 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes | 149 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes |
149 | 'activitypub-http-unicast': 60000 * 10, // 10 minutes | 150 | 'activitypub-http-unicast': 60000 * 10, // 10 minutes |
150 | 'activitypub-http-fetcher': 60000 * 10, // 10 minutes | 151 | 'activitypub-http-fetcher': 60000 * 10, // 10 minutes |
151 | 'activitypub-follow': 60000 * 10, // 10 minutes | 152 | 'activitypub-follow': 60000 * 10, // 10 minutes |
152 | 'video-file-import': 1000 * 3600, // 1 hour | 153 | 'video-file-import': 1000 * 3600, // 1 hour |
153 | 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long | 154 | 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long |
154 | 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long | ||
155 | 'video-import': 1000 * 3600 * 2, // hours | 155 | 'video-import': 1000 * 3600 * 2, // hours |
156 | 'email': 60000 * 10, // 10 minutes | 156 | 'email': 60000 * 10, // 10 minutes |
157 | 'videos-views': undefined, // Unlimited | 157 | 'videos-views': undefined, // Unlimited |
158 | 'activitypub-refresher': 60000 * 10 // 10 minutes | 158 | 'activitypub-refresher': 60000 * 10, // 10 minutes |
159 | 'video-redundancy': 1000 * 3600 * 3 // 3 hours | ||
159 | } | 160 | } |
160 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { | 161 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { |
161 | 'videos-views': { | 162 | 'videos-views': { |
@@ -309,6 +310,8 @@ let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour | |||
309 | 310 | ||
310 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { | 311 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { |
311 | MIN: 10, | 312 | MIN: 10, |
313 | STANDARD: [24, 25, 30], | ||
314 | HD_STANDARD: [50, 60], | ||
312 | AVERAGE: 30, | 315 | AVERAGE: 30, |
313 | MAX: 60, | 316 | MAX: 60, |
314 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) | 317 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) |
diff --git a/server/initializers/migrations/0475-redundancy-expires-on.ts b/server/initializers/migrations/0475-redundancy-expires-on.ts new file mode 100644 index 000000000..7e392c8c0 --- /dev/null +++ b/server/initializers/migrations/0475-redundancy-expires-on.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.DATE, | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.changeColumn('videoRedundancy', 'expiresOn', data) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index 65b2dcb49..8252e95e9 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -13,7 +13,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | 13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) |
14 | 14 | ||
15 | return { | 15 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 16 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, |
17 | url: cacheFileObject.id, | 17 | url: cacheFileObject.id, |
18 | fileUrl: url.href, | 18 | fileUrl: url.href, |
19 | strategy: null, | 19 | strategy: null, |
@@ -30,7 +30,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | 30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) |
31 | 31 | ||
32 | return { | 32 | return { |
33 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, |
34 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
35 | fileUrl: url.href, | 35 | fileUrl: url.href, |
36 | strategy: null, | 36 | strategy: null, |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index ade93150f..7a9d5168b 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -639,7 +639,6 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide | |||
639 | createdAt: new Date(videoObject.published), | 639 | createdAt: new Date(videoObject.published), |
640 | publishedAt: new Date(videoObject.published), | 640 | publishedAt: new Date(videoObject.published), |
641 | originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, | 641 | originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, |
642 | // FIXME: updatedAt does not seems to be considered by Sequelize | ||
643 | updatedAt: new Date(videoObject.updated), | 642 | updatedAt: new Date(videoObject.updated), |
644 | views: videoObject.views, | 643 | views: videoObject.views, |
645 | likes: 0, | 644 | likes: 0, |
diff --git a/server/lib/job-queue/handlers/video-redundancy.ts b/server/lib/job-queue/handlers/video-redundancy.ts new file mode 100644 index 000000000..319d7090e --- /dev/null +++ b/server/lib/job-queue/handlers/video-redundancy.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import * as Bull from 'bull' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { VideosRedundancyScheduler } from '@server/lib/schedulers/videos-redundancy-scheduler' | ||
4 | |||
5 | export type VideoRedundancyPayload = { | ||
6 | videoId: number | ||
7 | } | ||
8 | |||
9 | async function processVideoRedundancy (job: Bull.Job) { | ||
10 | const payload = job.data as VideoRedundancyPayload | ||
11 | logger.info('Processing video redundancy in job %d.', job.id) | ||
12 | |||
13 | return VideosRedundancyScheduler.Instance.createManualRedundancy(payload.videoId) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | processVideoRedundancy | ||
20 | } | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index ec601e9ea..a1c623b25 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -13,6 +13,7 @@ import { processVideoImport, VideoImportPayload } from './handlers/video-import' | |||
13 | import { processVideosViews } from './handlers/video-views' | 13 | import { processVideosViews } from './handlers/video-views' |
14 | import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' | 14 | import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' |
15 | import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' | 15 | import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' |
16 | import { processVideoRedundancy, VideoRedundancyPayload } from '@server/lib/job-queue/handlers/video-redundancy' | ||
16 | 17 | ||
17 | type CreateJobArgument = | 18 | type CreateJobArgument = |
18 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 19 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -24,20 +25,21 @@ type CreateJobArgument = | |||
24 | { type: 'email', payload: EmailPayload } | | 25 | { type: 'email', payload: EmailPayload } | |
25 | { type: 'video-import', payload: VideoImportPayload } | | 26 | { type: 'video-import', payload: VideoImportPayload } | |
26 | { type: 'activitypub-refresher', payload: RefreshPayload } | | 27 | { type: 'activitypub-refresher', payload: RefreshPayload } | |
27 | { type: 'videos-views', payload: {} } | 28 | { type: 'videos-views', payload: {} } | |
29 | { type: 'video-redundancy', payload: VideoRedundancyPayload } | ||
28 | 30 | ||
29 | const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = { | 31 | const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { |
30 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, | 32 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, |
31 | 'activitypub-http-unicast': processActivityPubHttpUnicast, | 33 | 'activitypub-http-unicast': processActivityPubHttpUnicast, |
32 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, | 34 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, |
33 | 'activitypub-follow': processActivityPubFollow, | 35 | 'activitypub-follow': processActivityPubFollow, |
34 | 'video-file-import': processVideoFileImport, | 36 | 'video-file-import': processVideoFileImport, |
35 | 'video-transcoding': processVideoTranscoding, | 37 | 'video-transcoding': processVideoTranscoding, |
36 | 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3) | ||
37 | 'email': processEmail, | 38 | 'email': processEmail, |
38 | 'video-import': processVideoImport, | 39 | 'video-import': processVideoImport, |
39 | 'videos-views': processVideosViews, | 40 | 'videos-views': processVideosViews, |
40 | 'activitypub-refresher': refreshAPObject | 41 | 'activitypub-refresher': refreshAPObject, |
42 | 'video-redundancy': processVideoRedundancy | ||
41 | } | 43 | } |
42 | 44 | ||
43 | const jobTypes: JobType[] = [ | 45 | const jobTypes: JobType[] = [ |
@@ -50,7 +52,8 @@ const jobTypes: JobType[] = [ | |||
50 | 'video-file-import', | 52 | 'video-file-import', |
51 | 'video-import', | 53 | 'video-import', |
52 | 'videos-views', | 54 | 'videos-views', |
53 | 'activitypub-refresher' | 55 | 'activitypub-refresher', |
56 | 'video-redundancy' | ||
54 | ] | 57 | ] |
55 | 58 | ||
56 | class JobQueue { | 59 | class JobQueue { |
@@ -141,8 +144,7 @@ class JobQueue { | |||
141 | continue | 144 | continue |
142 | } | 145 | } |
143 | 146 | ||
144 | // FIXME: Bull queue typings does not have getJobs method | 147 | const jobs = await queue.getJobs([ state ], 0, start + count, asc) |
145 | const jobs = await (queue as any).getJobs(state, 0, start + count, asc) | ||
146 | results = results.concat(jobs) | 148 | results = results.concat(jobs) |
147 | } | 149 | } |
148 | 150 | ||
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts index 1b4ecd7c0..78d84e02e 100644 --- a/server/lib/redundancy.ts +++ b/server/lib/redundancy.ts | |||
@@ -13,10 +13,10 @@ async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t? | |||
13 | await videoRedundancy.destroy({ transaction: t }) | 13 | await videoRedundancy.destroy({ transaction: t }) |
14 | } | 14 | } |
15 | 15 | ||
16 | async function removeRedundancyOf (serverId: number) { | 16 | async function removeRedundanciesOfServer (serverId: number) { |
17 | const videosRedundancy = await VideoRedundancyModel.listLocalOfServer(serverId) | 17 | const redundancies = await VideoRedundancyModel.listLocalOfServer(serverId) |
18 | 18 | ||
19 | for (const redundancy of videosRedundancy) { | 19 | for (const redundancy of redundancies) { |
20 | await removeVideoRedundancy(redundancy) | 20 | await removeVideoRedundancy(redundancy) |
21 | } | 21 | } |
22 | } | 22 | } |
@@ -24,6 +24,6 @@ async function removeRedundancyOf (serverId: number) { | |||
24 | // --------------------------------------------------------------------------- | 24 | // --------------------------------------------------------------------------- |
25 | 25 | ||
26 | export { | 26 | export { |
27 | removeRedundancyOf, | 27 | removeRedundanciesOfServer, |
28 | removeVideoRedundancy | 28 | removeVideoRedundancy |
29 | } | 29 | } |
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 350a335d3..956780a77 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -4,7 +4,6 @@ import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-upda | |||
4 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
5 | import { federateVideoIfNeeded } from '../activitypub' | 5 | import { federateVideoIfNeeded } from '../activitypub' |
6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | ||
8 | import { Notifier } from '../notifier' | 7 | import { Notifier } from '../notifier' |
9 | import { sequelizeTypescript } from '../../initializers/database' | 8 | import { sequelizeTypescript } from '../../initializers/database' |
10 | import { MVideoFullLight } from '@server/typings/models' | 9 | import { MVideoFullLight } from '@server/typings/models' |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index c1c91b656..6e61cbe7d 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { AbstractScheduler } from './abstract-scheduler' |
2 | import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' | 2 | import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancyStrategy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
6 | import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' | 6 | import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
@@ -25,9 +25,10 @@ import { | |||
25 | MVideoWithAllFiles | 25 | MVideoWithAllFiles |
26 | } from '@server/typings/models' | 26 | } from '@server/typings/models' |
27 | import { getVideoFilename } from '../video-paths' | 27 | import { getVideoFilename } from '../video-paths' |
28 | import { VideoModel } from '@server/models/video/video' | ||
28 | 29 | ||
29 | type CandidateToDuplicate = { | 30 | type CandidateToDuplicate = { |
30 | redundancy: VideosRedundancy, | 31 | redundancy: VideosRedundancyStrategy, |
31 | video: MVideoWithAllFiles, | 32 | video: MVideoWithAllFiles, |
32 | files: MVideoFile[], | 33 | files: MVideoFile[], |
33 | streamingPlaylists: MStreamingPlaylistFiles[] | 34 | streamingPlaylists: MStreamingPlaylistFiles[] |
@@ -41,7 +42,7 @@ function isMVideoRedundancyFileVideo ( | |||
41 | 42 | ||
42 | export class VideosRedundancyScheduler extends AbstractScheduler { | 43 | export class VideosRedundancyScheduler extends AbstractScheduler { |
43 | 44 | ||
44 | private static instance: AbstractScheduler | 45 | private static instance: VideosRedundancyScheduler |
45 | 46 | ||
46 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL | 47 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL |
47 | 48 | ||
@@ -49,6 +50,22 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
49 | super() | 50 | super() |
50 | } | 51 | } |
51 | 52 | ||
53 | async createManualRedundancy (videoId: number) { | ||
54 | const videoToDuplicate = await VideoModel.loadWithFiles(videoId) | ||
55 | |||
56 | if (!videoToDuplicate) { | ||
57 | logger.warn('Video to manually duplicate %d does not exist anymore.', videoId) | ||
58 | return | ||
59 | } | ||
60 | |||
61 | return this.createVideoRedundancies({ | ||
62 | video: videoToDuplicate, | ||
63 | redundancy: null, | ||
64 | files: videoToDuplicate.VideoFiles, | ||
65 | streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists | ||
66 | }) | ||
67 | } | ||
68 | |||
52 | protected async internalExecute () { | 69 | protected async internalExecute () { |
53 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | 70 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
54 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) | 71 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) |
@@ -94,7 +111,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
94 | for (const redundancyModel of expired) { | 111 | for (const redundancyModel of expired) { |
95 | try { | 112 | try { |
96 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) | 113 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
97 | const candidate = { | 114 | const candidate: CandidateToDuplicate = { |
98 | redundancy: redundancyConfig, | 115 | redundancy: redundancyConfig, |
99 | video: null, | 116 | video: null, |
100 | files: [], | 117 | files: [], |
@@ -140,7 +157,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
140 | } | 157 | } |
141 | } | 158 | } |
142 | 159 | ||
143 | private findVideoToDuplicate (cache: VideosRedundancy) { | 160 | private findVideoToDuplicate (cache: VideosRedundancyStrategy) { |
144 | if (cache.strategy === 'most-views') { | 161 | if (cache.strategy === 'most-views') { |
145 | return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) | 162 | return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) |
146 | } | 163 | } |
@@ -187,13 +204,21 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
187 | } | 204 | } |
188 | } | 205 | } |
189 | 206 | ||
190 | private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) { | 207 | private async createVideoFileRedundancy (redundancy: VideosRedundancyStrategy | null, video: MVideoAccountLight, fileArg: MVideoFile) { |
208 | let strategy = 'manual' | ||
209 | let expiresOn: Date = null | ||
210 | |||
211 | if (redundancy) { | ||
212 | strategy = redundancy.strategy | ||
213 | expiresOn = this.buildNewExpiration(redundancy.minLifetime) | ||
214 | } | ||
215 | |||
191 | const file = fileArg as MVideoFileVideo | 216 | const file = fileArg as MVideoFileVideo |
192 | file.Video = video | 217 | file.Video = video |
193 | 218 | ||
194 | const serverActor = await getServerActor() | 219 | const serverActor = await getServerActor() |
195 | 220 | ||
196 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 221 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy) |
197 | 222 | ||
198 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 223 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
199 | const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) | 224 | const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) |
@@ -204,10 +229,10 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
204 | await move(tmpPath, destPath, { overwrite: true }) | 229 | await move(tmpPath, destPath, { overwrite: true }) |
205 | 230 | ||
206 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ | 231 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ |
207 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 232 | expiresOn, |
208 | url: getVideoCacheFileActivityPubUrl(file), | 233 | url: getVideoCacheFileActivityPubUrl(file), |
209 | fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), | 234 | fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), |
210 | strategy: redundancy.strategy, | 235 | strategy, |
211 | videoFileId: file.id, | 236 | videoFileId: file.id, |
212 | actorId: serverActor.id | 237 | actorId: serverActor.id |
213 | }) | 238 | }) |
@@ -220,25 +245,33 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
220 | } | 245 | } |
221 | 246 | ||
222 | private async createStreamingPlaylistRedundancy ( | 247 | private async createStreamingPlaylistRedundancy ( |
223 | redundancy: VideosRedundancy, | 248 | redundancy: VideosRedundancyStrategy, |
224 | video: MVideoAccountLight, | 249 | video: MVideoAccountLight, |
225 | playlistArg: MStreamingPlaylist | 250 | playlistArg: MStreamingPlaylist |
226 | ) { | 251 | ) { |
252 | let strategy = 'manual' | ||
253 | let expiresOn: Date = null | ||
254 | |||
255 | if (redundancy) { | ||
256 | strategy = redundancy.strategy | ||
257 | expiresOn = this.buildNewExpiration(redundancy.minLifetime) | ||
258 | } | ||
259 | |||
227 | const playlist = playlistArg as MStreamingPlaylistVideo | 260 | const playlist = playlistArg as MStreamingPlaylistVideo |
228 | playlist.Video = video | 261 | playlist.Video = video |
229 | 262 | ||
230 | const serverActor = await getServerActor() | 263 | const serverActor = await getServerActor() |
231 | 264 | ||
232 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) | 265 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy) |
233 | 266 | ||
234 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | 267 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) |
235 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | 268 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) |
236 | 269 | ||
237 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ | 270 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ |
238 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 271 | expiresOn, |
239 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), | 272 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), |
240 | fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), | 273 | fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), |
241 | strategy: redundancy.strategy, | 274 | strategy, |
242 | videoStreamingPlaylistId: playlist.id, | 275 | videoStreamingPlaylistId: playlist.id, |
243 | actorId: serverActor.id | 276 | actorId: serverActor.id |
244 | }) | 277 | }) |
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 8c27e8237..75238228f 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts | |||
@@ -1,17 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { SortType } from '../models/utils' | 2 | import { SortType } from '../models/utils' |
3 | 3 | ||
4 | function setDefaultSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 4 | const setDefaultSort = setDefaultSortFactory('-createdAt') |
5 | if (!req.query.sort) req.query.sort = '-createdAt' | ||
6 | |||
7 | return next() | ||
8 | } | ||
9 | 5 | ||
10 | function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 6 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') |
11 | if (!req.query.sort) req.query.sort = '-match' | ||
12 | 7 | ||
13 | return next() | 8 | const setDefaultSearchSort = setDefaultSortFactory('-match') |
14 | } | ||
15 | 9 | ||
16 | function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 10 | function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { |
17 | let newSort: SortType = { sortModel: undefined, sortValue: '' } | 11 | let newSort: SortType = { sortModel: undefined, sortValue: '' } |
@@ -39,5 +33,16 @@ function setBlacklistSort (req: express.Request, res: express.Response, next: ex | |||
39 | export { | 33 | export { |
40 | setDefaultSort, | 34 | setDefaultSort, |
41 | setDefaultSearchSort, | 35 | setDefaultSearchSort, |
36 | setDefaultVideoRedundanciesSort, | ||
42 | setBlacklistSort | 37 | setBlacklistSort |
43 | } | 38 | } |
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | function setDefaultSortFactory (sort: string) { | ||
43 | return (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
44 | if (!req.query.sort) req.query.sort = sort | ||
45 | |||
46 | return next() | ||
47 | } | ||
48 | } | ||
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index 8098e3a44..16b42fc0d 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { exists, isBooleanValid, isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 3 | import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 6 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
7 | import { isHostValid } from '../../helpers/custom-validators/servers' | 7 | import { isHostValid } from '../../helpers/custom-validators/servers' |
8 | import { ServerModel } from '../../models/server/server' | 8 | import { ServerModel } from '../../models/server/server' |
9 | import { doesVideoExist } from '../../helpers/middlewares' | 9 | import { doesVideoExist } from '../../helpers/middlewares' |
10 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' | ||
10 | 11 | ||
11 | const videoFileRedundancyGetValidator = [ | 12 | const videoFileRedundancyGetValidator = [ |
12 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 13 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
@@ -101,10 +102,77 @@ const updateServerRedundancyValidator = [ | |||
101 | } | 102 | } |
102 | ] | 103 | ] |
103 | 104 | ||
105 | const listVideoRedundanciesValidator = [ | ||
106 | query('target') | ||
107 | .custom(isVideoRedundancyTarget).withMessage('Should have a valid video redundancies target'), | ||
108 | |||
109 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
110 | logger.debug('Checking listVideoRedundanciesValidator parameters', { parameters: req.query }) | ||
111 | |||
112 | if (areValidationErrors(req, res)) return | ||
113 | |||
114 | return next() | ||
115 | } | ||
116 | ] | ||
117 | |||
118 | const addVideoRedundancyValidator = [ | ||
119 | body('videoId') | ||
120 | .custom(isIdValid) | ||
121 | .withMessage('Should have a valid video id'), | ||
122 | |||
123 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
124 | logger.debug('Checking addVideoRedundancyValidator parameters', { parameters: req.query }) | ||
125 | |||
126 | if (areValidationErrors(req, res)) return | ||
127 | |||
128 | if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return | ||
129 | |||
130 | if (res.locals.onlyVideo.remote === false) { | ||
131 | return res.status(400) | ||
132 | .json({ error: 'Cannot create a redundancy on a local video' }) | ||
133 | .end() | ||
134 | } | ||
135 | |||
136 | const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) | ||
137 | if (alreadyExists) { | ||
138 | return res.status(409) | ||
139 | .json({ error: 'This video is already duplicated by your instance.' }) | ||
140 | } | ||
141 | |||
142 | return next() | ||
143 | } | ||
144 | ] | ||
145 | |||
146 | const removeVideoRedundancyValidator = [ | ||
147 | param('redundancyId') | ||
148 | .custom(isIdValid) | ||
149 | .withMessage('Should have a valid redundancy id'), | ||
150 | |||
151 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
152 | logger.debug('Checking removeVideoRedundancyValidator parameters', { parameters: req.query }) | ||
153 | |||
154 | if (areValidationErrors(req, res)) return | ||
155 | |||
156 | const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) | ||
157 | if (!redundancy) { | ||
158 | return res.status(404) | ||
159 | .json({ error: 'Video redundancy not found' }) | ||
160 | .end() | ||
161 | } | ||
162 | |||
163 | res.locals.videoRedundancy = redundancy | ||
164 | |||
165 | return next() | ||
166 | } | ||
167 | ] | ||
168 | |||
104 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
105 | 170 | ||
106 | export { | 171 | export { |
107 | videoFileRedundancyGetValidator, | 172 | videoFileRedundancyGetValidator, |
108 | videoPlaylistRedundancyGetValidator, | 173 | videoPlaylistRedundancyGetValidator, |
109 | updateServerRedundancyValidator | 174 | updateServerRedundancyValidator, |
175 | listVideoRedundanciesValidator, | ||
176 | addVideoRedundancyValidator, | ||
177 | removeVideoRedundancyValidator | ||
110 | } | 178 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index c75e701d6..b76dab722 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -23,6 +23,7 @@ const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUM | |||
23 | const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) | 23 | const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) |
24 | const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) | 24 | const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) |
25 | const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | 25 | const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) |
26 | const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | ||
26 | 27 | ||
27 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 28 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
28 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 29 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -45,6 +46,7 @@ const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COL | |||
45 | const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) | 46 | const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) |
46 | const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) | 47 | const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) |
47 | const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS) | 48 | const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS) |
49 | const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COLUMNS) | ||
48 | 50 | ||
49 | // --------------------------------------------------------------------------- | 51 | // --------------------------------------------------------------------------- |
50 | 52 | ||
@@ -69,5 +71,6 @@ export { | |||
69 | serversBlocklistSortValidator, | 71 | serversBlocklistSortValidator, |
70 | userNotificationsSortValidator, | 72 | userNotificationsSortValidator, |
71 | videoPlaylistsSortValidator, | 73 | videoPlaylistsSortValidator, |
74 | videoRedundanciesSortValidator, | ||
72 | pluginsSortValidator | 75 | pluginsSortValidator |
73 | } | 76 | } |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 6733d9dec..00ee375cb 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -81,7 +81,7 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([ | |||
81 | duration = await getDurationFromVideoFile(videoFile.path) | 81 | duration = await getDurationFromVideoFile(videoFile.path) |
82 | } catch (err) { | 82 | } catch (err) { |
83 | logger.error('Invalid input file in videosAddValidator.', { err }) | 83 | logger.error('Invalid input file in videosAddValidator.', { err }) |
84 | res.status(400) | 84 | res.status(422) |
85 | .json({ error: 'Invalid input file.' }) | 85 | .json({ error: 'Invalid input file.' }) |
86 | 86 | ||
87 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index d50e6527f..5fe864f8b 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts | |||
@@ -18,15 +18,14 @@ const webfingerValidator = [ | |||
18 | const nameWithHost = getHostWithPort(req.query.resource.substr(5)) | 18 | const nameWithHost = getHostWithPort(req.query.resource.substr(5)) |
19 | const [ name ] = nameWithHost.split('@') | 19 | const [ name ] = nameWithHost.split('@') |
20 | 20 | ||
21 | // FIXME: we don't need the full actor | 21 | const actor = await ActorModel.loadLocalUrlByName(name) |
22 | const actor = await ActorModel.loadLocalByName(name) | ||
23 | if (!actor) { | 22 | if (!actor) { |
24 | return res.status(404) | 23 | return res.status(404) |
25 | .send({ error: 'Actor not found' }) | 24 | .send({ error: 'Actor not found' }) |
26 | .end() | 25 | .end() |
27 | } | 26 | } |
28 | 27 | ||
29 | res.locals.actorFull = actor | 28 | res.locals.actorUrl = actor |
30 | return next() | 29 | return next() |
31 | } | 30 | } |
32 | ] | 31 | ] |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 6ebe32556..e2f66d733 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -80,7 +80,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | |||
80 | attributes: [ 'accountId', 'id' ], | 80 | attributes: [ 'accountId', 'id' ], |
81 | where: { | 81 | where: { |
82 | accountId: { | 82 | accountId: { |
83 | [Op.in]: accountIds // FIXME: sequelize ANY seems broken | 83 | [Op.in]: accountIds |
84 | }, | 84 | }, |
85 | targetAccountId | 85 | targetAccountId |
86 | }, | 86 | }, |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index a05f30175..f649b8f95 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -363,7 +363,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
363 | where: { | 363 | where: { |
364 | userId, | 364 | userId, |
365 | id: { | 365 | id: { |
366 | [Op.in]: notificationIds // FIXME: sequelize ANY seems broken | 366 | [Op.in]: notificationIds |
367 | } | 367 | } |
368 | } | 368 | } |
369 | } | 369 | } |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 007647ced..d651a281a 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -43,7 +43,7 @@ import { | |||
43 | MActorFull, | 43 | MActorFull, |
44 | MActorHost, | 44 | MActorHost, |
45 | MActorServer, | 45 | MActorServer, |
46 | MActorSummaryFormattable, | 46 | MActorSummaryFormattable, MActorUrl, |
47 | MActorWithInboxes | 47 | MActorWithInboxes |
48 | } from '../../typings/models' | 48 | } from '../../typings/models' |
49 | import * as Bluebird from 'bluebird' | 49 | import * as Bluebird from 'bluebird' |
@@ -276,7 +276,8 @@ export class ActorModel extends Model<ActorModel> { | |||
276 | }) | 276 | }) |
277 | VideoChannel: VideoChannelModel | 277 | VideoChannel: VideoChannelModel |
278 | 278 | ||
279 | private static cache: { [ id: string ]: any } = {} | 279 | private static localNameCache: { [ id: string ]: any } = {} |
280 | private static localUrlCache: { [ id: string ]: any } = {} | ||
280 | 281 | ||
281 | static load (id: number): Bluebird<MActor> { | 282 | static load (id: number): Bluebird<MActor> { |
282 | return ActorModel.unscoped().findByPk(id) | 283 | return ActorModel.unscoped().findByPk(id) |
@@ -345,8 +346,8 @@ export class ActorModel extends Model<ActorModel> { | |||
345 | 346 | ||
346 | static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { | 347 | static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { |
347 | // The server actor never change, so we can easily cache it | 348 | // The server actor never change, so we can easily cache it |
348 | if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.cache[preferredUsername]) { | 349 | if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localNameCache[preferredUsername]) { |
349 | return Bluebird.resolve(ActorModel.cache[preferredUsername]) | 350 | return Bluebird.resolve(ActorModel.localNameCache[preferredUsername]) |
350 | } | 351 | } |
351 | 352 | ||
352 | const query = { | 353 | const query = { |
@@ -361,7 +362,33 @@ export class ActorModel extends Model<ActorModel> { | |||
361 | .findOne(query) | 362 | .findOne(query) |
362 | .then(actor => { | 363 | .then(actor => { |
363 | if (preferredUsername === SERVER_ACTOR_NAME) { | 364 | if (preferredUsername === SERVER_ACTOR_NAME) { |
364 | ActorModel.cache[ preferredUsername ] = actor | 365 | ActorModel.localNameCache[ preferredUsername ] = actor |
366 | } | ||
367 | |||
368 | return actor | ||
369 | }) | ||
370 | } | ||
371 | |||
372 | static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> { | ||
373 | // The server actor never change, so we can easily cache it | ||
374 | if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localUrlCache[preferredUsername]) { | ||
375 | return Bluebird.resolve(ActorModel.localUrlCache[preferredUsername]) | ||
376 | } | ||
377 | |||
378 | const query = { | ||
379 | attributes: [ 'url' ], | ||
380 | where: { | ||
381 | preferredUsername, | ||
382 | serverId: null | ||
383 | }, | ||
384 | transaction | ||
385 | } | ||
386 | |||
387 | return ActorModel.unscoped() | ||
388 | .findOne(query) | ||
389 | .then(actor => { | ||
390 | if (preferredUsername === SERVER_ACTOR_NAME) { | ||
391 | ActorModel.localUrlCache[ preferredUsername ] = actor | ||
365 | } | 392 | } |
366 | 393 | ||
367 | return actor | 394 | return actor |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8c9a7eabf..4e66d72e3 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -13,13 +13,13 @@ import { | |||
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 16 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 18 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
22 | import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' | 22 | import { VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../shared/models/redundancy' |
23 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { CacheFileObject, VideoPrivacy } from '../../../shared' | 24 | import { CacheFileObject, VideoPrivacy } from '../../../shared' |
25 | import { VideoChannelModel } from '../video/video-channel' | 25 | import { VideoChannelModel } from '../video/video-channel' |
@@ -27,10 +27,16 @@ import { ServerModel } from '../server/server' | |||
27 | import { sample } from 'lodash' | 27 | import { sample } from 'lodash' |
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' | 30 | import { col, FindOptions, fn, literal, Op, Transaction, WhereOptions } from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | 31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' |
32 | import { CONFIG } from '../../initializers/config' | 32 | import { CONFIG } from '../../initializers/config' |
33 | import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' | 33 | import { MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' |
34 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' | ||
35 | import { | ||
36 | FileRedundancyInformation, | ||
37 | StreamingPlaylistRedundancyInformation, | ||
38 | VideoRedundancy | ||
39 | } from '@shared/models/redundancy/video-redundancy.model' | ||
34 | 40 | ||
35 | export enum ScopeNames { | 41 | export enum ScopeNames { |
36 | WITH_VIDEO = 'WITH_VIDEO' | 42 | WITH_VIDEO = 'WITH_VIDEO' |
@@ -86,7 +92,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
86 | @UpdatedAt | 92 | @UpdatedAt |
87 | updatedAt: Date | 93 | updatedAt: Date |
88 | 94 | ||
89 | @AllowNull(false) | 95 | @AllowNull(true) |
90 | @Column | 96 | @Column |
91 | expiresOn: Date | 97 | expiresOn: Date |
92 | 98 | ||
@@ -193,6 +199,15 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
193 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 199 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
194 | } | 200 | } |
195 | 201 | ||
202 | static loadByIdWithVideo (id: number, transaction?: Transaction): Bluebird<MVideoRedundancyVideo> { | ||
203 | const query = { | ||
204 | where: { id }, | ||
205 | transaction | ||
206 | } | ||
207 | |||
208 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
209 | } | ||
210 | |||
196 | static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> { | 211 | static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> { |
197 | const query = { | 212 | const query = { |
198 | where: { | 213 | where: { |
@@ -394,7 +409,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
394 | [Op.ne]: actor.id | 409 | [Op.ne]: actor.id |
395 | }, | 410 | }, |
396 | expiresOn: { | 411 | expiresOn: { |
397 | [ Op.lt ]: new Date() | 412 | [ Op.lt ]: new Date(), |
413 | [ Op.ne ]: null | ||
398 | } | 414 | } |
399 | } | 415 | } |
400 | } | 416 | } |
@@ -447,7 +463,112 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
447 | return VideoRedundancyModel.findAll(query) | 463 | return VideoRedundancyModel.findAll(query) |
448 | } | 464 | } |
449 | 465 | ||
450 | static async getStats (strategy: VideoRedundancyStrategy) { | 466 | static listForApi (options: { |
467 | start: number, | ||
468 | count: number, | ||
469 | sort: string, | ||
470 | target: VideoRedundanciesTarget, | ||
471 | strategy?: string | ||
472 | }) { | ||
473 | const { start, count, sort, target, strategy } = options | ||
474 | let redundancyWhere: WhereOptions = {} | ||
475 | let videosWhere: WhereOptions = {} | ||
476 | let redundancySqlSuffix = '' | ||
477 | |||
478 | if (target === 'my-videos') { | ||
479 | Object.assign(videosWhere, { remote: false }) | ||
480 | } else if (target === 'remote-videos') { | ||
481 | Object.assign(videosWhere, { remote: true }) | ||
482 | Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } }) | ||
483 | redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL' | ||
484 | } | ||
485 | |||
486 | if (strategy) { | ||
487 | Object.assign(redundancyWhere, { strategy: strategy }) | ||
488 | } | ||
489 | |||
490 | const videoFilterWhere = { | ||
491 | [Op.and]: [ | ||
492 | { | ||
493 | [ Op.or ]: [ | ||
494 | { | ||
495 | id: { | ||
496 | [ Op.in ]: literal( | ||
497 | '(' + | ||
498 | 'SELECT "videoId" FROM "videoFile" ' + | ||
499 | 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' + | ||
500 | redundancySqlSuffix + | ||
501 | ')' | ||
502 | ) | ||
503 | } | ||
504 | }, | ||
505 | { | ||
506 | id: { | ||
507 | [ Op.in ]: literal( | ||
508 | '(' + | ||
509 | 'select "videoId" FROM "videoStreamingPlaylist" ' + | ||
510 | 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' + | ||
511 | redundancySqlSuffix + | ||
512 | ')' | ||
513 | ) | ||
514 | } | ||
515 | } | ||
516 | ] | ||
517 | }, | ||
518 | |||
519 | videosWhere | ||
520 | ] | ||
521 | } | ||
522 | |||
523 | // /!\ On video model /!\ | ||
524 | const findOptions = { | ||
525 | offset: start, | ||
526 | limit: count, | ||
527 | order: getSort(sort), | ||
528 | include: [ | ||
529 | { | ||
530 | required: false, | ||
531 | model: VideoFileModel.unscoped(), | ||
532 | include: [ | ||
533 | { | ||
534 | model: VideoRedundancyModel.unscoped(), | ||
535 | required: false, | ||
536 | where: redundancyWhere | ||
537 | } | ||
538 | ] | ||
539 | }, | ||
540 | { | ||
541 | required: false, | ||
542 | model: VideoStreamingPlaylistModel.unscoped(), | ||
543 | include: [ | ||
544 | { | ||
545 | model: VideoRedundancyModel.unscoped(), | ||
546 | required: false, | ||
547 | where: redundancyWhere | ||
548 | }, | ||
549 | { | ||
550 | model: VideoFileModel.unscoped(), | ||
551 | required: false | ||
552 | } | ||
553 | ] | ||
554 | } | ||
555 | ], | ||
556 | where: videoFilterWhere | ||
557 | } | ||
558 | |||
559 | // /!\ On video model /!\ | ||
560 | const countOptions = { | ||
561 | where: videoFilterWhere | ||
562 | } | ||
563 | |||
564 | return Promise.all([ | ||
565 | VideoModel.findAll(findOptions), | ||
566 | |||
567 | VideoModel.count(countOptions) | ||
568 | ]).then(([ data, total ]) => ({ total, data })) | ||
569 | } | ||
570 | |||
571 | static async getStats (strategy: VideoRedundancyStrategyWithManual) { | ||
451 | const actor = await getServerActor() | 572 | const actor = await getServerActor() |
452 | 573 | ||
453 | const query: FindOptions = { | 574 | const query: FindOptions = { |
@@ -478,6 +599,53 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
478 | })) | 599 | })) |
479 | } | 600 | } |
480 | 601 | ||
602 | static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy { | ||
603 | let filesRedundancies: FileRedundancyInformation[] = [] | ||
604 | let streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = [] | ||
605 | |||
606 | for (const file of video.VideoFiles) { | ||
607 | for (const redundancy of file.RedundancyVideos) { | ||
608 | filesRedundancies.push({ | ||
609 | id: redundancy.id, | ||
610 | fileUrl: redundancy.fileUrl, | ||
611 | strategy: redundancy.strategy, | ||
612 | createdAt: redundancy.createdAt, | ||
613 | updatedAt: redundancy.updatedAt, | ||
614 | expiresOn: redundancy.expiresOn, | ||
615 | size: file.size | ||
616 | }) | ||
617 | } | ||
618 | } | ||
619 | |||
620 | for (const playlist of video.VideoStreamingPlaylists) { | ||
621 | const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0) | ||
622 | |||
623 | for (const redundancy of playlist.RedundancyVideos) { | ||
624 | streamingPlaylistsRedundancies.push({ | ||
625 | id: redundancy.id, | ||
626 | fileUrl: redundancy.fileUrl, | ||
627 | strategy: redundancy.strategy, | ||
628 | createdAt: redundancy.createdAt, | ||
629 | updatedAt: redundancy.updatedAt, | ||
630 | expiresOn: redundancy.expiresOn, | ||
631 | size | ||
632 | }) | ||
633 | } | ||
634 | } | ||
635 | |||
636 | return { | ||
637 | id: video.id, | ||
638 | name: video.name, | ||
639 | url: video.url, | ||
640 | uuid: video.uuid, | ||
641 | |||
642 | redundancies: { | ||
643 | files: filesRedundancies, | ||
644 | streamingPlaylists: streamingPlaylistsRedundancies | ||
645 | } | ||
646 | } | ||
647 | } | ||
648 | |||
481 | getVideo () { | 649 | getVideo () { |
482 | if (this.VideoFile) return this.VideoFile.Video | 650 | if (this.VideoFile) return this.VideoFile.Video |
483 | 651 | ||
@@ -494,7 +662,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
494 | id: this.url, | 662 | id: this.url, |
495 | type: 'CacheFile' as 'CacheFile', | 663 | type: 'CacheFile' as 'CacheFile', |
496 | object: this.VideoStreamingPlaylist.Video.url, | 664 | object: this.VideoStreamingPlaylist.Video.url, |
497 | expires: this.expiresOn.toISOString(), | 665 | expires: this.expiresOn ? this.expiresOn.toISOString() : null, |
498 | url: { | 666 | url: { |
499 | type: 'Link', | 667 | type: 'Link', |
500 | mediaType: 'application/x-mpegURL', | 668 | mediaType: 'application/x-mpegURL', |
@@ -507,7 +675,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
507 | id: this.url, | 675 | id: this.url, |
508 | type: 'CacheFile' as 'CacheFile', | 676 | type: 'CacheFile' as 'CacheFile', |
509 | object: this.VideoFile.Video.url, | 677 | object: this.VideoFile.Video.url, |
510 | expires: this.expiresOn.toISOString(), | 678 | expires: this.expiresOn ? this.expiresOn.toISOString() : null, |
511 | url: { | 679 | url: { |
512 | type: 'Link', | 680 | type: 'Link', |
513 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 681 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index b88df4fd5..883ae47ab 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -81,7 +81,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> { | |||
81 | attributes: [ 'accountId', 'id' ], | 81 | attributes: [ 'accountId', 'id' ], |
82 | where: { | 82 | where: { |
83 | accountId: { | 83 | accountId: { |
84 | [Op.in]: accountIds // FIXME: sequelize ANY seems broken | 84 | [Op.in]: accountIds |
85 | }, | 85 | }, |
86 | targetServerId | 86 | targetServerId |
87 | }, | 87 | }, |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index eeb2a4afd..6335d44e4 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -120,7 +120,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> { | |||
120 | language | 120 | language |
121 | } | 121 | } |
122 | 122 | ||
123 | return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings | 123 | return VideoCaptionModel.upsert(values, { transaction, returning: true }) |
124 | .then(([ caption ]) => caption) | 124 | .then(([ caption ]) => caption) |
125 | } | 125 | } |
126 | 126 | ||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index e10adcb3a..4e98e7ba3 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -43,18 +43,6 @@ import { | |||
43 | MChannelSummaryFormattable | 43 | MChannelSummaryFormattable |
44 | } from '../../typings/models/video' | 44 | } from '../../typings/models/video' |
45 | 45 | ||
46 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | ||
47 | const indexes: ModelIndexesOptions[] = [ | ||
48 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
49 | |||
50 | { | ||
51 | fields: [ 'accountId' ] | ||
52 | }, | ||
53 | { | ||
54 | fields: [ 'actorId' ] | ||
55 | } | ||
56 | ] | ||
57 | |||
58 | export enum ScopeNames { | 46 | export enum ScopeNames { |
59 | FOR_API = 'FOR_API', | 47 | FOR_API = 'FOR_API', |
60 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 48 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -176,7 +164,16 @@ export type SummaryOptions = { | |||
176 | })) | 164 | })) |
177 | @Table({ | 165 | @Table({ |
178 | tableName: 'videoChannel', | 166 | tableName: 'videoChannel', |
179 | indexes | 167 | indexes: [ |
168 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
169 | |||
170 | { | ||
171 | fields: [ 'accountId' ] | ||
172 | }, | ||
173 | { | ||
174 | fields: [ 'actorId' ] | ||
175 | } | ||
176 | ] | ||
180 | }) | 177 | }) |
181 | export class VideoChannelModel extends Model<VideoChannelModel> { | 178 | export class VideoChannelModel extends Model<VideoChannelModel> { |
182 | 179 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index bcdda36e5..ba1fc23ea 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -369,7 +369,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
369 | model: VideoPlaylistElementModel.unscoped(), | 369 | model: VideoPlaylistElementModel.unscoped(), |
370 | where: { | 370 | where: { |
371 | videoId: { | 371 | videoId: { |
372 | [Op.in]: videoIds // FIXME: sequelize ANY seems broken | 372 | [Op.in]: videoIds |
373 | } | 373 | } |
374 | }, | 374 | }, |
375 | required: true | 375 | required: true |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index eacffe186..20e1f1c4a 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,18 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy } from 'lodash' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { | 4 | import { CountOptions, FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
5 | CountOptions, | ||
6 | FindOptions, | ||
7 | IncludeOptions, | ||
8 | ModelIndexesOptions, | ||
9 | Op, | ||
10 | QueryTypes, | ||
11 | ScopeOptions, | ||
12 | Sequelize, | ||
13 | Transaction, | ||
14 | WhereOptions | ||
15 | } from 'sequelize' | ||
16 | import { | 5 | import { |
17 | AllowNull, | 6 | AllowNull, |
18 | BeforeDestroy, | 7 | BeforeDestroy, |
@@ -136,8 +125,7 @@ import { | |||
136 | MVideoThumbnailBlacklist, | 125 | MVideoThumbnailBlacklist, |
137 | MVideoWithAllFiles, | 126 | MVideoWithAllFiles, |
138 | MVideoWithFile, | 127 | MVideoWithFile, |
139 | MVideoWithRights, | 128 | MVideoWithRights |
140 | MStreamingPlaylistFiles | ||
141 | } from '../../typings/models' | 129 | } from '../../typings/models' |
142 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' | 130 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' |
143 | import { MThumbnail } from '../../typings/models/video/thumbnail' | 131 | import { MThumbnail } from '../../typings/models/video/thumbnail' |
@@ -145,74 +133,6 @@ import { VideoFile } from '@shared/models/videos/video-file.model' | |||
145 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 133 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
146 | import validator from 'validator' | 134 | import validator from 'validator' |
147 | 135 | ||
148 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | ||
149 | const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ | ||
150 | buildTrigramSearchIndex('video_name_trigram', 'name'), | ||
151 | |||
152 | { fields: [ 'createdAt' ] }, | ||
153 | { | ||
154 | fields: [ | ||
155 | { name: 'publishedAt', order: 'DESC' }, | ||
156 | { name: 'id', order: 'ASC' } | ||
157 | ] | ||
158 | }, | ||
159 | { fields: [ 'duration' ] }, | ||
160 | { fields: [ 'views' ] }, | ||
161 | { fields: [ 'channelId' ] }, | ||
162 | { | ||
163 | fields: [ 'originallyPublishedAt' ], | ||
164 | where: { | ||
165 | originallyPublishedAt: { | ||
166 | [Op.ne]: null | ||
167 | } | ||
168 | } | ||
169 | }, | ||
170 | { | ||
171 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
172 | where: { | ||
173 | category: { | ||
174 | [Op.ne]: null | ||
175 | } | ||
176 | } | ||
177 | }, | ||
178 | { | ||
179 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
180 | where: { | ||
181 | licence: { | ||
182 | [Op.ne]: null | ||
183 | } | ||
184 | } | ||
185 | }, | ||
186 | { | ||
187 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
188 | where: { | ||
189 | language: { | ||
190 | [Op.ne]: null | ||
191 | } | ||
192 | } | ||
193 | }, | ||
194 | { | ||
195 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
196 | where: { | ||
197 | nsfw: true | ||
198 | } | ||
199 | }, | ||
200 | { | ||
201 | fields: [ 'remote' ], // Only index local videos | ||
202 | where: { | ||
203 | remote: false | ||
204 | } | ||
205 | }, | ||
206 | { | ||
207 | fields: [ 'uuid' ], | ||
208 | unique: true | ||
209 | }, | ||
210 | { | ||
211 | fields: [ 'url' ], | ||
212 | unique: true | ||
213 | } | ||
214 | ] | ||
215 | |||
216 | export enum ScopeNames { | 136 | export enum ScopeNames { |
217 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 137 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
218 | FOR_API = 'FOR_API', | 138 | FOR_API = 'FOR_API', |
@@ -292,7 +212,7 @@ export type AvailableForListIDsOptions = { | |||
292 | if (options.ids) { | 212 | if (options.ids) { |
293 | query.where = { | 213 | query.where = { |
294 | id: { | 214 | id: { |
295 | [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken | 215 | [ Op.in ]: options.ids |
296 | } | 216 | } |
297 | } | 217 | } |
298 | } | 218 | } |
@@ -760,7 +680,72 @@ export type AvailableForListIDsOptions = { | |||
760 | })) | 680 | })) |
761 | @Table({ | 681 | @Table({ |
762 | tableName: 'video', | 682 | tableName: 'video', |
763 | indexes | 683 | indexes: [ |
684 | buildTrigramSearchIndex('video_name_trigram', 'name'), | ||
685 | |||
686 | { fields: [ 'createdAt' ] }, | ||
687 | { | ||
688 | fields: [ | ||
689 | { name: 'publishedAt', order: 'DESC' }, | ||
690 | { name: 'id', order: 'ASC' } | ||
691 | ] | ||
692 | }, | ||
693 | { fields: [ 'duration' ] }, | ||
694 | { fields: [ 'views' ] }, | ||
695 | { fields: [ 'channelId' ] }, | ||
696 | { | ||
697 | fields: [ 'originallyPublishedAt' ], | ||
698 | where: { | ||
699 | originallyPublishedAt: { | ||
700 | [Op.ne]: null | ||
701 | } | ||
702 | } | ||
703 | }, | ||
704 | { | ||
705 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
706 | where: { | ||
707 | category: { | ||
708 | [Op.ne]: null | ||
709 | } | ||
710 | } | ||
711 | }, | ||
712 | { | ||
713 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
714 | where: { | ||
715 | licence: { | ||
716 | [Op.ne]: null | ||
717 | } | ||
718 | } | ||
719 | }, | ||
720 | { | ||
721 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
722 | where: { | ||
723 | language: { | ||
724 | [Op.ne]: null | ||
725 | } | ||
726 | } | ||
727 | }, | ||
728 | { | ||
729 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
730 | where: { | ||
731 | nsfw: true | ||
732 | } | ||
733 | }, | ||
734 | { | ||
735 | fields: [ 'remote' ], // Only index local videos | ||
736 | where: { | ||
737 | remote: false | ||
738 | } | ||
739 | }, | ||
740 | { | ||
741 | fields: [ 'uuid' ], | ||
742 | unique: true | ||
743 | }, | ||
744 | { | ||
745 | fields: [ 'url' ], | ||
746 | unique: true | ||
747 | } | ||
748 | ] | ||
764 | }) | 749 | }) |
765 | export class VideoModel extends Model<VideoModel> { | 750 | export class VideoModel extends Model<VideoModel> { |
766 | 751 | ||
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts index 6471da840..7012a39ee 100644 --- a/server/tests/api/check-params/redundancy.ts +++ b/server/tests/api/check-params/redundancy.ts | |||
@@ -3,21 +3,25 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | ||
5 | import { | 5 | import { |
6 | checkBadCountPagination, | ||
7 | checkBadSortPagination, | ||
8 | checkBadStartPagination, | ||
6 | cleanupTests, | 9 | cleanupTests, |
7 | createUser, | 10 | createUser, |
8 | doubleFollow, | 11 | doubleFollow, |
9 | flushAndRunMultipleServers, | 12 | flushAndRunMultipleServers, makeDeleteRequest, |
10 | flushTests, | 13 | makeGetRequest, makePostBodyRequest, |
11 | killallServers, | ||
12 | makePutBodyRequest, | 14 | makePutBodyRequest, |
13 | ServerInfo, | 15 | ServerInfo, |
14 | setAccessTokensToServers, | 16 | setAccessTokensToServers, uploadVideoAndGetId, |
15 | userLogin | 17 | userLogin, waitJobs |
16 | } from '../../../../shared/extra-utils' | 18 | } from '../../../../shared/extra-utils' |
17 | 19 | ||
18 | describe('Test server redundancy API validators', function () { | 20 | describe('Test server redundancy API validators', function () { |
19 | let servers: ServerInfo[] | 21 | let servers: ServerInfo[] |
20 | let userAccessToken = null | 22 | let userAccessToken = null |
23 | let videoIdLocal: number | ||
24 | let videoIdRemote: number | ||
21 | 25 | ||
22 | // --------------------------------------------------------------- | 26 | // --------------------------------------------------------------- |
23 | 27 | ||
@@ -36,9 +40,134 @@ describe('Test server redundancy API validators', function () { | |||
36 | 40 | ||
37 | await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) | 41 | await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) |
38 | userAccessToken = await userLogin(servers[0], user) | 42 | userAccessToken = await userLogin(servers[0], user) |
43 | |||
44 | videoIdLocal = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video' })).id | ||
45 | videoIdRemote = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video' })).id | ||
46 | |||
47 | await waitJobs(servers) | ||
48 | }) | ||
49 | |||
50 | describe('When listing redundancies', function () { | ||
51 | const path = '/api/v1/server/redundancy/videos' | ||
52 | |||
53 | let url: string | ||
54 | let token: string | ||
55 | |||
56 | before(function () { | ||
57 | url = servers[0].url | ||
58 | token = servers[0].accessToken | ||
59 | }) | ||
60 | |||
61 | it('Should fail with an invalid token', async function () { | ||
62 | await makeGetRequest({ url, path, token: 'fake_token', statusCodeExpected: 401 }) | ||
63 | }) | ||
64 | |||
65 | it('Should fail if the user is not an administrator', async function () { | ||
66 | await makeGetRequest({ url, path, token: userAccessToken, statusCodeExpected: 403 }) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with a bad start pagination', async function () { | ||
70 | await checkBadStartPagination(url, path, servers[0].accessToken) | ||
71 | }) | ||
72 | |||
73 | it('Should fail with a bad count pagination', async function () { | ||
74 | await checkBadCountPagination(url, path, servers[0].accessToken) | ||
75 | }) | ||
76 | |||
77 | it('Should fail with an incorrect sort', async function () { | ||
78 | await checkBadSortPagination(url, path, servers[0].accessToken) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with a bad target', async function () { | ||
82 | await makeGetRequest({ url, path, token, query: { target: 'bad target' } }) | ||
83 | }) | ||
84 | |||
85 | it('Should fail without target', async function () { | ||
86 | await makeGetRequest({ url, path, token }) | ||
87 | }) | ||
88 | |||
89 | it('Should succeed with the correct params', async function () { | ||
90 | await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, statusCodeExpected: 200 }) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('When manually adding a redundancy', function () { | ||
95 | const path = '/api/v1/server/redundancy/videos' | ||
96 | |||
97 | let url: string | ||
98 | let token: string | ||
99 | |||
100 | before(function () { | ||
101 | url = servers[0].url | ||
102 | token = servers[0].accessToken | ||
103 | }) | ||
104 | |||
105 | it('Should fail with an invalid token', async function () { | ||
106 | await makePostBodyRequest({ url, path, token: 'fake_token', statusCodeExpected: 401 }) | ||
107 | }) | ||
108 | |||
109 | it('Should fail if the user is not an administrator', async function () { | ||
110 | await makePostBodyRequest({ url, path, token: userAccessToken, statusCodeExpected: 403 }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail without a video id', async function () { | ||
114 | await makePostBodyRequest({ url, path, token }) | ||
115 | }) | ||
116 | |||
117 | it('Should fail with an incorrect video id', async function () { | ||
118 | await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } }) | ||
119 | }) | ||
120 | |||
121 | it('Should fail with a not found video id', async function () { | ||
122 | await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, statusCodeExpected: 404 }) | ||
123 | }) | ||
124 | |||
125 | it('Should fail with a local a video id', async function () { | ||
126 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } }) | ||
127 | }) | ||
128 | |||
129 | it('Should succeed with the correct params', async function () { | ||
130 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: 204 }) | ||
131 | }) | ||
132 | |||
133 | it('Should fail if the video is already duplicated', async function () { | ||
134 | this.timeout(30000) | ||
135 | |||
136 | await waitJobs(servers) | ||
137 | |||
138 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: 409 }) | ||
139 | }) | ||
140 | }) | ||
141 | |||
142 | describe('When manually removing a redundancy', function () { | ||
143 | const path = '/api/v1/server/redundancy/videos/' | ||
144 | |||
145 | let url: string | ||
146 | let token: string | ||
147 | |||
148 | before(function () { | ||
149 | url = servers[0].url | ||
150 | token = servers[0].accessToken | ||
151 | }) | ||
152 | |||
153 | it('Should fail with an invalid token', async function () { | ||
154 | await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', statusCodeExpected: 401 }) | ||
155 | }) | ||
156 | |||
157 | it('Should fail if the user is not an administrator', async function () { | ||
158 | await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, statusCodeExpected: 403 }) | ||
159 | }) | ||
160 | |||
161 | it('Should fail with an incorrect video id', async function () { | ||
162 | await makeDeleteRequest({ url, path: path + 'toto', token }) | ||
163 | }) | ||
164 | |||
165 | it('Should fail with a not found video redundancy', async function () { | ||
166 | await makeDeleteRequest({ url, path: path + '454545', token, statusCodeExpected: 404 }) | ||
167 | }) | ||
39 | }) | 168 | }) |
40 | 169 | ||
41 | describe('When updating redundancy', function () { | 170 | describe('When updating server redundancy', function () { |
42 | const path = '/api/v1/server/redundancy' | 171 | const path = '/api/v1/server/redundancy' |
43 | 172 | ||
44 | it('Should fail with an invalid token', async function () { | 173 | it('Should fail with an invalid token', async function () { |
diff --git a/server/tests/api/redundancy/index.ts b/server/tests/api/redundancy/index.ts index 8e69b95a6..5359055b0 100644 --- a/server/tests/api/redundancy/index.ts +++ b/server/tests/api/redundancy/index.ts | |||
@@ -1 +1,2 @@ | |||
1 | import './redundancy' | 1 | import './redundancy' |
2 | import './manage-redundancy' | ||
diff --git a/server/tests/api/redundancy/manage-redundancy.ts b/server/tests/api/redundancy/manage-redundancy.ts new file mode 100644 index 000000000..6a8937f24 --- /dev/null +++ b/server/tests/api/redundancy/manage-redundancy.ts | |||
@@ -0,0 +1,373 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | getLocalIdByUUID, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | uploadVideo, | ||
13 | uploadVideoAndGetId, | ||
14 | waitUntilLog | ||
15 | } from '../../../../shared/extra-utils' | ||
16 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
17 | import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy, updateRedundancy } from '@shared/extra-utils/server/redundancy' | ||
18 | import { VideoPrivacy, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
19 | |||
20 | const expect = chai.expect | ||
21 | |||
22 | describe('Test manage videos redundancy', function () { | ||
23 | const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ] | ||
24 | |||
25 | let servers: ServerInfo[] | ||
26 | let video1Server2UUID: string | ||
27 | let video2Server2UUID: string | ||
28 | let redundanciesToRemove: number[] = [] | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | const config = { | ||
34 | transcoding: { | ||
35 | hls: { | ||
36 | enabled: true | ||
37 | } | ||
38 | }, | ||
39 | redundancy: { | ||
40 | videos: { | ||
41 | check_interval: '1 second', | ||
42 | strategies: [ | ||
43 | { | ||
44 | strategy: 'recently-added', | ||
45 | min_lifetime: '1 hour', | ||
46 | size: '10MB', | ||
47 | min_views: 0 | ||
48 | } | ||
49 | ] | ||
50 | } | ||
51 | } | ||
52 | } | ||
53 | servers = await flushAndRunMultipleServers(3, config) | ||
54 | |||
55 | // Get the access tokens | ||
56 | await setAccessTokensToServers(servers) | ||
57 | |||
58 | { | ||
59 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) | ||
60 | video1Server2UUID = res.body.video.uuid | ||
61 | } | ||
62 | |||
63 | { | ||
64 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | ||
65 | video2Server2UUID = res.body.video.uuid | ||
66 | } | ||
67 | |||
68 | await waitJobs(servers) | ||
69 | |||
70 | // Server 1 and server 2 follow each other | ||
71 | await doubleFollow(servers[ 0 ], servers[ 1 ]) | ||
72 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | ||
73 | |||
74 | await waitJobs(servers) | ||
75 | }) | ||
76 | |||
77 | it('Should not have redundancies on server 3', async function () { | ||
78 | for (const target of targets) { | ||
79 | const res = await listVideoRedundancies({ | ||
80 | url: servers[2].url, | ||
81 | accessToken: servers[2].accessToken, | ||
82 | target | ||
83 | }) | ||
84 | |||
85 | expect(res.body.total).to.equal(0) | ||
86 | expect(res.body.data).to.have.lengthOf(0) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should not have "remote-videos" redundancies on server 2', async function () { | ||
91 | this.timeout(120000) | ||
92 | |||
93 | await waitJobs(servers) | ||
94 | await waitUntilLog(servers[0], 'Duplicated ', 10) | ||
95 | await waitJobs(servers) | ||
96 | |||
97 | const res = await listVideoRedundancies({ | ||
98 | url: servers[1].url, | ||
99 | accessToken: servers[1].accessToken, | ||
100 | target: 'remote-videos' | ||
101 | }) | ||
102 | |||
103 | expect(res.body.total).to.equal(0) | ||
104 | expect(res.body.data).to.have.lengthOf(0) | ||
105 | }) | ||
106 | |||
107 | it('Should have "my-videos" redundancies on server 2', async function () { | ||
108 | this.timeout(120000) | ||
109 | |||
110 | const res = await listVideoRedundancies({ | ||
111 | url: servers[1].url, | ||
112 | accessToken: servers[1].accessToken, | ||
113 | target: 'my-videos' | ||
114 | }) | ||
115 | |||
116 | expect(res.body.total).to.equal(2) | ||
117 | |||
118 | const videos = res.body.data as VideoRedundancy[] | ||
119 | expect(videos).to.have.lengthOf(2) | ||
120 | |||
121 | const videos1 = videos.find(v => v.uuid === video1Server2UUID) | ||
122 | const videos2 = videos.find(v => v.uuid === video2Server2UUID) | ||
123 | |||
124 | expect(videos1.name).to.equal('video 1 server 2') | ||
125 | expect(videos2.name).to.equal('video 2 server 2') | ||
126 | |||
127 | expect(videos1.redundancies.files).to.have.lengthOf(4) | ||
128 | expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
129 | |||
130 | const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists) | ||
131 | |||
132 | for (const r of redundancies) { | ||
133 | expect(r.strategy).to.be.null | ||
134 | expect(r.fileUrl).to.exist | ||
135 | expect(r.createdAt).to.exist | ||
136 | expect(r.updatedAt).to.exist | ||
137 | expect(r.expiresOn).to.exist | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should not have "my-videos" redundancies on server 1', async function () { | ||
142 | const res = await listVideoRedundancies({ | ||
143 | url: servers[0].url, | ||
144 | accessToken: servers[0].accessToken, | ||
145 | target: 'my-videos' | ||
146 | }) | ||
147 | |||
148 | expect(res.body.total).to.equal(0) | ||
149 | expect(res.body.data).to.have.lengthOf(0) | ||
150 | }) | ||
151 | |||
152 | it('Should have "remote-videos" redundancies on server 1', async function () { | ||
153 | this.timeout(120000) | ||
154 | |||
155 | const res = await listVideoRedundancies({ | ||
156 | url: servers[0].url, | ||
157 | accessToken: servers[0].accessToken, | ||
158 | target: 'remote-videos' | ||
159 | }) | ||
160 | |||
161 | expect(res.body.total).to.equal(2) | ||
162 | |||
163 | const videos = res.body.data as VideoRedundancy[] | ||
164 | expect(videos).to.have.lengthOf(2) | ||
165 | |||
166 | const videos1 = videos.find(v => v.uuid === video1Server2UUID) | ||
167 | const videos2 = videos.find(v => v.uuid === video2Server2UUID) | ||
168 | |||
169 | expect(videos1.name).to.equal('video 1 server 2') | ||
170 | expect(videos2.name).to.equal('video 2 server 2') | ||
171 | |||
172 | expect(videos1.redundancies.files).to.have.lengthOf(4) | ||
173 | expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
174 | |||
175 | const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists) | ||
176 | |||
177 | for (const r of redundancies) { | ||
178 | expect(r.strategy).to.equal('recently-added') | ||
179 | expect(r.fileUrl).to.exist | ||
180 | expect(r.createdAt).to.exist | ||
181 | expect(r.updatedAt).to.exist | ||
182 | expect(r.expiresOn).to.exist | ||
183 | } | ||
184 | }) | ||
185 | |||
186 | it('Should correctly paginate and sort results', async function () { | ||
187 | { | ||
188 | const res = await listVideoRedundancies({ | ||
189 | url: servers[0].url, | ||
190 | accessToken: servers[0].accessToken, | ||
191 | target: 'remote-videos', | ||
192 | sort: 'name', | ||
193 | start: 0, | ||
194 | count: 2 | ||
195 | }) | ||
196 | |||
197 | const videos = res.body.data | ||
198 | expect(videos[ 0 ].name).to.equal('video 1 server 2') | ||
199 | expect(videos[ 1 ].name).to.equal('video 2 server 2') | ||
200 | } | ||
201 | |||
202 | { | ||
203 | const res = await listVideoRedundancies({ | ||
204 | url: servers[0].url, | ||
205 | accessToken: servers[0].accessToken, | ||
206 | target: 'remote-videos', | ||
207 | sort: '-name', | ||
208 | start: 0, | ||
209 | count: 2 | ||
210 | }) | ||
211 | |||
212 | const videos = res.body.data | ||
213 | expect(videos[ 0 ].name).to.equal('video 2 server 2') | ||
214 | expect(videos[ 1 ].name).to.equal('video 1 server 2') | ||
215 | } | ||
216 | |||
217 | { | ||
218 | const res = await listVideoRedundancies({ | ||
219 | url: servers[0].url, | ||
220 | accessToken: servers[0].accessToken, | ||
221 | target: 'remote-videos', | ||
222 | sort: '-name', | ||
223 | start: 1, | ||
224 | count: 1 | ||
225 | }) | ||
226 | |||
227 | const videos = res.body.data | ||
228 | expect(videos[ 0 ].name).to.equal('video 1 server 2') | ||
229 | } | ||
230 | }) | ||
231 | |||
232 | it('Should manually add a redundancy and list it', async function () { | ||
233 | this.timeout(120000) | ||
234 | |||
235 | const uuid = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid | ||
236 | await waitJobs(servers) | ||
237 | const videoId = await getLocalIdByUUID(servers[0].url, uuid) | ||
238 | |||
239 | await addVideoRedundancy({ | ||
240 | url: servers[0].url, | ||
241 | accessToken: servers[0].accessToken, | ||
242 | videoId | ||
243 | }) | ||
244 | |||
245 | await waitJobs(servers) | ||
246 | await waitUntilLog(servers[0], 'Duplicated ', 15) | ||
247 | await waitJobs(servers) | ||
248 | |||
249 | { | ||
250 | const res = await listVideoRedundancies({ | ||
251 | url: servers[0].url, | ||
252 | accessToken: servers[0].accessToken, | ||
253 | target: 'remote-videos', | ||
254 | sort: '-name', | ||
255 | start: 0, | ||
256 | count: 5 | ||
257 | }) | ||
258 | |||
259 | const videos = res.body.data | ||
260 | expect(videos[ 0 ].name).to.equal('video 3 server 2') | ||
261 | |||
262 | const video = videos[ 0 ] | ||
263 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
264 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
265 | |||
266 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
267 | |||
268 | for (const r of redundancies) { | ||
269 | redundanciesToRemove.push(r.id) | ||
270 | |||
271 | expect(r.strategy).to.equal('manual') | ||
272 | expect(r.fileUrl).to.exist | ||
273 | expect(r.createdAt).to.exist | ||
274 | expect(r.updatedAt).to.exist | ||
275 | expect(r.expiresOn).to.be.null | ||
276 | } | ||
277 | } | ||
278 | |||
279 | const res = await listVideoRedundancies({ | ||
280 | url: servers[1].url, | ||
281 | accessToken: servers[1].accessToken, | ||
282 | target: 'my-videos', | ||
283 | sort: '-name', | ||
284 | start: 0, | ||
285 | count: 5 | ||
286 | }) | ||
287 | |||
288 | const videos = res.body.data | ||
289 | expect(videos[ 0 ].name).to.equal('video 3 server 2') | ||
290 | |||
291 | const video = videos[ 0 ] | ||
292 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
293 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
294 | |||
295 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
296 | |||
297 | for (const r of redundancies) { | ||
298 | expect(r.strategy).to.be.null | ||
299 | expect(r.fileUrl).to.exist | ||
300 | expect(r.createdAt).to.exist | ||
301 | expect(r.updatedAt).to.exist | ||
302 | expect(r.expiresOn).to.be.null | ||
303 | } | ||
304 | }) | ||
305 | |||
306 | it('Should manually remove a redundancy and remove it from the list', async function () { | ||
307 | this.timeout(120000) | ||
308 | |||
309 | for (const redundancyId of redundanciesToRemove) { | ||
310 | await removeVideoRedundancy({ | ||
311 | url: servers[ 0 ].url, | ||
312 | accessToken: servers[ 0 ].accessToken, | ||
313 | redundancyId | ||
314 | }) | ||
315 | } | ||
316 | |||
317 | { | ||
318 | const res = await listVideoRedundancies({ | ||
319 | url: servers[0].url, | ||
320 | accessToken: servers[0].accessToken, | ||
321 | target: 'remote-videos', | ||
322 | sort: '-name', | ||
323 | start: 0, | ||
324 | count: 5 | ||
325 | }) | ||
326 | |||
327 | const videos = res.body.data | ||
328 | expect(videos).to.have.lengthOf(2) | ||
329 | |||
330 | expect(videos[ 0 ].name).to.equal('video 2 server 2') | ||
331 | |||
332 | redundanciesToRemove = [] | ||
333 | const video = videos[ 0 ] | ||
334 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
335 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
336 | |||
337 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
338 | |||
339 | for (const r of redundancies) { | ||
340 | redundanciesToRemove.push(r.id) | ||
341 | } | ||
342 | } | ||
343 | }) | ||
344 | |||
345 | it('Should remove another (auto) redundancy', async function () { | ||
346 | { | ||
347 | for (const redundancyId of redundanciesToRemove) { | ||
348 | await removeVideoRedundancy({ | ||
349 | url: servers[ 0 ].url, | ||
350 | accessToken: servers[ 0 ].accessToken, | ||
351 | redundancyId | ||
352 | }) | ||
353 | } | ||
354 | |||
355 | const res = await listVideoRedundancies({ | ||
356 | url: servers[0].url, | ||
357 | accessToken: servers[0].accessToken, | ||
358 | target: 'remote-videos', | ||
359 | sort: '-name', | ||
360 | start: 0, | ||
361 | count: 5 | ||
362 | }) | ||
363 | |||
364 | const videos = res.body.data | ||
365 | expect(videos[ 0 ].name).to.equal('video 1 server 2') | ||
366 | expect(videos).to.have.lengthOf(1) | ||
367 | } | ||
368 | }) | ||
369 | |||
370 | after(async function () { | ||
371 | await cleanupTests(servers) | ||
372 | }) | ||
373 | }) | ||
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 1cdf93aa1..f5bf130d5 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -5,7 +5,8 @@ import 'mocha' | |||
5 | import { VideoDetails } from '../../../../shared/models/videos' | 5 | import { VideoDetails } from '../../../../shared/models/videos' |
6 | import { | 6 | import { |
7 | checkSegmentHash, | 7 | checkSegmentHash, |
8 | checkVideoFilesWereRemoved, cleanupTests, | 8 | checkVideoFilesWereRemoved, |
9 | cleanupTests, | ||
9 | doubleFollow, | 10 | doubleFollow, |
10 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
11 | getFollowingListPaginationAndSort, | 12 | getFollowingListPaginationAndSort, |
@@ -28,11 +29,16 @@ import { | |||
28 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 29 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
29 | 30 | ||
30 | import * as magnetUtil from 'magnet-uri' | 31 | import * as magnetUtil from 'magnet-uri' |
31 | import { updateRedundancy } from '../../../../shared/extra-utils/server/redundancy' | 32 | import { |
33 | addVideoRedundancy, | ||
34 | listVideoRedundancies, | ||
35 | removeVideoRedundancy, | ||
36 | updateRedundancy | ||
37 | } from '../../../../shared/extra-utils/server/redundancy' | ||
32 | import { ActorFollow } from '../../../../shared/models/actors' | 38 | import { ActorFollow } from '../../../../shared/models/actors' |
33 | import { readdir } from 'fs-extra' | 39 | import { readdir } from 'fs-extra' |
34 | import { join } from 'path' | 40 | import { join } from 'path' |
35 | import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' | 41 | import { VideoRedundancy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../../shared/models/redundancy' |
36 | import { getStats } from '../../../../shared/extra-utils/server/stats' | 42 | import { getStats } from '../../../../shared/extra-utils/server/stats' |
37 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | 43 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' |
38 | 44 | ||
@@ -40,6 +46,7 @@ const expect = chai.expect | |||
40 | 46 | ||
41 | let servers: ServerInfo[] = [] | 47 | let servers: ServerInfo[] = [] |
42 | let video1Server2UUID: string | 48 | let video1Server2UUID: string |
49 | let video1Server2Id: number | ||
43 | 50 | ||
44 | function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { | 51 | function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { |
45 | const parsed = magnetUtil.decode(file.magnetUri) | 52 | const parsed = magnetUtil.decode(file.magnetUri) |
@@ -52,7 +59,19 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe | |||
52 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) | 59 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) |
53 | } | 60 | } |
54 | 61 | ||
55 | async function flushAndRunServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | 62 | async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}) { |
63 | const strategies: any[] = [] | ||
64 | |||
65 | if (strategy !== null) { | ||
66 | strategies.push( | ||
67 | immutableAssign({ | ||
68 | min_lifetime: '1 hour', | ||
69 | strategy: strategy, | ||
70 | size: '400KB' | ||
71 | }, additionalParams) | ||
72 | ) | ||
73 | } | ||
74 | |||
56 | const config = { | 75 | const config = { |
57 | transcoding: { | 76 | transcoding: { |
58 | hls: { | 77 | hls: { |
@@ -62,16 +81,11 @@ async function flushAndRunServers (strategy: VideoRedundancyStrategy, additional | |||
62 | redundancy: { | 81 | redundancy: { |
63 | videos: { | 82 | videos: { |
64 | check_interval: '5 seconds', | 83 | check_interval: '5 seconds', |
65 | strategies: [ | 84 | strategies |
66 | immutableAssign({ | ||
67 | min_lifetime: '1 hour', | ||
68 | strategy: strategy, | ||
69 | size: '400KB' | ||
70 | }, additionalParams) | ||
71 | ] | ||
72 | } | 85 | } |
73 | } | 86 | } |
74 | } | 87 | } |
88 | |||
75 | servers = await flushAndRunMultipleServers(3, config) | 89 | servers = await flushAndRunMultipleServers(3, config) |
76 | 90 | ||
77 | // Get the access tokens | 91 | // Get the access tokens |
@@ -80,6 +94,7 @@ async function flushAndRunServers (strategy: VideoRedundancyStrategy, additional | |||
80 | { | 94 | { |
81 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) | 95 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) |
82 | video1Server2UUID = res.body.video.uuid | 96 | video1Server2UUID = res.body.video.uuid |
97 | video1Server2Id = res.body.video.id | ||
83 | 98 | ||
84 | await viewVideo(servers[ 1 ].url, video1Server2UUID) | 99 | await viewVideo(servers[ 1 ].url, video1Server2UUID) |
85 | } | 100 | } |
@@ -216,29 +231,38 @@ async function check1PlaylistRedundancies (videoUUID?: string) { | |||
216 | } | 231 | } |
217 | } | 232 | } |
218 | 233 | ||
219 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | 234 | async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) { |
235 | let totalSize: number = null | ||
236 | let statsLength = 1 | ||
237 | |||
238 | if (strategy !== 'manual') { | ||
239 | totalSize = 409600 | ||
240 | statsLength = 2 | ||
241 | } | ||
242 | |||
220 | const res = await getStats(servers[0].url) | 243 | const res = await getStats(servers[0].url) |
221 | const data: ServerStats = res.body | 244 | const data: ServerStats = res.body |
222 | 245 | ||
223 | expect(data.videosRedundancy).to.have.lengthOf(1) | 246 | expect(data.videosRedundancy).to.have.lengthOf(statsLength) |
224 | const stat = data.videosRedundancy[0] | ||
225 | 247 | ||
248 | const stat = data.videosRedundancy[0] | ||
226 | expect(stat.strategy).to.equal(strategy) | 249 | expect(stat.strategy).to.equal(strategy) |
227 | expect(stat.totalSize).to.equal(409600) | 250 | expect(stat.totalSize).to.equal(totalSize) |
251 | |||
252 | return stat | ||
253 | } | ||
254 | |||
255 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategyWithManual) { | ||
256 | const stat = await checkStatsGlobal(strategy) | ||
257 | |||
228 | expect(stat.totalUsed).to.be.at.least(1).and.below(409601) | 258 | expect(stat.totalUsed).to.be.at.least(1).and.below(409601) |
229 | expect(stat.totalVideoFiles).to.equal(4) | 259 | expect(stat.totalVideoFiles).to.equal(4) |
230 | expect(stat.totalVideos).to.equal(1) | 260 | expect(stat.totalVideos).to.equal(1) |
231 | } | 261 | } |
232 | 262 | ||
233 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | 263 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategyWithManual) { |
234 | const res = await getStats(servers[0].url) | 264 | const stat = await checkStatsGlobal(strategy) |
235 | const data: ServerStats = res.body | ||
236 | |||
237 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
238 | 265 | ||
239 | const stat = data.videosRedundancy[0] | ||
240 | expect(stat.strategy).to.equal(strategy) | ||
241 | expect(stat.totalSize).to.equal(409600) | ||
242 | expect(stat.totalUsed).to.equal(0) | 266 | expect(stat.totalUsed).to.equal(0) |
243 | expect(stat.totalVideoFiles).to.equal(0) | 267 | expect(stat.totalVideoFiles).to.equal(0) |
244 | expect(stat.totalVideos).to.equal(0) | 268 | expect(stat.totalVideos).to.equal(0) |
@@ -446,6 +470,74 @@ describe('Test videos redundancy', function () { | |||
446 | }) | 470 | }) |
447 | }) | 471 | }) |
448 | 472 | ||
473 | describe('With manual strategy', function () { | ||
474 | before(function () { | ||
475 | this.timeout(120000) | ||
476 | |||
477 | return flushAndRunServers(null) | ||
478 | }) | ||
479 | |||
480 | it('Should have 1 webseed on the first video', async function () { | ||
481 | await check1WebSeed() | ||
482 | await check0PlaylistRedundancies() | ||
483 | await checkStatsWith1Webseed('manual') | ||
484 | }) | ||
485 | |||
486 | it('Should create a redundancy on first video', async function () { | ||
487 | await addVideoRedundancy({ | ||
488 | url: servers[0].url, | ||
489 | accessToken: servers[0].accessToken, | ||
490 | videoId: video1Server2Id | ||
491 | }) | ||
492 | }) | ||
493 | |||
494 | it('Should have 2 webseeds on the first video', async function () { | ||
495 | this.timeout(80000) | ||
496 | |||
497 | await waitJobs(servers) | ||
498 | await waitUntilLog(servers[0], 'Duplicated ', 5) | ||
499 | await waitJobs(servers) | ||
500 | |||
501 | await check2Webseeds() | ||
502 | await check1PlaylistRedundancies() | ||
503 | await checkStatsWith2Webseed('manual') | ||
504 | }) | ||
505 | |||
506 | it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () { | ||
507 | this.timeout(80000) | ||
508 | |||
509 | const res = await listVideoRedundancies({ | ||
510 | url: servers[0].url, | ||
511 | accessToken: servers[0].accessToken, | ||
512 | target: 'remote-videos' | ||
513 | }) | ||
514 | |||
515 | const videos = res.body.data as VideoRedundancy[] | ||
516 | expect(videos).to.have.lengthOf(1) | ||
517 | |||
518 | const video = videos[0] | ||
519 | for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) { | ||
520 | await removeVideoRedundancy({ | ||
521 | url: servers[0].url, | ||
522 | accessToken: servers[0].accessToken, | ||
523 | redundancyId: r.id | ||
524 | }) | ||
525 | } | ||
526 | |||
527 | await waitJobs(servers) | ||
528 | await wait(5000) | ||
529 | |||
530 | await check1WebSeed() | ||
531 | await check0PlaylistRedundancies() | ||
532 | |||
533 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | ||
534 | }) | ||
535 | |||
536 | after(async function () { | ||
537 | await cleanupTests(servers) | ||
538 | }) | ||
539 | }) | ||
540 | |||
449 | describe('Test expiration', function () { | 541 | describe('Test expiration', function () { |
450 | const strategy = 'recently-added' | 542 | const strategy = 'recently-added' |
451 | 543 | ||
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 4be74901a..0104c94fc 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | doubleFollow, | 11 | doubleFollow, |
12 | flushAndRunMultipleServers, | 12 | flushAndRunMultipleServers, |
13 | generateHighBitrateVideo, | 13 | generateHighBitrateVideo, |
14 | generateVideoWithFramerate, | ||
14 | getMyVideos, | 15 | getMyVideos, |
15 | getVideo, | 16 | getVideo, |
16 | getVideosList, | 17 | getVideosList, |
@@ -416,6 +417,39 @@ describe('Test video transcoding', function () { | |||
416 | } | 417 | } |
417 | }) | 418 | }) |
418 | 419 | ||
420 | it('Should downscale to the closest divisor standard framerate', async function () { | ||
421 | this.timeout(160000) | ||
422 | |||
423 | let tempFixturePath: string | ||
424 | |||
425 | { | ||
426 | tempFixturePath = await generateVideoWithFramerate() | ||
427 | |||
428 | const fps = await getVideoFileFPS(tempFixturePath) | ||
429 | expect(fps).to.be.equal(59) | ||
430 | } | ||
431 | |||
432 | const videoAttributes = { | ||
433 | name: '59fps video', | ||
434 | description: '59fps video', | ||
435 | fixture: tempFixturePath | ||
436 | } | ||
437 | |||
438 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
439 | |||
440 | await waitJobs(servers) | ||
441 | |||
442 | for (const server of servers) { | ||
443 | const res = await getVideosList(server.url) | ||
444 | |||
445 | const video = res.body.data.find(v => v.name === videoAttributes.name) | ||
446 | const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4') | ||
447 | const fps = await getVideoFileFPS(path) | ||
448 | |||
449 | expect(fps).to.be.equal(25) | ||
450 | } | ||
451 | }) | ||
452 | |||
419 | after(async function () { | 453 | after(async function () { |
420 | await cleanupTests(servers) | 454 | await cleanupTests(servers) |
421 | }) | 455 | }) |
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts index b8c0b1f79..15b6755f2 100644 --- a/server/tests/cli/peertube.ts +++ b/server/tests/cli/peertube.ts | |||
@@ -6,15 +6,15 @@ import { | |||
6 | addVideoChannel, | 6 | addVideoChannel, |
7 | buildAbsoluteFixturePath, | 7 | buildAbsoluteFixturePath, |
8 | cleanupTests, | 8 | cleanupTests, |
9 | createUser, | 9 | createUser, doubleFollow, |
10 | execCLI, | 10 | execCLI, |
11 | flushAndRunServer, | 11 | flushAndRunServer, |
12 | getEnvCli, | 12 | getEnvCli, getLocalIdByUUID, |
13 | getVideo, | 13 | getVideo, |
14 | getVideosList, | 14 | getVideosList, |
15 | getVideosListWithToken, removeVideo, | 15 | getVideosListWithToken, removeVideo, |
16 | ServerInfo, | 16 | ServerInfo, |
17 | setAccessTokensToServers, | 17 | setAccessTokensToServers, uploadVideo, uploadVideoAndGetId, |
18 | userLogin, | 18 | userLogin, |
19 | waitJobs | 19 | waitJobs |
20 | } from '../../../shared/extra-utils' | 20 | } from '../../../shared/extra-utils' |
@@ -210,6 +210,81 @@ describe('Test CLI wrapper', function () { | |||
210 | }) | 210 | }) |
211 | }) | 211 | }) |
212 | 212 | ||
213 | describe('Manage video redundancies', function () { | ||
214 | let anotherServer: ServerInfo | ||
215 | let video1Server2: number | ||
216 | let servers: ServerInfo[] | ||
217 | |||
218 | before(async function () { | ||
219 | this.timeout(120000) | ||
220 | |||
221 | anotherServer = await flushAndRunServer(2) | ||
222 | await setAccessTokensToServers([ anotherServer ]) | ||
223 | |||
224 | await doubleFollow(server, anotherServer) | ||
225 | |||
226 | servers = [ server, anotherServer ] | ||
227 | await waitJobs(servers) | ||
228 | |||
229 | const uuid = (await uploadVideoAndGetId({ server: anotherServer, videoName: 'super video' })).uuid | ||
230 | await waitJobs(servers) | ||
231 | |||
232 | video1Server2 = await getLocalIdByUUID(server.url, uuid) | ||
233 | }) | ||
234 | |||
235 | it('Should add a redundancy', async function () { | ||
236 | this.timeout(60000) | ||
237 | |||
238 | const env = getEnvCli(server) | ||
239 | |||
240 | const params = `add --video ${video1Server2}` | ||
241 | |||
242 | await execCLI(`${env} ${cmd} redundancy ${params}`) | ||
243 | |||
244 | await waitJobs(servers) | ||
245 | }) | ||
246 | |||
247 | it('Should list redundancies', async function () { | ||
248 | this.timeout(60000) | ||
249 | |||
250 | { | ||
251 | const env = getEnvCli(server) | ||
252 | |||
253 | const params = `list-my-redundancies` | ||
254 | const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`) | ||
255 | |||
256 | expect(stdout).to.contain('super video') | ||
257 | expect(stdout).to.contain(`localhost:${server.port}`) | ||
258 | } | ||
259 | }) | ||
260 | |||
261 | it('Should remove a redundancy', async function () { | ||
262 | this.timeout(60000) | ||
263 | |||
264 | const env = getEnvCli(server) | ||
265 | |||
266 | const params = `remove --video ${video1Server2}` | ||
267 | |||
268 | await execCLI(`${env} ${cmd} redundancy ${params}`) | ||
269 | |||
270 | await waitJobs(servers) | ||
271 | |||
272 | { | ||
273 | const env = getEnvCli(server) | ||
274 | const params = `list-my-redundancies` | ||
275 | const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`) | ||
276 | |||
277 | expect(stdout).to.not.contain('super video') | ||
278 | } | ||
279 | }) | ||
280 | |||
281 | after(async function () { | ||
282 | this.timeout(10000) | ||
283 | |||
284 | await cleanupTests([ anotherServer ]) | ||
285 | }) | ||
286 | }) | ||
287 | |||
213 | after(async function () { | 288 | after(async function () { |
214 | this.timeout(10000) | 289 | this.timeout(10000) |
215 | 290 | ||
diff --git a/server/tools/cli.ts b/server/tools/cli.ts index 58e2445ac..15ac6c6a8 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts | |||
@@ -6,6 +6,9 @@ import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' | |||
6 | import { Command } from 'commander' | 6 | import { Command } from 'commander' |
7 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' | 7 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' |
8 | import { createLogger, format, transports } from 'winston' | 8 | import { createLogger, format, transports } from 'winston' |
9 | import { getMyUserInformation } from '@shared/extra-utils/users/users' | ||
10 | import { User, UserRole } from '@shared/models' | ||
11 | import { getAccessToken } from '@shared/extra-utils/users/login' | ||
9 | 12 | ||
10 | let configName = 'PeerTube/CLI' | 13 | let configName = 'PeerTube/CLI' |
11 | if (isTestInstance()) configName += `-${getAppNumber()}` | 14 | if (isTestInstance()) configName += `-${getAppNumber()}` |
@@ -14,6 +17,19 @@ const config = require('application-config')(configName) | |||
14 | 17 | ||
15 | const version = require('../../../package.json').version | 18 | const version = require('../../../package.json').version |
16 | 19 | ||
20 | async function getAdminTokenOrDie (url: string, username: string, password: string) { | ||
21 | const accessToken = await getAccessToken(url, username, password) | ||
22 | const resMe = await getMyUserInformation(url, accessToken) | ||
23 | const me: User = resMe.body | ||
24 | |||
25 | if (me.role !== UserRole.ADMINISTRATOR) { | ||
26 | console.error('You must be an administrator.') | ||
27 | process.exit(-1) | ||
28 | } | ||
29 | |||
30 | return accessToken | ||
31 | } | ||
32 | |||
17 | interface Settings { | 33 | interface Settings { |
18 | remotes: any[], | 34 | remotes: any[], |
19 | default: number | 35 | default: number |
@@ -222,5 +238,7 @@ export { | |||
222 | getServerCredentials, | 238 | getServerCredentials, |
223 | 239 | ||
224 | buildCommonVideoOptions, | 240 | buildCommonVideoOptions, |
225 | buildVideoAttributesFromCommander | 241 | buildVideoAttributesFromCommander, |
242 | |||
243 | getAdminTokenOrDie | ||
226 | } | 244 | } |
diff --git a/server/tools/package.json b/server/tools/package.json index 40959d76e..06ad31cab 100644 --- a/server/tools/package.json +++ b/server/tools/package.json | |||
@@ -4,11 +4,12 @@ | |||
4 | "private": true, | 4 | "private": true, |
5 | "dependencies": { | 5 | "dependencies": { |
6 | "application-config": "^1.0.1", | 6 | "application-config": "^1.0.1", |
7 | "cli-table": "^0.3.1", | 7 | "cli-table3": "^0.5.1", |
8 | "netrc-parser": "^3.1.6", | 8 | "netrc-parser": "^3.1.6", |
9 | "webtorrent-hybrid": "^4.0.1" | 9 | "webtorrent-hybrid": "^4.0.1" |
10 | }, | 10 | }, |
11 | "summon": { | 11 | "summon": { |
12 | "silent": true | 12 | "silent": true |
13 | } | 13 | }, |
14 | "devDependencies": {} | ||
14 | } | 15 | } |
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts index 6597a5c36..acac75034 100644 --- a/server/tools/peertube-auth.ts +++ b/server/tools/peertube-auth.ts | |||
@@ -6,8 +6,7 @@ import * as prompt from 'prompt' | |||
6 | import { getNetrc, getSettings, writeSettings } from './cli' | 6 | import { getNetrc, getSettings, writeSettings } from './cli' |
7 | import { isUserUsernameValid } from '../helpers/custom-validators/users' | 7 | import { isUserUsernameValid } from '../helpers/custom-validators/users' |
8 | import { getAccessToken, login } from '../../shared/extra-utils' | 8 | import { getAccessToken, login } from '../../shared/extra-utils' |
9 | 9 | import * as CliTable3 from 'cli-table3' | |
10 | const Table = require('cli-table') | ||
11 | 10 | ||
12 | async function delInstance (url: string) { | 11 | async function delInstance (url: string) { |
13 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | 12 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) |
@@ -108,10 +107,10 @@ program | |||
108 | .action(async () => { | 107 | .action(async () => { |
109 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | 108 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) |
110 | 109 | ||
111 | const table = new Table({ | 110 | const table = new CliTable3({ |
112 | head: ['instance', 'login'], | 111 | head: ['instance', 'login'], |
113 | colWidths: [30, 30] | 112 | colWidths: [30, 30] |
114 | }) | 113 | }) as CliTable3.HorizontalTable |
115 | 114 | ||
116 | settings.remotes.forEach(element => { | 115 | settings.remotes.forEach(element => { |
117 | if (!netrc.machines[element]) return | 116 | if (!netrc.machines[element]) return |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index eaa792763..c7e85b570 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -1,10 +1,6 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../helpers/register-ts-paths' |
2 | |||
3 | registerTSPaths() | 2 | registerTSPaths() |
4 | 3 | ||
5 | // FIXME: https://github.com/nodejs/node/pull/16853 | ||
6 | require('tls').DEFAULT_ECDH_CURVE = 'auto' | ||
7 | |||
8 | import * as program from 'commander' | 4 | import * as program from 'commander' |
9 | import { join } from 'path' | 5 | import { join } from 'path' |
10 | import { doRequestAndSaveToFile } from '../helpers/requests' | 6 | import { doRequestAndSaveToFile } from '../helpers/requests' |
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts index e40606107..b341c14c1 100644 --- a/server/tools/peertube-plugins.ts +++ b/server/tools/peertube-plugins.ts | |||
@@ -3,15 +3,11 @@ registerTSPaths() | |||
3 | 3 | ||
4 | import * as program from 'commander' | 4 | import * as program from 'commander' |
5 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 5 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
6 | import { getAccessToken } from '../../shared/extra-utils/users/login' | ||
7 | import { getMyUserInformation } from '../../shared/extra-utils/users/users' | ||
8 | import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' | 6 | import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' |
9 | import { getServerCredentials } from './cli' | 7 | import { getAdminTokenOrDie, getServerCredentials } from './cli' |
10 | import { User, UserRole } from '../../shared/models/users' | ||
11 | import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' | 8 | import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' |
12 | import { isAbsolute } from 'path' | 9 | import { isAbsolute } from 'path' |
13 | 10 | import * as CliTable3 from 'cli-table3' | |
14 | const Table = require('cli-table') | ||
15 | 11 | ||
16 | program | 12 | program |
17 | .name('plugins') | 13 | .name('plugins') |
@@ -82,10 +78,10 @@ async function pluginsListCLI () { | |||
82 | }) | 78 | }) |
83 | const plugins: PeerTubePlugin[] = res.body.data | 79 | const plugins: PeerTubePlugin[] = res.body.data |
84 | 80 | ||
85 | const table = new Table({ | 81 | const table = new CliTable3({ |
86 | head: ['name', 'version', 'homepage'], | 82 | head: ['name', 'version', 'homepage'], |
87 | colWidths: [ 50, 10, 50 ] | 83 | colWidths: [ 50, 10, 50 ] |
88 | }) | 84 | }) as CliTable3.HorizontalTable |
89 | 85 | ||
90 | for (const plugin of plugins) { | 86 | for (const plugin of plugins) { |
91 | const npmName = plugin.type === PluginType.PLUGIN | 87 | const npmName = plugin.type === PluginType.PLUGIN |
@@ -192,16 +188,3 @@ async function uninstallPluginCLI (options: any) { | |||
192 | console.log('Plugin uninstalled.') | 188 | console.log('Plugin uninstalled.') |
193 | process.exit(0) | 189 | process.exit(0) |
194 | } | 190 | } |
195 | |||
196 | async function getAdminTokenOrDie (url: string, username: string, password: string) { | ||
197 | const accessToken = await getAccessToken(url, username, password) | ||
198 | const resMe = await getMyUserInformation(url, accessToken) | ||
199 | const me: User = resMe.body | ||
200 | |||
201 | if (me.role !== UserRole.ADMINISTRATOR) { | ||
202 | console.error('Cannot list plugins if you are not administrator.') | ||
203 | process.exit(-1) | ||
204 | } | ||
205 | |||
206 | return accessToken | ||
207 | } | ||
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts new file mode 100644 index 000000000..a71f48104 --- /dev/null +++ b/server/tools/peertube-redundancy.ts | |||
@@ -0,0 +1,194 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | ||
2 | registerTSPaths() | ||
3 | |||
4 | import * as program from 'commander' | ||
5 | import { getAdminTokenOrDie, getServerCredentials } from './cli' | ||
6 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
7 | import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy' | ||
8 | import validator from 'validator' | ||
9 | import bytes = require('bytes') | ||
10 | import * as CliTable3 from 'cli-table3' | ||
11 | import { parse } from 'url' | ||
12 | import { uniq } from 'lodash' | ||
13 | |||
14 | program | ||
15 | .name('plugins') | ||
16 | .usage('[command] [options]') | ||
17 | |||
18 | program | ||
19 | .command('list-remote-redundancies') | ||
20 | .description('List remote redundancies on your videos') | ||
21 | .option('-u, --url <url>', 'Server url') | ||
22 | .option('-U, --username <username>', 'Username') | ||
23 | .option('-p, --password <token>', 'Password') | ||
24 | .action(() => listRedundanciesCLI('my-videos')) | ||
25 | |||
26 | program | ||
27 | .command('list-my-redundancies') | ||
28 | .description('List your redundancies of remote videos') | ||
29 | .option('-u, --url <url>', 'Server url') | ||
30 | .option('-U, --username <username>', 'Username') | ||
31 | .option('-p, --password <token>', 'Password') | ||
32 | .action(() => listRedundanciesCLI('remote-videos')) | ||
33 | |||
34 | program | ||
35 | .command('add') | ||
36 | .description('Duplicate a video in your redundancy system') | ||
37 | .option('-u, --url <url>', 'Server url') | ||
38 | .option('-U, --username <username>', 'Username') | ||
39 | .option('-p, --password <token>', 'Password') | ||
40 | .option('-v, --video <videoId>', 'Video id to duplicate') | ||
41 | .action((options) => addRedundancyCLI(options)) | ||
42 | |||
43 | program | ||
44 | .command('remove') | ||
45 | .description('Remove a video from your redundancies') | ||
46 | .option('-u, --url <url>', 'Server url') | ||
47 | .option('-U, --username <username>', 'Username') | ||
48 | .option('-p, --password <token>', 'Password') | ||
49 | .option('-v, --video <videoId>', 'Video id to remove from redundancies') | ||
50 | .action((options) => removeRedundancyCLI(options)) | ||
51 | |||
52 | if (!process.argv.slice(2).length) { | ||
53 | program.outputHelp() | ||
54 | } | ||
55 | |||
56 | program.parse(process.argv) | ||
57 | |||
58 | // ---------------------------------------------------------------------------- | ||
59 | |||
60 | async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | ||
61 | const { url, username, password } = await getServerCredentials(program) | ||
62 | const accessToken = await getAdminTokenOrDie(url, username, password) | ||
63 | |||
64 | const redundancies = await listVideoRedundanciesData(url, accessToken, target) | ||
65 | |||
66 | const table = new CliTable3({ | ||
67 | head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ] | ||
68 | }) as CliTable3.HorizontalTable | ||
69 | |||
70 | for (const redundancy of redundancies) { | ||
71 | const webtorrentFiles = redundancy.redundancies.files | ||
72 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists | ||
73 | |||
74 | let totalSize = '' | ||
75 | if (target === 'remote-videos') { | ||
76 | const tmp = webtorrentFiles.concat(streamingPlaylists) | ||
77 | .reduce((a, b) => a + b.size, 0) | ||
78 | |||
79 | totalSize = bytes(tmp) | ||
80 | } | ||
81 | |||
82 | const instances = uniq( | ||
83 | webtorrentFiles.concat(streamingPlaylists) | ||
84 | .map(r => r.fileUrl) | ||
85 | .map(u => parse(u).host) | ||
86 | ) | ||
87 | |||
88 | table.push([ | ||
89 | redundancy.id.toString(), | ||
90 | redundancy.name, | ||
91 | redundancy.url, | ||
92 | webtorrentFiles.length, | ||
93 | streamingPlaylists.length, | ||
94 | instances.join('\n'), | ||
95 | totalSize | ||
96 | ]) | ||
97 | } | ||
98 | |||
99 | console.log(table.toString()) | ||
100 | process.exit(0) | ||
101 | } | ||
102 | |||
103 | async function addRedundancyCLI (options: { videoId: number }) { | ||
104 | const { url, username, password } = await getServerCredentials(program) | ||
105 | const accessToken = await getAdminTokenOrDie(url, username, password) | ||
106 | |||
107 | if (!options[ 'video' ] || validator.isInt('' + options[ 'video' ]) === false) { | ||
108 | console.error('You need to specify the video id to duplicate and it should be a number.\n') | ||
109 | program.outputHelp() | ||
110 | process.exit(-1) | ||
111 | } | ||
112 | |||
113 | try { | ||
114 | await addVideoRedundancy({ | ||
115 | url, | ||
116 | accessToken, | ||
117 | videoId: options[ 'video' ] | ||
118 | }) | ||
119 | |||
120 | console.log('Video will be duplicated by your instance!') | ||
121 | |||
122 | process.exit(0) | ||
123 | } catch (err) { | ||
124 | if (err.message.includes(409)) { | ||
125 | console.error('This video is already duplicated by your instance.') | ||
126 | } else if (err.message.includes(404)) { | ||
127 | console.error('This video id does not exist.') | ||
128 | } else { | ||
129 | console.error(err) | ||
130 | } | ||
131 | |||
132 | process.exit(-1) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | async function removeRedundancyCLI (options: { videoId: number }) { | ||
137 | const { url, username, password } = await getServerCredentials(program) | ||
138 | const accessToken = await getAdminTokenOrDie(url, username, password) | ||
139 | |||
140 | if (!options[ 'video' ] || validator.isInt('' + options[ 'video' ]) === false) { | ||
141 | console.error('You need to specify the video id to remove from your redundancies.\n') | ||
142 | program.outputHelp() | ||
143 | process.exit(-1) | ||
144 | } | ||
145 | |||
146 | const videoId = parseInt(options[ 'video' ] + '', 10) | ||
147 | |||
148 | let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos') | ||
149 | let videoRedundancy = redundancies.find(r => videoId === r.id) | ||
150 | |||
151 | if (!videoRedundancy) { | ||
152 | redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos') | ||
153 | videoRedundancy = redundancies.find(r => videoId === r.id) | ||
154 | } | ||
155 | |||
156 | if (!videoRedundancy) { | ||
157 | console.error('Video redundancy not found.') | ||
158 | process.exit(-1) | ||
159 | } | ||
160 | |||
161 | try { | ||
162 | const ids = videoRedundancy.redundancies.files | ||
163 | .concat(videoRedundancy.redundancies.streamingPlaylists) | ||
164 | .map(r => r.id) | ||
165 | |||
166 | for (const id of ids) { | ||
167 | await removeVideoRedundancy({ | ||
168 | url, | ||
169 | accessToken, | ||
170 | redundancyId: id | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | console.log('Video redundancy removed!') | ||
175 | |||
176 | process.exit(0) | ||
177 | } catch (err) { | ||
178 | console.error(err) | ||
179 | process.exit(-1) | ||
180 | } | ||
181 | } | ||
182 | |||
183 | async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) { | ||
184 | const res = await listVideoRedundancies({ | ||
185 | url, | ||
186 | accessToken, | ||
187 | start: 0, | ||
188 | count: 100, | ||
189 | sort: 'name', | ||
190 | target | ||
191 | }) | ||
192 | |||
193 | return res.body.data as VideoRedundancy[] | ||
194 | } | ||
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts index fc85c4210..9883bbf05 100644 --- a/server/tools/peertube.ts +++ b/server/tools/peertube.ts | |||
@@ -22,6 +22,7 @@ program | |||
22 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') | 22 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') |
23 | .command('repl', 'initiate a REPL to access internals') | 23 | .command('repl', 'initiate a REPL to access internals') |
24 | .command('plugins [action]', 'manage instance plugins/themes').alias('p') | 24 | .command('plugins [action]', 'manage instance plugins/themes').alias('p') |
25 | .command('redundancy [action]', 'manage instance redundancies').alias('r') | ||
25 | 26 | ||
26 | /* Not Yet Implemented */ | 27 | /* Not Yet Implemented */ |
27 | program | 28 | program |
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock index 28756cbc2..ccd716a51 100644 --- a/server/tools/yarn.lock +++ b/server/tools/yarn.lock | |||
@@ -347,12 +347,15 @@ chunk-store-stream@^4.0.0: | |||
347 | block-stream2 "^2.0.0" | 347 | block-stream2 "^2.0.0" |
348 | readable-stream "^3.4.0" | 348 | readable-stream "^3.4.0" |
349 | 349 | ||
350 | cli-table@^0.3.1: | 350 | cli-table3@^0.5.1: |
351 | version "0.3.1" | 351 | version "0.5.1" |
352 | resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" | 352 | resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" |
353 | integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= | 353 | integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== |
354 | dependencies: | 354 | dependencies: |
355 | colors "1.0.3" | 355 | object-assign "^4.1.0" |
356 | string-width "^2.1.1" | ||
357 | optionalDependencies: | ||
358 | colors "^1.1.2" | ||
356 | 359 | ||
357 | clivas@^0.2.0: | 360 | clivas@^0.2.0: |
358 | version "0.2.0" | 361 | version "0.2.0" |
@@ -364,10 +367,10 @@ code-point-at@^1.0.0: | |||
364 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | 367 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" |
365 | integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= | 368 | integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= |
366 | 369 | ||
367 | colors@1.0.3: | 370 | colors@^1.1.2: |
368 | version "1.0.3" | 371 | version "1.4.0" |
369 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" | 372 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" |
370 | integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= | 373 | integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== |
371 | 374 | ||
372 | common-tags@^1.8.0: | 375 | common-tags@^1.8.0: |
373 | version "1.8.0" | 376 | version "1.8.0" |
@@ -1609,7 +1612,7 @@ string-width@^1.0.1: | |||
1609 | is-fullwidth-code-point "^1.0.0" | 1612 | is-fullwidth-code-point "^1.0.0" |
1610 | strip-ansi "^3.0.0" | 1613 | strip-ansi "^3.0.0" |
1611 | 1614 | ||
1612 | "string-width@^1.0.2 || 2": | 1615 | "string-width@^1.0.2 || 2", string-width@^2.1.1: |
1613 | version "2.1.1" | 1616 | version "2.1.1" |
1614 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" | 1617 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" |
1615 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== | 1618 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== |
diff --git a/server/typings/express.ts b/server/typings/express.ts index 3cc7c7632..43a9b2c99 100644 --- a/server/typings/express.ts +++ b/server/typings/express.ts | |||
@@ -21,7 +21,7 @@ import { | |||
21 | } from './models' | 21 | } from './models' |
22 | import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist' | 22 | import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist' |
23 | import { MVideoImportDefault } from '@server/typings/models/video/video-import' | 23 | import { MVideoImportDefault } from '@server/typings/models/video/video-import' |
24 | import { MAccountBlocklist, MStreamingPlaylist, MVideoFile } from '@server/typings/models' | 24 | import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile } from '@server/typings/models' |
25 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element' | 25 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element' |
26 | import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate' | 26 | import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate' |
27 | import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership' | 27 | import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership' |
@@ -74,6 +74,7 @@ declare module 'express' { | |||
74 | 74 | ||
75 | account?: MAccountDefault | 75 | account?: MAccountDefault |
76 | 76 | ||
77 | actorUrl?: MActorUrl | ||
77 | actorFull?: MActorFull | 78 | actorFull?: MActorFull |
78 | 79 | ||
79 | user?: MUserDefault | 80 | user?: MUserDefault |
diff --git a/server/typings/models/video/video-file.ts b/server/typings/models/video/video-file.ts index 352fe3d32..139b22b2c 100644 --- a/server/typings/models/video/video-file.ts +++ b/server/typings/models/video/video-file.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { VideoFileModel } from '../../../models/video/video-file' | 1 | import { VideoFileModel } from '../../../models/video/video-file' |
2 | import { PickWith, PickWithOpt } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { MVideo, MVideoUUID } from './video' | 3 | import { MVideo, MVideoUUID } from './video' |
4 | import { MVideoRedundancyFileUrl } from './video-redundancy' | 4 | import { MVideoRedundancy, MVideoRedundancyFileUrl } from './video-redundancy' |
5 | import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist' | 5 | import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist' |
6 | 6 | ||
7 | type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M> | 7 | type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M> |
@@ -22,6 +22,9 @@ export type MVideoFileStreamingPlaylistVideo = MVideoFile & | |||
22 | export type MVideoFileVideoUUID = MVideoFile & | 22 | export type MVideoFileVideoUUID = MVideoFile & |
23 | Use<'Video', MVideoUUID> | 23 | Use<'Video', MVideoUUID> |
24 | 24 | ||
25 | export type MVideoFileRedundanciesAll = MVideoFile & | ||
26 | PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancy[]> | ||
27 | |||
25 | export type MVideoFileRedundanciesOpt = MVideoFile & | 28 | export type MVideoFileRedundanciesOpt = MVideoFile & |
26 | PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> | 29 | PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> |
27 | 30 | ||
diff --git a/server/typings/models/video/video-streaming-playlist.ts b/server/typings/models/video/video-streaming-playlist.ts index 436c0c072..6fd489945 100644 --- a/server/typings/models/video/video-streaming-playlist.ts +++ b/server/typings/models/video/video-streaming-playlist.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist' | 1 | import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist' |
2 | import { PickWith, PickWithOpt } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { MVideoRedundancyFileUrl } from './video-redundancy' | 3 | import { MVideoRedundancyFileUrl, MVideoRedundancy } from './video-redundancy' |
4 | import { MVideo } from './video' | 4 | import { MVideo } from './video' |
5 | import { MVideoFile } from './video-file' | 5 | import { MVideoFile } from './video-file' |
6 | 6 | ||
@@ -20,6 +20,10 @@ export type MStreamingPlaylistFilesVideo = MStreamingPlaylist & | |||
20 | Use<'VideoFiles', MVideoFile[]> & | 20 | Use<'VideoFiles', MVideoFile[]> & |
21 | Use<'Video', MVideo> | 21 | Use<'Video', MVideo> |
22 | 22 | ||
23 | export type MStreamingPlaylistRedundanciesAll = MStreamingPlaylist & | ||
24 | Use<'VideoFiles', MVideoFile[]> & | ||
25 | Use<'RedundancyVideos', MVideoRedundancy[]> | ||
26 | |||
23 | export type MStreamingPlaylistRedundancies = MStreamingPlaylist & | 27 | export type MStreamingPlaylistRedundancies = MStreamingPlaylist & |
24 | Use<'VideoFiles', MVideoFile[]> & | 28 | Use<'VideoFiles', MVideoFile[]> & |
25 | Use<'RedundancyVideos', MVideoRedundancyFileUrl[]> | 29 | Use<'RedundancyVideos', MVideoRedundancyFileUrl[]> |
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts index 7f69a91de..bcc5e5028 100644 --- a/server/typings/models/video/video.ts +++ b/server/typings/models/video/video.ts | |||
@@ -10,8 +10,13 @@ import { | |||
10 | } from './video-channels' | 10 | } from './video-channels' |
11 | import { MTag } from './tag' | 11 | import { MTag } from './tag' |
12 | import { MVideoCaptionLanguage } from './video-caption' | 12 | import { MVideoCaptionLanguage } from './video-caption' |
13 | import { MStreamingPlaylistFiles, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist' | 13 | import { |
14 | import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file' | 14 | MStreamingPlaylistFiles, |
15 | MStreamingPlaylistRedundancies, | ||
16 | MStreamingPlaylistRedundanciesAll, | ||
17 | MStreamingPlaylistRedundanciesOpt | ||
18 | } from './video-streaming-playlist' | ||
19 | import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file' | ||
15 | import { MThumbnail } from './thumbnail' | 20 | import { MThumbnail } from './thumbnail' |
16 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' | 21 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' |
17 | import { MScheduleVideoUpdate } from './schedule-video-update' | 22 | import { MScheduleVideoUpdate } from './schedule-video-update' |
@@ -158,6 +163,10 @@ export type MVideoForUser = MVideo & | |||
158 | Use<'VideoBlacklist', MVideoBlacklistLight> & | 163 | Use<'VideoBlacklist', MVideoBlacklistLight> & |
159 | Use<'Thumbnails', MThumbnail[]> | 164 | Use<'Thumbnails', MThumbnail[]> |
160 | 165 | ||
166 | export type MVideoForRedundancyAPI = MVideo & | ||
167 | Use<'VideoFiles', MVideoFileRedundanciesAll[]> & | ||
168 | Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesAll[]> | ||
169 | |||
161 | // ############################################################################ | 170 | // ############################################################################ |
162 | 171 | ||
163 | // Format for API or AP object | 172 | // Format for API or AP object |
diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts index 6b0f6d990..c957a6abe 100644 --- a/shared/extra-utils/miscs/miscs.ts +++ b/shared/extra-utils/miscs/miscs.ts | |||
@@ -104,6 +104,28 @@ async function generateHighBitrateVideo () { | |||
104 | return tempFixturePath | 104 | return tempFixturePath |
105 | } | 105 | } |
106 | 106 | ||
107 | async function generateVideoWithFramerate (fps = 60) { | ||
108 | const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true) | ||
109 | |||
110 | await ensureDir(dirname(tempFixturePath)) | ||
111 | |||
112 | const exists = await pathExists(tempFixturePath) | ||
113 | if (!exists) { | ||
114 | return new Promise<string>(async (res, rej) => { | ||
115 | ffmpeg() | ||
116 | .outputOptions([ '-f rawvideo', '-video_size 320x240', '-i /dev/urandom' ]) | ||
117 | .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) | ||
118 | .outputOptions([ `-r ${fps}` ]) | ||
119 | .output(tempFixturePath) | ||
120 | .on('error', rej) | ||
121 | .on('end', () => res(tempFixturePath)) | ||
122 | .run() | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | return tempFixturePath | ||
127 | } | ||
128 | |||
107 | // --------------------------------------------------------------------------- | 129 | // --------------------------------------------------------------------------- |
108 | 130 | ||
109 | export { | 131 | export { |
@@ -115,5 +137,6 @@ export { | |||
115 | testImage, | 137 | testImage, |
116 | buildAbsoluteFixturePath, | 138 | buildAbsoluteFixturePath, |
117 | root, | 139 | root, |
118 | generateHighBitrateVideo | 140 | generateHighBitrateVideo, |
141 | generateVideoWithFramerate | ||
119 | } | 142 | } |
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts index 167649c6d..42599c20e 100644 --- a/shared/extra-utils/miscs/sql.ts +++ b/shared/extra-utils/miscs/sql.ts | |||
@@ -59,7 +59,6 @@ async function countVideoViewsOf (internalServerNumber: number, uuid: string) { | |||
59 | 59 | ||
60 | if (!total) return 0 | 60 | if (!total) return 0 |
61 | 61 | ||
62 | // FIXME: check if we really need parseInt | ||
63 | return parseInt(total + '', 10) | 62 | return parseInt(total + '', 10) |
64 | } | 63 | } |
65 | 64 | ||
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts index c39ff2c8b..7b488e23e 100644 --- a/shared/extra-utils/server/redundancy.ts +++ b/shared/extra-utils/server/redundancy.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { makePutBodyRequest } from '../requests/requests' | 1 | import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' |
2 | import { VideoRedundanciesTarget } from '@shared/models' | ||
2 | 3 | ||
3 | async function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) { | 4 | function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) { |
4 | const path = '/api/v1/server/redundancy/' + host | 5 | const path = '/api/v1/server/redundancy/' + host |
5 | 6 | ||
6 | return makePutBodyRequest({ | 7 | return makePutBodyRequest({ |
@@ -12,6 +13,69 @@ async function updateRedundancy (url: string, accessToken: string, host: string, | |||
12 | }) | 13 | }) |
13 | } | 14 | } |
14 | 15 | ||
16 | function listVideoRedundancies (options: { | ||
17 | url: string | ||
18 | accessToken: string, | ||
19 | target: VideoRedundanciesTarget, | ||
20 | start?: number, | ||
21 | count?: number, | ||
22 | sort?: string, | ||
23 | statusCodeExpected?: number | ||
24 | }) { | ||
25 | const path = '/api/v1/server/redundancy/videos' | ||
26 | |||
27 | const { url, accessToken, target, statusCodeExpected, start, count, sort } = options | ||
28 | |||
29 | return makeGetRequest({ | ||
30 | url, | ||
31 | token: accessToken, | ||
32 | path, | ||
33 | query: { | ||
34 | start: start ?? 0, | ||
35 | count: count ?? 5, | ||
36 | sort: sort ?? 'name', | ||
37 | target | ||
38 | }, | ||
39 | statusCodeExpected: statusCodeExpected || 200 | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | function addVideoRedundancy (options: { | ||
44 | url: string, | ||
45 | accessToken: string, | ||
46 | videoId: number | ||
47 | }) { | ||
48 | const path = '/api/v1/server/redundancy/videos' | ||
49 | const { url, accessToken, videoId } = options | ||
50 | |||
51 | return makePostBodyRequest({ | ||
52 | url, | ||
53 | token: accessToken, | ||
54 | path, | ||
55 | fields: { videoId }, | ||
56 | statusCodeExpected: 204 | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | function removeVideoRedundancy (options: { | ||
61 | url: string, | ||
62 | accessToken: string, | ||
63 | redundancyId: number | ||
64 | }) { | ||
65 | const { url, accessToken, redundancyId } = options | ||
66 | const path = '/api/v1/server/redundancy/videos/' + redundancyId | ||
67 | |||
68 | return makeDeleteRequest({ | ||
69 | url, | ||
70 | token: accessToken, | ||
71 | path, | ||
72 | statusCodeExpected: 204 | ||
73 | }) | ||
74 | } | ||
75 | |||
15 | export { | 76 | export { |
16 | updateRedundancy | 77 | updateRedundancy, |
78 | listVideoRedundancies, | ||
79 | addVideoRedundancy, | ||
80 | removeVideoRedundancy | ||
17 | } | 81 | } |
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index 7a77a03ad..aa13273ae 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts | |||
@@ -607,15 +607,28 @@ async function videoUUIDToId (url: string, id: number | string) { | |||
607 | return res.body.id | 607 | return res.body.id |
608 | } | 608 | } |
609 | 609 | ||
610 | async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) { | 610 | async function uploadVideoAndGetId (options: { |
611 | server: ServerInfo, | ||
612 | videoName: string, | ||
613 | nsfw?: boolean, | ||
614 | privacy?: VideoPrivacy, | ||
615 | token?: string | ||
616 | }) { | ||
611 | const videoAttrs: any = { name: options.videoName } | 617 | const videoAttrs: any = { name: options.videoName } |
612 | if (options.nsfw) videoAttrs.nsfw = options.nsfw | 618 | if (options.nsfw) videoAttrs.nsfw = options.nsfw |
619 | if (options.privacy) videoAttrs.privacy = options.privacy | ||
613 | 620 | ||
614 | const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs) | 621 | const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs) |
615 | 622 | ||
616 | return { id: res.body.video.id, uuid: res.body.video.uuid } | 623 | return { id: res.body.video.id, uuid: res.body.video.uuid } |
617 | } | 624 | } |
618 | 625 | ||
626 | async function getLocalIdByUUID (url: string, uuid: string) { | ||
627 | const res = await getVideo(url, uuid) | ||
628 | |||
629 | return res.body.id | ||
630 | } | ||
631 | |||
619 | // --------------------------------------------------------------------------- | 632 | // --------------------------------------------------------------------------- |
620 | 633 | ||
621 | export { | 634 | export { |
@@ -645,5 +658,6 @@ export { | |||
645 | completeVideoCheck, | 658 | completeVideoCheck, |
646 | checkVideoFilesWereRemoved, | 659 | checkVideoFilesWereRemoved, |
647 | getPlaylistVideos, | 660 | getPlaylistVideos, |
648 | uploadVideoAndGetId | 661 | uploadVideoAndGetId, |
662 | getLocalIdByUUID | ||
649 | } | 663 | } |
diff --git a/shared/models/redundancy/index.ts b/shared/models/redundancy/index.ts index 61bf0fca7..649cc489f 100644 --- a/shared/models/redundancy/index.ts +++ b/shared/models/redundancy/index.ts | |||
@@ -1 +1,3 @@ | |||
1 | export * from './videos-redundancy.model' | 1 | export * from './videos-redundancy-strategy.model' |
2 | export * from './video-redundancies-filters.model' | ||
3 | export * from './video-redundancy.model' | ||
diff --git a/shared/models/redundancy/video-redundancies-filters.model.ts b/shared/models/redundancy/video-redundancies-filters.model.ts new file mode 100644 index 000000000..05ba7dfd3 --- /dev/null +++ b/shared/models/redundancy/video-redundancies-filters.model.ts | |||
@@ -0,0 +1 @@ | |||
export type VideoRedundanciesTarget = 'my-videos' | 'remote-videos' | |||
diff --git a/shared/models/redundancy/video-redundancy.model.ts b/shared/models/redundancy/video-redundancy.model.ts new file mode 100644 index 000000000..014f69634 --- /dev/null +++ b/shared/models/redundancy/video-redundancy.model.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | export interface VideoRedundancy { | ||
2 | id: number | ||
3 | name: string | ||
4 | url: string | ||
5 | uuid: string | ||
6 | |||
7 | redundancies: { | ||
8 | files: FileRedundancyInformation[] | ||
9 | |||
10 | streamingPlaylists: StreamingPlaylistRedundancyInformation[] | ||
11 | } | ||
12 | } | ||
13 | |||
14 | interface RedundancyInformation { | ||
15 | id: number | ||
16 | fileUrl: string | ||
17 | strategy: string | ||
18 | |||
19 | createdAt: Date | string | ||
20 | updatedAt: Date | string | ||
21 | |||
22 | expiresOn: Date | string | ||
23 | |||
24 | size: number | ||
25 | } | ||
26 | |||
27 | export interface FileRedundancyInformation extends RedundancyInformation { | ||
28 | |||
29 | } | ||
30 | |||
31 | export interface StreamingPlaylistRedundancyInformation extends RedundancyInformation { | ||
32 | |||
33 | } | ||
diff --git a/shared/models/redundancy/videos-redundancy.model.ts b/shared/models/redundancy/videos-redundancy-strategy.model.ts index a8c2743c1..15409abf0 100644 --- a/shared/models/redundancy/videos-redundancy.model.ts +++ b/shared/models/redundancy/videos-redundancy-strategy.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export type VideoRedundancyStrategy = 'most-views' | 'trending' | 'recently-added' | 1 | export type VideoRedundancyStrategy = 'most-views' | 'trending' | 'recently-added' |
2 | export type VideoRedundancyStrategyWithManual = VideoRedundancyStrategy | 'manual' | ||
2 | 3 | ||
3 | export type MostViewsRedundancyStrategy = { | 4 | export type MostViewsRedundancyStrategy = { |
4 | strategy: 'most-views' | 5 | strategy: 'most-views' |
@@ -19,4 +20,4 @@ export type RecentlyAddedStrategy = { | |||
19 | minLifetime: number | 20 | minLifetime: number |
20 | } | 21 | } |
21 | 22 | ||
22 | export type VideosRedundancy = MostViewsRedundancyStrategy | TrendingRedundancyStrategy | RecentlyAddedStrategy | 23 | export type VideosRedundancyStrategy = MostViewsRedundancyStrategy | TrendingRedundancyStrategy | RecentlyAddedStrategy |
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index b82a633b2..19fd4c659 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -9,7 +9,8 @@ export type JobType = 'activitypub-http-unicast' | | |||
9 | 'email' | | 9 | 'email' | |
10 | 'video-import' | | 10 | 'video-import' | |
11 | 'videos-views' | | 11 | 'videos-views' | |
12 | 'activitypub-refresher' | 12 | 'activitypub-refresher' | |
13 | 'video-redundancy' | ||
13 | 14 | ||
14 | export interface Job { | 15 | export interface Job { |
15 | id: number | 16 | id: number |
diff --git a/shared/models/server/server-stats.model.ts b/shared/models/server/server-stats.model.ts index 74f3de5d3..11778e6ed 100644 --- a/shared/models/server/server-stats.model.ts +++ b/shared/models/server/server-stats.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoRedundancyStrategy } from '../redundancy' | 1 | import { VideoRedundancyStrategyWithManual } from '../redundancy' |
2 | 2 | ||
3 | export interface ServerStats { | 3 | export interface ServerStats { |
4 | totalUsers: number | 4 | totalUsers: number |
@@ -13,11 +13,13 @@ export interface ServerStats { | |||
13 | totalInstanceFollowers: number | 13 | totalInstanceFollowers: number |
14 | totalInstanceFollowing: number | 14 | totalInstanceFollowing: number |
15 | 15 | ||
16 | videosRedundancy: { | 16 | videosRedundancy: VideosRedundancyStats[] |
17 | strategy: VideoRedundancyStrategy | 17 | } |
18 | totalSize: number | 18 | |
19 | totalUsed: number | 19 | export interface VideosRedundancyStats { |
20 | totalVideoFiles: number | 20 | strategy: VideoRedundancyStrategyWithManual |
21 | totalVideos: number | 21 | totalSize: number |
22 | }[] | 22 | totalUsed: number |
23 | totalVideoFiles: number | ||
24 | totalVideos: number | ||
23 | } | 25 | } |
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index 4a28a229d..2f88a65de 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts | |||
@@ -33,5 +33,7 @@ export enum UserRight { | |||
33 | SEE_ALL_VIDEOS, | 33 | SEE_ALL_VIDEOS, |
34 | CHANGE_VIDEO_OWNERSHIP, | 34 | CHANGE_VIDEO_OWNERSHIP, |
35 | 35 | ||
36 | MANAGE_PLUGINS | 36 | MANAGE_PLUGINS, |
37 | |||
38 | MANAGE_VIDEOS_REDUNDANCIES | ||
37 | } | 39 | } |
diff --git a/shared/models/videos/video-transcoding-fps.model.ts b/shared/models/videos/video-transcoding-fps.model.ts index 82022d2f1..25fc1c2da 100644 --- a/shared/models/videos/video-transcoding-fps.model.ts +++ b/shared/models/videos/video-transcoding-fps.model.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | export type VideoTranscodingFPS = { | 1 | export type VideoTranscodingFPS = { |
2 | MIN: number, | 2 | MIN: number |
3 | AVERAGE: number, | 3 | STANDARD: number[] |
4 | MAX: number, | 4 | HD_STANDARD: number[] |
5 | AVERAGE: number | ||
6 | MAX: number | ||
5 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: number | 7 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: number |
6 | } | 8 | } |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 7576439fe..a69152759 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { AccountSummary, VideoChannelSummary, VideoResolution, VideoState } from '../../index' | 1 | import { AccountSummary, VideoChannelSummary, VideoState } from '../../index' |
2 | import { Account } from '../actors' | 2 | import { Account } from '../actors' |
3 | import { VideoChannel } from './channel/video-channel.model' | 3 | import { VideoChannel } from './channel/video-channel.model' |
4 | import { VideoPrivacy } from './video-privacy.enum' | 4 | import { VideoPrivacy } from './video-privacy.enum' |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 43718e2a1..907187e4c 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -977,6 +977,12 @@ paths: | |||
977 | application/json: | 977 | application/json: |
978 | schema: | 978 | schema: |
979 | $ref: '#/components/schemas/VideoUploadResponse' | 979 | $ref: '#/components/schemas/VideoUploadResponse' |
980 | '403': | ||
981 | description: 'The user video quota is exceeded with this video.' | ||
982 | '408': | ||
983 | description: 'Upload has timed out' | ||
984 | '422': | ||
985 | description: 'Invalid input file.' | ||
980 | requestBody: | 986 | requestBody: |
981 | content: | 987 | content: |
982 | multipart/form-data: | 988 | multipart/form-data: |
diff --git a/support/doc/production.md b/support/doc/production.md index 8f061f868..6febaba5d 100644 --- a/support/doc/production.md +++ b/support/doc/production.md | |||
@@ -39,7 +39,7 @@ Create the production database and a peertube user inside PostgreSQL: | |||
39 | 39 | ||
40 | ``` | 40 | ``` |
41 | $ sudo -u postgres createuser -P peertube | 41 | $ sudo -u postgres createuser -P peertube |
42 | $ sudo -u postgres createdb -O peertube peertube_prod | 42 | $ sudo -u postgres createdb -O peertube -E UTF8 -T template0 peertube_prod |
43 | ``` | 43 | ``` |
44 | 44 | ||
45 | Then enable extensions PeerTube needs: | 45 | Then enable extensions PeerTube needs: |
diff --git a/support/doc/tools.md b/support/doc/tools.md index d5427b5b7..1f1e52c36 100644 --- a/support/doc/tools.md +++ b/support/doc/tools.md | |||
@@ -12,6 +12,7 @@ | |||
12 | - [peertube-upload.js](#peertube-uploadjs) | 12 | - [peertube-upload.js](#peertube-uploadjs) |
13 | - [peertube-watch.js](#peertube-watchjs) | 13 | - [peertube-watch.js](#peertube-watchjs) |
14 | - [peertube-plugins.js](#peertube-pluginsjs) | 14 | - [peertube-plugins.js](#peertube-pluginsjs) |
15 | - [peertube-redundancy.js](#peertube-redundancyjs) | ||
15 | - [Server tools](#server-tools) | 16 | - [Server tools](#server-tools) |
16 | - [parse-log](#parse-log) | 17 | - [parse-log](#parse-log) |
17 | - [create-transcoding-job.js](#create-transcoding-jobjs) | 18 | - [create-transcoding-job.js](#create-transcoding-jobjs) |
@@ -77,7 +78,8 @@ You can access it as `peertube` via an alias in your `.bashrc` like `alias peert | |||
77 | import-videos|import import a video from a streaming platform | 78 | import-videos|import import a video from a streaming platform |
78 | watch|w watch a video in the terminal ✩°。⋆ | 79 | watch|w watch a video in the terminal ✩°。⋆ |
79 | repl initiate a REPL to access internals | 80 | repl initiate a REPL to access internals |
80 | plugins|p [action] manag instance plugins | 81 | plugins|p [action] manage instance plugins |
82 | redundancy|r [action] manage video redundancies | ||
81 | help [cmd] display help for [cmd] | 83 | help [cmd] display help for [cmd] |
82 | ``` | 84 | ``` |
83 | 85 | ||
@@ -200,6 +202,34 @@ $ node dist/server/tools/peertube-plugins.js install --path /my/plugin/path | |||
200 | $ node dist/server/tools/peertube-plugins.js install --npm-name peertube-theme-example | 202 | $ node dist/server/tools/peertube-plugins.js install --npm-name peertube-theme-example |
201 | ``` | 203 | ``` |
202 | 204 | ||
205 | #### peertube-redundancy.js | ||
206 | |||
207 | Manage (list/add/remove) video redundancies: | ||
208 | |||
209 | To list your videos that are duplicated by remote instances: | ||
210 | |||
211 | ``` | ||
212 | $ node dist/server/tools/peertube.js redundancy list-remote-redundancies | ||
213 | ``` | ||
214 | |||
215 | To list remote videos that your instance duplicated: | ||
216 | |||
217 | ``` | ||
218 | $ node dist/server/tools/peertube.js redundancy list-my-redundancies | ||
219 | ``` | ||
220 | |||
221 | To duplicate a specific video in your redundancy system: | ||
222 | |||
223 | ``` | ||
224 | $ node dist/server/tools/peertube.js redundancy add --video 823 | ||
225 | ``` | ||
226 | |||
227 | To remove a video redundancy: | ||
228 | |||
229 | ``` | ||
230 | $ node dist/server/tools/peertube.js redundancy remove --video 823 | ||
231 | ``` | ||
232 | |||
203 | ## Server tools | 233 | ## Server tools |
204 | 234 | ||
205 | These scripts should be run on the server, in `peertube-latest` directory. | 235 | These scripts should be run on the server, in `peertube-latest` directory. |