aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/test.yml2
-rw-r--r--CHANGELOG.md123
-rw-r--r--client/.stylelintrc.json8
-rw-r--r--client/e2e/src/po/video-upload.po.ts7
-rw-r--r--client/package.json1
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts8
-rw-r--r--client/src/app/+accounts/accounts.component.ts2
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.ts4
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts6
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts8
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html9
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts16
-rw-r--r--client/src/app/+search/search-filters.component.html27
-rw-r--r--client/src/app/+search/search-filters.component.ts40
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss119
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts48
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html20
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts295
-rw-r--r--client/src/app/+videos/+video-edit/video-add.module.ts5
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts18
-rw-r--r--client/src/app/app.component.scss4
-rw-r--r--client/src/app/helpers/utils.ts48
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts6
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts2
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.scss2
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts19
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts16
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss2
-rw-r--r--client/src/sass/application.scss21
-rw-r--r--client/src/sass/include/_mixins.scss49
-rw-r--r--client/src/sass/player/context-menu.scss1
-rw-r--r--client/src/sass/player/peertube-skin.scss2
-rw-r--r--client/src/sass/player/playlist.scss8
-rw-r--r--client/yarn.lock7
-rw-r--r--engines.yaml5
-rw-r--r--package.json20
-rwxr-xr-xscripts/ci.sh2
-rw-r--r--server.ts2
-rw-r--r--server/controllers/api/server/debug.ts18
-rw-r--r--server/controllers/api/videos/index.ts108
-rw-r--r--server/controllers/feeds.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts39
-rw-r--r--server/helpers/custom-validators/misc.ts5
-rw-r--r--server/helpers/custom-validators/videos.ts9
-rw-r--r--server/helpers/database-utils.ts18
-rw-r--r--server/helpers/express-utils.ts8
-rw-r--r--server/helpers/ffmpeg-utils.ts10
-rw-r--r--server/helpers/upload.ts21
-rw-r--r--server/helpers/utils.ts4
-rw-r--r--server/initializers/constants.ts8
-rw-r--r--server/initializers/installer.ts5
-rw-r--r--server/initializers/migrations/0645-actor-remote-creation-date.ts26
-rw-r--r--server/lib/activitypub/actor.ts2
-rw-r--r--server/lib/activitypub/videos.ts3
-rw-r--r--server/lib/hls.ts11
-rw-r--r--server/lib/moderation.ts5
-rw-r--r--server/lib/plugins/plugin-helpers-builder.ts8
-rw-r--r--server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts61
-rw-r--r--server/lib/video.ts3
-rw-r--r--server/middlewares/validators/videos/videos.ts184
-rw-r--r--server/models/account/account.ts1
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/activitypub/actor.ts16
-rw-r--r--server/models/video/video-channel.ts8
-rw-r--r--server/models/video/video.ts7
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/upload-quota.ts152
-rw-r--r--server/tests/api/check-params/users.ts105
-rw-r--r--server/tests/api/check-params/videos.ts393
-rw-r--r--server/tests/api/live/live-constraints.ts70
-rw-r--r--server/tests/api/live/live-permanent.ts21
-rw-r--r--server/tests/api/live/live-save-replay.ts11
-rw-r--r--server/tests/api/server/follow-constraints.ts2
-rw-r--r--server/tests/api/users/users-multiple-servers.ts28
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/multiple-servers.ts2
-rw-r--r--server/tests/api/videos/resumable-upload.ts187
-rw-r--r--server/tests/api/videos/single-server.ts724
-rw-r--r--server/tests/api/videos/video-channels.ts82
-rw-r--r--server/tests/api/videos/video-transcoder.ts159
-rw-r--r--server/tests/fixtures/peertube-plugin-test-four/main.js5
-rw-r--r--server/tests/plugins/filter-hooks.ts2
-rw-r--r--server/tests/plugins/plugin-helpers.ts1
-rw-r--r--server/tools/peertube-plugins.ts8
-rw-r--r--server/types/models/account/actor.ts2
-rw-r--r--server/types/plugins/register-server-option.model.ts4
-rw-r--r--server/typings/express/index.d.ts140
-rw-r--r--shared/core-utils/miscs/http-methods.ts21
-rw-r--r--shared/core-utils/miscs/index.ts1
-rw-r--r--shared/extra-utils/server/config.ts15
-rw-r--r--shared/extra-utils/server/debug.ts18
-rw-r--r--shared/extra-utils/server/jobs.ts4
-rw-r--r--shared/extra-utils/server/servers.ts2
-rw-r--r--shared/extra-utils/users/users.ts23
-rw-r--r--shared/extra-utils/videos/video-channels.ts11
-rw-r--r--shared/extra-utils/videos/videos.ts258
-rw-r--r--shared/models/activitypub/activitypub-actor.ts2
-rw-r--r--shared/models/actors/account.model.ts2
-rw-r--r--shared/models/actors/actor.model.ts1
-rw-r--r--shared/models/search/boolean-both-query.model.ts1
-rw-r--r--shared/models/server/debug.model.ts4
-rw-r--r--shared/models/videos/channel/video-channel.model.ts3
-rw-r--r--support/doc/api/openapi.yaml920
-rw-r--r--support/doc/dependencies.md2
-rw-r--r--support/doc/plugins/guide.md4
-rw-r--r--support/docker/production/Dockerfile.buster2
-rw-r--r--support/nginx/peertube9
-rw-r--r--yarn.lock608
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'
2import { HttpErrorResponse } from '@angular/common/http' 2import { HttpErrorResponse } from '@angular/common/http'
3import { AfterViewChecked, Component, OnInit } from '@angular/core' 3import { AfterViewChecked, Component, OnInit } from '@angular/core'
4import { AuthService, Notifier, User, UserService } from '@app/core' 4import { AuthService, Notifier, User, UserService } from '@app/core'
5import { uploadErrorHandler } from '@app/helpers' 5import { 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'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { uploadErrorHandler } from '@app/helpers' 6import { genericUploadErrorHandler } from '@app/helpers'
7import { 7import {
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 @@
1import { switchMap } from 'rxjs/operators' 1import { SelectChannelItem } from 'src/types/select-options-item.model'
2import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { listUserChannels } from '@app/helpers'
4import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' 5import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
6import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' 7import { VideoOwnershipService } from '@app/shared/shared-main'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { VideoChangeOwnership, VideoChannel } from '@shared/models' 9import { 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'
3import { AdvancedSearch } from '@app/shared/shared-search' 3import { AdvancedSearch } from '@app/shared/shared-search'
4import { ServerConfig, VideoConstant } from '@shared/models' 4import { ServerConfig, VideoConstant } from '@shared/models'
5 5
6type 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 @@
1import { objectToFormData } from '@app/helpers'
2import { 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 */
15export 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 @@
1import { Subscription } from 'rxjs'
2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx'
4import { UploaderXFormData } from './uploaderx-form-data'
5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop, uploadErrorHandler } from '@app/helpers' 6import { scrollToTop, genericUploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
11import { VideoPrivacy } from '@shared/models' 11import { VideoPrivacy } from '@shared/models'
12import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
13import { 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})
23export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { 24export 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})`
289video 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 (
309video 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 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { CanDeactivateGuard } from '@app/core' 2import { CanDeactivateGuard } from '@app/core'
3import { UploadxModule } from 'ngx-uploadx'
3import { VideoEditModule } from './shared/video-edit.module' 4import { VideoEditModule } from './shared/video-edit.module'
4import { DragDropDirective } from './video-add-components/drag-drop.directive' 5import { DragDropDirective } from './video-add-components/drag-drop.directive'
5import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' 6import { 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'
2import { map, switchMap } from 'rxjs/operators' 2import { map, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, Resolve } from '@angular/router' 4import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
5import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main' 5import { AuthService } from '@app/core'
6import { listUserChannels } from '@app/helpers'
7import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
6import { LiveVideoService } from '@app/shared/shared-video-live' 8import { 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 @@
1import { map } from 'rxjs/operators' 1import { first, map } from 'rxjs/operators'
2import { SelectChannelItem } from 'src/types/select-options-item.model' 2import { SelectChannelItem } from 'src/types/select-options-item.model'
3import { DatePipe } from '@angular/common' 3import { DatePipe } from '@angular/common'
4import { HttpErrorResponse } from '@angular/common/http' 4import { HttpErrorResponse } from '@angular/common/http'
@@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) {
23 23
24function listUserChannels (authService: AuthService) { 24function 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
42function getAbsoluteAPIUrl () { 51function getAbsoluteAPIUrl () {
@@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
167 ) 176 )
168} 177}
169 178
170function uploadErrorHandler (parameters: { 179function 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'
4export class Account extends Actor implements ServerAccount { 4export 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 @@
1import { BooleanBothQuery, SearchTargetType } from '@shared/models' 1import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models'
2 2
3export class AdvancedSearch { 3export 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
7796ngx-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
7796nice-try@^1.0.4: 7803nice-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 @@
1node: ">=12.x"
2yarn: ">=1.x"
3postgres: ">=10.x"
4redis-server: ">=2.8.18"
5ffmpeg: ">=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
48elif [ "$1" = "cli" ]; then 48elif [ "$1" = "cli" ]; then
49 npm run build:server 49 npm run build:server
50 npm run setup:cli 50 npm run setup:cli
diff --git a/server.ts b/server.ts
index 2531080a3..97dffe756 100644
--- a/server.ts
+++ b/server.ts
@@ -116,6 +116,7 @@ import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-upd
116import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' 116import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
117import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler' 117import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler'
118import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances' 118import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances'
119import { RemoveDanglingResumableUploadsScheduler } from './server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
119import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' 120import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
120import { PeerTubeSocket } from './server/lib/peertube-socket' 121import { PeerTubeSocket } from './server/lib/peertube-socket'
121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' 122import { 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 @@
1import { InboxManager } from '@server/lib/activitypub/inbox-manager' 1import { InboxManager } from '@server/lib/activitypub/inbox-manager'
2import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
3import { SendDebugCommand } from '@shared/models'
2import * as express from 'express' 4import * as express from 'express'
3import { UserRight } from '../../../../shared/models/users' 5import { UserRight } from '../../../../shared/models/users'
4import { authenticate, ensureUserHasRight } from '../../../middlewares' 6import { authenticate, ensureUserHasRight } from '../../../middlewares'
@@ -11,6 +13,12 @@ debugRouter.get('/debug',
11 getDebug 13 getDebug
12) 14)
13 15
16debugRouter.post('/debug/run-command',
17 authenticate,
18 ensureUserHasRight(UserRight.MANAGE_DEBUG),
19 runCommand
20)
21
14// --------------------------------------------------------------------------- 22// ---------------------------------------------------------------------------
15 23
16export { 24export {
@@ -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
37async 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'
2import { move } from 'fs-extra' 2import { move } from 'fs-extra'
3import { extname } from 'path' 3import { extname } from 'path'
4import toInt from 'validator/lib/toInt' 4import toInt from 'validator/lib/toInt'
5import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { changeVideoChannelShare } from '@server/lib/activitypub/share' 7import { changeVideoChannelShare } from '@server/lib/activitypub/share'
7import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
@@ -10,8 +11,9 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
10import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 11import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
11import { getServerActor } from '@server/models/application/application' 12import { getServerActor } from '@server/models/application/application'
12import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 13import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
14import { uploadx } from '@uploadx/core'
13import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' 15import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
14import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 16import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
15import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 17import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
16import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 18import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
17import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 19import { 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'
69const lTags = loggerTagsFactory('api', 'video') 73const lTags = loggerTagsFactory('api', 'video')
70const auditLogger = auditLoggerFactory('videos') 74const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 75const videosRouter = express.Router()
76const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
72 77
73const reqVideoFileAdd = createReqFiles( 78const 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
88const reqVideoFileAddResumable = createReqFiles(
89 [ 'thumbnailfile', 'previewfile' ],
90 MIMETYPES.IMAGE.MIMETYPE_EXT,
91 {
92 thumbnailfile: getResumableUploadPath(),
93 previewfile: getResumableUploadPath()
94 }
95)
96
82const reqVideoFileUpdate = createReqFiles( 97const 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
130videosRouter.post('/upload',
131 authenticate,
132 reqVideoFileAdd,
133 asyncMiddleware(videosAddLegacyValidator),
134 asyncRetryTransactionMiddleware(addVideoLegacy)
135)
136
137videosRouter.post('/upload-resumable',
138 authenticate,
139 reqVideoFileAddResumable,
140 asyncMiddleware(videosAddResumableInitValidator),
141 uploadxMiddleware
142)
143
144videosRouter.delete('/upload-resumable',
145 authenticate,
146 uploadxMiddleware
147)
148
149videosRouter.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
114videosRouter.put('/:id', 156videosRouter.put('/:id',
115 authenticate, 157 authenticate,
116 reqVideoFileUpdate, 158 reqVideoFileUpdate,
117 asyncMiddleware(videosUpdateValidator), 159 asyncMiddleware(videosUpdateValidator),
118 asyncRetryTransactionMiddleware(updateVideo) 160 asyncRetryTransactionMiddleware(updateVideo)
119) 161)
120videosRouter.post('/upload',
121 authenticate,
122 reqVideoFileAdd,
123 asyncMiddleware(videosAddValidator),
124 asyncRetryTransactionMiddleware(addVideo)
125)
126 162
127videosRouter.get('/:id/description', 163videosRouter.get('/:id/description',
128 asyncMiddleware(videosGetValidator), 164 asyncMiddleware(videosGetValidator),
@@ -157,23 +193,23 @@ export {
157 193
158// --------------------------------------------------------------------------- 194// ---------------------------------------------------------------------------
159 195
160function listVideoCategories (req: express.Request, res: express.Response) { 196function listVideoCategories (_req: express.Request, res: express.Response) {
161 res.json(VIDEO_CATEGORIES) 197 res.json(VIDEO_CATEGORIES)
162} 198}
163 199
164function listVideoLicences (req: express.Request, res: express.Response) { 200function listVideoLicences (_req: express.Request, res: express.Response) {
165 res.json(VIDEO_LICENCES) 201 res.json(VIDEO_LICENCES)
166} 202}
167 203
168function listVideoLanguages (req: express.Request, res: express.Response) { 204function listVideoLanguages (_req: express.Request, res: express.Response) {
169 res.json(VIDEO_LANGUAGES) 205 res.json(VIDEO_LANGUAGES)
170} 206}
171 207
172function listVideoPrivacies (req: express.Request, res: express.Response) { 208function listVideoPrivacies (_req: express.Request, res: express.Response) {
173 res.json(VIDEO_PRIVACIES) 209 res.json(VIDEO_PRIVACIES)
174} 210}
175 211
176async function addVideo (req: express.Request, res: express.Response) { 212async 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) 227async 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
238async 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 @@
1import validator from 'validator' 1import validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { exists, isArray } from '../misc' 3import { exists, isArray, isDateValid } from '../misc'
4import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 4import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
5import { isHostValid } from '../servers' 5import { isHostValid } from '../servers'
6import { peertubeTruncate } from '@server/helpers/core-utils' 6import { 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
50function isActorObjectValid (actor: any) { 50function isActorFollowingCountValid (value: string) {
51 return exists(value) && validator.isInt('' + value, { min: 0 })
52}
53
54function isActorFollowersCountValid (value: string) {
55 return exists(value) && validator.isInt('' + value, { min: 0 })
56}
57
58function isActorDeleteActivityValid (activity: any) {
59 return isBaseActivityValid(activity, 'Delete')
60}
61
62function 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
71function isActorFollowingCountValid (value: string) {
72 return exists(value) && validator.isInt('' + value, { min: 0 })
73}
74
75function isActorFollowersCountValid (value: string) {
76 return exists(value) && validator.isInt('' + value, { min: 0 })
77}
78
79function isActorDeleteActivityValid (activity: any) {
80 return isBaseActivityValid(activity, 'Delete')
81}
82
83function sanitizeAndCheckActorObject (object: any) {
84 normalizeActor(object)
85
86 return isActorObjectValid(object)
87}
88
89function normalizeActor (actor: any) { 85function 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 @@
1import 'multer' 1import 'multer'
2import validator from 'validator' 2import { UploadFilesForCheck } from 'express'
3import { sep } from 'path' 3import { sep } from 'path'
4import validator from 'validator'
4 5
5function exists (value: any) { 6function 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
110function isFileMimeTypeValid ( 111function 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 @@
1import { UploadFilesForCheck } from 'express'
1import { values } from 'lodash' 2import { values } from 'lodash'
3import * as magnetUtil from 'magnet-uri'
2import validator from 'validator' 4import validator from 'validator'
3import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' 5import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
4import { 6import {
@@ -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'
14import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' 16import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc'
15import * as magnetUtil from 'magnet-uri'
16 17
17const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 18const 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
84function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 85function 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 @@
1import * as retry from 'async/retry' 1import * as retry from 'async/retry'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { QueryTypes, Transaction } from 'sequelize'
3import { Model } from 'sequelize-typescript' 4import { Model } from 'sequelize-typescript'
5import { sequelizeTypescript } from '@server/initializers/database'
4import { logger } from './logger' 6import { logger } from './logger'
5import { Transaction } from 'sequelize'
6 7
7function retryTransactionWrapper <T, A, B, C, D> ( 8function 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
101function 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
101export { 114export {
@@ -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'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { REMOTE_SCHEME } from '../initializers/constants' 3import { REMOTE_SCHEME } from '../initializers/constants'
4import { logger } from './logger' 4import { logger } from './logger'
5import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAndCatch, generateRandomString } from './utils'
6import { extname } from 'path' 6import { extname } from 'path'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { CONFIG } from '../initializers/config' 8import { 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 @@
1import { METAFILE_EXTNAME } from '@uploadx/core'
2import { remove } from 'fs-extra'
3import { join } from 'path'
4import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants'
5
6function getResumableUploadPath (filename?: string) {
7 if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename)
8
9 return RESUMABLE_UPLOAD_DIRECTORY
10}
11
12function deleteResumableUploadMetaFile (filepath: string) {
13 return remove(filepath + METAFILE_EXTNAME)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
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'
6import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' 6import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils'
7import { logger } from './logger' 7import { logger } from './logger'
8 8
9function deleteFileAsync (path: string) { 9function 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
85export { 85export {
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
27const LAST_MIGRATION_VERSION = 640 27const 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
650const RESUMABLE_UPLOAD_DIRECTORY = join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads')
648const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') 651const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls')
649const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') 652const 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'
6import { ApplicationModel } from '../models/application/application' 6import { ApplicationModel } from '../models/application/application'
7import { OAuthClientModel } from '../models/oauth/oauth-client' 7import { OAuthClientModel } from '../models/oauth/oauth-client'
8import { applicationExist, clientsExist, usersExist } from './checker-after-init' 8import { applicationExist, clientsExist, usersExist } from './checker-after-init'
9import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' 9import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants'
10import { sequelizeTypescript } from './database' 10import { sequelizeTypescript } from './database'
11import { ensureDir, remove } from 'fs-extra' 11import { ensureDir, remove } from 'fs-extra'
12import { CONFIG } from './config' 12import { 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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
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 @@
1import { VideoUploadFile } from 'express'
1import { PathLike } from 'fs-extra' 2import { PathLike } from 'fs-extra'
2import { Transaction } from 'sequelize/types' 3import { Transaction } from 'sequelize/types'
3import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' 4import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
5import { afterCommitIfTransaction } from '@server/helpers/database-utils'
4import { logger } from '@server/helpers/logger' 6import { logger } from '@server/helpers/logger'
5import { AbuseModel } from '@server/models/abuse/abuse' 7import { AbuseModel } from '@server/models/abuse/abuse'
6import { VideoAbuseModel } from '@server/models/abuse/video-abuse' 8import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
@@ -28,7 +30,6 @@ import { VideoModel } from '../models/video/video'
28import { VideoCommentModel } from '../models/video/video-comment' 30import { VideoCommentModel } from '../models/video/video-comment'
29import { sendAbuse } from './activitypub/send/send-flag' 31import { sendAbuse } from './activitypub/send/send-flag'
30import { Notifier } from './notifier' 32import { Notifier } from './notifier'
31import { afterCommitIfTransaction } from '@server/helpers/database-utils'
32 33
33export type AcceptResult = { 34export 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
39function isLocalVideoAccepted (object: { 40function 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'
17import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' 17import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
18import { getServerConfig } from '../config' 18import { getServerConfig } from '../config'
19import { blacklistVideo, unblacklistVideo } from '../video-blacklist' 19import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
20import { UserModel } from '@server/models/account/user'
20 21
21function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { 22function 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
164function buildUserHelpers () { 165function 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 @@
1import * as bluebird from 'bluebird'
2import { readdir, remove, stat } from 'fs-extra'
3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
6import { METAFILE_EXTNAME } from '@uploadx/core'
7import { AbstractScheduler } from './abstract-scheduler'
8
9const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
10
11export 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 @@
1import { UploadFiles } from 'express'
1import { Transaction } from 'sequelize/types' 2import { Transaction } from 'sequelize/types'
2import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' 3import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants'
3import { sequelizeTypescript } from '@server/initializers/database' 4import { sequelizeTypescript } from '@server/initializers/database'
@@ -32,7 +33,7 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil
32 33
33async function buildVideoThumbnailsFromReq (options: { 34async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator' 2import { body, header, param, query, ValidationChain } from 'express-validator'
3import { getResumableUploadPath } from '@server/helpers/upload'
3import { isAbleToUploadVideo } from '@server/lib/user' 4import { isAbleToUploadVideo } from '@server/lib/user'
4import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
5import { ExpressPromiseHandler } from '@server/types/express' 6import { ExpressPromiseHandler } from '@server/types/express'
6import { MVideoWithRights } from '@server/types/models' 7import { MUserAccountId, MVideoWithRights } from '@server/types/models'
7import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' 8import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' 10import { 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'
51import { deleteFileAndCatch } from '../../../helpers/utils'
50import { getVideoWithAttributes } from '../../../helpers/video' 52import { getVideoWithAttributes } from '../../../helpers/video'
51import { CONFIG } from '../../../initializers/config' 53import { CONFIG } from '../../../initializers/config'
52import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' 54import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
@@ -57,7 +59,7 @@ import { VideoModel } from '../../../models/video/video'
57import { authenticatePromiseIfNeeded } from '../../auth' 59import { authenticatePromiseIfNeeded } from '../../auth'
58import { areValidationErrors } from '../utils' 60import { areValidationErrors } from '../utils'
59 61
60const videosAddValidator = getCommonVideoEditAttributes().concat([ 62const 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 */
105const 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 */
142const 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
480export { 545export {
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
518async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) { 586async 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
626export 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
654async 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 @@
1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize'
2import { 2import {
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'
20import { setAsUpdated } from '@server/helpers/database-utils'
20import { MAccountActor } from '@server/types/models' 21import { MAccountActor } from '@server/types/models'
21import { ActivityPubActor } from '../../../shared/models/activitypub' 22import { ActivityPubActor } from '../../../shared/models/activitypub'
22import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' 23import { 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'
27import { setAsUpdated } from '@server/helpers/database-utils'
27import { buildNSFWFilter } from '@server/helpers/express-utils' 28import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
29import { LiveManager } from '@server/lib/live-manager' 30import { 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'
13import './redundancy' 13import './redundancy'
14import './search' 14import './search'
15import './services' 15import './services'
16import './upload-quota'
16import './user-notifications' 17import './user-notifications'
17import './user-subscriptions' 18import './user-subscriptions'
18import './users' 19import './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
3import 'mocha'
4import { expect } from 'chai'
5import { HttpStatusCode, randomInt } from '@shared/core-utils'
6import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '@shared/extra-utils/videos/video-imports'
7import { MyUser, VideoImport, VideoImportState, VideoPrivacy } from '@shared/models'
8import {
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
23describe('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
3import 'mocha' 3import 'mocha'
4import { expect } from 'chai'
5import { omit } from 'lodash' 4import { omit } from 'lodash'
6import { join } from 'path' 5import { join } from 'path'
7import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared' 6import { User, UserRole } from '../../../../shared'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
8import { 8import {
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'
42import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
43import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
44import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 41import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
45import { VideoPrivacy } from '../../../../shared/models/videos'
46import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
47 42
48describe('Test users API validators', function () { 43describe('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
3import 'mocha'
3import * as chai from 'chai' 4import * as chai from 'chai'
4import { omit } from 'lodash' 5import { omit } from 'lodash'
5import 'mocha'
6import { join } from 'path' 6import { join } from 'path'
7import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
8import { 8import {
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'
26import { 27import {
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'
31import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 32import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
33import { randomInt } from '@shared/core-utils'
32 34
33const expect = chai.expect 35const 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
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { User, VideoDetails, VideoPrivacy } from '@shared/models' 5import { VideoDetails, VideoPrivacy } from '@shared/models'
6import { 6import {
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 @@
1import './audio-only' 1import './audio-only'
2import './multiple-servers' 2import './multiple-servers'
3import './resumable-upload'
3import './single-server' 4import './single-server'
4import './video-captions' 5import './video-captions'
5import './video-change-ownership' 6import './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
3import 'mocha'
4import * as chai from 'chai'
5import { pathExists, readdir, stat } from 'fs-extra'
6import { join } from 'path'
7import { HttpStatusCode } from '@shared/core-utils'
8import {
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'
21import { MyUser, VideoPrivacy } from '@shared/models'
22
23const expect = chai.expect
24
25// Most classic resumable upload tests are done in other test suites
26
27describe('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
3import 'mocha'
3import * as chai from 'chai' 4import * as chai from 'chai'
4import { keyBy } from 'lodash' 5import { keyBy } from 'lodash'
5import 'mocha' 6
6import { VideoPrivacy } from '../../../../shared/models/videos'
7import { 7import {
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'
31import { VideoPrivacy } from '../../../../shared/models/videos'
32import { HttpStatusCode } from '@shared/core-utils'
31 33
32const expect = chai.expect 34const expect = chai.expect
33 35
34describe('Test a single server', function () { 36describe('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 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { basename } from 'path' 5import { basename } from 'path'
6import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
6import { 7import {
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'
34import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 36import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
35import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' 37import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
36import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
37 38
38const expect = chai.expect 39const expect = chai.expect
39 40
@@ -47,9 +48,10 @@ async function findChannel (server: ServerInfo, channelId: number) {
47describe('Test video channels', function () { 48describe('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
29program 29program
30 .command('install') 30 .command('install')
@@ -61,12 +61,10 @@ if (!process.argv.slice(2).length) {
61 61
62program.parse(process.argv) 62program.parse(process.argv)
63 63
64const options = program.opts()
65
66// ---------------------------------------------------------------------------- 64// ----------------------------------------------------------------------------
67 65
68async function pluginsListCLI () { 66async 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
151export type MActorFormattable = 151export 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'
19import { MVideoImportDefault } from '@server/types/models/video/video-import' 19import { MVideoImportDefault } from '@server/types/models/video/video-import'
20import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' 20import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
21import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' 21import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
22import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
23import { VideoCreate } from '@shared/models'
24import { File as UploadXFile, Metadata } from '@uploadx/core'
22import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 25import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
23import { 26import {
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
41declare module 'express' { 43declare 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
50interface 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. */
2export 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'
2export * from './miscs' 2export * from './miscs'
3export * from './types' 3export * from './types'
4export * from './http-error-codes' 4export * from './http-error-codes'
5export * 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
226function 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
226function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) { 238function 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 @@
1import { makeGetRequest } from '../requests/requests' 1import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes' 2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models'
3 4
4function getDebug (url: string, token: string) { 5function 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
16function 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
17export { 30export {
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: {
55async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { 55async 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
276async function checkTmpIsEmpty (server: ServerInfo) { 276async 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 @@
1import { omit } from 'lodash' 1import { omit } from 'lodash'
2import * as request from 'supertest' 2import * as request from 'supertest'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { UserUpdateMe } from '../../models/users' 4import { UserUpdateMe } from '../../models/users'
4import { UserAdminFlag } from '../../models/users/user-flag.model' 5import { UserAdminFlag } from '../../models/users/user-flag.model'
5import { UserRegister } from '../../models/users/user-register.model' 6import { UserRegister } from '../../models/users/user-register.model'
@@ -7,9 +8,8 @@ import { UserRole } from '../../models/users/user-role'
7import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests' 8import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests'
8import { ServerInfo } from '../server/servers' 9import { ServerInfo } from '../server/servers'
9import { userLogin } from './login' 10import { userLogin } from './login'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
11 11
12type CreateUserArgs = { 12function 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}) {
23function 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
54async 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
55async function generateUserAccessToken (server: ServerInfo, username: string) { 69async 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
5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' 5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests' 6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
7import { ServerInfo } from '../server/servers' 7import { ServerInfo } from '../server/servers'
8import { User } from '../../models/users/user.model' 8import { MyUser, User } from '../../models/users/user.model'
9import { getMyUserInformation } from '../users/users' 9import { getMyUserInformation } from '../users/users'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 10import { 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
173async 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
175export { 181export {
@@ -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
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { pathExists, readdir, readFile } from 'fs-extra' 4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source'
5import * as parseTorrent from 'parse-torrent' 6import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path' 7import { extname, join } from 'path'
7import * as request from 'supertest' 8import * 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
367async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { 369async 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
419function 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
431async 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) { 447async 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
484async 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
510function 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
464function updateVideo ( 567function 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
881function 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 @@
1export type BooleanBothQuery = 'true' | 'false' | 'both' 1export type BooleanBothQuery = 'true' | 'false' | 'both'
2export 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 @@
1export interface Debug { 1export interface Debug {
2 ip: string 2 ip: string
3} 3}
4
5export 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 |
51externalDocs: 74externalDocs:
52 url: https://docs.joinpeertube.org/api-rest-reference.html 75 url: https://docs.joinpeertube.org/api-rest-reference.html
53tags: 76tags:
@@ -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
148x-tagGroups: 199x-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
309On macOS, the `postgresql` user can be `_postgres` instead of `postgres`. 309On macOS, the `postgresql` user can be `_postgres` instead of `postgres`.
310If `sudo -u postgres createuser -P peertube` gives you an error, you can try `sudo -u _postgres createuser -U peertube`. 310If `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
9RUN apt update \ 9RUN 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
diff --git a/yarn.lock b/yarn.lock
index 3ce730fbd..adfb8c912 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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
1060abbrev@1: 1073abbrev@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
1122ajv@^8.0.1: 1135ajv@^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
1372autocannon@^7.0.4: 1385autocannon@^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
1399axios@0.21.1: 1412axios@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
1581bittorrent-tracker@^9.0.0: 1594bittorrent-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
1754buffer@^6.0.2: 1767buffer@^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
1769bull@^3.4.2: 1782bull@^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
1793bytes@3.1.0, bytes@^3.0.0: 1806bytes@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
1908chalk@4.1.0, chalk@^4.0.0, chalk@^4.1.0: 1921chalk@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
1965cheerio-select@^1.3.0: 1978cheerio-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
1976cheerio@^1.0.0-rc.3: 1989cheerio@^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
2301concurrently@^6.0.0: 2314concurrently@^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
2510css-select@^4.0.0: 2523css-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
2521css-what@^5.0.0: 2534css-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
2558date-fns@^2.0.1, date-fns@^2.16.1: 2571date-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
2563dateformat@^3.0.3: 2576dateformat@^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
2814domhandler@^4.0.0, domhandler@^4.1.0: 2827domhandler@^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
2821domutils@^2.0.0, domutils@^2.5.1, domutils@^2.5.2: 2834domutils@^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
2830dot-prop@^5.2.0: 2843dot-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
3016err-code@^2.0.3: 3029err-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
3021error-ex@^1.2.0, error-ex@^1.3.1: 3034error-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
3228eslint-visitor-keys@^2.0.0: 3241eslint-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
3233eslint@^7.2.0: 3246eslint@^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
3380express-validator@^6.4.0: 3393express-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
3388express@^4.12.4, express@^4.16.4, express@^4.17.1: 3401express@^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
3509fetch-blob@^2.1.1: 3522fetch-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
3514figures@^3.0.0: 3527figures@^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
3629follow-redirects@^1.10.0: 3642follow-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
3634for-each@^0.3.3: 3647for-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
3700fs-extra@9.1.0, fs-extra@^9.0.0: 3713fs-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
3723fs-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
3710fs-minipass@^2.0.0: 3732fs-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
3751get-browser-rtc@^1.0.2: 3773get-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
3806get-stream@^6.0.0: 3828get-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
3811getpass@^0.1.1: 3833getpass@^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
4003helmet@^4.1.0: 4025helmet@^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
4008hh-mm-ss@~1.2.0: 4030hh-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
4110http-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
4294ioredis@^4.22.0: 4327ioredis@^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
4362is-bigint@^1.0.1: 4395is-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
4367is-binary-path@~2.1.0: 4400is-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
4405is-core-module@^2.2.0: 4438is-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
4653js-yaml@4.0.0, js-yaml@^4.0.0: 4686js-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
4701js-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
4668jsbn@~0.1.0: 4708jsbn@~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
5120lt_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
5080magnet-uri@^6.0.0, magnet-uri@^6.1.0: 5128magnet-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
5183markdown-it@12.0.4: 5231markdown-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
5590multiparty@^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
5542multistream@^4.0.1, multistream@^4.1.0: 5599multistream@^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
5656nodemailer@6.5.0, nodemailer@^6.0.0, nodemailer@^6.5.0: 5713nodemailer@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
5723nodemailer@^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
5666nodemon@^2.0.1: 5728nodemon@^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
5779object-inspect@^1.9.0: 5841object-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
5784object-keys@^1.0.12, object-keys@^1.1.1: 5846object-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
5898p-cancelable@^2.0.0: 5960p-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
5903p-finally@^1.0.0: 5965p-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
6216pg-connection-string@^2.4.0: 6278pg-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
6221pg-int8@1.0.1: 6283pg-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
6226pg-pool@^3.2.2: 6288pg-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
6231pg-protocol@^1.4.0: 6293pg-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
6236pg-types@^2.1.0: 6298pg-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
6247pg@^8.2.1: 6309pg@^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
6316postcss@^8.0.2: 6378postcss@^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
6596queue-microtask@^1.2.0, queue-microtask@^1.2.2: 6658queue-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
6688random-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
6626random-iterate@^1.0.1: 6693random-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
6823redis@^3.0.2: 6890redis@^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
6893resolve-alpn@^1.0.0: 6960resolve-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
6898resolve-from@^4.0.0: 6965resolve-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
7010safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: 7077safe-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
7223setprototypeof@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
7156shebang-command@^1.2.0: 7228shebang-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
7208simple-peer@^9.7.1, simple-peer@^9.9.3: 7280simple-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
7247sitemap@^6.1.0: 7319sitemap@^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
7763subarg@^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
7691superagent@^6.1.0: 7770superagent@^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
7751table@^6.0.4: 7830table@^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
7766tar@^6.1.0: 7843tar@^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
7865tlds@1.219.0, tlds@^1.218.0: 7942tlds@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
7947tlds@^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
7870tmp@0.0.x, tmp@^0.0.33: 7952tmp@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
8181uid-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
8099uint64be@^2.0.2: 8188uint64be@^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
8220utf-8-validate@^5.0.2: 8309utf-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
8329validator@^13.0.0, validator@^13.5.2: 8418validator@^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
8334vary@^1, vary@~1.1.2: 8423vary@^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
8391webtorrent@^0.116.1: 8480webtorrent@^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
8600ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: 8690ws@^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
8605ws@~6.1.0: 8695ws@~6.1.0:
8606 version "6.1.4" 8696 version "6.1.4"