diff options
113 files changed, 3641 insertions, 1969 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 442317ce2..a1edde1ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml | |||
@@ -44,7 +44,7 @@ jobs: | |||
44 | env: | 44 | env: |
45 | PGUSER: peertube | 45 | PGUSER: peertube |
46 | PGHOST: localhost | 46 | PGHOST: localhost |
47 | NODE_PENDING_JOB_WAIT: 500 | 47 | NODE_PENDING_JOB_WAIT: 250 |
48 | 48 | ||
49 | steps: | 49 | steps: |
50 | - uses: actions/checkout@v2 | 50 | - uses: actions/checkout@v2 |
diff --git a/CHANGELOG.md b/CHANGELOG.md index d8fa4069f..ef0ec39bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -1,5 +1,128 @@ | |||
1 | # Changelog | 1 | # Changelog |
2 | 2 | ||
3 | ## v3.2.0-rc.1 (~ May) | ||
4 | |||
5 | ### IMPORTANT NOTES | ||
6 | |||
7 | * **Important:** Due to a bug in ffmpeg, PeerTube is not compatible with ffmpeg 4.4. See https://github.com/Chocobozzz/PeerTube/issues/3990 | ||
8 | * By default, HLS transcoding is now enabled and webtorrent is disabled. We suggest you to reflect this change. | ||
9 | See [the documentation](https://docs.joinpeertube.org/admin-configuration?id=webtorrent-transcoding-or-hls-transcoding) for more information | ||
10 | * PeerTube client now displays bigger video thumbnails. | ||
11 | To fix old thumbnails quality, run `regenerate-thumbnails` script after your PeerTube upgrade: https://docs.joinpeertube.org/maintain-tools?id=regenerate-thumbnailsjs | ||
12 | |||
13 | ### Maintenance | ||
14 | |||
15 | * Support `X-Frame-Options` header, enabled by default in the configuration | ||
16 | * Directly use `node` in [systemd template](https://github.com/Chocobozzz/PeerTube/blob/develop/support/systemd/peertube.service) | ||
17 | * Check ffmpeg version at PeerTube startup | ||
18 | |||
19 | ### CLI tools | ||
20 | |||
21 | * Add `regenerate-thumbnails` script to regenerate thumbnails of local videos | ||
22 | |||
23 | ### Plugins/Themes/Embed API | ||
24 | |||
25 | * Theme: | ||
26 | * `--submenuColor` becomes `--submenuBackgroundColor` | ||
27 | * Support HTML placeholders for plugins. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=html-placeholder-elements) for more information | ||
28 | * `player-next` next to the PeerTube player | ||
29 | * Support storing files for plugins in a dedicated directory. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=storage) for more information | ||
30 | * Transcoding: | ||
31 | * Add `inputOptions` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917) | ||
32 | * Add `scaleFilter.name` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917) | ||
33 | * Plugin settings: | ||
34 | * Add ability to register `html` and `select` setting | ||
35 | * Add ability to hide a plugin setting depending on the form state | ||
36 | * Plugin form fields (to add inputs to video form...): | ||
37 | * Add ability to hide a plugin field depending on the form state using `.hidden` property | ||
38 | * Add client helpers: | ||
39 | * `getServerConfig()` | ||
40 | * `getAuthHeader()` | ||
41 | * Add server helpers: | ||
42 | * `config.getServerConfig()` | ||
43 | * `plugin.getBaseStaticRoute()` | ||
44 | * `plugin.getBaseRouterRoute()` | ||
45 | * `plugin.getDataDirectoryPath()` | ||
46 | * `user.getAuthUser()` | ||
47 | * Add client plugin hooks (https://docs.joinpeertube.org/api-plugins): | ||
48 | * `action:modal.video-download.shown` | ||
49 | * `action:video-upload.init` | ||
50 | * `action:video-url-import.init` | ||
51 | * `action:video-torrent-import.init` | ||
52 | * `action:go-live.init` | ||
53 | * `action:auth-user.logged-in` & `action:auth-user.logged-out` | ||
54 | * `action:auth-user.information-loaded` | ||
55 | * `action:admin-plugin-settings.init` | ||
56 | * Add server plugin hooks (https://docs.joinpeertube.org/api-plugins): | ||
57 | * `filter:api.download.video.allowed.result` & `filter:api.download.torrent.allowed.result` to forbid download | ||
58 | * `filter:html.embed.video-playlist.allowed.result` & `filter:html.embed.video.allowed.result` to forbid embed | ||
59 | * `filter:api.search.videos.local.list.params` & `filter:api.search.videos.local.list.result` | ||
60 | * `filter:api.search.videos.index.list.params` & `filter:api.search.videos.index.list.result` | ||
61 | * `filter:api.search.video-channels.local.list.params` & `filter:api.search.video-channels.local.list.result` | ||
62 | * `filter:api.search.video-channels.index.list.params` & `filter:api.search.video-channels.index.list.result` | ||
63 | |||
64 | ### Features | ||
65 | |||
66 | * Accessibility/UI: | ||
67 | * :tada: Redesign channel and account page | ||
68 | * :tada: Increase video miniature size | ||
69 | * :tada: Add channel banner support | ||
70 | * Use a square avatar for channels and a round avatar for accounts | ||
71 | * Use account initial as default account avatar [#4002](https://github.com/Chocobozzz/PeerTube/pull/4002) | ||
72 | * Prefer channel display in video miniature | ||
73 | * Add *support* button in channel page | ||
74 | * Set direct download as default in video download modal [#3880](https://github.com/Chocobozzz/PeerTube/pull/3880) | ||
75 | * Show less information in video download modal by default [#3890](https://github.com/Chocobozzz/PeerTube/pull/3890) | ||
76 | * Autofocus admin plugin search input | ||
77 | * Add `1.75` playback rate to player [#3888](https://github.com/Chocobozzz/PeerTube/pull/3888) | ||
78 | * Add `title` attribute to embed code [#3901](https://github.com/Chocobozzz/PeerTube/pull/3901) | ||
79 | * Don't pause player when opening a modal [#3909](https://github.com/Chocobozzz/PeerTube/pull/3909) | ||
80 | * Add link below the player to open the video on origin instance [#3624](https://github.com/Chocobozzz/PeerTube/issues/3624) | ||
81 | * Notify admins on new available PeerTube version | ||
82 | * Notify admins on new available plugin version | ||
83 | * Video player: | ||
84 | * Add loop toggle to context menu [#3949](https://github.com/Chocobozzz/PeerTube/pull/3949) | ||
85 | * Add icons to context menu [#3955](https://github.com/Chocobozzz/PeerTube/pull/3955) | ||
86 | * Add a *Previous* button in playlist watch page [#3485](https://github.com/Chocobozzz/PeerTube/pull/3485) | ||
87 | * Automatically close the settings menu when clicking outside the player | ||
88 | * Add "stats for nerds" panel in context menu [#3958](https://github.com/Chocobozzz/PeerTube/pull/3958) | ||
89 | * Add channel and playlist stats to stats endpoint [#3747](https://github.com/Chocobozzz/PeerTube/pull/3747) | ||
90 | * Support `playlistPosition=last` and negative index (`playlistPosition=-2`) URL query parameters for playlists [#3974](https://github.com/Chocobozzz/PeerTube/pull/3974) | ||
91 | * My videos: | ||
92 | * Add ability to sort videos (publication date, most viewed...) | ||
93 | * Add ability to only display live videos | ||
94 | * Automatically resume videos for non logged-in users [#3885](https://github.com/Chocobozzz/PeerTube/pull/3885) | ||
95 | * Admin plugins: | ||
96 | * Show a modal when upgrading a plugin to a major version | ||
97 | * Display a setting button after plugin installation | ||
98 | * Add ability to search live videos | ||
99 | * Use bigger thumbnails for feeds | ||
100 | * Parse video description markdown for Opengraph/Twitter/HTML elements | ||
101 | * Open the remote interaction modal when replying to a comment if we are logged-out | ||
102 | * Handle `.srt` captions with broken durations | ||
103 | * Performance: | ||
104 | * Player now lazy loads video captions | ||
105 | * Faster admin table filters | ||
106 | |||
107 | ### Bug fixes | ||
108 | |||
109 | * More robust comments fetcher of remote video | ||
110 | * Fix database ssl connection | ||
111 | * Remove unnecessary black border above and below video in player [#3920](https://github.com/Chocobozzz/PeerTube/pull/3920) | ||
112 | * Reduce tag input excessive padding [#3927](https://github.com/Chocobozzz/PeerTube/pull/3927) | ||
113 | * Fix disappearing hamburger menu for narrow screens [#3929](https://github.com/Chocobozzz/PeerTube/pull/3929) | ||
114 | * Fix Youtube subtitle import with some languages | ||
115 | * Fix transcoding profile update in admin config | ||
116 | * Fix outbox fetch with subtitled videos | ||
117 | * Correctly unload a plugin on update/uninstall [#3940](https://github.com/Chocobozzz/PeerTube/pull/3940) | ||
118 | * Ensure to install plugins that are supported by PeerTube | ||
119 | * Fix welcome/warning modal displaying twice | ||
120 | * Fix h265 video import using CLI | ||
121 | * Fix context menu when watching a playlist | ||
122 | * Fix transcoding job priority preventing video publication when there are many videos to transcode | ||
123 | |||
124 | |||
125 | |||
3 | ## v3.1.0 | 126 | ## v3.1.0 |
4 | 127 | ||
5 | ### IMPORTANT NOTES | 128 | ### IMPORTANT NOTES |
diff --git a/client/.stylelintrc.json b/client/.stylelintrc.json index 25f0b1002..6a322da62 100644 --- a/client/.stylelintrc.json +++ b/client/.stylelintrc.json | |||
@@ -24,6 +24,12 @@ | |||
24 | "rule-empty-line-before": null, | 24 | "rule-empty-line-before": null, |
25 | "selector-max-id": null, | 25 | "selector-max-id": null, |
26 | "scss/at-function-pattern": null, | 26 | "scss/at-function-pattern": null, |
27 | "function-parentheses-space-inside": "never-single-line" | 27 | "function-parentheses-space-inside": "never-single-line", |
28 | "property-no-vendor-prefix": [ | ||
29 | true, | ||
30 | { | ||
31 | "ignoreProperties": [ "mask-image" ] | ||
32 | } | ||
33 | ] | ||
28 | } | 34 | } |
29 | } | 35 | } |
diff --git a/client/e2e/src/po/video-upload.po.ts b/client/e2e/src/po/video-upload.po.ts index 942025b6b..ad2acee7f 100644 --- a/client/e2e/src/po/video-upload.po.ts +++ b/client/e2e/src/po/video-upload.po.ts | |||
@@ -26,7 +26,12 @@ export class VideoUploadPage { | |||
26 | await elem.sendKeys(fileToUpload) | 26 | await elem.sendKeys(fileToUpload) |
27 | 27 | ||
28 | // Wait for the upload to finish | 28 | // Wait for the upload to finish |
29 | await browser.wait(browser.ExpectedConditions.elementToBeClickable(this.getSecondStepSubmitButton())) | 29 | await browser.wait(async () => { |
30 | const actionButton = this.getSecondStepSubmitButton().element(by.css('.action-button')) | ||
31 | |||
32 | const klass = await actionButton.getAttribute('class') | ||
33 | return !klass.includes('disabled') | ||
34 | }) | ||
30 | } | 35 | } |
31 | 36 | ||
32 | async validSecondUploadStep (videoName: string) { | 37 | async validSecondUploadStep (videoName: string) { |
diff --git a/client/package.json b/client/package.json index 140fc3095..8486ace22 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -96,6 +96,7 @@ | |||
96 | "lodash-es": "^4.17.4", | 96 | "lodash-es": "^4.17.4", |
97 | "markdown-it": "12.0.4", | 97 | "markdown-it": "12.0.4", |
98 | "mini-css-extract-plugin": "^1.3.1", | 98 | "mini-css-extract-plugin": "^1.3.1", |
99 | "ngx-uploadx": "^4.1.0", | ||
99 | "p2p-media-loader-hlsjs": "^0.6.2", | 100 | "p2p-media-loader-hlsjs": "^0.6.2", |
100 | "path-browserify": "^1.0.0", | 101 | "path-browserify": "^1.0.0", |
101 | "primeng": "^11.0.0-rc.1", | 102 | "primeng": "^11.0.0-rc.1", |
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html index f81465f88..6bc1d0448 100644 --- a/client/src/app/+about/about-follows/about-follows.component.html +++ b/client/src/app/+about/about-follows/about-follows.component.html | |||
@@ -9,7 +9,7 @@ | |||
9 | {{ follower}} | 9 | {{ follower}} |
10 | </a> | 10 | </a> |
11 | 11 | ||
12 | <button i18n class="showMore" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> | 12 | <button i18n class="show-more" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> |
13 | </div> | 13 | </div> |
14 | 14 | ||
15 | <div class="col-xl-6 col-md-12"> | 15 | <div class="col-xl-6 col-md-12"> |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 0628c7a96..7e916e122 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts | |||
@@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
79 | } | 79 | } |
80 | 80 | ||
81 | loadMoreChannels () { | 81 | loadMoreChannels () { |
82 | this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) | 82 | const options = { |
83 | account: this.account, | ||
84 | componentPagination: this.channelPagination, | ||
85 | sort: '-updatedAt' | ||
86 | } | ||
87 | |||
88 | this.videoChannelService.listAccountVideoChannels(options) | ||
83 | .pipe( | 89 | .pipe( |
84 | tap(res => this.channelPagination.totalItems = res.total), | 90 | tap(res => this.channelPagination.totalItems = res.total), |
85 | switchMap(res => from(res.data)), | 91 | switchMap(res => from(res.data)), |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index fbd7380a9..c69b04a01 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
66 | distinctUntilChanged(), | 66 | distinctUntilChanged(), |
67 | switchMap(accountId => this.accountService.getAccount(accountId)), | 67 | switchMap(accountId => this.accountService.getAccount(accountId)), |
68 | tap(account => this.onAccount(account)), | 68 | tap(account => this.onAccount(account)), |
69 | switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), | 69 | switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })), |
70 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ | 70 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ |
71 | HttpStatusCode.BAD_REQUEST_400, | 71 | HttpStatusCode.BAD_REQUEST_400, |
72 | HttpStatusCode.NOT_FOUND_404 | 72 | HttpStatusCode.NOT_FOUND_404 |
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index 6900e8717..8d8f12c48 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | <my-global-icon iconName="search"></my-global-icon> | 20 | <my-global-icon iconName="search"></my-global-icon> |
21 | 21 | ||
22 | <ng-container i18n> | 22 | <ng-container i18n> |
23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}" | 23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" |
24 | </ng-container> | 24 | </ng-container> |
25 | </ng-container> | 25 | </ng-container> |
26 | </div> | 26 | </div> |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index c16368952..a0f2f28f8 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' | |||
2 | import { HttpErrorResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse } from '@angular/common/http' |
3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' | 3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' |
4 | import { AuthService, Notifier, User, UserService } from '@app/core' | 4 | import { AuthService, Notifier, User, UserService } from '@app/core' |
5 | import { uploadErrorHandler } from '@app/helpers' | 5 | import { genericUploadErrorHandler } from '@app/helpers' |
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-account-settings', | 8 | selector: 'my-account-settings', |
@@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
46 | this.user.updateAccountAvatar(data.avatar) | 46 | this.user.updateAccountAvatar(data.avatar) |
47 | }, | 47 | }, |
48 | 48 | ||
49 | (err: HttpErrorResponse) => uploadErrorHandler({ | 49 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
50 | err, | 50 | err, |
51 | name: $localize`avatar`, | 51 | name: $localize`avatar`, |
52 | notifier: this.notifier | 52 | notifier: this.notifier |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index a29af176c..c9173039a 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts | |||
@@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http' | |||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { uploadErrorHandler } from '@app/helpers' | 6 | import { genericUploadErrorHandler } from '@app/helpers' |
7 | import { | 7 | import { |
8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | 8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, |
9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | 9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, |
@@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
109 | this.videoChannel.updateAvatar(data.avatar) | 109 | this.videoChannel.updateAvatar(data.avatar) |
110 | }, | 110 | }, |
111 | 111 | ||
112 | (err: HttpErrorResponse) => uploadErrorHandler({ | 112 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
113 | err, | 113 | err, |
114 | name: $localize`avatar`, | 114 | name: $localize`avatar`, |
115 | notifier: this.notifier | 115 | notifier: this.notifier |
@@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
139 | this.videoChannel.updateBanner(data.banner) | 139 | this.videoChannel.updateBanner(data.banner) |
140 | }, | 140 | }, |
141 | 141 | ||
142 | (err: HttpErrorResponse) => uploadErrorHandler({ | 142 | (err: HttpErrorResponse) => genericUploadErrorHandler({ |
143 | err, | 143 | err, |
144 | name: $localize`banner`, | 144 | name: $localize`banner`, |
145 | notifier: this.notifier | 145 | notifier: this.notifier |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index 9e3bf35b4..67b3ee496 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts | |||
@@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`, | |||
68 | this.authService.userInformationLoaded | 68 | this.authService.userInformationLoaded |
69 | .pipe(mergeMap(() => { | 69 | .pipe(mergeMap(() => { |
70 | const user = this.authService.getUser() | 70 | const user = this.authService.getUser() |
71 | const options = { | ||
72 | account: user.account, | ||
73 | withStats: true, | ||
74 | search: this.search, | ||
75 | sort: '-updatedAt' | ||
76 | } | ||
71 | 77 | ||
72 | return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search) | 78 | return this.videoChannelService.listAccountVideoChannels(options) |
73 | })).subscribe(res => { | 79 | })).subscribe(res => { |
74 | this.videoChannels = res.data | 80 | this.videoChannels = res.data |
75 | this.totalItems = res.total | 81 | this.totalItems = res.total |
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html index 088765b20..d0393a2a4 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html | |||
@@ -8,13 +8,8 @@ | |||
8 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
9 | <div class="form-group"> | 9 | <div class="form-group"> |
10 | <label i18n for="channel">Select a channel to receive the video</label> | 10 | <label i18n for="channel">Select a channel to receive the video</label> |
11 | <div class="peertube-select-container"> | 11 | <my-select-channel labelForId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel> |
12 | <select formControlName="channel" id="channel" class="form-control"> | 12 | |
13 | <option i18n value="undefined" disabled>Channel that will receive the video</option> | ||
14 | <option *ngFor="let channel of videoChannels" [value]="channel.id">{{ channel.displayName }} | ||
15 | </option> | ||
16 | </select> | ||
17 | </div> | ||
18 | <div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div> | 13 | <div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div> |
19 | </div> | 14 | </div> |
20 | </div> | 15 | </div> |
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts index 0e2395754..7889d0985 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { switchMap } from 'rxjs/operators' | 1 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | ||
4 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
6 | import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' | 7 | import { VideoOwnershipService } from '@app/shared/shared-main' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { VideoChangeOwnership, VideoChannel } from '@shared/models' | 9 | import { VideoChangeOwnership } from '@shared/models' |
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-accept-ownership', | 12 | selector: 'my-accept-ownership', |
@@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
18 | @ViewChild('modal', { static: true }) modal: ElementRef | 19 | @ViewChild('modal', { static: true }) modal: ElementRef |
19 | 20 | ||
20 | videoChangeOwnership: VideoChangeOwnership | undefined = undefined | 21 | videoChangeOwnership: VideoChangeOwnership | undefined = undefined |
21 | 22 | videoChannels: SelectChannelItem[] | |
22 | videoChannels: VideoChannel[] | ||
23 | 23 | ||
24 | error: string = null | 24 | error: string = null |
25 | 25 | ||
@@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
28 | private videoOwnershipService: VideoOwnershipService, | 28 | private videoOwnershipService: VideoOwnershipService, |
29 | private notifier: Notifier, | 29 | private notifier: Notifier, |
30 | private authService: AuthService, | 30 | private authService: AuthService, |
31 | private videoChannelService: VideoChannelService, | ||
32 | private modalService: NgbModal | 31 | private modalService: NgbModal |
33 | ) { | 32 | ) { |
34 | super() | 33 | super() |
@@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
37 | ngOnInit () { | 36 | ngOnInit () { |
38 | this.videoChannels = [] | 37 | this.videoChannels = [] |
39 | 38 | ||
40 | this.authService.userInformationLoaded | 39 | listUserChannels(this.authService) |
41 | .pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account))) | 40 | .subscribe(channels => this.videoChannels = channels) |
42 | .subscribe(videoChannels => this.videoChannels = videoChannels.data) | ||
43 | 41 | ||
44 | this.buildForm({ | 42 | this.buildForm({ |
45 | channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR | 43 | channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR |
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html index 1d1e7b868..421bc7f6f 100644 --- a/client/src/app/+search/search-filters.component.html +++ b/client/src/app/+search/search-filters.component.html | |||
@@ -18,6 +18,25 @@ | |||
18 | 18 | ||
19 | <div class="form-group"> | 19 | <div class="form-group"> |
20 | <div class="radio-label label-container"> | 20 | <div class="radio-label label-container"> |
21 | <label i18n>Display only</label> | ||
22 | <button i18n class="reset-button reset-button-small" (click)="resetField('isLive')" *ngIf="advancedSearch.isLive !== undefined"> | ||
23 | Reset | ||
24 | </button> | ||
25 | </div> | ||
26 | |||
27 | <div class="peertube-radio-container"> | ||
28 | <input type="radio" name="isLive" id="isLiveTrue" value="true" [(ngModel)]="advancedSearch.isLive"> | ||
29 | <label i18n for="isLiveTrue" class="radio">Live videos</label> | ||
30 | </div> | ||
31 | |||
32 | <div class="peertube-radio-container"> | ||
33 | <input type="radio" name="isLive" id="isLiveFalse" value="false" [(ngModel)]="advancedSearch.isLive"> | ||
34 | <label i18n for="isLiveFalse" class="radio">VOD videos</label> | ||
35 | </div> | ||
36 | </div> | ||
37 | |||
38 | <div class="form-group"> | ||
39 | <div class="radio-label label-container"> | ||
21 | <label i18n>Display sensitive content</label> | 40 | <label i18n>Display sensitive content</label> |
22 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> | 41 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> |
23 | Reset | 42 | Reset |
@@ -44,7 +63,7 @@ | |||
44 | </div> | 63 | </div> |
45 | 64 | ||
46 | <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> | 65 | <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> |
47 | <input type="radio" (change)="inputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> | 66 | <input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> |
48 | <label [for]="date.id" class="radio">{{ date.label }}</label> | 67 | <label [for]="date.id" class="radio">{{ date.label }}</label> |
49 | </div> | 68 | </div> |
50 | </div> | 69 | </div> |
@@ -60,7 +79,7 @@ | |||
60 | <div class="row"> | 79 | <div class="row"> |
61 | <div class="pl-0 col-sm-6"> | 80 | <div class="pl-0 col-sm-6"> |
62 | <input | 81 | <input |
63 | (change)="inputUpdated()" | 82 | (change)="onInputUpdated()" |
64 | (keydown.enter)="$event.preventDefault()" | 83 | (keydown.enter)="$event.preventDefault()" |
65 | type="text" id="original-publication-after" name="original-publication-after" | 84 | type="text" id="original-publication-after" name="original-publication-after" |
66 | i18n-placeholder placeholder="After..." | 85 | i18n-placeholder placeholder="After..." |
@@ -70,7 +89,7 @@ | |||
70 | </div> | 89 | </div> |
71 | <div class="pr-0 col-sm-6"> | 90 | <div class="pr-0 col-sm-6"> |
72 | <input | 91 | <input |
73 | (change)="inputUpdated()" | 92 | (change)="onInputUpdated()" |
74 | (keydown.enter)="$event.preventDefault()" | 93 | (keydown.enter)="$event.preventDefault()" |
75 | type="text" id="original-publication-before" name="original-publication-before" | 94 | type="text" id="original-publication-before" name="original-publication-before" |
76 | i18n-placeholder placeholder="Before..." | 95 | i18n-placeholder placeholder="Before..." |
@@ -93,7 +112,7 @@ | |||
93 | </div> | 112 | </div> |
94 | 113 | ||
95 | <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> | 114 | <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> |
96 | <input type="radio" (change)="inputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> | 115 | <input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> |
97 | <label [for]="duration.id" class="radio">{{ duration.label }}</label> | 116 | <label [for]="duration.id" class="radio">{{ duration.label }}</label> |
98 | </div> | 117 | </div> |
99 | </div> | 118 | </div> |
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts index a2af9a942..59aba22ff 100644 --- a/client/src/app/+search/search-filters.component.ts +++ b/client/src/app/+search/search-filters.component.ts | |||
@@ -3,6 +3,8 @@ import { ServerService } from '@app/core' | |||
3 | import { AdvancedSearch } from '@app/shared/shared-search' | 3 | import { AdvancedSearch } from '@app/shared/shared-search' |
4 | import { ServerConfig, VideoConstant } from '@shared/models' | 4 | import { ServerConfig, VideoConstant } from '@shared/models' |
5 | 5 | ||
6 | type FormOption = { id: string, label: string } | ||
7 | |||
6 | @Component({ | 8 | @Component({ |
7 | selector: 'my-search-filters', | 9 | selector: 'my-search-filters', |
8 | styleUrls: [ './search-filters.component.scss' ], | 10 | styleUrls: [ './search-filters.component.scss' ], |
@@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit { | |||
17 | videoLicences: VideoConstant<number>[] = [] | 19 | videoLicences: VideoConstant<number>[] = [] |
18 | videoLanguages: VideoConstant<string>[] = [] | 20 | videoLanguages: VideoConstant<string>[] = [] |
19 | 21 | ||
20 | publishedDateRanges: { id: string, label: string }[] = [] | 22 | publishedDateRanges: FormOption[] = [] |
21 | sorts: { id: string, label: string }[] = [] | 23 | sorts: FormOption[] = [] |
22 | durationRanges: { id: string, label: string }[] = [] | 24 | durationRanges: FormOption[] = [] |
25 | videoType: FormOption[] = [] | ||
23 | 26 | ||
24 | publishedDateRange: string | 27 | publishedDateRange: string |
25 | durationRange: string | 28 | durationRange: string |
@@ -34,10 +37,6 @@ export class SearchFiltersComponent implements OnInit { | |||
34 | ) { | 37 | ) { |
35 | this.publishedDateRanges = [ | 38 | this.publishedDateRanges = [ |
36 | { | 39 | { |
37 | id: 'any_published_date', | ||
38 | label: $localize`Any` | ||
39 | }, | ||
40 | { | ||
41 | id: 'today', | 40 | id: 'today', |
42 | label: $localize`Today` | 41 | label: $localize`Today` |
43 | }, | 42 | }, |
@@ -55,12 +54,19 @@ export class SearchFiltersComponent implements OnInit { | |||
55 | } | 54 | } |
56 | ] | 55 | ] |
57 | 56 | ||
58 | this.durationRanges = [ | 57 | this.videoType = [ |
59 | { | 58 | { |
60 | id: 'any_duration', | 59 | id: 'vod', |
61 | label: $localize`Any` | 60 | label: $localize`VOD videos` |
62 | }, | 61 | }, |
63 | { | 62 | { |
63 | id: 'live', | ||
64 | label: $localize`Live videos` | ||
65 | } | ||
66 | ] | ||
67 | |||
68 | this.durationRanges = [ | ||
69 | { | ||
64 | id: 'short', | 70 | id: 'short', |
65 | label: $localize`Short (< 4 min)` | 71 | label: $localize`Short (< 4 min)` |
66 | }, | 72 | }, |
@@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit { | |||
104 | this.loadOriginallyPublishedAtYears() | 110 | this.loadOriginallyPublishedAtYears() |
105 | } | 111 | } |
106 | 112 | ||
107 | inputUpdated () { | 113 | onInputUpdated () { |
108 | this.updateModelFromDurationRange() | 114 | this.updateModelFromDurationRange() |
109 | this.updateModelFromPublishedRange() | 115 | this.updateModelFromPublishedRange() |
110 | this.updateModelFromOriginallyPublishedAtYears() | 116 | this.updateModelFromOriginallyPublishedAtYears() |
111 | } | 117 | } |
112 | 118 | ||
113 | formUpdated () { | 119 | formUpdated () { |
114 | this.inputUpdated() | 120 | this.onInputUpdated() |
115 | this.filtered.emit(this.advancedSearch) | 121 | this.filtered.emit(this.advancedSearch) |
116 | } | 122 | } |
117 | 123 | ||
118 | reset () { | 124 | reset () { |
119 | this.advancedSearch.reset() | 125 | this.advancedSearch.reset() |
126 | |||
127 | this.resetOriginalPublicationYears() | ||
128 | |||
120 | this.durationRange = undefined | 129 | this.durationRange = undefined |
121 | this.publishedDateRange = undefined | 130 | this.publishedDateRange = undefined |
122 | this.originallyPublishedStartYear = undefined | 131 | |
123 | this.originallyPublishedEndYear = undefined | 132 | this.onInputUpdated() |
124 | this.inputUpdated() | ||
125 | } | 133 | } |
126 | 134 | ||
127 | resetField (fieldName: string, value?: any) { | 135 | resetField (fieldName: string, value?: any) { |
@@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit { | |||
130 | 138 | ||
131 | resetLocalField (fieldName: string, value?: any) { | 139 | resetLocalField (fieldName: string, value?: any) { |
132 | this[fieldName] = value | 140 | this[fieldName] = value |
133 | this.inputUpdated() | 141 | this.onInputUpdated() |
134 | } | 142 | } |
135 | 143 | ||
136 | resetOriginalPublicationYears () { | 144 | resetOriginalPublicationYears () { |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 094b4d3b3..16233f9e0 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -5,7 +5,7 @@ | |||
5 | <a ngbNavLink i18n>Basic info</a> | 5 | <a ngbNavLink i18n>Basic info</a> |
6 | 6 | ||
7 | <ng-template ngbNavContent> | 7 | <ng-template ngbNavContent> |
8 | <div class="row"> | 8 | <div class="form-columns"> |
9 | <div class="col-video-edit"> | 9 | <div class="col-video-edit"> |
10 | <div class="form-group"> | 10 | <div class="form-group"> |
11 | <label i18n for="name">Title</label> | 11 | <label i18n for="name">Title</label> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index bc32d7964..c1c7c686d 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss | |||
@@ -1,9 +1,3 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import 'variables'; | 1 | @import 'variables'; |
8 | @import 'mixins'; | 2 | @import 'mixins'; |
9 | 3 | ||
@@ -57,65 +51,62 @@ my-peertube-checkbox { | |||
57 | } | 51 | } |
58 | } | 52 | } |
59 | 53 | ||
60 | .captions { | 54 | .captions-header { |
61 | 55 | text-align: right; | |
62 | .captions-header { | 56 | margin-bottom: 1rem; |
63 | text-align: right; | 57 | } |
64 | margin-bottom: 1rem; | ||
65 | 58 | ||
66 | .create-caption { | 59 | .create-caption { |
67 | @include create-button; | 60 | @include create-button; |
68 | } | 61 | } |
69 | } | ||
70 | 62 | ||
71 | .caption-entry { | 63 | .caption-entry { |
72 | display: flex; | 64 | display: flex; |
73 | height: 40px; | 65 | height: 40px; |
74 | align-items: center; | 66 | align-items: center; |
75 | 67 | ||
76 | a.caption-entry-label { | 68 | a.caption-entry-label { |
77 | @include disable-default-a-behaviour; | 69 | @include disable-default-a-behaviour; |
78 | 70 | ||
79 | flex-grow: 1; | 71 | flex-grow: 1; |
80 | color: #000; | 72 | color: #000; |
81 | 73 | ||
82 | &:hover { | 74 | &:hover { |
83 | opacity: 0.8; | 75 | opacity: 0.8; |
84 | } | ||
85 | } | 76 | } |
77 | } | ||
86 | 78 | ||
87 | .caption-entry-label { | 79 | .caption-entry-label { |
88 | font-size: 15px; | 80 | font-size: 15px; |
89 | font-weight: bold; | 81 | font-weight: bold; |
90 | |||
91 | margin-right: 20px; | ||
92 | width: 150px; | ||
93 | } | ||
94 | 82 | ||
95 | .caption-entry-state { | 83 | margin-right: 20px; |
96 | width: 200px; | 84 | width: 150px; |
85 | } | ||
97 | 86 | ||
98 | &.caption-entry-state-create { | 87 | .caption-entry-state { |
99 | color: #39CC0B; | 88 | width: 200px; |
100 | } | ||
101 | 89 | ||
102 | &.caption-entry-state-delete { | 90 | &.caption-entry-state-create { |
103 | color: #FF0000; | 91 | color: #39CC0B; |
104 | } | ||
105 | } | 92 | } |
106 | 93 | ||
107 | .caption-entry-delete { | 94 | &.caption-entry-state-delete { |
108 | @include peertube-button; | 95 | color: #FF0000; |
109 | @include grey-button; | ||
110 | } | 96 | } |
111 | } | 97 | } |
112 | 98 | ||
113 | .no-caption { | 99 | .caption-entry-delete { |
114 | text-align: center; | 100 | @include peertube-button; |
115 | font-size: 15px; | 101 | @include grey-button; |
116 | } | 102 | } |
117 | } | 103 | } |
118 | 104 | ||
105 | .no-caption { | ||
106 | text-align: center; | ||
107 | font-size: 15px; | ||
108 | } | ||
109 | |||
119 | .submit-container { | 110 | .submit-container { |
120 | text-align: right; | 111 | text-align: right; |
121 | 112 | ||
@@ -143,35 +134,15 @@ p-calendar { | |||
143 | } | 134 | } |
144 | } | 135 | } |
145 | 136 | ||
146 | // columns for the video | 137 | .form-columns { |
147 | .col-video-edit { | 138 | display: grid; |
148 | @include make-col-ready(); | ||
149 | 139 | ||
150 | @include media-breakpoint-up(md) { | 140 | grid-template-columns: 66% 1fr; |
151 | @include make-col(7); | 141 | grid-gap: 30px; |
152 | |||
153 | + .col-video-edit { | ||
154 | @include make-col(5); | ||
155 | } | ||
156 | } | ||
157 | |||
158 | @include media-breakpoint-up(xl) { | ||
159 | @include make-col(8); | ||
160 | |||
161 | + .col-video-edit { | ||
162 | @include make-col(4); | ||
163 | } | ||
164 | } | ||
165 | } | 142 | } |
166 | 143 | ||
167 | :host-context(.expanded) { | 144 | @include on-small-main-col { |
168 | .col-video-edit { | 145 | .form-columns { |
169 | @include media-breakpoint-up(md) { | 146 | grid-template-columns: 1fr; |
170 | @include make-col(8); | ||
171 | |||
172 | + .col-video-edit { | ||
173 | @include make-col(4); | ||
174 | } | ||
175 | } | ||
176 | } | 147 | } |
177 | } | 148 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts new file mode 100644 index 000000000..3392a0d8a --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { objectToFormData } from '@app/helpers' | ||
2 | import { resolveUrl, UploaderX } from 'ngx-uploadx' | ||
3 | |||
4 | /** | ||
5 | * multipart/form-data uploader extending the UploaderX implementation of Google Resumable | ||
6 | * for use with multer | ||
7 | * | ||
8 | * @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts | ||
9 | * @example | ||
10 | * | ||
11 | * options: UploadxOptions = { | ||
12 | * uploaderClass: UploaderXFormData | ||
13 | * }; | ||
14 | */ | ||
15 | export class UploaderXFormData extends UploaderX { | ||
16 | |||
17 | async getFileUrl (): Promise<string> { | ||
18 | const headers = { | ||
19 | 'X-Upload-Content-Length': this.size.toString(), | ||
20 | 'X-Upload-Content-Type': this.file.type || 'application/octet-stream' | ||
21 | } | ||
22 | |||
23 | const previewfile = this.metadata.previewfile as any as File | ||
24 | delete this.metadata.previewfile | ||
25 | |||
26 | const data = objectToFormData(this.metadata) | ||
27 | if (previewfile !== undefined) { | ||
28 | data.append('previewfile', previewfile, previewfile.name) | ||
29 | data.append('thumbnailfile', previewfile, previewfile.name) | ||
30 | } | ||
31 | |||
32 | await this.request({ | ||
33 | method: 'POST', | ||
34 | body: data, | ||
35 | url: this.endpoint, | ||
36 | headers | ||
37 | }) | ||
38 | |||
39 | const location = this.getValueFromResponse('location') | ||
40 | if (!location) { | ||
41 | throw new Error('Invalid or missing Location header') | ||
42 | } | ||
43 | |||
44 | this.offset = this.responseStatus === 201 ? 0 : undefined | ||
45 | |||
46 | return resolveUrl(location, this.endpoint) | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 4c0b09894..86a779f8a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -1,12 +1,17 @@ | |||
1 | <div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)"> | 1 | <div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="onFileDropped($event)"> |
2 | <div class="first-step-block"> | 2 | <div class="first-step-block"> |
3 | <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon> | 3 | <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'"> | 5 | <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'"> |
6 | <span i18n>Select the file to upload</span> | 6 | <span i18n>Select the file to upload</span> |
7 | <input | 7 | <input |
8 | aria-label="Select the file to upload" i18n-aria-label | 8 | aria-label="Select the file to upload" |
9 | #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus | 9 | i18n-aria-label |
10 | #videofileInput | ||
11 | [accept]="videoExtensions" | ||
12 | (change)="onFileChange($event)" | ||
13 | id="videofile" | ||
14 | type="file" | ||
10 | /> | 15 | /> |
11 | </div> | 16 | </div> |
12 | 17 | ||
@@ -41,7 +46,13 @@ | |||
41 | </div> | 46 | </div> |
42 | 47 | ||
43 | <div class="form-group upload-audio-button"> | 48 | <div class="form-group upload-audio-button"> |
44 | <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button> | 49 | <my-button |
50 | className="orange-button" | ||
51 | [label]="getAudioUploadLabel()" | ||
52 | icon="upload" | ||
53 | (click)="uploadAudio()" | ||
54 | > | ||
55 | </my-button> | ||
45 | </div> | 56 | </div> |
46 | </ng-container> | 57 | </ng-container> |
47 | </div> | 58 | </div> |
@@ -64,6 +75,7 @@ | |||
64 | <span>{{ error }}</span> | 75 | <span>{{ error }}</span> |
65 | </div> | 76 | </div> |
66 | </div> | 77 | </div> |
78 | |||
67 | <div class="btn-group" role="group"> | 79 | <div class="btn-group" role="group"> |
68 | <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> | 80 | <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> |
69 | <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> | 81 | <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9549257f6..d9f348a70 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -47,8 +47,4 @@ | |||
47 | 47 | ||
48 | margin-left: 10px; | 48 | margin-left: 10px; |
49 | } | 49 | } |
50 | |||
51 | .btn-group > input:not(:first-child) { | ||
52 | margin-left: 0; | ||
53 | } | ||
54 | } | 50 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index effb37077..2d3fc3578 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -1,15 +1,16 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' | ||
3 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
4 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' | ||
4 | import { UploaderXFormData } from './uploaderx-form-data' | ||
5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' |
6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' | 6 | import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
11 | import { VideoPrivacy } from '@shared/models' | 11 | import { VideoPrivacy } from '@shared/models' |
12 | import { VideoSend } from './video-send' | 12 | import { VideoSend } from './video-send' |
13 | import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' | ||
13 | 14 | ||
14 | @Component({ | 15 | @Component({ |
15 | selector: 'my-video-upload', | 16 | selector: 'my-video-upload', |
@@ -20,23 +21,18 @@ import { VideoSend } from './video-send' | |||
20 | './video-send.scss' | 21 | './video-send.scss' |
21 | ] | 22 | ] |
22 | }) | 23 | }) |
23 | export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { | 24 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate { |
24 | @Output() firstStepDone = new EventEmitter<string>() | 25 | @Output() firstStepDone = new EventEmitter<string>() |
25 | @Output() firstStepError = new EventEmitter<void>() | 26 | @Output() firstStepError = new EventEmitter<void>() |
26 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> | 27 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> |
27 | 28 | ||
28 | // So that it can be accessed in the template | ||
29 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | ||
30 | |||
31 | userVideoQuotaUsed = 0 | 29 | userVideoQuotaUsed = 0 |
32 | userVideoQuotaUsedDaily = 0 | 30 | userVideoQuotaUsedDaily = 0 |
33 | 31 | ||
34 | isUploadingAudioFile = false | 32 | isUploadingAudioFile = false |
35 | isUploadingVideo = false | 33 | isUploadingVideo = false |
36 | isUpdatingVideo = false | ||
37 | 34 | ||
38 | videoUploaded = false | 35 | videoUploaded = false |
39 | videoUploadObservable: Subscription = null | ||
40 | videoUploadPercents = 0 | 36 | videoUploadPercents = 0 |
41 | videoUploadedIds = { | 37 | videoUploadedIds = { |
42 | id: 0, | 38 | id: 0, |
@@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
49 | error: string | 45 | error: string |
50 | enableRetryAfterError: boolean | 46 | enableRetryAfterError: boolean |
51 | 47 | ||
48 | // So that it can be accessed in the template | ||
52 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 49 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
50 | protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable' | ||
51 | |||
52 | private uploadxOptions: UploadxOptions | ||
53 | private isUpdatingVideo = false | ||
54 | private fileToUpload: File | ||
53 | 55 | ||
54 | constructor ( | 56 | constructor ( |
55 | protected formValidatorService: FormValidatorService, | 57 | protected formValidatorService: FormValidatorService, |
@@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
61 | protected videoCaptionService: VideoCaptionService, | 63 | protected videoCaptionService: VideoCaptionService, |
62 | private userService: UserService, | 64 | private userService: UserService, |
63 | private router: Router, | 65 | private router: Router, |
64 | private hooks: HooksService | 66 | private hooks: HooksService, |
65 | ) { | 67 | private resumableUploadService: UploadxService |
68 | ) { | ||
66 | super() | 69 | super() |
70 | |||
71 | this.uploadxOptions = { | ||
72 | endpoint: this.BASE_VIDEO_UPLOAD_URL, | ||
73 | multiple: false, | ||
74 | token: this.authService.getAccessToken(), | ||
75 | uploaderClass: UploaderXFormData, | ||
76 | retryConfig: { | ||
77 | maxAttempts: 6, | ||
78 | shouldRetry: (code: number) => { | ||
79 | return code < 400 || code >= 501 | ||
80 | } | ||
81 | } | ||
82 | } | ||
67 | } | 83 | } |
68 | 84 | ||
69 | get videoExtensions () { | 85 | get videoExtensions () { |
70 | return this.serverConfig.video.file.extensions.join(', ') | 86 | return this.serverConfig.video.file.extensions.join(', ') |
71 | } | 87 | } |
72 | 88 | ||
89 | onUploadVideoOngoing (state: UploadState) { | ||
90 | switch (state.status) { | ||
91 | case 'error': | ||
92 | const error = state.response?.error || 'Unknow error' | ||
93 | |||
94 | this.handleUploadError({ | ||
95 | error: new Error(error), | ||
96 | name: 'HttpErrorResponse', | ||
97 | message: error, | ||
98 | ok: false, | ||
99 | headers: new HttpHeaders(state.responseHeaders), | ||
100 | status: +state.responseStatus, | ||
101 | statusText: error, | ||
102 | type: HttpEventType.Response, | ||
103 | url: state.url | ||
104 | }) | ||
105 | break | ||
106 | |||
107 | case 'cancelled': | ||
108 | this.isUploadingVideo = false | ||
109 | this.videoUploadPercents = 0 | ||
110 | |||
111 | this.firstStepError.emit() | ||
112 | this.enableRetryAfterError = false | ||
113 | this.error = '' | ||
114 | break | ||
115 | |||
116 | case 'queue': | ||
117 | this.closeFirstStep(state.name) | ||
118 | break | ||
119 | |||
120 | case 'uploading': | ||
121 | this.videoUploadPercents = state.progress | ||
122 | break | ||
123 | |||
124 | case 'paused': | ||
125 | this.notifier.info($localize`Upload cancelled`) | ||
126 | break | ||
127 | |||
128 | case 'complete': | ||
129 | this.videoUploaded = true | ||
130 | this.videoUploadPercents = 100 | ||
131 | |||
132 | this.videoUploadedIds = state?.response.video | ||
133 | break | ||
134 | } | ||
135 | } | ||
136 | |||
73 | ngOnInit () { | 137 | ngOnInit () { |
74 | super.ngOnInit() | 138 | super.ngOnInit() |
75 | 139 | ||
@@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
78 | this.userVideoQuotaUsed = data.videoQuotaUsed | 142 | this.userVideoQuotaUsed = data.videoQuotaUsed |
79 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily | 143 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily |
80 | }) | 144 | }) |
145 | |||
146 | this.resumableUploadService.events | ||
147 | .subscribe(state => this.onUploadVideoOngoing(state)) | ||
81 | } | 148 | } |
82 | 149 | ||
83 | ngAfterViewInit () { | 150 | ngAfterViewInit () { |
@@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
85 | } | 152 | } |
86 | 153 | ||
87 | ngOnDestroy () { | 154 | ngOnDestroy () { |
88 | if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() | 155 | this.cancelUpload() |
89 | } | 156 | } |
90 | 157 | ||
91 | canDeactivate () { | 158 | canDeactivate () { |
@@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
105 | } | 172 | } |
106 | } | 173 | } |
107 | 174 | ||
108 | getVideoFile () { | 175 | onFileDropped (files: FileList) { |
109 | return this.videofileInput.nativeElement.files[0] | ||
110 | } | ||
111 | |||
112 | setVideoFile (files: FileList) { | ||
113 | this.videofileInput.nativeElement.files = files | 176 | this.videofileInput.nativeElement.files = files |
114 | this.fileChange() | ||
115 | } | ||
116 | |||
117 | getAudioUploadLabel () { | ||
118 | const videofile = this.getVideoFile() | ||
119 | if (!videofile) return $localize`Upload` | ||
120 | 177 | ||
121 | return $localize`Upload ${videofile.name}` | 178 | this.onFileChange({ target: this.videofileInput.nativeElement }) |
122 | } | 179 | } |
123 | 180 | ||
124 | fileChange () { | 181 | onFileChange (event: Event | { target: HTMLInputElement }) { |
125 | this.uploadFirstStep() | 182 | const file = (event.target as HTMLInputElement).files[0] |
126 | } | ||
127 | |||
128 | retryUpload () { | ||
129 | this.enableRetryAfterError = false | ||
130 | this.error = '' | ||
131 | this.uploadVideo() | ||
132 | } | ||
133 | |||
134 | cancelUpload () { | ||
135 | if (this.videoUploadObservable !== null) { | ||
136 | this.videoUploadObservable.unsubscribe() | ||
137 | } | ||
138 | |||
139 | this.isUploadingVideo = false | ||
140 | this.videoUploadPercents = 0 | ||
141 | this.videoUploadObservable = null | ||
142 | 183 | ||
143 | this.firstStepError.emit() | 184 | if (!file) return |
144 | this.enableRetryAfterError = false | ||
145 | this.error = '' | ||
146 | 185 | ||
147 | this.notifier.info($localize`Upload cancelled`) | 186 | if (!this.checkGlobalUserQuota(file)) return |
148 | } | 187 | if (!this.checkDailyUserQuota(file)) return |
149 | 188 | ||
150 | uploadFirstStep (clickedOnButton = false) { | 189 | if (this.isAudioFile(file.name)) { |
151 | const videofile = this.getVideoFile() | ||
152 | if (!videofile) return | ||
153 | |||
154 | if (!this.checkGlobalUserQuota(videofile)) return | ||
155 | if (!this.checkDailyUserQuota(videofile)) return | ||
156 | |||
157 | if (clickedOnButton === false && this.isAudioFile(videofile.name)) { | ||
158 | this.isUploadingAudioFile = true | 190 | this.isUploadingAudioFile = true |
159 | return | 191 | return |
160 | } | 192 | } |
161 | 193 | ||
162 | // Build name field | ||
163 | const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') | ||
164 | let name: string | ||
165 | |||
166 | // If the name of the file is very small, keep the extension | ||
167 | if (nameWithoutExtension.length < 3) name = videofile.name | ||
168 | else name = nameWithoutExtension | ||
169 | |||
170 | const nsfw = this.serverConfig.instance.isNSFW | ||
171 | const waitTranscoding = true | ||
172 | const commentsEnabled = true | ||
173 | const downloadEnabled = true | ||
174 | const channelId = this.firstStepChannelId.toString() | ||
175 | |||
176 | this.formData = new FormData() | ||
177 | this.formData.append('name', name) | ||
178 | // Put the video "private" -> we are waiting the user validation of the second step | ||
179 | this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) | ||
180 | this.formData.append('nsfw', '' + nsfw) | ||
181 | this.formData.append('commentsEnabled', '' + commentsEnabled) | ||
182 | this.formData.append('downloadEnabled', '' + downloadEnabled) | ||
183 | this.formData.append('waitTranscoding', '' + waitTranscoding) | ||
184 | this.formData.append('channelId', '' + channelId) | ||
185 | this.formData.append('videofile', videofile) | ||
186 | |||
187 | if (this.previewfileUpload) { | ||
188 | this.formData.append('previewfile', this.previewfileUpload) | ||
189 | this.formData.append('thumbnailfile', this.previewfileUpload) | ||
190 | } | ||
191 | |||
192 | this.isUploadingVideo = true | 194 | this.isUploadingVideo = true |
193 | this.firstStepDone.emit(name) | 195 | this.fileToUpload = file |
194 | |||
195 | this.form.patchValue({ | ||
196 | name, | ||
197 | privacy: this.firstStepPrivacyId, | ||
198 | nsfw, | ||
199 | channelId: this.firstStepChannelId, | ||
200 | previewfile: this.previewfileUpload | ||
201 | }) | ||
202 | 196 | ||
203 | this.uploadVideo() | 197 | this.uploadFile(file) |
204 | } | 198 | } |
205 | 199 | ||
206 | uploadVideo () { | 200 | uploadAudio () { |
207 | this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( | 201 | this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) |
208 | event => { | 202 | } |
209 | if (event.type === HttpEventType.UploadProgress) { | ||
210 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) | ||
211 | } else if (event instanceof HttpResponse) { | ||
212 | this.videoUploaded = true | ||
213 | |||
214 | this.videoUploadedIds = event.body.video | ||
215 | |||
216 | this.videoUploadObservable = null | ||
217 | } | ||
218 | }, | ||
219 | 203 | ||
220 | (err: HttpErrorResponse) => { | 204 | retryUpload () { |
221 | // Reset progress (but keep isUploadingVideo true) | 205 | this.enableRetryAfterError = false |
222 | this.videoUploadPercents = 0 | 206 | this.error = '' |
223 | this.videoUploadObservable = null | 207 | this.uploadFile(this.fileToUpload) |
224 | this.enableRetryAfterError = true | 208 | } |
225 | |||
226 | this.error = uploadErrorHandler({ | ||
227 | err, | ||
228 | name: $localize`video`, | ||
229 | notifier: this.notifier, | ||
230 | sticky: false | ||
231 | }) | ||
232 | 209 | ||
233 | if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 || | 210 | cancelUpload () { |
234 | err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { | 211 | this.resumableUploadService.control({ action: 'cancel' }) |
235 | this.cancelUpload() | ||
236 | } | ||
237 | } | ||
238 | ) | ||
239 | } | 212 | } |
240 | 213 | ||
241 | isPublishingButtonDisabled () { | 214 | isPublishingButtonDisabled () { |
@@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
245 | !this.videoUploadedIds.id | 218 | !this.videoUploadedIds.id |
246 | } | 219 | } |
247 | 220 | ||
221 | getAudioUploadLabel () { | ||
222 | const videofile = this.getInputVideoFile() | ||
223 | if (!videofile) return $localize`Upload` | ||
224 | |||
225 | return $localize`Upload ${videofile.name}` | ||
226 | } | ||
227 | |||
248 | updateSecondStep () { | 228 | updateSecondStep () { |
249 | if (this.isPublishingButtonDisabled() || !this.checkForm()) { | 229 | if (this.isPublishingButtonDisabled() || !this.checkForm()) { |
250 | return | 230 | return |
@@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
275 | ) | 255 | ) |
276 | } | 256 | } |
277 | 257 | ||
258 | private getInputVideoFile () { | ||
259 | return this.videofileInput.nativeElement.files[0] | ||
260 | } | ||
261 | |||
262 | private uploadFile (file: File, previewfile?: File) { | ||
263 | const metadata = { | ||
264 | waitTranscoding: true, | ||
265 | commentsEnabled: true, | ||
266 | downloadEnabled: true, | ||
267 | channelId: this.firstStepChannelId, | ||
268 | nsfw: this.serverConfig.instance.isNSFW, | ||
269 | privacy: VideoPrivacy.PRIVATE.toString(), | ||
270 | filename: file.name, | ||
271 | previewfile: previewfile as any | ||
272 | } | ||
273 | |||
274 | this.resumableUploadService.handleFiles(file, { | ||
275 | ...this.uploadxOptions, | ||
276 | metadata | ||
277 | }) | ||
278 | |||
279 | this.isUploadingVideo = true | ||
280 | } | ||
281 | |||
282 | private handleUploadError (err: HttpErrorResponse) { | ||
283 | // Reset progress (but keep isUploadingVideo true) | ||
284 | this.videoUploadPercents = 0 | ||
285 | this.enableRetryAfterError = true | ||
286 | |||
287 | this.error = genericUploadErrorHandler({ | ||
288 | err, | ||
289 | name: $localize`video`, | ||
290 | notifier: this.notifier, | ||
291 | sticky: false | ||
292 | }) | ||
293 | |||
294 | if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { | ||
295 | this.cancelUpload() | ||
296 | } | ||
297 | } | ||
298 | |||
299 | private closeFirstStep (filename: string) { | ||
300 | const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '') | ||
301 | const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension | ||
302 | |||
303 | this.form.patchValue({ | ||
304 | name, | ||
305 | privacy: this.firstStepPrivacyId, | ||
306 | nsfw: this.serverConfig.instance.isNSFW, | ||
307 | channelId: this.firstStepChannelId, | ||
308 | previewfile: this.previewfileUpload | ||
309 | }) | ||
310 | |||
311 | this.firstStepDone.emit(name) | ||
312 | } | ||
313 | |||
278 | private checkGlobalUserQuota (videofile: File) { | 314 | private checkGlobalUserQuota (videofile: File) { |
279 | const bytePipes = new BytesPipe() | 315 | const bytePipes = new BytesPipe() |
280 | 316 | ||
@@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView | |||
285 | const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) | 321 | const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) |
286 | const videoQuotaBytes = bytePipes.transform(videoQuota, 0) | 322 | const videoQuotaBytes = bytePipes.transform(videoQuota, 0) |
287 | 323 | ||
288 | const msg = $localize`Your video quota is exceeded with this video ( | 324 | const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` |
289 | video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` | ||
290 | this.notifier.error(msg) | 325 | this.notifier.error(msg) |
291 | 326 | ||
292 | return false | 327 | return false |
@@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota | |||
304 | const videoSizeBytes = bytePipes.transform(videofile.size, 0) | 339 | const videoSizeBytes = bytePipes.transform(videofile.size, 0) |
305 | const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) | 340 | const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) |
306 | const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) | 341 | const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) |
307 | 342 | const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` | |
308 | const msg = $localize`Your daily video quota is exceeded with this video ( | ||
309 | video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` | ||
310 | this.notifier.error(msg) | 343 | this.notifier.error(msg) |
311 | 344 | ||
312 | return false | 345 | return false |
diff --git a/client/src/app/+videos/+video-edit/video-add.module.ts b/client/src/app/+videos/+video-edit/video-add.module.ts index da651119b..e836cf81e 100644 --- a/client/src/app/+videos/+video-edit/video-add.module.ts +++ b/client/src/app/+videos/+video-edit/video-add.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { CanDeactivateGuard } from '@app/core' | 2 | import { CanDeactivateGuard } from '@app/core' |
3 | import { UploadxModule } from 'ngx-uploadx' | ||
3 | import { VideoEditModule } from './shared/video-edit.module' | 4 | import { VideoEditModule } from './shared/video-edit.module' |
4 | import { DragDropDirective } from './video-add-components/drag-drop.directive' | 5 | import { DragDropDirective } from './video-add-components/drag-drop.directive' |
5 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' | 6 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' |
@@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component' | |||
13 | imports: [ | 14 | imports: [ |
14 | VideoAddRoutingModule, | 15 | VideoAddRoutingModule, |
15 | 16 | ||
16 | VideoEditModule | 17 | VideoEditModule, |
18 | |||
19 | UploadxModule | ||
17 | ], | 20 | ], |
18 | 21 | ||
19 | declarations: [ | 22 | declarations: [ |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 276548b79..9172b78a8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs' | |||
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | 4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' |
5 | import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | ||
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | ||
6 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
7 | 9 | ||
8 | @Injectable() | 10 | @Injectable() |
@@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
10 | constructor ( | 12 | constructor ( |
11 | private videoService: VideoService, | 13 | private videoService: VideoService, |
12 | private liveVideoService: LiveVideoService, | 14 | private liveVideoService: LiveVideoService, |
13 | private videoChannelService: VideoChannelService, | 15 | private authService: AuthService, |
14 | private videoCaptionService: VideoCaptionService | 16 | private videoCaptionService: VideoCaptionService |
15 | ) { | 17 | ) { |
16 | } | 18 | } |
@@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
31 | .loadCompleteDescription(video.descriptionPath) | 33 | .loadCompleteDescription(video.descriptionPath) |
32 | .pipe(map(description => Object.assign(video, { description }))), | 34 | .pipe(map(description => Object.assign(video, { description }))), |
33 | 35 | ||
34 | this.videoChannelService | 36 | listUserChannels(this.authService), |
35 | .listAccountVideoChannels(video.account) | ||
36 | .pipe( | ||
37 | map(result => result.data), | ||
38 | map(videoChannels => videoChannels.map(c => ({ | ||
39 | id: c.id, | ||
40 | label: c.displayName, | ||
41 | support: c.support, | ||
42 | avatarPath: c.avatar?.path | ||
43 | }))) | ||
44 | ), | ||
45 | 37 | ||
46 | this.videoCaptionService | 38 | this.videoCaptionService |
47 | .listCaptions(video.id) | 39 | .listCaptions(video.id) |
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index e21ada0f1..0543564b4 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -40,8 +40,10 @@ | |||
40 | } | 40 | } |
41 | 41 | ||
42 | .icon-menu { | 42 | .icon-menu { |
43 | background-color: pvar(--mainForegroundColor); | ||
44 | mask-image: url('../assets/images/misc/menu.svg'); | 43 | mask-image: url('../assets/images/misc/menu.svg'); |
44 | -webkit-mask-image: url('../assets/images/misc/menu.svg'); | ||
45 | |||
46 | background-color: pvar(--mainForegroundColor); | ||
45 | margin: 0 18px 0 20px; | 47 | margin: 0 18px 0 20px; |
46 | 48 | ||
47 | @media screen and (max-width: $mobile-view) { | 49 | @media screen and (max-width: $mobile-view) { |
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index a1747af3c..94f6def26 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { map } from 'rxjs/operators' | 1 | import { first, map } from 'rxjs/operators' |
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 2 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
3 | import { DatePipe } from '@angular/common' | 3 | import { DatePipe } from '@angular/common' |
4 | import { HttpErrorResponse } from '@angular/common/http' | 4 | import { HttpErrorResponse } from '@angular/common/http' |
@@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) { | |||
23 | 23 | ||
24 | function listUserChannels (authService: AuthService) { | 24 | function listUserChannels (authService: AuthService) { |
25 | return authService.userInformationLoaded | 25 | return authService.userInformationLoaded |
26 | .pipe(map(() => { | 26 | .pipe( |
27 | const user = authService.getUser() | 27 | first(), |
28 | if (!user) return undefined | 28 | map(() => { |
29 | 29 | const user = authService.getUser() | |
30 | const videoChannels = user.videoChannels | 30 | if (!user) return undefined |
31 | if (Array.isArray(videoChannels) === false) return undefined | 31 | |
32 | 32 | const videoChannels = user.videoChannels | |
33 | return videoChannels.map(c => ({ | 33 | if (Array.isArray(videoChannels) === false) return undefined |
34 | id: c.id, | 34 | |
35 | label: c.displayName, | 35 | return videoChannels |
36 | support: c.support, | 36 | .sort((a, b) => { |
37 | avatarPath: c.avatar?.path | 37 | if (a.updatedAt < b.updatedAt) return 1 |
38 | }) as SelectChannelItem) | 38 | if (a.updatedAt > b.updatedAt) return -1 |
39 | })) | 39 | return 0 |
40 | }) | ||
41 | .map(c => ({ | ||
42 | id: c.id, | ||
43 | label: c.displayName, | ||
44 | support: c.support, | ||
45 | avatarPath: c.avatar?.path | ||
46 | }) as SelectChannelItem) | ||
47 | }) | ||
48 | ) | ||
40 | } | 49 | } |
41 | 50 | ||
42 | function getAbsoluteAPIUrl () { | 51 | function getAbsoluteAPIUrl () { |
@@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | |||
167 | ) | 176 | ) |
168 | } | 177 | } |
169 | 178 | ||
170 | function uploadErrorHandler (parameters: { | 179 | function genericUploadErrorHandler (parameters: { |
171 | err: HttpErrorResponse | 180 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> |
172 | name: string | 181 | name: string |
173 | notifier: Notifier | 182 | notifier: Notifier |
174 | sticky?: boolean | 183 | sticky?: boolean |
@@ -180,6 +189,9 @@ function uploadErrorHandler (parameters: { | |||
180 | if (err instanceof ErrorEvent) { // network error | 189 | if (err instanceof ErrorEvent) { // network error |
181 | message = $localize`The connection was interrupted` | 190 | message = $localize`The connection was interrupted` |
182 | notifier.error(message, title, null, sticky) | 191 | notifier.error(message, title, null, sticky) |
192 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
193 | message = $localize`The server encountered an error` | ||
194 | notifier.error(message, title, null, sticky) | ||
183 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | 195 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { |
184 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | 196 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` |
185 | notifier.error(message, title, null, sticky) | 197 | notifier.error(message, title, null, sticky) |
@@ -210,5 +222,5 @@ export { | |||
210 | isInViewport, | 222 | isInViewport, |
211 | isXPercentInViewport, | 223 | isXPercentInViewport, |
212 | listUserChannels, | 224 | listUserChannels, |
213 | uploadErrorHandler | 225 | genericUploadErrorHandler |
214 | } | 226 | } |
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index 6d9f0ee65..7b5611f35 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -4,8 +4,12 @@ import { Actor } from './actor.model' | |||
4 | export class Account extends Actor implements ServerAccount { | 4 | export class Account extends Actor implements ServerAccount { |
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | |||
8 | updatedAt: Date | string | ||
9 | |||
7 | nameWithHost: string | 10 | nameWithHost: string |
8 | nameWithHostForced: string | 11 | nameWithHostForced: string |
12 | |||
9 | mutedByUser: boolean | 13 | mutedByUser: boolean |
10 | mutedByInstance: boolean | 14 | mutedByInstance: boolean |
11 | mutedServerByUser: boolean | 15 | mutedServerByUser: boolean |
@@ -30,6 +34,8 @@ export class Account extends Actor implements ServerAccount { | |||
30 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 34 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
31 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 35 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
32 | 36 | ||
37 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
38 | |||
33 | this.mutedByUser = false | 39 | this.mutedByUser = false |
34 | this.mutedByInstance = false | 40 | this.mutedByInstance = false |
35 | this.mutedServerByUser = false | 41 | this.mutedServerByUser = false |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 6ba0bb09e..2fccc472a 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -12,7 +12,6 @@ export abstract class Actor implements ServerActor { | |||
12 | followersCount: number | 12 | followersCount: number |
13 | 13 | ||
14 | createdAt: Date | string | 14 | createdAt: Date | string |
15 | updatedAt: Date | string | ||
16 | 15 | ||
17 | avatar: ActorImage | 16 | avatar: ActorImage |
18 | 17 | ||
@@ -55,7 +54,6 @@ export abstract class Actor implements ServerActor { | |||
55 | this.followersCount = hash.followersCount | 54 | this.followersCount = hash.followersCount |
56 | 55 | ||
57 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) | 56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
58 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
59 | 57 | ||
60 | this.avatar = hash.avatar | 58 | this.avatar = hash.avatar |
61 | this.isLocal = Actor.IS_LOCAL(this.host) | 59 | this.isLocal = Actor.IS_LOCAL(this.host) |
diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss index 09b5f95d7..22b24c853 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.scss +++ b/client/src/app/shared/shared-main/buttons/button.component.scss | |||
@@ -30,7 +30,7 @@ span[class$=-button] { | |||
30 | 30 | ||
31 | .action-button { | 31 | .action-button { |
32 | @include peertube-button-link; | 32 | @include peertube-button-link; |
33 | @include button-with-icon(21px, 0, -1px); | 33 | @include button-with-icon(21px); |
34 | } | 34 | } |
35 | 35 | ||
36 | .orange-button { | 36 | .orange-button { |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index c40dd5311..a9dcf2fa2 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts | |||
@@ -16,6 +16,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
16 | banner: ActorImage | 16 | banner: ActorImage |
17 | bannerUrl: string | 17 | bannerUrl: string |
18 | 18 | ||
19 | updatedAt: Date | string | ||
20 | |||
19 | ownerAccount?: ServerAccount | 21 | ownerAccount?: ServerAccount |
20 | ownerBy?: string | 22 | ownerBy?: string |
21 | 23 | ||
@@ -59,6 +61,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
59 | 61 | ||
60 | this.videosCount = hash.videosCount | 62 | this.videosCount = hash.videosCount |
61 | 63 | ||
64 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
65 | |||
62 | if (hash.viewsPerDay) { | 66 | if (hash.viewsPerDay) { |
63 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) | 67 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) |
64 | } | 68 | } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index e65261763..a89f1065a 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -40,23 +40,24 @@ export class VideoChannelService { | |||
40 | ) | 40 | ) |
41 | } | 41 | } |
42 | 42 | ||
43 | listAccountVideoChannels ( | 43 | listAccountVideoChannels (options: { |
44 | account: Account, | 44 | account: Account |
45 | componentPagination?: ComponentPaginationLight, | 45 | componentPagination?: ComponentPaginationLight |
46 | withStats = false, | 46 | withStats?: boolean |
47 | sort?: string | ||
47 | search?: string | 48 | search?: string |
48 | ): Observable<ResultList<VideoChannel>> { | 49 | }): Observable<ResultList<VideoChannel>> { |
50 | const { account, componentPagination, withStats = false, sort, search } = options | ||
51 | |||
49 | const pagination = componentPagination | 52 | const pagination = componentPagination |
50 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 53 | ? this.restService.componentPaginationToRestPagination(componentPagination) |
51 | : { start: 0, count: 20 } | 54 | : { start: 0, count: 20 } |
52 | 55 | ||
53 | let params = new HttpParams() | 56 | let params = new HttpParams() |
54 | params = this.restService.addRestGetParams(params, pagination) | 57 | params = this.restService.addRestGetParams(params, pagination, sort) |
55 | params = params.set('withStats', withStats + '') | 58 | params = params.set('withStats', withStats + '') |
56 | 59 | ||
57 | if (search) { | 60 | if (search) params = params.set('search', search) |
58 | params = params.set('search', search) | ||
59 | } | ||
60 | 61 | ||
61 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | 62 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' |
62 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 63 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index 0e3924841..2c83f53b6 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { BooleanBothQuery, SearchTargetType } from '@shared/models' | 1 | import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' |
2 | 2 | ||
3 | export class AdvancedSearch { | 3 | export class AdvancedSearch { |
4 | startDate: string // ISO 8601 | 4 | startDate: string // ISO 8601 |
@@ -21,6 +21,8 @@ export class AdvancedSearch { | |||
21 | durationMin: number // seconds | 21 | durationMin: number // seconds |
22 | durationMax: number // seconds | 22 | durationMax: number // seconds |
23 | 23 | ||
24 | isLive: BooleanQuery | ||
25 | |||
24 | sort: string | 26 | sort: string |
25 | 27 | ||
26 | searchTarget: SearchTargetType | 28 | searchTarget: SearchTargetType |
@@ -41,6 +43,8 @@ export class AdvancedSearch { | |||
41 | tagsOneOf?: any | 43 | tagsOneOf?: any |
42 | tagsAllOf?: any | 44 | tagsAllOf?: any |
43 | 45 | ||
46 | isLive?: BooleanQuery | ||
47 | |||
44 | durationMin?: string | 48 | durationMin?: string |
45 | durationMax?: string | 49 | durationMax?: string |
46 | sort?: string | 50 | sort?: string |
@@ -54,6 +58,8 @@ export class AdvancedSearch { | |||
54 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined | 58 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined |
55 | 59 | ||
56 | this.nsfw = options.nsfw || undefined | 60 | this.nsfw = options.nsfw || undefined |
61 | this.isLive = options.isLive || undefined | ||
62 | |||
57 | this.categoryOneOf = options.categoryOneOf || undefined | 63 | this.categoryOneOf = options.categoryOneOf || undefined |
58 | this.licenceOneOf = options.licenceOneOf || undefined | 64 | this.licenceOneOf = options.licenceOneOf || undefined |
59 | this.languageOneOf = options.languageOneOf || undefined | 65 | this.languageOneOf = options.languageOneOf || undefined |
@@ -94,6 +100,7 @@ export class AdvancedSearch { | |||
94 | this.tagsAllOf = undefined | 100 | this.tagsAllOf = undefined |
95 | this.durationMin = undefined | 101 | this.durationMin = undefined |
96 | this.durationMax = undefined | 102 | this.durationMax = undefined |
103 | this.isLive = undefined | ||
97 | 104 | ||
98 | this.sort = '-match' | 105 | this.sort = '-match' |
99 | } | 106 | } |
@@ -112,12 +119,16 @@ export class AdvancedSearch { | |||
112 | tagsAllOf: this.tagsAllOf, | 119 | tagsAllOf: this.tagsAllOf, |
113 | durationMin: this.durationMin, | 120 | durationMin: this.durationMin, |
114 | durationMax: this.durationMax, | 121 | durationMax: this.durationMax, |
122 | isLive: this.isLive, | ||
115 | sort: this.sort, | 123 | sort: this.sort, |
116 | searchTarget: this.searchTarget | 124 | searchTarget: this.searchTarget |
117 | } | 125 | } |
118 | } | 126 | } |
119 | 127 | ||
120 | toAPIObject () { | 128 | toAPIObject (): VideosSearchQuery { |
129 | let isLive: boolean | ||
130 | if (this.isLive) isLive = this.isLive === 'true' | ||
131 | |||
121 | return { | 132 | return { |
122 | startDate: this.startDate, | 133 | startDate: this.startDate, |
123 | endDate: this.endDate, | 134 | endDate: this.endDate, |
@@ -131,6 +142,7 @@ export class AdvancedSearch { | |||
131 | tagsAllOf: this.tagsAllOf, | 142 | tagsAllOf: this.tagsAllOf, |
132 | durationMin: this.durationMin, | 143 | durationMin: this.durationMin, |
133 | durationMax: this.durationMax, | 144 | durationMax: this.durationMax, |
145 | isLive, | ||
134 | sort: this.sort, | 146 | sort: this.sort, |
135 | searchTarget: this.searchTarget | 147 | searchTarget: this.searchTarget |
136 | } | 148 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 5df89d019..0bbdff1e6 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -95,6 +95,7 @@ my-actor-avatar { | |||
95 | .video-bottom { | 95 | .video-bottom { |
96 | display: flex; | 96 | display: flex; |
97 | width: 100%; | 97 | width: 100%; |
98 | min-width: 1px; | ||
98 | } | 99 | } |
99 | 100 | ||
100 | .video-miniature-name { | 101 | .video-miniature-name { |
@@ -145,6 +146,7 @@ my-actor-avatar { | |||
145 | 146 | ||
146 | .video-bottom { | 147 | .video-bottom { |
147 | display: flex; | 148 | display: flex; |
149 | min-width: 1px; | ||
148 | } | 150 | } |
149 | 151 | ||
150 | // We don't display avatar in row mode | 152 | // We don't display avatar in row mode |
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 89b6f0c4c..ae511aa02 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -402,7 +402,26 @@ ngx-loading-bar { | |||
402 | } | 402 | } |
403 | 403 | ||
404 | .admin-sub-header { | 404 | .admin-sub-header { |
405 | @include admin-sub-header-responsive; | 405 | flex-direction: column; |
406 | |||
407 | .form-sub-title { | ||
408 | margin-right: 0 !important; | ||
409 | margin-bottom: 10px; | ||
410 | text-align: center; | ||
411 | } | ||
412 | |||
413 | .admin-sub-nav { | ||
414 | display: block; | ||
415 | overflow-x: auto; | ||
416 | white-space: nowrap; | ||
417 | height: 50px; | ||
418 | padding: 10px 0; | ||
419 | width: 100%; | ||
420 | |||
421 | a { | ||
422 | margin-left: 5px; | ||
423 | } | ||
424 | } | ||
406 | } | 425 | } |
407 | 426 | ||
408 | my-markdown-textarea { | 427 | my-markdown-textarea { |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index b2083e516..06e55532a 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -336,20 +336,6 @@ | |||
336 | cursor: pointer; | 336 | cursor: pointer; |
337 | } | 337 | } |
338 | 338 | ||
339 | @mixin select-arrow-down { | ||
340 | top: 50%; | ||
341 | right: calc(0% + 15px); | ||
342 | content: ' '; | ||
343 | height: 0; | ||
344 | width: 0; | ||
345 | position: absolute; | ||
346 | pointer-events: none; | ||
347 | border: 5px solid rgba(0, 0, 0, 0); | ||
348 | border-top-color: #000; | ||
349 | margin-top: -2px; | ||
350 | z-index: 100; | ||
351 | } | ||
352 | |||
353 | @mixin responsive-width ($width) { | 339 | @mixin responsive-width ($width) { |
354 | width: $width; | 340 | width: $width; |
355 | 341 | ||
@@ -381,7 +367,17 @@ | |||
381 | } | 367 | } |
382 | 368 | ||
383 | &::after { | 369 | &::after { |
384 | @include select-arrow-down; | 370 | top: 50%; |
371 | right: calc(0% + 15px); | ||
372 | content: ' '; | ||
373 | height: 0; | ||
374 | width: 0; | ||
375 | position: absolute; | ||
376 | pointer-events: none; | ||
377 | border: 5px solid rgba(0, 0, 0, 0); | ||
378 | border-top-color: #000; | ||
379 | margin-top: -2px; | ||
380 | z-index: 100; | ||
385 | } | 381 | } |
386 | 382 | ||
387 | select { | 383 | select { |
@@ -849,29 +845,6 @@ | |||
849 | } | 845 | } |
850 | } | 846 | } |
851 | 847 | ||
852 | @mixin admin-sub-header-responsive { | ||
853 | flex-direction: column; | ||
854 | |||
855 | .form-sub-title { | ||
856 | margin-right: 0 !important; | ||
857 | margin-bottom: 10px; | ||
858 | text-align: center; | ||
859 | } | ||
860 | |||
861 | .admin-sub-nav { | ||
862 | display: block; | ||
863 | overflow-x: auto; | ||
864 | white-space: nowrap; | ||
865 | height: 50px; | ||
866 | padding: 10px 0; | ||
867 | width: 100%; | ||
868 | |||
869 | a { | ||
870 | margin-left: 5px; | ||
871 | } | ||
872 | } | ||
873 | } | ||
874 | |||
875 | // applies ratio (default to 16:9) to a child element (using $selector) only using | 848 | // applies ratio (default to 16:9) to a child element (using $selector) only using |
876 | // an immediate's parent size. This allows to set a ratio without explicit | 849 | // an immediate's parent size. This allows to set a ratio without explicit |
877 | // dimensions, as width/height cannot be computed from each other. | 850 | // dimensions, as width/height cannot be computed from each other. |
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index 45cee3e77..1738f486d 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss | |||
@@ -47,6 +47,7 @@ $context-menu-width: 350px; | |||
47 | @each $icon in $icons { | 47 | @each $icon in $icons { |
48 | &[class$="-#{$icon}"] { | 48 | &[class$="-#{$icon}"] { |
49 | mask-image: url('#{$assets-path}/player/images/#{$icon}.svg'); | 49 | mask-image: url('#{$assets-path}/player/images/#{$icon}.svg'); |
50 | -webkit-mask-image: url('#{$assets-path}/player/images/#{$icon}.svg'); | ||
50 | } | 51 | } |
51 | } | 52 | } |
52 | 53 | ||
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index 8fe2e054d..c010f7297 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -346,6 +346,8 @@ body { | |||
346 | &.icon-next, | 346 | &.icon-next, |
347 | &.icon-previous { | 347 | &.icon-previous { |
348 | mask-image: url('#{$assets-path}/player/images/next.svg'); | 348 | mask-image: url('#{$assets-path}/player/images/next.svg'); |
349 | -webkit-mask-image: url('#{$assets-path}/player/images/next.svg'); | ||
350 | |||
349 | background-color: #fff; | 351 | background-color: #fff; |
350 | mask-size: cover; | 352 | mask-size: cover; |
351 | width: 11px; | 353 | width: 11px; |
diff --git a/client/src/sass/player/playlist.scss b/client/src/sass/player/playlist.scss index 8558fc837..3279bd263 100644 --- a/client/src/sass/player/playlist.scss +++ b/client/src/sass/player/playlist.scss | |||
@@ -40,10 +40,12 @@ $playlist-menu-width: 350px; | |||
40 | } | 40 | } |
41 | 41 | ||
42 | .cross { | 42 | .cross { |
43 | mask-image: url('#{$assets-path}/images/feather/x.svg'); | ||
44 | -webkit-mask-image: url('#{$assets-path}/images/feather/x.svg'); | ||
45 | |||
43 | cursor: pointer; | 46 | cursor: pointer; |
44 | width: 20px; | 47 | width: 20px; |
45 | height: 20px; | 48 | height: 20px; |
46 | mask-image: url('#{$assets-path}/images/feather/x.svg'); | ||
47 | background-color: #fff; | 49 | background-color: #fff; |
48 | mask-size: cover; | 50 | mask-size: cover; |
49 | } | 51 | } |
@@ -85,9 +87,11 @@ $playlist-menu-width: 350px; | |||
85 | } | 87 | } |
86 | 88 | ||
87 | .vjs-playlist-icon { | 89 | .vjs-playlist-icon { |
90 | mask-image: url('#{$assets-path}/images/feather/list.svg'); | ||
91 | -webkit-mask-image: url('#{$assets-path}/images/feather/list.svg'); | ||
92 | |||
88 | width: 22px; | 93 | width: 22px; |
89 | height: 22px; | 94 | height: 22px; |
90 | mask-image: url('#{$assets-path}/images/feather/list.svg'); | ||
91 | background-color: #fff; | 95 | background-color: #fff; |
92 | mask-size: cover; | 96 | mask-size: cover; |
93 | margin-bottom: 3px; | 97 | margin-bottom: 3px; |
diff --git a/client/yarn.lock b/client/yarn.lock index 571314f22..1b1455cc8 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -7793,6 +7793,13 @@ next-tick@~1.0.0: | |||
7793 | resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" | 7793 | resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" |
7794 | integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= | 7794 | integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= |
7795 | 7795 | ||
7796 | ngx-uploadx@^4.1.0: | ||
7797 | version "4.1.0" | ||
7798 | resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-4.1.0.tgz#b3ed4566a2505239026bbdc10c2345aae28d67df" | ||
7799 | integrity sha512-KCG0NT4SBc/5MRl8aR6joHHg+WeTdrkhLeC1DrNgVxrTBuuenlEwOVDpkLJMPX/8HE6Bq33rx1U2NNZYVl9NMQ== | ||
7800 | dependencies: | ||
7801 | tslib "^1.9.0" | ||
7802 | |||
7796 | nice-try@^1.0.4: | 7803 | nice-try@^1.0.4: |
7797 | version "1.0.5" | 7804 | version "1.0.5" |
7798 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" | 7805 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" |
diff --git a/engines.yaml b/engines.yaml new file mode 100644 index 000000000..5a68ca4ba --- /dev/null +++ b/engines.yaml | |||
@@ -0,0 +1,5 @@ | |||
1 | node: ">=12.x" | ||
2 | yarn: ">=1.x" | ||
3 | postgres: ">=10.x" | ||
4 | redis-server: ">=2.8.18" | ||
5 | ffmpeg: ">=4.1" | ||
diff --git a/package.json b/package.json index 2c4c478ac..d3375c7d4 100644 --- a/package.json +++ b/package.json | |||
@@ -5,11 +5,8 @@ | |||
5 | "private": true, | 5 | "private": true, |
6 | "licence": "AGPL-3.0", | 6 | "licence": "AGPL-3.0", |
7 | "engines": { | 7 | "engines": { |
8 | "node": ">=10.x", | 8 | "node": ">=12.x", |
9 | "yarn": ">=1.x", | 9 | "yarn": ">=1.x" |
10 | "postgres": ">=10.x", | ||
11 | "redis-server": ">=2.8.18", | ||
12 | "ffmpeg": ">=4.1" | ||
13 | }, | 10 | }, |
14 | "bin": { | 11 | "bin": { |
15 | "peertube": "dist/server/tools/peertube.js" | 12 | "peertube": "dist/server/tools/peertube.js" |
@@ -76,6 +73,7 @@ | |||
76 | "swagger-cli": "swagger-cli" | 73 | "swagger-cli": "swagger-cli" |
77 | }, | 74 | }, |
78 | "dependencies": { | 75 | "dependencies": { |
76 | "@uploadx/core": "^4.4.0", | ||
79 | "apicache": "1.6.2", | 77 | "apicache": "1.6.2", |
80 | "async": "^3.0.1", | 78 | "async": "^3.0.1", |
81 | "async-lru": "^1.1.1", | 79 | "async-lru": "^1.1.1", |
@@ -99,7 +97,7 @@ | |||
99 | "express-validator": "^6.4.0", | 97 | "express-validator": "^6.4.0", |
100 | "flat": "^5.0.0", | 98 | "flat": "^5.0.0", |
101 | "fluent-ffmpeg": "^2.1.0", | 99 | "fluent-ffmpeg": "^2.1.0", |
102 | "fs-extra": "^9.0.0", | 100 | "fs-extra": "^10.0.0", |
103 | "got": "^11.8.2", | 101 | "got": "^11.8.2", |
104 | "helmet": "^4.1.0", | 102 | "helmet": "^4.1.0", |
105 | "http-signature": "1.3.5", | 103 | "http-signature": "1.3.5", |
@@ -113,7 +111,7 @@ | |||
113 | "lodash": "^4.17.10", | 111 | "lodash": "^4.17.10", |
114 | "lru-cache": "^6.0.0", | 112 | "lru-cache": "^6.0.0", |
115 | "magnet-uri": "^6.1.0", | 113 | "magnet-uri": "^6.1.0", |
116 | "markdown-it": "12.0.4", | 114 | "markdown-it": "^12.0.4", |
117 | "markdown-it-emoji": "^2.0.0", | 115 | "markdown-it-emoji": "^2.0.0", |
118 | "memoizee": "^0.4.14", | 116 | "memoizee": "^0.4.14", |
119 | "morgan": "^1.5.3", | 117 | "morgan": "^1.5.3", |
@@ -133,7 +131,7 @@ | |||
133 | "sanitize-html": "2.x", | 131 | "sanitize-html": "2.x", |
134 | "sequelize": "6.6.2", | 132 | "sequelize": "6.6.2", |
135 | "sequelize-typescript": "^2.0.0-beta.1", | 133 | "sequelize-typescript": "^2.0.0-beta.1", |
136 | "sitemap": "^6.1.0", | 134 | "sitemap": "^7.0.0", |
137 | "socket.io": "^4.0.1", | 135 | "socket.io": "^4.0.1", |
138 | "sql-formatter": "^4.0.0-beta.0", | 136 | "sql-formatter": "^4.0.0-beta.0", |
139 | "srt-to-vtt": "^1.1.2", | 137 | "srt-to-vtt": "^1.1.2", |
@@ -143,7 +141,7 @@ | |||
143 | "uuid": "^8.1.0", | 141 | "uuid": "^8.1.0", |
144 | "validator": "^13.0.0", | 142 | "validator": "^13.0.0", |
145 | "webfinger.js": "^2.6.6", | 143 | "webfinger.js": "^2.6.6", |
146 | "webtorrent": "^0.116.1", | 144 | "webtorrent": "^0.118.0", |
147 | "winston": "3.3.3", | 145 | "winston": "3.3.3", |
148 | "ws": "^7.0.0", | 146 | "ws": "^7.0.0", |
149 | "youtube-dl": "^3.0.2" | 147 | "youtube-dl": "^3.0.2" |
@@ -154,9 +152,9 @@ | |||
154 | "@types/async": "^3.0.0", | 152 | "@types/async": "^3.0.0", |
155 | "@types/async-lock": "^1.1.0", | 153 | "@types/async-lock": "^1.1.0", |
156 | "@types/bcrypt": "^3.0.0", | 154 | "@types/bcrypt": "^3.0.0", |
157 | "@types/bluebird": "3.5.33", | 155 | "@types/bluebird": "^3.5.33", |
158 | "@types/body-parser": "^1.16.3", | 156 | "@types/body-parser": "^1.16.3", |
159 | "@types/bull": "3.15.0", | 157 | "@types/bull": "^3.15.0", |
160 | "@types/bytes": "^3.0.0", | 158 | "@types/bytes": "^3.0.0", |
161 | "@types/chai": "^4.0.4", | 159 | "@types/chai": "^4.0.4", |
162 | "@types/chai-json-schema": "^1.4.3", | 160 | "@types/chai-json-schema": "^1.4.3", |
diff --git a/scripts/ci.sh b/scripts/ci.sh index a0de62d91..f4a200a00 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh | |||
@@ -44,7 +44,7 @@ if [ "$1" = "misc" ]; then | |||
44 | pluginsFiles=$(findTestFiles server/tests/plugins) | 44 | pluginsFiles=$(findTestFiles server/tests/plugins) |
45 | miscFiles="server/tests/client.ts server/tests/misc-endpoints.ts" | 45 | miscFiles="server/tests/client.ts server/tests/misc-endpoints.ts" |
46 | 46 | ||
47 | TS_NODE_FILES=true runTest "$1" 1 $feedsFiles $helperFiles $pluginsFiles $miscFiles | 47 | MOCHA_PARALLEL=true TS_NODE_FILES=true runTest "$1" 2 $feedsFiles $helperFiles $pluginsFiles $miscFiles |
48 | elif [ "$1" = "cli" ]; then | 48 | elif [ "$1" = "cli" ]; then |
49 | npm run build:server | 49 | npm run build:server |
50 | npm run setup:cli | 50 | npm run setup:cli |
@@ -116,6 +116,7 @@ import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-upd | |||
116 | import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' | 116 | import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' |
117 | import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler' | 117 | import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler' |
118 | import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances' | 118 | import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances' |
119 | import { RemoveDanglingResumableUploadsScheduler } from './server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' | ||
119 | import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' | 120 | import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' |
120 | import { PeerTubeSocket } from './server/lib/peertube-socket' | 121 | import { PeerTubeSocket } from './server/lib/peertube-socket' |
121 | import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' | 122 | import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' |
@@ -280,6 +281,7 @@ async function startApplication () { | |||
280 | PluginsCheckScheduler.Instance.enable() | 281 | PluginsCheckScheduler.Instance.enable() |
281 | PeerTubeVersionCheckScheduler.Instance.enable() | 282 | PeerTubeVersionCheckScheduler.Instance.enable() |
282 | AutoFollowIndexInstances.Instance.enable() | 283 | AutoFollowIndexInstances.Instance.enable() |
284 | RemoveDanglingResumableUploadsScheduler.Instance.enable() | ||
283 | 285 | ||
284 | // Redis initialization | 286 | // Redis initialization |
285 | Redis.Instance.init() | 287 | Redis.Instance.init() |
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index 7787186be..ff0d9ca3c 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' | 1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' |
2 | import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' | ||
3 | import { SendDebugCommand } from '@shared/models' | ||
2 | import * as express from 'express' | 4 | import * as express from 'express' |
3 | import { UserRight } from '../../../../shared/models/users' | 5 | import { UserRight } from '../../../../shared/models/users' |
4 | import { authenticate, ensureUserHasRight } from '../../../middlewares' | 6 | import { authenticate, ensureUserHasRight } from '../../../middlewares' |
@@ -11,6 +13,12 @@ debugRouter.get('/debug', | |||
11 | getDebug | 13 | getDebug |
12 | ) | 14 | ) |
13 | 15 | ||
16 | debugRouter.post('/debug/run-command', | ||
17 | authenticate, | ||
18 | ensureUserHasRight(UserRight.MANAGE_DEBUG), | ||
19 | runCommand | ||
20 | ) | ||
21 | |||
14 | // --------------------------------------------------------------------------- | 22 | // --------------------------------------------------------------------------- |
15 | 23 | ||
16 | export { | 24 | export { |
@@ -25,3 +33,13 @@ function getDebug (req: express.Request, res: express.Response) { | |||
25 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() | 33 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() |
26 | }) | 34 | }) |
27 | } | 35 | } |
36 | |||
37 | async function runCommand (req: express.Request, res: express.Response) { | ||
38 | const body: SendDebugCommand = req.body | ||
39 | |||
40 | if (body.command === 'remove-dandling-resumable-uploads') { | ||
41 | await RemoveDanglingResumableUploadsScheduler.Instance.execute() | ||
42 | } | ||
43 | |||
44 | return res.sendStatus(204) | ||
45 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 6ec6478e4..c32626d30 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -2,6 +2,7 @@ import * as express from 'express' | |||
2 | import { move } from 'fs-extra' | 2 | import { move } from 'fs-extra' |
3 | import { extname } from 'path' | 3 | import { extname } from 'path' |
4 | import toInt from 'validator/lib/toInt' | 4 | import toInt from 'validator/lib/toInt' |
5 | import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
6 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | 7 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' |
7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
@@ -10,8 +11,9 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail | |||
10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 11 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
11 | import { getServerActor } from '@server/models/application/application' | 12 | import { getServerActor } from '@server/models/application/application' |
12 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 13 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
14 | import { uploadx } from '@uploadx/core' | ||
13 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' | 15 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 16 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' |
15 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 17 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 18 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
17 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 19 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
@@ -47,7 +49,9 @@ import { | |||
47 | setDefaultPagination, | 49 | setDefaultPagination, |
48 | setDefaultVideosSort, | 50 | setDefaultVideosSort, |
49 | videoFileMetadataGetValidator, | 51 | videoFileMetadataGetValidator, |
50 | videosAddValidator, | 52 | videosAddLegacyValidator, |
53 | videosAddResumableInitValidator, | ||
54 | videosAddResumableValidator, | ||
51 | videosCustomGetValidator, | 55 | videosCustomGetValidator, |
52 | videosGetValidator, | 56 | videosGetValidator, |
53 | videosRemoveValidator, | 57 | videosRemoveValidator, |
@@ -69,6 +73,7 @@ import { watchingRouter } from './watching' | |||
69 | const lTags = loggerTagsFactory('api', 'video') | 73 | const lTags = loggerTagsFactory('api', 'video') |
70 | const auditLogger = auditLoggerFactory('videos') | 74 | const auditLogger = auditLoggerFactory('videos') |
71 | const videosRouter = express.Router() | 75 | const videosRouter = express.Router() |
76 | const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) | ||
72 | 77 | ||
73 | const reqVideoFileAdd = createReqFiles( | 78 | const reqVideoFileAdd = createReqFiles( |
74 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | 79 | [ 'videofile', 'thumbnailfile', 'previewfile' ], |
@@ -79,6 +84,16 @@ const reqVideoFileAdd = createReqFiles( | |||
79 | previewfile: CONFIG.STORAGE.TMP_DIR | 84 | previewfile: CONFIG.STORAGE.TMP_DIR |
80 | } | 85 | } |
81 | ) | 86 | ) |
87 | |||
88 | const reqVideoFileAddResumable = createReqFiles( | ||
89 | [ 'thumbnailfile', 'previewfile' ], | ||
90 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
91 | { | ||
92 | thumbnailfile: getResumableUploadPath(), | ||
93 | previewfile: getResumableUploadPath() | ||
94 | } | ||
95 | ) | ||
96 | |||
82 | const reqVideoFileUpdate = createReqFiles( | 97 | const reqVideoFileUpdate = createReqFiles( |
83 | [ 'thumbnailfile', 'previewfile' ], | 98 | [ 'thumbnailfile', 'previewfile' ], |
84 | MIMETYPES.IMAGE.MIMETYPE_EXT, | 99 | MIMETYPES.IMAGE.MIMETYPE_EXT, |
@@ -111,18 +126,39 @@ videosRouter.get('/', | |||
111 | commonVideosFiltersValidator, | 126 | commonVideosFiltersValidator, |
112 | asyncMiddleware(listVideos) | 127 | asyncMiddleware(listVideos) |
113 | ) | 128 | ) |
129 | |||
130 | videosRouter.post('/upload', | ||
131 | authenticate, | ||
132 | reqVideoFileAdd, | ||
133 | asyncMiddleware(videosAddLegacyValidator), | ||
134 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
135 | ) | ||
136 | |||
137 | videosRouter.post('/upload-resumable', | ||
138 | authenticate, | ||
139 | reqVideoFileAddResumable, | ||
140 | asyncMiddleware(videosAddResumableInitValidator), | ||
141 | uploadxMiddleware | ||
142 | ) | ||
143 | |||
144 | videosRouter.delete('/upload-resumable', | ||
145 | authenticate, | ||
146 | uploadxMiddleware | ||
147 | ) | ||
148 | |||
149 | videosRouter.put('/upload-resumable', | ||
150 | authenticate, | ||
151 | uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes | ||
152 | asyncMiddleware(videosAddResumableValidator), | ||
153 | asyncMiddleware(addVideoResumable) | ||
154 | ) | ||
155 | |||
114 | videosRouter.put('/:id', | 156 | videosRouter.put('/:id', |
115 | authenticate, | 157 | authenticate, |
116 | reqVideoFileUpdate, | 158 | reqVideoFileUpdate, |
117 | asyncMiddleware(videosUpdateValidator), | 159 | asyncMiddleware(videosUpdateValidator), |
118 | asyncRetryTransactionMiddleware(updateVideo) | 160 | asyncRetryTransactionMiddleware(updateVideo) |
119 | ) | 161 | ) |
120 | videosRouter.post('/upload', | ||
121 | authenticate, | ||
122 | reqVideoFileAdd, | ||
123 | asyncMiddleware(videosAddValidator), | ||
124 | asyncRetryTransactionMiddleware(addVideo) | ||
125 | ) | ||
126 | 162 | ||
127 | videosRouter.get('/:id/description', | 163 | videosRouter.get('/:id/description', |
128 | asyncMiddleware(videosGetValidator), | 164 | asyncMiddleware(videosGetValidator), |
@@ -157,23 +193,23 @@ export { | |||
157 | 193 | ||
158 | // --------------------------------------------------------------------------- | 194 | // --------------------------------------------------------------------------- |
159 | 195 | ||
160 | function listVideoCategories (req: express.Request, res: express.Response) { | 196 | function listVideoCategories (_req: express.Request, res: express.Response) { |
161 | res.json(VIDEO_CATEGORIES) | 197 | res.json(VIDEO_CATEGORIES) |
162 | } | 198 | } |
163 | 199 | ||
164 | function listVideoLicences (req: express.Request, res: express.Response) { | 200 | function listVideoLicences (_req: express.Request, res: express.Response) { |
165 | res.json(VIDEO_LICENCES) | 201 | res.json(VIDEO_LICENCES) |
166 | } | 202 | } |
167 | 203 | ||
168 | function listVideoLanguages (req: express.Request, res: express.Response) { | 204 | function listVideoLanguages (_req: express.Request, res: express.Response) { |
169 | res.json(VIDEO_LANGUAGES) | 205 | res.json(VIDEO_LANGUAGES) |
170 | } | 206 | } |
171 | 207 | ||
172 | function listVideoPrivacies (req: express.Request, res: express.Response) { | 208 | function listVideoPrivacies (_req: express.Request, res: express.Response) { |
173 | res.json(VIDEO_PRIVACIES) | 209 | res.json(VIDEO_PRIVACIES) |
174 | } | 210 | } |
175 | 211 | ||
176 | async function addVideo (req: express.Request, res: express.Response) { | 212 | async function addVideoLegacy (req: express.Request, res: express.Response) { |
177 | // Uploading the video could be long | 213 | // Uploading the video could be long |
178 | // Set timeout to 10 minutes, as Express's default is 2 minutes | 214 | // Set timeout to 10 minutes, as Express's default is 2 minutes |
179 | req.setTimeout(1000 * 60 * 10, () => { | 215 | req.setTimeout(1000 * 60 * 10, () => { |
@@ -183,13 +219,42 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
183 | 219 | ||
184 | const videoPhysicalFile = req.files['videofile'][0] | 220 | const videoPhysicalFile = req.files['videofile'][0] |
185 | const videoInfo: VideoCreate = req.body | 221 | const videoInfo: VideoCreate = req.body |
222 | const files = req.files | ||
223 | |||
224 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
225 | } | ||
186 | 226 | ||
187 | const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) | 227 | async function addVideoResumable (_req: express.Request, res: express.Response) { |
188 | videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED | 228 | const videoPhysicalFile = res.locals.videoFileResumable |
189 | videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware | 229 | const videoInfo = videoPhysicalFile.metadata |
230 | const files = { previewfile: videoInfo.previewfile } | ||
231 | |||
232 | // Don't need the meta file anymore | ||
233 | await deleteResumableUploadMetaFile(videoPhysicalFile.path) | ||
234 | |||
235 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
236 | } | ||
237 | |||
238 | async function addVideo (options: { | ||
239 | res: express.Response | ||
240 | videoPhysicalFile: express.VideoUploadFile | ||
241 | videoInfo: VideoCreate | ||
242 | files: express.UploadFiles | ||
243 | }) { | ||
244 | const { res, videoPhysicalFile, videoInfo, files } = options | ||
245 | const videoChannel = res.locals.videoChannel | ||
246 | const user = res.locals.oauth.token.User | ||
247 | |||
248 | const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
249 | |||
250 | videoData.state = CONFIG.TRANSCODING.ENABLED | ||
251 | ? VideoState.TO_TRANSCODE | ||
252 | : VideoState.PUBLISHED | ||
253 | |||
254 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
190 | 255 | ||
191 | const video = new VideoModel(videoData) as MVideoFullLight | 256 | const video = new VideoModel(videoData) as MVideoFullLight |
192 | video.VideoChannel = res.locals.videoChannel | 257 | video.VideoChannel = videoChannel |
193 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 258 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
194 | 259 | ||
195 | const videoFile = new VideoFileModel({ | 260 | const videoFile = new VideoFileModel({ |
@@ -217,7 +282,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
217 | 282 | ||
218 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 283 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
219 | video, | 284 | video, |
220 | files: req.files, | 285 | files, |
221 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | 286 | fallback: type => generateVideoMiniature({ video, videoFile, type }) |
222 | }) | 287 | }) |
223 | 288 | ||
@@ -248,9 +313,12 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
248 | }, { transaction: t }) | 313 | }, { transaction: t }) |
249 | } | 314 | } |
250 | 315 | ||
316 | // Channel has a new content, set as updated | ||
317 | await videoCreated.VideoChannel.setAsUpdated(t) | ||
318 | |||
251 | await autoBlacklistVideoIfNeeded({ | 319 | await autoBlacklistVideoIfNeeded({ |
252 | video, | 320 | video, |
253 | user: res.locals.oauth.token.User, | 321 | user, |
254 | isRemote: false, | 322 | isRemote: false, |
255 | isNew: true, | 323 | isNew: true, |
256 | transaction: t | 324 | transaction: t |
@@ -279,7 +347,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
279 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | 347 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) |
280 | 348 | ||
281 | if (video.state === VideoState.TO_TRANSCODE) { | 349 | if (video.state === VideoState.TO_TRANSCODE) { |
282 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) | 350 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) |
283 | } | 351 | } |
284 | 352 | ||
285 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | 353 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 921067e65..f0717bbbc 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -167,7 +167,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { | |||
167 | videoChannelId: videoChannel ? videoChannel.id : null | 167 | videoChannelId: videoChannel ? videoChannel.id : null |
168 | } | 168 | } |
169 | 169 | ||
170 | const resultList = await VideoModel.listForApi({ | 170 | const { data } = await VideoModel.listForApi({ |
171 | start, | 171 | start, |
172 | count: FEEDS.COUNT, | 172 | count: FEEDS.COUNT, |
173 | sort: req.query.sort, | 173 | sort: req.query.sort, |
@@ -175,10 +175,11 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { | |||
175 | nsfw, | 175 | nsfw, |
176 | filter: req.query.filter as VideoFilter, | 176 | filter: req.query.filter as VideoFilter, |
177 | withFiles: true, | 177 | withFiles: true, |
178 | countVideos: false, | ||
178 | ...options | 179 | ...options |
179 | }) | 180 | }) |
180 | 181 | ||
181 | addVideosToFeed(feed, resultList.data) | 182 | addVideosToFeed(feed, data) |
182 | 183 | ||
183 | // Now the feed generation is done, let's send it! | 184 | // Now the feed generation is done, let's send it! |
184 | return sendFeed(feed, req, res) | 185 | return sendFeed(feed, req, res) |
@@ -198,20 +199,22 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp | |||
198 | queryString: new URL(WEBSERVER.URL + req.url).search | 199 | queryString: new URL(WEBSERVER.URL + req.url).search |
199 | }) | 200 | }) |
200 | 201 | ||
201 | const resultList = await VideoModel.listForApi({ | 202 | const { data } = await VideoModel.listForApi({ |
202 | start, | 203 | start, |
203 | count: FEEDS.COUNT, | 204 | count: FEEDS.COUNT, |
204 | sort: req.query.sort, | 205 | sort: req.query.sort, |
205 | includeLocalVideos: false, | 206 | includeLocalVideos: false, |
206 | nsfw, | 207 | nsfw, |
207 | filter: req.query.filter as VideoFilter, | 208 | filter: req.query.filter as VideoFilter, |
209 | |||
208 | withFiles: true, | 210 | withFiles: true, |
211 | countVideos: false, | ||
209 | 212 | ||
210 | followerActorId: res.locals.user.Account.Actor.id, | 213 | followerActorId: res.locals.user.Account.Actor.id, |
211 | user: res.locals.user | 214 | user: res.locals.user |
212 | }) | 215 | }) |
213 | 216 | ||
214 | addVideosToFeed(feed, resultList.data) | 217 | addVideosToFeed(feed, data) |
215 | 218 | ||
216 | // Now the feed generation is done, let's send it! | 219 | // Now the feed generation is done, let's send it! |
217 | return sendFeed(feed, req, res) | 220 | return sendFeed(feed, req, res) |
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 877345157..675a7b663 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 2 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
3 | import { exists, isArray } from '../misc' | 3 | import { exists, isArray, isDateValid } from '../misc' |
4 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 4 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
5 | import { isHostValid } from '../servers' | 5 | import { isHostValid } from '../servers' |
6 | import { peertubeTruncate } from '@server/helpers/core-utils' | 6 | import { peertubeTruncate } from '@server/helpers/core-utils' |
@@ -47,7 +47,21 @@ function isActorPrivateKeyValid (privateKey: string) { | |||
47 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) | 47 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) |
48 | } | 48 | } |
49 | 49 | ||
50 | function isActorObjectValid (actor: any) { | 50 | function isActorFollowingCountValid (value: string) { |
51 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
52 | } | ||
53 | |||
54 | function isActorFollowersCountValid (value: string) { | ||
55 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
56 | } | ||
57 | |||
58 | function isActorDeleteActivityValid (activity: any) { | ||
59 | return isBaseActivityValid(activity, 'Delete') | ||
60 | } | ||
61 | |||
62 | function sanitizeAndCheckActorObject (actor: any) { | ||
63 | normalizeActor(actor) | ||
64 | |||
51 | return exists(actor) && | 65 | return exists(actor) && |
52 | isActivityPubUrlValid(actor.id) && | 66 | isActivityPubUrlValid(actor.id) && |
53 | isActorTypeValid(actor.type) && | 67 | isActorTypeValid(actor.type) && |
@@ -68,24 +82,6 @@ function isActorObjectValid (actor: any) { | |||
68 | (actor.type !== 'Group' || actor.attributedTo.length !== 0) | 82 | (actor.type !== 'Group' || actor.attributedTo.length !== 0) |
69 | } | 83 | } |
70 | 84 | ||
71 | function isActorFollowingCountValid (value: string) { | ||
72 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
73 | } | ||
74 | |||
75 | function isActorFollowersCountValid (value: string) { | ||
76 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
77 | } | ||
78 | |||
79 | function isActorDeleteActivityValid (activity: any) { | ||
80 | return isBaseActivityValid(activity, 'Delete') | ||
81 | } | ||
82 | |||
83 | function sanitizeAndCheckActorObject (object: any) { | ||
84 | normalizeActor(object) | ||
85 | |||
86 | return isActorObjectValid(object) | ||
87 | } | ||
88 | |||
89 | function normalizeActor (actor: any) { | 85 | function normalizeActor (actor: any) { |
90 | if (!actor) return | 86 | if (!actor) return |
91 | 87 | ||
@@ -95,6 +91,8 @@ function normalizeActor (actor: any) { | |||
95 | actor.url = actor.url.href || actor.url.url | 91 | actor.url = actor.url.href || actor.url.url |
96 | } | 92 | } |
97 | 93 | ||
94 | if (!isDateValid(actor.published)) actor.published = undefined | ||
95 | |||
98 | if (actor.summary && typeof actor.summary === 'string') { | 96 | if (actor.summary && typeof actor.summary === 'string') { |
99 | actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max }) | 97 | actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max }) |
100 | 98 | ||
@@ -135,7 +133,6 @@ export { | |||
135 | isActorPublicKeyValid, | 133 | isActorPublicKeyValid, |
136 | isActorPreferredUsernameValid, | 134 | isActorPreferredUsernameValid, |
137 | isActorPrivateKeyValid, | 135 | isActorPrivateKeyValid, |
138 | isActorObjectValid, | ||
139 | isActorFollowingCountValid, | 136 | isActorFollowingCountValid, |
140 | isActorFollowersCountValid, | 137 | isActorFollowersCountValid, |
141 | isActorDeleteActivityValid, | 138 | isActorDeleteActivityValid, |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index effdd98cb..fd3b45804 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import validator from 'validator' | 2 | import { UploadFilesForCheck } from 'express' |
3 | import { sep } from 'path' | 3 | import { sep } from 'path' |
4 | import validator from 'validator' | ||
4 | 5 | ||
5 | function exists (value: any) { | 6 | function exists (value: any) { |
6 | return value !== undefined && value !== null | 7 | return value !== undefined && value !== null |
@@ -108,7 +109,7 @@ function isFileFieldValid ( | |||
108 | } | 109 | } |
109 | 110 | ||
110 | function isFileMimeTypeValid ( | 111 | function isFileMimeTypeValid ( |
111 | files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], | 112 | files: UploadFilesForCheck, |
112 | mimeTypeRegex: string, | 113 | mimeTypeRegex: string, |
113 | field: string, | 114 | field: string, |
114 | optional = false | 115 | optional = false |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 87966798f..b33e088eb 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | ||
1 | import { values } from 'lodash' | 2 | import { values } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | ||
2 | import validator from 'validator' | 4 | import validator from 'validator' |
3 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' | 5 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
4 | import { | 6 | import { |
@@ -6,13 +8,12 @@ import { | |||
6 | MIMETYPES, | 8 | MIMETYPES, |
7 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
8 | VIDEO_LICENCES, | 10 | VIDEO_LICENCES, |
11 | VIDEO_LIVE, | ||
9 | VIDEO_PRIVACIES, | 12 | VIDEO_PRIVACIES, |
10 | VIDEO_RATE_TYPES, | 13 | VIDEO_RATE_TYPES, |
11 | VIDEO_STATES, | 14 | VIDEO_STATES |
12 | VIDEO_LIVE | ||
13 | } from '../../initializers/constants' | 15 | } from '../../initializers/constants' |
14 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' | 16 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' |
15 | import * as magnetUtil from 'magnet-uri' | ||
16 | 17 | ||
17 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
18 | 19 | ||
@@ -81,7 +82,7 @@ function isVideoFileExtnameValid (value: string) { | |||
81 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) | 82 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) |
82 | } | 83 | } |
83 | 84 | ||
84 | function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 85 | function isVideoFileMimeTypeValid (files: UploadFilesForCheck) { |
85 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') | 86 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') |
86 | } | 87 | } |
87 | 88 | ||
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index 2b916efc2..f9cb33aca 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as retry from 'async/retry' | 1 | import * as retry from 'async/retry' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { QueryTypes, Transaction } from 'sequelize' | ||
3 | import { Model } from 'sequelize-typescript' | 4 | import { Model } from 'sequelize-typescript' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | ||
4 | import { logger } from './logger' | 6 | import { logger } from './logger' |
5 | import { Transaction } from 'sequelize' | ||
6 | 7 | ||
7 | function retryTransactionWrapper <T, A, B, C, D> ( | 8 | function retryTransactionWrapper <T, A, B, C, D> ( |
8 | functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T> | Bluebird<T>, | 9 | functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T> | Bluebird<T>, |
@@ -96,6 +97,18 @@ function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): | |||
96 | .map(f => f.destroy({ transaction: t })) | 97 | .map(f => f.destroy({ transaction: t })) |
97 | } | 98 | } |
98 | 99 | ||
100 | // Sequelize always skip the update if we only update updatedAt field | ||
101 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | ||
102 | return sequelizeTypescript.query( | ||
103 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | ||
104 | { | ||
105 | replacements: { table, id, updatedAt: new Date() }, | ||
106 | type: QueryTypes.UPDATE, | ||
107 | transaction | ||
108 | } | ||
109 | ) | ||
110 | } | ||
111 | |||
99 | // --------------------------------------------------------------------------- | 112 | // --------------------------------------------------------------------------- |
100 | 113 | ||
101 | export { | 114 | export { |
@@ -104,5 +117,6 @@ export { | |||
104 | transactionRetryer, | 117 | transactionRetryer, |
105 | updateInstanceWithAnother, | 118 | updateInstanceWithAnother, |
106 | afterCommitIfTransaction, | 119 | afterCommitIfTransaction, |
107 | deleteNonExistingModels | 120 | deleteNonExistingModels, |
121 | setAsUpdated | ||
108 | } | 122 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index c0d3f8f32..ede22a3cc 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { REMOTE_SCHEME } from '../initializers/constants' | 3 | import { REMOTE_SCHEME } from '../initializers/constants' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAndCatch, generateRandomString } from './utils' |
6 | import { extname } from 'path' | 6 | import { extname } from 'path' |
7 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
8 | import { CONFIG } from '../initializers/config' | 8 | import { CONFIG } from '../initializers/config' |
@@ -36,15 +36,15 @@ function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.Fi | |||
36 | if (!files) return | 36 | if (!files) return |
37 | 37 | ||
38 | if (isArray(files)) { | 38 | if (isArray(files)) { |
39 | (files as Express.Multer.File[]).forEach(f => deleteFileAsync(f.path)) | 39 | (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path)) |
40 | return | 40 | return |
41 | } | 41 | } |
42 | 42 | ||
43 | for (const key of Object.keys(files)) { | 43 | for (const key of Object.keys(files)) { |
44 | const file = files[key] | 44 | const file = files[key] |
45 | 45 | ||
46 | if (isArray(file)) file.forEach(f => deleteFileAsync(f.path)) | 46 | if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path)) |
47 | else deleteFileAsync(file.path) | 47 | else deleteFileAndCatch(file.path) |
48 | } | 48 | } |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 75297df8f..25d9d4951 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -679,10 +679,16 @@ function getFFmpegVersion () { | |||
679 | 679 | ||
680 | return execPromise(`${ffmpegPath} -version`) | 680 | return execPromise(`${ffmpegPath} -version`) |
681 | .then(stdout => { | 681 | .then(stdout => { |
682 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+\.\d+)/) | 682 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/) |
683 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | 683 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) |
684 | 684 | ||
685 | return res(parsed[1]) | 685 | // Fix ffmpeg version that does not include patch version (4.4 for example) |
686 | let version = parsed[1] | ||
687 | if (version.match(/^\d+\.\d+$/)) { | ||
688 | version += '.0' | ||
689 | } | ||
690 | |||
691 | return res(version) | ||
686 | }) | 692 | }) |
687 | .catch(err => rej(err)) | 693 | .catch(err => rej(err)) |
688 | }) | 694 | }) |
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts new file mode 100644 index 000000000..030a6b7d5 --- /dev/null +++ b/server/helpers/upload.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { METAFILE_EXTNAME } from '@uploadx/core' | ||
2 | import { remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | ||
5 | |||
6 | function getResumableUploadPath (filename?: string) { | ||
7 | if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename) | ||
8 | |||
9 | return RESUMABLE_UPLOAD_DIRECTORY | ||
10 | } | ||
11 | |||
12 | function deleteResumableUploadMetaFile (filepath: string) { | ||
13 | return remove(filepath + METAFILE_EXTNAME) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | getResumableUploadPath, | ||
20 | deleteResumableUploadMetaFile | ||
21 | } | ||
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 0545e8996..6c95a43b6 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -6,7 +6,7 @@ import { CONFIG } from '../initializers/config' | |||
6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' | 6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' |
7 | import { logger } from './logger' | 7 | import { logger } from './logger' |
8 | 8 | ||
9 | function deleteFileAsync (path: string) { | 9 | function deleteFileAndCatch (path: string) { |
10 | remove(path) | 10 | remove(path) |
11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) | 11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) |
12 | } | 12 | } |
@@ -83,7 +83,7 @@ function getUUIDFromFilename (filename: string) { | |||
83 | // --------------------------------------------------------------------------- | 83 | // --------------------------------------------------------------------------- |
84 | 84 | ||
85 | export { | 85 | export { |
86 | deleteFileAsync, | 86 | deleteFileAndCatch, |
87 | generateRandomString, | 87 | generateRandomString, |
88 | getFormattedObjects, | 88 | getFormattedObjects, |
89 | getSecureTorrentName, | 89 | getSecureTorrentName, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index d390fd95e..6f388420e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 640 | 27 | const LAST_MIGRATION_VERSION = 645 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -208,7 +208,8 @@ const SCHEDULER_INTERVALS_MS = { | |||
208 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day | 208 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day |
209 | removeOldViews: 60000 * 60 * 24, // 1 day | 209 | removeOldViews: 60000 * 60 * 24, // 1 day |
210 | removeOldHistory: 60000 * 60 * 24, // 1 day | 210 | removeOldHistory: 60000 * 60 * 24, // 1 day |
211 | updateInboxStats: 1000 * 60// 1 minute | 211 | updateInboxStats: 1000 * 60, // 1 minute |
212 | removeDanglingResumableUploads: 60000 * 60 * 16 // 16 hours | ||
212 | } | 213 | } |
213 | 214 | ||
214 | // --------------------------------------------------------------------------- | 215 | // --------------------------------------------------------------------------- |
@@ -285,6 +286,7 @@ const CONSTRAINTS_FIELDS = { | |||
285 | LIKES: { min: 0 }, | 286 | LIKES: { min: 0 }, |
286 | DISLIKES: { min: 0 }, | 287 | DISLIKES: { min: 0 }, |
287 | FILE_SIZE: { min: -1 }, | 288 | FILE_SIZE: { min: -1 }, |
289 | PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB | ||
288 | URL: { min: 3, max: 2000 } // Length | 290 | URL: { min: 3, max: 2000 } // Length |
289 | }, | 291 | }, |
290 | VIDEO_PLAYLISTS: { | 292 | VIDEO_PLAYLISTS: { |
@@ -645,6 +647,7 @@ const LRU_CACHE = { | |||
645 | } | 647 | } |
646 | } | 648 | } |
647 | 649 | ||
650 | const RESUMABLE_UPLOAD_DIRECTORY = join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads') | ||
648 | const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') | 651 | const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') |
649 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | 652 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') |
650 | 653 | ||
@@ -819,6 +822,7 @@ export { | |||
819 | PEERTUBE_VERSION, | 822 | PEERTUBE_VERSION, |
820 | LAZY_STATIC_PATHS, | 823 | LAZY_STATIC_PATHS, |
821 | SEARCH_INDEX, | 824 | SEARCH_INDEX, |
825 | RESUMABLE_UPLOAD_DIRECTORY, | ||
822 | HLS_REDUNDANCY_DIRECTORY, | 826 | HLS_REDUNDANCY_DIRECTORY, |
823 | P2P_MEDIA_LOADER_PEER_VERSION, | 827 | P2P_MEDIA_LOADER_PEER_VERSION, |
824 | ACTOR_IMAGES_SIZE, | 828 | ACTOR_IMAGES_SIZE, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index cb58454cb..8dcff64e2 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -6,7 +6,7 @@ import { UserModel } from '../models/account/user' | |||
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
9 | import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' | 9 | import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants' |
10 | import { sequelizeTypescript } from './database' | 10 | import { sequelizeTypescript } from './database' |
11 | import { ensureDir, remove } from 'fs-extra' | 11 | import { ensureDir, remove } from 'fs-extra' |
12 | import { CONFIG } from './config' | 12 | import { CONFIG } from './config' |
@@ -79,6 +79,9 @@ function createDirectoriesIfNotExist () { | |||
79 | // Playlist directories | 79 | // Playlist directories |
80 | tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY)) | 80 | tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY)) |
81 | 81 | ||
82 | // Resumable upload directory | ||
83 | tasks.push(ensureDir(RESUMABLE_UPLOAD_DIRECTORY)) | ||
84 | |||
82 | return Promise.all(tasks) | 85 | return Promise.all(tasks) |
83 | } | 86 | } |
84 | 87 | ||
diff --git a/server/initializers/migrations/0645-actor-remote-creation-date.ts b/server/initializers/migrations/0645-actor-remote-creation-date.ts new file mode 100644 index 000000000..38b3b881c --- /dev/null +++ b/server/initializers/migrations/0645-actor-remote-creation-date.ts | |||
@@ -0,0 +1,26 @@ | |||
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 | defaultValue: null, | ||
13 | allowNull: true | ||
14 | } | ||
15 | await utils.queryInterface.addColumn('actor', 'remoteCreatedAt', data) | ||
16 | } | ||
17 | } | ||
18 | |||
19 | function down (options) { | ||
20 | throw new Error('Not implemented.') | ||
21 | } | ||
22 | |||
23 | export { | ||
24 | up, | ||
25 | down | ||
26 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index eec951d4e..5fe7381c9 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -165,6 +165,8 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ | |||
165 | actorInstance.followersUrl = attributes.followers | 165 | actorInstance.followersUrl = attributes.followers |
166 | actorInstance.followingUrl = attributes.following | 166 | actorInstance.followingUrl = attributes.following |
167 | 167 | ||
168 | if (attributes.published) actorInstance.remoteCreatedAt = new Date(attributes.published) | ||
169 | |||
168 | if (attributes.endpoints?.sharedInbox) { | 170 | if (attributes.endpoints?.sharedInbox) { |
169 | actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox | 171 | actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox |
170 | } | 172 | } |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 506204674..15726f90b 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -697,6 +697,9 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi | |||
697 | videoCreated.VideoLive = await videoLive.save({ transaction: t }) | 697 | videoCreated.VideoLive = await videoLive.save({ transaction: t }) |
698 | } | 698 | } |
699 | 699 | ||
700 | // We added a video in this channel, set it as updated | ||
701 | await channel.setAsUpdated(t) | ||
702 | |||
700 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | 703 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ |
701 | video: videoCreated, | 704 | video: videoCreated, |
702 | user: undefined, | 705 | user: undefined, |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 84539e2c1..05be403f3 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -50,13 +50,12 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) { | |||
50 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | 50 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` |
51 | if (file.fps) line += ',FRAME-RATE=' + file.fps | 51 | if (file.fps) line += ',FRAME-RATE=' + file.fps |
52 | 52 | ||
53 | const videoCodec = await getVideoStreamCodec(videoFilePath) | 53 | const codecs = await Promise.all([ |
54 | line += `,CODECS="${videoCodec}` | 54 | getVideoStreamCodec(videoFilePath), |
55 | getAudioStreamCodec(videoFilePath) | ||
56 | ]) | ||
55 | 57 | ||
56 | const audioCodec = await getAudioStreamCodec(videoFilePath) | 58 | line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` |
57 | if (audioCodec) line += `,${audioCodec}` | ||
58 | |||
59 | line += '"' | ||
60 | 59 | ||
61 | masterPlaylists.push(line) | 60 | masterPlaylists.push(line) |
62 | masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | 61 | masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) |
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 5180b3299..925d64902 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { VideoUploadFile } from 'express' | ||
1 | import { PathLike } from 'fs-extra' | 2 | import { PathLike } from 'fs-extra' |
2 | import { Transaction } from 'sequelize/types' | 3 | import { Transaction } from 'sequelize/types' |
3 | import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' | 4 | import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' |
5 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
4 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
5 | import { AbuseModel } from '@server/models/abuse/abuse' | 7 | import { AbuseModel } from '@server/models/abuse/abuse' |
6 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | 8 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' |
@@ -28,7 +30,6 @@ import { VideoModel } from '../models/video/video' | |||
28 | import { VideoCommentModel } from '../models/video/video-comment' | 30 | import { VideoCommentModel } from '../models/video/video-comment' |
29 | import { sendAbuse } from './activitypub/send/send-flag' | 31 | import { sendAbuse } from './activitypub/send/send-flag' |
30 | import { Notifier } from './notifier' | 32 | import { Notifier } from './notifier' |
31 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
32 | 33 | ||
33 | export type AcceptResult = { | 34 | export type AcceptResult = { |
34 | accepted: boolean | 35 | accepted: boolean |
@@ -38,7 +39,7 @@ export type AcceptResult = { | |||
38 | // Can be filtered by plugins | 39 | // Can be filtered by plugins |
39 | function isLocalVideoAccepted (object: { | 40 | function isLocalVideoAccepted (object: { |
40 | videoBody: VideoCreate | 41 | videoBody: VideoCreate |
41 | videoFile: Express.Multer.File & { duration?: number } | 42 | videoFile: VideoUploadFile |
42 | user: UserModel | 43 | user: UserModel |
43 | }): AcceptResult { | 44 | }): AcceptResult { |
44 | return { accepted: true } | 45 | return { accepted: true } |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index d57c69ef0..f1bc24d8b 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -17,6 +17,7 @@ import { VideoBlacklistCreate } from '@shared/models' | |||
17 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' | 17 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' |
18 | import { getServerConfig } from '../config' | 18 | import { getServerConfig } from '../config' |
19 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' | 19 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' |
20 | import { UserModel } from '@server/models/account/user' | ||
20 | 21 | ||
21 | function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { | 22 | function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { |
22 | const logger = buildPluginLogger(npmName) | 23 | const logger = buildPluginLogger(npmName) |
@@ -163,6 +164,11 @@ function buildPluginRelatedHelpers (plugin: MPlugin, npmName: string) { | |||
163 | 164 | ||
164 | function buildUserHelpers () { | 165 | function buildUserHelpers () { |
165 | return { | 166 | return { |
166 | getAuthUser: (res: express.Response) => res.locals.oauth?.token?.User | 167 | getAuthUser: (res: express.Response) => { |
168 | const user = res.locals.oauth?.token?.User | ||
169 | if (!user) return undefined | ||
170 | |||
171 | return UserModel.loadByIdFull(user.id) | ||
172 | } | ||
167 | } | 173 | } |
168 | } | 174 | } |
diff --git a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts new file mode 100644 index 000000000..1acea7998 --- /dev/null +++ b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts | |||
@@ -0,0 +1,61 @@ | |||
1 | import * as bluebird from 'bluebird' | ||
2 | import { readdir, remove, stat } from 'fs-extra' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' | ||
6 | import { METAFILE_EXTNAME } from '@uploadx/core' | ||
7 | import { AbstractScheduler } from './abstract-scheduler' | ||
8 | |||
9 | const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') | ||
10 | |||
11 | export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler { | ||
12 | |||
13 | private static instance: AbstractScheduler | ||
14 | private lastExecutionTimeMs: number | ||
15 | |||
16 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeDanglingResumableUploads | ||
17 | |||
18 | private constructor () { | ||
19 | super() | ||
20 | |||
21 | this.lastExecutionTimeMs = new Date().getTime() | ||
22 | } | ||
23 | |||
24 | protected async internalExecute () { | ||
25 | const path = getResumableUploadPath() | ||
26 | const files = await readdir(path) | ||
27 | |||
28 | const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME)) | ||
29 | |||
30 | if (metafiles.length === 0) return | ||
31 | |||
32 | logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags()) | ||
33 | |||
34 | try { | ||
35 | await bluebird.map(metafiles, metafile => { | ||
36 | return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs) | ||
37 | }, { concurrency: 5 }) | ||
38 | } catch (error) { | ||
39 | logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() }) | ||
40 | } finally { | ||
41 | this.lastExecutionTimeMs = new Date().getTime() | ||
42 | } | ||
43 | } | ||
44 | |||
45 | private async deleteIfOlderThan (metafile: string, olderThan: number) { | ||
46 | const metafilePath = getResumableUploadPath(metafile) | ||
47 | const statResult = await stat(metafilePath) | ||
48 | |||
49 | // Delete uploads that started since a long time | ||
50 | if (statResult.ctimeMs < olderThan) { | ||
51 | await remove(metafilePath) | ||
52 | |||
53 | const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '') | ||
54 | await remove(datafile) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | static get Instance () { | ||
59 | return this.instance || (this.instance = new this()) | ||
60 | } | ||
61 | } | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index 9469b8178..21e4b7ff2 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { UploadFiles } from 'express' | ||
1 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
2 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' | 3 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' |
3 | import { sequelizeTypescript } from '@server/initializers/database' | 4 | import { sequelizeTypescript } from '@server/initializers/database' |
@@ -32,7 +33,7 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil | |||
32 | 33 | ||
33 | async function buildVideoThumbnailsFromReq (options: { | 34 | async function buildVideoThumbnailsFromReq (options: { |
34 | video: MVideoThumbnail | 35 | video: MVideoThumbnail |
35 | files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] | 36 | files: UploadFiles |
36 | fallback: (type: ThumbnailType) => Promise<MThumbnail> | 37 | fallback: (type: ThumbnailType) => Promise<MThumbnail> |
37 | automaticallyGenerated?: boolean | 38 | automaticallyGenerated?: boolean |
38 | }) { | 39 | }) { |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index bb617d77c..d26bcd4a6 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
3 | import { isAbleToUploadVideo } from '@server/lib/user' | 4 | import { isAbleToUploadVideo } from '@server/lib/user' |
4 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
5 | import { ExpressPromiseHandler } from '@server/types/express' | 6 | import { ExpressPromiseHandler } from '@server/types/express' |
6 | import { MVideoWithRights } from '@server/types/models' | 7 | import { MUserAccountId, MVideoWithRights } from '@server/types/models' |
7 | import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' | 8 | import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
9 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' | 10 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' |
@@ -47,6 +48,7 @@ import { | |||
47 | doesVideoExist, | 48 | doesVideoExist, |
48 | doesVideoFileOfVideoExist | 49 | doesVideoFileOfVideoExist |
49 | } from '../../../helpers/middlewares' | 50 | } from '../../../helpers/middlewares' |
51 | import { deleteFileAndCatch } from '../../../helpers/utils' | ||
50 | import { getVideoWithAttributes } from '../../../helpers/video' | 52 | import { getVideoWithAttributes } from '../../../helpers/video' |
51 | import { CONFIG } from '../../../initializers/config' | 53 | import { CONFIG } from '../../../initializers/config' |
52 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 54 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
@@ -57,7 +59,7 @@ import { VideoModel } from '../../../models/video/video' | |||
57 | import { authenticatePromiseIfNeeded } from '../../auth' | 59 | import { authenticatePromiseIfNeeded } from '../../auth' |
58 | import { areValidationErrors } from '../utils' | 60 | import { areValidationErrors } from '../utils' |
59 | 61 | ||
60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ | 62 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
61 | body('videofile') | 63 | body('videofile') |
62 | .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) | 64 | .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) |
63 | .withMessage('Should have a file'), | 65 | .withMessage('Should have a file'), |
@@ -73,54 +75,117 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([ | |||
73 | logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) | 75 | logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) |
74 | 76 | ||
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 77 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
76 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | ||
77 | 78 | ||
78 | const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0] | 79 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] |
79 | const user = res.locals.oauth.token.User | 80 | const user = res.locals.oauth.token.User |
80 | 81 | ||
81 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 82 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) { |
82 | |||
83 | if (!isVideoFileMimeTypeValid(req.files)) { | ||
84 | res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) | ||
85 | .json({ | ||
86 | error: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
87 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
88 | }) | ||
89 | |||
90 | return cleanUpReqFiles(req) | 83 | return cleanUpReqFiles(req) |
91 | } | 84 | } |
92 | 85 | ||
93 | if (!isVideoFileSizeValid(videoFile.size.toString())) { | 86 | try { |
94 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | 87 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
95 | .json({ | 88 | } catch (err) { |
96 | error: 'This file is too large.' | 89 | logger.error('Invalid input file in videosAddLegacyValidator.', { err }) |
97 | }) | 90 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) |
91 | .json({ error: 'Video file unreadable.' }) | ||
98 | 92 | ||
99 | return cleanUpReqFiles(req) | 93 | return cleanUpReqFiles(req) |
100 | } | 94 | } |
101 | 95 | ||
102 | if (await isAbleToUploadVideo(user.id, videoFile.size) === false) { | 96 | if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) |
103 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
104 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
105 | 97 | ||
106 | return cleanUpReqFiles(req) | 98 | return next() |
107 | } | 99 | } |
100 | ]) | ||
101 | |||
102 | /** | ||
103 | * Gets called after the last PUT request | ||
104 | */ | ||
105 | const videosAddResumableValidator = [ | ||
106 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
107 | const user = res.locals.oauth.token.User | ||
108 | |||
109 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | ||
110 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.id), filename: body.metadata.filename } | ||
111 | |||
112 | const cleanup = () => deleteFileAndCatch(file.path) | ||
108 | 113 | ||
109 | let duration: number | 114 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() |
110 | 115 | ||
111 | try { | 116 | try { |
112 | duration = await getDurationFromVideoFile(videoFile.path) | 117 | if (!file.duration) await addDurationToVideo(file) |
113 | } catch (err) { | 118 | } catch (err) { |
114 | logger.error('Invalid input file in videosAddValidator.', { err }) | 119 | logger.error('Invalid input file in videosAddResumableValidator.', { err }) |
115 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) | 120 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) |
116 | .json({ error: 'Video file unreadable.' }) | 121 | .json({ error: 'Video file unreadable.' }) |
117 | 122 | ||
118 | return cleanUpReqFiles(req) | 123 | return cleanup() |
119 | } | 124 | } |
120 | 125 | ||
121 | videoFile.duration = duration | 126 | if (!await isVideoAccepted(req, res, file)) return cleanup() |
122 | 127 | ||
123 | if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) | 128 | res.locals.videoFileResumable = file |
129 | |||
130 | return next() | ||
131 | } | ||
132 | ] | ||
133 | |||
134 | /** | ||
135 | * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use. | ||
136 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts | ||
137 | * | ||
138 | * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx | ||
139 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts | ||
140 | * | ||
141 | */ | ||
142 | const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | ||
143 | body('filename') | ||
144 | .isString() | ||
145 | .exists() | ||
146 | .withMessage('Should have a valid filename'), | ||
147 | body('name') | ||
148 | .trim() | ||
149 | .custom(isVideoNameValid) | ||
150 | .withMessage('Should have a valid name'), | ||
151 | body('channelId') | ||
152 | .customSanitizer(toIntOrNull) | ||
153 | .custom(isIdValid).withMessage('Should have correct video channel id'), | ||
154 | |||
155 | header('x-upload-content-length') | ||
156 | .isNumeric() | ||
157 | .exists() | ||
158 | .withMessage('Should specify the file length'), | ||
159 | header('x-upload-content-type') | ||
160 | .isString() | ||
161 | .exists() | ||
162 | .withMessage('Should specify the file mimetype'), | ||
163 | |||
164 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
165 | const videoFileMetadata = { | ||
166 | mimetype: req.headers['x-upload-content-type'] as string, | ||
167 | size: +req.headers['x-upload-content-length'], | ||
168 | originalname: req.body.name | ||
169 | } | ||
170 | |||
171 | const user = res.locals.oauth.token.User | ||
172 | const cleanup = () => cleanUpReqFiles(req) | ||
173 | |||
174 | logger.debug('Checking videosAddResumableInitValidator parameters and headers', { | ||
175 | parameters: req.body, | ||
176 | headers: req.headers, | ||
177 | files: req.files | ||
178 | }) | ||
179 | |||
180 | if (areValidationErrors(req, res)) return cleanup() | ||
181 | |||
182 | const files = { videofile: [ videoFileMetadata ] } | ||
183 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | ||
184 | |||
185 | // multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
186 | req.headers['content-type'] = 'application/json; charset=utf-8' | ||
187 | // place previewfile in metadata so that uploadx saves it in .META | ||
188 | if (req.files['previewfile']) req.body.previewfile = req.files['previewfile'] | ||
124 | 189 | ||
125 | return next() | 190 | return next() |
126 | } | 191 | } |
@@ -478,7 +543,10 @@ const commonVideosFiltersValidator = [ | |||
478 | // --------------------------------------------------------------------------- | 543 | // --------------------------------------------------------------------------- |
479 | 544 | ||
480 | export { | 545 | export { |
481 | videosAddValidator, | 546 | videosAddLegacyValidator, |
547 | videosAddResumableValidator, | ||
548 | videosAddResumableInitValidator, | ||
549 | |||
482 | videosUpdateValidator, | 550 | videosUpdateValidator, |
483 | videosGetValidator, | 551 | videosGetValidator, |
484 | videoFileMetadataGetValidator, | 552 | videoFileMetadataGetValidator, |
@@ -515,7 +583,51 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) | |||
515 | return false | 583 | return false |
516 | } | 584 | } |
517 | 585 | ||
518 | async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) { | 586 | async function commonVideoChecksPass (parameters: { |
587 | req: express.Request | ||
588 | res: express.Response | ||
589 | user: MUserAccountId | ||
590 | videoFileSize: number | ||
591 | files: express.UploadFilesForCheck | ||
592 | }): Promise<boolean> { | ||
593 | const { req, res, user, videoFileSize, files } = parameters | ||
594 | |||
595 | if (areErrorsInScheduleUpdate(req, res)) return false | ||
596 | |||
597 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | ||
598 | |||
599 | if (!isVideoFileMimeTypeValid(files)) { | ||
600 | res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) | ||
601 | .json({ | ||
602 | error: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
603 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
604 | }) | ||
605 | |||
606 | return false | ||
607 | } | ||
608 | |||
609 | if (!isVideoFileSizeValid(videoFileSize.toString())) { | ||
610 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
611 | .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) | ||
612 | |||
613 | return false | ||
614 | } | ||
615 | |||
616 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { | ||
617 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
618 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
619 | |||
620 | return false | ||
621 | } | ||
622 | |||
623 | return true | ||
624 | } | ||
625 | |||
626 | export async function isVideoAccepted ( | ||
627 | req: express.Request, | ||
628 | res: express.Response, | ||
629 | videoFile: express.VideoUploadFile | ||
630 | ) { | ||
519 | // Check we accept this video | 631 | // Check we accept this video |
520 | const acceptParameters = { | 632 | const acceptParameters = { |
521 | videoBody: req.body, | 633 | videoBody: req.body, |
@@ -538,3 +650,11 @@ async function isVideoAccepted (req: express.Request, res: express.Response, vid | |||
538 | 650 | ||
539 | return true | 651 | return true |
540 | } | 652 | } |
653 | |||
654 | async function addDurationToVideo (videoFile: { path: string, duration?: number }) { | ||
655 | const duration: number = await getDurationFromVideoFile(videoFile.path) | ||
656 | |||
657 | if (isNaN(duration)) throw new Error(`Couldn't get video duration`) | ||
658 | |||
659 | videoFile.duration = duration | ||
660 | } | ||
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 312451abe..d33353af7 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -411,7 +411,6 @@ export class AccountModel extends Model { | |||
411 | id: this.id, | 411 | id: this.id, |
412 | displayName: this.getDisplayName(), | 412 | displayName: this.getDisplayName(), |
413 | description: this.description, | 413 | description: this.description, |
414 | createdAt: this.createdAt, | ||
415 | updatedAt: this.updatedAt, | 414 | updatedAt: this.updatedAt, |
416 | userId: this.userId ? this.userId : undefined | 415 | userId: this.userId ? this.userId : undefined |
417 | } | 416 | } |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 00c6d73aa..513455773 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -565,6 +565,10 @@ export class UserModel extends Model { | |||
565 | return UserModel.unscoped().findByPk(id) | 565 | return UserModel.unscoped().findByPk(id) |
566 | } | 566 | } |
567 | 567 | ||
568 | static loadByIdFull (id: number): Promise<MUserDefault> { | ||
569 | return UserModel.findByPk(id) | ||
570 | } | ||
571 | |||
568 | static loadByIdWithChannels (id: number, withStats = false): Promise<MUserDefault> { | 572 | static loadByIdWithChannels (id: number, withStats = false): Promise<MUserDefault> { |
569 | const scopes = [ | 573 | const scopes = [ |
570 | ScopeNames.WITH_VIDEOCHANNELS | 574 | ScopeNames.WITH_VIDEOCHANNELS |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 19f3f7e04..1af9efac2 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -69,9 +69,7 @@ export const unusedActorAttributesForAPI = [ | |||
69 | 'outboxUrl', | 69 | 'outboxUrl', |
70 | 'sharedInboxUrl', | 70 | 'sharedInboxUrl', |
71 | 'followersUrl', | 71 | 'followersUrl', |
72 | 'followingUrl', | 72 | 'followingUrl' |
73 | 'createdAt', | ||
74 | 'updatedAt' | ||
75 | ] | 73 | ] |
76 | 74 | ||
77 | @DefaultScope(() => ({ | 75 | @DefaultScope(() => ({ |
@@ -222,6 +220,10 @@ export class ActorModel extends Model { | |||
222 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) | 220 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) |
223 | followingUrl: string | 221 | followingUrl: string |
224 | 222 | ||
223 | @AllowNull(true) | ||
224 | @Column | ||
225 | remoteCreatedAt: Date | ||
226 | |||
225 | @CreatedAt | 227 | @CreatedAt |
226 | createdAt: Date | 228 | createdAt: Date |
227 | 229 | ||
@@ -555,8 +557,7 @@ export class ActorModel extends Model { | |||
555 | followingCount: this.followingCount, | 557 | followingCount: this.followingCount, |
556 | followersCount: this.followersCount, | 558 | followersCount: this.followersCount, |
557 | banner, | 559 | banner, |
558 | createdAt: this.createdAt, | 560 | createdAt: this.getCreatedAt() |
559 | updatedAt: this.updatedAt | ||
560 | }) | 561 | }) |
561 | } | 562 | } |
562 | 563 | ||
@@ -608,6 +609,7 @@ export class ActorModel extends Model { | |||
608 | owner: this.url, | 609 | owner: this.url, |
609 | publicKeyPem: this.publicKey | 610 | publicKeyPem: this.publicKey |
610 | }, | 611 | }, |
612 | published: this.getCreatedAt().toISOString(), | ||
611 | icon, | 613 | icon, |
612 | image | 614 | image |
613 | } | 615 | } |
@@ -690,4 +692,8 @@ export class ActorModel extends Model { | |||
690 | 692 | ||
691 | return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL) | 693 | return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL) |
692 | } | 694 | } |
695 | |||
696 | getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) { | ||
697 | return this.remoteCreatedAt || this.createdAt | ||
698 | } | ||
693 | } | 699 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index b7ffbd3b1..081b21f2d 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -17,6 +17,7 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
20 | import { MAccountActor } from '@server/types/models' | 21 | import { MAccountActor } from '@server/types/models' |
21 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 22 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
22 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' | 23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
@@ -653,7 +654,6 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
653 | description: this.description, | 654 | description: this.description, |
654 | support: this.support, | 655 | support: this.support, |
655 | isLocal: this.Actor.isOwned(), | 656 | isLocal: this.Actor.isOwned(), |
656 | createdAt: this.createdAt, | ||
657 | updatedAt: this.updatedAt, | 657 | updatedAt: this.updatedAt, |
658 | ownerAccount: undefined, | 658 | ownerAccount: undefined, |
659 | videosCount, | 659 | videosCount, |
@@ -691,4 +691,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
691 | isOutdated () { | 691 | isOutdated () { |
692 | return this.Actor.isOutdated() | 692 | return this.Actor.isOutdated() |
693 | } | 693 | } |
694 | |||
695 | setAsUpdated (transaction: Transaction) { | ||
696 | return setAsUpdated('videoChannel', this.id, transaction) | ||
697 | } | ||
694 | } | 698 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e55a21a6b..8c316e00c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -24,6 +24,7 @@ import { | |||
24 | Table, | 24 | Table, |
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
27 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 28 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
29 | import { LiveManager } from '@server/lib/live-manager' | 30 | import { LiveManager } from '@server/lib/live-manager' |
@@ -2053,11 +2054,7 @@ export class VideoModel extends Model { | |||
2053 | } | 2054 | } |
2054 | 2055 | ||
2055 | setAsRefreshed () { | 2056 | setAsRefreshed () { |
2056 | const options = { | 2057 | return setAsUpdated('video', this.id) |
2057 | where: { id: this.id } | ||
2058 | } | ||
2059 | |||
2060 | return VideoModel.update({ updatedAt: new Date() }, options) | ||
2061 | } | 2058 | } |
2062 | 2059 | ||
2063 | requiresAuth () { | 2060 | requiresAuth () { |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index d0b0b9c21..143515838 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -13,6 +13,7 @@ import './plugins' | |||
13 | import './redundancy' | 13 | import './redundancy' |
14 | import './search' | 14 | import './search' |
15 | import './services' | 15 | import './services' |
16 | import './upload-quota' | ||
16 | import './user-notifications' | 17 | import './user-notifications' |
17 | import './user-subscriptions' | 18 | import './user-subscriptions' |
18 | import './users' | 19 | import './users' |
diff --git a/server/tests/api/check-params/upload-quota.ts b/server/tests/api/check-params/upload-quota.ts new file mode 100644 index 000000000..d0fbec415 --- /dev/null +++ b/server/tests/api/check-params/upload-quota.ts | |||
@@ -0,0 +1,152 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { HttpStatusCode, randomInt } from '@shared/core-utils' | ||
6 | import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '@shared/extra-utils/videos/video-imports' | ||
7 | import { MyUser, VideoImport, VideoImportState, VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | flushAndRunServer, | ||
11 | getMyUserInformation, | ||
12 | immutableAssign, | ||
13 | registerUser, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultVideoChannel, | ||
17 | updateUser, | ||
18 | uploadVideo, | ||
19 | userLogin, | ||
20 | waitJobs | ||
21 | } from '../../../../shared/extra-utils' | ||
22 | |||
23 | describe('Test upload quota', function () { | ||
24 | let server: ServerInfo | ||
25 | let rootId: number | ||
26 | |||
27 | // --------------------------------------------------------------- | ||
28 | |||
29 | before(async function () { | ||
30 | this.timeout(30000) | ||
31 | |||
32 | server = await flushAndRunServer(1) | ||
33 | await setAccessTokensToServers([ server ]) | ||
34 | await setDefaultVideoChannel([ server ]) | ||
35 | |||
36 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
37 | rootId = (res.body as MyUser).id | ||
38 | |||
39 | await updateUser({ | ||
40 | url: server.url, | ||
41 | userId: rootId, | ||
42 | accessToken: server.accessToken, | ||
43 | videoQuota: 42 | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | describe('When having a video quota', function () { | ||
48 | |||
49 | it('Should fail with a registered user having too many videos with legacy upload', async function () { | ||
50 | this.timeout(30000) | ||
51 | |||
52 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
53 | await registerUser(server.url, user.username, user.password) | ||
54 | const userAccessToken = await userLogin(server, user) | ||
55 | |||
56 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
57 | for (let i = 0; i < 5; i++) { | ||
58 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
59 | } | ||
60 | |||
61 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
62 | }) | ||
63 | |||
64 | it('Should fail with a registered user having too many videos with resumable upload', async function () { | ||
65 | this.timeout(30000) | ||
66 | |||
67 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
68 | await registerUser(server.url, user.username, user.password) | ||
69 | const userAccessToken = await userLogin(server, user) | ||
70 | |||
71 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
72 | for (let i = 0; i < 5; i++) { | ||
73 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
74 | } | ||
75 | |||
76 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
77 | }) | ||
78 | |||
79 | it('Should fail to import with HTTP/Torrent/magnet', async function () { | ||
80 | this.timeout(120000) | ||
81 | |||
82 | const baseAttributes = { | ||
83 | channelId: server.videoChannel.id, | ||
84 | privacy: VideoPrivacy.PUBLIC | ||
85 | } | ||
86 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() })) | ||
87 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() })) | ||
88 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any })) | ||
89 | |||
90 | await waitJobs([ server ]) | ||
91 | |||
92 | const res = await getMyVideoImports(server.url, server.accessToken) | ||
93 | |||
94 | expect(res.body.total).to.equal(3) | ||
95 | const videoImports: VideoImport[] = res.body.data | ||
96 | expect(videoImports).to.have.lengthOf(3) | ||
97 | |||
98 | for (const videoImport of videoImports) { | ||
99 | expect(videoImport.state.id).to.equal(VideoImportState.FAILED) | ||
100 | expect(videoImport.error).not.to.be.undefined | ||
101 | expect(videoImport.error).to.contain('user video quota is exceeded') | ||
102 | } | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | describe('When having a daily video quota', function () { | ||
107 | |||
108 | it('Should fail with a user having too many videos daily', async function () { | ||
109 | await updateUser({ | ||
110 | url: server.url, | ||
111 | userId: rootId, | ||
112 | accessToken: server.accessToken, | ||
113 | videoQuotaDaily: 42 | ||
114 | }) | ||
115 | |||
116 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
117 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | describe('When having an absolute and daily video quota', function () { | ||
122 | it('Should fail if exceeding total quota', async function () { | ||
123 | await updateUser({ | ||
124 | url: server.url, | ||
125 | userId: rootId, | ||
126 | accessToken: server.accessToken, | ||
127 | videoQuota: 42, | ||
128 | videoQuotaDaily: 1024 * 1024 * 1024 | ||
129 | }) | ||
130 | |||
131 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
132 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
133 | }) | ||
134 | |||
135 | it('Should fail if exceeding daily quota', async function () { | ||
136 | await updateUser({ | ||
137 | url: server.url, | ||
138 | userId: rootId, | ||
139 | accessToken: server.accessToken, | ||
140 | videoQuota: 1024 * 1024 * 1024, | ||
141 | videoQuotaDaily: 42 | ||
142 | }) | ||
143 | |||
144 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
145 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
146 | }) | ||
147 | }) | ||
148 | |||
149 | after(async function () { | ||
150 | await cleanupTests([ server ]) | ||
151 | }) | ||
152 | }) | ||
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 2b03fde2d..dcff0d52b 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | ||
5 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
6 | import { join } from 'path' | 5 | import { join } from 'path' |
7 | import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared' | 6 | import { User, UserRole } from '../../../../shared' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
8 | import { | 8 | import { |
9 | addVideoChannel, | 9 | addVideoChannel, |
10 | blockUser, | 10 | blockUser, |
@@ -29,7 +29,6 @@ import { | |||
29 | ServerInfo, | 29 | ServerInfo, |
30 | setAccessTokensToServers, | 30 | setAccessTokensToServers, |
31 | unblockUser, | 31 | unblockUser, |
32 | updateUser, | ||
33 | uploadVideo, | 32 | uploadVideo, |
34 | userLogin | 33 | userLogin |
35 | } from '../../../../shared/extra-utils' | 34 | } from '../../../../shared/extra-utils' |
@@ -39,11 +38,7 @@ import { | |||
39 | checkBadSortPagination, | 38 | checkBadSortPagination, |
40 | checkBadStartPagination | 39 | checkBadStartPagination |
41 | } from '../../../../shared/extra-utils/requests/check-api-params' | 40 | } from '../../../../shared/extra-utils/requests/check-api-params' |
42 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
43 | import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '../../../../shared/extra-utils/videos/video-imports' | ||
44 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 41 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
45 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
46 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
47 | 42 | ||
48 | describe('Test users API validators', function () { | 43 | describe('Test users API validators', function () { |
49 | const path = '/api/v1/users/' | 44 | const path = '/api/v1/users/' |
@@ -1093,102 +1088,6 @@ describe('Test users API validators', function () { | |||
1093 | }) | 1088 | }) |
1094 | }) | 1089 | }) |
1095 | 1090 | ||
1096 | describe('When having a video quota', function () { | ||
1097 | it('Should fail with a user having too many videos', async function () { | ||
1098 | await updateUser({ | ||
1099 | url: server.url, | ||
1100 | userId: rootId, | ||
1101 | accessToken: server.accessToken, | ||
1102 | videoQuota: 42 | ||
1103 | }) | ||
1104 | |||
1105 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1106 | }) | ||
1107 | |||
1108 | it('Should fail with a registered user having too many videos', async function () { | ||
1109 | this.timeout(30000) | ||
1110 | |||
1111 | const user = { | ||
1112 | username: 'user3', | ||
1113 | password: 'my super password' | ||
1114 | } | ||
1115 | userAccessToken = await userLogin(server, user) | ||
1116 | |||
1117 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
1118 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1119 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1120 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1121 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1122 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1123 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1124 | }) | ||
1125 | |||
1126 | it('Should fail to import with HTTP/Torrent/magnet', async function () { | ||
1127 | this.timeout(120000) | ||
1128 | |||
1129 | const baseAttributes = { | ||
1130 | channelId: 1, | ||
1131 | privacy: VideoPrivacy.PUBLIC | ||
1132 | } | ||
1133 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() })) | ||
1134 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() })) | ||
1135 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any })) | ||
1136 | |||
1137 | await waitJobs([ server ]) | ||
1138 | |||
1139 | const res = await getMyVideoImports(server.url, server.accessToken) | ||
1140 | |||
1141 | expect(res.body.total).to.equal(3) | ||
1142 | const videoImports: VideoImport[] = res.body.data | ||
1143 | expect(videoImports).to.have.lengthOf(3) | ||
1144 | |||
1145 | for (const videoImport of videoImports) { | ||
1146 | expect(videoImport.state.id).to.equal(VideoImportState.FAILED) | ||
1147 | expect(videoImport.error).not.to.be.undefined | ||
1148 | expect(videoImport.error).to.contain('user video quota is exceeded') | ||
1149 | } | ||
1150 | }) | ||
1151 | }) | ||
1152 | |||
1153 | describe('When having a daily video quota', function () { | ||
1154 | it('Should fail with a user having too many videos daily', async function () { | ||
1155 | await updateUser({ | ||
1156 | url: server.url, | ||
1157 | userId: rootId, | ||
1158 | accessToken: server.accessToken, | ||
1159 | videoQuotaDaily: 42 | ||
1160 | }) | ||
1161 | |||
1162 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1163 | }) | ||
1164 | }) | ||
1165 | |||
1166 | describe('When having an absolute and daily video quota', function () { | ||
1167 | it('Should fail if exceeding total quota', async function () { | ||
1168 | await updateUser({ | ||
1169 | url: server.url, | ||
1170 | userId: rootId, | ||
1171 | accessToken: server.accessToken, | ||
1172 | videoQuota: 42, | ||
1173 | videoQuotaDaily: 1024 * 1024 * 1024 | ||
1174 | }) | ||
1175 | |||
1176 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1177 | }) | ||
1178 | |||
1179 | it('Should fail if exceeding daily quota', async function () { | ||
1180 | await updateUser({ | ||
1181 | url: server.url, | ||
1182 | userId: rootId, | ||
1183 | accessToken: server.accessToken, | ||
1184 | videoQuota: 1024 * 1024 * 1024, | ||
1185 | videoQuotaDaily: 42 | ||
1186 | }) | ||
1187 | |||
1188 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1189 | }) | ||
1190 | }) | ||
1191 | |||
1192 | describe('When asking a password reset', function () { | 1091 | describe('When asking a password reset', function () { |
1193 | const path = '/api/v1/users/ask-reset-password' | 1092 | const path = '/api/v1/users/ask-reset-password' |
1194 | 1093 | ||
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 188d1835c..c970c4a15 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | ||
3 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
4 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
5 | import 'mocha' | ||
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | 7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { | 8 | import { |
9 | checkUploadVideoParam, | ||
9 | cleanupTests, | 10 | cleanupTests, |
10 | createUser, | 11 | createUser, |
11 | flushAndRunServer, | 12 | flushAndRunServer, |
@@ -18,17 +19,18 @@ import { | |||
18 | makePutBodyRequest, | 19 | makePutBodyRequest, |
19 | makeUploadRequest, | 20 | makeUploadRequest, |
20 | removeVideo, | 21 | removeVideo, |
22 | root, | ||
21 | ServerInfo, | 23 | ServerInfo, |
22 | setAccessTokensToServers, | 24 | setAccessTokensToServers, |
23 | userLogin, | 25 | userLogin |
24 | root | ||
25 | } from '../../../../shared/extra-utils' | 26 | } from '../../../../shared/extra-utils' |
26 | import { | 27 | import { |
27 | checkBadCountPagination, | 28 | checkBadCountPagination, |
28 | checkBadSortPagination, | 29 | checkBadSortPagination, |
29 | checkBadStartPagination | 30 | checkBadStartPagination |
30 | } from '../../../../shared/extra-utils/requests/check-api-params' | 31 | } from '../../../../shared/extra-utils/requests/check-api-params' |
31 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 32 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' |
33 | import { randomInt } from '@shared/core-utils' | ||
32 | 34 | ||
33 | const expect = chai.expect | 35 | const expect = chai.expect |
34 | 36 | ||
@@ -183,7 +185,7 @@ describe('Test videos API validator', function () { | |||
183 | describe('When adding a video', function () { | 185 | describe('When adding a video', function () { |
184 | let baseCorrectParams | 186 | let baseCorrectParams |
185 | const baseCorrectAttaches = { | 187 | const baseCorrectAttaches = { |
186 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') | 188 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') |
187 | } | 189 | } |
188 | 190 | ||
189 | before(function () { | 191 | before(function () { |
@@ -206,256 +208,243 @@ describe('Test videos API validator', function () { | |||
206 | } | 208 | } |
207 | }) | 209 | }) |
208 | 210 | ||
209 | it('Should fail with nothing', async function () { | 211 | function runSuite (mode: 'legacy' | 'resumable') { |
210 | const fields = {} | ||
211 | const attaches = {} | ||
212 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
213 | }) | ||
214 | 212 | ||
215 | it('Should fail without name', async function () { | 213 | it('Should fail with nothing', async function () { |
216 | const fields = omit(baseCorrectParams, 'name') | 214 | const fields = {} |
217 | const attaches = baseCorrectAttaches | 215 | const attaches = {} |
216 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) | ||
217 | }) | ||
218 | 218 | ||
219 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 219 | it('Should fail without name', async function () { |
220 | }) | 220 | const fields = omit(baseCorrectParams, 'name') |
221 | const attaches = baseCorrectAttaches | ||
221 | 222 | ||
222 | it('Should fail with a long name', async function () { | 223 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
223 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) | 224 | }) |
224 | const attaches = baseCorrectAttaches | ||
225 | 225 | ||
226 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 226 | it('Should fail with a long name', async function () { |
227 | }) | 227 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) |
228 | const attaches = baseCorrectAttaches | ||
228 | 229 | ||
229 | it('Should fail with a bad category', async function () { | 230 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
230 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) | 231 | }) |
231 | const attaches = baseCorrectAttaches | ||
232 | 232 | ||
233 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 233 | it('Should fail with a bad category', async function () { |
234 | }) | 234 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) |
235 | const attaches = baseCorrectAttaches | ||
235 | 236 | ||
236 | it('Should fail with a bad licence', async function () { | 237 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
237 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) | 238 | }) |
238 | const attaches = baseCorrectAttaches | ||
239 | 239 | ||
240 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 240 | it('Should fail with a bad licence', async function () { |
241 | }) | 241 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) |
242 | const attaches = baseCorrectAttaches | ||
242 | 243 | ||
243 | it('Should fail with a bad language', async function () { | 244 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
244 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) | 245 | }) |
245 | const attaches = baseCorrectAttaches | ||
246 | 246 | ||
247 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 247 | it('Should fail with a bad language', async function () { |
248 | }) | 248 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) |
249 | const attaches = baseCorrectAttaches | ||
249 | 250 | ||
250 | it('Should fail with a long description', async function () { | 251 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
251 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) | 252 | }) |
252 | const attaches = baseCorrectAttaches | ||
253 | 253 | ||
254 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 254 | it('Should fail with a long description', async function () { |
255 | }) | 255 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) |
256 | const attaches = baseCorrectAttaches | ||
256 | 257 | ||
257 | it('Should fail with a long support text', async function () { | 258 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
258 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) | 259 | }) |
259 | const attaches = baseCorrectAttaches | ||
260 | 260 | ||
261 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 261 | it('Should fail with a long support text', async function () { |
262 | }) | 262 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
263 | const attaches = baseCorrectAttaches | ||
263 | 264 | ||
264 | it('Should fail without a channel', async function () { | 265 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
265 | const fields = omit(baseCorrectParams, 'channelId') | 266 | }) |
266 | const attaches = baseCorrectAttaches | ||
267 | 267 | ||
268 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 268 | it('Should fail without a channel', async function () { |
269 | }) | 269 | const fields = omit(baseCorrectParams, 'channelId') |
270 | const attaches = baseCorrectAttaches | ||
270 | 271 | ||
271 | it('Should fail with a bad channel', async function () { | 272 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
272 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) | 273 | }) |
273 | const attaches = baseCorrectAttaches | ||
274 | 274 | ||
275 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 275 | it('Should fail with a bad channel', async function () { |
276 | }) | 276 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) |
277 | const attaches = baseCorrectAttaches | ||
277 | 278 | ||
278 | it('Should fail with another user channel', async function () { | 279 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
279 | const user = { | 280 | }) |
280 | username: 'fake', | ||
281 | password: 'fake_password' | ||
282 | } | ||
283 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
284 | 281 | ||
285 | const accessTokenUser = await userLogin(server, user) | 282 | it('Should fail with another user channel', async function () { |
286 | const res = await getMyUserInformation(server.url, accessTokenUser) | 283 | const user = { |
287 | const customChannelId = res.body.videoChannels[0].id | 284 | username: 'fake' + randomInt(0, 1500), |
285 | password: 'fake_password' | ||
286 | } | ||
287 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
288 | 288 | ||
289 | const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) | 289 | const accessTokenUser = await userLogin(server, user) |
290 | const attaches = baseCorrectAttaches | 290 | const res = await getMyUserInformation(server.url, accessTokenUser) |
291 | const customChannelId = res.body.videoChannels[0].id | ||
291 | 292 | ||
292 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: userAccessToken, fields, attaches }) | 293 | const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) |
293 | }) | 294 | const attaches = baseCorrectAttaches |
294 | 295 | ||
295 | it('Should fail with too many tags', async function () { | 296 | await checkUploadVideoParam(server.url, userAccessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
296 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) | 297 | }) |
297 | const attaches = baseCorrectAttaches | ||
298 | 298 | ||
299 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 299 | it('Should fail with too many tags', async function () { |
300 | }) | 300 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) |
301 | const attaches = baseCorrectAttaches | ||
301 | 302 | ||
302 | it('Should fail with a tag length too low', async function () { | 303 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
303 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) | 304 | }) |
304 | const attaches = baseCorrectAttaches | ||
305 | 305 | ||
306 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 306 | it('Should fail with a tag length too low', async function () { |
307 | }) | 307 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) |
308 | const attaches = baseCorrectAttaches | ||
308 | 309 | ||
309 | it('Should fail with a tag length too big', async function () { | 310 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
310 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) | 311 | }) |
311 | const attaches = baseCorrectAttaches | ||
312 | 312 | ||
313 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 313 | it('Should fail with a tag length too big', async function () { |
314 | }) | 314 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) |
315 | const attaches = baseCorrectAttaches | ||
315 | 316 | ||
316 | it('Should fail with a bad schedule update (miss updateAt)', async function () { | 317 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
317 | const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC }) | 318 | }) |
318 | const attaches = baseCorrectAttaches | ||
319 | 319 | ||
320 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 320 | it('Should fail with a bad schedule update (miss updateAt)', async function () { |
321 | }) | 321 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }) |
322 | const attaches = baseCorrectAttaches | ||
322 | 323 | ||
323 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { | 324 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
324 | const fields = immutableAssign(baseCorrectParams, { | ||
325 | 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC, | ||
326 | 'scheduleUpdate[updateAt]': 'toto' | ||
327 | }) | 325 | }) |
328 | const attaches = baseCorrectAttaches | ||
329 | 326 | ||
330 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 327 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { |
331 | }) | 328 | const fields = immutableAssign(baseCorrectParams, { |
329 | scheduleUpdate: { | ||
330 | privacy: VideoPrivacy.PUBLIC, | ||
331 | updateAt: 'toto' | ||
332 | } | ||
333 | }) | ||
334 | const attaches = baseCorrectAttaches | ||
332 | 335 | ||
333 | it('Should fail with a bad originally published at attribute', async function () { | 336 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
334 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) | 337 | }) |
335 | const attaches = baseCorrectAttaches | ||
336 | 338 | ||
337 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 339 | it('Should fail with a bad originally published at attribute', async function () { |
338 | }) | 340 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) |
341 | const attaches = baseCorrectAttaches | ||
339 | 342 | ||
340 | it('Should fail without an input file', async function () { | 343 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
341 | const fields = baseCorrectParams | 344 | }) |
342 | const attaches = {} | ||
343 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
344 | }) | ||
345 | 345 | ||
346 | it('Should fail with an incorrect input file', async function () { | 346 | it('Should fail without an input file', async function () { |
347 | const fields = baseCorrectParams | 347 | const fields = baseCorrectParams |
348 | let attaches = { | 348 | const attaches = {} |
349 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') | 349 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
350 | } | ||
351 | await makeUploadRequest({ | ||
352 | url: server.url, | ||
353 | path: path + '/upload', | ||
354 | token: server.accessToken, | ||
355 | fields, | ||
356 | attaches, | ||
357 | statusCodeExpected: HttpStatusCode.UNPROCESSABLE_ENTITY_422 | ||
358 | }) | 350 | }) |
359 | 351 | ||
360 | attaches = { | 352 | it('Should fail with an incorrect input file', async function () { |
361 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') | 353 | const fields = baseCorrectParams |
362 | } | 354 | let attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') } |
363 | await makeUploadRequest({ | 355 | |
364 | url: server.url, | 356 | await checkUploadVideoParam( |
365 | path: path + '/upload', | 357 | server.url, |
366 | token: server.accessToken, | 358 | server.accessToken, |
367 | fields, | 359 | { ...fields, ...attaches }, |
368 | attaches, | 360 | HttpStatusCode.UNPROCESSABLE_ENTITY_422, |
369 | statusCodeExpected: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 | 361 | mode |
362 | ) | ||
363 | |||
364 | attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') } | ||
365 | await checkUploadVideoParam( | ||
366 | server.url, | ||
367 | server.accessToken, | ||
368 | { ...fields, ...attaches }, | ||
369 | HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, | ||
370 | mode | ||
371 | ) | ||
370 | }) | 372 | }) |
371 | }) | ||
372 | 373 | ||
373 | it('Should fail with an incorrect thumbnail file', async function () { | 374 | it('Should fail with an incorrect thumbnail file', async function () { |
374 | const fields = baseCorrectParams | 375 | const fields = baseCorrectParams |
375 | const attaches = { | 376 | const attaches = { |
376 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), | 377 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), |
377 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 378 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
378 | } | 379 | } |
379 | 380 | ||
380 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 381 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
381 | }) | 382 | }) |
382 | 383 | ||
383 | it('Should fail with a big thumbnail file', async function () { | 384 | it('Should fail with a big thumbnail file', async function () { |
384 | const fields = baseCorrectParams | 385 | const fields = baseCorrectParams |
385 | const attaches = { | 386 | const attaches = { |
386 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), |
387 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
388 | } | 389 | } |
389 | 390 | ||
390 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 391 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
391 | }) | 392 | }) |
392 | 393 | ||
393 | it('Should fail with an incorrect preview file', async function () { | 394 | it('Should fail with an incorrect preview file', async function () { |
394 | const fields = baseCorrectParams | 395 | const fields = baseCorrectParams |
395 | const attaches = { | 396 | const attaches = { |
396 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), | 397 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), |
397 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 398 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
398 | } | 399 | } |
399 | 400 | ||
400 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 401 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
401 | }) | 402 | }) |
402 | 403 | ||
403 | it('Should fail with a big preview file', async function () { | 404 | it('Should fail with a big preview file', async function () { |
404 | const fields = baseCorrectParams | 405 | const fields = baseCorrectParams |
405 | const attaches = { | 406 | const attaches = { |
406 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), |
407 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
408 | } | 409 | } |
409 | 410 | ||
410 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 411 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
411 | }) | 412 | }) |
412 | 413 | ||
413 | it('Should succeed with the correct parameters', async function () { | 414 | it('Should succeed with the correct parameters', async function () { |
414 | this.timeout(10000) | 415 | this.timeout(10000) |
415 | 416 | ||
416 | const fields = baseCorrectParams | 417 | const fields = baseCorrectParams |
417 | 418 | ||
418 | { | 419 | { |
419 | const attaches = baseCorrectAttaches | 420 | const attaches = baseCorrectAttaches |
420 | await makeUploadRequest({ | 421 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
421 | url: server.url, | 422 | } |
422 | path: path + '/upload', | ||
423 | token: server.accessToken, | ||
424 | fields, | ||
425 | attaches, | ||
426 | statusCodeExpected: HttpStatusCode.OK_200 | ||
427 | }) | ||
428 | } | ||
429 | 423 | ||
430 | { | 424 | { |
431 | const attaches = immutableAssign(baseCorrectAttaches, { | 425 | const attaches = immutableAssign(baseCorrectAttaches, { |
432 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 426 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
433 | }) | 427 | }) |
434 | 428 | ||
435 | await makeUploadRequest({ | 429 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
436 | url: server.url, | 430 | } |
437 | path: path + '/upload', | ||
438 | token: server.accessToken, | ||
439 | fields, | ||
440 | attaches, | ||
441 | statusCodeExpected: HttpStatusCode.OK_200 | ||
442 | }) | ||
443 | } | ||
444 | 431 | ||
445 | { | 432 | { |
446 | const attaches = immutableAssign(baseCorrectAttaches, { | 433 | const attaches = immutableAssign(baseCorrectAttaches, { |
447 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv') | 434 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv') |
448 | }) | 435 | }) |
449 | 436 | ||
450 | await makeUploadRequest({ | 437 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
451 | url: server.url, | 438 | } |
452 | path: path + '/upload', | 439 | }) |
453 | token: server.accessToken, | 440 | } |
454 | fields, | 441 | |
455 | attaches, | 442 | describe('Resumable upload', function () { |
456 | statusCodeExpected: HttpStatusCode.OK_200 | 443 | runSuite('resumable') |
457 | }) | 444 | }) |
458 | } | 445 | |
446 | describe('Legacy upload', function () { | ||
447 | runSuite('legacy') | ||
459 | }) | 448 | }) |
460 | }) | 449 | }) |
461 | 450 | ||
@@ -678,7 +667,7 @@ describe('Test videos API validator', function () { | |||
678 | }) | 667 | }) |
679 | 668 | ||
680 | expect(res.body.data).to.be.an('array') | 669 | expect(res.body.data).to.be.an('array') |
681 | expect(res.body.data.length).to.equal(3) | 670 | expect(res.body.data.length).to.equal(6) |
682 | }) | 671 | }) |
683 | 672 | ||
684 | it('Should fail without a correct uuid', async function () { | 673 | it('Should fail without a correct uuid', async function () { |
diff --git a/server/tests/api/live/live-constraints.ts b/server/tests/api/live/live-constraints.ts index 5569e6066..cc635de33 100644 --- a/server/tests/api/live/live-constraints.ts +++ b/server/tests/api/live/live-constraints.ts | |||
@@ -2,15 +2,15 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { User, VideoDetails, VideoPrivacy } from '@shared/models' | 5 | import { VideoDetails, VideoPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | checkLiveCleanup, | 7 | checkLiveCleanup, |
8 | cleanupTests, | 8 | cleanupTests, |
9 | createLive, | 9 | createLive, |
10 | createUser, | ||
11 | doubleFollow, | 10 | doubleFollow, |
12 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
13 | getMyUserInformation, | 12 | generateUser, |
13 | getCustomConfigResolutions, | ||
14 | getVideo, | 14 | getVideo, |
15 | runAndTestFfmpegStreamError, | 15 | runAndTestFfmpegStreamError, |
16 | ServerInfo, | 16 | ServerInfo, |
@@ -18,7 +18,6 @@ import { | |||
18 | setDefaultVideoChannel, | 18 | setDefaultVideoChannel, |
19 | updateCustomSubConfig, | 19 | updateCustomSubConfig, |
20 | updateUser, | 20 | updateUser, |
21 | userLogin, | ||
22 | wait, | 21 | wait, |
23 | waitJobs, | 22 | waitJobs, |
24 | waitUntilLivePublished | 23 | waitUntilLivePublished |
@@ -62,6 +61,16 @@ describe('Test live constraints', function () { | |||
62 | } | 61 | } |
63 | } | 62 | } |
64 | 63 | ||
64 | function updateQuota (options: { total: number, daily: number }) { | ||
65 | return updateUser({ | ||
66 | url: servers[0].url, | ||
67 | accessToken: servers[0].accessToken, | ||
68 | userId, | ||
69 | videoQuota: options.total, | ||
70 | videoQuotaDaily: options.daily | ||
71 | }) | ||
72 | } | ||
73 | |||
65 | before(async function () { | 74 | before(async function () { |
66 | this.timeout(120000) | 75 | this.timeout(120000) |
67 | 76 | ||
@@ -82,27 +91,12 @@ describe('Test live constraints', function () { | |||
82 | }) | 91 | }) |
83 | 92 | ||
84 | { | 93 | { |
85 | const user = { username: 'user1', password: 'superpassword' } | 94 | const res = await generateUser(servers[0], 'user1') |
86 | const res = await createUser({ | 95 | userId = res.userId |
87 | url: servers[0].url, | 96 | userChannelId = res.userChannelId |
88 | accessToken: servers[0].accessToken, | 97 | userAccessToken = res.token |
89 | username: user.username, | 98 | |
90 | password: user.password | 99 | await updateQuota({ total: 1, daily: -1 }) |
91 | }) | ||
92 | userId = res.body.user.id | ||
93 | |||
94 | userAccessToken = await userLogin(servers[0], user) | ||
95 | |||
96 | const resMe = await getMyUserInformation(servers[0].url, userAccessToken) | ||
97 | userChannelId = (resMe.body as User).videoChannels[0].id | ||
98 | |||
99 | await updateUser({ | ||
100 | url: servers[0].url, | ||
101 | userId, | ||
102 | accessToken: servers[0].accessToken, | ||
103 | videoQuota: 1, | ||
104 | videoQuotaDaily: -1 | ||
105 | }) | ||
106 | } | 100 | } |
107 | 101 | ||
108 | // Server 1 and server 2 follow each other | 102 | // Server 1 and server 2 follow each other |
@@ -137,13 +131,7 @@ describe('Test live constraints', function () { | |||
137 | // Wait for user quota memoize cache invalidation | 131 | // Wait for user quota memoize cache invalidation |
138 | await wait(5000) | 132 | await wait(5000) |
139 | 133 | ||
140 | await updateUser({ | 134 | await updateQuota({ total: -1, daily: 1 }) |
141 | url: servers[0].url, | ||
142 | userId, | ||
143 | accessToken: servers[0].accessToken, | ||
144 | videoQuota: -1, | ||
145 | videoQuotaDaily: 1 | ||
146 | }) | ||
147 | 135 | ||
148 | const userVideoLiveoId = await createLiveWrapper(true) | 136 | const userVideoLiveoId = await createLiveWrapper(true) |
149 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | 137 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) |
@@ -160,13 +148,7 @@ describe('Test live constraints', function () { | |||
160 | // Wait for user quota memoize cache invalidation | 148 | // Wait for user quota memoize cache invalidation |
161 | await wait(5000) | 149 | await wait(5000) |
162 | 150 | ||
163 | await updateUser({ | 151 | await updateQuota({ total: 10 * 1000 * 1000, daily: -1 }) |
164 | url: servers[0].url, | ||
165 | userId, | ||
166 | accessToken: servers[0].accessToken, | ||
167 | videoQuota: 10 * 1000 * 1000, | ||
168 | videoQuotaDaily: -1 | ||
169 | }) | ||
170 | 152 | ||
171 | const userVideoLiveoId = await createLiveWrapper(true) | 153 | const userVideoLiveoId = await createLiveWrapper(true) |
172 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | 154 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) |
@@ -182,15 +164,7 @@ describe('Test live constraints', function () { | |||
182 | maxDuration: 1, | 164 | maxDuration: 1, |
183 | transcoding: { | 165 | transcoding: { |
184 | enabled: true, | 166 | enabled: true, |
185 | resolutions: { | 167 | resolutions: getCustomConfigResolutions(true) |
186 | '240p': true, | ||
187 | '360p': true, | ||
188 | '480p': true, | ||
189 | '720p': true, | ||
190 | '1080p': true, | ||
191 | '1440p': true, | ||
192 | '2160p': true | ||
193 | } | ||
194 | } | 168 | } |
195 | } | 169 | } |
196 | }) | 170 | }) |
diff --git a/server/tests/api/live/live-permanent.ts b/server/tests/api/live/live-permanent.ts index a5bda009f..d52e8c7e4 100644 --- a/server/tests/api/live/live-permanent.ts +++ b/server/tests/api/live/live-permanent.ts | |||
@@ -8,6 +8,7 @@ import { | |||
8 | createLive, | 8 | createLive, |
9 | doubleFollow, | 9 | doubleFollow, |
10 | flushAndRunMultipleServers, | 10 | flushAndRunMultipleServers, |
11 | getCustomConfigResolutions, | ||
11 | getLive, | 12 | getLive, |
12 | getPlaylistsCount, | 13 | getPlaylistsCount, |
13 | getVideo, | 14 | getVideo, |
@@ -69,15 +70,7 @@ describe('Permenant live', function () { | |||
69 | maxDuration: -1, | 70 | maxDuration: -1, |
70 | transcoding: { | 71 | transcoding: { |
71 | enabled: true, | 72 | enabled: true, |
72 | resolutions: { | 73 | resolutions: getCustomConfigResolutions(true) |
73 | '240p': true, | ||
74 | '360p': true, | ||
75 | '480p': true, | ||
76 | '720p': true, | ||
77 | '1080p': true, | ||
78 | '1440p': true, | ||
79 | '2160p': true | ||
80 | } | ||
81 | } | 74 | } |
82 | } | 75 | } |
83 | }) | 76 | }) |
@@ -159,15 +152,7 @@ describe('Permenant live', function () { | |||
159 | maxDuration: -1, | 152 | maxDuration: -1, |
160 | transcoding: { | 153 | transcoding: { |
161 | enabled: true, | 154 | enabled: true, |
162 | resolutions: { | 155 | resolutions: getCustomConfigResolutions(false) |
163 | '240p': false, | ||
164 | '360p': false, | ||
165 | '480p': false, | ||
166 | '720p': false, | ||
167 | '1080p': false, | ||
168 | '1440p': false, | ||
169 | '2160p': false | ||
170 | } | ||
171 | } | 156 | } |
172 | } | 157 | } |
173 | }) | 158 | }) |
diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts index 61c8e74dd..3d4736c8f 100644 --- a/server/tests/api/live/live-save-replay.ts +++ b/server/tests/api/live/live-save-replay.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | createLive, | 12 | createLive, |
13 | doubleFollow, | 13 | doubleFollow, |
14 | flushAndRunMultipleServers, | 14 | flushAndRunMultipleServers, |
15 | getCustomConfigResolutions, | ||
15 | getVideo, | 16 | getVideo, |
16 | getVideosList, | 17 | getVideosList, |
17 | removeVideo, | 18 | removeVideo, |
@@ -108,15 +109,7 @@ describe('Save replay setting', function () { | |||
108 | maxDuration: -1, | 109 | maxDuration: -1, |
109 | transcoding: { | 110 | transcoding: { |
110 | enabled: false, | 111 | enabled: false, |
111 | resolutions: { | 112 | resolutions: getCustomConfigResolutions(true) |
112 | '240p': true, | ||
113 | '360p': true, | ||
114 | '480p': true, | ||
115 | '720p': true, | ||
116 | '1080p': true, | ||
117 | '1440p': true, | ||
118 | '2160p': true | ||
119 | } | ||
120 | } | 113 | } |
121 | } | 114 | } |
122 | }) | 115 | }) |
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts index 0846b04f4..8a91fbba3 100644 --- a/server/tests/api/server/follow-constraints.ts +++ b/server/tests/api/server/follow-constraints.ts | |||
@@ -28,7 +28,7 @@ describe('Test follow constraints', function () { | |||
28 | let userAccessToken: string | 28 | let userAccessToken: string |
29 | 29 | ||
30 | before(async function () { | 30 | before(async function () { |
31 | this.timeout(60000) | 31 | this.timeout(90000) |
32 | 32 | ||
33 | servers = await flushAndRunMultipleServers(2) | 33 | servers = await flushAndRunMultipleServers(2) |
34 | 34 | ||
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index dcd03879b..f60c66e4b 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -130,26 +130,32 @@ describe('Test users with multiple servers', function () { | |||
130 | }) | 130 | }) |
131 | 131 | ||
132 | it('Should have updated my profile on other servers too', async function () { | 132 | it('Should have updated my profile on other servers too', async function () { |
133 | let createdAt: string | Date | ||
134 | |||
133 | for (const server of servers) { | 135 | for (const server of servers) { |
134 | const resAccounts = await getAccountsList(server.url, '-createdAt') | 136 | const resAccounts = await getAccountsList(server.url, '-createdAt') |
135 | 137 | ||
136 | const rootServer1List = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:' + servers[0].port) as Account | 138 | const resList = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:' + servers[0].port) as Account |
137 | expect(rootServer1List).not.to.be.undefined | 139 | expect(resList).not.to.be.undefined |
140 | |||
141 | const resAccount = await getAccount(server.url, resList.name + '@' + resList.host) | ||
142 | const account = resAccount.body as Account | ||
143 | |||
144 | if (!createdAt) createdAt = account.createdAt | ||
138 | 145 | ||
139 | const resAccount = await getAccount(server.url, rootServer1List.name + '@' + rootServer1List.host) | 146 | expect(account.name).to.equal('root') |
140 | const rootServer1Get = resAccount.body as Account | 147 | expect(account.host).to.equal('localhost:' + servers[0].port) |
141 | expect(rootServer1Get.name).to.equal('root') | 148 | expect(account.displayName).to.equal('my super display name') |
142 | expect(rootServer1Get.host).to.equal('localhost:' + servers[0].port) | 149 | expect(account.description).to.equal('my super description updated') |
143 | expect(rootServer1Get.displayName).to.equal('my super display name') | 150 | expect(createdAt).to.equal(account.createdAt) |
144 | expect(rootServer1Get.description).to.equal('my super description updated') | ||
145 | 151 | ||
146 | if (server.serverNumber === 1) { | 152 | if (server.serverNumber === 1) { |
147 | expect(rootServer1Get.userId).to.be.a('number') | 153 | expect(account.userId).to.be.a('number') |
148 | } else { | 154 | } else { |
149 | expect(rootServer1Get.userId).to.be.undefined | 155 | expect(account.userId).to.be.undefined |
150 | } | 156 | } |
151 | 157 | ||
152 | await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') | 158 | await testImage(server.url, 'avatar2-resized', account.avatar.path, '.png') |
153 | } | 159 | } |
154 | }) | 160 | }) |
155 | 161 | ||
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index fc8b447b7..5c07f8926 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import './audio-only' | 1 | import './audio-only' |
2 | import './multiple-servers' | 2 | import './multiple-servers' |
3 | import './resumable-upload' | ||
3 | import './single-server' | 4 | import './single-server' |
4 | import './video-captions' | 5 | import './video-captions' |
5 | import './video-change-ownership' | 6 | import './video-change-ownership' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 55e280e9f..41cd814e0 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -181,7 +181,7 @@ describe('Test multiple servers', function () { | |||
181 | thumbnailfile: 'thumbnail.jpg', | 181 | thumbnailfile: 'thumbnail.jpg', |
182 | previewfile: 'preview.jpg' | 182 | previewfile: 'preview.jpg' |
183 | } | 183 | } |
184 | await uploadVideo(servers[1].url, userAccessToken, videoAttributes) | 184 | await uploadVideo(servers[1].url, userAccessToken, videoAttributes, HttpStatusCode.OK_200, 'resumable') |
185 | 185 | ||
186 | // Transcoding | 186 | // Transcoding |
187 | await waitJobs(servers) | 187 | await waitJobs(servers) |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts new file mode 100644 index 000000000..af9221c43 --- /dev/null +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -0,0 +1,187 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { pathExists, readdir, stat } from 'fs-extra' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode } from '@shared/core-utils' | ||
8 | import { | ||
9 | buildAbsoluteFixturePath, | ||
10 | buildServerDirectory, | ||
11 | flushAndRunServer, | ||
12 | getMyUserInformation, | ||
13 | prepareResumableUpload, | ||
14 | sendDebugCommand, | ||
15 | sendResumableChunks, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultVideoChannel, | ||
19 | updateUser | ||
20 | } from '@shared/extra-utils' | ||
21 | import { MyUser, VideoPrivacy } from '@shared/models' | ||
22 | |||
23 | const expect = chai.expect | ||
24 | |||
25 | // Most classic resumable upload tests are done in other test suites | ||
26 | |||
27 | describe('Test resumable upload', function () { | ||
28 | const defaultFixture = 'video_short.mp4' | ||
29 | let server: ServerInfo | ||
30 | let rootId: number | ||
31 | |||
32 | async function buildSize (fixture: string, size?: number) { | ||
33 | if (size !== undefined) return size | ||
34 | |||
35 | const baseFixture = buildAbsoluteFixturePath(fixture) | ||
36 | return (await stat(baseFixture)).size | ||
37 | } | ||
38 | |||
39 | async function prepareUpload (sizeArg?: number) { | ||
40 | const size = await buildSize(defaultFixture, sizeArg) | ||
41 | |||
42 | const attributes = { | ||
43 | name: 'video', | ||
44 | channelId: server.videoChannel.id, | ||
45 | privacy: VideoPrivacy.PUBLIC, | ||
46 | fixture: defaultFixture | ||
47 | } | ||
48 | |||
49 | const mimetype = 'video/mp4' | ||
50 | |||
51 | const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype }) | ||
52 | |||
53 | return res.header['location'].split('?')[1] | ||
54 | } | ||
55 | |||
56 | async function sendChunks (options: { | ||
57 | pathUploadId: string | ||
58 | size?: number | ||
59 | expectedStatus?: HttpStatusCode | ||
60 | contentLength?: number | ||
61 | contentRange?: string | ||
62 | contentRangeBuilder?: (start: number, chunk: any) => string | ||
63 | }) { | ||
64 | const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options | ||
65 | |||
66 | const size = await buildSize(defaultFixture, options.size) | ||
67 | const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture) | ||
68 | |||
69 | return sendResumableChunks({ | ||
70 | url: server.url, | ||
71 | token: server.accessToken, | ||
72 | pathUploadId, | ||
73 | videoFilePath: absoluteFilePath, | ||
74 | size, | ||
75 | contentLength, | ||
76 | contentRangeBuilder, | ||
77 | specialStatus: expectedStatus | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | async function checkFileSize (uploadIdArg: string, expectedSize: number | null) { | ||
82 | const uploadId = uploadIdArg.replace(/^upload_id=/, '') | ||
83 | |||
84 | const subPath = join('tmp', 'resumable-uploads', uploadId) | ||
85 | const filePath = buildServerDirectory(server, subPath) | ||
86 | const exists = await pathExists(filePath) | ||
87 | |||
88 | if (expectedSize === null) { | ||
89 | expect(exists).to.be.false | ||
90 | return | ||
91 | } | ||
92 | |||
93 | expect(exists).to.be.true | ||
94 | |||
95 | expect((await stat(filePath)).size).to.equal(expectedSize) | ||
96 | } | ||
97 | |||
98 | async function countResumableUploads () { | ||
99 | const subPath = join('tmp', 'resumable-uploads') | ||
100 | const filePath = buildServerDirectory(server, subPath) | ||
101 | |||
102 | const files = await readdir(filePath) | ||
103 | return files.length | ||
104 | } | ||
105 | |||
106 | before(async function () { | ||
107 | this.timeout(30000) | ||
108 | |||
109 | server = await flushAndRunServer(1) | ||
110 | await setAccessTokensToServers([ server ]) | ||
111 | await setDefaultVideoChannel([ server ]) | ||
112 | |||
113 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
114 | rootId = (res.body as MyUser).id | ||
115 | |||
116 | await updateUser({ | ||
117 | url: server.url, | ||
118 | userId: rootId, | ||
119 | accessToken: server.accessToken, | ||
120 | videoQuota: 10_000_000 | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | describe('Directory cleaning', function () { | ||
125 | |||
126 | it('Should correctly delete files after an upload', async function () { | ||
127 | const uploadId = await prepareUpload() | ||
128 | await sendChunks({ pathUploadId: uploadId }) | ||
129 | |||
130 | expect(await countResumableUploads()).to.equal(0) | ||
131 | }) | ||
132 | |||
133 | it('Should not delete files after an unfinished upload', async function () { | ||
134 | await prepareUpload() | ||
135 | |||
136 | expect(await countResumableUploads()).to.equal(2) | ||
137 | }) | ||
138 | |||
139 | it('Should not delete recent uploads', async function () { | ||
140 | await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' }) | ||
141 | |||
142 | expect(await countResumableUploads()).to.equal(2) | ||
143 | }) | ||
144 | |||
145 | it('Should delete old uploads', async function () { | ||
146 | await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' }) | ||
147 | |||
148 | expect(await countResumableUploads()).to.equal(0) | ||
149 | }) | ||
150 | }) | ||
151 | |||
152 | describe('Resumable upload and chunks', function () { | ||
153 | |||
154 | it('Should accept the same amount of chunks', async function () { | ||
155 | const uploadId = await prepareUpload() | ||
156 | await sendChunks({ pathUploadId: uploadId }) | ||
157 | |||
158 | await checkFileSize(uploadId, null) | ||
159 | }) | ||
160 | |||
161 | it('Should not accept more chunks than expected', async function () { | ||
162 | const size = 100 | ||
163 | const uploadId = await prepareUpload(size) | ||
164 | |||
165 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
166 | await checkFileSize(uploadId, 0) | ||
167 | }) | ||
168 | |||
169 | it('Should not accept more chunks than expected with an invalid content length/content range', async function () { | ||
170 | const uploadId = await prepareUpload(1500) | ||
171 | |||
172 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 }) | ||
173 | await checkFileSize(uploadId, 0) | ||
174 | }) | ||
175 | |||
176 | it('Should not accept more chunks than expected with an invalid content length', async function () { | ||
177 | const uploadId = await prepareUpload(500) | ||
178 | |||
179 | const size = 1000 | ||
180 | |||
181 | const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}` | ||
182 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size }) | ||
183 | await checkFileSize(uploadId, 0) | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | }) | ||
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index a79648bf7..1058a1e9c 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | ||
3 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
4 | import { keyBy } from 'lodash' | 5 | import { keyBy } from 'lodash' |
5 | import 'mocha' | 6 | |
6 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
7 | import { | 7 | import { |
8 | checkVideoFilesWereRemoved, | 8 | checkVideoFilesWereRemoved, |
9 | cleanupTests, | 9 | cleanupTests, |
@@ -28,430 +28,432 @@ import { | |||
28 | viewVideo, | 28 | viewVideo, |
29 | wait | 29 | wait |
30 | } from '../../../../shared/extra-utils' | 30 | } from '../../../../shared/extra-utils' |
31 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
32 | import { HttpStatusCode } from '@shared/core-utils' | ||
31 | 33 | ||
32 | const expect = chai.expect | 34 | const expect = chai.expect |
33 | 35 | ||
34 | describe('Test a single server', function () { | 36 | describe('Test a single server', function () { |
35 | let server: ServerInfo = null | ||
36 | let videoId = -1 | ||
37 | let videoId2 = -1 | ||
38 | let videoUUID = '' | ||
39 | let videosListBase: any[] = null | ||
40 | |||
41 | const getCheckAttributes = () => ({ | ||
42 | name: 'my super name', | ||
43 | category: 2, | ||
44 | licence: 6, | ||
45 | language: 'zh', | ||
46 | nsfw: true, | ||
47 | description: 'my super description', | ||
48 | support: 'my super support text', | ||
49 | account: { | ||
50 | name: 'root', | ||
51 | host: 'localhost:' + server.port | ||
52 | }, | ||
53 | isLocal: true, | ||
54 | duration: 5, | ||
55 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
56 | privacy: VideoPrivacy.PUBLIC, | ||
57 | commentsEnabled: true, | ||
58 | downloadEnabled: true, | ||
59 | channel: { | ||
60 | displayName: 'Main root channel', | ||
61 | name: 'root_channel', | ||
62 | description: '', | ||
63 | isLocal: true | ||
64 | }, | ||
65 | fixture: 'video_short.webm', | ||
66 | files: [ | ||
67 | { | ||
68 | resolution: 720, | ||
69 | size: 218910 | ||
70 | } | ||
71 | ] | ||
72 | }) | ||
73 | |||
74 | const updateCheckAttributes = () => ({ | ||
75 | name: 'my super video updated', | ||
76 | category: 4, | ||
77 | licence: 2, | ||
78 | language: 'ar', | ||
79 | nsfw: false, | ||
80 | description: 'my super description updated', | ||
81 | support: 'my super support text updated', | ||
82 | account: { | ||
83 | name: 'root', | ||
84 | host: 'localhost:' + server.port | ||
85 | }, | ||
86 | isLocal: true, | ||
87 | tags: [ 'tagup1', 'tagup2' ], | ||
88 | privacy: VideoPrivacy.PUBLIC, | ||
89 | duration: 5, | ||
90 | commentsEnabled: false, | ||
91 | downloadEnabled: false, | ||
92 | channel: { | ||
93 | name: 'root_channel', | ||
94 | displayName: 'Main root channel', | ||
95 | description: '', | ||
96 | isLocal: true | ||
97 | }, | ||
98 | fixture: 'video_short3.webm', | ||
99 | files: [ | ||
100 | { | ||
101 | resolution: 720, | ||
102 | size: 292677 | ||
103 | } | ||
104 | ] | ||
105 | }) | ||
106 | |||
107 | before(async function () { | ||
108 | this.timeout(30000) | ||
109 | |||
110 | server = await flushAndRunServer(1) | ||
111 | |||
112 | await setAccessTokensToServers([ server ]) | ||
113 | }) | ||
114 | |||
115 | it('Should list video categories', async function () { | ||
116 | const res = await getVideoCategories(server.url) | ||
117 | |||
118 | const categories = res.body | ||
119 | expect(Object.keys(categories)).to.have.length.above(10) | ||
120 | |||
121 | expect(categories[11]).to.equal('News & Politics') | ||
122 | }) | ||
123 | |||
124 | it('Should list video licences', async function () { | ||
125 | const res = await getVideoLicences(server.url) | ||
126 | |||
127 | const licences = res.body | ||
128 | expect(Object.keys(licences)).to.have.length.above(5) | ||
129 | |||
130 | expect(licences[3]).to.equal('Attribution - No Derivatives') | ||
131 | }) | ||
132 | |||
133 | it('Should list video languages', async function () { | ||
134 | const res = await getVideoLanguages(server.url) | ||
135 | |||
136 | const languages = res.body | ||
137 | expect(Object.keys(languages)).to.have.length.above(5) | ||
138 | |||
139 | expect(languages['ru']).to.equal('Russian') | ||
140 | }) | ||
141 | |||
142 | it('Should list video privacies', async function () { | ||
143 | const res = await getVideoPrivacies(server.url) | ||
144 | |||
145 | const privacies = res.body | ||
146 | expect(Object.keys(privacies)).to.have.length.at.least(3) | ||
147 | |||
148 | expect(privacies[3]).to.equal('Private') | ||
149 | }) | ||
150 | |||
151 | it('Should not have videos', async function () { | ||
152 | const res = await getVideosList(server.url) | ||
153 | |||
154 | expect(res.body.total).to.equal(0) | ||
155 | expect(res.body.data).to.be.an('array') | ||
156 | expect(res.body.data.length).to.equal(0) | ||
157 | }) | ||
158 | 37 | ||
159 | it('Should upload the video', async function () { | 38 | function runSuite (mode: 'legacy' | 'resumable') { |
160 | this.timeout(10000) | 39 | let server: ServerInfo = null |
40 | let videoId = -1 | ||
41 | let videoId2 = -1 | ||
42 | let videoUUID = '' | ||
43 | let videosListBase: any[] = null | ||
161 | 44 | ||
162 | const videoAttributes = { | 45 | const getCheckAttributes = () => ({ |
163 | name: 'my super name', | 46 | name: 'my super name', |
164 | category: 2, | 47 | category: 2, |
165 | nsfw: true, | ||
166 | licence: 6, | 48 | licence: 6, |
167 | tags: [ 'tag1', 'tag2', 'tag3' ] | 49 | language: 'zh', |
168 | } | 50 | nsfw: true, |
169 | const res = await uploadVideo(server.url, server.accessToken, videoAttributes) | 51 | description: 'my super description', |
170 | expect(res.body.video).to.not.be.undefined | 52 | support: 'my super support text', |
171 | expect(res.body.video.id).to.equal(1) | 53 | account: { |
172 | expect(res.body.video.uuid).to.have.length.above(5) | 54 | name: 'root', |
173 | 55 | host: 'localhost:' + server.port | |
174 | videoId = res.body.video.id | 56 | }, |
175 | videoUUID = res.body.video.uuid | 57 | isLocal: true, |
176 | }) | 58 | duration: 5, |
177 | 59 | tags: [ 'tag1', 'tag2', 'tag3' ], | |
178 | it('Should get and seed the uploaded video', async function () { | 60 | privacy: VideoPrivacy.PUBLIC, |
179 | this.timeout(5000) | 61 | commentsEnabled: true, |
180 | 62 | downloadEnabled: true, | |
181 | const res = await getVideosList(server.url) | 63 | channel: { |
182 | 64 | displayName: 'Main root channel', | |
183 | expect(res.body.total).to.equal(1) | 65 | name: 'root_channel', |
184 | expect(res.body.data).to.be.an('array') | 66 | description: '', |
185 | expect(res.body.data.length).to.equal(1) | 67 | isLocal: true |
186 | 68 | }, | |
187 | const video = res.body.data[0] | 69 | fixture: 'video_short.webm', |
188 | await completeVideoCheck(server.url, video, getCheckAttributes()) | 70 | files: [ |
189 | }) | 71 | { |
72 | resolution: 720, | ||
73 | size: 218910 | ||
74 | } | ||
75 | ] | ||
76 | }) | ||
77 | |||
78 | const updateCheckAttributes = () => ({ | ||
79 | name: 'my super video updated', | ||
80 | category: 4, | ||
81 | licence: 2, | ||
82 | language: 'ar', | ||
83 | nsfw: false, | ||
84 | description: 'my super description updated', | ||
85 | support: 'my super support text updated', | ||
86 | account: { | ||
87 | name: 'root', | ||
88 | host: 'localhost:' + server.port | ||
89 | }, | ||
90 | isLocal: true, | ||
91 | tags: [ 'tagup1', 'tagup2' ], | ||
92 | privacy: VideoPrivacy.PUBLIC, | ||
93 | duration: 5, | ||
94 | commentsEnabled: false, | ||
95 | downloadEnabled: false, | ||
96 | channel: { | ||
97 | name: 'root_channel', | ||
98 | displayName: 'Main root channel', | ||
99 | description: '', | ||
100 | isLocal: true | ||
101 | }, | ||
102 | fixture: 'video_short3.webm', | ||
103 | files: [ | ||
104 | { | ||
105 | resolution: 720, | ||
106 | size: 292677 | ||
107 | } | ||
108 | ] | ||
109 | }) | ||
190 | 110 | ||
191 | it('Should get the video by UUID', async function () { | 111 | before(async function () { |
192 | this.timeout(5000) | 112 | this.timeout(30000) |
193 | 113 | ||
194 | const res = await getVideo(server.url, videoUUID) | 114 | server = await flushAndRunServer(1) |
195 | 115 | ||
196 | const video = res.body | 116 | await setAccessTokensToServers([ server ]) |
197 | await completeVideoCheck(server.url, video, getCheckAttributes()) | 117 | }) |
198 | }) | ||
199 | 118 | ||
200 | it('Should have the views updated', async function () { | 119 | it('Should list video categories', async function () { |
201 | this.timeout(20000) | 120 | const res = await getVideoCategories(server.url) |
202 | 121 | ||
203 | await viewVideo(server.url, videoId) | 122 | const categories = res.body |
204 | await viewVideo(server.url, videoId) | 123 | expect(Object.keys(categories)).to.have.length.above(10) |
205 | await viewVideo(server.url, videoId) | ||
206 | 124 | ||
207 | await wait(1500) | 125 | expect(categories[11]).to.equal('News & Politics') |
126 | }) | ||
208 | 127 | ||
209 | await viewVideo(server.url, videoId) | 128 | it('Should list video licences', async function () { |
210 | await viewVideo(server.url, videoId) | 129 | const res = await getVideoLicences(server.url) |
211 | 130 | ||
212 | await wait(1500) | 131 | const licences = res.body |
132 | expect(Object.keys(licences)).to.have.length.above(5) | ||
213 | 133 | ||
214 | await viewVideo(server.url, videoId) | 134 | expect(licences[3]).to.equal('Attribution - No Derivatives') |
215 | await viewVideo(server.url, videoId) | 135 | }) |
216 | 136 | ||
217 | // Wait the repeatable job | 137 | it('Should list video languages', async function () { |
218 | await wait(8000) | 138 | const res = await getVideoLanguages(server.url) |
219 | 139 | ||
220 | const res = await getVideo(server.url, videoId) | 140 | const languages = res.body |
141 | expect(Object.keys(languages)).to.have.length.above(5) | ||
221 | 142 | ||
222 | const video = res.body | 143 | expect(languages['ru']).to.equal('Russian') |
223 | expect(video.views).to.equal(3) | 144 | }) |
224 | }) | ||
225 | 145 | ||
226 | it('Should remove the video', async function () { | 146 | it('Should list video privacies', async function () { |
227 | await removeVideo(server.url, server.accessToken, videoId) | 147 | const res = await getVideoPrivacies(server.url) |
228 | 148 | ||
229 | await checkVideoFilesWereRemoved(videoUUID, 1) | 149 | const privacies = res.body |
230 | }) | 150 | expect(Object.keys(privacies)).to.have.length.at.least(3) |
231 | 151 | ||
232 | it('Should not have videos', async function () { | 152 | expect(privacies[3]).to.equal('Private') |
233 | const res = await getVideosList(server.url) | 153 | }) |
234 | 154 | ||
235 | expect(res.body.total).to.equal(0) | 155 | it('Should not have videos', async function () { |
236 | expect(res.body.data).to.be.an('array') | 156 | const res = await getVideosList(server.url) |
237 | expect(res.body.data).to.have.lengthOf(0) | ||
238 | }) | ||
239 | 157 | ||
240 | it('Should upload 6 videos', async function () { | 158 | expect(res.body.total).to.equal(0) |
241 | this.timeout(25000) | 159 | expect(res.body.data).to.be.an('array') |
160 | expect(res.body.data.length).to.equal(0) | ||
161 | }) | ||
242 | 162 | ||
243 | const videos = new Set([ | 163 | it('Should upload the video', async function () { |
244 | 'video_short.mp4', 'video_short.ogv', 'video_short.webm', | 164 | this.timeout(10000) |
245 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' | ||
246 | ]) | ||
247 | 165 | ||
248 | for (const video of videos) { | ||
249 | const videoAttributes = { | 166 | const videoAttributes = { |
250 | name: video + ' name', | 167 | name: 'my super name', |
251 | description: video + ' description', | ||
252 | category: 2, | 168 | category: 2, |
253 | licence: 1, | ||
254 | language: 'en', | ||
255 | nsfw: true, | 169 | nsfw: true, |
256 | tags: [ 'tag1', 'tag2', 'tag3' ], | 170 | licence: 6, |
257 | fixture: video | 171 | tags: [ 'tag1', 'tag2', 'tag3' ] |
258 | } | 172 | } |
173 | const res = await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode) | ||
174 | expect(res.body.video).to.not.be.undefined | ||
175 | expect(res.body.video.id).to.equal(1) | ||
176 | expect(res.body.video.uuid).to.have.length.above(5) | ||
259 | 177 | ||
260 | await uploadVideo(server.url, server.accessToken, videoAttributes) | 178 | videoId = res.body.video.id |
261 | } | 179 | videoUUID = res.body.video.uuid |
262 | }) | 180 | }) |
263 | 181 | ||
264 | it('Should have the correct durations', async function () { | 182 | it('Should get and seed the uploaded video', async function () { |
265 | const res = await getVideosList(server.url) | 183 | this.timeout(5000) |
266 | |||
267 | expect(res.body.total).to.equal(6) | ||
268 | const videos = res.body.data | ||
269 | expect(videos).to.be.an('array') | ||
270 | expect(videos).to.have.lengthOf(6) | ||
271 | |||
272 | const videosByName = keyBy<{ duration: number }>(videos, 'name') | ||
273 | expect(videosByName['video_short.mp4 name'].duration).to.equal(5) | ||
274 | expect(videosByName['video_short.ogv name'].duration).to.equal(5) | ||
275 | expect(videosByName['video_short.webm name'].duration).to.equal(5) | ||
276 | expect(videosByName['video_short1.webm name'].duration).to.equal(10) | ||
277 | expect(videosByName['video_short2.webm name'].duration).to.equal(5) | ||
278 | expect(videosByName['video_short3.webm name'].duration).to.equal(5) | ||
279 | }) | ||
280 | 184 | ||
281 | it('Should have the correct thumbnails', async function () { | 185 | const res = await getVideosList(server.url) |
282 | const res = await getVideosList(server.url) | ||
283 | 186 | ||
284 | const videos = res.body.data | 187 | expect(res.body.total).to.equal(1) |
285 | // For the next test | 188 | expect(res.body.data).to.be.an('array') |
286 | videosListBase = videos | 189 | expect(res.body.data.length).to.equal(1) |
287 | 190 | ||
288 | for (const video of videos) { | 191 | const video = res.body.data[0] |
289 | const videoName = video.name.replace(' name', '') | 192 | await completeVideoCheck(server.url, video, getCheckAttributes()) |
290 | await testImage(server.url, videoName, video.thumbnailPath) | 193 | }) |
291 | } | ||
292 | }) | ||
293 | 194 | ||
294 | it('Should list only the two first videos', async function () { | 195 | it('Should get the video by UUID', async function () { |
295 | const res = await getVideosListPagination(server.url, 0, 2, 'name') | 196 | this.timeout(5000) |
296 | 197 | ||
297 | const videos = res.body.data | 198 | const res = await getVideo(server.url, videoUUID) |
298 | expect(res.body.total).to.equal(6) | ||
299 | expect(videos.length).to.equal(2) | ||
300 | expect(videos[0].name).to.equal(videosListBase[0].name) | ||
301 | expect(videos[1].name).to.equal(videosListBase[1].name) | ||
302 | }) | ||
303 | 199 | ||
304 | it('Should list only the next three videos', async function () { | 200 | const video = res.body |
305 | const res = await getVideosListPagination(server.url, 2, 3, 'name') | 201 | await completeVideoCheck(server.url, video, getCheckAttributes()) |
202 | }) | ||
306 | 203 | ||
307 | const videos = res.body.data | 204 | it('Should have the views updated', async function () { |
308 | expect(res.body.total).to.equal(6) | 205 | this.timeout(20000) |
309 | expect(videos.length).to.equal(3) | ||
310 | expect(videos[0].name).to.equal(videosListBase[2].name) | ||
311 | expect(videos[1].name).to.equal(videosListBase[3].name) | ||
312 | expect(videos[2].name).to.equal(videosListBase[4].name) | ||
313 | }) | ||
314 | 206 | ||
315 | it('Should list the last video', async function () { | 207 | await viewVideo(server.url, videoId) |
316 | const res = await getVideosListPagination(server.url, 5, 6, 'name') | 208 | await viewVideo(server.url, videoId) |
209 | await viewVideo(server.url, videoId) | ||
317 | 210 | ||
318 | const videos = res.body.data | 211 | await wait(1500) |
319 | expect(res.body.total).to.equal(6) | ||
320 | expect(videos.length).to.equal(1) | ||
321 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
322 | }) | ||
323 | 212 | ||
324 | it('Should not have the total field', async function () { | 213 | await viewVideo(server.url, videoId) |
325 | const res = await getVideosListPagination(server.url, 5, 6, 'name', true) | 214 | await viewVideo(server.url, videoId) |
326 | 215 | ||
327 | const videos = res.body.data | 216 | await wait(1500) |
328 | expect(res.body.total).to.not.exist | ||
329 | expect(videos.length).to.equal(1) | ||
330 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
331 | }) | ||
332 | 217 | ||
333 | it('Should list and sort by name in descending order', async function () { | 218 | await viewVideo(server.url, videoId) |
334 | const res = await getVideosListSort(server.url, '-name') | 219 | await viewVideo(server.url, videoId) |
335 | |||
336 | const videos = res.body.data | ||
337 | expect(res.body.total).to.equal(6) | ||
338 | expect(videos.length).to.equal(6) | ||
339 | expect(videos[0].name).to.equal('video_short.webm name') | ||
340 | expect(videos[1].name).to.equal('video_short.ogv name') | ||
341 | expect(videos[2].name).to.equal('video_short.mp4 name') | ||
342 | expect(videos[3].name).to.equal('video_short3.webm name') | ||
343 | expect(videos[4].name).to.equal('video_short2.webm name') | ||
344 | expect(videos[5].name).to.equal('video_short1.webm name') | ||
345 | |||
346 | videoId = videos[3].uuid | ||
347 | videoId2 = videos[5].uuid | ||
348 | }) | ||
349 | 220 | ||
350 | it('Should list and sort by trending in descending order', async function () { | 221 | // Wait the repeatable job |
351 | const res = await getVideosListPagination(server.url, 0, 2, '-trending') | 222 | await wait(8000) |
352 | 223 | ||
353 | const videos = res.body.data | 224 | const res = await getVideo(server.url, videoId) |
354 | expect(res.body.total).to.equal(6) | ||
355 | expect(videos.length).to.equal(2) | ||
356 | }) | ||
357 | 225 | ||
358 | it('Should list and sort by hotness in descending order', async function () { | 226 | const video = res.body |
359 | const res = await getVideosListPagination(server.url, 0, 2, '-hot') | 227 | expect(video.views).to.equal(3) |
228 | }) | ||
360 | 229 | ||
361 | const videos = res.body.data | 230 | it('Should remove the video', async function () { |
362 | expect(res.body.total).to.equal(6) | 231 | await removeVideo(server.url, server.accessToken, videoId) |
363 | expect(videos.length).to.equal(2) | ||
364 | }) | ||
365 | 232 | ||
366 | it('Should list and sort by best in descending order', async function () { | 233 | await checkVideoFilesWereRemoved(videoUUID, 1) |
367 | const res = await getVideosListPagination(server.url, 0, 2, '-best') | 234 | }) |
368 | 235 | ||
369 | const videos = res.body.data | 236 | it('Should not have videos', async function () { |
370 | expect(res.body.total).to.equal(6) | 237 | const res = await getVideosList(server.url) |
371 | expect(videos.length).to.equal(2) | ||
372 | }) | ||
373 | 238 | ||
374 | it('Should update a video', async function () { | 239 | expect(res.body.total).to.equal(0) |
375 | const attributes = { | 240 | expect(res.body.data).to.be.an('array') |
376 | name: 'my super video updated', | 241 | expect(res.body.data).to.have.lengthOf(0) |
377 | category: 4, | 242 | }) |
378 | licence: 2, | ||
379 | language: 'ar', | ||
380 | nsfw: false, | ||
381 | description: 'my super description updated', | ||
382 | commentsEnabled: false, | ||
383 | downloadEnabled: false, | ||
384 | tags: [ 'tagup1', 'tagup2' ] | ||
385 | } | ||
386 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
387 | }) | ||
388 | 243 | ||
389 | it('Should filter by tags and category', async function () { | 244 | it('Should upload 6 videos', async function () { |
390 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) | 245 | this.timeout(25000) |
391 | expect(res1.body.total).to.equal(1) | ||
392 | expect(res1.body.data[0].name).to.equal('my super video updated') | ||
393 | 246 | ||
394 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) | 247 | const videos = new Set([ |
395 | expect(res2.body.total).to.equal(0) | 248 | 'video_short.mp4', 'video_short.ogv', 'video_short.webm', |
396 | }) | 249 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' |
250 | ]) | ||
397 | 251 | ||
398 | it('Should have the video updated', async function () { | 252 | for (const video of videos) { |
399 | this.timeout(60000) | 253 | const videoAttributes = { |
254 | name: video + ' name', | ||
255 | description: video + ' description', | ||
256 | category: 2, | ||
257 | licence: 1, | ||
258 | language: 'en', | ||
259 | nsfw: true, | ||
260 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
261 | fixture: video | ||
262 | } | ||
400 | 263 | ||
401 | const res = await getVideo(server.url, videoId) | 264 | await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode) |
402 | const video = res.body | 265 | } |
266 | }) | ||
267 | |||
268 | it('Should have the correct durations', async function () { | ||
269 | const res = await getVideosList(server.url) | ||
270 | |||
271 | expect(res.body.total).to.equal(6) | ||
272 | const videos = res.body.data | ||
273 | expect(videos).to.be.an('array') | ||
274 | expect(videos).to.have.lengthOf(6) | ||
275 | |||
276 | const videosByName = keyBy<{ duration: number }>(videos, 'name') | ||
277 | expect(videosByName['video_short.mp4 name'].duration).to.equal(5) | ||
278 | expect(videosByName['video_short.ogv name'].duration).to.equal(5) | ||
279 | expect(videosByName['video_short.webm name'].duration).to.equal(5) | ||
280 | expect(videosByName['video_short1.webm name'].duration).to.equal(10) | ||
281 | expect(videosByName['video_short2.webm name'].duration).to.equal(5) | ||
282 | expect(videosByName['video_short3.webm name'].duration).to.equal(5) | ||
283 | }) | ||
284 | |||
285 | it('Should have the correct thumbnails', async function () { | ||
286 | const res = await getVideosList(server.url) | ||
287 | |||
288 | const videos = res.body.data | ||
289 | // For the next test | ||
290 | videosListBase = videos | ||
291 | |||
292 | for (const video of videos) { | ||
293 | const videoName = video.name.replace(' name', '') | ||
294 | await testImage(server.url, videoName, video.thumbnailPath) | ||
295 | } | ||
296 | }) | ||
297 | |||
298 | it('Should list only the two first videos', async function () { | ||
299 | const res = await getVideosListPagination(server.url, 0, 2, 'name') | ||
300 | |||
301 | const videos = res.body.data | ||
302 | expect(res.body.total).to.equal(6) | ||
303 | expect(videos.length).to.equal(2) | ||
304 | expect(videos[0].name).to.equal(videosListBase[0].name) | ||
305 | expect(videos[1].name).to.equal(videosListBase[1].name) | ||
306 | }) | ||
307 | |||
308 | it('Should list only the next three videos', async function () { | ||
309 | const res = await getVideosListPagination(server.url, 2, 3, 'name') | ||
310 | |||
311 | const videos = res.body.data | ||
312 | expect(res.body.total).to.equal(6) | ||
313 | expect(videos.length).to.equal(3) | ||
314 | expect(videos[0].name).to.equal(videosListBase[2].name) | ||
315 | expect(videos[1].name).to.equal(videosListBase[3].name) | ||
316 | expect(videos[2].name).to.equal(videosListBase[4].name) | ||
317 | }) | ||
318 | |||
319 | it('Should list the last video', async function () { | ||
320 | const res = await getVideosListPagination(server.url, 5, 6, 'name') | ||
321 | |||
322 | const videos = res.body.data | ||
323 | expect(res.body.total).to.equal(6) | ||
324 | expect(videos.length).to.equal(1) | ||
325 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
326 | }) | ||
327 | |||
328 | it('Should not have the total field', async function () { | ||
329 | const res = await getVideosListPagination(server.url, 5, 6, 'name', true) | ||
330 | |||
331 | const videos = res.body.data | ||
332 | expect(res.body.total).to.not.exist | ||
333 | expect(videos.length).to.equal(1) | ||
334 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
335 | }) | ||
336 | |||
337 | it('Should list and sort by name in descending order', async function () { | ||
338 | const res = await getVideosListSort(server.url, '-name') | ||
339 | |||
340 | const videos = res.body.data | ||
341 | expect(res.body.total).to.equal(6) | ||
342 | expect(videos.length).to.equal(6) | ||
343 | expect(videos[0].name).to.equal('video_short.webm name') | ||
344 | expect(videos[1].name).to.equal('video_short.ogv name') | ||
345 | expect(videos[2].name).to.equal('video_short.mp4 name') | ||
346 | expect(videos[3].name).to.equal('video_short3.webm name') | ||
347 | expect(videos[4].name).to.equal('video_short2.webm name') | ||
348 | expect(videos[5].name).to.equal('video_short1.webm name') | ||
349 | |||
350 | videoId = videos[3].uuid | ||
351 | videoId2 = videos[5].uuid | ||
352 | }) | ||
353 | |||
354 | it('Should list and sort by trending in descending order', async function () { | ||
355 | const res = await getVideosListPagination(server.url, 0, 2, '-trending') | ||
356 | |||
357 | const videos = res.body.data | ||
358 | expect(res.body.total).to.equal(6) | ||
359 | expect(videos.length).to.equal(2) | ||
360 | }) | ||
361 | |||
362 | it('Should list and sort by hotness in descending order', async function () { | ||
363 | const res = await getVideosListPagination(server.url, 0, 2, '-hot') | ||
364 | |||
365 | const videos = res.body.data | ||
366 | expect(res.body.total).to.equal(6) | ||
367 | expect(videos.length).to.equal(2) | ||
368 | }) | ||
369 | |||
370 | it('Should list and sort by best in descending order', async function () { | ||
371 | const res = await getVideosListPagination(server.url, 0, 2, '-best') | ||
372 | |||
373 | const videos = res.body.data | ||
374 | expect(res.body.total).to.equal(6) | ||
375 | expect(videos.length).to.equal(2) | ||
376 | }) | ||
377 | |||
378 | it('Should update a video', async function () { | ||
379 | const attributes = { | ||
380 | name: 'my super video updated', | ||
381 | category: 4, | ||
382 | licence: 2, | ||
383 | language: 'ar', | ||
384 | nsfw: false, | ||
385 | description: 'my super description updated', | ||
386 | commentsEnabled: false, | ||
387 | downloadEnabled: false, | ||
388 | tags: [ 'tagup1', 'tagup2' ] | ||
389 | } | ||
390 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
391 | }) | ||
403 | 392 | ||
404 | await completeVideoCheck(server.url, video, updateCheckAttributes()) | 393 | it('Should filter by tags and category', async function () { |
405 | }) | 394 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) |
395 | expect(res1.body.total).to.equal(1) | ||
396 | expect(res1.body.data[0].name).to.equal('my super video updated') | ||
406 | 397 | ||
407 | it('Should update only the tags of a video', async function () { | 398 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) |
408 | const attributes = { | 399 | expect(res2.body.total).to.equal(0) |
409 | tags: [ 'supertag', 'tag1', 'tag2' ] | 400 | }) |
410 | } | ||
411 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
412 | 401 | ||
413 | const res = await getVideo(server.url, videoId) | 402 | it('Should have the video updated', async function () { |
414 | const video = res.body | 403 | this.timeout(60000) |
415 | 404 | ||
416 | await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes)) | 405 | const res = await getVideo(server.url, videoId) |
417 | }) | 406 | const video = res.body |
418 | 407 | ||
419 | it('Should update only the description of a video', async function () { | 408 | await completeVideoCheck(server.url, video, updateCheckAttributes()) |
420 | const attributes = { | 409 | }) |
421 | description: 'hello everybody' | ||
422 | } | ||
423 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
424 | 410 | ||
425 | const res = await getVideo(server.url, videoId) | 411 | it('Should update only the tags of a video', async function () { |
426 | const video = res.body | 412 | const attributes = { |
413 | tags: [ 'supertag', 'tag1', 'tag2' ] | ||
414 | } | ||
415 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
427 | 416 | ||
428 | const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes) | 417 | const res = await getVideo(server.url, videoId) |
429 | await completeVideoCheck(server.url, video, expectedAttributes) | 418 | const video = res.body |
430 | }) | ||
431 | 419 | ||
432 | it('Should like a video', async function () { | 420 | await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes)) |
433 | await rateVideo(server.url, server.accessToken, videoId, 'like') | 421 | }) |
434 | 422 | ||
435 | const res = await getVideo(server.url, videoId) | 423 | it('Should update only the description of a video', async function () { |
436 | const video = res.body | 424 | const attributes = { |
425 | description: 'hello everybody' | ||
426 | } | ||
427 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
437 | 428 | ||
438 | expect(video.likes).to.equal(1) | 429 | const res = await getVideo(server.url, videoId) |
439 | expect(video.dislikes).to.equal(0) | 430 | const video = res.body |
440 | }) | ||
441 | 431 | ||
442 | it('Should dislike the same video', async function () { | 432 | const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes) |
443 | await rateVideo(server.url, server.accessToken, videoId, 'dislike') | 433 | await completeVideoCheck(server.url, video, expectedAttributes) |
434 | }) | ||
444 | 435 | ||
445 | const res = await getVideo(server.url, videoId) | 436 | it('Should like a video', async function () { |
446 | const video = res.body | 437 | await rateVideo(server.url, server.accessToken, videoId, 'like') |
447 | 438 | ||
448 | expect(video.likes).to.equal(0) | 439 | const res = await getVideo(server.url, videoId) |
449 | expect(video.dislikes).to.equal(1) | 440 | const video = res.body |
450 | }) | ||
451 | 441 | ||
452 | it('Should sort by originallyPublishedAt', async function () { | 442 | expect(video.likes).to.equal(1) |
453 | { | 443 | expect(video.dislikes).to.equal(0) |
444 | }) | ||
454 | 445 | ||
446 | it('Should dislike the same video', async function () { | ||
447 | await rateVideo(server.url, server.accessToken, videoId, 'dislike') | ||
448 | |||
449 | const res = await getVideo(server.url, videoId) | ||
450 | const video = res.body | ||
451 | |||
452 | expect(video.likes).to.equal(0) | ||
453 | expect(video.dislikes).to.equal(1) | ||
454 | }) | ||
455 | |||
456 | it('Should sort by originallyPublishedAt', async function () { | ||
455 | { | 457 | { |
456 | const now = new Date() | 458 | const now = new Date() |
457 | const attributes = { originallyPublishedAt: now.toISOString() } | 459 | const attributes = { originallyPublishedAt: now.toISOString() } |
@@ -483,10 +485,18 @@ describe('Test a single server', function () { | |||
483 | expect(names[4]).to.equal('video_short.ogv name') | 485 | expect(names[4]).to.equal('video_short.ogv name') |
484 | expect(names[5]).to.equal('video_short.mp4 name') | 486 | expect(names[5]).to.equal('video_short.mp4 name') |
485 | } | 487 | } |
486 | } | 488 | }) |
489 | |||
490 | after(async function () { | ||
491 | await cleanupTests([ server ]) | ||
492 | }) | ||
493 | } | ||
494 | |||
495 | describe('Legacy upload', function () { | ||
496 | runSuite('legacy') | ||
487 | }) | 497 | }) |
488 | 498 | ||
489 | after(async function () { | 499 | describe('Resumable upload', function () { |
490 | await cleanupTests([ server ]) | 500 | runSuite('resumable') |
491 | }) | 501 | }) |
492 | }) | 502 | }) |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index d12d58e75..7e7ad028c 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { basename } from 'path' | 5 | import { basename } from 'path' |
6 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | createUser, | 9 | createUser, |
@@ -13,6 +14,7 @@ import { | |||
13 | getVideo, | 14 | getVideo, |
14 | getVideoChannel, | 15 | getVideoChannel, |
15 | getVideoChannelVideos, | 16 | getVideoChannelVideos, |
17 | setDefaultVideoChannel, | ||
16 | testImage, | 18 | testImage, |
17 | updateVideo, | 19 | updateVideo, |
18 | updateVideoChannelImage, | 20 | updateVideoChannelImage, |
@@ -33,7 +35,6 @@ import { | |||
33 | } from '../../../../shared/extra-utils/index' | 35 | } from '../../../../shared/extra-utils/index' |
34 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 36 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
35 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' | 37 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' |
36 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
37 | 38 | ||
38 | const expect = chai.expect | 39 | const expect = chai.expect |
39 | 40 | ||
@@ -47,9 +48,10 @@ async function findChannel (server: ServerInfo, channelId: number) { | |||
47 | describe('Test video channels', function () { | 48 | describe('Test video channels', function () { |
48 | let servers: ServerInfo[] | 49 | let servers: ServerInfo[] |
49 | let userInfo: User | 50 | let userInfo: User |
50 | let firstVideoChannelId: number | ||
51 | let secondVideoChannelId: number | 51 | let secondVideoChannelId: number |
52 | let totoChannel: number | ||
52 | let videoUUID: string | 53 | let videoUUID: string |
54 | let accountName: string | ||
53 | 55 | ||
54 | before(async function () { | 56 | before(async function () { |
55 | this.timeout(60000) | 57 | this.timeout(60000) |
@@ -57,16 +59,9 @@ describe('Test video channels', function () { | |||
57 | servers = await flushAndRunMultipleServers(2) | 59 | servers = await flushAndRunMultipleServers(2) |
58 | 60 | ||
59 | await setAccessTokensToServers(servers) | 61 | await setAccessTokensToServers(servers) |
60 | await doubleFollow(servers[0], servers[1]) | 62 | await setDefaultVideoChannel(servers) |
61 | |||
62 | { | ||
63 | const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) | ||
64 | const user: User = res.body | ||
65 | |||
66 | firstVideoChannelId = user.videoChannels[0].id | ||
67 | } | ||
68 | 63 | ||
69 | await waitJobs(servers) | 64 | await doubleFollow(servers[0], servers[1]) |
70 | }) | 65 | }) |
71 | 66 | ||
72 | it('Should have one video channel (created with root)', async () => { | 67 | it('Should have one video channel (created with root)', async () => { |
@@ -116,12 +111,14 @@ describe('Test video channels', function () { | |||
116 | expect(videoChannels[1].displayName).to.equal('second video channel') | 111 | expect(videoChannels[1].displayName).to.equal('second video channel') |
117 | expect(videoChannels[1].description).to.equal('super video channel description') | 112 | expect(videoChannels[1].description).to.equal('super video channel description') |
118 | expect(videoChannels[1].support).to.equal('super video channel support text') | 113 | expect(videoChannels[1].support).to.equal('super video channel support text') |
114 | |||
115 | accountName = userInfo.account.name + '@' + userInfo.account.host | ||
119 | }) | 116 | }) |
120 | 117 | ||
121 | it('Should have two video channels when getting account channels on server 1', async function () { | 118 | it('Should have two video channels when getting account channels on server 1', async function () { |
122 | const res = await getAccountVideoChannelsList({ | 119 | const res = await getAccountVideoChannelsList({ |
123 | url: servers[0].url, | 120 | url: servers[0].url, |
124 | accountName: userInfo.account.name + '@' + userInfo.account.host | 121 | accountName |
125 | }) | 122 | }) |
126 | 123 | ||
127 | expect(res.body.total).to.equal(2) | 124 | expect(res.body.total).to.equal(2) |
@@ -142,7 +139,7 @@ describe('Test video channels', function () { | |||
142 | { | 139 | { |
143 | const res = await getAccountVideoChannelsList({ | 140 | const res = await getAccountVideoChannelsList({ |
144 | url: servers[0].url, | 141 | url: servers[0].url, |
145 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 142 | accountName, |
146 | start: 0, | 143 | start: 0, |
147 | count: 1, | 144 | count: 1, |
148 | sort: 'createdAt' | 145 | sort: 'createdAt' |
@@ -158,7 +155,7 @@ describe('Test video channels', function () { | |||
158 | { | 155 | { |
159 | const res = await getAccountVideoChannelsList({ | 156 | const res = await getAccountVideoChannelsList({ |
160 | url: servers[0].url, | 157 | url: servers[0].url, |
161 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 158 | accountName, |
162 | start: 0, | 159 | start: 0, |
163 | count: 1, | 160 | count: 1, |
164 | sort: '-createdAt' | 161 | sort: '-createdAt' |
@@ -174,7 +171,7 @@ describe('Test video channels', function () { | |||
174 | { | 171 | { |
175 | const res = await getAccountVideoChannelsList({ | 172 | const res = await getAccountVideoChannelsList({ |
176 | url: servers[0].url, | 173 | url: servers[0].url, |
177 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 174 | accountName, |
178 | start: 1, | 175 | start: 1, |
179 | count: 1, | 176 | count: 1, |
180 | sort: '-createdAt' | 177 | sort: '-createdAt' |
@@ -191,7 +188,7 @@ describe('Test video channels', function () { | |||
191 | it('Should have one video channel when getting account channels on server 2', async function () { | 188 | it('Should have one video channel when getting account channels on server 2', async function () { |
192 | const res = await getAccountVideoChannelsList({ | 189 | const res = await getAccountVideoChannelsList({ |
193 | url: servers[1].url, | 190 | url: servers[1].url, |
194 | accountName: userInfo.account.name + '@' + userInfo.account.host | 191 | accountName |
195 | }) | 192 | }) |
196 | 193 | ||
197 | expect(res.body.total).to.equal(1) | 194 | expect(res.body.total).to.equal(1) |
@@ -379,7 +376,7 @@ describe('Test video channels', function () { | |||
379 | it('Should change the video channel of a video', async function () { | 376 | it('Should change the video channel of a video', async function () { |
380 | this.timeout(10000) | 377 | this.timeout(10000) |
381 | 378 | ||
382 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { channelId: firstVideoChannelId }) | 379 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { channelId: servers[0].videoChannel.id }) |
383 | 380 | ||
384 | await waitJobs(servers) | 381 | await waitJobs(servers) |
385 | }) | 382 | }) |
@@ -419,7 +416,8 @@ describe('Test video channels', function () { | |||
419 | it('Should create the main channel with an uuid if there is a conflict', async function () { | 416 | it('Should create the main channel with an uuid if there is a conflict', async function () { |
420 | { | 417 | { |
421 | const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' } | 418 | const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' } |
422 | await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) | 419 | const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) |
420 | totoChannel = res.body.videoChannel.id | ||
423 | } | 421 | } |
424 | 422 | ||
425 | { | 423 | { |
@@ -438,7 +436,7 @@ describe('Test video channels', function () { | |||
438 | { | 436 | { |
439 | const res = await getAccountVideoChannelsList({ | 437 | const res = await getAccountVideoChannelsList({ |
440 | url: servers[0].url, | 438 | url: servers[0].url, |
441 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 439 | accountName, |
442 | withStats: true | 440 | withStats: true |
443 | }) | 441 | }) |
444 | 442 | ||
@@ -456,7 +454,7 @@ describe('Test video channels', function () { | |||
456 | } | 454 | } |
457 | 455 | ||
458 | { | 456 | { |
459 | // video has been posted on channel firstVideoChannelId since last update | 457 | // video has been posted on channel servers[0].videoChannel.id since last update |
460 | await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.1,127.0.0.1') | 458 | await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.1,127.0.0.1') |
461 | await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.2,127.0.0.1') | 459 | await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.2,127.0.0.1') |
462 | 460 | ||
@@ -465,10 +463,10 @@ describe('Test video channels', function () { | |||
465 | 463 | ||
466 | const res = await getAccountVideoChannelsList({ | 464 | const res = await getAccountVideoChannelsList({ |
467 | url: servers[0].url, | 465 | url: servers[0].url, |
468 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 466 | accountName, |
469 | withStats: true | 467 | withStats: true |
470 | }) | 468 | }) |
471 | const channelWithView = res.body.data.find((channel: VideoChannel) => channel.id === firstVideoChannelId) | 469 | const channelWithView = res.body.data.find((channel: VideoChannel) => channel.id === servers[0].videoChannel.id) |
472 | expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2) | 470 | expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2) |
473 | } | 471 | } |
474 | }) | 472 | }) |
@@ -476,7 +474,7 @@ describe('Test video channels', function () { | |||
476 | it('Should report correct videos count', async function () { | 474 | it('Should report correct videos count', async function () { |
477 | const res = await getAccountVideoChannelsList({ | 475 | const res = await getAccountVideoChannelsList({ |
478 | url: servers[0].url, | 476 | url: servers[0].url, |
479 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 477 | accountName, |
480 | withStats: true | 478 | withStats: true |
481 | }) | 479 | }) |
482 | const channels: VideoChannel[] = res.body.data | 480 | const channels: VideoChannel[] = res.body.data |
@@ -492,7 +490,7 @@ describe('Test video channels', function () { | |||
492 | { | 490 | { |
493 | const res = await getAccountVideoChannelsList({ | 491 | const res = await getAccountVideoChannelsList({ |
494 | url: servers[0].url, | 492 | url: servers[0].url, |
495 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 493 | accountName, |
496 | search: 'root' | 494 | search: 'root' |
497 | }) | 495 | }) |
498 | expect(res.body.total).to.equal(1) | 496 | expect(res.body.total).to.equal(1) |
@@ -504,7 +502,7 @@ describe('Test video channels', function () { | |||
504 | { | 502 | { |
505 | const res = await getAccountVideoChannelsList({ | 503 | const res = await getAccountVideoChannelsList({ |
506 | url: servers[0].url, | 504 | url: servers[0].url, |
507 | accountName: userInfo.account.name + '@' + userInfo.account.host, | 505 | accountName, |
508 | search: 'does not exist' | 506 | search: 'does not exist' |
509 | }) | 507 | }) |
510 | expect(res.body.total).to.equal(0) | 508 | expect(res.body.total).to.equal(0) |
@@ -514,6 +512,40 @@ describe('Test video channels', function () { | |||
514 | } | 512 | } |
515 | }) | 513 | }) |
516 | 514 | ||
515 | it('Should list channels by updatedAt desc if a video has been uploaded', async function () { | ||
516 | this.timeout(30000) | ||
517 | |||
518 | await uploadVideo(servers[0].url, servers[0].accessToken, { channelId: totoChannel }) | ||
519 | await waitJobs(servers) | ||
520 | |||
521 | for (const server of servers) { | ||
522 | const res = await getAccountVideoChannelsList({ | ||
523 | url: server.url, | ||
524 | accountName, | ||
525 | sort: '-updatedAt' | ||
526 | }) | ||
527 | |||
528 | const channels: VideoChannel[] = res.body.data | ||
529 | expect(channels[0].name).to.equal('toto_channel') | ||
530 | expect(channels[1].name).to.equal('root_channel') | ||
531 | } | ||
532 | |||
533 | await uploadVideo(servers[0].url, servers[0].accessToken, { channelId: servers[0].videoChannel.id }) | ||
534 | await waitJobs(servers) | ||
535 | |||
536 | for (const server of servers) { | ||
537 | const res = await getAccountVideoChannelsList({ | ||
538 | url: server.url, | ||
539 | accountName, | ||
540 | sort: '-updatedAt' | ||
541 | }) | ||
542 | |||
543 | const channels: VideoChannel[] = res.body.data | ||
544 | expect(channels[0].name).to.equal('root_channel') | ||
545 | expect(channels[1].name).to.equal('toto_channel') | ||
546 | } | ||
547 | }) | ||
548 | |||
517 | after(async function () { | 549 | after(async function () { |
518 | await cleanupTests(servers) | 550 | await cleanupTests(servers) |
519 | }) | 551 | }) |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 1c99f26df..ea5ffd239 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -361,106 +361,117 @@ describe('Test video transcoding', function () { | |||
361 | 361 | ||
362 | describe('Audio upload', function () { | 362 | describe('Audio upload', function () { |
363 | 363 | ||
364 | before(async function () { | 364 | function runSuite (mode: 'legacy' | 'resumable') { |
365 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | 365 | |
366 | transcoding: { | 366 | before(async function () { |
367 | hls: { enabled: true }, | 367 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { |
368 | webtorrent: { enabled: true }, | 368 | transcoding: { |
369 | resolutions: { | 369 | hls: { enabled: true }, |
370 | '0p': false, | 370 | webtorrent: { enabled: true }, |
371 | '240p': false, | 371 | resolutions: { |
372 | '360p': false, | 372 | '0p': false, |
373 | '480p': false, | 373 | '240p': false, |
374 | '720p': false, | 374 | '360p': false, |
375 | '1080p': false, | 375 | '480p': false, |
376 | '1440p': false, | 376 | '720p': false, |
377 | '2160p': false | 377 | '1080p': false, |
378 | '1440p': false, | ||
379 | '2160p': false | ||
380 | } | ||
378 | } | 381 | } |
379 | } | 382 | }) |
380 | }) | 383 | }) |
381 | }) | ||
382 | |||
383 | it('Should merge an audio file with the preview file', async function () { | ||
384 | this.timeout(60_000) | ||
385 | |||
386 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | ||
387 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | ||
388 | 384 | ||
389 | await waitJobs(servers) | 385 | it('Should merge an audio file with the preview file', async function () { |
386 | this.timeout(60_000) | ||
390 | 387 | ||
391 | for (const server of servers) { | 388 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } |
392 | const res = await getVideosList(server.url) | 389 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) |
393 | 390 | ||
394 | const video = res.body.data.find(v => v.name === 'audio_with_preview') | 391 | await waitJobs(servers) |
395 | const res2 = await getVideo(server.url, video.id) | ||
396 | const videoDetails: VideoDetails = res2.body | ||
397 | 392 | ||
398 | expect(videoDetails.files).to.have.lengthOf(1) | 393 | for (const server of servers) { |
394 | const res = await getVideosList(server.url) | ||
399 | 395 | ||
400 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 396 | const video = res.body.data.find(v => v.name === 'audio_with_preview') |
401 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 397 | const res2 = await getVideo(server.url, video.id) |
398 | const videoDetails: VideoDetails = res2.body | ||
402 | 399 | ||
403 | const magnetUri = videoDetails.files[0].magnetUri | 400 | expect(videoDetails.files).to.have.lengthOf(1) |
404 | expect(magnetUri).to.contain('.mp4') | ||
405 | } | ||
406 | }) | ||
407 | 401 | ||
408 | it('Should upload an audio file and choose a default background image', async function () { | 402 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
409 | this.timeout(60_000) | 403 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
410 | 404 | ||
411 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } | 405 | const magnetUri = videoDetails.files[0].magnetUri |
412 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 406 | expect(magnetUri).to.contain('.mp4') |
407 | } | ||
408 | }) | ||
413 | 409 | ||
414 | await waitJobs(servers) | 410 | it('Should upload an audio file and choose a default background image', async function () { |
411 | this.timeout(60_000) | ||
415 | 412 | ||
416 | for (const server of servers) { | 413 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } |
417 | const res = await getVideosList(server.url) | 414 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) |
418 | 415 | ||
419 | const video = res.body.data.find(v => v.name === 'audio_without_preview') | 416 | await waitJobs(servers) |
420 | const res2 = await getVideo(server.url, video.id) | ||
421 | const videoDetails = res2.body | ||
422 | 417 | ||
423 | expect(videoDetails.files).to.have.lengthOf(1) | 418 | for (const server of servers) { |
419 | const res = await getVideosList(server.url) | ||
424 | 420 | ||
425 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 421 | const video = res.body.data.find(v => v.name === 'audio_without_preview') |
426 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 422 | const res2 = await getVideo(server.url, video.id) |
423 | const videoDetails = res2.body | ||
427 | 424 | ||
428 | const magnetUri = videoDetails.files[0].magnetUri | 425 | expect(videoDetails.files).to.have.lengthOf(1) |
429 | expect(magnetUri).to.contain('.mp4') | ||
430 | } | ||
431 | }) | ||
432 | 426 | ||
433 | it('Should upload an audio file and create an audio version only', async function () { | 427 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
434 | this.timeout(60_000) | 428 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
435 | 429 | ||
436 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | 430 | const magnetUri = videoDetails.files[0].magnetUri |
437 | transcoding: { | 431 | expect(magnetUri).to.contain('.mp4') |
438 | hls: { enabled: true }, | ||
439 | webtorrent: { enabled: true }, | ||
440 | resolutions: { | ||
441 | '0p': true, | ||
442 | '240p': false, | ||
443 | '360p': false | ||
444 | } | ||
445 | } | 432 | } |
446 | }) | 433 | }) |
447 | 434 | ||
448 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 435 | it('Should upload an audio file and create an audio version only', async function () { |
449 | const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 436 | this.timeout(60_000) |
437 | |||
438 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | ||
439 | transcoding: { | ||
440 | hls: { enabled: true }, | ||
441 | webtorrent: { enabled: true }, | ||
442 | resolutions: { | ||
443 | '0p': true, | ||
444 | '240p': false, | ||
445 | '360p': false | ||
446 | } | ||
447 | } | ||
448 | }) | ||
450 | 449 | ||
451 | await waitJobs(servers) | 450 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } |
451 | const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) | ||
452 | 452 | ||
453 | for (const server of servers) { | 453 | await waitJobs(servers) |
454 | const res2 = await getVideo(server.url, resVideo.body.video.id) | 454 | |
455 | const videoDetails: VideoDetails = res2.body | 455 | for (const server of servers) { |
456 | const res2 = await getVideo(server.url, resVideo.body.video.id) | ||
457 | const videoDetails: VideoDetails = res2.body | ||
456 | 458 | ||
457 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { | 459 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { |
458 | expect(files).to.have.lengthOf(2) | 460 | expect(files).to.have.lengthOf(2) |
459 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined | 461 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined |
462 | } | ||
460 | } | 463 | } |
461 | } | ||
462 | 464 | ||
463 | await updateConfigForTranscoding(servers[1]) | 465 | await updateConfigForTranscoding(servers[1]) |
466 | }) | ||
467 | } | ||
468 | |||
469 | describe('Legacy upload', function () { | ||
470 | runSuite('legacy') | ||
471 | }) | ||
472 | |||
473 | describe('Resumable upload', function () { | ||
474 | runSuite('resumable') | ||
464 | }) | 475 | }) |
465 | }) | 476 | }) |
466 | 477 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js index 6ed0c20d2..b9b207b81 100644 --- a/server/tests/fixtures/peertube-plugin-test-four/main.js +++ b/server/tests/fixtures/peertube-plugin-test-four/main.js | |||
@@ -88,8 +88,8 @@ async function register ({ | |||
88 | return res.json({ routerRoute }) | 88 | return res.json({ routerRoute }) |
89 | }) | 89 | }) |
90 | 90 | ||
91 | router.get('/user', (req, res) => { | 91 | router.get('/user', async (req, res) => { |
92 | const user = peertubeHelpers.user.getAuthUser(res) | 92 | const user = await peertubeHelpers.user.getAuthUser(res) |
93 | if (!user) return res.sendStatus(404) | 93 | if (!user) return res.sendStatus(404) |
94 | 94 | ||
95 | const isAdmin = user.role === 0 | 95 | const isAdmin = user.role === 0 |
@@ -98,6 +98,7 @@ async function register ({ | |||
98 | 98 | ||
99 | return res.json({ | 99 | return res.json({ |
100 | username: user.username, | 100 | username: user.username, |
101 | displayName: user.Account.name, | ||
101 | isAdmin, | 102 | isAdmin, |
102 | isModerator, | 103 | isModerator, |
103 | isUser | 104 | isUser |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index ac958c5f5..cf1dd0854 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -55,7 +55,7 @@ describe('Test plugin filter hooks', function () { | |||
55 | let threadId: number | 55 | let threadId: number |
56 | 56 | ||
57 | before(async function () { | 57 | before(async function () { |
58 | this.timeout(30000) | 58 | this.timeout(60000) |
59 | 59 | ||
60 | servers = await flushAndRunMultipleServers(2) | 60 | servers = await flushAndRunMultipleServers(2) |
61 | await setAccessTokensToServers(servers) | 61 | await setAccessTokensToServers(servers) |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index 20020ec41..f72de8229 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -133,6 +133,7 @@ describe('Test plugin helpers', function () { | |||
133 | }) | 133 | }) |
134 | 134 | ||
135 | expect(res.body.username).to.equal('root') | 135 | expect(res.body.username).to.equal('root') |
136 | expect(res.body.displayName).to.equal('root') | ||
136 | expect(res.body.isAdmin).to.be.true | 137 | expect(res.body.isAdmin).to.be.true |
137 | expect(res.body.isModerator).to.be.false | 138 | expect(res.body.isModerator).to.be.false |
138 | expect(res.body.isUser).to.be.false | 139 | expect(res.body.isUser).to.be.false |
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts index 08e8cd713..c8a576844 100644 --- a/server/tools/peertube-plugins.ts +++ b/server/tools/peertube-plugins.ts | |||
@@ -24,7 +24,7 @@ program | |||
24 | .option('-p, --password <token>', 'Password') | 24 | .option('-p, --password <token>', 'Password') |
25 | .option('-t, --only-themes', 'List themes only') | 25 | .option('-t, --only-themes', 'List themes only') |
26 | .option('-P, --only-plugins', 'List plugins only') | 26 | .option('-P, --only-plugins', 'List plugins only') |
27 | .action(() => pluginsListCLI()) | 27 | .action((options, command) => pluginsListCLI(command, options)) |
28 | 28 | ||
29 | program | 29 | program |
30 | .command('install') | 30 | .command('install') |
@@ -61,12 +61,10 @@ if (!process.argv.slice(2).length) { | |||
61 | 61 | ||
62 | program.parse(process.argv) | 62 | program.parse(process.argv) |
63 | 63 | ||
64 | const options = program.opts() | ||
65 | |||
66 | // ---------------------------------------------------------------------------- | 64 | // ---------------------------------------------------------------------------- |
67 | 65 | ||
68 | async function pluginsListCLI () { | 66 | async function pluginsListCLI (command: commander.CommanderStatic, options: commander.OptionValues) { |
69 | const { url, username, password } = await getServerCredentials(program) | 67 | const { url, username, password } = await getServerCredentials(command) |
70 | const accessToken = await getAdminTokenOrDie(url, username, password) | 68 | const accessToken = await getAdminTokenOrDie(url, username, password) |
71 | 69 | ||
72 | let pluginType: PluginType | 70 | let pluginType: PluginType |
diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts index 8f3f30074..0b620872e 100644 --- a/server/types/models/account/actor.ts +++ b/server/types/models/account/actor.ts | |||
@@ -150,7 +150,7 @@ export type MActorSummaryFormattable = | |||
150 | 150 | ||
151 | export type MActorFormattable = | 151 | export type MActorFormattable = |
152 | MActorSummaryFormattable & | 152 | MActorSummaryFormattable & |
153 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> & | 153 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'remoteCreatedAt' | 'bannerId' | 'avatarId'> & |
154 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & | 154 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & |
155 | UseOpt<'Banner', MActorImageFormattable> | 155 | UseOpt<'Banner', MActorImageFormattable> |
156 | 156 | ||
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 4af476ed2..2432b7ac4 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -70,13 +70,13 @@ export type PeerTubeHelpers = { | |||
70 | 70 | ||
71 | user: { | 71 | user: { |
72 | // PeerTube >= 3.2 | 72 | // PeerTube >= 3.2 |
73 | getAuthUser: (response: Response) => { | 73 | getAuthUser: (response: Response) => Promise<{ |
74 | id?: string | 74 | id?: string |
75 | username: string | 75 | username: string |
76 | email: string | 76 | email: string |
77 | blocked: boolean | 77 | blocked: boolean |
78 | role: UserRole | 78 | role: UserRole |
79 | } | undefined | 79 | } | undefined> |
80 | } | 80 | } |
81 | } | 81 | } |
82 | 82 | ||
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index cf3e7ae34..55b6e0039 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -19,6 +19,9 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server' | |||
19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' | 19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' |
20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
22 | import { HttpMethod } from '@shared/core-utils/miscs/http-methods' | ||
23 | import { VideoCreate } from '@shared/models' | ||
24 | import { File as UploadXFile, Metadata } from '@uploadx/core' | ||
22 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 25 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
23 | import { | 26 | import { |
24 | MAccountDefault, | 27 | MAccountDefault, |
@@ -37,86 +40,125 @@ import { | |||
37 | MVideoThumbnail, | 40 | MVideoThumbnail, |
38 | MVideoWithRights | 41 | MVideoWithRights |
39 | } from '../../types/models' | 42 | } from '../../types/models' |
40 | |||
41 | declare module 'express' { | 43 | declare module 'express' { |
42 | export interface Request { | 44 | export interface Request { |
43 | query: any | 45 | query: any |
46 | method: HttpMethod | ||
44 | } | 47 | } |
45 | interface Response { | 48 | |
46 | locals: PeerTubeLocals | 49 | // Upload using multer or uploadx middleware |
50 | export type MulterOrUploadXFile = UploadXFile | Express.Multer.File | ||
51 | |||
52 | export type UploadFiles = { | ||
53 | [fieldname: string]: MulterOrUploadXFile[] | ||
54 | } | MulterOrUploadXFile[] | ||
55 | |||
56 | // Partial object used by some functions to check the file mimetype/extension | ||
57 | export type UploadFileForCheck = { | ||
58 | originalname: string | ||
59 | mimetype: string | ||
47 | } | 60 | } |
48 | } | ||
49 | 61 | ||
50 | interface PeerTubeLocals { | 62 | export type UploadFilesForCheck = { |
51 | videoAll?: MVideoFullLight | 63 | [fieldname: string]: UploadFileForCheck[] |
52 | onlyImmutableVideo?: MVideoImmutable | 64 | } | UploadFileForCheck[] |
53 | onlyVideo?: MVideoThumbnail | ||
54 | onlyVideoWithRights?: MVideoWithRights | ||
55 | videoId?: MVideoIdThumbnail | ||
56 | 65 | ||
57 | videoLive?: MVideoLive | 66 | // Upload file with a duration added by our middleware |
67 | export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & { | ||
68 | duration: number | ||
69 | } | ||
58 | 70 | ||
59 | videoShare?: MVideoShareActor | 71 | // Extends Metadata property of UploadX object |
72 | export type UploadXFileMetadata = Metadata & VideoCreate & { | ||
73 | previewfile: Express.Multer.File[] | ||
74 | thumbnailfile: Express.Multer.File[] | ||
75 | } | ||
60 | 76 | ||
61 | videoFile?: MVideoFile | 77 | // Our custom UploadXFile object using our custom metadata |
78 | export type CustomUploadXFile <T extends Metadata> = UploadXFile & { metadata: T } | ||
62 | 79 | ||
63 | videoImport?: MVideoImportDefault | 80 | export type EnhancedUploadXFile = CustomUploadXFile<UploadXFileMetadata> & { |
81 | duration: number | ||
82 | path: string | ||
83 | filename: string | ||
84 | } | ||
64 | 85 | ||
65 | videoBlacklist?: MVideoBlacklist | 86 | // Extends locals property from Response |
87 | interface Response { | ||
88 | locals: { | ||
89 | videoAll?: MVideoFullLight | ||
90 | onlyImmutableVideo?: MVideoImmutable | ||
91 | onlyVideo?: MVideoThumbnail | ||
92 | onlyVideoWithRights?: MVideoWithRights | ||
93 | videoId?: MVideoIdThumbnail | ||
66 | 94 | ||
67 | videoCaption?: MVideoCaptionVideo | 95 | videoLive?: MVideoLive |
68 | 96 | ||
69 | abuse?: MAbuseReporter | 97 | videoShare?: MVideoShareActor |
70 | abuseMessage?: MAbuseMessage | ||
71 | 98 | ||
72 | videoStreamingPlaylist?: MStreamingPlaylist | 99 | videoFile?: MVideoFile |
73 | 100 | ||
74 | videoChannel?: MChannelBannerAccountDefault | 101 | videoFileResumable?: EnhancedUploadXFile |
75 | 102 | ||
76 | videoPlaylistFull?: MVideoPlaylistFull | 103 | videoImport?: MVideoImportDefault |
77 | videoPlaylistSummary?: MVideoPlaylistFullSummary | ||
78 | 104 | ||
79 | videoPlaylistElement?: MVideoPlaylistElement | 105 | videoBlacklist?: MVideoBlacklist |
80 | videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy | ||
81 | 106 | ||
82 | accountVideoRate?: MAccountVideoRateAccountVideo | 107 | videoCaption?: MVideoCaptionVideo |
83 | 108 | ||
84 | videoCommentFull?: MCommentOwnerVideoReply | 109 | abuse?: MAbuseReporter |
85 | videoCommentThread?: MComment | 110 | abuseMessage?: MAbuseMessage |
86 | 111 | ||
87 | follow?: MActorFollowActorsDefault | 112 | videoStreamingPlaylist?: MStreamingPlaylist |
88 | subscription?: MActorFollowActorsDefaultSubscription | ||
89 | 113 | ||
90 | nextOwner?: MAccountDefault | 114 | videoChannel?: MChannelBannerAccountDefault |
91 | videoChangeOwnership?: MVideoChangeOwnershipFull | ||
92 | 115 | ||
93 | account?: MAccountDefault | 116 | videoPlaylistFull?: MVideoPlaylistFull |
117 | videoPlaylistSummary?: MVideoPlaylistFullSummary | ||
94 | 118 | ||
95 | actorUrl?: MActorUrl | 119 | videoPlaylistElement?: MVideoPlaylistElement |
96 | actorFull?: MActorFull | 120 | videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy |
97 | 121 | ||
98 | user?: MUserDefault | 122 | accountVideoRate?: MAccountVideoRateAccountVideo |
99 | 123 | ||
100 | server?: MServer | 124 | videoCommentFull?: MCommentOwnerVideoReply |
125 | videoCommentThread?: MComment | ||
101 | 126 | ||
102 | videoRedundancy?: MVideoRedundancyVideo | 127 | follow?: MActorFollowActorsDefault |
128 | subscription?: MActorFollowActorsDefaultSubscription | ||
103 | 129 | ||
104 | accountBlock?: MAccountBlocklist | 130 | nextOwner?: MAccountDefault |
105 | serverBlock?: MServerBlocklist | 131 | videoChangeOwnership?: MVideoChangeOwnershipFull |
106 | 132 | ||
107 | oauth?: { | 133 | account?: MAccountDefault |
108 | token: MOAuthTokenUser | ||
109 | } | ||
110 | 134 | ||
111 | signature?: { | 135 | actorUrl?: MActorUrl |
112 | actor: MActorAccountChannelId | 136 | actorFull?: MActorFull |
113 | } | 137 | |
138 | user?: MUserDefault | ||
139 | |||
140 | server?: MServer | ||
141 | |||
142 | videoRedundancy?: MVideoRedundancyVideo | ||
114 | 143 | ||
115 | authenticated?: boolean | 144 | accountBlock?: MAccountBlocklist |
145 | serverBlock?: MServerBlocklist | ||
116 | 146 | ||
117 | registeredPlugin?: RegisteredPlugin | 147 | oauth?: { |
148 | token: MOAuthTokenUser | ||
149 | } | ||
118 | 150 | ||
119 | externalAuth?: RegisterServerAuthExternalOptions | 151 | signature?: { |
152 | actor: MActorAccountChannelId | ||
153 | } | ||
120 | 154 | ||
121 | plugin?: MPlugin | 155 | authenticated?: boolean |
156 | |||
157 | registeredPlugin?: RegisteredPlugin | ||
158 | |||
159 | externalAuth?: RegisterServerAuthExternalOptions | ||
160 | |||
161 | plugin?: MPlugin | ||
162 | } | ||
163 | } | ||
122 | } | 164 | } |
diff --git a/shared/core-utils/miscs/http-methods.ts b/shared/core-utils/miscs/http-methods.ts new file mode 100644 index 000000000..1cfa458b9 --- /dev/null +++ b/shared/core-utils/miscs/http-methods.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | /** HTTP request method to indicate the desired action to be performed for a given resource. */ | ||
2 | export enum HttpMethod { | ||
3 | /** The CONNECT method establishes a tunnel to the server identified by the target resource. */ | ||
4 | CONNECT = 'CONNECT', | ||
5 | /** The DELETE method deletes the specified resource. */ | ||
6 | DELETE = 'DELETE', | ||
7 | /** The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. */ | ||
8 | GET = 'GET', | ||
9 | /** The HEAD method asks for a response identical to that of a GET request, but without the response body. */ | ||
10 | HEAD = 'HEAD', | ||
11 | /** The OPTIONS method is used to describe the communication options for the target resource. */ | ||
12 | OPTIONS = 'OPTIONS', | ||
13 | /** The PATCH method is used to apply partial modifications to a resource. */ | ||
14 | PATCH = 'PATCH', | ||
15 | /** The POST method is used to submit an entity to the specified resource */ | ||
16 | POST = 'POST', | ||
17 | /** The PUT method replaces all current representations of the target resource with the request payload. */ | ||
18 | PUT = 'PUT', | ||
19 | /** The TRACE method performs a message loop-back test along the path to the target resource. */ | ||
20 | TRACE = 'TRACE' | ||
21 | } | ||
diff --git a/shared/core-utils/miscs/index.ts b/shared/core-utils/miscs/index.ts index 898fd4791..251df1de2 100644 --- a/shared/core-utils/miscs/index.ts +++ b/shared/core-utils/miscs/index.ts | |||
@@ -2,3 +2,4 @@ export * from './date' | |||
2 | export * from './miscs' | 2 | export * from './miscs' |
3 | export * from './types' | 3 | export * from './types' |
4 | export * from './http-error-codes' | 4 | export * from './http-error-codes' |
5 | export * from './http-methods' | ||
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index 026a5e61c..b70110852 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts | |||
@@ -223,6 +223,18 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti | |||
223 | return updateCustomConfig(url, token, updateParams) | 223 | return updateCustomConfig(url, token, updateParams) |
224 | } | 224 | } |
225 | 225 | ||
226 | function getCustomConfigResolutions (enabled: boolean) { | ||
227 | return { | ||
228 | '240p': enabled, | ||
229 | '360p': enabled, | ||
230 | '480p': enabled, | ||
231 | '720p': enabled, | ||
232 | '1080p': enabled, | ||
233 | '1440p': enabled, | ||
234 | '2160p': enabled | ||
235 | } | ||
236 | } | ||
237 | |||
226 | function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) { | 238 | function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) { |
227 | const path = '/api/v1/config/custom' | 239 | const path = '/api/v1/config/custom' |
228 | 240 | ||
@@ -242,5 +254,6 @@ export { | |||
242 | updateCustomConfig, | 254 | updateCustomConfig, |
243 | getAbout, | 255 | getAbout, |
244 | deleteCustomConfig, | 256 | deleteCustomConfig, |
245 | updateCustomSubConfig | 257 | updateCustomSubConfig, |
258 | getCustomConfigResolutions | ||
246 | } | 259 | } |
diff --git a/shared/extra-utils/server/debug.ts b/shared/extra-utils/server/debug.ts index 5cf80a5fb..f196812b7 100644 --- a/shared/extra-utils/server/debug.ts +++ b/shared/extra-utils/server/debug.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { makeGetRequest } from '../requests/requests' | 1 | import { makeGetRequest, makePostBodyRequest } from '../requests/requests' |
2 | import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes' | 2 | import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes' |
3 | import { SendDebugCommand } from '@shared/models' | ||
3 | 4 | ||
4 | function getDebug (url: string, token: string) { | 5 | function getDebug (url: string, token: string) { |
5 | const path = '/api/v1/server/debug' | 6 | const path = '/api/v1/server/debug' |
@@ -12,8 +13,21 @@ function getDebug (url: string, token: string) { | |||
12 | }) | 13 | }) |
13 | } | 14 | } |
14 | 15 | ||
16 | function sendDebugCommand (url: string, token: string, body: SendDebugCommand) { | ||
17 | const path = '/api/v1/server/debug/run-command' | ||
18 | |||
19 | return makePostBodyRequest({ | ||
20 | url, | ||
21 | path, | ||
22 | token, | ||
23 | fields: body, | ||
24 | statusCodeExpected: HttpStatusCode.NO_CONTENT_204 | ||
25 | }) | ||
26 | } | ||
27 | |||
15 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
16 | 29 | ||
17 | export { | 30 | export { |
18 | getDebug | 31 | getDebug, |
32 | sendDebugCommand | ||
19 | } | 33 | } |
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts index 704929bd4..763374e03 100644 --- a/shared/extra-utils/server/jobs.ts +++ b/shared/extra-utils/server/jobs.ts | |||
@@ -55,7 +55,7 @@ function getJobsListPaginationAndSort (options: { | |||
55 | async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | 55 | async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { |
56 | const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT | 56 | const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT |
57 | ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) | 57 | ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) |
58 | : 500 | 58 | : 250 |
59 | 59 | ||
60 | let servers: ServerInfo[] | 60 | let servers: ServerInfo[] |
61 | 61 | ||
@@ -115,7 +115,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | |||
115 | } | 115 | } |
116 | 116 | ||
117 | if (pendingRequests) { | 117 | if (pendingRequests) { |
118 | await wait(1000) | 118 | await wait(pendingJobWait) |
119 | } | 119 | } |
120 | } while (pendingRequests) | 120 | } while (pendingRequests) |
121 | } | 121 | } |
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 779a3cc36..479f08e12 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts | |||
@@ -274,7 +274,7 @@ async function reRunServer (server: ServerInfo, configOverride?: any) { | |||
274 | } | 274 | } |
275 | 275 | ||
276 | async function checkTmpIsEmpty (server: ServerInfo) { | 276 | async function checkTmpIsEmpty (server: ServerInfo) { |
277 | await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls' ]) | 277 | await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) |
278 | 278 | ||
279 | if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { | 279 | if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { |
280 | await checkDirectoryIsEmpty(server, 'tmp/hls') | 280 | await checkDirectoryIsEmpty(server, 'tmp/hls') |
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index 6040dd9c0..0f15962ad 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { omit } from 'lodash' | 1 | import { omit } from 'lodash' |
2 | import * as request from 'supertest' | 2 | import * as request from 'supertest' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { UserUpdateMe } from '../../models/users' | 4 | import { UserUpdateMe } from '../../models/users' |
4 | import { UserAdminFlag } from '../../models/users/user-flag.model' | 5 | import { UserAdminFlag } from '../../models/users/user-flag.model' |
5 | import { UserRegister } from '../../models/users/user-register.model' | 6 | import { UserRegister } from '../../models/users/user-register.model' |
@@ -7,9 +8,8 @@ import { UserRole } from '../../models/users/user-role' | |||
7 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests' | 8 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests' |
8 | import { ServerInfo } from '../server/servers' | 9 | import { ServerInfo } from '../server/servers' |
9 | import { userLogin } from './login' | 10 | import { userLogin } from './login' |
10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
11 | 11 | ||
12 | type CreateUserArgs = { | 12 | function createUser (parameters: { |
13 | url: string | 13 | url: string |
14 | accessToken: string | 14 | accessToken: string |
15 | username: string | 15 | username: string |
@@ -19,8 +19,7 @@ type CreateUserArgs = { | |||
19 | role?: UserRole | 19 | role?: UserRole |
20 | adminFlags?: UserAdminFlag | 20 | adminFlags?: UserAdminFlag |
21 | specialStatus?: number | 21 | specialStatus?: number |
22 | } | 22 | }) { |
23 | function createUser (parameters: CreateUserArgs) { | ||
24 | const { | 23 | const { |
25 | url, | 24 | url, |
26 | accessToken, | 25 | accessToken, |
@@ -52,6 +51,21 @@ function createUser (parameters: CreateUserArgs) { | |||
52 | .expect(specialStatus) | 51 | .expect(specialStatus) |
53 | } | 52 | } |
54 | 53 | ||
54 | async function generateUser (server: ServerInfo, username: string) { | ||
55 | const password = 'my super password' | ||
56 | const resCreate = await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) | ||
57 | |||
58 | const token = await userLogin(server, { username, password }) | ||
59 | |||
60 | const resMe = await getMyUserInformation(server.url, token) | ||
61 | |||
62 | return { | ||
63 | token, | ||
64 | userId: resCreate.body.user.id, | ||
65 | userChannelId: resMe.body.videoChannels[0].id | ||
66 | } | ||
67 | } | ||
68 | |||
55 | async function generateUserAccessToken (server: ServerInfo, username: string) { | 69 | async function generateUserAccessToken (server: ServerInfo, username: string) { |
56 | const password = 'my super password' | 70 | const password = 'my super password' |
57 | await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) | 71 | await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) |
@@ -393,6 +407,7 @@ export { | |||
393 | resetPassword, | 407 | resetPassword, |
394 | renewUserScopedTokens, | 408 | renewUserScopedTokens, |
395 | updateMyAvatar, | 409 | updateMyAvatar, |
410 | generateUser, | ||
396 | askSendVerifyEmail, | 411 | askSendVerifyEmail, |
397 | generateUserAccessToken, | 412 | generateUserAccessToken, |
398 | verifyEmail, | 413 | verifyEmail, |
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts index d0dfb5856..0aab93e52 100644 --- a/shared/extra-utils/videos/video-channels.ts +++ b/shared/extra-utils/videos/video-channels.ts | |||
@@ -5,7 +5,7 @@ import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-up | |||
5 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' | 5 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' |
6 | import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests' | 6 | import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests' |
7 | import { ServerInfo } from '../server/servers' | 7 | import { ServerInfo } from '../server/servers' |
8 | import { User } from '../../models/users/user.model' | 8 | import { MyUser, User } from '../../models/users/user.model' |
9 | import { getMyUserInformation } from '../users/users' | 9 | import { getMyUserInformation } from '../users/users' |
10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
11 | 11 | ||
@@ -170,6 +170,12 @@ function setDefaultVideoChannel (servers: ServerInfo[]) { | |||
170 | return Promise.all(tasks) | 170 | return Promise.all(tasks) |
171 | } | 171 | } |
172 | 172 | ||
173 | async function getDefaultVideoChannel (url: string, token: string) { | ||
174 | const res = await getMyUserInformation(url, token) | ||
175 | |||
176 | return (res.body as MyUser).videoChannels[0].id | ||
177 | } | ||
178 | |||
173 | // --------------------------------------------------------------------------- | 179 | // --------------------------------------------------------------------------- |
174 | 180 | ||
175 | export { | 181 | export { |
@@ -181,5 +187,6 @@ export { | |||
181 | deleteVideoChannel, | 187 | deleteVideoChannel, |
182 | getVideoChannel, | 188 | getVideoChannel, |
183 | setDefaultVideoChannel, | 189 | setDefaultVideoChannel, |
184 | deleteVideoChannelImage | 190 | deleteVideoChannelImage, |
191 | getDefaultVideoChannel | ||
185 | } | 192 | } |
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index a0143b0ef..e88256ac0 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { pathExists, readdir, readFile } from 'fs-extra' | 4 | import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' |
5 | import got, { Response as GotResponse } from 'got/dist/source' | ||
5 | import * as parseTorrent from 'parse-torrent' | 6 | import * as parseTorrent from 'parse-torrent' |
6 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
7 | import * as request from 'supertest' | 8 | import * as request from 'supertest' |
@@ -42,6 +43,7 @@ type VideoAttributes = { | |||
42 | channelId?: number | 43 | channelId?: number |
43 | privacy?: VideoPrivacy | 44 | privacy?: VideoPrivacy |
44 | fixture?: string | 45 | fixture?: string |
46 | support?: string | ||
45 | thumbnailfile?: string | 47 | thumbnailfile?: string |
46 | previewfile?: string | 48 | previewfile?: string |
47 | scheduleUpdate?: { | 49 | scheduleUpdate?: { |
@@ -364,8 +366,13 @@ async function checkVideoFilesWereRemoved ( | |||
364 | } | 366 | } |
365 | } | 367 | } |
366 | 368 | ||
367 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { | 369 | async function uploadVideo ( |
368 | const path = '/api/v1/videos/upload' | 370 | url: string, |
371 | accessToken: string, | ||
372 | videoAttributesArg: VideoAttributes, | ||
373 | specialStatus = HttpStatusCode.OK_200, | ||
374 | mode: 'legacy' | 'resumable' = 'legacy' | ||
375 | ) { | ||
369 | let defaultChannelId = '1' | 376 | let defaultChannelId = '1' |
370 | 377 | ||
371 | try { | 378 | try { |
@@ -391,74 +398,170 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg | |||
391 | fixture: 'video_short.webm' | 398 | fixture: 'video_short.webm' |
392 | }, videoAttributesArg) | 399 | }, videoAttributesArg) |
393 | 400 | ||
401 | const res = mode === 'legacy' | ||
402 | ? await buildLegacyUpload(url, accessToken, attributes, specialStatus) | ||
403 | : await buildResumeUpload(url, accessToken, attributes, specialStatus) | ||
404 | |||
405 | // Wait torrent generation | ||
406 | if (specialStatus === HttpStatusCode.OK_200) { | ||
407 | let video: VideoDetails | ||
408 | do { | ||
409 | const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid) | ||
410 | video = resVideo.body | ||
411 | |||
412 | await wait(50) | ||
413 | } while (!video.files[0].torrentUrl) | ||
414 | } | ||
415 | |||
416 | return res | ||
417 | } | ||
418 | |||
419 | function checkUploadVideoParam ( | ||
420 | url: string, | ||
421 | token: string, | ||
422 | attributes: Partial<VideoAttributes>, | ||
423 | specialStatus = HttpStatusCode.OK_200, | ||
424 | mode: 'legacy' | 'resumable' = 'legacy' | ||
425 | ) { | ||
426 | return mode === 'legacy' | ||
427 | ? buildLegacyUpload(url, token, attributes, specialStatus) | ||
428 | : buildResumeUpload(url, token, attributes, specialStatus) | ||
429 | } | ||
430 | |||
431 | async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { | ||
432 | const path = '/api/v1/videos/upload' | ||
394 | const req = request(url) | 433 | const req = request(url) |
395 | .post(path) | 434 | .post(path) |
396 | .set('Accept', 'application/json') | 435 | .set('Accept', 'application/json') |
397 | .set('Authorization', 'Bearer ' + accessToken) | 436 | .set('Authorization', 'Bearer ' + token) |
398 | .field('name', attributes.name) | ||
399 | .field('nsfw', JSON.stringify(attributes.nsfw)) | ||
400 | .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) | ||
401 | .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled)) | ||
402 | .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding)) | ||
403 | .field('privacy', attributes.privacy.toString()) | ||
404 | .field('channelId', attributes.channelId) | ||
405 | |||
406 | if (attributes.support !== undefined) { | ||
407 | req.field('support', attributes.support) | ||
408 | } | ||
409 | 437 | ||
410 | if (attributes.description !== undefined) { | 438 | buildUploadReq(req, attributes) |
411 | req.field('description', attributes.description) | ||
412 | } | ||
413 | if (attributes.language !== undefined) { | ||
414 | req.field('language', attributes.language.toString()) | ||
415 | } | ||
416 | if (attributes.category !== undefined) { | ||
417 | req.field('category', attributes.category.toString()) | ||
418 | } | ||
419 | if (attributes.licence !== undefined) { | ||
420 | req.field('licence', attributes.licence.toString()) | ||
421 | } | ||
422 | 439 | ||
423 | const tags = attributes.tags || [] | 440 | if (attributes.fixture !== undefined) { |
424 | for (let i = 0; i < tags.length; i++) { | 441 | req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) |
425 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
426 | } | 442 | } |
427 | 443 | ||
428 | if (attributes.thumbnailfile !== undefined) { | 444 | return req.expect(specialStatus) |
429 | req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) | 445 | } |
430 | } | ||
431 | if (attributes.previewfile !== undefined) { | ||
432 | req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) | ||
433 | } | ||
434 | 446 | ||
435 | if (attributes.scheduleUpdate) { | 447 | async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { |
436 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | 448 | let size = 0 |
449 | let videoFilePath: string | ||
450 | let mimetype = 'video/mp4' | ||
437 | 451 | ||
438 | if (attributes.scheduleUpdate.privacy) { | 452 | if (attributes.fixture) { |
439 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | 453 | videoFilePath = buildAbsoluteFixturePath(attributes.fixture) |
454 | size = (await stat(videoFilePath)).size | ||
455 | |||
456 | if (videoFilePath.endsWith('.mkv')) { | ||
457 | mimetype = 'video/x-matroska' | ||
458 | } else if (videoFilePath.endsWith('.webm')) { | ||
459 | mimetype = 'video/webm' | ||
440 | } | 460 | } |
441 | } | 461 | } |
442 | 462 | ||
443 | if (attributes.originallyPublishedAt !== undefined) { | 463 | const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype }) |
444 | req.field('originallyPublishedAt', attributes.originallyPublishedAt) | 464 | const initStatus = initializeSessionRes.status |
465 | |||
466 | if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) { | ||
467 | const locationHeader = initializeSessionRes.header['location'] | ||
468 | expect(locationHeader).to.not.be.undefined | ||
469 | |||
470 | const pathUploadId = locationHeader.split('?')[1] | ||
471 | |||
472 | return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus }) | ||
445 | } | 473 | } |
446 | 474 | ||
447 | const res = await req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) | 475 | const expectedInitStatus = specialStatus === HttpStatusCode.OK_200 |
448 | .expect(specialStatus) | 476 | ? HttpStatusCode.CREATED_201 |
477 | : specialStatus | ||
449 | 478 | ||
450 | // Wait torrent generation | 479 | expect(initStatus).to.equal(expectedInitStatus) |
451 | if (specialStatus === HttpStatusCode.OK_200) { | ||
452 | let video: VideoDetails | ||
453 | do { | ||
454 | const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid) | ||
455 | video = resVideo.body | ||
456 | 480 | ||
457 | await wait(50) | 481 | return initializeSessionRes |
458 | } while (!video.files[0].torrentUrl) | 482 | } |
483 | |||
484 | async function prepareResumableUpload (options: { | ||
485 | url: string | ||
486 | token: string | ||
487 | attributes: VideoAttributes | ||
488 | size: number | ||
489 | mimetype: string | ||
490 | }) { | ||
491 | const { url, token, attributes, size, mimetype } = options | ||
492 | |||
493 | const path = '/api/v1/videos/upload-resumable' | ||
494 | |||
495 | const req = request(url) | ||
496 | .post(path) | ||
497 | .set('Authorization', 'Bearer ' + token) | ||
498 | .set('X-Upload-Content-Type', mimetype) | ||
499 | .set('X-Upload-Content-Length', size.toString()) | ||
500 | |||
501 | buildUploadReq(req, attributes) | ||
502 | |||
503 | if (attributes.fixture) { | ||
504 | req.field('filename', attributes.fixture) | ||
459 | } | 505 | } |
460 | 506 | ||
461 | return res | 507 | return req |
508 | } | ||
509 | |||
510 | function sendResumableChunks (options: { | ||
511 | url: string | ||
512 | token: string | ||
513 | pathUploadId: string | ||
514 | videoFilePath: string | ||
515 | size: number | ||
516 | specialStatus?: HttpStatusCode | ||
517 | contentLength?: number | ||
518 | contentRangeBuilder?: (start: number, chunk: any) => string | ||
519 | }) { | ||
520 | const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options | ||
521 | |||
522 | const expectedStatus = specialStatus || HttpStatusCode.OK_200 | ||
523 | |||
524 | const path = '/api/v1/videos/upload-resumable' | ||
525 | let start = 0 | ||
526 | |||
527 | const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 }) | ||
528 | return new Promise<GotResponse>((resolve, reject) => { | ||
529 | readable.on('data', async function onData (chunk) { | ||
530 | readable.pause() | ||
531 | |||
532 | const headers = { | ||
533 | 'Authorization': 'Bearer ' + token, | ||
534 | 'Content-Type': 'application/octet-stream', | ||
535 | 'Content-Range': contentRangeBuilder | ||
536 | ? contentRangeBuilder(start, chunk) | ||
537 | : `bytes ${start}-${start + chunk.length - 1}/${size}`, | ||
538 | 'Content-Length': contentLength ? contentLength + '' : chunk.length + '' | ||
539 | } | ||
540 | |||
541 | const res = await got({ | ||
542 | url, | ||
543 | method: 'put', | ||
544 | headers, | ||
545 | path: path + '?' + pathUploadId, | ||
546 | body: chunk, | ||
547 | responseType: 'json', | ||
548 | throwHttpErrors: false | ||
549 | }) | ||
550 | |||
551 | start += chunk.length | ||
552 | |||
553 | if (res.statusCode === expectedStatus) { | ||
554 | return resolve(res) | ||
555 | } | ||
556 | |||
557 | if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) { | ||
558 | readable.off('data', onData) | ||
559 | return reject(new Error('Incorrect transient behaviour sending intermediary chunks')) | ||
560 | } | ||
561 | |||
562 | readable.resume() | ||
563 | }) | ||
564 | }) | ||
462 | } | 565 | } |
463 | 566 | ||
464 | function updateVideo ( | 567 | function updateVideo ( |
@@ -749,11 +852,13 @@ export { | |||
749 | getVideoWithToken, | 852 | getVideoWithToken, |
750 | getVideosList, | 853 | getVideosList, |
751 | removeAllVideos, | 854 | removeAllVideos, |
855 | checkUploadVideoParam, | ||
752 | getVideosListPagination, | 856 | getVideosListPagination, |
753 | getVideosListSort, | 857 | getVideosListSort, |
754 | removeVideo, | 858 | removeVideo, |
755 | getVideosListWithToken, | 859 | getVideosListWithToken, |
756 | uploadVideo, | 860 | uploadVideo, |
861 | sendResumableChunks, | ||
757 | getVideosWithFilters, | 862 | getVideosWithFilters, |
758 | uploadRandomVideoOnServers, | 863 | uploadRandomVideoOnServers, |
759 | updateVideo, | 864 | updateVideo, |
@@ -767,5 +872,50 @@ export { | |||
767 | getMyVideosWithFilter, | 872 | getMyVideosWithFilter, |
768 | uploadVideoAndGetId, | 873 | uploadVideoAndGetId, |
769 | getLocalIdByUUID, | 874 | getLocalIdByUUID, |
770 | getVideoIdFromUUID | 875 | getVideoIdFromUUID, |
876 | prepareResumableUpload | ||
877 | } | ||
878 | |||
879 | // --------------------------------------------------------------------------- | ||
880 | |||
881 | function buildUploadReq (req: request.Test, attributes: VideoAttributes) { | ||
882 | |||
883 | for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) { | ||
884 | if (attributes[key] !== undefined) { | ||
885 | req.field(key, attributes[key]) | ||
886 | } | ||
887 | } | ||
888 | |||
889 | for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) { | ||
890 | if (attributes[key] !== undefined) { | ||
891 | req.field(key, JSON.stringify(attributes[key])) | ||
892 | } | ||
893 | } | ||
894 | |||
895 | for (const key of [ 'language', 'privacy', 'category', 'licence' ]) { | ||
896 | if (attributes[key] !== undefined) { | ||
897 | req.field(key, attributes[key].toString()) | ||
898 | } | ||
899 | } | ||
900 | |||
901 | const tags = attributes.tags || [] | ||
902 | for (let i = 0; i < tags.length; i++) { | ||
903 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
904 | } | ||
905 | |||
906 | for (const key of [ 'thumbnailfile', 'previewfile' ]) { | ||
907 | if (attributes[key] !== undefined) { | ||
908 | req.attach(key, buildAbsoluteFixturePath(attributes[key])) | ||
909 | } | ||
910 | } | ||
911 | |||
912 | if (attributes.scheduleUpdate) { | ||
913 | if (attributes.scheduleUpdate.updateAt) { | ||
914 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | ||
915 | } | ||
916 | |||
917 | if (attributes.scheduleUpdate.privacy) { | ||
918 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | ||
919 | } | ||
920 | } | ||
771 | } | 921 | } |
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index c59be3f3b..09d4f7402 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts | |||
@@ -29,4 +29,6 @@ export interface ActivityPubActor { | |||
29 | 29 | ||
30 | icon?: ActivityIconObject | 30 | icon?: ActivityIconObject |
31 | image?: ActivityIconObject | 31 | image?: ActivityIconObject |
32 | |||
33 | published?: string | ||
32 | } | 34 | } |
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts index 120dec271..f2138077e 100644 --- a/shared/models/actors/account.model.ts +++ b/shared/models/actors/account.model.ts | |||
@@ -5,6 +5,8 @@ export interface Account extends Actor { | |||
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | 7 | ||
8 | updatedAt: Date | string | ||
9 | |||
8 | userId?: number | 10 | userId?: number |
9 | } | 11 | } |
10 | 12 | ||
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts index 7d9f35b10..fd0662331 100644 --- a/shared/models/actors/actor.model.ts +++ b/shared/models/actors/actor.model.ts | |||
@@ -8,6 +8,5 @@ export interface Actor { | |||
8 | followingCount: number | 8 | followingCount: number |
9 | followersCount: number | 9 | followersCount: number |
10 | createdAt: Date | string | 10 | createdAt: Date | string |
11 | updatedAt: Date | string | ||
12 | avatar?: ActorImage | 11 | avatar?: ActorImage |
13 | } | 12 | } |
diff --git a/shared/models/search/boolean-both-query.model.ts b/shared/models/search/boolean-both-query.model.ts index 57b0e8d44..d6a438249 100644 --- a/shared/models/search/boolean-both-query.model.ts +++ b/shared/models/search/boolean-both-query.model.ts | |||
@@ -1 +1,2 @@ | |||
1 | export type BooleanBothQuery = 'true' | 'false' | 'both' | 1 | export type BooleanBothQuery = 'true' | 'false' | 'both' |
2 | export type BooleanQuery = 'true' | 'false' | ||
diff --git a/shared/models/server/debug.model.ts b/shared/models/server/debug.model.ts index 61cba6518..7ceff9137 100644 --- a/shared/models/server/debug.model.ts +++ b/shared/models/server/debug.model.ts | |||
@@ -1,3 +1,7 @@ | |||
1 | export interface Debug { | 1 | export interface Debug { |
2 | ip: string | 2 | ip: string |
3 | } | 3 | } |
4 | |||
5 | export interface SendDebugCommand { | ||
6 | command: 'remove-dandling-resumable-uploads' | ||
7 | } | ||
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index 56517972d..5393f924d 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -11,6 +11,9 @@ export interface VideoChannel extends Actor { | |||
11 | description: string | 11 | description: string |
12 | support: string | 12 | support: string |
13 | isLocal: boolean | 13 | isLocal: boolean |
14 | |||
15 | updatedAt: Date | string | ||
16 | |||
14 | ownerAccount?: Account | 17 | ownerAccount?: Account |
15 | 18 | ||
16 | videosCount?: number | 19 | videosCount?: number |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index d4fe15664..4fbf5b055 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -12,8 +12,6 @@ info: | |||
12 | url: 'https://joinpeertube.org/img/brand.png' | 12 | url: 'https://joinpeertube.org/img/brand.png' |
13 | altText: PeerTube Project Homepage | 13 | altText: PeerTube Project Homepage |
14 | description: | | 14 | description: | |
15 | # Introduction | ||
16 | |||
17 | The PeerTube API is built on HTTP(S) and is RESTful. You can use your favorite | 15 | The PeerTube API is built on HTTP(S) and is RESTful. You can use your favorite |
18 | HTTP/REST library for your programming language to use PeerTube. The spec API is fully compatible with | 16 | HTTP/REST library for your programming language to use PeerTube. The spec API is fully compatible with |
19 | [openapi-generator](https://github.com/OpenAPITools/openapi-generator/wiki/API-client-generator-HOWTO) | 17 | [openapi-generator](https://github.com/OpenAPITools/openapi-generator/wiki/API-client-generator-HOWTO) |
@@ -23,13 +21,14 @@ info: | |||
23 | - [Go](https://framagit.org/framasoft/peertube/clients/go) | 21 | - [Go](https://framagit.org/framasoft/peertube/clients/go) |
24 | - [Kotlin](https://framagit.org/framasoft/peertube/clients/kotlin) | 22 | - [Kotlin](https://framagit.org/framasoft/peertube/clients/kotlin) |
25 | 23 | ||
26 | See the [Quick Start guide](https://docs.joinpeertube.org/api-rest-getting-started) so you can play with the PeerTube API. | 24 | See the [REST API quick start](https://docs.joinpeertube.org/api-rest-getting-started) for a few |
25 | examples of using with the PeerTube API. | ||
27 | 26 | ||
28 | # Authentication | 27 | # Authentication |
29 | 28 | ||
30 | When you sign up for an account, you are given the possibility to generate | 29 | When you sign up for an account on a PeerTube instance, you are given the possibility |
31 | sessions, and authenticate using this session token. One session token can | 30 | to generate sessions on it, and authenticate there using a session token. Only __one |
32 | currently be used at a time. | 31 | session token can currently be used at a time__. |
33 | 32 | ||
34 | ## Roles | 33 | ## Roles |
35 | 34 | ||
@@ -48,6 +47,30 @@ info: | |||
48 | "error": "Token is invalid." // example exposed error message | 47 | "error": "Token is invalid." // example exposed error message |
49 | } | 48 | } |
50 | ``` | 49 | ``` |
50 | |||
51 | # Rate limits | ||
52 | |||
53 | We are rate-limiting all endpoints of PeerTube's API. Custom values can be set by administrators: | ||
54 | |||
55 | | Endpoint | Calls | Time frame | | ||
56 | |-------------------------|------------------|---------------------------| | ||
57 | | `/*` | 50 | 10 seconds | | ||
58 | | `POST /users/token` | 15 | 5 minutes | | ||
59 | | `POST /users/register` | 2¹ | 5 minutes | | ||
60 | | `POST /users/ask-send-verify-email` | 3 | 5 minutes | | ||
61 | |||
62 | Depending on the endpoint, ¹failed requests are not taken into account. A service | ||
63 | limit is announced by a `429 Too Many Requests` status code. | ||
64 | |||
65 | You can get details about the current state of your rate limit by reading the | ||
66 | following headers: | ||
67 | |||
68 | | Header | Description | | ||
69 | |-------------------------|------------------------------------------------------------| | ||
70 | | X-RateLimit-Limit | Number of max requests allowed in the current time period | | ||
71 | | X-RateLimit-Remaining | Number of remaining requests in the current time period | | ||
72 | | X-RateLimit-Reset | Timestamp of end of current time period as UNIX timestamp | | ||
73 | | Retry-After | Seconds to delay after the first `429` is received | | ||
51 | externalDocs: | 74 | externalDocs: |
52 | url: https://docs.joinpeertube.org/api-rest-reference.html | 75 | url: https://docs.joinpeertube.org/api-rest-reference.html |
53 | tags: | 76 | tags: |
@@ -101,7 +124,7 @@ tags: | |||
101 | Redundancy is part of the inter-server solidarity that PeerTube fosters. | 124 | Redundancy is part of the inter-server solidarity that PeerTube fosters. |
102 | Manage the list of instances you wish to help by seeding their videos according | 125 | Manage the list of instances you wish to help by seeding their videos according |
103 | to the policy of video selection of your choice. Note that you have a similar functionality | 126 | to the policy of video selection of your choice. Note that you have a similar functionality |
104 | to mirror individual videos, see `Video Mirroring`. | 127 | to mirror individual videos, see [video mirroring](#tag/Video-Mirroring). |
105 | externalDocs: | 128 | externalDocs: |
106 | url: https://docs.joinpeertube.org/admin-following-instances?id=instances-redundancy | 129 | url: https://docs.joinpeertube.org/admin-following-instances?id=instances-redundancy |
107 | - name: Plugins | 130 | - name: Plugins |
@@ -115,6 +138,50 @@ tags: | |||
115 | - name: Video | 138 | - name: Video |
116 | description: | | 139 | description: | |
117 | Operations dealing with listing, uploading, fetching or modifying videos. | 140 | Operations dealing with listing, uploading, fetching or modifying videos. |
141 | - name: Video Upload | ||
142 | description: | | ||
143 | Operations dealing with adding video or audio. PeerTube supports two upload modes, and three import modes. | ||
144 | |||
145 | ### Upload | ||
146 | |||
147 | - [_legacy_](#operation/uploadLegacy), where the video file is sent in a single request | ||
148 | - [_resumable_](#operation/uploadResumableInit), where the video file is sent in chunks | ||
149 | |||
150 | You can upload videos more reliably by using the resumable variant. Its protocol lets | ||
151 | you resume an upload operation after a network interruption or other transmission failure, | ||
152 | saving time and bandwidth in the event of network failures. | ||
153 | |||
154 | Favor using resumable uploads in any of the following cases: | ||
155 | - You are transferring large files | ||
156 | - The likelihood of a network interruption is high | ||
157 | - Uploads are originating from a device with a low-bandwidth or unstable Internet connection, | ||
158 | such as a mobile device | ||
159 | |||
160 | ### Import | ||
161 | |||
162 | - _URL_-based: where the URL points to any service supported by [youtube-dl](https://ytdl-org.github.io/youtube-dl/) | ||
163 | - _magnet_-based: where the URI resolves to a BitTorrent ressource containing a single supported video file | ||
164 | - _torrent_-based: where the metainfo file resolves to a BitTorrent ressource containing a single supported video file | ||
165 | |||
166 | The import function is practical when the desired video/audio is available online. It makes PeerTube | ||
167 | download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have. | ||
168 | - name: Video Captions | ||
169 | description: Operations dealing with listing, adding and removing closed captions of a video. | ||
170 | - name: Video Channels | ||
171 | description: Operations dealing with the creation, modification and listing of videos within a channel. | ||
172 | - name: Video Comments | ||
173 | description: > | ||
174 | Operations dealing with comments to a video. Comments are organized in threads: adding a | ||
175 | comment in response to the video starts a thread, adding a reply to a comment adds it to | ||
176 | its root comment thread. | ||
177 | - name: Video Blocks | ||
178 | description: Operations dealing with blocking videos (removing them from view and preventing interactions). | ||
179 | - name: Video Rates | ||
180 | description: Like/dislike a video. | ||
181 | - name: Video Playlists | ||
182 | description: Operations dealing with playlists of videos. Playlists are bound to users and/or channels. | ||
183 | - name: Feeds | ||
184 | description: Server syndication feeds | ||
118 | - name: Search | 185 | - name: Search |
119 | description: | | 186 | description: | |
120 | The search helps to find _videos_ or _channels_ from within the instance and beyond. | 187 | The search helps to find _videos_ or _channels_ from within the instance and beyond. |
@@ -124,27 +191,11 @@ tags: | |||
124 | 191 | ||
125 | Administrators can also enable the use of a remote search system, indexing | 192 | Administrators can also enable the use of a remote search system, indexing |
126 | videos and channels not could be not federated by the instance. | 193 | videos and channels not could be not federated by the instance. |
127 | - name: Video Comments | 194 | - name: Video Mirroring |
128 | description: > | 195 | description: | |
129 | Operations dealing with comments to a video. Comments are organized in | 196 | PeerTube instances can mirror videos from one another, and help distribute some videos. |
130 | threads. | 197 | |
131 | - name: Video Playlists | 198 | For importing videos as your own, refer to [video imports](#tag/Video-Upload/paths/~1videos~1imports/post). |
132 | description: > | ||
133 | Operations dealing with playlists of videos. Playlists are bound to users | ||
134 | and/or channels. | ||
135 | - name: Video Channels | ||
136 | description: > | ||
137 | Operations dealing with the creation, modification and listing of videos within a channel. | ||
138 | - name: Video Blocks | ||
139 | description: > | ||
140 | Operations dealing with blocking videos (removing them from view and | ||
141 | preventing interactions). | ||
142 | - name: Video Rates | ||
143 | description: > | ||
144 | Like/dislike a video. | ||
145 | - name: Feeds | ||
146 | description: > | ||
147 | Server syndication feeds | ||
148 | x-tagGroups: | 199 | x-tagGroups: |
149 | - name: Accounts | 200 | - name: Accounts |
150 | tags: | 201 | tags: |
@@ -157,6 +208,7 @@ x-tagGroups: | |||
157 | - name: Videos | 208 | - name: Videos |
158 | tags: | 209 | tags: |
159 | - Video | 210 | - Video |
211 | - Video Upload | ||
160 | - Video Captions | 212 | - Video Captions |
161 | - Video Channels | 213 | - Video Channels |
162 | - Video Comments | 214 | - Video Comments |
@@ -228,7 +280,7 @@ paths: | |||
228 | application/json: | 280 | application/json: |
229 | schema: | 281 | schema: |
230 | $ref: '#/components/schemas/VideoListResponse' | 282 | $ref: '#/components/schemas/VideoListResponse' |
231 | x-code-samples: | 283 | x-codeSamples: |
232 | - lang: JavaScript | 284 | - lang: JavaScript |
233 | source: | | 285 | source: | |
234 | fetch('https://peertube2.cpy.re/api/v1/accounts/{name}/videos') | 286 | fetch('https://peertube2.cpy.re/api/v1/accounts/{name}/videos') |
@@ -292,6 +344,9 @@ paths: | |||
292 | application/json: | 344 | application/json: |
293 | schema: | 345 | schema: |
294 | $ref: '#/components/schemas/ServerConfig' | 346 | $ref: '#/components/schemas/ServerConfig' |
347 | examples: | ||
348 | nightly: | ||
349 | externalValue: https://peertube2.cpy.re/api/v1/config | ||
295 | /config/about: | 350 | /config/about: |
296 | get: | 351 | get: |
297 | summary: Get instance "About" information | 352 | summary: Get instance "About" information |
@@ -304,6 +359,9 @@ paths: | |||
304 | application/json: | 359 | application/json: |
305 | schema: | 360 | schema: |
306 | $ref: '#/components/schemas/ServerConfigAbout' | 361 | $ref: '#/components/schemas/ServerConfigAbout' |
362 | examples: | ||
363 | nightly: | ||
364 | externalValue: https://peertube2.cpy.re/api/v1/config/about | ||
307 | /config/custom: | 365 | /config/custom: |
308 | get: | 366 | get: |
309 | summary: Get instance runtime configuration | 367 | summary: Get instance runtime configuration |
@@ -566,13 +624,24 @@ paths: | |||
566 | tags: | 624 | tags: |
567 | - Users | 625 | - Users |
568 | operationId: getUserId | 626 | operationId: getUserId |
627 | parameters: | ||
628 | - name: withStats | ||
629 | in: query | ||
630 | description: include statistics about the user (only available as a moderator/admin) | ||
631 | schema: | ||
632 | type: boolean | ||
569 | responses: | 633 | responses: |
570 | '200': | 634 | '200': |
571 | description: successful operation | 635 | x-summary: successful operation |
636 | description: | | ||
637 | As an admin/moderator, you can request a response augmented with statistics about the user's | ||
638 | moderation relations and videos usage, by using the `withStats` parameter. | ||
572 | content: | 639 | content: |
573 | application/json: | 640 | application/json: |
574 | schema: | 641 | schema: |
575 | $ref: '#/components/schemas/User' | 642 | oneOf: |
643 | - $ref: '#/components/schemas/User' | ||
644 | - $ref: '#/components/schemas/UserWithStats' | ||
576 | put: | 645 | put: |
577 | summary: Update a user | 646 | summary: Update a user |
578 | security: | 647 | security: |
@@ -655,7 +724,7 @@ paths: | |||
655 | content: | 724 | content: |
656 | application/json: | 725 | application/json: |
657 | schema: | 726 | schema: |
658 | $ref: '#/components/schemas/VideoImport' | 727 | $ref: '#/components/schemas/VideoImportsList' |
659 | /users/me/video-quota-used: | 728 | /users/me/video-quota-used: |
660 | get: | 729 | get: |
661 | summary: Get my user used quota | 730 | summary: Get my user used quota |
@@ -670,7 +739,14 @@ paths: | |||
670 | content: | 739 | content: |
671 | application/json: | 740 | application/json: |
672 | schema: | 741 | schema: |
673 | type: number | 742 | type: object |
743 | properties: | ||
744 | videoQuotaUsed: | ||
745 | type: number | ||
746 | example: 16810141515 | ||
747 | videoQuotaUsedDaily: | ||
748 | type: number | ||
749 | example: 1681014151 | ||
674 | '/users/me/videos/{videoId}/rating': | 750 | '/users/me/videos/{videoId}/rating': |
675 | get: | 751 | get: |
676 | summary: Get rate of my user for a video | 752 | summary: Get rate of my user for a video |
@@ -728,6 +804,10 @@ paths: | |||
728 | responses: | 804 | responses: |
729 | '200': | 805 | '200': |
730 | description: successful operation | 806 | description: successful operation |
807 | content: | ||
808 | application/json: | ||
809 | schema: | ||
810 | $ref: '#/components/schemas/VideoChannelList' | ||
731 | post: | 811 | post: |
732 | tags: | 812 | tags: |
733 | - My Subscriptions | 813 | - My Subscriptions |
@@ -1109,6 +1189,7 @@ paths: | |||
1109 | /videos/categories: | 1189 | /videos/categories: |
1110 | get: | 1190 | get: |
1111 | summary: List available video categories | 1191 | summary: List available video categories |
1192 | operationId: getCategories | ||
1112 | tags: | 1193 | tags: |
1113 | - Video | 1194 | - Video |
1114 | responses: | 1195 | responses: |
@@ -1126,6 +1207,7 @@ paths: | |||
1126 | /videos/licences: | 1207 | /videos/licences: |
1127 | get: | 1208 | get: |
1128 | summary: List available video licences | 1209 | summary: List available video licences |
1210 | operationId: getLicences | ||
1129 | tags: | 1211 | tags: |
1130 | - Video | 1212 | - Video |
1131 | responses: | 1213 | responses: |
@@ -1143,6 +1225,7 @@ paths: | |||
1143 | /videos/languages: | 1225 | /videos/languages: |
1144 | get: | 1226 | get: |
1145 | summary: List available video languages | 1227 | summary: List available video languages |
1228 | operationId: getLanguages | ||
1146 | tags: | 1229 | tags: |
1147 | - Video | 1230 | - Video |
1148 | responses: | 1231 | responses: |
@@ -1159,7 +1242,8 @@ paths: | |||
1159 | externalValue: https://peertube2.cpy.re/api/v1/videos/languages | 1242 | externalValue: https://peertube2.cpy.re/api/v1/videos/languages |
1160 | /videos/privacies: | 1243 | /videos/privacies: |
1161 | get: | 1244 | get: |
1162 | summary: List available video privacies | 1245 | summary: List available video privacy policies |
1246 | operationId: getPrivacyPolicies | ||
1163 | tags: | 1247 | tags: |
1164 | - Video | 1248 | - Video |
1165 | responses: | 1249 | responses: |
@@ -1201,16 +1285,11 @@ paths: | |||
1201 | type: string | 1285 | type: string |
1202 | format: binary | 1286 | format: binary |
1203 | category: | 1287 | category: |
1204 | description: Video category | 1288 | $ref: '#/components/schemas/VideoCategorySet' |
1205 | type: integer | ||
1206 | example: 4 | ||
1207 | licence: | 1289 | licence: |
1208 | description: Video licence | 1290 | $ref: '#/components/schemas/VideoLicenceSet' |
1209 | type: integer | ||
1210 | example: 2 | ||
1211 | language: | 1291 | language: |
1212 | description: Video language | 1292 | $ref: '#/components/schemas/VideoLanguageSet' |
1213 | type: string | ||
1214 | privacy: | 1293 | privacy: |
1215 | $ref: '#/components/schemas/VideoPrivacySet' | 1294 | $ref: '#/components/schemas/VideoPrivacySet' |
1216 | description: | 1295 | description: |
@@ -1323,10 +1402,13 @@ paths: | |||
1323 | /videos/upload: | 1402 | /videos/upload: |
1324 | post: | 1403 | post: |
1325 | summary: Upload a video | 1404 | summary: Upload a video |
1405 | description: Uses a single request to upload a video. | ||
1406 | operationId: uploadLegacy | ||
1326 | security: | 1407 | security: |
1327 | - OAuth2: [] | 1408 | - OAuth2: [] |
1328 | tags: | 1409 | tags: |
1329 | - Video | 1410 | - Video |
1411 | - Video Upload | ||
1330 | responses: | 1412 | responses: |
1331 | '200': | 1413 | '200': |
1332 | description: successful operation | 1414 | description: successful operation |
@@ -1356,80 +1438,7 @@ paths: | |||
1356 | content: | 1438 | content: |
1357 | multipart/form-data: | 1439 | multipart/form-data: |
1358 | schema: | 1440 | schema: |
1359 | type: object | 1441 | $ref: '#/components/schemas/VideoUploadRequestLegacy' |
1360 | properties: | ||
1361 | videofile: | ||
1362 | description: Video file | ||
1363 | type: string | ||
1364 | format: binary | ||
1365 | channelId: | ||
1366 | description: Channel id that will contain this video | ||
1367 | type: integer | ||
1368 | thumbnailfile: | ||
1369 | description: Video thumbnail file | ||
1370 | type: string | ||
1371 | format: binary | ||
1372 | previewfile: | ||
1373 | description: Video preview file | ||
1374 | type: string | ||
1375 | format: binary | ||
1376 | privacy: | ||
1377 | $ref: '#/components/schemas/VideoPrivacySet' | ||
1378 | category: | ||
1379 | description: Video category | ||
1380 | type: integer | ||
1381 | example: 4 | ||
1382 | licence: | ||
1383 | description: Video licence | ||
1384 | type: integer | ||
1385 | example: 2 | ||
1386 | language: | ||
1387 | description: Video language | ||
1388 | type: string | ||
1389 | description: | ||
1390 | description: Video description | ||
1391 | type: string | ||
1392 | waitTranscoding: | ||
1393 | description: Whether or not we wait transcoding before publish the video | ||
1394 | type: boolean | ||
1395 | support: | ||
1396 | description: A text tell the audience how to support the video creator | ||
1397 | example: Please support my work on <insert crowdfunding plateform>! <3 | ||
1398 | type: string | ||
1399 | nsfw: | ||
1400 | description: Whether or not this video contains sensitive content | ||
1401 | type: boolean | ||
1402 | name: | ||
1403 | description: Video name | ||
1404 | type: string | ||
1405 | minLength: 3 | ||
1406 | maxLength: 120 | ||
1407 | tags: | ||
1408 | description: Video tags (maximum 5 tags each between 2 and 30 characters) | ||
1409 | type: array | ||
1410 | minItems: 1 | ||
1411 | maxItems: 5 | ||
1412 | uniqueItems: true | ||
1413 | items: | ||
1414 | type: string | ||
1415 | minLength: 2 | ||
1416 | maxLength: 30 | ||
1417 | commentsEnabled: | ||
1418 | description: Enable or disable comments for this video | ||
1419 | type: boolean | ||
1420 | downloadEnabled: | ||
1421 | description: Enable or disable downloading for this video | ||
1422 | type: boolean | ||
1423 | originallyPublishedAt: | ||
1424 | description: Date when the content was originally published | ||
1425 | type: string | ||
1426 | format: date-time | ||
1427 | scheduleUpdate: | ||
1428 | $ref: '#/components/schemas/VideoScheduledUpdate' | ||
1429 | required: | ||
1430 | - videofile | ||
1431 | - channelId | ||
1432 | - name | ||
1433 | encoding: | 1442 | encoding: |
1434 | videofile: | 1443 | videofile: |
1435 | contentType: video/mp4, video/webm, video/ogg, video/avi, video/quicktime, video/x-msvideo, video/x-flv, video/x-matroska, application/octet-stream | 1444 | contentType: video/mp4, video/webm, video/ogg, video/avi, video/quicktime, video/x-msvideo, video/x-flv, video/x-matroska, application/octet-stream |
@@ -1437,7 +1446,7 @@ paths: | |||
1437 | contentType: image/jpeg | 1446 | contentType: image/jpeg |
1438 | previewfile: | 1447 | previewfile: |
1439 | contentType: image/jpeg | 1448 | contentType: image/jpeg |
1440 | x-code-samples: | 1449 | x-codeSamples: |
1441 | - lang: Shell | 1450 | - lang: Shell |
1442 | source: | | 1451 | source: | |
1443 | ## DEPENDENCIES: jq | 1452 | ## DEPENDENCIES: jq |
@@ -1466,14 +1475,177 @@ paths: | |||
1466 | --form videofile=@"$FILE_PATH" \ | 1475 | --form videofile=@"$FILE_PATH" \ |
1467 | --form channelId=$CHANNEL_ID \ | 1476 | --form channelId=$CHANNEL_ID \ |
1468 | --form name="$NAME" | 1477 | --form name="$NAME" |
1478 | /videos/upload-resumable: | ||
1479 | post: | ||
1480 | summary: Initialize the resumable upload of a video | ||
1481 | description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to initialize the upload of a video | ||
1482 | operationId: uploadResumableInit | ||
1483 | security: | ||
1484 | - OAuth2: [] | ||
1485 | tags: | ||
1486 | - Video | ||
1487 | - Video Upload | ||
1488 | parameters: | ||
1489 | - name: X-Upload-Content-Length | ||
1490 | in: header | ||
1491 | schema: | ||
1492 | type: number | ||
1493 | example: 2469036 | ||
1494 | required: true | ||
1495 | description: Number of bytes that will be uploaded in subsequent requests. Set this value to the size of the file you are uploading. | ||
1496 | - name: X-Upload-Content-Type | ||
1497 | in: header | ||
1498 | schema: | ||
1499 | type: string | ||
1500 | format: mimetype | ||
1501 | example: video/mp4 | ||
1502 | required: true | ||
1503 | description: MIME type of the file that you are uploading. Depending on your instance settings, acceptable values might vary. | ||
1504 | requestBody: | ||
1505 | content: | ||
1506 | application/json: | ||
1507 | schema: | ||
1508 | $ref: '#/components/schemas/VideoUploadRequestResumable' | ||
1509 | responses: | ||
1510 | '200': | ||
1511 | description: file already exists, send a [`resume`](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) request instead | ||
1512 | '201': | ||
1513 | description: created | ||
1514 | headers: | ||
1515 | Location: | ||
1516 | schema: | ||
1517 | type: string | ||
1518 | format: url | ||
1519 | example: /api/v1/videos/upload-resumable?upload_id=471e97554f21dec3b8bb5d4602939c51 | ||
1520 | Content-Length: | ||
1521 | schema: | ||
1522 | type: number | ||
1523 | example: 0 | ||
1524 | '400': | ||
1525 | description: invalid file field, schedule date or parameter | ||
1526 | '413': | ||
1527 | description: video file too large, due to quota, absolute max file size or concurrent partial upload limit | ||
1528 | '415': | ||
1529 | description: video type unsupported | ||
1530 | put: | ||
1531 | summary: Send chunk for the resumable upload of a video | ||
1532 | description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to continue, pause or resume the upload of a video | ||
1533 | operationId: uploadResumable | ||
1534 | security: | ||
1535 | - OAuth2: [] | ||
1536 | tags: | ||
1537 | - Video | ||
1538 | - Video Upload | ||
1539 | parameters: | ||
1540 | - name: upload_id | ||
1541 | in: path | ||
1542 | required: true | ||
1543 | description: | | ||
1544 | Created session id to proceed with. If you didn't send chunks in the last 12 hours, it is | ||
1545 | not valid anymore and you need to initialize a new upload. | ||
1546 | schema: | ||
1547 | type: string | ||
1548 | - name: Content-Range | ||
1549 | in: header | ||
1550 | schema: | ||
1551 | type: string | ||
1552 | example: bytes 0-262143/2469036 | ||
1553 | required: true | ||
1554 | description: | | ||
1555 | Specifies the bytes in the file that the request is uploading. | ||
1556 | |||
1557 | For example, a value of `bytes 0-262143/1000000` shows that the request is sending the first | ||
1558 | 262144 bytes (256 x 1024) in a 2,469,036 byte file. | ||
1559 | - name: Content-Length | ||
1560 | in: header | ||
1561 | schema: | ||
1562 | type: number | ||
1563 | example: 262144 | ||
1564 | required: true | ||
1565 | description: | | ||
1566 | Size of the chunk that the request is sending. | ||
1567 | |||
1568 | The chunk size __must be a multiple of 256 KB__, and unlike [Google Resumable](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol) | ||
1569 | doesn't mandate for chunks to have the same size throughout the upload sequence. | ||
1570 | |||
1571 | Remember that larger chunks are more efficient. PeerTube's web client uses chunks varying from | ||
1572 | 1048576 bytes (~1MB) and increases or reduces size depending on connection health. | ||
1573 | requestBody: | ||
1574 | content: | ||
1575 | application/octet-stream: | ||
1576 | schema: | ||
1577 | type: string | ||
1578 | format: binary | ||
1579 | responses: | ||
1580 | '200': | ||
1581 | description: last chunk received | ||
1582 | headers: | ||
1583 | Content-Length: | ||
1584 | schema: | ||
1585 | type: number | ||
1586 | content: | ||
1587 | application/json: | ||
1588 | schema: | ||
1589 | $ref: '#/components/schemas/VideoUploadResponse' | ||
1590 | '308': | ||
1591 | description: resume incomplete | ||
1592 | headers: | ||
1593 | Range: | ||
1594 | schema: | ||
1595 | type: string | ||
1596 | example: bytes=0-262143 | ||
1597 | Content-Length: | ||
1598 | schema: | ||
1599 | type: number | ||
1600 | example: 0 | ||
1601 | '403': | ||
1602 | description: video didn't pass upload filter | ||
1603 | '413': | ||
1604 | description: video file too large, due to quota or max body size limit set by the reverse-proxy | ||
1605 | '422': | ||
1606 | description: video unreadable | ||
1607 | delete: | ||
1608 | summary: Cancel the resumable upload of a video, deleting any data uploaded so far | ||
1609 | description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to cancel the upload of a video | ||
1610 | operationId: uploadResumableCancel | ||
1611 | security: | ||
1612 | - OAuth2: [] | ||
1613 | tags: | ||
1614 | - Video | ||
1615 | - Video Upload | ||
1616 | parameters: | ||
1617 | - name: upload_id | ||
1618 | in: path | ||
1619 | required: true | ||
1620 | description: | | ||
1621 | Created session id to proceed with. If you didn't send chunks in the last 12 hours, it is | ||
1622 | not valid anymore and the upload session has already been deleted with its data ;-) | ||
1623 | schema: | ||
1624 | type: string | ||
1625 | - name: Content-Length | ||
1626 | in: header | ||
1627 | required: true | ||
1628 | schema: | ||
1629 | type: number | ||
1630 | example: 0 | ||
1631 | responses: | ||
1632 | '204': | ||
1633 | description: upload cancelled | ||
1634 | headers: | ||
1635 | Content-Length: | ||
1636 | schema: | ||
1637 | type: number | ||
1638 | example: 0 | ||
1469 | /videos/imports: | 1639 | /videos/imports: |
1470 | post: | 1640 | post: |
1471 | summary: Import a video | 1641 | summary: Import a video |
1472 | description: Import a torrent or magnetURI or HTTP resource (if enabled by the instance administrator) | 1642 | description: Import a torrent or magnetURI or HTTP resource (if enabled by the instance administrator) |
1643 | operationId: importVideo | ||
1473 | security: | 1644 | security: |
1474 | - OAuth2: [] | 1645 | - OAuth2: [] |
1475 | tags: | 1646 | tags: |
1476 | - Video | 1647 | - Video |
1648 | - Video Upload | ||
1477 | requestBody: | 1649 | requestBody: |
1478 | content: | 1650 | content: |
1479 | multipart/form-data: | 1651 | multipart/form-data: |
@@ -1504,16 +1676,11 @@ paths: | |||
1504 | privacy: | 1676 | privacy: |
1505 | $ref: '#/components/schemas/VideoPrivacySet' | 1677 | $ref: '#/components/schemas/VideoPrivacySet' |
1506 | category: | 1678 | category: |
1507 | description: Video category | 1679 | $ref: '#/components/schemas/VideoCategorySet' |
1508 | type: integer | ||
1509 | example: 4 | ||
1510 | licence: | 1680 | licence: |
1511 | description: Video licence | 1681 | $ref: '#/components/schemas/VideoLicenceSet' |
1512 | type: integer | ||
1513 | example: 2 | ||
1514 | language: | 1682 | language: |
1515 | description: Video language | 1683 | $ref: '#/components/schemas/VideoLanguageSet' |
1516 | type: string | ||
1517 | description: | 1684 | description: |
1518 | description: Video description | 1685 | description: Video description |
1519 | type: string | 1686 | type: string |
@@ -1576,6 +1743,7 @@ paths: | |||
1576 | /videos/live: | 1743 | /videos/live: |
1577 | post: | 1744 | post: |
1578 | summary: Create a live | 1745 | summary: Create a live |
1746 | operationId: createLive | ||
1579 | security: | 1747 | security: |
1580 | - OAuth2: [] | 1748 | - OAuth2: [] |
1581 | tags: | 1749 | tags: |
@@ -1615,14 +1783,11 @@ paths: | |||
1615 | privacy: | 1783 | privacy: |
1616 | $ref: '#/components/schemas/VideoPrivacySet' | 1784 | $ref: '#/components/schemas/VideoPrivacySet' |
1617 | category: | 1785 | category: |
1618 | description: Live video/replay category | 1786 | $ref: '#/components/schemas/VideoCategorySet' |
1619 | type: string | ||
1620 | licence: | 1787 | licence: |
1621 | description: Live video/replay licence | 1788 | $ref: '#/components/schemas/VideoLicenceSet' |
1622 | type: string | ||
1623 | language: | 1789 | language: |
1624 | description: Live video/replay language | 1790 | $ref: '#/components/schemas/VideoLanguageSet' |
1625 | type: string | ||
1626 | description: | 1791 | description: |
1627 | description: Live video/replay description | 1792 | description: Live video/replay description |
1628 | type: string | 1793 | type: string |
@@ -1664,7 +1829,8 @@ paths: | |||
1664 | 1829 | ||
1665 | /videos/live/{id}: | 1830 | /videos/live/{id}: |
1666 | get: | 1831 | get: |
1667 | summary: Get a live information | 1832 | summary: Get information about a live |
1833 | operationId: getLiveId | ||
1668 | security: | 1834 | security: |
1669 | - OAuth2: [] | 1835 | - OAuth2: [] |
1670 | tags: | 1836 | tags: |
@@ -1680,7 +1846,8 @@ paths: | |||
1680 | schema: | 1846 | schema: |
1681 | $ref: '#/components/schemas/LiveVideoResponse' | 1847 | $ref: '#/components/schemas/LiveVideoResponse' |
1682 | put: | 1848 | put: |
1683 | summary: Update a live information | 1849 | summary: Update information about a live |
1850 | operationId: updateLiveId | ||
1684 | security: | 1851 | security: |
1685 | - OAuth2: [] | 1852 | - OAuth2: [] |
1686 | tags: | 1853 | tags: |
@@ -1704,6 +1871,7 @@ paths: | |||
1704 | /users/me/abuses: | 1871 | /users/me/abuses: |
1705 | get: | 1872 | get: |
1706 | summary: List my abuses | 1873 | summary: List my abuses |
1874 | operationId: getMyAbuses | ||
1707 | security: | 1875 | security: |
1708 | - OAuth2: [] | 1876 | - OAuth2: [] |
1709 | tags: | 1877 | tags: |
@@ -1719,22 +1887,29 @@ paths: | |||
1719 | in: query | 1887 | in: query |
1720 | schema: | 1888 | schema: |
1721 | $ref: '#/components/schemas/AbuseStateSet' | 1889 | $ref: '#/components/schemas/AbuseStateSet' |
1890 | - $ref: '#/components/parameters/abusesSort' | ||
1722 | - $ref: '#/components/parameters/start' | 1891 | - $ref: '#/components/parameters/start' |
1723 | - $ref: '#/components/parameters/count' | 1892 | - $ref: '#/components/parameters/count' |
1724 | - $ref: '#/components/parameters/abusesSort' | ||
1725 | responses: | 1893 | responses: |
1726 | '200': | 1894 | '200': |
1727 | description: successful operation | 1895 | description: successful operation |
1728 | content: | 1896 | content: |
1729 | application/json: | 1897 | application/json: |
1730 | schema: | 1898 | schema: |
1731 | type: array | 1899 | type: object |
1732 | items: | 1900 | properties: |
1733 | $ref: '#/components/schemas/Abuse' | 1901 | total: |
1902 | type: integer | ||
1903 | example: 1 | ||
1904 | data: | ||
1905 | type: array | ||
1906 | items: | ||
1907 | $ref: '#/components/schemas/Abuse' | ||
1734 | 1908 | ||
1735 | /abuses: | 1909 | /abuses: |
1736 | get: | 1910 | get: |
1737 | summary: List abuses | 1911 | summary: List abuses |
1912 | operationId: getAbuses | ||
1738 | security: | 1913 | security: |
1739 | - OAuth2: | 1914 | - OAuth2: |
1740 | - admin | 1915 | - admin |
@@ -1807,9 +1982,15 @@ paths: | |||
1807 | content: | 1982 | content: |
1808 | application/json: | 1983 | application/json: |
1809 | schema: | 1984 | schema: |
1810 | type: array | 1985 | type: object |
1811 | items: | 1986 | properties: |
1812 | $ref: '#/components/schemas/Abuse' | 1987 | total: |
1988 | type: integer | ||
1989 | example: 1 | ||
1990 | data: | ||
1991 | type: array | ||
1992 | items: | ||
1993 | $ref: '#/components/schemas/Abuse' | ||
1813 | 1994 | ||
1814 | post: | 1995 | post: |
1815 | summary: Report an abuse | 1996 | summary: Report an abuse |
@@ -2124,15 +2305,7 @@ paths: | |||
2124 | content: | 2305 | content: |
2125 | application/json: | 2306 | application/json: |
2126 | schema: | 2307 | schema: |
2127 | type: object | 2308 | $ref: '#/components/schemas/VideoChannelList' |
2128 | properties: | ||
2129 | total: | ||
2130 | type: integer | ||
2131 | example: 1 | ||
2132 | data: | ||
2133 | type: array | ||
2134 | items: | ||
2135 | $ref: '#/components/schemas/VideoChannel' | ||
2136 | post: | 2309 | post: |
2137 | summary: Create a video channel | 2310 | summary: Create a video channel |
2138 | security: | 2311 | security: |
@@ -2324,7 +2497,8 @@ paths: | |||
2324 | 2497 | ||
2325 | /video-playlists/privacies: | 2498 | /video-playlists/privacies: |
2326 | get: | 2499 | get: |
2327 | summary: List available playlist privacies | 2500 | summary: List available playlist privacy policies |
2501 | operationId: getPlaylistPrivacyPolicies | ||
2328 | tags: | 2502 | tags: |
2329 | - Video Playlists | 2503 | - Video Playlists |
2330 | responses: | 2504 | responses: |
@@ -2343,6 +2517,7 @@ paths: | |||
2343 | /video-playlists: | 2517 | /video-playlists: |
2344 | get: | 2518 | get: |
2345 | summary: List video playlists | 2519 | summary: List video playlists |
2520 | operationId: getPlaylists | ||
2346 | tags: | 2521 | tags: |
2347 | - Video Playlists | 2522 | - Video Playlists |
2348 | parameters: | 2523 | parameters: |
@@ -2367,6 +2542,7 @@ paths: | |||
2367 | post: | 2542 | post: |
2368 | summary: Create a video playlist | 2543 | summary: Create a video playlist |
2369 | description: 'If the video playlist is set as public, the videoChannelId is mandatory.' | 2544 | description: 'If the video playlist is set as public, the videoChannelId is mandatory.' |
2545 | operationId: createPlaylist | ||
2370 | security: | 2546 | security: |
2371 | - OAuth2: [] | 2547 | - OAuth2: [] |
2372 | tags: | 2548 | tags: |
@@ -2666,14 +2842,7 @@ paths: | |||
2666 | content: | 2842 | content: |
2667 | application/json: | 2843 | application/json: |
2668 | schema: | 2844 | schema: |
2669 | properties: | 2845 | $ref: '#/components/schemas/VideoChannelList' |
2670 | total: | ||
2671 | type: integer | ||
2672 | example: 1 | ||
2673 | data: | ||
2674 | type: array | ||
2675 | items: | ||
2676 | $ref: '#/components/schemas/VideoChannel' | ||
2677 | '/accounts/{name}/ratings': | 2846 | '/accounts/{name}/ratings': |
2678 | get: | 2847 | get: |
2679 | summary: List ratings of an account | 2848 | summary: List ratings of an account |
@@ -2931,9 +3100,7 @@ paths: | |||
2931 | content: | 3100 | content: |
2932 | application/json: | 3101 | application/json: |
2933 | schema: | 3102 | schema: |
2934 | type: array | 3103 | $ref: '#/components/schemas/VideoChannelList' |
2935 | items: | ||
2936 | $ref: '#/components/schemas/VideoChannel' | ||
2937 | '500': | 3104 | '500': |
2938 | description: search index unavailable | 3105 | description: search index unavailable |
2939 | /blocklist/accounts: | 3106 | /blocklist/accounts: |
@@ -3168,13 +3335,6 @@ paths: | |||
3168 | tags: | 3335 | tags: |
3169 | - Feeds | 3336 | - Feeds |
3170 | summary: List comments on videos | 3337 | summary: List comments on videos |
3171 | servers: | ||
3172 | - url: 'https://peertube2.cpy.re' | ||
3173 | description: Live Test Server (live data - latest nightly version) | ||
3174 | - url: 'https://peertube3.cpy.re' | ||
3175 | description: Live Test Server (live data - latest RC version) | ||
3176 | - url: 'https://peertube.cpy.re' | ||
3177 | description: Live Test Server (live data - stable version) | ||
3178 | parameters: | 3338 | parameters: |
3179 | - name: format | 3339 | - name: format |
3180 | in: path | 3340 | in: path |
@@ -3227,18 +3387,33 @@ paths: | |||
3227 | application/xml: | 3387 | application/xml: |
3228 | schema: | 3388 | schema: |
3229 | $ref: '#/components/schemas/VideoCommentsForXML' | 3389 | $ref: '#/components/schemas/VideoCommentsForXML' |
3390 | examples: | ||
3391 | nightly: | ||
3392 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
3230 | application/rss+xml: | 3393 | application/rss+xml: |
3231 | schema: | 3394 | schema: |
3232 | $ref: '#/components/schemas/VideoCommentsForXML' | 3395 | $ref: '#/components/schemas/VideoCommentsForXML' |
3396 | examples: | ||
3397 | nightly: | ||
3398 | externalValue: https://peertube2.cpy.re/feeds/video-comments.rss?filter=local | ||
3233 | text/xml: | 3399 | text/xml: |
3234 | schema: | 3400 | schema: |
3235 | $ref: '#/components/schemas/VideoCommentsForXML' | 3401 | $ref: '#/components/schemas/VideoCommentsForXML' |
3402 | examples: | ||
3403 | nightly: | ||
3404 | externalValue: https://peertube2.cpy.re/feeds/video-comments.xml?filter=local | ||
3236 | application/atom+xml: | 3405 | application/atom+xml: |
3237 | schema: | 3406 | schema: |
3238 | $ref: '#/components/schemas/VideoCommentsForXML' | 3407 | $ref: '#/components/schemas/VideoCommentsForXML' |
3408 | examples: | ||
3409 | nightly: | ||
3410 | externalValue: https://peertube2.cpy.re/feeds/video-comments.atom?filter=local | ||
3239 | application/json: | 3411 | application/json: |
3240 | schema: | 3412 | schema: |
3241 | type: object | 3413 | type: object |
3414 | examples: | ||
3415 | nightly: | ||
3416 | externalValue: https://peertube2.cpy.re/feeds/video-comments.json?filter=local | ||
3242 | '400': | 3417 | '400': |
3243 | x-summary: field inconsistencies | 3418 | x-summary: field inconsistencies |
3244 | description: > | 3419 | description: > |
@@ -3253,13 +3428,6 @@ paths: | |||
3253 | tags: | 3428 | tags: |
3254 | - Feeds | 3429 | - Feeds |
3255 | summary: List videos | 3430 | summary: List videos |
3256 | servers: | ||
3257 | - url: 'https://peertube2.cpy.re' | ||
3258 | description: Live Test Server (live data - latest nightly version) | ||
3259 | - url: 'https://peertube3.cpy.re' | ||
3260 | description: Live Test Server (live data - latest RC version) | ||
3261 | - url: 'https://peertube.cpy.re' | ||
3262 | description: Live Test Server (live data - stable version) | ||
3263 | parameters: | 3431 | parameters: |
3264 | - name: format | 3432 | - name: format |
3265 | in: path | 3433 | in: path |
@@ -3316,19 +3484,93 @@ paths: | |||
3316 | application/rss+xml: | 3484 | application/rss+xml: |
3317 | schema: | 3485 | schema: |
3318 | $ref: '#/components/schemas/VideosForXML' | 3486 | $ref: '#/components/schemas/VideosForXML' |
3487 | examples: | ||
3488 | nightly: | ||
3489 | externalValue: https://peertube2.cpy.re/feeds/videos.rss?filter=local | ||
3319 | text/xml: | 3490 | text/xml: |
3320 | schema: | 3491 | schema: |
3321 | $ref: '#/components/schemas/VideosForXML' | 3492 | $ref: '#/components/schemas/VideosForXML' |
3493 | examples: | ||
3494 | nightly: | ||
3495 | externalValue: https://peertube2.cpy.re/feeds/videos.xml?filter=local | ||
3322 | application/atom+xml: | 3496 | application/atom+xml: |
3323 | schema: | 3497 | schema: |
3324 | $ref: '#/components/schemas/VideosForXML' | 3498 | $ref: '#/components/schemas/VideosForXML' |
3499 | examples: | ||
3500 | nightly: | ||
3501 | externalValue: https://peertube2.cpy.re/feeds/videos.atom?filter=local | ||
3325 | application/json: | 3502 | application/json: |
3326 | schema: | 3503 | schema: |
3327 | type: object | 3504 | type: object |
3505 | examples: | ||
3506 | nightly: | ||
3507 | externalValue: https://peertube2.cpy.re/feeds/videos.json?filter=local | ||
3328 | '404': | 3508 | '404': |
3329 | description: video channel or account not found | 3509 | description: video channel or account not found |
3330 | '406': | 3510 | '406': |
3331 | description: accept header unsupported | 3511 | description: accept header unsupported |
3512 | '/feeds/subscriptions.{format}': | ||
3513 | get: | ||
3514 | tags: | ||
3515 | - Feeds | ||
3516 | - Account | ||
3517 | summary: List videos of subscriptions tied to a token | ||
3518 | parameters: | ||
3519 | - name: format | ||
3520 | in: path | ||
3521 | required: true | ||
3522 | description: 'format expected (we focus on making `rss` the most featureful ; it serves [Media RSS](https://www.rssboard.org/media-rss))' | ||
3523 | schema: | ||
3524 | type: string | ||
3525 | enum: | ||
3526 | - xml | ||
3527 | - rss | ||
3528 | - rss2 | ||
3529 | - atom | ||
3530 | - atom1 | ||
3531 | - json | ||
3532 | - json1 | ||
3533 | - name: accountId | ||
3534 | in: query | ||
3535 | description: limit listing to a specific account | ||
3536 | schema: | ||
3537 | type: string | ||
3538 | required: true | ||
3539 | - name: token | ||
3540 | in: query | ||
3541 | description: private token allowing access | ||
3542 | schema: | ||
3543 | type: string | ||
3544 | required: true | ||
3545 | - $ref: '#/components/parameters/sort' | ||
3546 | - $ref: '#/components/parameters/nsfw' | ||
3547 | - $ref: '#/components/parameters/filter' | ||
3548 | responses: | ||
3549 | '204': | ||
3550 | description: successful operation | ||
3551 | headers: | ||
3552 | Cache-Control: | ||
3553 | schema: | ||
3554 | type: string | ||
3555 | default: 'max-age=900' # 15 min cache | ||
3556 | content: | ||
3557 | application/xml: | ||
3558 | schema: | ||
3559 | $ref: '#/components/schemas/VideosForXML' | ||
3560 | application/rss+xml: | ||
3561 | schema: | ||
3562 | $ref: '#/components/schemas/VideosForXML' | ||
3563 | text/xml: | ||
3564 | schema: | ||
3565 | $ref: '#/components/schemas/VideosForXML' | ||
3566 | application/atom+xml: | ||
3567 | schema: | ||
3568 | $ref: '#/components/schemas/VideosForXML' | ||
3569 | application/json: | ||
3570 | schema: | ||
3571 | type: object | ||
3572 | '406': | ||
3573 | description: accept header unsupported | ||
3332 | /plugins: | 3574 | /plugins: |
3333 | get: | 3575 | get: |
3334 | tags: | 3576 | tags: |
@@ -3823,7 +4065,7 @@ components: | |||
3823 | name: categoryOneOf | 4065 | name: categoryOneOf |
3824 | in: query | 4066 | in: query |
3825 | required: false | 4067 | required: false |
3826 | description: category id of the video (see [/videos/categories](#tag/Video/paths/~1videos~1categories/get)) | 4068 | description: category id of the video (see [/videos/categories](#operation/getCategories)) |
3827 | schema: | 4069 | schema: |
3828 | oneOf: | 4070 | oneOf: |
3829 | - type: integer | 4071 | - type: integer |
@@ -3841,6 +4083,7 @@ components: | |||
3841 | oneOf: | 4083 | oneOf: |
3842 | - type: string | 4084 | - type: string |
3843 | - type: array | 4085 | - type: array |
4086 | maxItems: 5 | ||
3844 | items: | 4087 | items: |
3845 | type: string | 4088 | type: string |
3846 | style: form | 4089 | style: form |
@@ -3862,7 +4105,7 @@ components: | |||
3862 | name: languageOneOf | 4105 | name: languageOneOf |
3863 | in: query | 4106 | in: query |
3864 | required: false | 4107 | required: false |
3865 | description: language id of the video (see [/videos/languages](#tag/Video/paths/~1videos~1languages/get)). Use `_unknown` to filter on videos that don't have a video language | 4108 | description: language id of the video (see [/videos/languages](#operation/getLanguages)). Use `_unknown` to filter on videos that don't have a video language |
3866 | schema: | 4109 | schema: |
3867 | oneOf: | 4110 | oneOf: |
3868 | - type: string | 4111 | - type: string |
@@ -3875,7 +4118,7 @@ components: | |||
3875 | name: licenceOneOf | 4118 | name: licenceOneOf |
3876 | in: query | 4119 | in: query |
3877 | required: false | 4120 | required: false |
3878 | description: licence id of the video (see [/videos/licences](#tag/Video/paths/~1videos~1licences/get)) | 4121 | description: licence id of the video (see [/videos/licences](#operation/getLicences)) |
3879 | schema: | 4122 | schema: |
3880 | oneOf: | 4123 | oneOf: |
3881 | - type: integer | 4124 | - type: integer |
@@ -3959,19 +4202,16 @@ components: | |||
3959 | - video-live-ending | 4202 | - video-live-ending |
3960 | securitySchemes: | 4203 | securitySchemes: |
3961 | OAuth2: | 4204 | OAuth2: |
3962 | description: > | 4205 | description: | |
3963 | In the header: *Authorization: Bearer <token\>* | ||
3964 | |||
3965 | |||
3966 | Authenticating via OAuth requires the following steps: | 4206 | Authenticating via OAuth requires the following steps: |
3967 | 4207 | - Have an activated account | |
3968 | |||
3969 | - Have an account with sufficient authorization levels | ||
3970 | |||
3971 | - [Generate](https://docs.joinpeertube.org/api-rest-getting-started) a | 4208 | - [Generate](https://docs.joinpeertube.org/api-rest-getting-started) a |
3972 | Bearer Token | 4209 | Bearer Token for that account at `/api/v1/users/token` |
4210 | - Make authenticated requests, putting *Authorization: Bearer <token\>* | ||
4211 | - Profit, depending on the role assigned to the account | ||
3973 | 4212 | ||
3974 | - Make Authenticated Requests | 4213 | Note that the __access token is valid for 1 day__ and, and is given |
4214 | along with a __refresh token valid for 2 weeks__. | ||
3975 | type: oauth2 | 4215 | type: oauth2 |
3976 | flows: | 4216 | flows: |
3977 | password: | 4217 | password: |
@@ -3991,25 +4231,33 @@ components: | |||
3991 | minLength: 36 | 4231 | minLength: 36 |
3992 | maxLength: 36 | 4232 | maxLength: 36 |
3993 | 4233 | ||
4234 | VideoCategorySet: | ||
4235 | type: integer | ||
4236 | description: category id of the video (see [/videos/categories](#operation/getCategories)) | ||
3994 | VideoConstantNumber-Category: | 4237 | VideoConstantNumber-Category: |
3995 | properties: | 4238 | properties: |
3996 | id: | 4239 | id: |
3997 | type: integer | 4240 | $ref: '#/components/schemas/VideoCategorySet' |
3998 | description: category id of the video (see [/videos/categories](#tag/Video/paths/~1videos~1categories/get)) | ||
3999 | label: | 4241 | label: |
4000 | type: string | 4242 | type: string |
4243 | |||
4244 | VideoLicenceSet: | ||
4245 | type: integer | ||
4246 | description: licence id of the video (see [/videos/licences](#operation/getLicences)) | ||
4001 | VideoConstantNumber-Licence: | 4247 | VideoConstantNumber-Licence: |
4002 | properties: | 4248 | properties: |
4003 | id: | 4249 | id: |
4004 | type: integer | 4250 | $ref: '#/components/schemas/VideoLicenceSet' |
4005 | description: licence id of the video (see [/videos/licences](#tag/Video/paths/~1videos~1licences/get)) | ||
4006 | label: | 4251 | label: |
4007 | type: string | 4252 | type: string |
4253 | |||
4254 | VideoLanguageSet: | ||
4255 | type: string | ||
4256 | description: language id of the video (see [/videos/languages](#operation/getLanguages)) | ||
4008 | VideoConstantString-Language: | 4257 | VideoConstantString-Language: |
4009 | properties: | 4258 | properties: |
4010 | id: | 4259 | id: |
4011 | type: string | 4260 | $ref: '#/components/schemas/VideoLanguageSet' |
4012 | description: language id of the video (see [/videos/languages](#tag/Video/paths/~1videos~1languages/get)) | ||
4013 | label: | 4261 | label: |
4014 | type: string | 4262 | type: string |
4015 | 4263 | ||
@@ -4019,7 +4267,7 @@ components: | |||
4019 | - 1 | 4267 | - 1 |
4020 | - 2 | 4268 | - 2 |
4021 | - 3 | 4269 | - 3 |
4022 | description: 'The video playlist privacy (Public = `1`, Unlisted = `2`, Private = `3`)' | 4270 | description: Video playlist privacy policy (see [/video-playlists/privacies]) |
4023 | VideoPlaylistPrivacyConstant: | 4271 | VideoPlaylistPrivacyConstant: |
4024 | properties: | 4272 | properties: |
4025 | id: | 4273 | id: |
@@ -4032,7 +4280,7 @@ components: | |||
4032 | enum: | 4280 | enum: |
4033 | - 1 | 4281 | - 1 |
4034 | - 2 | 4282 | - 2 |
4035 | description: 'The video playlist type (Regular = `1`, Watch Later = `2`)' | 4283 | description: The video playlist type (Regular = `1`, Watch Later = `2`) |
4036 | VideoPlaylistTypeConstant: | 4284 | VideoPlaylistTypeConstant: |
4037 | properties: | 4285 | properties: |
4038 | id: | 4286 | id: |
@@ -4047,7 +4295,7 @@ components: | |||
4047 | - 2 | 4295 | - 2 |
4048 | - 3 | 4296 | - 3 |
4049 | - 4 | 4297 | - 4 |
4050 | description: 'The video privacy (Public = `1`, Unlisted = `2`, Private = `3`, Internal = `4`)' | 4298 | description: privacy id of the video (see [/videos/privacies](#operation/getPrivacyPolicies)) |
4051 | VideoPrivacyConstant: | 4299 | VideoPrivacyConstant: |
4052 | properties: | 4300 | properties: |
4053 | id: | 4301 | id: |
@@ -4118,12 +4366,17 @@ components: | |||
4118 | - captions | 4366 | - captions |
4119 | example: [spamOrMisleading] | 4367 | example: [spamOrMisleading] |
4120 | 4368 | ||
4369 | VideoResolutionSet: | ||
4370 | type: integer | ||
4371 | description: | | ||
4372 | Video resolution (`0`, `240`, `360`, `720`, `1080`, `1440` or `2160`) | ||
4373 | |||
4374 | `0` is used as a special value for stillimage videos dedicated to audio, a.k.a. audio-only videos. | ||
4375 | example: 240 | ||
4121 | VideoResolutionConstant: | 4376 | VideoResolutionConstant: |
4122 | properties: | 4377 | properties: |
4123 | id: | 4378 | id: |
4124 | type: integer | 4379 | $ref: '#/components/schemas/VideoResolutionSet' |
4125 | description: 'Video resolution (240, 360, 720, 1080, 1440 or 2160)' | ||
4126 | example: 240 | ||
4127 | label: | 4380 | label: |
4128 | type: string | 4381 | type: string |
4129 | example: 240p | 4382 | example: 240p |
@@ -4507,6 +4760,16 @@ components: | |||
4507 | format: date-time | 4760 | format: date-time |
4508 | video: | 4761 | video: |
4509 | $ref: '#/components/schemas/Video' | 4762 | $ref: '#/components/schemas/Video' |
4763 | VideoImportsList: | ||
4764 | properties: | ||
4765 | total: | ||
4766 | type: integer | ||
4767 | example: 1 | ||
4768 | data: | ||
4769 | type: array | ||
4770 | maxItems: 100 | ||
4771 | items: | ||
4772 | $ref: '#/components/schemas/VideoImport' | ||
4510 | Abuse: | 4773 | Abuse: |
4511 | properties: | 4774 | properties: |
4512 | id: | 4775 | id: |
@@ -4540,7 +4803,7 @@ components: | |||
4540 | message: | 4803 | message: |
4541 | type: string | 4804 | type: string |
4542 | minLength: 2 | 4805 | minLength: 2 |
4543 | maxLength: 3000 | 4806 | maxLength: 3000 |
4544 | byModerator: | 4807 | byModerator: |
4545 | type: boolean | 4808 | type: boolean |
4546 | createdAt: | 4809 | createdAt: |
@@ -4717,6 +4980,8 @@ components: | |||
4717 | host: | 4980 | host: |
4718 | type: string | 4981 | type: string |
4719 | format: hostname | 4982 | format: hostname |
4983 | hostRedundancyAllowed: | ||
4984 | type: boolean | ||
4720 | followingCount: | 4985 | followingCount: |
4721 | type: integer | 4986 | type: integer |
4722 | followersCount: | 4987 | followersCount: |
@@ -4831,7 +5096,7 @@ components: | |||
4831 | enabledResolutions: | 5096 | enabledResolutions: |
4832 | type: array | 5097 | type: array |
4833 | items: | 5098 | items: |
4834 | type: integer | 5099 | $ref: '#/components/schemas/VideoResolutionSet' |
4835 | import: | 5100 | import: |
4836 | type: object | 5101 | type: object |
4837 | properties: | 5102 | properties: |
@@ -5032,6 +5297,7 @@ components: | |||
5032 | type: boolean | 5297 | type: boolean |
5033 | user: | 5298 | user: |
5034 | type: object | 5299 | type: object |
5300 | description: Settings that apply to new users, if registration is enabled | ||
5035 | properties: | 5301 | properties: |
5036 | videoQuota: | 5302 | videoQuota: |
5037 | type: integer | 5303 | type: integer |
@@ -5039,18 +5305,34 @@ components: | |||
5039 | type: integer | 5305 | type: integer |
5040 | transcoding: | 5306 | transcoding: |
5041 | type: object | 5307 | type: object |
5308 | description: Settings pertaining to transcoding jobs | ||
5042 | properties: | 5309 | properties: |
5043 | enabled: | 5310 | enabled: |
5044 | type: boolean | 5311 | type: boolean |
5045 | allowAdditionalExtensions: | 5312 | allowAdditionalExtensions: |
5046 | type: boolean | 5313 | type: boolean |
5314 | description: Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos | ||
5047 | allowAudioFiles: | 5315 | allowAudioFiles: |
5048 | type: boolean | 5316 | type: boolean |
5317 | description: If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file | ||
5049 | threads: | 5318 | threads: |
5050 | type: integer | 5319 | type: integer |
5320 | description: Amount of threads used by ffmpeg for 1 transcoding job | ||
5321 | concurrency: | ||
5322 | type: number | ||
5323 | description: Amount of transcoding jobs to execute in parallel | ||
5324 | profile: | ||
5325 | type: string | ||
5326 | enum: | ||
5327 | - default | ||
5328 | description: | | ||
5329 | New profiles can be added by plugins ; available in core PeerTube: 'default'. | ||
5051 | resolutions: | 5330 | resolutions: |
5052 | type: object | 5331 | type: object |
5332 | description: Resolutions to transcode _new videos_ to | ||
5053 | properties: | 5333 | properties: |
5334 | 0p: | ||
5335 | type: boolean | ||
5054 | 240p: | 5336 | 240p: |
5055 | type: boolean | 5337 | type: boolean |
5056 | 360p: | 5338 | 360p: |
@@ -5065,8 +5347,15 @@ components: | |||
5065 | type: boolean | 5347 | type: boolean |
5066 | 2160p: | 5348 | 2160p: |
5067 | type: boolean | 5349 | type: boolean |
5350 | webtorrent: | ||
5351 | type: object | ||
5352 | description: WebTorrent-specific settings | ||
5353 | properties: | ||
5354 | enabled: | ||
5355 | type: boolean | ||
5068 | hls: | 5356 | hls: |
5069 | type: object | 5357 | type: object |
5358 | description: HLS-specific settings | ||
5070 | properties: | 5359 | properties: |
5071 | enabled: | 5360 | enabled: |
5072 | type: boolean | 5361 | type: boolean |
@@ -5133,6 +5422,7 @@ components: | |||
5133 | PredefinedAbuseReasons: | 5422 | PredefinedAbuseReasons: |
5134 | description: Reason categories that help triage reports | 5423 | description: Reason categories that help triage reports |
5135 | type: array | 5424 | type: array |
5425 | maxItems: 8 | ||
5136 | items: | 5426 | items: |
5137 | type: string | 5427 | type: string |
5138 | enum: | 5428 | enum: |
@@ -5202,6 +5492,98 @@ components: | |||
5202 | id: | 5492 | id: |
5203 | type: integer | 5493 | type: integer |
5204 | example: 37 | 5494 | example: 37 |
5495 | VideoUploadRequestCommon: | ||
5496 | properties: | ||
5497 | name: | ||
5498 | description: Video name | ||
5499 | type: string | ||
5500 | channelId: | ||
5501 | description: Channel id that will contain this video | ||
5502 | type: integer | ||
5503 | privacy: | ||
5504 | $ref: '#/components/schemas/VideoPrivacySet' | ||
5505 | category: | ||
5506 | $ref: '#/components/schemas/VideoCategorySet' | ||
5507 | licence: | ||
5508 | $ref: '#/components/schemas/VideoLicenceSet' | ||
5509 | language: | ||
5510 | $ref: '#/components/schemas/VideoLanguageSet' | ||
5511 | description: | ||
5512 | description: Video description | ||
5513 | type: string | ||
5514 | waitTranscoding: | ||
5515 | description: Whether or not we wait transcoding before publish the video | ||
5516 | type: boolean | ||
5517 | support: | ||
5518 | description: A text tell the audience how to support the video creator | ||
5519 | example: Please support my work on <insert crowdfunding plateform>! <3 | ||
5520 | type: string | ||
5521 | nsfw: | ||
5522 | description: Whether or not this video contains sensitive content | ||
5523 | type: boolean | ||
5524 | tags: | ||
5525 | description: Video tags (maximum 5 tags each between 2 and 30 characters) | ||
5526 | type: array | ||
5527 | minItems: 1 | ||
5528 | maxItems: 5 | ||
5529 | uniqueItems: true | ||
5530 | items: | ||
5531 | type: string | ||
5532 | minLength: 2 | ||
5533 | maxLength: 30 | ||
5534 | commentsEnabled: | ||
5535 | description: Enable or disable comments for this video | ||
5536 | type: boolean | ||
5537 | downloadEnabled: | ||
5538 | description: Enable or disable downloading for this video | ||
5539 | type: boolean | ||
5540 | originallyPublishedAt: | ||
5541 | description: Date when the content was originally published | ||
5542 | type: string | ||
5543 | format: date-time | ||
5544 | scheduleUpdate: | ||
5545 | $ref: '#/components/schemas/VideoScheduledUpdate' | ||
5546 | thumbnailfile: | ||
5547 | description: Video thumbnail file | ||
5548 | type: string | ||
5549 | format: binary | ||
5550 | previewfile: | ||
5551 | description: Video preview file | ||
5552 | type: string | ||
5553 | format: binary | ||
5554 | required: | ||
5555 | - channelId | ||
5556 | - name | ||
5557 | VideoUploadRequestLegacy: | ||
5558 | allOf: | ||
5559 | - $ref: '#/components/schemas/VideoUploadRequestCommon' | ||
5560 | - type: object | ||
5561 | required: | ||
5562 | - videofile | ||
5563 | properties: | ||
5564 | videofile: | ||
5565 | description: Video file | ||
5566 | type: string | ||
5567 | format: binary | ||
5568 | VideoUploadRequestResumable: | ||
5569 | allOf: | ||
5570 | - $ref: '#/components/schemas/VideoUploadRequestCommon' | ||
5571 | - type: object | ||
5572 | required: | ||
5573 | - filename | ||
5574 | properties: | ||
5575 | filename: | ||
5576 | description: Video filename including extension | ||
5577 | type: string | ||
5578 | format: filename | ||
5579 | thumbnailfile: | ||
5580 | description: Video thumbnail file | ||
5581 | type: string | ||
5582 | format: binary | ||
5583 | previewfile: | ||
5584 | description: Video preview file | ||
5585 | type: string | ||
5586 | format: binary | ||
5205 | VideoUploadResponse: | 5587 | VideoUploadResponse: |
5206 | properties: | 5588 | properties: |
5207 | video: | 5589 | video: |
@@ -5238,35 +5620,45 @@ components: | |||
5238 | $ref: '#/components/schemas/Video' | 5620 | $ref: '#/components/schemas/Video' |
5239 | User: | 5621 | User: |
5240 | properties: | 5622 | properties: |
5241 | id: | 5623 | account: |
5242 | type: integer | 5624 | $ref: '#/components/schemas/Account' |
5243 | readOnly: true | 5625 | autoPlayNextVideo: |
5244 | username: | 5626 | type: boolean |
5627 | description: Automatically start playing the upcoming video after the currently playing video | ||
5628 | autoPlayNextVideoPlaylist: | ||
5629 | type: boolean | ||
5630 | description: Automatically start playing the video on the playlist after the currently playing video | ||
5631 | autoPlayVideo: | ||
5632 | type: boolean | ||
5633 | description: Automatically start playing the video on the watch page | ||
5634 | blocked: | ||
5635 | type: boolean | ||
5636 | blockedReason: | ||
5637 | type: string | ||
5638 | createdAt: | ||
5245 | type: string | 5639 | type: string |
5246 | description: The user username | ||
5247 | minLength: 1 | ||
5248 | maxLength: 50 | ||
5249 | email: | 5640 | email: |
5250 | type: string | 5641 | type: string |
5251 | format: email | 5642 | format: email |
5252 | description: The user email | 5643 | description: The user email |
5644 | emailVerified: | ||
5645 | type: boolean | ||
5646 | description: Has the user confirmed their email address? | ||
5647 | id: | ||
5648 | type: integer | ||
5649 | readOnly: true | ||
5253 | pluginAuth: | 5650 | pluginAuth: |
5254 | type: string | 5651 | type: string |
5255 | description: Auth plugin to use to authenticate the user | 5652 | description: Auth plugin to use to authenticate the user |
5256 | theme: | 5653 | lastLoginDate: |
5257 | type: string | 5654 | type: string |
5258 | description: Theme enabled by this user | 5655 | format: date-time |
5259 | emailVerified: | 5656 | noInstanceConfigWarningModal: |
5657 | type: boolean | ||
5658 | noWelcomeModal: | ||
5260 | type: boolean | 5659 | type: boolean |
5261 | description: Has the user confirmed their email address? | ||
5262 | nsfwPolicy: | 5660 | nsfwPolicy: |
5263 | $ref: '#/components/schemas/NSFWPolicy' | 5661 | $ref: '#/components/schemas/NSFWPolicy' |
5264 | webtorrentEnabled: | ||
5265 | type: boolean | ||
5266 | description: Enable P2P in the player | ||
5267 | autoPlayVideo: | ||
5268 | type: boolean | ||
5269 | description: Automatically start playing the video on the watch page | ||
5270 | role: | 5662 | role: |
5271 | $ref: '#/components/schemas/UserRole' | 5663 | $ref: '#/components/schemas/UserRole' |
5272 | roleLabel: | 5664 | roleLabel: |
@@ -5275,38 +5667,49 @@ components: | |||
5275 | - User | 5667 | - User |
5276 | - Moderator | 5668 | - Moderator |
5277 | - Administrator | 5669 | - Administrator |
5278 | videoQuota: | 5670 | theme: |
5279 | type: integer | ||
5280 | description: The user video quota | ||
5281 | videoQuotaDaily: | ||
5282 | type: integer | ||
5283 | description: The user daily video quota | ||
5284 | videosCount: | ||
5285 | type: integer | ||
5286 | abusesCount: | ||
5287 | type: integer | ||
5288 | abusesAcceptedCount: | ||
5289 | type: integer | ||
5290 | abusesCreatedCount: | ||
5291 | type: integer | ||
5292 | videoCommentsCount: | ||
5293 | type: integer | ||
5294 | noInstanceConfigWarningModal: | ||
5295 | type: boolean | ||
5296 | noWelcomeModal: | ||
5297 | type: boolean | ||
5298 | blocked: | ||
5299 | type: boolean | ||
5300 | blockedReason: | ||
5301 | type: string | 5671 | type: string |
5302 | createdAt: | 5672 | description: Theme enabled by this user |
5673 | username: | ||
5303 | type: string | 5674 | type: string |
5304 | account: | 5675 | description: The user username |
5305 | $ref: '#/components/schemas/Account' | 5676 | minLength: 1 |
5677 | maxLength: 50 | ||
5306 | videoChannels: | 5678 | videoChannels: |
5307 | type: array | 5679 | type: array |
5308 | items: | 5680 | items: |
5309 | $ref: '#/components/schemas/VideoChannel' | 5681 | $ref: '#/components/schemas/VideoChannel' |
5682 | videoQuota: | ||
5683 | type: integer | ||
5684 | description: The user video quota in bytes | ||
5685 | example: -1 | ||
5686 | videoQuotaDaily: | ||
5687 | type: integer | ||
5688 | description: The user daily video quota in bytes | ||
5689 | example: -1 | ||
5690 | webtorrentEnabled: | ||
5691 | type: boolean | ||
5692 | description: Enable P2P in the player | ||
5693 | UserWithStats: | ||
5694 | allOf: | ||
5695 | - $ref: '#/components/schemas/User' | ||
5696 | - properties: | ||
5697 | # optionally present fields: they require WITH_STATS scope | ||
5698 | videosCount: | ||
5699 | type: integer | ||
5700 | description: Count of videos published | ||
5701 | abusesCount: | ||
5702 | type: integer | ||
5703 | description: Count of reports/abuses of which the user is a target | ||
5704 | abusesAcceptedCount: | ||
5705 | type: integer | ||
5706 | description: Count of reports/abuses created by the user and accepted/acted upon by the moderation team | ||
5707 | abusesCreatedCount: | ||
5708 | type: integer | ||
5709 | description: Count of reports/abuses created by the user | ||
5710 | videoCommentsCount: | ||
5711 | type: integer | ||
5712 | description: Count of comments published | ||
5310 | AddUser: | 5713 | AddUser: |
5311 | properties: | 5714 | properties: |
5312 | username: | 5715 | username: |
@@ -5314,6 +5717,7 @@ components: | |||
5314 | description: The user username | 5717 | description: The user username |
5315 | minLength: 1 | 5718 | minLength: 1 |
5316 | maxLength: 50 | 5719 | maxLength: 50 |
5720 | pattern: '/^[a-z0-9._]{1,50}$/' | ||
5317 | password: | 5721 | password: |
5318 | type: string | 5722 | type: string |
5319 | format: password | 5723 | format: password |
@@ -5333,6 +5737,7 @@ components: | |||
5333 | channelName: | 5737 | channelName: |
5334 | type: string | 5738 | type: string |
5335 | description: The user default channel username | 5739 | description: The user default channel username |
5740 | pattern: '/^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]+$/' | ||
5336 | role: | 5741 | role: |
5337 | $ref: '#/components/schemas/UserRole' | 5742 | $ref: '#/components/schemas/UserRole' |
5338 | adminFlags: | 5743 | adminFlags: |
@@ -5406,7 +5811,11 @@ components: | |||
5406 | type: string | 5811 | type: string |
5407 | description: Id of the video | 5812 | description: Id of the video |
5408 | rating: | 5813 | rating: |
5409 | type: number | 5814 | type: string |
5815 | enum: | ||
5816 | - like | ||
5817 | - dislike | ||
5818 | - none | ||
5410 | description: Rating of the video | 5819 | description: Rating of the video |
5411 | required: | 5820 | required: |
5412 | - id | 5821 | - id |
@@ -5416,8 +5825,12 @@ components: | |||
5416 | video: | 5825 | video: |
5417 | $ref: '#/components/schemas/Video' | 5826 | $ref: '#/components/schemas/Video' |
5418 | rating: | 5827 | rating: |
5419 | type: number | 5828 | type: string |
5420 | description: 'Rating of the video' | 5829 | enum: |
5830 | - like | ||
5831 | - dislike | ||
5832 | - none | ||
5833 | description: Rating of the video | ||
5421 | required: | 5834 | required: |
5422 | - video | 5835 | - video |
5423 | - rating | 5836 | - rating |
@@ -5495,6 +5908,17 @@ components: | |||
5495 | bulkVideosSupportUpdate: | 5908 | bulkVideosSupportUpdate: |
5496 | type: boolean | 5909 | type: boolean |
5497 | description: 'Update the support field for all videos of this channel' | 5910 | description: 'Update the support field for all videos of this channel' |
5911 | VideoChannelList: | ||
5912 | properties: | ||
5913 | total: | ||
5914 | type: integer | ||
5915 | example: 1 | ||
5916 | data: | ||
5917 | type: array | ||
5918 | items: | ||
5919 | allOf: | ||
5920 | - $ref: '#/components/schemas/VideoChannel' | ||
5921 | - $ref: '#/components/schemas/Actor' | ||
5498 | 5922 | ||
5499 | MRSSPeerLink: | 5923 | MRSSPeerLink: |
5500 | type: object | 5924 | type: object |
diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md index 9666d72af..939772a9d 100644 --- a/support/doc/dependencies.md +++ b/support/doc/dependencies.md | |||
@@ -307,7 +307,7 @@ brew services run redis | |||
307 | ``` | 307 | ``` |
308 | 308 | ||
309 | On macOS, the `postgresql` user can be `_postgres` instead of `postgres`. | 309 | On macOS, the `postgresql` user can be `_postgres` instead of `postgres`. |
310 | If `sudo -u postgres createuser -P peertube` gives you an error, you can try `sudo -u _postgres createuser -U peertube`. | 310 | If `sudo -u postgres createuser -P peertube` gives you an `unknown user: postgres` error, you can try `sudo -u _postgres createuser -U peertube`. |
311 | 311 | ||
312 | ## Gentoo | 312 | ## Gentoo |
313 | 313 | ||
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index 53d53c26d..5b7d1cb31 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -261,8 +261,8 @@ function register ({ | |||
261 | router.get('/ping', (req, res) => res.json({ message: 'pong' })) | 261 | router.get('/ping', (req, res) => res.json({ message: 'pong' })) |
262 | 262 | ||
263 | // Users are automatically authenticated | 263 | // Users are automatically authenticated |
264 | router.get('/auth', (res, res) => { | 264 | router.get('/auth', async (res, res) => { |
265 | const user = peertubeHelpers.user.getAuthUser(res) | 265 | const user = await peertubeHelpers.user.getAuthUser(res) |
266 | 266 | ||
267 | const isAdmin = user.role === 0 | 267 | const isAdmin = user.role === 0 |
268 | const isModerator = user.role === 1 | 268 | const isModerator = user.role === 1 |
diff --git a/support/docker/production/Dockerfile.buster b/support/docker/production/Dockerfile.buster index b3822964d..2ff0591f9 100644 --- a/support/docker/production/Dockerfile.buster +++ b/support/docker/production/Dockerfile.buster | |||
@@ -7,7 +7,7 @@ ARG NPM_RUN_BUILD_OPTS | |||
7 | 7 | ||
8 | # Install dependencies | 8 | # Install dependencies |
9 | RUN apt update \ | 9 | RUN apt update \ |
10 | && apt install -y --no-install-recommends openssl ffmpeg python ca-certificates gnupg gosu build-essential \ | 10 | && apt install -y --no-install-recommends openssl ffmpeg python ca-certificates gnupg gosu build-essential curl \ |
11 | && gosu nobody true \ | 11 | && gosu nobody true \ |
12 | && rm /var/lib/apt/lists/* -fR | 12 | && rm /var/lib/apt/lists/* -fR |
13 | 13 | ||
diff --git a/support/nginx/peertube b/support/nginx/peertube index 00ce1d0dc..d03f14613 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube | |||
@@ -78,6 +78,15 @@ server { | |||
78 | try_files /dev/null @api; | 78 | try_files /dev/null @api; |
79 | } | 79 | } |
80 | 80 | ||
81 | location = /api/v1/videos/upload-resumable { | ||
82 | if ($request_method = 'PUT') { | ||
83 | client_max_body_size 0; | ||
84 | proxy_request_buffering off; | ||
85 | } | ||
86 | |||
87 | try_files /dev/null @api; | ||
88 | } | ||
89 | |||
81 | location = /api/v1/videos/upload { | 90 | location = /api/v1/videos/upload { |
82 | limit_except POST HEAD { deny all; } | 91 | limit_except POST HEAD { deny all; } |
83 | 92 | ||
@@ -12,9 +12,9 @@ | |||
12 | js-yaml "^3.13.1" | 12 | js-yaml "^3.13.1" |
13 | 13 | ||
14 | "@apidevtools/openapi-schemas@^2.0.4": | 14 | "@apidevtools/openapi-schemas@^2.0.4": |
15 | version "2.0.4" | 15 | version "2.1.0" |
16 | resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.4.tgz#bae1cef77ebb2b3705c7cc6911281da5153c1ab3" | 16 | resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" |
17 | integrity sha512-ob5c4UiaMYkb24pNhvfSABShAwpREvUGCkqjiz/BX9gKZ32y/S22M+ALIHftTAuv9KsFVSpVdIDzi9ZzFh5TCA== | 17 | integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== |
18 | 18 | ||
19 | "@apidevtools/swagger-cli@4.0.4": | 19 | "@apidevtools/swagger-cli@4.0.4": |
20 | version "4.0.4" | 20 | version "4.0.4" |
@@ -62,39 +62,38 @@ | |||
62 | dependencies: | 62 | dependencies: |
63 | "@babel/highlight" "^7.12.13" | 63 | "@babel/highlight" "^7.12.13" |
64 | 64 | ||
65 | "@babel/helper-validator-identifier@^7.12.11": | 65 | "@babel/helper-validator-identifier@^7.14.0": |
66 | version "7.12.11" | 66 | version "7.14.0" |
67 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" | 67 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" |
68 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== | 68 | integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== |
69 | 69 | ||
70 | "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": | 70 | "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": |
71 | version "7.13.10" | 71 | version "7.14.0" |
72 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" | 72 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" |
73 | integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== | 73 | integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== |
74 | dependencies: | 74 | dependencies: |
75 | "@babel/helper-validator-identifier" "^7.12.11" | 75 | "@babel/helper-validator-identifier" "^7.14.0" |
76 | chalk "^2.0.0" | 76 | chalk "^2.0.0" |
77 | js-tokens "^4.0.0" | 77 | js-tokens "^4.0.0" |
78 | 78 | ||
79 | "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": | 79 | "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": |
80 | version "7.13.15" | 80 | version "7.14.1" |
81 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8" | 81 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.1.tgz#1bd644b5db3f5797c4479d89ec1817fe02b84c47" |
82 | integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ== | 82 | integrity sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q== |
83 | 83 | ||
84 | "@babel/runtime@^7.7.2": | 84 | "@babel/runtime@^7.7.2": |
85 | version "7.13.10" | 85 | version "7.14.0" |
86 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" | 86 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" |
87 | integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== | 87 | integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== |
88 | dependencies: | 88 | dependencies: |
89 | regenerator-runtime "^0.13.4" | 89 | regenerator-runtime "^0.13.4" |
90 | 90 | ||
91 | "@babel/types@^7.6.1", "@babel/types@^7.9.6": | 91 | "@babel/types@^7.6.1", "@babel/types@^7.9.6": |
92 | version "7.13.14" | 92 | version "7.14.1" |
93 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d" | 93 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.1.tgz#095bd12f1c08ab63eff6e8f7745fa7c9cc15a9db" |
94 | integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ== | 94 | integrity sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA== |
95 | dependencies: | 95 | dependencies: |
96 | "@babel/helper-validator-identifier" "^7.12.11" | 96 | "@babel/helper-validator-identifier" "^7.14.0" |
97 | lodash "^4.17.19" | ||
98 | to-fast-properties "^2.0.0" | 97 | to-fast-properties "^2.0.0" |
99 | 98 | ||
100 | "@dabh/diagnostics@^2.0.2": | 99 | "@dabh/diagnostics@^2.0.2": |
@@ -138,9 +137,9 @@ | |||
138 | "@hapi/hoek" "9.x.x" | 137 | "@hapi/hoek" "9.x.x" |
139 | 138 | ||
140 | "@hapi/hoek@9.x.x": | 139 | "@hapi/hoek@9.x.x": |
141 | version "9.1.1" | 140 | version "9.2.0" |
142 | resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" | 141 | resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" |
143 | integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== | 142 | integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== |
144 | 143 | ||
145 | "@jimp/bmp@^0.16.1": | 144 | "@jimp/bmp@^0.16.1": |
146 | version "0.16.1" | 145 | version "0.16.1" |
@@ -456,9 +455,9 @@ | |||
456 | tlds "^1.218.0" | 455 | tlds "^1.218.0" |
457 | 456 | ||
458 | "@mapbox/node-pre-gyp@^1.0.0": | 457 | "@mapbox/node-pre-gyp@^1.0.0": |
459 | version "1.0.3" | 458 | version "1.0.4" |
460 | resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.3.tgz#c740c23ec1007b9278d4c28f767b6e843a88c3d3" | 459 | resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.4.tgz#6c76e7a40138eac39e1a4dc869a083e43e236c00" |
461 | integrity sha512-9dTIfQW8HVCxLku5QrJ/ysS/b2MdYngs9+/oPrOTLvp3TrggdANYVW2h8FGJGDf0J7MYfp44W+c90cVJx+ASuA== | 460 | integrity sha512-M669Qo4nRT7iDmQEjQYC7RU8Z6dpz9UmSbkJ1OFEja3uevCdLKh7IZZki7L1TZj02kRyl82snXFY8QqkyfowrQ== |
462 | dependencies: | 461 | dependencies: |
463 | detect-libc "^1.0.3" | 462 | detect-libc "^1.0.3" |
464 | https-proxy-agent "^5.0.0" | 463 | https-proxy-agent "^5.0.0" |
@@ -524,14 +523,14 @@ | |||
524 | node-fetch "^2.6.1" | 523 | node-fetch "^2.6.1" |
525 | 524 | ||
526 | "@openapitools/openapi-generator-cli@^2.1.4": | 525 | "@openapitools/openapi-generator-cli@^2.1.4": |
527 | version "2.2.5" | 526 | version "2.2.6" |
528 | resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.2.5.tgz#363611ae37d21fabf15ed297dfd5fe82a013faba" | 527 | resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.2.6.tgz#024314000616895edeada87ddf98d53ba8e1e4f0" |
529 | integrity sha512-8eTiw9U5PWYZx41RDIrJ6iZ4XCteD6ljupbJS6/ichZEUwa1Pv78y14QSLVfky+1KpJsq/RlqrgjnPm6yMHo6g== | 528 | integrity sha512-TFKHY1lcknzg6IhPe9f8wWUO11PeKBPnrM40jZjZNUHlQRvh7WLsW6vNlbGcm78eBYTcyY0cKqr1QXUpB9Ez2Q== |
530 | dependencies: | 529 | dependencies: |
531 | "@nestjs/common" "7.6.15" | 530 | "@nestjs/common" "7.6.15" |
532 | "@nestjs/core" "7.6.15" | 531 | "@nestjs/core" "7.6.15" |
533 | "@nuxtjs/opencollective" "0.3.2" | 532 | "@nuxtjs/opencollective" "0.3.2" |
534 | chalk "4.1.0" | 533 | chalk "4.1.1" |
535 | commander "6.2.1" | 534 | commander "6.2.1" |
536 | compare-versions "3.6.0" | 535 | compare-versions "3.6.0" |
537 | concurrently "5.3.0" | 536 | concurrently "5.3.0" |
@@ -550,9 +549,9 @@ | |||
550 | integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== | 549 | integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== |
551 | 550 | ||
552 | "@sindresorhus/is@^4.0.0": | 551 | "@sindresorhus/is@^4.0.0": |
553 | version "4.0.0" | 552 | version "4.0.1" |
554 | resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" | 553 | resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" |
555 | integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== | 554 | integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g== |
556 | 555 | ||
557 | "@szmarczak/http-timer@^1.1.2": | 556 | "@szmarczak/http-timer@^1.1.2": |
558 | version "1.1.2" | 557 | version "1.1.2" |
@@ -597,10 +596,10 @@ | |||
597 | dependencies: | 596 | dependencies: |
598 | "@types/node" "*" | 597 | "@types/node" "*" |
599 | 598 | ||
600 | "@types/bluebird@3.5.33": | 599 | "@types/bluebird@^3.5.33": |
601 | version "3.5.33" | 600 | version "3.5.34" |
602 | resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.33.tgz#d79c020f283bd50bd76101d7d300313c107325fc" | 601 | resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.34.tgz#0e9f1f4f5dfab98a421fb973b5f5690d22411893" |
603 | integrity sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ== | 602 | integrity sha512-QMc57Pf067Rr78l6f4FftvuIXPYxu0VYFRKrZk1Clv+LWy7gN2fTBiAiv68askFHEHZcTLPFd01kNlpKOiSPgQ== |
604 | 603 | ||
605 | "@types/body-parser@*", "@types/body-parser@^1.16.3": | 604 | "@types/body-parser@*", "@types/body-parser@^1.16.3": |
606 | version "1.19.0" | 605 | version "1.19.0" |
@@ -610,10 +609,10 @@ | |||
610 | "@types/connect" "*" | 609 | "@types/connect" "*" |
611 | "@types/node" "*" | 610 | "@types/node" "*" |
612 | 611 | ||
613 | "@types/bull@3.15.0": | 612 | "@types/bull@^3.15.0": |
614 | version "3.15.0" | 613 | version "3.15.1" |
615 | resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.0.tgz#69c518d4e7a53056f287cebcc4ef4ffe91aaf201" | 614 | resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.1.tgz#3c3fd665b43ef383ca95a91b8d1448461fae0898" |
616 | integrity sha512-54Y1RYkJt6i+4dH45w4gZOP6fyhksTvOImfgBYAxgq/nt5ZrES4xFWwOzt2bxAgSR7FMH9fwvaiJN/pripPzag== | 615 | integrity sha512-thZyjxikoyuDa/ptZEqtTEPUjwlDenkpPigpIyad1X5UMp7U0fXTLiDHJjZ/5yXmVPuWx0cXFXj3drmva/UJRA== |
617 | dependencies: | 616 | dependencies: |
618 | "@types/ioredis" "*" | 617 | "@types/ioredis" "*" |
619 | 618 | ||
@@ -652,9 +651,9 @@ | |||
652 | "@types/chai" "*" | 651 | "@types/chai" "*" |
653 | 652 | ||
654 | "@types/chai@*", "@types/chai@^4.0.4": | 653 | "@types/chai@*", "@types/chai@^4.0.4": |
655 | version "4.2.16" | 654 | version "4.2.17" |
656 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.16.tgz#f09cc36e18d28274f942e7201147cce34d97e8c8" | 655 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.17.tgz#85f9f0610f514b22a94125d441f73eef65bde5cc" |
657 | integrity sha512-vI5iOAsez9+roLS3M3+Xx7w+WRuDtSmF8bQkrbcIJ2sC1PcDgVoA0WGpa+bIrJ+y8zqY2oi//fUctkxtIcXJCw== | 656 | integrity sha512-LaiwWNnYuL8xJlQcE91QB2JoswWZckq9A4b+nMPq8dt8AP96727Nb3X4e74u+E3tm4NLTILNI9MYFsyVc30wSA== |
658 | 657 | ||
659 | "@types/component-emitter@^1.2.10": | 658 | "@types/component-emitter@^1.2.10": |
660 | version "1.2.10" | 659 | version "1.2.10" |
@@ -744,9 +743,9 @@ | |||
744 | integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== | 743 | integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== |
745 | 744 | ||
746 | "@types/ioredis@*": | 745 | "@types/ioredis@*": |
747 | version "4.22.3" | 746 | version "4.26.1" |
748 | resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.22.3.tgz#72762efa0374c4a2e879ef697c6c3e4a47f9d641" | 747 | resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.1.tgz#761f1812c48a3ecfbd9d9ecd35c49849f8693e2e" |
749 | integrity sha512-V23g0XZUmkm0Hp/GsQYV5Wz12ynm6h6lyi5v/o63iyqFV7+8t3k5YxCnFWVWFEjO6mvRI7V6f7YxNVxIjPsSUw== | 748 | integrity sha512-k9f+bda6y0ZNrwUMNYI9mqIPqjPZ4Jqc+jTRTUyhFz8aD8cHQBk+uenTKCZj9RhdfrU4sSqrot5sn5LqkAHODw== |
750 | dependencies: | 749 | dependencies: |
751 | "@types/node" "*" | 750 | "@types/node" "*" |
752 | 751 | ||
@@ -832,10 +831,15 @@ | |||
832 | dependencies: | 831 | dependencies: |
833 | "@types/express" "*" | 832 | "@types/express" "*" |
834 | 833 | ||
835 | "@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31": | 834 | "@types/node@*", "@types/node@>=10.0.0", "@types/node@^15.0.1": |
836 | version "14.14.37" | 835 | version "15.0.2" |
837 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" | 836 | resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" |
838 | integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== | 837 | integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== |
838 | |||
839 | "@types/node@^14.14.31": | ||
840 | version "14.14.44" | ||
841 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215" | ||
842 | integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA== | ||
839 | 843 | ||
840 | "@types/nodemailer@^6.2.0": | 844 | "@types/nodemailer@^6.2.0": |
841 | version "6.4.1" | 845 | version "6.4.1" |
@@ -936,9 +940,9 @@ | |||
936 | "@types/node" "*" | 940 | "@types/node" "*" |
937 | 941 | ||
938 | "@types/superagent@*": | 942 | "@types/superagent@*": |
939 | version "4.1.10" | 943 | version "4.1.11" |
940 | resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" | 944 | resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.11.tgz#4822bc64a82a0f579261a77097dbca276556c20e" |
941 | integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== | 945 | integrity sha512-cZkWBXZI+jESnUTp8RDGBmk1Zn2MkScP4V5bjD7DyqB7L0WNWpblh4KX5K/6aTqxFZMhfo1bhi2cwoAEDVBBJw== |
942 | dependencies: | 946 | dependencies: |
943 | "@types/cookiejar" "*" | 947 | "@types/cookiejar" "*" |
944 | "@types/node" "*" | 948 | "@types/node" "*" |
@@ -976,19 +980,19 @@ | |||
976 | "@types/simple-peer" "*" | 980 | "@types/simple-peer" "*" |
977 | 981 | ||
978 | "@types/ws@^7.2.1": | 982 | "@types/ws@^7.2.1": |
979 | version "7.4.1" | 983 | version "7.4.2" |
980 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.1.tgz#49eacb15a0534663d53a36fbf5b4d98f5ae9a73a" | 984 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.2.tgz#bfe739b5f8b3a39742605fbe415ae7e88ee614c8" |
981 | integrity sha512-ISCK1iFnR+jYv7+jLNX0wDqesZ/5RAeY3wUx6QaphmocphU61h+b+PHjS18TF4WIPTu/MMzxIq2PHr32o2TS5Q== | 985 | integrity sha512-PbeN0Eydl7LQl4OIav29YmkO2LxbVuz3nZD/kb19lOS+wLgIkRbWMNmU/QQR7ABpOJ7D7xDOU8co7iohObewrw== |
982 | dependencies: | 986 | dependencies: |
983 | "@types/node" "*" | 987 | "@types/node" "*" |
984 | 988 | ||
985 | "@typescript-eslint/eslint-plugin@^4.8.1": | 989 | "@typescript-eslint/eslint-plugin@^4.8.1": |
986 | version "4.21.0" | 990 | version "4.22.1" |
987 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz#3fce2bfa76d95c00ac4f33dff369cb593aab8878" | 991 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.1.tgz#6bcdbaa4548553ab861b4e5f34936ead1349a543" |
988 | integrity sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ== | 992 | integrity sha512-kVTAghWDDhsvQ602tHBc6WmQkdaYbkcTwZu+7l24jtJiYvm9l+/y/b2BZANEezxPDiX5MK2ZecE+9BFi/YJryw== |
989 | dependencies: | 993 | dependencies: |
990 | "@typescript-eslint/experimental-utils" "4.21.0" | 994 | "@typescript-eslint/experimental-utils" "4.22.1" |
991 | "@typescript-eslint/scope-manager" "4.21.0" | 995 | "@typescript-eslint/scope-manager" "4.22.1" |
992 | debug "^4.1.1" | 996 | debug "^4.1.1" |
993 | functional-red-black-tree "^1.0.1" | 997 | functional-red-black-tree "^1.0.1" |
994 | lodash "^4.17.15" | 998 | lodash "^4.17.15" |
@@ -996,60 +1000,60 @@ | |||
996 | semver "^7.3.2" | 1000 | semver "^7.3.2" |
997 | tsutils "^3.17.1" | 1001 | tsutils "^3.17.1" |
998 | 1002 | ||
999 | "@typescript-eslint/experimental-utils@4.21.0": | 1003 | "@typescript-eslint/experimental-utils@4.22.1": |
1000 | version "4.21.0" | 1004 | version "4.22.1" |
1001 | resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz#0b0bb7c15d379140a660c003bdbafa71ae9134b6" | 1005 | resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.1.tgz#3938a5c89b27dc9a39b5de63a62ab1623ab27497" |
1002 | integrity sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA== | 1006 | integrity sha512-svYlHecSMCQGDO2qN1v477ax/IDQwWhc7PRBiwAdAMJE7GXk5stF4Z9R/8wbRkuX/5e9dHqbIWxjeOjckK3wLQ== |
1003 | dependencies: | 1007 | dependencies: |
1004 | "@types/json-schema" "^7.0.3" | 1008 | "@types/json-schema" "^7.0.3" |
1005 | "@typescript-eslint/scope-manager" "4.21.0" | 1009 | "@typescript-eslint/scope-manager" "4.22.1" |
1006 | "@typescript-eslint/types" "4.21.0" | 1010 | "@typescript-eslint/types" "4.22.1" |
1007 | "@typescript-eslint/typescript-estree" "4.21.0" | 1011 | "@typescript-eslint/typescript-estree" "4.22.1" |
1008 | eslint-scope "^5.0.0" | 1012 | eslint-scope "^5.0.0" |
1009 | eslint-utils "^2.0.0" | 1013 | eslint-utils "^2.0.0" |
1010 | 1014 | ||
1011 | "@typescript-eslint/parser@^4.0.0": | 1015 | "@typescript-eslint/parser@^4.0.0": |
1012 | version "4.21.0" | 1016 | version "4.22.1" |
1013 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.21.0.tgz#a227fc2af4001668c3e3f7415d4feee5093894c1" | 1017 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.22.1.tgz#a95bda0fd01d994a15fc3e99dc984294f25c19cc" |
1014 | integrity sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA== | 1018 | integrity sha512-l+sUJFInWhuMxA6rtirzjooh8cM/AATAe3amvIkqKFeMzkn85V+eLzb1RyuXkHak4dLfYzOmF6DXPyflJvjQnw== |
1015 | dependencies: | 1019 | dependencies: |
1016 | "@typescript-eslint/scope-manager" "4.21.0" | 1020 | "@typescript-eslint/scope-manager" "4.22.1" |
1017 | "@typescript-eslint/types" "4.21.0" | 1021 | "@typescript-eslint/types" "4.22.1" |
1018 | "@typescript-eslint/typescript-estree" "4.21.0" | 1022 | "@typescript-eslint/typescript-estree" "4.22.1" |
1019 | debug "^4.1.1" | 1023 | debug "^4.1.1" |
1020 | 1024 | ||
1021 | "@typescript-eslint/scope-manager@4.21.0": | 1025 | "@typescript-eslint/scope-manager@4.22.1": |
1022 | version "4.21.0" | 1026 | version "4.22.1" |
1023 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz#c81b661c4b8af1ec0c010d847a8f9ab76ab95b4d" | 1027 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz#5bb357f94f9cd8b94e6be43dd637eb73b8f355b4" |
1024 | integrity sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw== | 1028 | integrity sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g== |
1025 | dependencies: | 1029 | dependencies: |
1026 | "@typescript-eslint/types" "4.21.0" | 1030 | "@typescript-eslint/types" "4.22.1" |
1027 | "@typescript-eslint/visitor-keys" "4.21.0" | 1031 | "@typescript-eslint/visitor-keys" "4.22.1" |
1028 | 1032 | ||
1029 | "@typescript-eslint/types@4.21.0": | 1033 | "@typescript-eslint/types@4.22.1": |
1030 | version "4.21.0" | 1034 | version "4.22.1" |
1031 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.21.0.tgz#abdc3463bda5d31156984fa5bc316789c960edef" | 1035 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.1.tgz#bf99c6cec0b4a23d53a61894816927f2adad856a" |
1032 | integrity sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w== | 1036 | integrity sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw== |
1033 | 1037 | ||
1034 | "@typescript-eslint/typescript-estree@4.21.0": | 1038 | "@typescript-eslint/typescript-estree@4.22.1": |
1035 | version "4.21.0" | 1039 | version "4.22.1" |
1036 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz#3817bd91857beeaeff90f69f1f112ea58d350b0a" | 1040 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz#dca379eead8cdfd4edc04805e83af6d148c164f9" |
1037 | integrity sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w== | 1041 | integrity sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A== |
1038 | dependencies: | 1042 | dependencies: |
1039 | "@typescript-eslint/types" "4.21.0" | 1043 | "@typescript-eslint/types" "4.22.1" |
1040 | "@typescript-eslint/visitor-keys" "4.21.0" | 1044 | "@typescript-eslint/visitor-keys" "4.22.1" |
1041 | debug "^4.1.1" | 1045 | debug "^4.1.1" |
1042 | globby "^11.0.1" | 1046 | globby "^11.0.1" |
1043 | is-glob "^4.0.1" | 1047 | is-glob "^4.0.1" |
1044 | semver "^7.3.2" | 1048 | semver "^7.3.2" |
1045 | tsutils "^3.17.1" | 1049 | tsutils "^3.17.1" |
1046 | 1050 | ||
1047 | "@typescript-eslint/visitor-keys@4.21.0": | 1051 | "@typescript-eslint/visitor-keys@4.22.1": |
1048 | version "4.21.0" | 1052 | version "4.22.1" |
1049 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz#990a9acdc124331f5863c2cf21c88ba65233cd8d" | 1053 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz#6045ae25a11662c671f90b3a403d682dfca0b7a6" |
1050 | integrity sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w== | 1054 | integrity sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ== |
1051 | dependencies: | 1055 | dependencies: |
1052 | "@typescript-eslint/types" "4.21.0" | 1056 | "@typescript-eslint/types" "4.22.1" |
1053 | eslint-visitor-keys "^2.0.0" | 1057 | eslint-visitor-keys "^2.0.0" |
1054 | 1058 | ||
1055 | "@ungap/promise-all-settled@1.1.2": | 1059 | "@ungap/promise-all-settled@1.1.2": |
@@ -1057,6 +1061,15 @@ | |||
1057 | resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" | 1061 | resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" |
1058 | integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== | 1062 | integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== |
1059 | 1063 | ||
1064 | "@uploadx/core@^4.4.0": | ||
1065 | version "4.4.0" | ||
1066 | resolved "https://registry.yarnpkg.com/@uploadx/core/-/core-4.4.0.tgz#27ea2b0d28125e81a6bdd65637dc5c7829306cc7" | ||
1067 | integrity sha512-dU0oDURYR5RvuAzf63EL9e/fCY4OOQKOs237UTbZDulbRbiyxwEZR+IpRYYr3hKRjjij03EF/Y5j54VGkebAKg== | ||
1068 | dependencies: | ||
1069 | bytes "^3.1.0" | ||
1070 | debug "^4.3.1" | ||
1071 | multiparty "^4.2.2" | ||
1072 | |||
1060 | abbrev@1: | 1073 | abbrev@1: |
1061 | version "1.1.1" | 1074 | version "1.1.1" |
1062 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" | 1075 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" |
@@ -1120,9 +1133,9 @@ ajv@^6.10.0, ajv@^6.12.4: | |||
1120 | uri-js "^4.2.2" | 1133 | uri-js "^4.2.2" |
1121 | 1134 | ||
1122 | ajv@^8.0.1: | 1135 | ajv@^8.0.1: |
1123 | version "8.1.0" | 1136 | version "8.2.0" |
1124 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.1.0.tgz#45d5d3d36c7cdd808930cc3e603cf6200dbeb736" | 1137 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.2.0.tgz#c89d3380a784ce81b2085f48811c4c101df4c602" |
1125 | integrity sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ== | 1138 | integrity sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA== |
1126 | dependencies: | 1139 | dependencies: |
1127 | fast-deep-equal "^3.1.1" | 1140 | fast-deep-equal "^3.1.1" |
1128 | json-schema-traverse "^1.0.0" | 1141 | json-schema-traverse "^1.0.0" |
@@ -1370,9 +1383,9 @@ at-least-node@^1.0.0: | |||
1370 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== | 1383 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== |
1371 | 1384 | ||
1372 | autocannon@^7.0.4: | 1385 | autocannon@^7.0.4: |
1373 | version "7.0.5" | 1386 | version "7.2.0" |
1374 | resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.5.tgz#7c195ba09ae3b299d6f7532950d1e07041538b29" | 1387 | resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.2.0.tgz#63b6f5321c8d26cf8360d9931dbcde6e61494121" |
1375 | integrity sha512-VMOfWf0e9EB5Crr7/snXTb64oC7I3lofpAjBcPWvHGet94DKjHCsbj05iIt2WTenPKub++6PETb/H9qleV9yJg== | 1388 | integrity sha512-yMXnHdGcHiz+dk/VHsgIXHBn7qK5hIaRS5yKbLzGTvn3REbEJA37xFI7+KkCEAtSOGnMLhd6VTZ69g9GReCClQ== |
1376 | dependencies: | 1389 | dependencies: |
1377 | chalk "^4.1.0" | 1390 | chalk "^4.1.0" |
1378 | char-spinner "^1.0.1" | 1391 | char-spinner "^1.0.1" |
@@ -1387,13 +1400,13 @@ autocannon@^7.0.4: | |||
1387 | http-parser-js "^0.5.2" | 1400 | http-parser-js "^0.5.2" |
1388 | hyperid "^2.0.3" | 1401 | hyperid "^2.0.3" |
1389 | manage-path "^2.0.0" | 1402 | manage-path "^2.0.0" |
1390 | minimist "^1.2.0" | ||
1391 | on-net-listen "^1.1.1" | 1403 | on-net-listen "^1.1.1" |
1392 | pretty-bytes "^5.4.1" | 1404 | pretty-bytes "^5.4.1" |
1393 | progress "^2.0.3" | 1405 | progress "^2.0.3" |
1394 | reinterval "^1.1.0" | 1406 | reinterval "^1.1.0" |
1395 | retimer "^3.0.0" | 1407 | retimer "^3.0.0" |
1396 | semver "^7.3.2" | 1408 | semver "^7.3.2" |
1409 | subarg "^1.0.0" | ||
1397 | timestring "^6.0.0" | 1410 | timestring "^6.0.0" |
1398 | 1411 | ||
1399 | axios@0.21.1: | 1412 | axios@0.21.1: |
@@ -1579,9 +1592,9 @@ bittorrent-protocol@^3.2.0: | |||
1579 | unordered-array-remove "^1.0.2" | 1592 | unordered-array-remove "^1.0.2" |
1580 | 1593 | ||
1581 | bittorrent-tracker@^9.0.0: | 1594 | bittorrent-tracker@^9.0.0: |
1582 | version "9.16.1" | 1595 | version "9.17.0" |
1583 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.16.1.tgz#8eff88dcf8180fe2b5c57943127d1fdef5918ee7" | 1596 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.0.tgz#8b4b6f6a49efa9023267c3ca22e1a5f63216fc1f" |
1584 | integrity sha512-JjegXwpWK8xRTHd5sqKTVqPhlhzAqJrR37gSiciTa1UkSSM6SWKVUDq7ZiGS3d8FhqonDSuPLQ9wUOC2q2jeIA== | 1597 | integrity sha512-ErpOx8AAUW8eLwxnEHp15vs0LDJECLADHISEBM+HXclG3J2/9kMBJ31IjwlB8kUNigknSwm8odAThjJEeyL1yA== |
1585 | dependencies: | 1598 | dependencies: |
1586 | bencode "^2.0.1" | 1599 | bencode "^2.0.1" |
1587 | bittorrent-peerid "^1.3.2" | 1600 | bittorrent-peerid "^1.3.2" |
@@ -1751,7 +1764,7 @@ buffer@^5.2.0: | |||
1751 | base64-js "^1.3.1" | 1764 | base64-js "^1.3.1" |
1752 | ieee754 "^1.1.13" | 1765 | ieee754 "^1.1.13" |
1753 | 1766 | ||
1754 | buffer@^6.0.2: | 1767 | buffer@^6.0.3: |
1755 | version "6.0.3" | 1768 | version "6.0.3" |
1756 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" | 1769 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" |
1757 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== | 1770 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== |
@@ -1767,9 +1780,9 @@ bufferutil@^4.0.1: | |||
1767 | node-gyp-build "^4.2.0" | 1780 | node-gyp-build "^4.2.0" |
1768 | 1781 | ||
1769 | bull@^3.4.2: | 1782 | bull@^3.4.2: |
1770 | version "3.22.0" | 1783 | version "3.22.4" |
1771 | resolved "https://registry.yarnpkg.com/bull/-/bull-3.22.0.tgz#fb04b68189bd49e56155f4366df96330c059868c" | 1784 | resolved "https://registry.yarnpkg.com/bull/-/bull-3.22.4.tgz#1bda418d2aebdf892413d0c45d75c8ea5e0fc984" |
1772 | integrity sha512-csQTIuvoKnVuW6gbZmIe9mVkLy2DzvRodywjXN7cfYlvXKme3156FIc1Zssn5IRKpDKyyq0++AYsLO4mdtnf0Q== | 1785 | integrity sha512-CV78TuSKyDj3SuZvySTOFXqZBtHxebhctLTq2Ff9Jrn51XOaxkEDioIDzq2LIUKEhTW8l3rFK5bIWNwweY0LXQ== |
1773 | dependencies: | 1786 | dependencies: |
1774 | cron-parser "^2.13.0" | 1787 | cron-parser "^2.13.0" |
1775 | debuglog "^1.0.0" | 1788 | debuglog "^1.0.0" |
@@ -1790,7 +1803,7 @@ busboy@^0.2.11: | |||
1790 | dicer "0.2.5" | 1803 | dicer "0.2.5" |
1791 | readable-stream "1.1.x" | 1804 | readable-stream "1.1.x" |
1792 | 1805 | ||
1793 | bytes@3.1.0, bytes@^3.0.0: | 1806 | bytes@3.1.0, bytes@^3.0.0, bytes@^3.1.0: |
1794 | version "3.1.0" | 1807 | version "3.1.0" |
1795 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | 1808 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" |
1796 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== | 1809 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== |
@@ -1905,10 +1918,10 @@ chai@^4.1.1: | |||
1905 | pathval "^1.1.1" | 1918 | pathval "^1.1.1" |
1906 | type-detect "^4.0.5" | 1919 | type-detect "^4.0.5" |
1907 | 1920 | ||
1908 | chalk@4.1.0, chalk@^4.0.0, chalk@^4.1.0: | 1921 | chalk@4.1.1, chalk@^4.0.0, chalk@^4.1.0: |
1909 | version "4.1.0" | 1922 | version "4.1.1" |
1910 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" | 1923 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" |
1911 | integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== | 1924 | integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== |
1912 | dependencies: | 1925 | dependencies: |
1913 | ansi-styles "^4.1.0" | 1926 | ansi-styles "^4.1.0" |
1914 | supports-color "^7.1.0" | 1927 | supports-color "^7.1.0" |
@@ -1963,15 +1976,15 @@ check-error@^1.0.2: | |||
1963 | integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= | 1976 | integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= |
1964 | 1977 | ||
1965 | cheerio-select@^1.3.0: | 1978 | cheerio-select@^1.3.0: |
1966 | version "1.3.0" | 1979 | version "1.4.0" |
1967 | resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.3.0.tgz#26a50968260b7e4281238c1e7da7ed2766652f3b" | 1980 | resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9" |
1968 | integrity sha512-mLgqdHxVOQyhOIkG5QnRkDg7h817Dkf0dAvlCio2TJMmR72cJKH0bF28SHXvLkVrGcGOiub0/Bs/CMnPeQO7qw== | 1981 | integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew== |
1969 | dependencies: | 1982 | dependencies: |
1970 | css-select "^4.0.0" | 1983 | css-select "^4.1.2" |
1971 | css-what "^5.0.0" | 1984 | css-what "^5.0.0" |
1972 | domelementtype "^2.2.0" | 1985 | domelementtype "^2.2.0" |
1973 | domhandler "^4.1.0" | 1986 | domhandler "^4.2.0" |
1974 | domutils "^2.5.2" | 1987 | domutils "^2.6.0" |
1975 | 1988 | ||
1976 | cheerio@^1.0.0-rc.3: | 1989 | cheerio@^1.0.0-rc.3: |
1977 | version "1.0.0-rc.6" | 1990 | version "1.0.0-rc.6" |
@@ -2299,13 +2312,13 @@ concurrently@5.3.0: | |||
2299 | yargs "^13.3.0" | 2312 | yargs "^13.3.0" |
2300 | 2313 | ||
2301 | concurrently@^6.0.0: | 2314 | concurrently@^6.0.0: |
2302 | version "6.0.1" | 2315 | version "6.0.2" |
2303 | resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.0.1.tgz#b472efd9398bd9f5b117e22f72c3e50bf0a8a651" | 2316 | resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.0.2.tgz#4ecdfc78a72a6f626a3a5d3c2a7a81962f3663e3" |
2304 | integrity sha512-YCF/Wf31a910hXu7eGN9/SyHKD/usw3Shw4IPYuqIsxxC39v92engYlIlOs/zXnBJtX/6aVuhgzfhZeGJkhU4w== | 2317 | integrity sha512-u+1Q0dJG5BidgUTpz9CU16yoHTt/oApFDQ3mbvHwSDgMjU7aGqy0q8ZQyaZyaNxdwRKTD872Ux3Twc6//sWA+Q== |
2305 | dependencies: | 2318 | dependencies: |
2306 | chalk "^4.1.0" | 2319 | chalk "^4.1.0" |
2307 | date-fns "^2.16.1" | 2320 | date-fns "^2.16.1" |
2308 | lodash "^4.17.20" | 2321 | lodash "^4.17.21" |
2309 | read-pkg "^5.2.0" | 2322 | read-pkg "^5.2.0" |
2310 | rxjs "^6.6.3" | 2323 | rxjs "^6.6.3" |
2311 | spawn-command "^0.0.2-1" | 2324 | spawn-command "^0.0.2-1" |
@@ -2507,15 +2520,15 @@ crypto-random-string@^2.0.0: | |||
2507 | resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" | 2520 | resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" |
2508 | integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== | 2521 | integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== |
2509 | 2522 | ||
2510 | css-select@^4.0.0: | 2523 | css-select@^4.1.2: |
2511 | version "4.0.0" | 2524 | version "4.1.2" |
2512 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.0.0.tgz#9b7b53bd82e4b348a6e0924ce37645e5db43af8e" | 2525 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" |
2513 | integrity sha512-I7favumBlDP/nuHBKLfL5RqvlvRdn/W29evvWJ+TaoGPm7QD+xSIN5eY2dyGjtkUmemh02TZrqJb4B8DWo6PoQ== | 2526 | integrity sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw== |
2514 | dependencies: | 2527 | dependencies: |
2515 | boolbase "^1.0.0" | 2528 | boolbase "^1.0.0" |
2516 | css-what "^5.0.0" | 2529 | css-what "^5.0.0" |
2517 | domhandler "^4.1.0" | 2530 | domhandler "^4.2.0" |
2518 | domutils "^2.5.1" | 2531 | domutils "^2.6.0" |
2519 | nth-check "^2.0.0" | 2532 | nth-check "^2.0.0" |
2520 | 2533 | ||
2521 | css-what@^5.0.0: | 2534 | css-what@^5.0.0: |
@@ -2556,9 +2569,9 @@ data-uri-to-buffer@^3.0.1: | |||
2556 | integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== | 2569 | integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== |
2557 | 2570 | ||
2558 | date-fns@^2.0.1, date-fns@^2.16.1: | 2571 | date-fns@^2.0.1, date-fns@^2.16.1: |
2559 | version "2.20.2" | 2572 | version "2.21.2" |
2560 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.20.2.tgz#7f05d1275e1e43c3bdde5998201920098e19c6a1" | 2573 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.2.tgz#9db92305cf00626e9122e56c72195b17725594aa" |
2561 | integrity sha512-QS0Z8SD/ALhKFvhtU4Fhz+1crsI7fPzBquXmdWay33KJPEU7btro2hnmmErpQRmt2D624B1lbjXQKDUMLnQTmQ== | 2574 | integrity sha512-FMkG7pIPx64mGIpS2LOb3Wp3O606H/hatoiz7G0oiYWai1izdM4tF1dd7QABv2NogkIDI4wxsfLLFQSuVvDHgA== |
2562 | 2575 | ||
2563 | dateformat@^3.0.3: | 2576 | dateformat@^3.0.3: |
2564 | version "3.0.3" | 2577 | version "3.0.3" |
@@ -2811,21 +2824,21 @@ domhandler@^3.0.0: | |||
2811 | dependencies: | 2824 | dependencies: |
2812 | domelementtype "^2.0.1" | 2825 | domelementtype "^2.0.1" |
2813 | 2826 | ||
2814 | domhandler@^4.0.0, domhandler@^4.1.0: | 2827 | domhandler@^4.0.0, domhandler@^4.1.0, domhandler@^4.2.0: |
2815 | version "4.1.0" | 2828 | version "4.2.0" |
2816 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43" | 2829 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" |
2817 | integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ== | 2830 | integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== |
2818 | dependencies: | 2831 | dependencies: |
2819 | domelementtype "^2.2.0" | 2832 | domelementtype "^2.2.0" |
2820 | 2833 | ||
2821 | domutils@^2.0.0, domutils@^2.5.1, domutils@^2.5.2: | 2834 | domutils@^2.0.0, domutils@^2.5.2, domutils@^2.6.0: |
2822 | version "2.5.2" | 2835 | version "2.6.0" |
2823 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.2.tgz#37ef8ba087dff1a17175e7092e8a042e4b050e6c" | 2836 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" |
2824 | integrity sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ== | 2837 | integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== |
2825 | dependencies: | 2838 | dependencies: |
2826 | dom-serializer "^1.0.1" | 2839 | dom-serializer "^1.0.1" |
2827 | domelementtype "^2.2.0" | 2840 | domelementtype "^2.2.0" |
2828 | domhandler "^4.1.0" | 2841 | domhandler "^4.2.0" |
2829 | 2842 | ||
2830 | dot-prop@^5.2.0: | 2843 | dot-prop@^5.2.0: |
2831 | version "5.3.0" | 2844 | version "5.3.0" |
@@ -3013,10 +3026,10 @@ entities@~2.1.0: | |||
3013 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" | 3026 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" |
3014 | integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== | 3027 | integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== |
3015 | 3028 | ||
3016 | err-code@^2.0.3: | 3029 | err-code@^3.0.1: |
3017 | version "2.0.3" | 3030 | version "3.0.1" |
3018 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" | 3031 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" |
3019 | integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== | 3032 | integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== |
3020 | 3033 | ||
3021 | error-ex@^1.2.0, error-ex@^1.3.1: | 3034 | error-ex@^1.2.0, error-ex@^1.3.1: |
3022 | version "1.3.2" | 3035 | version "1.3.2" |
@@ -3226,14 +3239,14 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: | |||
3226 | integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== | 3239 | integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== |
3227 | 3240 | ||
3228 | eslint-visitor-keys@^2.0.0: | 3241 | eslint-visitor-keys@^2.0.0: |
3229 | version "2.0.0" | 3242 | version "2.1.0" |
3230 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" | 3243 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" |
3231 | integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== | 3244 | integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== |
3232 | 3245 | ||
3233 | eslint@^7.2.0: | 3246 | eslint@^7.2.0: |
3234 | version "7.24.0" | 3247 | version "7.25.0" |
3235 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a" | 3248 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67" |
3236 | integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ== | 3249 | integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw== |
3237 | dependencies: | 3250 | dependencies: |
3238 | "@babel/code-frame" "7.12.11" | 3251 | "@babel/code-frame" "7.12.11" |
3239 | "@eslint/eslintrc" "^0.4.0" | 3252 | "@eslint/eslintrc" "^0.4.0" |
@@ -3378,11 +3391,11 @@ express-rate-limit@^5.0.0: | |||
3378 | integrity sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA== | 3391 | integrity sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA== |
3379 | 3392 | ||
3380 | express-validator@^6.4.0: | 3393 | express-validator@^6.4.0: |
3381 | version "6.10.0" | 3394 | version "6.10.1" |
3382 | resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.10.0.tgz#66f70f73d04fb55c227401c75fe3713879c9cb70" | 3395 | resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.10.1.tgz#0ec71a2e472d85158fede76f0199054034b06e7c" |
3383 | integrity sha512-gDtepU94EpUzgFvKO/8JzjZ4uqIF4xHekjYtcNgFDiBK6Hob3MQhPU8s/c3NaWd1xi5e5nA0oVmOJ0b0ZBO36Q== | 3396 | integrity sha512-joYSJdkUyKMZ2gAUvyQNmqJ7x1vhrC/IHCKWauhKfoXNF83j65KnlqEEXXynBnJRd0QrNZ/aXw9uIhS6ptG0Cg== |
3384 | dependencies: | 3397 | dependencies: |
3385 | lodash "^4.17.20" | 3398 | lodash "^4.17.21" |
3386 | validator "^13.5.2" | 3399 | validator "^13.5.2" |
3387 | 3400 | ||
3388 | express@^4.12.4, express@^4.16.4, express@^4.17.1: | 3401 | express@^4.12.4, express@^4.16.4, express@^4.17.1: |
@@ -3507,9 +3520,9 @@ fecha@^4.2.0: | |||
3507 | integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== | 3520 | integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== |
3508 | 3521 | ||
3509 | fetch-blob@^2.1.1: | 3522 | fetch-blob@^2.1.1: |
3510 | version "2.1.1" | 3523 | version "2.1.2" |
3511 | resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.1.tgz#a54ab0d5ed7ccdb0691db77b6674308b23fb2237" | 3524 | resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" |
3512 | integrity sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ== | 3525 | integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== |
3513 | 3526 | ||
3514 | figures@^3.0.0: | 3527 | figures@^3.0.0: |
3515 | version "3.2.0" | 3528 | version "3.2.0" |
@@ -3627,9 +3640,9 @@ fn.name@1.x.x: | |||
3627 | integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== | 3640 | integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== |
3628 | 3641 | ||
3629 | follow-redirects@^1.10.0: | 3642 | follow-redirects@^1.10.0: |
3630 | version "1.13.3" | 3643 | version "1.14.0" |
3631 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" | 3644 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.0.tgz#f5d260f95c5f8c105894491feee5dc8993b402fe" |
3632 | integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== | 3645 | integrity sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg== |
3633 | 3646 | ||
3634 | for-each@^0.3.3: | 3647 | for-each@^0.3.3: |
3635 | version "0.3.3" | 3648 | version "0.3.3" |
@@ -3697,7 +3710,7 @@ fs-chunk-store@^2.0.2: | |||
3697 | run-parallel "^1.1.2" | 3710 | run-parallel "^1.1.2" |
3698 | thunky "^1.0.1" | 3711 | thunky "^1.0.1" |
3699 | 3712 | ||
3700 | fs-extra@9.1.0, fs-extra@^9.0.0: | 3713 | fs-extra@9.1.0: |
3701 | version "9.1.0" | 3714 | version "9.1.0" |
3702 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" | 3715 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" |
3703 | integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== | 3716 | integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== |
@@ -3707,6 +3720,15 @@ fs-extra@9.1.0, fs-extra@^9.0.0: | |||
3707 | jsonfile "^6.0.1" | 3720 | jsonfile "^6.0.1" |
3708 | universalify "^2.0.0" | 3721 | universalify "^2.0.0" |
3709 | 3722 | ||
3723 | fs-extra@^10.0.0: | ||
3724 | version "10.0.0" | ||
3725 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" | ||
3726 | integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== | ||
3727 | dependencies: | ||
3728 | graceful-fs "^4.2.0" | ||
3729 | jsonfile "^6.0.1" | ||
3730 | universalify "^2.0.0" | ||
3731 | |||
3710 | fs-minipass@^2.0.0: | 3732 | fs-minipass@^2.0.0: |
3711 | version "2.1.0" | 3733 | version "2.1.0" |
3712 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" | 3734 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" |
@@ -3748,7 +3770,7 @@ gauge@~2.7.3: | |||
3748 | strip-ansi "^3.0.1" | 3770 | strip-ansi "^3.0.1" |
3749 | wide-align "^1.1.0" | 3771 | wide-align "^1.1.0" |
3750 | 3772 | ||
3751 | get-browser-rtc@^1.0.2: | 3773 | get-browser-rtc@^1.1.0: |
3752 | version "1.1.0" | 3774 | version "1.1.0" |
3753 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" | 3775 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" |
3754 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== | 3776 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== |
@@ -3804,9 +3826,9 @@ get-stream@^5.1.0: | |||
3804 | pump "^3.0.0" | 3826 | pump "^3.0.0" |
3805 | 3827 | ||
3806 | get-stream@^6.0.0: | 3828 | get-stream@^6.0.0: |
3807 | version "6.0.0" | 3829 | version "6.0.1" |
3808 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718" | 3830 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" |
3809 | integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg== | 3831 | integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== |
3810 | 3832 | ||
3811 | getpass@^0.1.1: | 3833 | getpass@^0.1.1: |
3812 | version "0.1.7" | 3834 | version "0.1.7" |
@@ -4001,9 +4023,9 @@ he@1.2.0, he@^1.2.0: | |||
4001 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== | 4023 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== |
4002 | 4024 | ||
4003 | helmet@^4.1.0: | 4025 | helmet@^4.1.0: |
4004 | version "4.4.1" | 4026 | version "4.6.0" |
4005 | resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.4.1.tgz#a17e1444d81d7a83ddc6e6f9bc6e2055b994efe7" | 4027 | resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df" |
4006 | integrity sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw== | 4028 | integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg== |
4007 | 4029 | ||
4008 | hh-mm-ss@~1.2.0: | 4030 | hh-mm-ss@~1.2.0: |
4009 | version "1.2.0" | 4031 | version "1.2.0" |
@@ -4085,6 +4107,17 @@ http-errors@~1.7.2: | |||
4085 | statuses ">= 1.5.0 < 2" | 4107 | statuses ">= 1.5.0 < 2" |
4086 | toidentifier "1.0.0" | 4108 | toidentifier "1.0.0" |
4087 | 4109 | ||
4110 | http-errors@~1.8.0: | ||
4111 | version "1.8.0" | ||
4112 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" | ||
4113 | integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== | ||
4114 | dependencies: | ||
4115 | depd "~1.1.2" | ||
4116 | inherits "2.0.4" | ||
4117 | setprototypeof "1.2.0" | ||
4118 | statuses ">= 1.5.0 < 2" | ||
4119 | toidentifier "1.0.0" | ||
4120 | |||
4088 | "http-node@github:feross/http-node#webtorrent": | 4121 | "http-node@github:feross/http-node#webtorrent": |
4089 | version "1.2.0" | 4122 | version "1.2.0" |
4090 | resolved "https://codeload.github.com/feross/http-node/tar.gz/342ef8624495343ffd050bd0808b3750cf0e3974" | 4123 | resolved "https://codeload.github.com/feross/http-node/tar.gz/342ef8624495343ffd050bd0808b3750cf0e3974" |
@@ -4292,9 +4325,9 @@ inquirer@7.3.3: | |||
4292 | through "^2.3.6" | 4325 | through "^2.3.6" |
4293 | 4326 | ||
4294 | ioredis@^4.22.0: | 4327 | ioredis@^4.22.0: |
4295 | version "4.26.0" | 4328 | version "4.27.2" |
4296 | resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.26.0.tgz#dbbfb5e5da085fc2b1de8174db50fa42f9fed66a" | 4329 | resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.2.tgz#6a79bca05164482da796f8fa010bccefd3bf4811" |
4297 | integrity sha512-nh39okWezWWZ35/RxXXzHksMFt4WCaev8SNO2kozRDeVdEAJj16EarqPP3JeHz8IEjEXN5CiVtbWMk62Z0eveQ== | 4330 | integrity sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg== |
4298 | dependencies: | 4331 | dependencies: |
4299 | cluster-key-slot "^1.1.0" | 4332 | cluster-key-slot "^1.1.0" |
4300 | debug "^4.3.1" | 4333 | debug "^4.3.1" |
@@ -4360,9 +4393,9 @@ is-ascii@^1.0.0: | |||
4360 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= | 4393 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= |
4361 | 4394 | ||
4362 | is-bigint@^1.0.1: | 4395 | is-bigint@^1.0.1: |
4363 | version "1.0.1" | 4396 | version "1.0.2" |
4364 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" | 4397 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" |
4365 | integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== | 4398 | integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== |
4366 | 4399 | ||
4367 | is-binary-path@~2.1.0: | 4400 | is-binary-path@~2.1.0: |
4368 | version "2.1.0" | 4401 | version "2.1.0" |
@@ -4403,9 +4436,9 @@ is-cidr@^4.0.0: | |||
4403 | cidr-regex "^3.1.1" | 4436 | cidr-regex "^3.1.1" |
4404 | 4437 | ||
4405 | is-core-module@^2.2.0: | 4438 | is-core-module@^2.2.0: |
4406 | version "2.2.0" | 4439 | version "2.3.0" |
4407 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" | 4440 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887" |
4408 | integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== | 4441 | integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw== |
4409 | dependencies: | 4442 | dependencies: |
4410 | has "^1.0.3" | 4443 | has "^1.0.3" |
4411 | 4444 | ||
@@ -4650,7 +4683,7 @@ js-tokens@^4.0.0: | |||
4650 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | 4683 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" |
4651 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== | 4684 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== |
4652 | 4685 | ||
4653 | js-yaml@4.0.0, js-yaml@^4.0.0: | 4686 | js-yaml@4.0.0: |
4654 | version "4.0.0" | 4687 | version "4.0.0" |
4655 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" | 4688 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" |
4656 | integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== | 4689 | integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== |
@@ -4665,6 +4698,13 @@ js-yaml@^3.13.1, js-yaml@^3.14.0: | |||
4665 | argparse "^1.0.7" | 4698 | argparse "^1.0.7" |
4666 | esprima "^4.0.0" | 4699 | esprima "^4.0.0" |
4667 | 4700 | ||
4701 | js-yaml@^4.0.0: | ||
4702 | version "4.1.0" | ||
4703 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" | ||
4704 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== | ||
4705 | dependencies: | ||
4706 | argparse "^2.0.1" | ||
4707 | |||
4668 | jsbn@~0.1.0: | 4708 | jsbn@~0.1.0: |
4669 | version "0.1.1" | 4709 | version "0.1.1" |
4670 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" | 4710 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" |
@@ -5077,10 +5117,18 @@ lru@^3.1.0: | |||
5077 | dependencies: | 5117 | dependencies: |
5078 | inherits "^2.0.1" | 5118 | inherits "^2.0.1" |
5079 | 5119 | ||
5120 | lt_donthave@^1.0.1: | ||
5121 | version "1.0.1" | ||
5122 | resolved "https://registry.yarnpkg.com/lt_donthave/-/lt_donthave-1.0.1.tgz#a160e08bdf15b9e092172063688855a6c031d8b3" | ||
5123 | integrity sha512-PfOXfDN9GnUjlNHjjxKQuMxPC8s12iSrnmg+Ff1BU1uLn7S1BFAKzpZCu6Gwg3WsCUvTZrZoDSHvy6B/j+N4/Q== | ||
5124 | dependencies: | ||
5125 | debug "^4.2.0" | ||
5126 | unordered-array-remove "^1.0.2" | ||
5127 | |||
5080 | magnet-uri@^6.0.0, magnet-uri@^6.1.0: | 5128 | magnet-uri@^6.0.0, magnet-uri@^6.1.0: |
5081 | version "6.1.0" | 5129 | version "6.1.1" |
5082 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.1.0.tgz#fe73026ba1ee77c955097a4979d1003f4fb7ecf7" | 5130 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.1.1.tgz#e05de8db0224436ae4e8eb3d1085bec9b488347e" |
5083 | integrity sha512-731qLviHaqN/Ni96wm6gNKuvoip+QHWTznjHNz/4qDlsHh3/CWJoL8fZ18IIRhGJgnWoKJp8RVE5lZvQ60Khhw== | 5131 | integrity sha512-TUyzaLB36TqqIHzgvkMrlZUPN6mfoLX/+2do5YJH3gjBQL2auEtivT+99npIiA77YepJ6pYA/AzWhboXTAAm0w== |
5084 | dependencies: | 5132 | dependencies: |
5085 | bep53-range "^1.0.0" | 5133 | bep53-range "^1.0.0" |
5086 | thirty-two "^1.0.2" | 5134 | thirty-two "^1.0.2" |
@@ -5180,10 +5228,10 @@ markdown-it-emoji@^2.0.0: | |||
5180 | resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231" | 5228 | resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231" |
5181 | integrity sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ== | 5229 | integrity sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ== |
5182 | 5230 | ||
5183 | markdown-it@12.0.4: | 5231 | markdown-it@^12.0.4: |
5184 | version "12.0.4" | 5232 | version "12.0.6" |
5185 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" | 5233 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.6.tgz#adcc8e5fe020af292ccbdf161fe84f1961516138" |
5186 | integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== | 5234 | integrity sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w== |
5187 | dependencies: | 5235 | dependencies: |
5188 | argparse "^2.0.1" | 5236 | argparse "^2.0.1" |
5189 | entities "~2.1.0" | 5237 | entities "~2.1.0" |
@@ -5539,6 +5587,15 @@ multimatch@^5.0.0: | |||
5539 | arrify "^2.0.1" | 5587 | arrify "^2.0.1" |
5540 | minimatch "^3.0.4" | 5588 | minimatch "^3.0.4" |
5541 | 5589 | ||
5590 | multiparty@^4.2.2: | ||
5591 | version "4.2.2" | ||
5592 | resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.2.tgz#bee5fb5737247628d39dab4979ffd6d57bf60ef6" | ||
5593 | integrity sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q== | ||
5594 | dependencies: | ||
5595 | http-errors "~1.8.0" | ||
5596 | safe-buffer "5.2.1" | ||
5597 | uid-safe "2.1.5" | ||
5598 | |||
5542 | multistream@^4.0.1, multistream@^4.1.0: | 5599 | multistream@^4.0.1, multistream@^4.1.0: |
5543 | version "4.1.0" | 5600 | version "4.1.0" |
5544 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" | 5601 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" |
@@ -5653,7 +5710,7 @@ nodemailer@5.0.0: | |||
5653 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716" | 5710 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716" |
5654 | integrity sha512-XI4PI5L7GYcJyHkPcHlvPyRrYohNYBNRNbt1tU8PXNU3E1ADJC84a13V0vbL9AM431OP+ETacaGXAF8fGn1JvA== | 5711 | integrity sha512-XI4PI5L7GYcJyHkPcHlvPyRrYohNYBNRNbt1tU8PXNU3E1ADJC84a13V0vbL9AM431OP+ETacaGXAF8fGn1JvA== |
5655 | 5712 | ||
5656 | nodemailer@6.5.0, nodemailer@^6.0.0, nodemailer@^6.5.0: | 5713 | nodemailer@6.5.0: |
5657 | version "6.5.0" | 5714 | version "6.5.0" |
5658 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.5.0.tgz#d12c28d8d48778918e25f1999d97910231b175d9" | 5715 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.5.0.tgz#d12c28d8d48778918e25f1999d97910231b175d9" |
5659 | integrity sha512-Tm4RPrrIZbnqDKAvX+/4M+zovEReiKlEXWDzG4iwtpL9X34MJY+D5LnQPH/+eghe8DLlAVshHAJZAZWBGhkguw== | 5716 | integrity sha512-Tm4RPrrIZbnqDKAvX+/4M+zovEReiKlEXWDzG4iwtpL9X34MJY+D5LnQPH/+eghe8DLlAVshHAJZAZWBGhkguw== |
@@ -5663,6 +5720,11 @@ nodemailer@^3.1.1: | |||
5663 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-3.1.8.tgz#febfaccb4bd273678473a309c6cb4b4a2f3c48e3" | 5720 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-3.1.8.tgz#febfaccb4bd273678473a309c6cb4b4a2f3c48e3" |
5664 | integrity sha1-/r+sy0vSc2eEc6MJxstLSi88SOM= | 5721 | integrity sha1-/r+sy0vSc2eEc6MJxstLSi88SOM= |
5665 | 5722 | ||
5723 | nodemailer@^6.0.0, nodemailer@^6.5.0: | ||
5724 | version "6.6.0" | ||
5725 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.0.tgz#ed47bb572b48d9d0dca3913fdc156203f438f427" | ||
5726 | integrity sha512-ikSMDU1nZqpo2WUPE0wTTw/NGGImTkwpJKDIFPZT+YvvR9Sj+ze5wzu95JHkBMglQLoG2ITxU21WukCC/XsFkg== | ||
5727 | |||
5666 | nodemon@^2.0.1: | 5728 | nodemon@^2.0.1: |
5667 | version "2.0.7" | 5729 | version "2.0.7" |
5668 | resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32" | 5730 | resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32" |
@@ -5777,9 +5839,9 @@ object-hash@2.1.1: | |||
5777 | integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== | 5839 | integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== |
5778 | 5840 | ||
5779 | object-inspect@^1.9.0: | 5841 | object-inspect@^1.9.0: |
5780 | version "1.9.0" | 5842 | version "1.10.2" |
5781 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" | 5843 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" |
5782 | integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== | 5844 | integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== |
5783 | 5845 | ||
5784 | object-keys@^1.0.12, object-keys@^1.1.1: | 5846 | object-keys@^1.0.12, object-keys@^1.1.1: |
5785 | version "1.1.1" | 5847 | version "1.1.1" |
@@ -5896,9 +5958,9 @@ p-cancelable@^1.0.0: | |||
5896 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== | 5958 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== |
5897 | 5959 | ||
5898 | p-cancelable@^2.0.0: | 5960 | p-cancelable@^2.0.0: |
5899 | version "2.1.0" | 5961 | version "2.1.1" |
5900 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.0.tgz#4d51c3b91f483d02a0d300765321fca393d758dd" | 5962 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" |
5901 | integrity sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ== | 5963 | integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== |
5902 | 5964 | ||
5903 | p-finally@^1.0.0: | 5965 | p-finally@^1.0.0: |
5904 | version "1.0.0" | 5966 | version "1.0.0" |
@@ -6213,25 +6275,25 @@ pfeed@1.1.11: | |||
6213 | lodash "^4.17.15" | 6275 | lodash "^4.17.15" |
6214 | xml "^1.0.1" | 6276 | xml "^1.0.1" |
6215 | 6277 | ||
6216 | pg-connection-string@^2.4.0: | 6278 | pg-connection-string@^2.5.0: |
6217 | version "2.4.0" | 6279 | version "2.5.0" |
6218 | resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" | 6280 | resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" |
6219 | integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== | 6281 | integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== |
6220 | 6282 | ||
6221 | pg-int8@1.0.1: | 6283 | pg-int8@1.0.1: |
6222 | version "1.0.1" | 6284 | version "1.0.1" |
6223 | resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" | 6285 | resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" |
6224 | integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== | 6286 | integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== |
6225 | 6287 | ||
6226 | pg-pool@^3.2.2: | 6288 | pg-pool@^3.3.0: |
6227 | version "3.2.2" | 6289 | version "3.3.0" |
6228 | resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.2.tgz#a560e433443ed4ad946b84d774b3f22452694dff" | 6290 | resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.3.0.tgz#12d5c7f65ea18a6e99ca9811bd18129071e562fc" |
6229 | integrity sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA== | 6291 | integrity sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg== |
6230 | 6292 | ||
6231 | pg-protocol@^1.4.0: | 6293 | pg-protocol@^1.5.0: |
6232 | version "1.4.0" | 6294 | version "1.5.0" |
6233 | resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.4.0.tgz#43a71a92f6fe3ac559952555aa3335c8cb4908be" | 6295 | resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" |
6234 | integrity sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA== | 6296 | integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== |
6235 | 6297 | ||
6236 | pg-types@^2.1.0: | 6298 | pg-types@^2.1.0: |
6237 | version "2.2.0" | 6299 | version "2.2.0" |
@@ -6245,15 +6307,15 @@ pg-types@^2.1.0: | |||
6245 | postgres-interval "^1.1.0" | 6307 | postgres-interval "^1.1.0" |
6246 | 6308 | ||
6247 | pg@^8.2.1: | 6309 | pg@^8.2.1: |
6248 | version "8.5.1" | 6310 | version "8.6.0" |
6249 | resolved "https://registry.yarnpkg.com/pg/-/pg-8.5.1.tgz#34dcb15f6db4a29c702bf5031ef2e1e25a06a120" | 6311 | resolved "https://registry.yarnpkg.com/pg/-/pg-8.6.0.tgz#e222296b0b079b280cce106ea991703335487db2" |
6250 | integrity sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw== | 6312 | integrity sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ== |
6251 | dependencies: | 6313 | dependencies: |
6252 | buffer-writer "2.0.0" | 6314 | buffer-writer "2.0.0" |
6253 | packet-reader "1.0.0" | 6315 | packet-reader "1.0.0" |
6254 | pg-connection-string "^2.4.0" | 6316 | pg-connection-string "^2.5.0" |
6255 | pg-pool "^3.2.2" | 6317 | pg-pool "^3.3.0" |
6256 | pg-protocol "^1.4.0" | 6318 | pg-protocol "^1.5.0" |
6257 | pg-types "^2.1.0" | 6319 | pg-types "^2.1.0" |
6258 | pgpass "1.x" | 6320 | pgpass "1.x" |
6259 | 6321 | ||
@@ -6314,9 +6376,9 @@ pngjs@^3.0.0, pngjs@^3.3.3: | |||
6314 | integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== | 6376 | integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== |
6315 | 6377 | ||
6316 | postcss@^8.0.2: | 6378 | postcss@^8.0.2: |
6317 | version "8.2.10" | 6379 | version "8.2.13" |
6318 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b" | 6380 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.13.tgz#dbe043e26e3c068e45113b1ed6375d2d37e2129f" |
6319 | integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw== | 6381 | integrity sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ== |
6320 | dependencies: | 6382 | dependencies: |
6321 | colorette "^1.2.2" | 6383 | colorette "^1.2.2" |
6322 | nanoid "^3.1.22" | 6384 | nanoid "^3.1.22" |
@@ -6593,7 +6655,7 @@ qs@^6.9.4, qs@^6.9.6: | |||
6593 | dependencies: | 6655 | dependencies: |
6594 | side-channel "^1.0.4" | 6656 | side-channel "^1.0.4" |
6595 | 6657 | ||
6596 | queue-microtask@^1.2.0, queue-microtask@^1.2.2: | 6658 | queue-microtask@^1.2.0, queue-microtask@^1.2.2, queue-microtask@^1.2.3: |
6597 | version "1.2.3" | 6659 | version "1.2.3" |
6598 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" | 6660 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
6599 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== | 6661 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
@@ -6623,6 +6685,11 @@ random-access-storage@^1.1.1: | |||
6623 | dependencies: | 6685 | dependencies: |
6624 | inherits "^2.0.3" | 6686 | inherits "^2.0.3" |
6625 | 6687 | ||
6688 | random-bytes@~1.0.0: | ||
6689 | version "1.0.0" | ||
6690 | resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" | ||
6691 | integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= | ||
6692 | |||
6626 | random-iterate@^1.0.1: | 6693 | random-iterate@^1.0.1: |
6627 | version "1.0.1" | 6694 | version "1.0.1" |
6628 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" | 6695 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" |
@@ -6821,9 +6888,9 @@ redis-parser@^3.0.0: | |||
6821 | redis-errors "^1.0.0" | 6888 | redis-errors "^1.0.0" |
6822 | 6889 | ||
6823 | redis@^3.0.2: | 6890 | redis@^3.0.2: |
6824 | version "3.1.0" | 6891 | version "3.1.2" |
6825 | resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.0.tgz#39daec130d74b78decca93513c61db0af5d86ce6" | 6892 | resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c" |
6826 | integrity sha512-//lAOcEtNIKk2ekZibes5oyWKYUVWMvMB71lyD/hS9KRePNkB7AU3nXGkArX6uDKEb2N23EyJBthAv6pagD0uw== | 6893 | integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw== |
6827 | dependencies: | 6894 | dependencies: |
6828 | denque "^1.5.0" | 6895 | denque "^1.5.0" |
6829 | redis-commands "^1.7.0" | 6896 | redis-commands "^1.7.0" |
@@ -6891,9 +6958,9 @@ require-main-filename@^2.0.0: | |||
6891 | integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== | 6958 | integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== |
6892 | 6959 | ||
6893 | resolve-alpn@^1.0.0: | 6960 | resolve-alpn@^1.0.0: |
6894 | version "1.1.1" | 6961 | version "1.1.2" |
6895 | resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.1.tgz#4a006a7d533c81a5dd04681612090fde227cd6e1" | 6962 | resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.2.tgz#30b60cfbb0c0b8dc897940fe13fe255afcdd4d28" |
6896 | integrity sha512-0KbFjFPR2bnJhNx1t8Ad6RqVc8+QPJC4y561FYyC/Q/6OzB3fhUzB5PEgitYhPK6aifwR5gXBSnDMllaDWixGQ== | 6963 | integrity sha512-8OyfzhAtA32LVUsJSke3auIyINcwdh5l3cvYKdKO0nvsYSKuiLfTM5i78PJswFPT8y6cPW+L1v6/hE95chcpDA== |
6897 | 6964 | ||
6898 | resolve-from@^4.0.0: | 6965 | resolve-from@^4.0.0: |
6899 | version "4.0.0" | 6966 | version "4.0.0" |
@@ -7007,7 +7074,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: | |||
7007 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" | 7074 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" |
7008 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== | 7075 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== |
7009 | 7076 | ||
7010 | safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: | 7077 | safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: |
7011 | version "5.2.1" | 7078 | version "5.2.1" |
7012 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" | 7079 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" |
7013 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== | 7080 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== |
@@ -7153,6 +7220,11 @@ setprototypeof@1.1.1: | |||
7153 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" | 7220 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" |
7154 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== | 7221 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== |
7155 | 7222 | ||
7223 | setprototypeof@1.2.0: | ||
7224 | version "1.2.0" | ||
7225 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" | ||
7226 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== | ||
7227 | |||
7156 | shebang-command@^1.2.0: | 7228 | shebang-command@^1.2.0: |
7157 | version "1.2.0" | 7229 | version "1.2.0" |
7158 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" | 7230 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" |
@@ -7206,15 +7278,15 @@ simple-get@^4.0.0: | |||
7206 | simple-concat "^1.0.0" | 7278 | simple-concat "^1.0.0" |
7207 | 7279 | ||
7208 | simple-peer@^9.7.1, simple-peer@^9.9.3: | 7280 | simple-peer@^9.7.1, simple-peer@^9.9.3: |
7209 | version "9.10.0" | 7281 | version "9.11.0" |
7210 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.10.0.tgz#f458444300f635e6fcc2f5a5166c45d71eafb57f" | 7282 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571" |
7211 | integrity sha512-sKrKtca1UdmwdZIbvuT3iEL05tDGt/xdLP6+ej8rh1ADgtDk44yLaEZjIyPJ6c34zsSih46Ou7zUIT7e4hPK7g== | 7283 | integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg== |
7212 | dependencies: | 7284 | dependencies: |
7213 | buffer "^6.0.2" | 7285 | buffer "^6.0.3" |
7214 | debug "^4.2.0" | 7286 | debug "^4.3.1" |
7215 | err-code "^2.0.3" | 7287 | err-code "^3.0.1" |
7216 | get-browser-rtc "^1.0.2" | 7288 | get-browser-rtc "^1.1.0" |
7217 | queue-microtask "^1.2.0" | 7289 | queue-microtask "^1.2.3" |
7218 | randombytes "^2.1.0" | 7290 | randombytes "^2.1.0" |
7219 | readable-stream "^3.6.0" | 7291 | readable-stream "^3.6.0" |
7220 | 7292 | ||
@@ -7244,12 +7316,12 @@ simple-websocket@^9.0.0: | |||
7244 | readable-stream "^3.6.0" | 7316 | readable-stream "^3.6.0" |
7245 | ws "^7.4.2" | 7317 | ws "^7.4.2" |
7246 | 7318 | ||
7247 | sitemap@^6.1.0: | 7319 | sitemap@^7.0.0: |
7248 | version "6.4.0" | 7320 | version "7.0.0" |
7249 | resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-6.4.0.tgz#b4bc4edf36de742405a7572bc3e467ba484b852e" | 7321 | resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.0.0.tgz#022bef4df8cba42e38e1fe77039f234cab0372b6" |
7250 | integrity sha512-DoPKNc2/apQZTUnfiOONWctwq7s6dZVspxAZe2VPMNtoqNq7HgXRvlRnbIpKjf+8+piQdWncwcy+YhhTGY5USQ== | 7322 | integrity sha512-Ud0jrRQO2k7fEtPAM+cQkBKoMvxQyPKNXKDLn8tRVHxRCsdDQ2JZvw+aZ5IRYYQVAV9iGxEar6boTwZzev+x3g== |
7251 | dependencies: | 7323 | dependencies: |
7252 | "@types/node" "^14.14.28" | 7324 | "@types/node" "^15.0.1" |
7253 | "@types/sax" "^1.2.1" | 7325 | "@types/sax" "^1.2.1" |
7254 | arg "^5.0.0" | 7326 | arg "^5.0.0" |
7255 | sax "^1.2.4" | 7327 | sax "^1.2.4" |
@@ -7688,6 +7760,13 @@ strip-json-comments@~2.0.1: | |||
7688 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" | 7760 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" |
7689 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= | 7761 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= |
7690 | 7762 | ||
7763 | subarg@^1.0.0: | ||
7764 | version "1.0.0" | ||
7765 | resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" | ||
7766 | integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI= | ||
7767 | dependencies: | ||
7768 | minimist "^1.1.0" | ||
7769 | |||
7691 | superagent@^6.1.0: | 7770 | superagent@^6.1.0: |
7692 | version "6.1.0" | 7771 | version "6.1.0" |
7693 | resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" | 7772 | resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" |
@@ -7749,19 +7828,17 @@ swagger-cli@^4.0.2: | |||
7749 | "@apidevtools/swagger-cli" "4.0.4" | 7828 | "@apidevtools/swagger-cli" "4.0.4" |
7750 | 7829 | ||
7751 | table@^6.0.4: | 7830 | table@^6.0.4: |
7752 | version "6.0.9" | 7831 | version "6.6.0" |
7753 | resolved "https://registry.yarnpkg.com/table/-/table-6.0.9.tgz#790a12bf1e09b87b30e60419bafd6a1fd85536fb" | 7832 | resolved "https://registry.yarnpkg.com/table/-/table-6.6.0.tgz#905654b79df98d9e9a973de1dd58682532c40e8e" |
7754 | integrity sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ== | 7833 | integrity sha512-iZMtp5tUvcnAdtHpZTWLPF0M7AgiQsURR2DwmxnJwSy8I3+cY+ozzVvYha3BOLG2TB+L0CqjIz+91htuj6yCXg== |
7755 | dependencies: | 7834 | dependencies: |
7756 | ajv "^8.0.1" | 7835 | ajv "^8.0.1" |
7757 | is-boolean-object "^1.1.0" | ||
7758 | is-number-object "^1.0.4" | ||
7759 | is-string "^1.0.5" | ||
7760 | lodash.clonedeep "^4.5.0" | 7836 | lodash.clonedeep "^4.5.0" |
7761 | lodash.flatten "^4.4.0" | 7837 | lodash.flatten "^4.4.0" |
7762 | lodash.truncate "^4.4.2" | 7838 | lodash.truncate "^4.4.2" |
7763 | slice-ansi "^4.0.0" | 7839 | slice-ansi "^4.0.0" |
7764 | string-width "^4.2.0" | 7840 | string-width "^4.2.0" |
7841 | strip-ansi "^6.0.0" | ||
7765 | 7842 | ||
7766 | tar@^6.1.0: | 7843 | tar@^6.1.0: |
7767 | version "6.1.0" | 7844 | version "6.1.0" |
@@ -7862,11 +7939,16 @@ titleize@^2.1.0: | |||
7862 | resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f" | 7939 | resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f" |
7863 | integrity sha512-m+apkYlfiQTKLW+sI4vqUkwMEzfgEUEYSqljx1voUE3Wz/z1ZsxyzSxvH2X8uKVrOp7QkByWt0rA6+gvhCKy6g== | 7940 | integrity sha512-m+apkYlfiQTKLW+sI4vqUkwMEzfgEUEYSqljx1voUE3Wz/z1ZsxyzSxvH2X8uKVrOp7QkByWt0rA6+gvhCKy6g== |
7864 | 7941 | ||
7865 | tlds@1.219.0, tlds@^1.218.0: | 7942 | tlds@1.219.0: |
7866 | version "1.219.0" | 7943 | version "1.219.0" |
7867 | resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" | 7944 | resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" |
7868 | integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== | 7945 | integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== |
7869 | 7946 | ||
7947 | tlds@^1.218.0: | ||
7948 | version "1.221.1" | ||
7949 | resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.221.1.tgz#6cf6bff5eaf30c5618c5801c3f425a6dc61ca0ad" | ||
7950 | integrity sha512-N1Afn/SLeOQRpxMwHBuNFJ3GvGrdtY4XPXKPFcx8he0U9Jg9ZkvTKE1k3jQDtCmlFn44UxjVtouF6PT4rEGd3Q== | ||
7951 | |||
7870 | tmp@0.0.x, tmp@^0.0.33: | 7952 | tmp@0.0.x, tmp@^0.0.33: |
7871 | version "0.0.33" | 7953 | version "0.0.33" |
7872 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" | 7954 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" |
@@ -8096,6 +8178,13 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: | |||
8096 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" | 8178 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" |
8097 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== | 8179 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== |
8098 | 8180 | ||
8181 | uid-safe@2.1.5: | ||
8182 | version "2.1.5" | ||
8183 | resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" | ||
8184 | integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== | ||
8185 | dependencies: | ||
8186 | random-bytes "~1.0.0" | ||
8187 | |||
8099 | uint64be@^2.0.2: | 8188 | uint64be@^2.0.2: |
8100 | version "2.0.2" | 8189 | version "2.0.2" |
8101 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" | 8190 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" |
@@ -8218,9 +8307,9 @@ ut_pex@^2.0.1: | |||
8218 | string2compact "^1.2.5" | 8307 | string2compact "^1.2.5" |
8219 | 8308 | ||
8220 | utf-8-validate@^5.0.2: | 8309 | utf-8-validate@^5.0.2: |
8221 | version "5.0.4" | 8310 | version "5.0.5" |
8222 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.4.tgz#72a1735983ddf7a05a43a9c6b67c5ce1c910f9b8" | 8311 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.5.tgz#dd32c2e82c72002dc9f02eb67ba6761f43456ca1" |
8223 | integrity sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q== | 8312 | integrity sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ== |
8224 | dependencies: | 8313 | dependencies: |
8225 | node-gyp-build "^4.2.0" | 8314 | node-gyp-build "^4.2.0" |
8226 | 8315 | ||
@@ -8327,9 +8416,9 @@ validator@^12.0.0: | |||
8327 | integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ== | 8416 | integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ== |
8328 | 8417 | ||
8329 | validator@^13.0.0, validator@^13.5.2: | 8418 | validator@^13.0.0, validator@^13.5.2: |
8330 | version "13.5.2" | 8419 | version "13.6.0" |
8331 | resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46" | 8420 | resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" |
8332 | integrity sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ== | 8421 | integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg== |
8333 | 8422 | ||
8334 | vary@^1, vary@~1.1.2: | 8423 | vary@^1, vary@~1.1.2: |
8335 | version "1.1.2" | 8424 | version "1.1.2" |
@@ -8388,10 +8477,10 @@ webfinger.js@^2.6.6: | |||
8388 | dependencies: | 8477 | dependencies: |
8389 | xhr2 "^0.1.4" | 8478 | xhr2 "^0.1.4" |
8390 | 8479 | ||
8391 | webtorrent@^0.116.1: | 8480 | webtorrent@^0.118.0: |
8392 | version "0.116.1" | 8481 | version "0.118.0" |
8393 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.116.1.tgz#db8e884e9ecfd5775dcadfec01bccd3b5e57b2af" | 8482 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.118.0.tgz#9e0a75a2e270e27a818cb7395b53c89fa7532c6f" |
8394 | integrity sha512-xCmA9U8RviUbGD2Gv8pAPEGaPzGw8ZXseuUb5bbNrTg7zseRw4SrRvhPM17ri3yKN7+jWPeDvVXPPsNY9scFHw== | 8483 | integrity sha512-xXwwM2P+vtDsMRx9eRPNQqHD+6E7Zz7OTZqWAr2XDXg3TWGCf9HmwpgV53+F9H0oqw+l4j7vR9DRjAjChPQpZA== |
8395 | dependencies: | 8484 | dependencies: |
8396 | addr-to-ip-port "^1.5.1" | 8485 | addr-to-ip-port "^1.5.1" |
8397 | bitfield "^4.0.0" | 8486 | bitfield "^4.0.0" |
@@ -8408,6 +8497,7 @@ webtorrent@^0.116.1: | |||
8408 | http-node "github:feross/http-node#webtorrent" | 8497 | http-node "github:feross/http-node#webtorrent" |
8409 | immediate-chunk-store "^2.1.1" | 8498 | immediate-chunk-store "^2.1.1" |
8410 | load-ip-set "^2.1.2" | 8499 | load-ip-set "^2.1.2" |
8500 | lt_donthave "^1.0.1" | ||
8411 | memory-chunk-store "^1.3.1" | 8501 | memory-chunk-store "^1.3.1" |
8412 | mime "^2.5.0" | 8502 | mime "^2.5.0" |
8413 | multistream "^4.1.0" | 8503 | multistream "^4.1.0" |
@@ -8598,9 +8688,9 @@ ws@^5.2.2: | |||
8598 | async-limiter "~1.0.0" | 8688 | async-limiter "~1.0.0" |
8599 | 8689 | ||
8600 | ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: | 8690 | ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: |
8601 | version "7.4.4" | 8691 | version "7.4.5" |
8602 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" | 8692 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" |
8603 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== | 8693 | integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== |
8604 | 8694 | ||
8605 | ws@~6.1.0: | 8695 | ws@~6.1.0: |
8606 | version "6.1.4" | 8696 | version "6.1.4" |