diff options
479 files changed, 10283 insertions, 6689 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 407907e53..baddba758 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml | |||
@@ -46,6 +46,7 @@ jobs: | |||
46 | PGHOST: localhost | 46 | PGHOST: localhost |
47 | NODE_PENDING_JOB_WAIT: 250 | 47 | NODE_PENDING_JOB_WAIT: 250 |
48 | ENABLE_OBJECT_STORAGE_TESTS: true | 48 | ENABLE_OBJECT_STORAGE_TESTS: true |
49 | ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS: true | ||
49 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} | 50 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} |
50 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} | 51 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} |
51 | YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }} | 52 | YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
diff --git a/.gitignore b/.gitignore index c6029ad65..e0004004d 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -23,6 +23,7 @@ yarn-error.log | |||
23 | /ffmpeg-4/ | 23 | /ffmpeg-4/ |
24 | /thumbnails/ | 24 | /thumbnails/ |
25 | /torrents/ | 25 | /torrents/ |
26 | /web-videos/ | ||
26 | /videos/ | 27 | /videos/ |
27 | /previews/ | 28 | /previews/ |
28 | /logs/ | 29 | /logs/ |
@@ -116,7 +116,7 @@ Be it as a user or an instance administrator, you can decide what your experienc | |||
116 | 116 | ||
117 | <h3 align="right">Communities that help each other</h3> | 117 | <h3 align="right">Communities that help each other</h3> |
118 | <p align="right"> | 118 | <p align="right"> |
119 | In addition to visitors using WebTorrent to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our <a href="https://docs.joinpeertube.org/contribute/architecture#redundancy-between-instances">redundancy guide</a>). | 119 | In addition to visitors using P2P with WebRTC to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our <a href="https://docs.joinpeertube.org/contribute/architecture#redundancy-between-instances">redundancy guide</a>). |
120 | </p> | 120 | </p> |
121 | <p align="right"> | 121 | <p align="right"> |
122 | Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and alter creativity (more about that in our <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md">FAQ</a>). | 122 | Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and alter creativity (more about that in our <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md">FAQ</a>). |
diff --git a/client/.eslintrc.json b/client/.eslintrc.json index a297cdc94..c5685b9dc 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json | |||
@@ -3,7 +3,7 @@ | |||
3 | "ignorePatterns": [ | 3 | "ignorePatterns": [ |
4 | "projects/**/*", | 4 | "projects/**/*", |
5 | "node_modules/", | 5 | "node_modules/", |
6 | "src/standalone/player/dist" | 6 | "src/standalone/embed-player-api/dist" |
7 | ], | 7 | ], |
8 | "overrides": [ | 8 | "overrides": [ |
9 | { | 9 | { |
diff --git a/client/.gitignore b/client/.gitignore index ca68413c8..cb85788f5 100644 --- a/client/.gitignore +++ b/client/.gitignore | |||
@@ -12,5 +12,5 @@ | |||
12 | /e2e/local.log | 12 | /e2e/local.log |
13 | /e2e/browserstack.err | 13 | /e2e/browserstack.err |
14 | /e2e/screenshots | 14 | /e2e/screenshots |
15 | /src/standalone/player/build | 15 | /src/standalone/embed-player-api/build |
16 | /src/standalone/player/dist | 16 | /src/standalone/embed-player-api/dist |
diff --git a/client/angular.json b/client/angular.json index d929248d4..9b069422f 100644 --- a/client/angular.json +++ b/client/angular.json | |||
@@ -199,7 +199,11 @@ | |||
199 | "is-plain-object", | 199 | "is-plain-object", |
200 | "parse-srcset", | 200 | "parse-srcset", |
201 | "deepmerge", | 201 | "deepmerge", |
202 | "core-js/features/reflect" | 202 | "core-js/features/reflect", |
203 | "@formatjs/intl-locale/polyfill", | ||
204 | "@formatjs/intl-locale/should-polyfill", | ||
205 | "@formatjs/intl-pluralrules/polyfill-force", | ||
206 | "@formatjs/intl-pluralrules/should-polyfill" | ||
203 | ], | 207 | ], |
204 | "scripts": [], | 208 | "scripts": [], |
205 | "vendorChunk": true, | 209 | "vendorChunk": true, |
diff --git a/client/e2e/src/suites-all/private-videos.e2e-spec.ts b/client/e2e/src/suites-all/private-videos.e2e-spec.ts index a25208bb3..829d76a84 100644 --- a/client/e2e/src/suites-all/private-videos.e2e-spec.ts +++ b/client/e2e/src/suites-all/private-videos.e2e-spec.ts | |||
@@ -31,8 +31,8 @@ describe('Private videos all workflow', () => { | |||
31 | return loginPage.loginOnPeerTube2() | 31 | return loginPage.loginOnPeerTube2() |
32 | }) | 32 | }) |
33 | 33 | ||
34 | it('Should play an internal webtorrent video', async () => { | 34 | it('Should play an internal web video video', async () => { |
35 | await go(FIXTURE_URLS.INTERNAL_WEBTORRENT_VIDEO) | 35 | await go(FIXTURE_URLS.INTERNAL_WEB_VIDEO) |
36 | 36 | ||
37 | await videoWatchPage.waitWatchVideoName(internalVideoName) | 37 | await videoWatchPage.waitWatchVideoName(internalVideoName) |
38 | await checkCorrectlyPlay(playerPage) | 38 | await checkCorrectlyPlay(playerPage) |
@@ -52,8 +52,8 @@ describe('Private videos all workflow', () => { | |||
52 | await checkCorrectlyPlay(playerPage) | 52 | await checkCorrectlyPlay(playerPage) |
53 | }) | 53 | }) |
54 | 54 | ||
55 | it('Should play an internal WebTorrent video in embed', async () => { | 55 | it('Should play an internal Web Video in embed', async () => { |
56 | await go(FIXTURE_URLS.INTERNAL_EMBED_WEBTORRENT_VIDEO) | 56 | await go(FIXTURE_URLS.INTERNAL_EMBED_WEB_VIDEO) |
57 | 57 | ||
58 | await videoWatchPage.waitEmbedForDisplayed() | 58 | await videoWatchPage.waitEmbedForDisplayed() |
59 | await checkCorrectlyPlay(playerPage) | 59 | await checkCorrectlyPlay(playerPage) |
diff --git a/client/e2e/src/suites-all/videos.e2e-spec.ts b/client/e2e/src/suites-all/videos.e2e-spec.ts index d1ab9aef3..5d0f8c152 100644 --- a/client/e2e/src/suites-all/videos.e2e-spec.ts +++ b/client/e2e/src/suites-all/videos.e2e-spec.ts | |||
@@ -89,7 +89,7 @@ describe('Videos all workflow', () => { | |||
89 | let videoNameToExcept = videoName | 89 | let videoNameToExcept = videoName |
90 | 90 | ||
91 | if (isMobileDevice() || isSafari()) { | 91 | if (isMobileDevice() || isSafari()) { |
92 | await go(FIXTURE_URLS.WEBTORRENT_VIDEO) | 92 | await go(FIXTURE_URLS.WEB_VIDEO) |
93 | videoNameToExcept = 'E2E tests' | 93 | videoNameToExcept = 'E2E tests' |
94 | } else { | 94 | } else { |
95 | await videoListPage.clickOnVideo(videoName) | 95 | await videoListPage.clickOnVideo(videoName) |
@@ -176,7 +176,7 @@ describe('Videos all workflow', () => { | |||
176 | await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000) | 176 | await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000) |
177 | }) | 177 | }) |
178 | 178 | ||
179 | it('Should watch the webtorrent playlist in the embed', async () => { | 179 | it('Should watch the WEB VIDEO playlist in the embed', async () => { |
180 | if (isUploadUnsupported()) return | 180 | if (isUploadUnsupported()) return |
181 | 181 | ||
182 | const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`) | 182 | const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`) |
diff --git a/client/e2e/src/utils/urls.ts b/client/e2e/src/utils/urls.ts index cc0bdfbff..eafe0aa5d 100644 --- a/client/e2e/src/utils/urls.ts +++ b/client/e2e/src/utils/urls.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | const FIXTURE_URLS = { | 1 | const FIXTURE_URLS = { |
2 | INTERNAL_WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?mode=webtorrent&start=0', | 2 | INTERNAL_WEB_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0', |
3 | INTERNAL_HLS_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?start=0', | 3 | INTERNAL_HLS_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?start=0', |
4 | 4 | ||
5 | INTERNAL_EMBED_WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?mode=webtorrent&start=0', | 5 | INTERNAL_EMBED_WEB_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0', |
6 | INTERNAL_EMBED_HLS_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?start=0', | 6 | INTERNAL_EMBED_HLS_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?start=0', |
7 | 7 | ||
8 | INTERNAL_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/w/tKQmHcqdYZRdCszLUiWM3V?start=0', | 8 | INTERNAL_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/w/tKQmHcqdYZRdCszLUiWM3V?start=0', |
9 | INTERNAL_EMBED_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/videos/embed/tKQmHcqdYZRdCszLUiWM3V?start=0', | 9 | INTERNAL_EMBED_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/videos/embed/tKQmHcqdYZRdCszLUiWM3V?start=0', |
10 | 10 | ||
11 | WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e', | 11 | WEB_VIDEO: 'https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e', |
12 | 12 | ||
13 | HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50', | 13 | HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50', |
14 | HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a', | 14 | HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a', |
diff --git a/client/e2e/wdio.local-test.conf.ts b/client/e2e/wdio.local-test.conf.ts index 96ddc67ca..6c0171372 100644 --- a/client/e2e/wdio.local-test.conf.ts +++ b/client/e2e/wdio.local-test.conf.ts | |||
@@ -24,19 +24,19 @@ module.exports = { | |||
24 | specFileRetries: 0, | 24 | specFileRetries: 0, |
25 | 25 | ||
26 | capabilities: [ | 26 | capabilities: [ |
27 | { | 27 | // { |
28 | 'browserName': 'chrome', | 28 | // 'browserName': 'chrome', |
29 | 'acceptInsecureCerts': true, | 29 | // 'acceptInsecureCerts': true, |
30 | 'goog:chromeOptions': { | 30 | // 'goog:chromeOptions': { |
31 | args: [ '--disable-gpu', windowSizeArg ], | 31 | // args: [ '--disable-gpu', windowSizeArg ], |
32 | prefs | 32 | // prefs |
33 | } | 33 | // } |
34 | }, | 34 | // }, |
35 | { | 35 | { |
36 | 'browserName': 'firefox', | 36 | 'browserName': 'firefox', |
37 | 'moz:firefoxOptions': { | 37 | 'moz:firefoxOptions': { |
38 | binary: '/usr/bin/firefox-developer-edition', | 38 | binary: '/usr/bin/firefox-developer-edition', |
39 | args: [ '--headless', windowSizeArg ], | 39 | // args: [ '--headless', windowSizeArg ], |
40 | 40 | ||
41 | prefs | 41 | prefs |
42 | } | 42 | } |
diff --git a/client/package.json b/client/package.json index 202a0f836..149322192 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -47,6 +47,8 @@ | |||
47 | "@angular/service-worker": "^16.0.2", | 47 | "@angular/service-worker": "^16.0.2", |
48 | "@babel/core": "^7.18.5", | 48 | "@babel/core": "^7.18.5", |
49 | "@babel/preset-env": "^7.18.2", | 49 | "@babel/preset-env": "^7.18.2", |
50 | "@formatjs/intl-locale": "^3.3.1", | ||
51 | "@formatjs/intl-pluralrules": "^5.2.2", | ||
50 | "@ng-bootstrap/ng-bootstrap": "^14.0.1", | 52 | "@ng-bootstrap/ng-bootstrap": "^14.0.1", |
51 | "@ng-select/ng-select": "^10.0.3", | 53 | "@ng-select/ng-select": "^10.0.3", |
52 | "@ngx-loading-bar/core": "^6.0.0", | 54 | "@ngx-loading-bar/core": "^6.0.0", |
@@ -69,7 +71,6 @@ | |||
69 | "@types/sanitize-html": "2.6.2", | 71 | "@types/sanitize-html": "2.6.2", |
70 | "@types/sha.js": "^2.4.0", | 72 | "@types/sha.js": "^2.4.0", |
71 | "@types/video.js": "^7.3.40", | 73 | "@types/video.js": "^7.3.40", |
72 | "@types/webtorrent": "^0.109.0", | ||
73 | "@typescript-eslint/eslint-plugin": "^5.43.0", | 74 | "@typescript-eslint/eslint-plugin": "^5.43.0", |
74 | "@typescript-eslint/parser": "^5.43.0", | 75 | "@typescript-eslint/parser": "^5.43.0", |
75 | "@wdio/browserstack-service": "^8.10.5", | 76 | "@wdio/browserstack-service": "^8.10.5", |
@@ -83,14 +84,12 @@ | |||
83 | "babel-loader": "^9.1.0", | 84 | "babel-loader": "^9.1.0", |
84 | "bootstrap": "^5.1.3", | 85 | "bootstrap": "^5.1.3", |
85 | "buffer": "^6.0.3", | 86 | "buffer": "^6.0.3", |
86 | "cache-chunk-store": "^3.0.0", | ||
87 | "chart.js": "^4.3.0", | 87 | "chart.js": "^4.3.0", |
88 | "chartjs-plugin-zoom": "~2.0.1", | 88 | "chartjs-plugin-zoom": "~2.0.1", |
89 | "chromedriver": "^113.0.0", | 89 | "chromedriver": "^113.0.0", |
90 | "core-js": "^3.22.8", | 90 | "core-js": "^3.22.8", |
91 | "css-loader": "^6.2.0", | 91 | "css-loader": "^6.2.0", |
92 | "debug": "^4.3.1", | 92 | "debug": "^4.3.1", |
93 | "dexie": "^3.2.2", | ||
94 | "eslint": "^8.28.0", | 93 | "eslint": "^8.28.0", |
95 | "eslint-plugin-import": "2.27.5", | 94 | "eslint-plugin-import": "2.27.5", |
96 | "eslint-plugin-jsdoc": "^44.2.4", | 95 | "eslint-plugin-jsdoc": "^44.2.4", |
@@ -101,7 +100,6 @@ | |||
101 | "hls.js": "~1.3", | 100 | "hls.js": "~1.3", |
102 | "html-loader": "^4.1.0", | 101 | "html-loader": "^4.1.0", |
103 | "html-webpack-plugin": "^5.3.1", | 102 | "html-webpack-plugin": "^5.3.1", |
104 | "https-browserify": "^1.0.0", | ||
105 | "intl-messageformat": "^10.1.0", | 103 | "intl-messageformat": "^10.1.0", |
106 | "jschannel": "^1.0.2", | 104 | "jschannel": "^1.0.2", |
107 | "linkify-html": "^4.0.2", | 105 | "linkify-html": "^4.0.2", |
@@ -113,9 +111,7 @@ | |||
113 | "path-browserify": "^1.0.0", | 111 | "path-browserify": "^1.0.0", |
114 | "postcss": "^8.4.14", | 112 | "postcss": "^8.4.14", |
115 | "primeng": "^16.0.0-rc.2", | 113 | "primeng": "^16.0.0-rc.2", |
116 | "process": "^0.11.10", | ||
117 | "purify-css": "^1.2.5", | 114 | "purify-css": "^1.2.5", |
118 | "querystring": "^0.2.1", | ||
119 | "raw-loader": "^4.0.2", | 115 | "raw-loader": "^4.0.2", |
120 | "rxjs": "^7.3.0", | 116 | "rxjs": "^7.3.0", |
121 | "sanitize-html": "^2.1.2", | 117 | "sanitize-html": "^2.1.2", |
@@ -123,23 +119,17 @@ | |||
123 | "sass-loader": "^13.2.0", | 119 | "sass-loader": "^13.2.0", |
124 | "sha.js": "^2.4.11", | 120 | "sha.js": "^2.4.11", |
125 | "socket.io-client": "^4.5.4", | 121 | "socket.io-client": "^4.5.4", |
126 | "stream-browserify": "^3.0.0", | ||
127 | "stream-http": "^3.0.0", | ||
128 | "stylelint": "^15.1.0", | 122 | "stylelint": "^15.1.0", |
129 | "stylelint-config-sass-guidelines": "^10.0.0", | 123 | "stylelint-config-sass-guidelines": "^10.0.0", |
130 | "ts-loader": "^9.3.0", | 124 | "ts-loader": "^9.3.0", |
131 | "tslib": "^2.4.0", | 125 | "tslib": "^2.4.0", |
132 | "typescript": "~4.9.5", | 126 | "typescript": "~4.9.5", |
133 | "url": "^0.11.0", | ||
134 | "video.js": "^7.19.2", | 127 | "video.js": "^7.19.2", |
135 | "videostream": "~3.2.1", | ||
136 | "wdio-chromedriver-service": "^8.1.1", | 128 | "wdio-chromedriver-service": "^8.1.1", |
137 | "wdio-geckodriver-service": "^5.0.1", | 129 | "wdio-geckodriver-service": "^5.0.1", |
138 | "webpack": "^5.73.0", | 130 | "webpack": "^5.73.0", |
139 | "webpack-bundle-analyzer": "^4.4.2", | 131 | "webpack-bundle-analyzer": "^4.4.2", |
140 | "webpack-cli": "^5.0.1", | 132 | "webpack-cli": "^5.0.1", |
141 | "webtorrent": "1.8.26", | ||
142 | "whatwg-fetch": "^3.0.0", | ||
143 | "zone.js": "~0.13.0" | 133 | "zone.js": "~0.13.0" |
144 | }, | 134 | }, |
145 | "dependencies": {} | 135 | "dependencies": {} |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html index bbf946df0..9701e7f85 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html | |||
@@ -52,6 +52,20 @@ | |||
52 | 52 | ||
53 | <div *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div> | 53 | <div *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div> |
54 | </div> | 54 | </div> |
55 | |||
56 | <div class="form-group" formGroupName="torrents"> | ||
57 | <label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label> | ||
58 | |||
59 | <div class="number-with-unit"> | ||
60 | <input | ||
61 | type="number" min="0" id="cacheStoryboardsSize" class="form-control" | ||
62 | formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.storyboards.size'] }" | ||
63 | > | ||
64 | <span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span> | ||
65 | </div> | ||
66 | |||
67 | <div *ngIf="formErrors.cache.storyboards.size" class="form-error">{{ formErrors.cache.storyboards.size }}</div> | ||
68 | </div> | ||
55 | </ng-container> | 69 | </ng-container> |
56 | 70 | ||
57 | </div> | 71 | </div> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts index 79a98f288..06c5e6221 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts | |||
@@ -10,7 +10,7 @@ export class EditAdvancedConfigurationComponent { | |||
10 | @Input() form: FormGroup | 10 | @Input() form: FormGroup |
11 | @Input() formErrors: any | 11 | @Input() formErrors: any |
12 | 12 | ||
13 | getCacheSize (type: 'captions' | 'previews' | 'torrents') { | 13 | getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') { |
14 | return this.form.value['cache'][type]['size'] | 14 | return this.form.value['cache'][type]['size'] |
15 | } | 15 | } |
16 | } | 16 | } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts index 628c2d102..42c0e6dc2 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | 4 | ||
5 | export type ResolutionOption = { | 5 | export type ResolutionOption = { |
6 | id: string | 6 | id: string |
@@ -99,10 +99,7 @@ export class EditConfigurationService { | |||
99 | return { | 99 | return { |
100 | value, | 100 | value, |
101 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible | 101 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible |
102 | unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)( | 102 | unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value }) |
103 | { value }, | ||
104 | $localize`threads` | ||
105 | ) | ||
106 | } | 103 | } |
107 | } | 104 | } |
108 | } | 105 | } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 2c3b7560d..b381473d6 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -9,8 +9,7 @@ import { Notifier } from '@app/core' | |||
9 | import { ServerService } from '@app/core/server/server.service' | 9 | import { ServerService } from '@app/core/server/server.service' |
10 | import { | 10 | import { |
11 | ADMIN_EMAIL_VALIDATOR, | 11 | ADMIN_EMAIL_VALIDATOR, |
12 | CACHE_CAPTIONS_SIZE_VALIDATOR, | 12 | CACHE_SIZE_VALIDATOR, |
13 | CACHE_PREVIEWS_SIZE_VALIDATOR, | ||
14 | CONCURRENCY_VALIDATOR, | 13 | CONCURRENCY_VALIDATOR, |
15 | INDEX_URL_VALIDATOR, | 14 | INDEX_URL_VALIDATOR, |
16 | INSTANCE_NAME_VALIDATOR, | 15 | INSTANCE_NAME_VALIDATOR, |
@@ -120,13 +119,16 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
120 | }, | 119 | }, |
121 | cache: { | 120 | cache: { |
122 | previews: { | 121 | previews: { |
123 | size: CACHE_PREVIEWS_SIZE_VALIDATOR | 122 | size: CACHE_SIZE_VALIDATOR |
124 | }, | 123 | }, |
125 | captions: { | 124 | captions: { |
126 | size: CACHE_CAPTIONS_SIZE_VALIDATOR | 125 | size: CACHE_SIZE_VALIDATOR |
127 | }, | 126 | }, |
128 | torrents: { | 127 | torrents: { |
129 | size: CACHE_CAPTIONS_SIZE_VALIDATOR | 128 | size: CACHE_SIZE_VALIDATOR |
129 | }, | ||
130 | storyboards: { | ||
131 | size: CACHE_SIZE_VALIDATOR | ||
130 | } | 132 | } |
131 | }, | 133 | }, |
132 | signup: { | 134 | signup: { |
@@ -188,7 +190,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
188 | hls: { | 190 | hls: { |
189 | enabled: null | 191 | enabled: null |
190 | }, | 192 | }, |
191 | webtorrent: { | 193 | webVideos: { |
192 | enabled: null | 194 | enabled: null |
193 | }, | 195 | }, |
194 | remoteRunners: { | 196 | remoteRunners: { |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html index fb750aca6..accf2c28c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html | |||
@@ -67,11 +67,11 @@ | |||
67 | <div class="callout callout-light pt-2 mt-2 pb-0"> | 67 | <div class="callout callout-light pt-2 mt-2 pb-0"> |
68 | <h3 class="callout-title" i18n>Output formats</h3> | 68 | <h3 class="callout-title" i18n>Output formats</h3> |
69 | 69 | ||
70 | <ng-container formGroupName="webtorrent"> | 70 | <ng-container formGroupName="webVideos"> |
71 | <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> | 71 | <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> |
72 | <my-peertube-checkbox | 72 | <my-peertube-checkbox |
73 | inputName="transcodingWebTorrentEnabled" formControlName="enabled" | 73 | inputName="transcodingWebVideosEnabled" formControlName="enabled" |
74 | i18n-labelText labelText="WebTorrent enabled" | 74 | i18n-labelText labelText="Web Videos enabled" |
75 | > | 75 | > |
76 | <ng-template ptTemplate="help"> | 76 | <ng-template ptTemplate="help"> |
77 | <ng-container> | 77 | <ng-container> |
@@ -93,14 +93,14 @@ | |||
93 | <ng-container i18n> | 93 | <ng-container i18n> |
94 | <strong>Requires ffmpeg >= 4.1</strong> | 94 | <strong>Requires ffmpeg >= 4.1</strong> |
95 | 95 | ||
96 | <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p> | 96 | <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with Web Videos:</p> |
97 | <ul> | 97 | <ul> |
98 | <li>Resolution change is smoother</li> | 98 | <li>Resolution change is smoother</li> |
99 | <li>Faster playback especially with long videos</li> | 99 | <li>Faster playback especially with long videos</li> |
100 | <li>More stable playback (less bugs/infinite loading)</li> | 100 | <li>More stable playback (less bugs/infinite loading)</li> |
101 | </ul> | 101 | </ul> |
102 | 102 | ||
103 | <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p> | 103 | <p>If you also enabled Web Videos support, it will multiply videos storage by 2</p> |
104 | </ng-container> | 104 | </ng-container> |
105 | </ng-template> | 105 | </ng-template> |
106 | </my-peertube-checkbox> | 106 | </my-peertube-checkbox> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts index c5f4ecddb..6496e8753 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts | |||
@@ -90,9 +90,9 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
90 | const transcodingControl = this.form.get('transcoding.enabled') | 90 | const transcodingControl = this.form.get('transcoding.enabled') |
91 | const videoStudioControl = this.form.get('videoStudio.enabled') | 91 | const videoStudioControl = this.form.get('videoStudio.enabled') |
92 | const hlsControl = this.form.get('transcoding.hls.enabled') | 92 | const hlsControl = this.form.get('transcoding.hls.enabled') |
93 | const webtorrentControl = this.form.get('transcoding.webtorrent.enabled') | 93 | const webVideosControl = this.form.get('transcoding.webVideos.enabled') |
94 | 94 | ||
95 | webtorrentControl.valueChanges | 95 | webVideosControl.valueChanges |
96 | .subscribe(newValue => { | 96 | .subscribe(newValue => { |
97 | if (newValue === false && !hlsControl.disabled) { | 97 | if (newValue === false && !hlsControl.disabled) { |
98 | hlsControl.disable() | 98 | hlsControl.disable() |
@@ -105,12 +105,12 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
105 | 105 | ||
106 | hlsControl.valueChanges | 106 | hlsControl.valueChanges |
107 | .subscribe(newValue => { | 107 | .subscribe(newValue => { |
108 | if (newValue === false && !webtorrentControl.disabled) { | 108 | if (newValue === false && !webVideosControl.disabled) { |
109 | webtorrentControl.disable() | 109 | webVideosControl.disable() |
110 | } | 110 | } |
111 | 111 | ||
112 | if (newValue === true && !webtorrentControl.enabled) { | 112 | if (newValue === true && !webVideosControl.enabled) { |
113 | webtorrentControl.enable() | 113 | webVideosControl.enable() |
114 | } | 114 | } |
115 | }) | 115 | }) |
116 | 116 | ||
@@ -122,7 +122,7 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
122 | }) | 122 | }) |
123 | 123 | ||
124 | transcodingControl.updateValueAndValidity() | 124 | transcodingControl.updateValueAndValidity() |
125 | webtorrentControl.updateValueAndValidity() | 125 | webVideosControl.updateValueAndValidity() |
126 | videoStudioControl.updateValueAndValidity() | 126 | videoStudioControl.updateValueAndValidity() |
127 | hlsControl.updateValueAndValidity() | 127 | hlsControl.updateValueAndValidity() |
128 | } | 128 | } |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index cebb2e1a2..618892242 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
6 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
7 | import { DropdownAction } from '@app/shared/shared-main' | 7 | import { DropdownAction } from '@app/shared/shared-main' |
@@ -63,9 +63,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
63 | .subscribe({ | 63 | .subscribe({ |
64 | next: () => { | 64 | next: () => { |
65 | // eslint-disable-next-line max-len | 65 | // eslint-disable-next-line max-len |
66 | const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 66 | const message = formatICU( |
67 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 67 | $localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
68 | $localize`Follow requests accepted` | 68 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
69 | ) | 69 | ) |
70 | this.notifier.success(message) | 70 | this.notifier.success(message) |
71 | 71 | ||
@@ -78,9 +78,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
78 | 78 | ||
79 | async rejectFollower (follows: ActorFollow[]) { | 79 | async rejectFollower (follows: ActorFollow[]) { |
80 | // eslint-disable-next-line max-len | 80 | // eslint-disable-next-line max-len |
81 | const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 81 | const message = formatICU( |
82 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 82 | $localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`, |
83 | $localize`Do you really want to reject these follow requests?` | 83 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
84 | ) | 84 | ) |
85 | 85 | ||
86 | const res = await this.confirmService.confirm(message, $localize`Reject`) | 86 | const res = await this.confirmService.confirm(message, $localize`Reject`) |
@@ -90,9 +90,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
90 | .subscribe({ | 90 | .subscribe({ |
91 | next: () => { | 91 | next: () => { |
92 | // eslint-disable-next-line max-len | 92 | // eslint-disable-next-line max-len |
93 | const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 93 | const message = formatICU( |
94 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 94 | $localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
95 | $localize`Follow requests rejected` | 95 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
96 | ) | 96 | ) |
97 | this.notifier.success(message) | 97 | this.notifier.success(message) |
98 | 98 | ||
@@ -110,9 +110,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
110 | message += '<br /><br />' | 110 | message += '<br /><br />' |
111 | 111 | ||
112 | // eslint-disable-next-line max-len | 112 | // eslint-disable-next-line max-len |
113 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 113 | message += formatICU( |
114 | icuParams, | 114 | $localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`, |
115 | $localize`Do you really want to delete these follow requests?` | 115 | icuParams |
116 | ) | 116 | ) |
117 | 117 | ||
118 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 118 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -122,9 +122,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
122 | .subscribe({ | 122 | .subscribe({ |
123 | next: () => { | 123 | next: () => { |
124 | // eslint-disable-next-line max-len | 124 | // eslint-disable-next-line max-len |
125 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 125 | const message = formatICU( |
126 | icuParams, | 126 | $localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
127 | $localize`Follow requests removed` | 127 | icuParams |
128 | ) | 128 | ) |
129 | 129 | ||
130 | this.notifier.success(message) | 130 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts index 8f74e82a6..54b3cebc5 100644 --- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' | 4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' |
5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
@@ -62,9 +62,9 @@ export class FollowModalComponent extends FormReactive implements OnInit { | |||
62 | .subscribe({ | 62 | .subscribe({ |
63 | next: () => { | 63 | next: () => { |
64 | this.notifier.success( | 64 | this.notifier.success( |
65 | prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)( | 65 | formatICU( |
66 | { count: hostsOrHandles.length }, | 66 | $localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`, |
67 | $localize`Follow request(s) sent!` | 67 | { count: hostsOrHandles.length } |
68 | ) | 68 | ) |
69 | ) | 69 | ) |
70 | 70 | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index 71f2fbe66..6c8723c16 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -6,7 +6,7 @@ import { InstanceFollowService } from '@app/shared/shared-instance' | |||
6 | import { ActorFollow } from '@shared/models' | 6 | import { ActorFollow } from '@shared/models' |
7 | import { FollowModalComponent } from './follow-modal.component' | 7 | import { FollowModalComponent } from './follow-modal.component' |
8 | import { DropdownAction } from '@app/shared/shared-main' | 8 | import { DropdownAction } from '@app/shared/shared-main' |
9 | import { prepareIcu } from '@app/helpers' | 9 | import { formatICU } from '@app/helpers' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | templateUrl: './following-list.component.html', | 12 | templateUrl: './following-list.component.html', |
@@ -64,9 +64,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O | |||
64 | async removeFollowing (follows: ActorFollow[]) { | 64 | async removeFollowing (follows: ActorFollow[]) { |
65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } | 65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } |
66 | 66 | ||
67 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( | 67 | const message = formatICU( |
68 | icuParams, | 68 | $localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`, |
69 | $localize`Do you really want to unfollow these entries?` | 69 | icuParams |
70 | ) | 70 | ) |
71 | 71 | ||
72 | const res = await this.confirmService.confirm(message, $localize`Unfollow`) | 72 | const res = await this.confirmService.confirm(message, $localize`Unfollow`) |
@@ -76,9 +76,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O | |||
76 | .subscribe({ | 76 | .subscribe({ |
77 | next: () => { | 77 | next: () => { |
78 | // eslint-disable-next-line max-len | 78 | // eslint-disable-next-line max-len |
79 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( | 79 | const message = formatICU( |
80 | icuParams, | 80 | $localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`, |
81 | $localize`You are not following them anymore.` | 81 | icuParams |
82 | ) | 82 | ) |
83 | 83 | ||
84 | this.notifier.success(message) | 84 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts index 3ca1ceab8..35d9d13d7 100644 --- a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { prepareIcu } from '@app/helpers' | 5 | import { formatICU } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { DropdownAction } from '@app/shared/shared-main' | 7 | import { DropdownAction } from '@app/shared/shared-main' |
8 | import { UserRegistration, UserRegistrationState } from '@shared/models' | 8 | import { UserRegistration, UserRegistrationState } from '@shared/models' |
@@ -121,9 +121,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl | |||
121 | const icuParams = { count: registrations.length, username: registrations[0].username } | 121 | const icuParams = { count: registrations.length, username: registrations[0].username } |
122 | 122 | ||
123 | // eslint-disable-next-line max-len | 123 | // eslint-disable-next-line max-len |
124 | const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)( | 124 | const message = formatICU( |
125 | icuParams, | 125 | $localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`, |
126 | $localize`Do you really want to delete these registration requests?` | 126 | icuParams |
127 | ) | 127 | ) |
128 | 128 | ||
129 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 129 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -133,9 +133,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl | |||
133 | .subscribe({ | 133 | .subscribe({ |
134 | next: () => { | 134 | next: () => { |
135 | // eslint-disable-next-line max-len | 135 | // eslint-disable-next-line max-len |
136 | const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)( | 136 | const message = formatICU( |
137 | icuParams, | 137 | $localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`, |
138 | $localize`Registration requests removed` | 138 | icuParams |
139 | ) | 139 | ) |
140 | 140 | ||
141 | this.notifier.success(message) | 141 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index 28efdc076..b77072665 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -7,7 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main' | |||
7 | import { BulkService } from '@app/shared/shared-moderation' | 7 | import { BulkService } from '@app/shared/shared-moderation' |
8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' | 8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' |
9 | import { FeedFormat, UserRight } from '@shared/models' | 9 | import { FeedFormat, UserRight } from '@shared/models' |
10 | import { prepareIcu } from '@app/helpers' | 10 | import { formatICU } from '@app/helpers' |
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
13 | selector: 'my-video-comment-list', | 13 | selector: 'my-video-comment-list', |
@@ -146,9 +146,9 @@ export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> imp | |||
146 | .subscribe({ | 146 | .subscribe({ |
147 | next: () => { | 147 | next: () => { |
148 | this.notifier.success( | 148 | this.notifier.success( |
149 | prepareIcu($localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`)( | 149 | formatICU( |
150 | { count: commentArgs.length }, | 150 | $localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`, |
151 | $localize`${commentArgs.length} comment(s) deleted.` | 151 | { count: commentArgs.length } |
152 | ) | 152 | ) |
153 | ) | 153 | ) |
154 | 154 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 19420b748..5d5abf6f4 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { getAPIHost, prepareIcu } from '@app/helpers' | 5 | import { formatICU, getAPIHost } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { Actor, DropdownAction } from '@app/shared/shared-main' | 7 | import { Actor, DropdownAction } from '@app/shared/shared-main' |
8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' | 8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' |
@@ -210,9 +210,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
210 | 210 | ||
211 | async unbanUsers (users: User[]) { | 211 | async unbanUsers (users: User[]) { |
212 | const res = await this.confirmService.confirm( | 212 | const res = await this.confirmService.confirm( |
213 | prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)( | 213 | formatICU( |
214 | { count: users.length }, | 214 | $localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`, |
215 | $localize`Do you really want to unban ${users.length} users?` | 215 | { count: users.length } |
216 | ), | 216 | ), |
217 | $localize`Unban` | 217 | $localize`Unban` |
218 | ) | 218 | ) |
@@ -223,9 +223,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
223 | .subscribe({ | 223 | .subscribe({ |
224 | next: () => { | 224 | next: () => { |
225 | this.notifier.success( | 225 | this.notifier.success( |
226 | prepareIcu($localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`)( | 226 | formatICU( |
227 | { count: users.length }, | 227 | $localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`, |
228 | $localize`${users.length} users unbanned.` | 228 | { count: users.length } |
229 | ) | 229 | ) |
230 | ) | 230 | ) |
231 | this.reloadData() | 231 | this.reloadData() |
@@ -252,9 +252,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
252 | .subscribe({ | 252 | .subscribe({ |
253 | next: () => { | 253 | next: () => { |
254 | this.notifier.success( | 254 | this.notifier.success( |
255 | prepareIcu($localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`)( | 255 | formatICU( |
256 | { count: users.length }, | 256 | $localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`, |
257 | $localize`${users.length} users deleted.` | 257 | { count: users.length } |
258 | ) | 258 | ) |
259 | ) | 259 | ) |
260 | 260 | ||
@@ -270,9 +270,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
270 | .subscribe({ | 270 | .subscribe({ |
271 | next: () => { | 271 | next: () => { |
272 | this.notifier.success( | 272 | this.notifier.success( |
273 | prepareIcu($localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`)( | 273 | formatICU( |
274 | { count: users.length }, | 274 | $localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`, |
275 | $localize`${users.length} users email set as verified.` | 275 | { count: users.length } |
276 | ) | 276 | ) |
277 | ) | 277 | ) |
278 | 278 | ||
diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts index 4b9357fb7..722495706 100644 --- a/client/src/app/+admin/overview/videos/video-admin.service.ts +++ b/client/src/app/+admin/overview/videos/video-admin.service.ts | |||
@@ -59,12 +59,12 @@ export class VideoAdminService { | |||
59 | title: $localize`Video files`, | 59 | title: $localize`Video files`, |
60 | children: [ | 60 | children: [ |
61 | { | 61 | { |
62 | value: 'webtorrent:true isLocal:true', | 62 | value: 'webVideos:true isLocal:true', |
63 | label: $localize`With WebTorrent` | 63 | label: $localize`With Web Videos` |
64 | }, | 64 | }, |
65 | { | 65 | { |
66 | value: 'webtorrent:false isLocal:true', | 66 | value: 'webVideos:false isLocal:true', |
67 | label: $localize`Without WebTorrent` | 67 | label: $localize`Without Web Videos` |
68 | }, | 68 | }, |
69 | { | 69 | { |
70 | value: 'hls:true isLocal:true', | 70 | value: 'hls:true isLocal:true', |
@@ -126,8 +126,8 @@ export class VideoAdminService { | |||
126 | prefix: 'hls:', | 126 | prefix: 'hls:', |
127 | isBoolean: true | 127 | isBoolean: true |
128 | }, | 128 | }, |
129 | hasWebtorrentFiles: { | 129 | hasWebVideoFiles: { |
130 | prefix: 'webtorrent:', | 130 | prefix: 'webVideos:', |
131 | isBoolean: true | 131 | isBoolean: true |
132 | }, | 132 | }, |
133 | isLive: { | 133 | isLive: { |
@@ -151,7 +151,7 @@ export class VideoAdminService { | |||
151 | } | 151 | } |
152 | 152 | ||
153 | if (filters.excludePublic) { | 153 | if (filters.excludePublic) { |
154 | privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ] | 154 | privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ] |
155 | 155 | ||
156 | filters.excludePublic = undefined | 156 | filters.excludePublic = undefined |
157 | } | 157 | } |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index c4f78cadc..3a4666435 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -83,8 +83,8 @@ | |||
83 | </td> | 83 | </td> |
84 | 84 | ||
85 | <td> | 85 | <td> |
86 | <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span> | 86 | <span *ngIf="hasHLS(video)" class="pt-badge badge-blue">HLS</span> |
87 | <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span> | 87 | <span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue">Web Videos ({{ video.files.length }})</span> |
88 | <span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span> | 88 | <span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span> |
89 | <span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span> | 89 | <span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span> |
90 | 90 | ||
@@ -102,8 +102,8 @@ | |||
102 | <tr> | 102 | <tr> |
103 | <td class="video-info expand-cell" myAutoColspan> | 103 | <td class="video-info expand-cell" myAutoColspan> |
104 | <div> | 104 | <div> |
105 | <div *ngIf="isWebTorrent(video)"> | 105 | <div *ngIf="hasWebVideos(video)"> |
106 | WebTorrent: | 106 | Web Videos: |
107 | 107 | ||
108 | <ul> | 108 | <ul> |
109 | <li *ngFor="let file of video.files"> | 109 | <li *ngFor="let file of video.files"> |
@@ -112,13 +112,13 @@ | |||
112 | <my-global-icon | 112 | <my-global-icon |
113 | *ngIf="canRemoveOneFile(video)" | 113 | *ngIf="canRemoveOneFile(video)" |
114 | i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" | 114 | i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" |
115 | (click)="removeVideoFile(video, file, 'webtorrent')" | 115 | (click)="removeVideoFile(video, file, 'web-videos')" |
116 | ></my-global-icon> | 116 | ></my-global-icon> |
117 | </li> | 117 | </li> |
118 | </ul> | 118 | </ul> |
119 | </div> | 119 | </div> |
120 | 120 | ||
121 | <div *ngIf="isHLS(video)"> | 121 | <div *ngIf="hasHLS(video)"> |
122 | HLS: | 122 | HLS: |
123 | 123 | ||
124 | <ul> | 124 | <ul> |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index ebf82ce16..52f02d8d0 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -3,7 +3,7 @@ import { finalize } from 'rxjs/operators' | |||
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
6 | import { prepareIcu } from '@app/helpers' | 6 | import { formatICU } from '@app/helpers' |
7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' | 9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' |
@@ -99,8 +99,8 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
99 | iconName: 'cog' | 99 | iconName: 'cog' |
100 | }, | 100 | }, |
101 | { | 101 | { |
102 | label: $localize`Run WebTorrent transcoding`, | 102 | label: $localize`Run Web Video transcoding`, |
103 | handler: videos => this.runTranscoding(videos, 'webtorrent'), | 103 | handler: videos => this.runTranscoding(videos, 'web-video'), |
104 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), | 104 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), |
105 | iconName: 'cog' | 105 | iconName: 'cog' |
106 | }, | 106 | }, |
@@ -111,8 +111,8 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
111 | iconName: 'delete' | 111 | iconName: 'delete' |
112 | }, | 112 | }, |
113 | { | 113 | { |
114 | label: $localize`Delete WebTorrent files`, | 114 | label: $localize`Delete Web Video files`, |
115 | handler: videos => this.removeVideoFiles(videos, 'webtorrent'), | 115 | handler: videos => this.removeVideoFiles(videos, 'web-videos'), |
116 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), | 116 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), |
117 | iconName: 'delete' | 117 | iconName: 'delete' |
118 | } | 118 | } |
@@ -150,14 +150,14 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
150 | return video.state.id === VideoState.TO_IMPORT | 150 | return video.state.id === VideoState.TO_IMPORT |
151 | } | 151 | } |
152 | 152 | ||
153 | isHLS (video: Video) { | 153 | hasHLS (video: Video) { |
154 | const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 154 | const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
155 | if (!p) return false | 155 | if (!p) return false |
156 | 156 | ||
157 | return p.files.length !== 0 | 157 | return p.files.length !== 0 |
158 | } | 158 | } |
159 | 159 | ||
160 | isWebTorrent (video: Video) { | 160 | hasWebVideos (video: Video) { |
161 | return video.files.length !== 0 | 161 | return video.files.length !== 0 |
162 | } | 162 | } |
163 | 163 | ||
@@ -176,14 +176,14 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
176 | getFilesSize (video: Video) { | 176 | getFilesSize (video: Video) { |
177 | let files = video.files | 177 | let files = video.files |
178 | 178 | ||
179 | if (this.isHLS(video)) { | 179 | if (this.hasHLS(video)) { |
180 | files = files.concat(video.streamingPlaylists[0].files) | 180 | files = files.concat(video.streamingPlaylists[0].files) |
181 | } | 181 | } |
182 | 182 | ||
183 | return files.reduce((p, f) => p += f.size, 0) | 183 | return files.reduce((p, f) => p += f.size, 0) |
184 | } | 184 | } |
185 | 185 | ||
186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { | 186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'web-videos') { |
187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` | 187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` |
188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | 188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) |
189 | if (res === false) return | 189 | if (res === false) return |
@@ -219,9 +219,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
219 | } | 219 | } |
220 | 220 | ||
221 | private async removeVideos (videos: Video[]) { | 221 | private async removeVideos (videos: Video[]) { |
222 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( | 222 | const message = formatICU( |
223 | { count: videos.length }, | 223 | $localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`, |
224 | $localize`Are you sure you want to delete these ${videos.length} videos?` | 224 | { count: videos.length } |
225 | ) | 225 | ) |
226 | 226 | ||
227 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 227 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -231,9 +231,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
231 | .subscribe({ | 231 | .subscribe({ |
232 | next: () => { | 232 | next: () => { |
233 | this.notifier.success( | 233 | this.notifier.success( |
234 | prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)( | 234 | formatICU( |
235 | { count: videos.length }, | 235 | $localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`, |
236 | $localize`Deleted ${videos.length} videos.` | 236 | { count: videos.length } |
237 | ) | 237 | ) |
238 | ) | 238 | ) |
239 | 239 | ||
@@ -249,9 +249,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
249 | .subscribe({ | 249 | .subscribe({ |
250 | next: () => { | 250 | next: () => { |
251 | this.notifier.success( | 251 | this.notifier.success( |
252 | prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)( | 252 | formatICU( |
253 | { count: videos.length }, | 253 | $localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`, |
254 | $localize`Unblocked ${videos.length} videos.` | 254 | { count: videos.length } |
255 | ) | 255 | ) |
256 | ) | 256 | ) |
257 | 257 | ||
@@ -262,20 +262,20 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
262 | }) | 262 | }) |
263 | } | 263 | } |
264 | 264 | ||
265 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { | 265 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'web-videos') { |
266 | let message: string | 266 | let message: string |
267 | 267 | ||
268 | if (type === 'hls') { | 268 | if (type === 'hls') { |
269 | // eslint-disable-next-line max-len | 269 | // eslint-disable-next-line max-len |
270 | message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)( | 270 | message = formatICU( |
271 | { count: videos.length }, | 271 | $localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`, |
272 | $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` | 272 | { count: videos.length } |
273 | ) | 273 | ) |
274 | } else { | 274 | } else { |
275 | // eslint-disable-next-line max-len | 275 | // eslint-disable-next-line max-len |
276 | message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)( | 276 | message = formatICU( |
277 | { count: videos.length }, | 277 | $localize`Are you sure you want to delete Web Video files of {count, plural, =1 {1 video} other {{count} videos}}?`, |
278 | $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` | 278 | { count: videos.length } |
279 | ) | 279 | ) |
280 | } | 280 | } |
281 | 281 | ||
@@ -293,7 +293,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
293 | }) | 293 | }) |
294 | } | 294 | } |
295 | 295 | ||
296 | private runTranscoding (videos: Video[], type: 'hls' | 'webtorrent') { | 296 | private runTranscoding (videos: Video[], type: 'hls' | 'web-video') { |
297 | this.videoService.runTranscoding(videos.map(v => v.id), type) | 297 | this.videoService.runTranscoding(videos.map(v => v.id), type) |
298 | .subscribe({ | 298 | .subscribe({ |
299 | next: () => { | 299 | next: () => { |
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts index 8ba956eb8..8994c1d00 100644 --- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts +++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { DropdownAction } from '@app/shared/shared-main' | 5 | import { DropdownAction } from '@app/shared/shared-main' |
6 | import { RunnerJob, RunnerJobState } from '@shared/models' | 6 | import { RunnerJob, RunnerJobState } from '@shared/models' |
7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' | 7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' |
@@ -57,9 +57,10 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI | |||
57 | } | 57 | } |
58 | 58 | ||
59 | async cancelJobs (jobs: RunnerJob[]) { | 59 | async cancelJobs (jobs: RunnerJob[]) { |
60 | const message = prepareIcu( | 60 | const message = formatICU( |
61 | $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.` | 61 | $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.`, |
62 | )({ count: jobs.length }, $localize`Do you really want to cancel these jobs? Children jobs will also be cancelled.`) | 62 | { count: jobs.length } |
63 | ) | ||
63 | 64 | ||
64 | const res = await this.confirmService.confirm(message, $localize`Cancel`) | 65 | const res = await this.confirmService.confirm(message, $localize`Cancel`) |
65 | 66 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts index 97ffb6013..393c3ad6b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts | |||
@@ -30,7 +30,7 @@ export class MyAccountTwoFactorButtonComponent implements OnInit { | |||
30 | async disableTwoFactor () { | 30 | async disableTwoFactor () { |
31 | const message = $localize`Are you sure you want to disable two factor authentication of your account?` | 31 | const message = $localize`Are you sure you want to disable two factor authentication of your account?` |
32 | 32 | ||
33 | const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`) | 33 | const { confirmed, password } = await this.confirmService.confirmWithPassword({ message, title: $localize`Disable two factor` }) |
34 | if (confirmed === false) return | 34 | if (confirmed === false) return |
35 | 35 | ||
36 | this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password }) | 36 | this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password }) |
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 633720a6c..4d5dbbc2b 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 | |||
@@ -54,7 +54,7 @@ export class MyVideoChannelsComponent { | |||
54 | const res = await this.confirmService.confirmWithExpectedInput( | 54 | const res = await this.confirmService.confirmWithExpectedInput( |
55 | $localize`Do you really want to delete ${videoChannel.displayName}? | 55 | $localize`Do you really want to delete ${videoChannel.displayName}? |
56 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another | 56 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another |
57 | channel with the same name (${videoChannel.name})!`, | 57 | channel or account with the same name (${videoChannel.name})!`, |
58 | 58 | ||
59 | $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`, | 59 | $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`, |
60 | 60 | ||
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 57b8bdf7d..1827d6a0b 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -5,7 +5,7 @@ import { Component, OnInit, ViewChild } from '@angular/core' | |||
5 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
6 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' | 6 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
7 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 7 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
8 | import { immutableAssign, prepareIcu } from '@app/helpers' | 8 | import { immutableAssign, formatICU } from '@app/helpers' |
9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
11 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 11 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
@@ -184,9 +184,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
184 | .map(([ k, _v ]) => parseInt(k, 10)) | 184 | .map(([ k, _v ]) => parseInt(k, 10)) |
185 | 185 | ||
186 | const res = await this.confirmService.confirm( | 186 | const res = await this.confirmService.confirm( |
187 | prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)( | 187 | formatICU( |
188 | { length: toDeleteVideosIds.length }, | 188 | $localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`, |
189 | $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?` | 189 | { length: toDeleteVideosIds.length } |
190 | ), | 190 | ), |
191 | $localize`Delete` | 191 | $localize`Delete` |
192 | ) | 192 | ) |
@@ -205,9 +205,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
205 | .subscribe({ | 205 | .subscribe({ |
206 | next: () => { | 206 | next: () => { |
207 | this.notifier.success( | 207 | this.notifier.success( |
208 | prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)( | 208 | formatICU( |
209 | { length: toDeleteVideosIds.length }, | 209 | $localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`, |
210 | $localize`${toDeleteVideosIds.length} have been deleted.` | 210 | { length: toDeleteVideosIds.length } |
211 | ) | 211 | ) |
212 | ) | 212 | ) |
213 | 213 | ||
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 b607dabe9..97b713874 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 | |||
@@ -120,7 +120,12 @@ | |||
120 | </div> | 120 | </div> |
121 | </div> | 121 | </div> |
122 | 122 | ||
123 | <div *ngIf="schedulePublicationEnabled" class="form-group"> | 123 | <div *ngIf="passwordProtectionSelected" class="form-group"> |
124 | <label i18n for="videoPassword">Password</label> | ||
125 | <my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text> | ||
126 | </div> | ||
127 | |||
128 | <div *ngIf="schedulePublicationSelected" class="form-group"> | ||
124 | <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label> | 129 | <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label> |
125 | <p-calendar | 130 | <p-calendar |
126 | id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat" | 131 | id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat" |
@@ -287,7 +292,7 @@ | |||
287 | <div class="form-group mx-4" *ngIf="isSaveReplayEnabled()"> | 292 | <div class="form-group mx-4" *ngIf="isSaveReplayEnabled()"> |
288 | <label i18n for="replayPrivacy">Privacy of the new replay</label> | 293 | <label i18n for="replayPrivacy">Privacy of the new replay</label> |
289 | <my-select-options | 294 | <my-select-options |
290 | labelForId="replayPrivacy" [items]="videoPrivacies" [clearable]="false" formControlName="replayPrivacy" | 295 | labelForId="replayPrivacy" [items]="replayPrivacies" [clearable]="false" formControlName="replayPrivacy" |
291 | ></my-select-options> | 296 | ></my-select-options> |
292 | </div> | 297 | </div> |
293 | 298 | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 8ed54ce6b..5e5df8db7 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | VIDEO_LICENCE_VALIDATOR, | 14 | VIDEO_LICENCE_VALIDATOR, |
15 | VIDEO_NAME_VALIDATOR, | 15 | VIDEO_NAME_VALIDATOR, |
16 | VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, | 16 | VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, |
17 | VIDEO_PASSWORD_VALIDATOR, | ||
17 | VIDEO_PRIVACY_VALIDATOR, | 18 | VIDEO_PRIVACY_VALIDATOR, |
18 | VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, | 19 | VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, |
19 | VIDEO_SUPPORT_VALIDATOR, | 20 | VIDEO_SUPPORT_VALIDATOR, |
@@ -79,7 +80,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
79 | // So that it can be accessed in the template | 80 | // So that it can be accessed in the template |
80 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 81 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
81 | 82 | ||
82 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 83 | videoPrivacies: VideoConstant<VideoPrivacy | typeof VideoEdit.SPECIAL_SCHEDULED_PRIVACY > [] = [] |
84 | replayPrivacies: VideoConstant<VideoPrivacy> [] = [] | ||
83 | videoCategories: VideoConstant<number>[] = [] | 85 | videoCategories: VideoConstant<number>[] = [] |
84 | videoLicences: VideoConstant<number>[] = [] | 86 | videoLicences: VideoConstant<number>[] = [] |
85 | videoLanguages: VideoLanguages[] = [] | 87 | videoLanguages: VideoLanguages[] = [] |
@@ -103,7 +105,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
103 | 105 | ||
104 | pluginDataFormGroup: FormGroup | 106 | pluginDataFormGroup: FormGroup |
105 | 107 | ||
106 | schedulePublicationEnabled = false | 108 | schedulePublicationSelected = false |
109 | passwordProtectionSelected = false | ||
107 | 110 | ||
108 | calendarLocale: any = {} | 111 | calendarLocale: any = {} |
109 | minScheduledDate = new Date() | 112 | minScheduledDate = new Date() |
@@ -148,6 +151,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
148 | const obj: { [ id: string ]: BuildFormValidator } = { | 151 | const obj: { [ id: string ]: BuildFormValidator } = { |
149 | name: VIDEO_NAME_VALIDATOR, | 152 | name: VIDEO_NAME_VALIDATOR, |
150 | privacy: VIDEO_PRIVACY_VALIDATOR, | 153 | privacy: VIDEO_PRIVACY_VALIDATOR, |
154 | videoPassword: VIDEO_PASSWORD_VALIDATOR, | ||
151 | channelId: VIDEO_CHANNEL_VALIDATOR, | 155 | channelId: VIDEO_CHANNEL_VALIDATOR, |
152 | nsfw: null, | 156 | nsfw: null, |
153 | commentsEnabled: null, | 157 | commentsEnabled: null, |
@@ -222,7 +226,9 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
222 | 226 | ||
223 | this.serverService.getVideoPrivacies() | 227 | this.serverService.getVideoPrivacies() |
224 | .subscribe(privacies => { | 228 | .subscribe(privacies => { |
225 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies | 229 | const videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies |
230 | this.videoPrivacies = videoPrivacies | ||
231 | this.replayPrivacies = videoPrivacies.filter((privacy) => privacy.id !== VideoPrivacy.PASSWORD_PROTECTED) | ||
226 | 232 | ||
227 | // Can't schedule publication if private privacy is not available (could be deleted by a plugin) | 233 | // Can't schedule publication if private privacy is not available (could be deleted by a plugin) |
228 | const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE) | 234 | const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE) |
@@ -410,13 +416,13 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
410 | .subscribe( | 416 | .subscribe( |
411 | newPrivacyId => { | 417 | newPrivacyId => { |
412 | 418 | ||
413 | this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY | 419 | this.schedulePublicationSelected = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY |
414 | 420 | ||
415 | // Value changed | 421 | // Value changed |
416 | const scheduleControl = this.form.get('schedulePublicationAt') | 422 | const scheduleControl = this.form.get('schedulePublicationAt') |
417 | const waitTranscodingControl = this.form.get('waitTranscoding') | 423 | const waitTranscodingControl = this.form.get('waitTranscoding') |
418 | 424 | ||
419 | if (this.schedulePublicationEnabled) { | 425 | if (this.schedulePublicationSelected) { |
420 | scheduleControl.setValidators([ Validators.required ]) | 426 | scheduleControl.setValidators([ Validators.required ]) |
421 | 427 | ||
422 | waitTranscodingControl.disable() | 428 | waitTranscodingControl.disable() |
@@ -437,6 +443,16 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
437 | 443 | ||
438 | this.firstPatchDone = true | 444 | this.firstPatchDone = true |
439 | 445 | ||
446 | this.passwordProtectionSelected = newPrivacyId === VideoPrivacy.PASSWORD_PROTECTED | ||
447 | const videoPasswordControl = this.form.get('videoPassword') | ||
448 | |||
449 | if (this.passwordProtectionSelected) { | ||
450 | videoPasswordControl.setValidators([ Validators.required ]) | ||
451 | } else { | ||
452 | videoPasswordControl.clearValidators() | ||
453 | } | ||
454 | videoPasswordControl.updateValueAndValidity() | ||
455 | |||
440 | } | 456 | } |
441 | ) | 457 | ) |
442 | } | 458 | } |
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index ad71162b8..e51047e8c 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts | |||
@@ -10,7 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live' | |||
10 | import { LoadingBarService } from '@ngx-loading-bar/core' | 10 | import { LoadingBarService } from '@ngx-loading-bar/core' |
11 | import { logger } from '@root-helpers/logger' | 11 | import { logger } from '@root-helpers/logger' |
12 | import { pick, simpleObjectsDeepEqual } from '@shared/core-utils' | 12 | import { pick, simpleObjectsDeepEqual } from '@shared/core-utils' |
13 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' | 13 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoState } from '@shared/models' |
14 | import { VideoSource } from '@shared/models/videos/video-source' | 14 | import { VideoSource } from '@shared/models/videos/video-source' |
15 | import { hydrateFormFromVideo } from './shared/video-edit-utils' | 15 | import { hydrateFormFromVideo } from './shared/video-edit-utils' |
16 | 16 | ||
@@ -49,10 +49,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
49 | this.buildForm({}) | 49 | this.buildForm({}) |
50 | 50 | ||
51 | const { videoData } = this.route.snapshot.data | 51 | const { videoData } = this.route.snapshot.data |
52 | const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData | 52 | const { video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword } = videoData |
53 | 53 | ||
54 | this.videoDetails = video | 54 | this.videoDetails = video |
55 | this.videoEdit = new VideoEdit(this.videoDetails) | 55 | this.videoEdit = new VideoEdit(this.videoDetails, videoPassword) |
56 | 56 | ||
57 | this.userVideoChannels = videoChannels | 57 | this.userVideoChannels = videoChannels |
58 | this.videoCaptions = videoCaptions | 58 | this.videoCaptions = videoCaptions |
@@ -98,11 +98,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
98 | } | 98 | } |
99 | 99 | ||
100 | isWaitTranscodingHidden () { | 100 | isWaitTranscodingHidden () { |
101 | if (this.videoDetails.getFiles().length > 1) { // Already transcoded | 101 | return this.videoDetails.state.id !== VideoState.TO_TRANSCODE |
102 | return true | ||
103 | } | ||
104 | |||
105 | return false | ||
106 | } | 102 | } |
107 | 103 | ||
108 | async update () { | 104 | async update () { |
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 6612d22de..2c99b36a8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -4,8 +4,9 @@ import { Injectable } from '@angular/core' | |||
4 | import { ActivatedRouteSnapshot } from '@angular/router' | 4 | import { ActivatedRouteSnapshot } from '@angular/router' |
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannelsForSelect } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoDetails, VideoService, VideoPasswordService } from '@app/shared/shared-main' |
8 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
9 | import { VideoPrivacy } from '@shared/models/videos' | ||
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class VideoUpdateResolver { | 12 | export class VideoUpdateResolver { |
@@ -13,7 +14,8 @@ export class VideoUpdateResolver { | |||
13 | private videoService: VideoService, | 14 | private videoService: VideoService, |
14 | private liveVideoService: LiveVideoService, | 15 | private liveVideoService: LiveVideoService, |
15 | private authService: AuthService, | 16 | private authService: AuthService, |
16 | private videoCaptionService: VideoCaptionService | 17 | private videoCaptionService: VideoCaptionService, |
18 | private videoPasswordService: VideoPasswordService | ||
17 | ) { | 19 | ) { |
18 | } | 20 | } |
19 | 21 | ||
@@ -21,11 +23,11 @@ export class VideoUpdateResolver { | |||
21 | const uuid: string = route.params['uuid'] | 23 | const uuid: string = route.params['uuid'] |
22 | 24 | ||
23 | return this.videoService.getVideo({ videoId: uuid }) | 25 | return this.videoService.getVideo({ videoId: uuid }) |
24 | .pipe( | 26 | .pipe( |
25 | switchMap(video => forkJoin(this.buildVideoObservables(video))), | 27 | switchMap(video => forkJoin(this.buildVideoObservables(video))), |
26 | map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) => | 28 | map(([ video, videoSource, videoChannels, videoCaptions, liveVideo, videoPassword ]) => |
27 | ({ video, videoChannels, videoCaptions, videoSource, liveVideo })) | 29 | ({ video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword })) |
28 | ) | 30 | ) |
29 | } | 31 | } |
30 | 32 | ||
31 | private buildVideoObservables (video: VideoDetails) { | 33 | private buildVideoObservables (video: VideoDetails) { |
@@ -46,6 +48,10 @@ export class VideoUpdateResolver { | |||
46 | 48 | ||
47 | video.isLive | 49 | video.isLive |
48 | ? this.liveVideoService.getVideoLive(video.id) | 50 | ? this.liveVideoService.getVideoLive(video.id) |
51 | : of(undefined), | ||
52 | |||
53 | video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
54 | ? this.videoPasswordService.getVideoPasswords({ videoUUID: video.uuid }) | ||
49 | : of(undefined) | 55 | : of(undefined) |
50 | ] | 56 | ] |
51 | } | 57 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html index cf32e371a..140a391e9 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <div class="video-actions-rates"> | 1 | <div class="video-actions-rates"> |
2 | <div class="video-actions justify-content-end"> | 2 | <div class="video-actions justify-content-end"> |
3 | <my-video-rate | 3 | <my-video-rate |
4 | [video]="video" [isUserLoggedIn]="isUserLoggedIn" | 4 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn" |
5 | (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" | 5 | (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" |
6 | ></my-video-rate> | 6 | ></my-video-rate> |
7 | 7 | ||
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | <div | 21 | <div |
22 | class="action-dropdown" ngbDropdown placement="top" role="button" autoClose="outside" | 22 | class="action-dropdown" ngbDropdown placement="top" role="button" autoClose="outside" |
23 | *ngIf="isUserLoggedIn" (openChange)="addContent.openChange($event)" | 23 | *ngIf="isVideoAddableToPlaylist()" (openChange)="addContent.openChange($event)" |
24 | [ngbTooltip]="tooltipSaveToPlaylist" | 24 | [ngbTooltip]="tooltipSaveToPlaylist" |
25 | placement="bottom auto" | 25 | placement="bottom auto" |
26 | > | 26 | > |
@@ -43,7 +43,7 @@ | |||
43 | <span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span> | 43 | <span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span> |
44 | </button> | 44 | </button> |
45 | 45 | ||
46 | <my-video-download #videoDownloadModal></my-video-download> | 46 | <my-video-download #videoDownloadModal [videoPassword]="videoPassword"></my-video-download> |
47 | </ng-container> | 47 | </ng-container> |
48 | 48 | ||
49 | <ng-container *ngIf="isUserLoggedIn"> | 49 | <ng-container *ngIf="isUserLoggedIn"> |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts index 51718827d..e6c0d4de1 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts | |||
@@ -5,7 +5,7 @@ import { VideoShareComponent } from '@app/shared/shared-share-modal' | |||
5 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | 5 | import { SupportModalComponent } from '@app/shared/shared-support-modal' |
6 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | 6 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' |
7 | import { VideoPlaylist } from '@app/shared/shared-video-playlist' | 7 | import { VideoPlaylist } from '@app/shared/shared-video-playlist' |
8 | import { UserVideoRateType, VideoCaption } from '@shared/models/videos' | 8 | import { UserVideoRateType, VideoCaption, VideoPrivacy } from '@shared/models/videos' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
11 | selector: 'my-action-buttons', | 11 | selector: 'my-action-buttons', |
@@ -18,10 +18,12 @@ export class ActionButtonsComponent implements OnInit, OnChanges { | |||
18 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent | 18 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent |
19 | 19 | ||
20 | @Input() video: VideoDetails | 20 | @Input() video: VideoDetails |
21 | @Input() videoPassword: string | ||
21 | @Input() videoCaptions: VideoCaption[] | 22 | @Input() videoCaptions: VideoCaption[] |
22 | @Input() playlist: VideoPlaylist | 23 | @Input() playlist: VideoPlaylist |
23 | 24 | ||
24 | @Input() isUserLoggedIn: boolean | 25 | @Input() isUserLoggedIn: boolean |
26 | @Input() isUserOwner: boolean | ||
25 | 27 | ||
26 | @Input() currentTime: number | 28 | @Input() currentTime: number |
27 | @Input() currentPlaylistPosition: number | 29 | @Input() currentPlaylistPosition: number |
@@ -92,4 +94,14 @@ export class ActionButtonsComponent implements OnInit, OnChanges { | |||
92 | private setVideoLikesBarTooltipText () { | 94 | private setVideoLikesBarTooltipText () { |
93 | this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` | 95 | this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` |
94 | } | 96 | } |
97 | |||
98 | isVideoAddableToPlaylist () { | ||
99 | const isPasswordProtected = this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
100 | |||
101 | if (!this.isUserLoggedIn) return false | ||
102 | |||
103 | if (isPasswordProtected) return this.isUserOwner | ||
104 | |||
105 | return true | ||
106 | } | ||
95 | } | 107 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts index d0c138834..11966ce34 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts | |||
@@ -12,6 +12,7 @@ import { UserVideoRateType } from '@shared/models' | |||
12 | }) | 12 | }) |
13 | export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { | 13 | export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { |
14 | @Input() video: VideoDetails | 14 | @Input() video: VideoDetails |
15 | @Input() videoPassword: string | ||
15 | @Input() isUserLoggedIn: boolean | 16 | @Input() isUserLoggedIn: boolean |
16 | 17 | ||
17 | @Output() userRatingLoaded = new EventEmitter<UserVideoRateType>() | 18 | @Output() userRatingLoaded = new EventEmitter<UserVideoRateType>() |
@@ -103,13 +104,13 @@ export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { | |||
103 | } | 104 | } |
104 | 105 | ||
105 | private setRating (nextRating: UserVideoRateType) { | 106 | private setRating (nextRating: UserVideoRateType) { |
106 | const ratingMethods: { [id in UserVideoRateType]: (id: string) => Observable<any> } = { | 107 | const ratingMethods: { [id in UserVideoRateType]: (id: string, videoPassword: string) => Observable<any> } = { |
107 | like: this.videoService.setVideoLike, | 108 | like: this.videoService.setVideoLike, |
108 | dislike: this.videoService.setVideoDislike, | 109 | dislike: this.videoService.setVideoDislike, |
109 | none: this.videoService.unsetVideoLike | 110 | none: this.videoService.unsetVideoLike |
110 | } | 111 | } |
111 | 112 | ||
112 | ratingMethods[nextRating].call(this.videoService, this.video.uuid) | 113 | ratingMethods[nextRating].call(this.videoService, this.video.uuid, this.videoPassword) |
113 | .subscribe({ | 114 | .subscribe({ |
114 | next: () => { | 115 | next: () => { |
115 | // Update the video like attribute | 116 | // Update the video like attribute |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts index 033097084..1d9e10d0a 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts | |||
@@ -29,6 +29,7 @@ import { VideoCommentCreate } from '@shared/models' | |||
29 | export class VideoCommentAddComponent extends FormReactive implements OnChanges, OnInit { | 29 | export class VideoCommentAddComponent extends FormReactive implements OnChanges, OnInit { |
30 | @Input() user: User | 30 | @Input() user: User |
31 | @Input() video: Video | 31 | @Input() video: Video |
32 | @Input() videoPassword: string | ||
32 | @Input() parentComment?: VideoComment | 33 | @Input() parentComment?: VideoComment |
33 | @Input() parentComments?: VideoComment[] | 34 | @Input() parentComments?: VideoComment[] |
34 | @Input() focusOnInit = false | 35 | @Input() focusOnInit = false |
@@ -176,12 +177,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges, | |||
176 | 177 | ||
177 | private addCommentReply (commentCreate: VideoCommentCreate) { | 178 | private addCommentReply (commentCreate: VideoCommentCreate) { |
178 | return this.videoCommentService | 179 | return this.videoCommentService |
179 | .addCommentReply(this.video.uuid, this.parentComment.id, commentCreate) | 180 | .addCommentReply({ |
181 | videoId: this.video.uuid, | ||
182 | inReplyToCommentId: this.parentComment.id, | ||
183 | comment: commentCreate, | ||
184 | videoPassword: this.videoPassword | ||
185 | }) | ||
180 | } | 186 | } |
181 | 187 | ||
182 | private addCommentThread (commentCreate: VideoCommentCreate) { | 188 | private addCommentThread (commentCreate: VideoCommentCreate) { |
183 | return this.videoCommentService | 189 | return this.videoCommentService |
184 | .addCommentThread(this.video.uuid, commentCreate) | 190 | .addCommentThread(this.video.uuid, commentCreate, this.videoPassword) |
185 | } | 191 | } |
186 | 192 | ||
187 | private initTextValue () { | 193 | private initTextValue () { |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html index 91bd8309c..80ea22a20 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html | |||
@@ -62,6 +62,7 @@ | |||
62 | *ngIf="!comment.isDeleted && inReplyToCommentId === comment.id" | 62 | *ngIf="!comment.isDeleted && inReplyToCommentId === comment.id" |
63 | [user]="user" | 63 | [user]="user" |
64 | [video]="video" | 64 | [video]="video" |
65 | [videoPassword]="videoPassword" | ||
65 | [parentComment]="comment" | 66 | [parentComment]="comment" |
66 | [parentComments]="newParentComments" | 67 | [parentComments]="newParentComments" |
67 | [focusOnInit]="true" | 68 | [focusOnInit]="true" |
@@ -75,6 +76,7 @@ | |||
75 | <my-video-comment | 76 | <my-video-comment |
76 | [comment]="commentChild.comment" | 77 | [comment]="commentChild.comment" |
77 | [video]="video" | 78 | [video]="video" |
79 | [videoPassword]="videoPassword" | ||
78 | [inReplyToCommentId]="inReplyToCommentId" | 80 | [inReplyToCommentId]="inReplyToCommentId" |
79 | [commentTree]="commentChild" | 81 | [commentTree]="commentChild" |
80 | [parentComments]="newParentComments" | 82 | [parentComments]="newParentComments" |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts index 191ec4a28..4c85df657 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts | |||
@@ -16,6 +16,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
16 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent | 16 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent |
17 | 17 | ||
18 | @Input() video: Video | 18 | @Input() video: Video |
19 | @Input() videoPassword: string | ||
19 | @Input() comment: VideoComment | 20 | @Input() comment: VideoComment |
20 | @Input() parentComments: VideoComment[] = [] | 21 | @Input() parentComments: VideoComment[] = [] |
21 | @Input() commentTree: VideoCommentThreadTree | 22 | @Input() commentTree: VideoCommentThreadTree |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html index a003a10eb..0932d2b7f 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html | |||
@@ -20,6 +20,7 @@ | |||
20 | <ng-template [ngIf]="video.commentsEnabled === true"> | 20 | <ng-template [ngIf]="video.commentsEnabled === true"> |
21 | <my-video-comment-add | 21 | <my-video-comment-add |
22 | [video]="video" | 22 | [video]="video" |
23 | [videoPassword]="videoPassword" | ||
23 | [user]="user" | 24 | [user]="user" |
24 | (commentCreated)="onCommentThreadCreated($event)" | 25 | (commentCreated)="onCommentThreadCreated($event)" |
25 | [textValue]="commentThreadRedraftValue" | 26 | [textValue]="commentThreadRedraftValue" |
@@ -34,6 +35,7 @@ | |||
34 | *ngIf="highlightedThread" | 35 | *ngIf="highlightedThread" |
35 | [comment]="highlightedThread" | 36 | [comment]="highlightedThread" |
36 | [video]="video" | 37 | [video]="video" |
38 | [videoPassword]="videoPassword" | ||
37 | [inReplyToCommentId]="inReplyToCommentId" | 39 | [inReplyToCommentId]="inReplyToCommentId" |
38 | [commentTree]="threadComments[highlightedThread.id]" | 40 | [commentTree]="threadComments[highlightedThread.id]" |
39 | [highlightedComment]="true" | 41 | [highlightedComment]="true" |
@@ -53,6 +55,7 @@ | |||
53 | *ngIf="!highlightedThread || comment.id !== highlightedThread.id" | 55 | *ngIf="!highlightedThread || comment.id !== highlightedThread.id" |
54 | [comment]="comment" | 56 | [comment]="comment" |
55 | [video]="video" | 57 | [video]="video" |
58 | [videoPassword]="videoPassword" | ||
56 | [inReplyToCommentId]="inReplyToCommentId" | 59 | [inReplyToCommentId]="inReplyToCommentId" |
57 | [commentTree]="threadComments[comment.id]" | 60 | [commentTree]="threadComments[comment.id]" |
58 | [firstInThread]="i + 1 !== comments.length" | 61 | [firstInThread]="i + 1 !== comments.length" |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts index 96bdb28c9..848936f91 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts | |||
@@ -15,6 +15,7 @@ import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' | |||
15 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | 15 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { |
16 | @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef | 16 | @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef |
17 | @Input() video: VideoDetails | 17 | @Input() video: VideoDetails |
18 | @Input() videoPassword: string | ||
18 | @Input() user: User | 19 | @Input() user: User |
19 | 20 | ||
20 | @Output() timestampClicked = new EventEmitter<number>() | 21 | @Output() timestampClicked = new EventEmitter<number>() |
@@ -80,7 +81,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
80 | 81 | ||
81 | const params = { | 82 | const params = { |
82 | videoId: this.video.uuid, | 83 | videoId: this.video.uuid, |
83 | threadId: commentId | 84 | threadId: commentId, |
85 | videoPassword: this.videoPassword | ||
84 | } | 86 | } |
85 | 87 | ||
86 | const obs = this.hooks.wrapObsFun( | 88 | const obs = this.hooks.wrapObsFun( |
@@ -119,6 +121,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
119 | loadMoreThreads () { | 121 | loadMoreThreads () { |
120 | const params = { | 122 | const params = { |
121 | videoId: this.video.uuid, | 123 | videoId: this.video.uuid, |
124 | videoPassword: this.videoPassword, | ||
122 | componentPagination: this.componentPagination, | 125 | componentPagination: this.componentPagination, |
123 | sort: this.sort | 126 | sort: this.sort |
124 | } | 127 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html index 79b83811d..45e222743 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html | |||
@@ -42,3 +42,7 @@ | |||
42 | <div class="blocked-label" i18n>This video is blocked.</div> | 42 | <div class="blocked-label" i18n>This video is blocked.</div> |
43 | {{ video.blacklistedReason }} | 43 | {{ video.blacklistedReason }} |
44 | </div> | 44 | </div> |
45 | |||
46 | <div i18n class="alert alert-warning" *ngIf="video?.canAccessPasswordProtectedVideoWithoutPassword(user)"> | ||
47 | This video is password protected. | ||
48 | </div> | ||
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts index ba79fabc8..8781ead7e 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { AuthUser } from '@app/core' | ||
2 | import { VideoDetails } from '@app/shared/shared-main' | 3 | import { VideoDetails } from '@app/shared/shared-main' |
3 | import { VideoState } from '@shared/models' | 4 | import { VideoPrivacy, VideoState } from '@shared/models' |
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-video-alert', | 7 | selector: 'my-video-alert', |
@@ -8,6 +9,7 @@ import { VideoState } from '@shared/models' | |||
8 | styleUrls: [ './video-alert.component.scss' ] | 9 | styleUrls: [ './video-alert.component.scss' ] |
9 | }) | 10 | }) |
10 | export class VideoAlertComponent { | 11 | export class VideoAlertComponent { |
12 | @Input() user: AuthUser | ||
11 | @Input() video: VideoDetails | 13 | @Input() video: VideoDetails |
12 | @Input() noPlaylistVideoFound: boolean | 14 | @Input() noPlaylistVideoFound: boolean |
13 | 15 | ||
@@ -46,4 +48,8 @@ export class VideoAlertComponent { | |||
46 | isLiveEnded () { | 48 | isLiveEnded () { |
47 | return this.video?.state.id === VideoState.LIVE_ENDED | 49 | return this.video?.state.id === VideoState.LIVE_ENDED |
48 | } | 50 | } |
51 | |||
52 | isVideoPasswordProtected () { | ||
53 | return this.video?.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
54 | } | ||
49 | } | 55 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts index ec85db0ff..97d71a510 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts | |||
@@ -152,12 +152,24 @@ export class VideoWatchPlaylistComponent { | |||
152 | this.onPlaylistVideosNearOfBottom(position) | 152 | this.onPlaylistVideosNearOfBottom(position) |
153 | } | 153 | } |
154 | 154 | ||
155 | // --------------------------------------------------------------------------- | ||
156 | |||
155 | hasPreviousVideo () { | 157 | hasPreviousVideo () { |
156 | return !!this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | 158 | return !!this.getPreviousVideo() |
159 | } | ||
160 | |||
161 | getPreviousVideo () { | ||
162 | return this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | ||
157 | } | 163 | } |
158 | 164 | ||
165 | // --------------------------------------------------------------------------- | ||
166 | |||
159 | hasNextVideo () { | 167 | hasNextVideo () { |
160 | return !!this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | 168 | return !!this.getNextVideo() |
169 | } | ||
170 | |||
171 | getNextVideo () { | ||
172 | return this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | ||
161 | } | 173 | } |
162 | 174 | ||
163 | navigateToPreviousPlaylistVideo () { | 175 | navigateToPreviousPlaylistVideo () { |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index 461891779..294ff4b3a 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | </div> | 8 | </div> |
9 | 9 | ||
10 | <div id="videojs-wrapper"> | 10 | <div id="videojs-wrapper"> |
11 | <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> | 11 | <video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video> |
12 | </div> | 12 | </div> |
13 | 13 | ||
14 | <my-video-watch-playlist | 14 | <my-video-watch-playlist |
@@ -19,7 +19,7 @@ | |||
19 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> | 19 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> |
20 | </div> | 20 | </div> |
21 | 21 | ||
22 | <my-video-alert [video]="video" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert> | 22 | <my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert> |
23 | 23 | ||
24 | <!-- Video information --> | 24 | <!-- Video information --> |
25 | <div *ngIf="video" class="margin-content video-bottom"> | 25 | <div *ngIf="video" class="margin-content video-bottom"> |
@@ -51,8 +51,8 @@ | |||
51 | </div> | 51 | </div> |
52 | 52 | ||
53 | <my-action-buttons | 53 | <my-action-buttons |
54 | [video]="video" [isUserLoggedIn]="isUserLoggedIn()" [videoCaptions]="videoCaptions" [playlist]="playlist" | 54 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions" |
55 | [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" | 55 | [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" |
56 | ></my-action-buttons> | 56 | ></my-action-buttons> |
57 | </div> | 57 | </div> |
58 | </div> | 58 | </div> |
@@ -92,6 +92,7 @@ | |||
92 | <my-video-comments | 92 | <my-video-comments |
93 | class="border-top" | 93 | class="border-top" |
94 | [video]="video" | 94 | [video]="video" |
95 | [videoPassword]="videoPassword" | ||
95 | [user]="user" | 96 | [user]="user" |
96 | (timestampClicked)="handleTimestampClicked($event)" | 97 | (timestampClicked)="handleTimestampClicked($event)" |
97 | ></my-video-comments> | 98 | ></my-video-comments> |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 19ad97d42..aebec52fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' | 2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' |
3 | import { VideoJsPlayer } from 'video.js' | ||
4 | import { PlatformLocation } from '@angular/common' | 3 | import { PlatformLocation } from '@angular/common' |
5 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
6 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -19,13 +18,13 @@ import { | |||
19 | UserService | 18 | UserService |
20 | } from '@app/core' | 19 | } from '@app/core' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 20 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 23 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 24 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 26 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' | 27 | import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 28 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 29 | import { |
31 | HTMLServerConfig, | 30 | HTMLServerConfig, |
@@ -33,15 +32,16 @@ import { | |||
33 | LiveVideo, | 32 | LiveVideo, |
34 | PeerTubeProblemDocument, | 33 | PeerTubeProblemDocument, |
35 | ServerErrorCode, | 34 | ServerErrorCode, |
35 | Storyboard, | ||
36 | VideoCaption, | 36 | VideoCaption, |
37 | VideoPrivacy, | 37 | VideoPrivacy, |
38 | VideoState | 38 | VideoState |
39 | } from '@shared/models' | 39 | } from '@shared/models' |
40 | import { | 40 | import { |
41 | CustomizationOptions, | 41 | HLSOptions, |
42 | P2PMediaLoaderOptions, | 42 | PeerTubePlayer, |
43 | PeertubePlayerManager, | 43 | PeerTubePlayerContructorOptions, |
44 | PeertubePlayerManagerOptions, | 44 | PeerTubePlayerLoadOptions, |
45 | PlayerMode, | 45 | PlayerMode, |
46 | videojs | 46 | videojs |
47 | } from '../../../assets/player' | 47 | } from '../../../assets/player' |
@@ -49,7 +49,24 @@ import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from | |||
49 | import { environment } from '../../../environments/environment' | 49 | import { environment } from '../../../environments/environment' |
50 | import { VideoWatchPlaylistComponent } from './shared' | 50 | import { VideoWatchPlaylistComponent } from './shared' |
51 | 51 | ||
52 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 52 | type URLOptions = { |
53 | playerMode: PlayerMode | ||
54 | |||
55 | startTime: number | string | ||
56 | stopTime: number | string | ||
57 | |||
58 | controls?: boolean | ||
59 | controlBar?: boolean | ||
60 | |||
61 | muted?: boolean | ||
62 | loop?: boolean | ||
63 | subtitle?: string | ||
64 | resume?: string | ||
65 | |||
66 | peertubeLink: boolean | ||
67 | |||
68 | playbackRate?: number | string | ||
69 | } | ||
53 | 70 | ||
54 | @Component({ | 71 | @Component({ |
55 | selector: 'my-video-watch', | 72 | selector: 'my-video-watch', |
@@ -59,15 +76,16 @@ type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | |||
59 | export class VideoWatchComponent implements OnInit, OnDestroy { | 76 | export class VideoWatchComponent implements OnInit, OnDestroy { |
60 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 77 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
61 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 78 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
79 | @ViewChild('playerElement') playerElement: ElementRef<HTMLVideoElement> | ||
62 | 80 | ||
63 | player: VideoJsPlayer | 81 | peertubePlayer: PeerTubePlayer |
64 | playerElement: HTMLVideoElement | ||
65 | playerPlaceholderImgSrc: string | ||
66 | theaterEnabled = false | 82 | theaterEnabled = false |
67 | 83 | ||
68 | video: VideoDetails = null | 84 | video: VideoDetails = null |
69 | videoCaptions: VideoCaption[] = [] | 85 | videoCaptions: VideoCaption[] = [] |
70 | liveVideo: LiveVideo | 86 | liveVideo: LiveVideo |
87 | videoPassword: string | ||
88 | storyboards: Storyboard[] = [] | ||
71 | 89 | ||
72 | playlistPosition: number | 90 | playlistPosition: number |
73 | playlist: VideoPlaylist = null | 91 | playlist: VideoPlaylist = null |
@@ -75,8 +93,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
75 | remoteServerDown = false | 93 | remoteServerDown = false |
76 | noPlaylistVideoFound = false | 94 | noPlaylistVideoFound = false |
77 | 95 | ||
78 | private nextVideoUUID = '' | 96 | private nextRecommendedVideoUUID = '' |
79 | private nextVideoTitle = '' | 97 | private nextRecommendedVideoTitle = '' |
80 | 98 | ||
81 | private videoFileToken: string | 99 | private videoFileToken: string |
82 | 100 | ||
@@ -127,11 +145,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
127 | return this.userService.getAnonymousUser() | 145 | return this.userService.getAnonymousUser() |
128 | } | 146 | } |
129 | 147 | ||
130 | ngOnInit () { | 148 | async ngOnInit () { |
131 | this.serverConfig = this.serverService.getHTMLConfig() | 149 | this.serverConfig = this.serverService.getHTMLConfig() |
132 | 150 | ||
133 | PeertubePlayerManager.initState() | ||
134 | |||
135 | this.loadRouteParams() | 151 | this.loadRouteParams() |
136 | this.loadRouteQuery() | 152 | this.loadRouteQuery() |
137 | 153 | ||
@@ -140,10 +156,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 156 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
141 | 157 | ||
142 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI | 158 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI |
159 | |||
160 | const constructorOptions = await this.hooks.wrapFun( | ||
161 | this.buildPeerTubePlayerConstructorOptions.bind(this), | ||
162 | { urlOptions: this.getUrlOptions() }, | ||
163 | 'video-watch', | ||
164 | 'filter:internal.video-watch.player.build-options.params', | ||
165 | 'filter:internal.video-watch.player.build-options.result' | ||
166 | ) | ||
167 | |||
168 | this.peertubePlayer = new PeerTubePlayer(constructorOptions) | ||
143 | } | 169 | } |
144 | 170 | ||
145 | ngOnDestroy () { | 171 | ngOnDestroy () { |
146 | this.flushPlayer() | 172 | if (this.peertubePlayer) this.peertubePlayer.destroy() |
147 | 173 | ||
148 | // Unsubscribe subscriptions | 174 | // Unsubscribe subscriptions |
149 | if (this.paramsSub) this.paramsSub.unsubscribe() | 175 | if (this.paramsSub) this.paramsSub.unsubscribe() |
@@ -168,14 +194,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
168 | 194 | ||
169 | // The recommended videos's first element should be the next video | 195 | // The recommended videos's first element should be the next video |
170 | const video = videos[0] | 196 | const video = videos[0] |
171 | this.nextVideoUUID = video.uuid | 197 | this.nextRecommendedVideoUUID = video.uuid |
172 | this.nextVideoTitle = video.name | 198 | this.nextRecommendedVideoTitle = video.name |
173 | } | 199 | } |
174 | 200 | ||
175 | handleTimestampClicked (timestamp: number) { | 201 | handleTimestampClicked (timestamp: number) { |
176 | if (!this.player || this.video.isLive) return | 202 | if (!this.peertubePlayer || this.video.isLive) return |
177 | 203 | ||
178 | this.player.currentTime(timestamp) | 204 | this.peertubePlayer.getPlayer().currentTime(timestamp) |
179 | scrollToTop() | 205 | scrollToTop() |
180 | } | 206 | } |
181 | 207 | ||
@@ -191,6 +217,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
191 | return this.authService.isLoggedIn() | 217 | return this.authService.isLoggedIn() |
192 | } | 218 | } |
193 | 219 | ||
220 | isUserOwner () { | ||
221 | return this.video.isLocal === true && this.video.account.name === this.user?.username | ||
222 | } | ||
223 | |||
194 | isVideoBlur (video: Video) { | 224 | isVideoBlur (video: Video) { |
195 | return video.isVideoNSFWForUser(this.user, this.serverConfig) | 225 | return video.isVideoNSFWForUser(this.user, this.serverConfig) |
196 | } | 226 | } |
@@ -236,25 +266,24 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
236 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) | 266 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) |
237 | 267 | ||
238 | const start = queryParams['start'] | 268 | const start = queryParams['start'] |
239 | if (this.player && start) this.player.currentTime(parseInt(start, 10)) | 269 | if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10)) |
240 | }) | 270 | }) |
241 | } | 271 | } |
242 | 272 | ||
243 | private loadVideo (options: { | 273 | private loadVideo (options: { |
244 | videoId: string | 274 | videoId: string |
245 | forceAutoplay: boolean | 275 | forceAutoplay: boolean |
276 | videoPassword?: string | ||
246 | }) { | 277 | }) { |
247 | const { videoId, forceAutoplay } = options | 278 | const { videoId, forceAutoplay, videoPassword } = options |
248 | 279 | ||
249 | if (this.isSameElement(this.video, videoId)) return | 280 | if (this.isSameElement(this.video, videoId)) return |
250 | 281 | ||
251 | if (this.player) this.player.pause() | ||
252 | |||
253 | this.video = undefined | 282 | this.video = undefined |
254 | 283 | ||
255 | const videoObs = this.hooks.wrapObsFun( | 284 | const videoObs = this.hooks.wrapObsFun( |
256 | this.videoService.getVideo.bind(this.videoService), | 285 | this.videoService.getVideo.bind(this.videoService), |
257 | { videoId }, | 286 | { videoId, videoPassword }, |
258 | 'video-watch', | 287 | 'video-watch', |
259 | 'filter:api.video-watch.video.get.params', | 288 | 'filter:api.video-watch.video.get.params', |
260 | 'filter:api.video-watch.video.get.result' | 289 | 'filter:api.video-watch.video.get.result' |
@@ -269,48 +298,44 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
269 | }), | 298 | }), |
270 | 299 | ||
271 | switchMap(({ video, live }) => { | 300 | switchMap(({ video, live }) => { |
272 | if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined }) | 301 | if (!videoRequiresFileToken(video)) return of({ video, live, videoFileToken: undefined }) |
273 | 302 | ||
274 | return this.videoFileTokenService.getVideoFileToken(video.uuid) | 303 | return this.videoFileTokenService.getVideoFileToken({ videoUUID: video.uuid, videoPassword }) |
275 | .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) | 304 | .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) |
276 | }) | 305 | }) |
277 | ) | 306 | ) |
278 | 307 | ||
279 | forkJoin([ | 308 | forkJoin([ |
280 | videoAndLiveObs, | 309 | videoAndLiveObs, |
281 | this.videoCaptionService.listCaptions(videoId), | 310 | this.videoCaptionService.listCaptions(videoId, videoPassword), |
311 | this.videoService.getStoryboards(videoId, videoPassword), | ||
282 | this.userService.getAnonymousOrLoggedUser() | 312 | this.userService.getAnonymousOrLoggedUser() |
283 | ]).subscribe({ | 313 | ]).subscribe({ |
284 | next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => { | 314 | next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => { |
285 | const queryParams = this.route.snapshot.queryParams | ||
286 | |||
287 | const urlOptions = { | ||
288 | resume: queryParams.resume, | ||
289 | |||
290 | startTime: queryParams.start, | ||
291 | stopTime: queryParams.stop, | ||
292 | |||
293 | muted: queryParams.muted, | ||
294 | loop: queryParams.loop, | ||
295 | subtitle: queryParams.subtitle, | ||
296 | |||
297 | playerMode: queryParams.mode, | ||
298 | playbackRate: queryParams.playbackRate, | ||
299 | peertubeLink: false | ||
300 | } | ||
301 | |||
302 | this.onVideoFetched({ | 315 | this.onVideoFetched({ |
303 | video, | 316 | video, |
304 | live, | 317 | live, |
305 | videoCaptions: captionsResult.data, | 318 | videoCaptions: captionsResult.data, |
319 | storyboards, | ||
306 | videoFileToken, | 320 | videoFileToken, |
321 | videoPassword, | ||
307 | loggedInOrAnonymousUser, | 322 | loggedInOrAnonymousUser, |
308 | urlOptions, | ||
309 | forceAutoplay | 323 | forceAutoplay |
310 | }).catch(err => this.handleGlobalError(err)) | 324 | }).catch(err => { |
325 | this.handleGlobalError(err) | ||
326 | }) | ||
311 | }, | 327 | }, |
328 | error: async err => { | ||
329 | if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD || err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) { | ||
330 | const { confirmed, password } = await this.handleVideoPasswordError(err) | ||
312 | 331 | ||
313 | error: err => this.handleRequestError(err) | 332 | if (confirmed === false) return this.location.back() |
333 | |||
334 | this.loadVideo({ ...options, videoPassword: password }) | ||
335 | } else { | ||
336 | this.handleRequestError(err) | ||
337 | } | ||
338 | } | ||
314 | }) | 339 | }) |
315 | } | 340 | } |
316 | 341 | ||
@@ -364,28 +389,47 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
364 | const errorMessage: string = typeof err === 'string' ? err : err.message | 389 | const errorMessage: string = typeof err === 'string' ? err : err.message |
365 | if (!errorMessage) return | 390 | if (!errorMessage) return |
366 | 391 | ||
367 | // Display a message in the video player instead of a notification | 392 | this.notifier.error(errorMessage) |
368 | if (errorMessage.includes('from xs param')) { | 393 | } |
369 | this.flushPlayer() | ||
370 | this.remoteServerDown = true | ||
371 | 394 | ||
372 | return | 395 | private handleVideoPasswordError (err: any) { |
396 | let isIncorrectPassword: boolean | ||
397 | |||
398 | if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) { | ||
399 | isIncorrectPassword = false | ||
400 | } else if (err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) { | ||
401 | this.videoPassword = undefined | ||
402 | isIncorrectPassword = true | ||
373 | } | 403 | } |
374 | 404 | ||
375 | this.notifier.error(errorMessage) | 405 | return this.confirmService.confirmWithPassword({ |
406 | message: $localize`You need a password to watch this video`, | ||
407 | title: $localize`This video is password protected`, | ||
408 | errorMessage: isIncorrectPassword ? $localize`Incorrect password, please enter a correct password` : '' | ||
409 | }) | ||
376 | } | 410 | } |
377 | 411 | ||
378 | private async onVideoFetched (options: { | 412 | private async onVideoFetched (options: { |
379 | video: VideoDetails | 413 | video: VideoDetails |
380 | live: LiveVideo | 414 | live: LiveVideo |
381 | videoCaptions: VideoCaption[] | 415 | videoCaptions: VideoCaption[] |
416 | storyboards: Storyboard[] | ||
382 | videoFileToken: string | 417 | videoFileToken: string |
418 | videoPassword: string | ||
383 | 419 | ||
384 | urlOptions: URLOptions | ||
385 | loggedInOrAnonymousUser: User | 420 | loggedInOrAnonymousUser: User |
386 | forceAutoplay: boolean | 421 | forceAutoplay: boolean |
387 | }) { | 422 | }) { |
388 | const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser, forceAutoplay } = options | 423 | const { |
424 | video, | ||
425 | live, | ||
426 | videoCaptions, | ||
427 | storyboards, | ||
428 | videoFileToken, | ||
429 | videoPassword, | ||
430 | loggedInOrAnonymousUser, | ||
431 | forceAutoplay | ||
432 | } = options | ||
389 | 433 | ||
390 | this.subscribeToLiveEventsIfNeeded(this.video, video) | 434 | this.subscribeToLiveEventsIfNeeded(this.video, video) |
391 | 435 | ||
@@ -393,9 +437,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
393 | this.videoCaptions = videoCaptions | 437 | this.videoCaptions = videoCaptions |
394 | this.liveVideo = live | 438 | this.liveVideo = live |
395 | this.videoFileToken = videoFileToken | 439 | this.videoFileToken = videoFileToken |
440 | this.videoPassword = videoPassword | ||
441 | this.storyboards = storyboards | ||
396 | 442 | ||
397 | // Re init attributes | 443 | // Re init attributes |
398 | this.playerPlaceholderImgSrc = undefined | ||
399 | this.remoteServerDown = false | 444 | this.remoteServerDown = false |
400 | this.currentTime = undefined | 445 | this.currentTime = undefined |
401 | 446 | ||
@@ -409,7 +454,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
409 | 454 | ||
410 | this.buildHotkeysHelp(video) | 455 | this.buildHotkeysHelp(video) |
411 | 456 | ||
412 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 457 | this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay }) |
413 | .catch(err => logger.error('Cannot build the player', err)) | 458 | .catch(err => logger.error('Cannot build the player', err)) |
414 | 459 | ||
415 | this.setOpenGraphTags() | 460 | this.setOpenGraphTags() |
@@ -422,114 +467,70 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
422 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) | 467 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) |
423 | } | 468 | } |
424 | 469 | ||
425 | private async buildPlayer (options: { | 470 | private async loadPlayer (options: { |
426 | urlOptions: URLOptions | ||
427 | loggedInOrAnonymousUser: User | 471 | loggedInOrAnonymousUser: User |
428 | forceAutoplay: boolean | 472 | forceAutoplay: boolean |
429 | }) { | 473 | }) { |
430 | const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options | 474 | const { loggedInOrAnonymousUser, forceAutoplay } = options |
431 | |||
432 | // Flush old player if needed | ||
433 | this.flushPlayer() | ||
434 | 475 | ||
435 | const videoState = this.video.state.id | 476 | const videoState = this.video.state.id |
436 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { | 477 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { |
437 | this.playerPlaceholderImgSrc = this.video.previewPath | 478 | this.updatePlayerOnNoLive() |
438 | return | 479 | return |
439 | } | 480 | } |
440 | 481 | ||
441 | // Build video element, because videojs removes it on dispose | 482 | this.peertubePlayer?.enable() |
442 | const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper') | ||
443 | this.playerElement = document.createElement('video') | ||
444 | this.playerElement.className = 'video-js vjs-peertube-skin' | ||
445 | this.playerElement.setAttribute('playsinline', 'true') | ||
446 | playerElementWrapper.appendChild(this.playerElement) | ||
447 | 483 | ||
448 | const params = { | 484 | const params = { |
449 | video: this.video, | 485 | video: this.video, |
450 | videoCaptions: this.videoCaptions, | 486 | videoCaptions: this.videoCaptions, |
487 | storyboards: this.storyboards, | ||
451 | liveVideo: this.liveVideo, | 488 | liveVideo: this.liveVideo, |
452 | videoFileToken: this.videoFileToken, | 489 | videoFileToken: this.videoFileToken, |
453 | urlOptions, | 490 | videoPassword: this.videoPassword, |
491 | urlOptions: this.getUrlOptions(), | ||
454 | loggedInOrAnonymousUser, | 492 | loggedInOrAnonymousUser, |
455 | forceAutoplay, | 493 | forceAutoplay, |
456 | user: this.user | 494 | user: this.user |
457 | } | 495 | } |
458 | const { playerMode, playerOptions } = await this.hooks.wrapFun( | 496 | |
459 | this.buildPlayerManagerOptions.bind(this), | 497 | const loadOptions = await this.hooks.wrapFun( |
498 | this.buildPeerTubePlayerLoadOptions.bind(this), | ||
460 | params, | 499 | params, |
461 | 'video-watch', | 500 | 'video-watch', |
462 | 'filter:internal.video-watch.player.build-options.params', | 501 | 'filter:internal.video-watch.player.load-options.params', |
463 | 'filter:internal.video-watch.player.build-options.result' | 502 | 'filter:internal.video-watch.player.load-options.result' |
464 | ) | 503 | ) |
465 | 504 | ||
466 | this.zone.runOutsideAngular(async () => { | 505 | this.zone.runOutsideAngular(async () => { |
467 | this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player) | 506 | await this.peertubePlayer.load(loadOptions) |
468 | 507 | ||
469 | this.player.on('customError', (_e, data: any) => { | 508 | const player = this.peertubePlayer.getPlayer() |
470 | this.zone.run(() => this.handleGlobalError(data.err)) | ||
471 | }) | ||
472 | 509 | ||
473 | this.player.on('timeupdate', () => { | 510 | player.on('timeupdate', () => { |
474 | // Don't need to trigger angular change for this variable, that is sent to children components on click | 511 | // Don't need to trigger angular change for this variable, that is sent to children components on click |
475 | this.currentTime = Math.floor(this.player.currentTime()) | 512 | this.currentTime = Math.floor(player.currentTime()) |
476 | }) | 513 | }) |
477 | 514 | ||
478 | /** | 515 | if (this.video.isLive) { |
479 | * condition: true to make the upnext functionality trigger, false to disable the upnext functionality | 516 | player.one('ended', () => { |
480 | * go to the next video in 'condition()' if you don't want of the timer. | 517 | this.zone.run(() => { |
481 | * next: function triggered at the end of the timer. | 518 | // We changed the video, it's not a live anymore |
482 | * suspended: function used at each click of the timer checking if we need to reset progress | 519 | if (!this.video.isLive) return |
483 | * and wait until suspended becomes truthy again. | ||
484 | */ | ||
485 | this.player.upnext({ | ||
486 | timeout: 5000, // 5s | ||
487 | |||
488 | headText: $localize`Up Next`, | ||
489 | cancelText: $localize`Cancel`, | ||
490 | suspendedText: $localize`Autoplay is suspended`, | ||
491 | |||
492 | getTitle: () => this.nextVideoTitle, | ||
493 | 520 | ||
494 | next: () => this.zone.run(() => this.playNextVideoInAngularZone()), | 521 | this.video.state.id = VideoState.LIVE_ENDED |
495 | condition: () => { | ||
496 | if (!this.playlist) return this.isAutoPlayNext() | ||
497 | 522 | ||
498 | // Don't wait timeout to play the next playlist video | 523 | this.updatePlayerOnNoLive() |
499 | if (this.isPlaylistAutoPlayNext()) { | 524 | }) |
500 | this.playNextVideoInAngularZone() | 525 | }) |
501 | return undefined | 526 | } |
502 | } | ||
503 | |||
504 | return false | ||
505 | }, | ||
506 | |||
507 | suspended: () => { | ||
508 | return ( | ||
509 | !isXPercentInViewport(this.player.el() as HTMLElement, 80) || | ||
510 | !document.getElementById('content').contains(document.activeElement) | ||
511 | ) | ||
512 | } | ||
513 | }) | ||
514 | |||
515 | this.player.one('stopped', () => { | ||
516 | if (this.playlist && this.isPlaylistAutoPlayNext()) { | ||
517 | this.playNextVideoInAngularZone() | ||
518 | } | ||
519 | }) | ||
520 | |||
521 | this.player.one('ended', () => { | ||
522 | if (this.video.isLive) { | ||
523 | this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED) | ||
524 | } | ||
525 | }) | ||
526 | 527 | ||
527 | this.player.on('theaterChange', (_: any, enabled: boolean) => { | 528 | player.on('theater-change', (_: any, enabled: boolean) => { |
528 | this.zone.run(() => this.theaterEnabled = enabled) | 529 | this.zone.run(() => this.theaterEnabled = enabled) |
529 | }) | 530 | }) |
530 | 531 | ||
531 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { | 532 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { |
532 | player: this.player, | 533 | player, |
533 | playlist: this.playlist, | 534 | playlist: this.playlist, |
534 | playlistPosition: this.playlistPosition, | 535 | playlistPosition: this.playlistPosition, |
535 | videojs, | 536 | videojs, |
@@ -546,15 +547,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
546 | return true | 547 | return true |
547 | } | 548 | } |
548 | 549 | ||
549 | private playNextVideoInAngularZone () { | 550 | private getNextVideoTitle () { |
550 | if (this.playlist) { | 551 | if (this.playlist) { |
551 | this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) | 552 | return this.videoWatchPlaylist.getNextVideo()?.video?.name || '' |
552 | return | ||
553 | } | 553 | } |
554 | 554 | ||
555 | if (this.nextVideoUUID) { | 555 | return this.nextRecommendedVideoTitle |
556 | this.router.navigate([ '/w', this.nextVideoUUID ]) | 556 | } |
557 | } | 557 | |
558 | private playNextVideoInAngularZone () { | ||
559 | this.zone.run(() => { | ||
560 | if (this.playlist) { | ||
561 | this.videoWatchPlaylist.navigateToNextPlaylistVideo() | ||
562 | return | ||
563 | } | ||
564 | |||
565 | if (this.nextRecommendedVideoUUID) { | ||
566 | this.router.navigate([ '/w', this.nextRecommendedVideoUUID ]) | ||
567 | } | ||
568 | }) | ||
558 | } | 569 | } |
559 | 570 | ||
560 | private isAutoplay () { | 571 | private isAutoplay () { |
@@ -582,32 +593,93 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
582 | ) | 593 | ) |
583 | } | 594 | } |
584 | 595 | ||
585 | private flushPlayer () { | 596 | private buildPeerTubePlayerConstructorOptions (options: { |
586 | // Remove player if it exists | 597 | urlOptions: URLOptions |
587 | if (!this.player) return | 598 | }): PeerTubePlayerContructorOptions { |
599 | const { urlOptions } = options | ||
600 | |||
601 | return { | ||
602 | playerElement: () => this.playerElement.nativeElement, | ||
603 | |||
604 | enableHotkeys: true, | ||
605 | inactivityTimeout: 2500, | ||
606 | |||
607 | theaterButton: true, | ||
608 | |||
609 | controls: urlOptions.controls, | ||
610 | controlBar: urlOptions.controlBar, | ||
611 | |||
612 | muted: urlOptions.muted, | ||
613 | loop: urlOptions.loop, | ||
614 | |||
615 | playbackRate: urlOptions.playbackRate, | ||
616 | |||
617 | instanceName: this.serverConfig.instance.name, | ||
618 | language: this.localeId, | ||
619 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | ||
620 | |||
621 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | ||
622 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | ||
588 | 623 | ||
589 | try { | 624 | serverUrl: environment.originServerUrl || window.location.origin, |
590 | this.player.dispose() | 625 | |
591 | this.player = undefined | 626 | errorNotifier: (message: string) => this.notifier.error(message), |
592 | } catch (err) { | 627 | |
593 | logger.error('Cannot dispose player.', err) | 628 | peertubeLink: () => false, |
629 | |||
630 | pluginsManager: this.pluginService.getPluginsManager() | ||
594 | } | 631 | } |
595 | } | 632 | } |
596 | 633 | ||
597 | private buildPlayerManagerOptions (params: { | 634 | private buildPeerTubePlayerLoadOptions (options: { |
598 | video: VideoDetails | 635 | video: VideoDetails |
599 | liveVideo: LiveVideo | 636 | liveVideo: LiveVideo |
600 | videoCaptions: VideoCaption[] | 637 | videoCaptions: VideoCaption[] |
638 | storyboards: Storyboard[] | ||
601 | 639 | ||
602 | videoFileToken: string | 640 | videoFileToken: string |
641 | videoPassword: string | ||
603 | 642 | ||
604 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 643 | urlOptions: URLOptions |
605 | 644 | ||
606 | loggedInOrAnonymousUser: User | 645 | loggedInOrAnonymousUser: User |
607 | forceAutoplay: boolean | 646 | forceAutoplay: boolean |
608 | user?: AuthUser // Keep for plugins | 647 | user?: AuthUser // Keep for plugins |
609 | }) { | 648 | }): PeerTubePlayerLoadOptions { |
610 | const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser, forceAutoplay } = params | 649 | const { |
650 | video, | ||
651 | liveVideo, | ||
652 | videoCaptions, | ||
653 | storyboards, | ||
654 | videoFileToken, | ||
655 | videoPassword, | ||
656 | urlOptions, | ||
657 | loggedInOrAnonymousUser, | ||
658 | forceAutoplay | ||
659 | } = options | ||
660 | |||
661 | let mode: PlayerMode | ||
662 | |||
663 | if (urlOptions.playerMode) { | ||
664 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | ||
665 | else mode = 'web-video' | ||
666 | } else { | ||
667 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
668 | else mode = 'web-video' | ||
669 | } | ||
670 | |||
671 | let hlsOptions: HLSOptions | ||
672 | if (video.hasHlsPlaylist()) { | ||
673 | const hlsPlaylist = video.getHlsPlaylist() | ||
674 | |||
675 | hlsOptions = { | ||
676 | playlistUrl: hlsPlaylist.playlistUrl, | ||
677 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
678 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
679 | trackerAnnounce: video.trackerUrls, | ||
680 | videoFiles: hlsPlaylist.files | ||
681 | } | ||
682 | } | ||
611 | 683 | ||
612 | const getStartTime = () => { | 684 | const getStartTime = () => { |
613 | const byUrl = urlOptions.startTime !== undefined | 685 | const byUrl = urlOptions.startTime !== undefined |
@@ -634,117 +706,93 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
634 | src: environment.apiUrl + c.captionPath | 706 | src: environment.apiUrl + c.captionPath |
635 | })) | 707 | })) |
636 | 708 | ||
709 | const storyboard = storyboards.length !== 0 | ||
710 | ? { | ||
711 | url: environment.apiUrl + storyboards[0].storyboardPath, | ||
712 | height: storyboards[0].spriteHeight, | ||
713 | width: storyboards[0].spriteWidth, | ||
714 | interval: storyboards[0].spriteDuration | ||
715 | } | ||
716 | : undefined | ||
717 | |||
637 | const liveOptions = video.isLive | 718 | const liveOptions = video.isLive |
638 | ? { latencyMode: liveVideo.latencyMode } | 719 | ? { latencyMode: liveVideo.latencyMode } |
639 | : undefined | 720 | : undefined |
640 | 721 | ||
641 | const options: PeertubePlayerManagerOptions = { | 722 | return { |
642 | common: { | 723 | mode, |
643 | autoplay: this.isAutoplay(), | ||
644 | forceAutoplay, | ||
645 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
646 | |||
647 | hasNextVideo: () => this.hasNextVideo(), | ||
648 | nextVideo: () => this.playNextVideoInAngularZone(), | ||
649 | |||
650 | playerElement: this.playerElement, | ||
651 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, | ||
652 | |||
653 | videoDuration: video.duration, | ||
654 | enableHotkeys: true, | ||
655 | inactivityTimeout: 2500, | ||
656 | poster: video.previewUrl, | ||
657 | |||
658 | startTime, | ||
659 | stopTime: urlOptions.stopTime, | ||
660 | controlBar: urlOptions.controlBar, | ||
661 | controls: urlOptions.controls, | ||
662 | muted: urlOptions.muted, | ||
663 | loop: urlOptions.loop, | ||
664 | subtitle: urlOptions.subtitle, | ||
665 | playbackRate: urlOptions.playbackRate, | ||
666 | 724 | ||
667 | peertubeLink: urlOptions.peertubeLink, | 725 | autoplay: this.isAutoplay(), |
726 | forceAutoplay, | ||
668 | 727 | ||
669 | theaterButton: true, | 728 | duration: this.video.duration, |
670 | captions: videoCaptions.length !== 0, | 729 | poster: video.previewUrl, |
730 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
671 | 731 | ||
672 | embedUrl: video.embedUrl, | 732 | startTime, |
673 | embedTitle: video.name, | 733 | stopTime: urlOptions.stopTime, |
674 | instanceName: this.serverConfig.instance.name, | ||
675 | 734 | ||
676 | isLive: video.isLive, | 735 | embedUrl: video.embedUrl, |
677 | liveOptions, | 736 | embedTitle: video.name, |
678 | 737 | ||
679 | language: this.localeId, | 738 | isLive: video.isLive, |
739 | liveOptions, | ||
680 | 740 | ||
681 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | 741 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE |
742 | ? this.videoService.getVideoViewUrl(video.uuid) | ||
743 | : null, | ||
682 | 744 | ||
683 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE | 745 | videoFileToken: () => videoFileToken, |
684 | ? this.videoService.getVideoViewUrl(video.uuid) | 746 | requiresUserAuth: videoRequiresUserAuth(video, videoPassword), |
685 | : null, | 747 | requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && |
686 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | 748 | !video.canAccessPasswordProtectedVideoWithoutPassword(this.user), |
687 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | 749 | videoPassword: () => videoPassword, |
688 | 750 | ||
689 | serverUrl: environment.originServerUrl || window.location.origin, | 751 | videoCaptions: playerCaptions, |
752 | storyboard, | ||
690 | 753 | ||
691 | videoFileToken: () => videoFileToken, | 754 | videoShortUUID: video.shortUUID, |
692 | requiresAuth: videoRequiresAuth(video), | 755 | videoUUID: video.uuid, |
693 | 756 | ||
694 | videoCaptions: playerCaptions, | 757 | previousVideo: { |
758 | enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(), | ||
695 | 759 | ||
696 | videoShortUUID: video.shortUUID, | 760 | handler: this.playlist |
697 | videoUUID: video.uuid, | 761 | ? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) |
762 | : undefined, | ||
698 | 763 | ||
699 | errorNotifier: (message: string) => this.notifier.error(message) | 764 | displayControlBarButton: !!this.playlist |
700 | }, | 765 | }, |
701 | 766 | ||
702 | webtorrent: { | 767 | nextVideo: { |
703 | videoFiles: video.files | 768 | enabled: this.hasNextVideo(), |
769 | handler: () => this.playNextVideoInAngularZone(), | ||
770 | getVideoTitle: () => this.getNextVideoTitle(), | ||
771 | displayControlBarButton: this.hasNextVideo() | ||
704 | }, | 772 | }, |
705 | 773 | ||
706 | pluginsManager: this.pluginService.getPluginsManager() | 774 | upnext: { |
707 | } | 775 | isEnabled: () => { |
776 | if (this.playlist) return this.isPlaylistAutoPlayNext() | ||
708 | 777 | ||
709 | // Only set this if we're in a playlist | 778 | return this.isAutoPlayNext() |
710 | if (this.playlist) { | 779 | }, |
711 | options.common.hasPreviousVideo = () => this.videoWatchPlaylist.hasPreviousVideo() | ||
712 | |||
713 | options.common.previousVideo = () => { | ||
714 | this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) | ||
715 | } | ||
716 | } | ||
717 | |||
718 | let mode: PlayerMode | ||
719 | |||
720 | if (urlOptions.playerMode) { | ||
721 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | ||
722 | else mode = 'webtorrent' | ||
723 | } else { | ||
724 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
725 | else mode = 'webtorrent' | ||
726 | } | ||
727 | 780 | ||
728 | // p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available | 781 | isSuspended: (player: videojs.Player) => { |
729 | if (typeof TextEncoder === 'undefined') { | 782 | return !isXPercentInViewport(player.el() as HTMLElement, 80) |
730 | mode = 'webtorrent' | 783 | }, |
731 | } | ||
732 | 784 | ||
733 | if (mode === 'p2p-media-loader') { | 785 | timeout: this.playlist |
734 | const hlsPlaylist = video.getHlsPlaylist() | 786 | ? 0 // Don't wait to play next video in playlist |
787 | : 5000 // 5 seconds for a recommended video | ||
788 | }, | ||
735 | 789 | ||
736 | const p2pMediaLoader = { | 790 | hls: hlsOptions, |
737 | playlistUrl: hlsPlaylist.playlistUrl, | ||
738 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
739 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
740 | trackerAnnounce: video.trackerUrls, | ||
741 | videoFiles: hlsPlaylist.files | ||
742 | } as P2PMediaLoaderOptions | ||
743 | 791 | ||
744 | Object.assign(options, { p2pMediaLoader }) | 792 | webVideo: { |
793 | videoFiles: video.files | ||
794 | } | ||
745 | } | 795 | } |
746 | |||
747 | return { playerMode: mode, playerOptions: options } | ||
748 | } | 796 | } |
749 | 797 | ||
750 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 798 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
@@ -792,6 +840,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
792 | this.video.viewers = newViewers | 840 | this.video.viewers = newViewers |
793 | } | 841 | } |
794 | 842 | ||
843 | private updatePlayerOnNoLive () { | ||
844 | this.peertubePlayer.unload() | ||
845 | this.peertubePlayer.disable() | ||
846 | this.peertubePlayer.setPoster(this.video.previewPath) | ||
847 | } | ||
848 | |||
795 | private buildHotkeysHelp (video: Video) { | 849 | private buildHotkeysHelp (video: Video) { |
796 | if (this.hotkeys.length !== 0) { | 850 | if (this.hotkeys.length !== 0) { |
797 | this.hotkeysService.remove(this.hotkeys) | 851 | this.hotkeysService.remove(this.hotkeys) |
@@ -863,4 +917,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
863 | this.metaService.setTag('og:url', window.location.href) | 917 | this.metaService.setTag('og:url', window.location.href) |
864 | this.metaService.setTag('url', window.location.href) | 918 | this.metaService.setTag('url', window.location.href) |
865 | } | 919 | } |
920 | |||
921 | private getUrlOptions (): URLOptions { | ||
922 | const queryParams = this.route.snapshot.queryParams | ||
923 | |||
924 | return { | ||
925 | resume: queryParams.resume, | ||
926 | |||
927 | startTime: queryParams.start, | ||
928 | stopTime: queryParams.stop, | ||
929 | |||
930 | muted: toBoolean(queryParams.muted), | ||
931 | loop: toBoolean(queryParams.loop), | ||
932 | subtitle: queryParams.subtitle, | ||
933 | |||
934 | playerMode: queryParams.mode, | ||
935 | playbackRate: queryParams.playbackRate, | ||
936 | |||
937 | controlBar: toBoolean(queryParams.controlBar), | ||
938 | |||
939 | peertubeLink: false | ||
940 | } | ||
941 | } | ||
866 | } | 942 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 7e4fac730..9339865f1 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -12,13 +12,14 @@ import { CoreModule, PluginService, RedirectService, ServerService } from './cor | |||
12 | import { EmptyComponent } from './empty.component' | 12 | import { EmptyComponent } from './empty.component' |
13 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' | 13 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' |
14 | import { HighlightPipe } from './header/highlight.pipe' | 14 | import { HighlightPipe } from './header/highlight.pipe' |
15 | import { polyfillICU } from './helpers' | ||
15 | import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu' | 16 | import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu' |
17 | import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component' | ||
18 | import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component' | ||
16 | import { ConfirmComponent } from './modal/confirm.component' | 19 | import { ConfirmComponent } from './modal/confirm.component' |
17 | import { CustomModalComponent } from './modal/custom-modal.component' | 20 | import { CustomModalComponent } from './modal/custom-modal.component' |
18 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' | 21 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' |
19 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' | 22 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' |
20 | import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component' | ||
21 | import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component' | ||
22 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' | 23 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' |
23 | import { SharedFormModule } from './shared/shared-forms' | 24 | import { SharedFormModule } from './shared/shared-forms' |
24 | import { SharedGlobalIconModule } from './shared/shared-icons' | 25 | import { SharedGlobalIconModule } from './shared/shared-icons' |
@@ -90,6 +91,11 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS | |||
90 | useFactory: loadConfigFactory, | 91 | useFactory: loadConfigFactory, |
91 | deps: [ ServerService, PluginService, RedirectService ], | 92 | deps: [ ServerService, PluginService, RedirectService ], |
92 | multi: true | 93 | multi: true |
94 | }, | ||
95 | { | ||
96 | provide: APP_INITIALIZER, | ||
97 | useFactory: () => polyfillICU, | ||
98 | multi: true | ||
93 | } | 99 | } |
94 | ] | 100 | ] |
95 | }) | 101 | }) |
diff --git a/client/src/app/core/confirm/confirm.service.ts b/client/src/app/core/confirm/confirm.service.ts index 89a25f0a5..abe163aae 100644 --- a/client/src/app/core/confirm/confirm.service.ts +++ b/client/src/app/core/confirm/confirm.service.ts | |||
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core' | |||
4 | type ConfirmOptions = { | 4 | type ConfirmOptions = { |
5 | title: string | 5 | title: string |
6 | message: string | 6 | message: string |
7 | errorMessage?: string | ||
7 | } & ( | 8 | } & ( |
8 | { | 9 | { |
9 | type: 'confirm' | 10 | type: 'confirm' |
@@ -12,6 +13,7 @@ type ConfirmOptions = { | |||
12 | { | 13 | { |
13 | type: 'confirm-password' | 14 | type: 'confirm-password' |
14 | confirmButtonText?: string | 15 | confirmButtonText?: string |
16 | isIncorrectPassword?: boolean | ||
15 | } | | 17 | } | |
16 | { | 18 | { |
17 | type: 'confirm-expected-input' | 19 | type: 'confirm-expected-input' |
@@ -32,8 +34,14 @@ export class ConfirmService { | |||
32 | return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable())) | 34 | return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable())) |
33 | } | 35 | } |
34 | 36 | ||
35 | confirmWithPassword (message: string, title = '', confirmButtonText?: string) { | 37 | confirmWithPassword (options: { |
36 | this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText }) | 38 | message: string |
39 | title?: string | ||
40 | confirmButtonText?: string | ||
41 | errorMessage?: string | ||
42 | }) { | ||
43 | const { message, title = '', confirmButtonText, errorMessage } = options | ||
44 | this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText, errorMessage }) | ||
37 | 45 | ||
38 | const obs = this.confirmResponse.asObservable() | 46 | const obs = this.confirmResponse.asObservable() |
39 | .pipe(map(({ confirmed, value }) => ({ confirmed, password: value }))) | 47 | .pipe(map(({ confirmed, value }) => ({ confirmed, password: value }))) |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index d57608f1c..5aa02e472 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -30,8 +30,6 @@ export class User implements UserServerModel { | |||
30 | autoPlayNextVideoPlaylist: boolean | 30 | autoPlayNextVideoPlaylist: boolean |
31 | 31 | ||
32 | p2pEnabled: boolean | 32 | p2pEnabled: boolean |
33 | // FIXME: deprecated in 4.1 | ||
34 | webTorrentEnabled: never | ||
35 | 33 | ||
36 | videosHistoryEnabled: boolean | 34 | videosHistoryEnabled: boolean |
37 | videoLanguages: string[] | 35 | videoLanguages: string[] |
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts index b7d73d16b..9e22bb4c1 100644 --- a/client/src/app/helpers/i18n-utils.ts +++ b/client/src/app/helpers/i18n-utils.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import IntlMessageFormat from 'intl-messageformat' | 1 | import IntlMessageFormat from 'intl-messageformat' |
2 | import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill' | ||
3 | import { shouldPolyfill as shouldPolyfillPlural } from '@formatjs/intl-pluralrules/should-polyfill' | ||
2 | import { logger } from '@root-helpers/logger' | 4 | import { logger } from '@root-helpers/logger' |
3 | import { environment } from '../../environments/environment' | 5 | import { environment } from '../../environments/environment' |
4 | 6 | ||
@@ -10,31 +12,68 @@ function getDevLocale () { | |||
10 | return 'fr-FR' | 12 | return 'fr-FR' |
11 | } | 13 | } |
12 | 14 | ||
13 | function prepareIcu (icu: string) { | 15 | async function polyfillICU () { |
14 | let alreadyWarned = false | 16 | // Important to be in this order, Plural needs Locale (https://formatjs.io/docs/polyfills/intl-pluralrules) |
17 | await polyfillICULocale() | ||
18 | await polyfillICUPlural() | ||
19 | } | ||
15 | 20 | ||
16 | try { | 21 | async function polyfillICULocale () { |
17 | const msg = new IntlMessageFormat(icu, $localize.locale) | 22 | // This locale is supported |
23 | if (shouldPolyfillLocale()) { | ||
24 | // TODO: remove, it's only needed to support Plural polyfill and so iOS 12 | ||
25 | console.log('Loading Intl Locale polyfill for ' + $localize.locale) | ||
26 | |||
27 | await import('@formatjs/intl-locale/polyfill') | ||
28 | } | ||
29 | } | ||
30 | |||
31 | async function polyfillICUPlural () { | ||
32 | const unsupportedLocale = shouldPolyfillPlural($localize.locale) | ||
33 | |||
34 | // This locale is supported | ||
35 | if (!unsupportedLocale) { | ||
36 | return | ||
37 | } | ||
18 | 38 | ||
19 | return (context: { [id: string]: number | string }, fallback: string) => { | 39 | // TODO: remove, it's only needed to support iOS 12 |
20 | try { | 40 | console.log('Loading Intl Plural rules polyfill for ' + $localize.locale) |
21 | return msg.format(context) as string | ||
22 | } catch (err) { | ||
23 | if (!alreadyWarned) logger.warn(`Cannot format ICU ${icu}.`, err) | ||
24 | 41 | ||
25 | alreadyWarned = true | 42 | // Load the polyfill 1st BEFORE loading data |
26 | return fallback | 43 | await import('@formatjs/intl-pluralrules/polyfill-force') |
27 | } | 44 | // Degraded mode, so only load the en local data |
45 | await import(`@formatjs/intl-pluralrules/locale-data/en.js`) | ||
46 | } | ||
47 | |||
48 | // --------------------------------------------------------------------------- | ||
49 | |||
50 | const icuCache = new Map<string, IntlMessageFormat>() | ||
51 | const icuWarnings = new Set<string>() | ||
52 | const fallback = 'String translation error' | ||
53 | |||
54 | function formatICU (icu: string, context: { [id: string]: number | string }) { | ||
55 | try { | ||
56 | let msg = icuCache.get(icu) | ||
57 | |||
58 | if (!msg) { | ||
59 | msg = new IntlMessageFormat(icu, $localize.locale) | ||
60 | icuCache.set(icu, msg) | ||
28 | } | 61 | } |
62 | |||
63 | return msg.format(context) as string | ||
29 | } catch (err) { | 64 | } catch (err) { |
30 | logger.warn(`Cannot build intl message ${icu}.`, err) | 65 | if (!icuWarnings.has(icu)) { |
66 | logger.warn(`Cannot format ICU ${icu}.`, err) | ||
67 | } | ||
31 | 68 | ||
32 | return (_context: unknown, fallback: string) => fallback | 69 | icuWarnings.add(icu) |
70 | return fallback | ||
33 | } | 71 | } |
34 | } | 72 | } |
35 | 73 | ||
36 | export { | 74 | export { |
37 | getDevLocale, | 75 | getDevLocale, |
38 | prepareIcu, | 76 | polyfillICU, |
77 | formatICU, | ||
39 | isOnDevLocale | 78 | isOnDevLocale |
40 | } | 79 | } |
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts index 69b2b18c0..b69e31edf 100644 --- a/client/src/app/helpers/utils/object.ts +++ b/client/src/app/helpers/utils/object.ts | |||
@@ -34,6 +34,8 @@ function toBoolean (value: any) { | |||
34 | 34 | ||
35 | if (value === 'true') return true | 35 | if (value === 'true') return true |
36 | if (value === 'false') return false | 36 | if (value === 'false') return false |
37 | if (value === '1') return true | ||
38 | if (value === '0') return false | ||
37 | 39 | ||
38 | return undefined | 40 | return undefined |
39 | } | 41 | } |
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html index 6584db3e6..33696d0a5 100644 --- a/client/src/app/modal/confirm.component.html +++ b/client/src/app/modal/confirm.component.html | |||
@@ -12,10 +12,12 @@ | |||
12 | <div *ngIf="inputLabel" class="form-group mt-3"> | 12 | <div *ngIf="inputLabel" class="form-group mt-3"> |
13 | <label for="confirmInput">{{ inputLabel }}</label> | 13 | <label for="confirmInput">{{ inputLabel }}</label> |
14 | 14 | ||
15 | <input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" /> | 15 | <input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" (keyup.enter)="confirm()" /> |
16 | 16 | ||
17 | <my-input-text *ngIf="isPasswordInput" inputId="confirmInput" [(ngModel)]="inputValue"></my-input-text> | 17 | <my-input-text *ngIf="isPasswordInput" inputId="confirmInput" [(ngModel)]="inputValue" (keyup.enter)="confirm()"></my-input-text> |
18 | </div> | 18 | </div> |
19 | |||
20 | <div *ngIf="hasError()" class="text-danger">{{ errorMessage }}</div> | ||
19 | </div> | 21 | </div> |
20 | 22 | ||
21 | <div class="modal-footer inputs"> | 23 | <div class="modal-footer inputs"> |
diff --git a/client/src/app/modal/confirm.component.ts b/client/src/app/modal/confirm.component.ts index 3bb8b9b21..43369befa 100644 --- a/client/src/app/modal/confirm.component.ts +++ b/client/src/app/modal/confirm.component.ts | |||
@@ -21,6 +21,8 @@ export class ConfirmComponent implements OnInit { | |||
21 | inputValue = '' | 21 | inputValue = '' |
22 | confirmButtonText = '' | 22 | confirmButtonText = '' |
23 | 23 | ||
24 | errorMessage = '' | ||
25 | |||
24 | isPasswordInput = false | 26 | isPasswordInput = false |
25 | 27 | ||
26 | private openedModal: NgbModalRef | 28 | private openedModal: NgbModalRef |
@@ -42,8 +44,9 @@ export class ConfirmComponent implements OnInit { | |||
42 | this.inputValue = '' | 44 | this.inputValue = '' |
43 | this.confirmButtonText = '' | 45 | this.confirmButtonText = '' |
44 | this.isPasswordInput = false | 46 | this.isPasswordInput = false |
47 | this.errorMessage = '' | ||
45 | 48 | ||
46 | const { type, title, message, confirmButtonText } = payload | 49 | const { type, title, message, confirmButtonText, errorMessage } = payload |
47 | 50 | ||
48 | this.title = title | 51 | this.title = title |
49 | 52 | ||
@@ -53,6 +56,7 @@ export class ConfirmComponent implements OnInit { | |||
53 | } else if (type === 'confirm-password') { | 56 | } else if (type === 'confirm-password') { |
54 | this.inputLabel = $localize`Confirm your password` | 57 | this.inputLabel = $localize`Confirm your password` |
55 | this.isPasswordInput = true | 58 | this.isPasswordInput = true |
59 | this.errorMessage = errorMessage | ||
56 | } | 60 | } |
57 | 61 | ||
58 | this.confirmButtonText = confirmButtonText || $localize`Confirm` | 62 | this.confirmButtonText = confirmButtonText || $localize`Confirm` |
@@ -78,6 +82,9 @@ export class ConfirmComponent implements OnInit { | |||
78 | return this.expectedInputValue !== this.inputValue | 82 | return this.expectedInputValue !== this.inputValue |
79 | } | 83 | } |
80 | 84 | ||
85 | hasError () { | ||
86 | return this.errorMessage | ||
87 | } | ||
81 | showModal () { | 88 | showModal () { |
82 | this.inputValue = '' | 89 | this.inputValue = '' |
83 | 90 | ||
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts index ff0813f7d..3672e5610 100644 --- a/client/src/app/shared/form-validators/custom-config-validators.ts +++ b/client/src/app/shared/form-validators/custom-config-validators.ts | |||
@@ -22,21 +22,12 @@ export const SERVICES_TWITTER_USERNAME_VALIDATOR: BuildFormValidator = { | |||
22 | } | 22 | } |
23 | } | 23 | } |
24 | 24 | ||
25 | export const CACHE_PREVIEWS_SIZE_VALIDATOR: BuildFormValidator = { | 25 | export const CACHE_SIZE_VALIDATOR: BuildFormValidator = { |
26 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], | 26 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], |
27 | MESSAGES: { | 27 | MESSAGES: { |
28 | required: $localize`Previews cache size is required.`, | 28 | required: $localize`Cache size is required.`, |
29 | min: $localize`Previews cache size must be greater than 1.`, | 29 | min: $localize`Cache size must be greater than 1.`, |
30 | pattern: $localize`Previews cache size must be a number.` | 30 | pattern: $localize`Cache size must be a number.` |
31 | } | ||
32 | } | ||
33 | |||
34 | export const CACHE_CAPTIONS_SIZE_VALIDATOR: BuildFormValidator = { | ||
35 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], | ||
36 | MESSAGES: { | ||
37 | required: $localize`Captions cache size is required.`, | ||
38 | min: $localize`Captions cache size must be greater than 1.`, | ||
39 | pattern: $localize`Captions cache size must be a number.` | ||
40 | } | 31 | } |
41 | } | 32 | } |
42 | 33 | ||
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts index a4bda8f16..090a76e43 100644 --- a/client/src/app/shared/form-validators/video-validators.ts +++ b/client/src/app/shared/form-validators/video-validators.ts | |||
@@ -26,6 +26,15 @@ export const VIDEO_PRIVACY_VALIDATOR: BuildFormValidator = { | |||
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | export const VIDEO_PASSWORD_VALIDATOR: BuildFormValidator = { | ||
30 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(100) ], // Required is set dynamically | ||
31 | MESSAGES: { | ||
32 | minLength: $localize`A password should be at least 2 characters long.`, | ||
33 | maxLength: $localize`A password should be shorter than 100 characters long.`, | ||
34 | required: $localize`A password is required for password protected video.` | ||
35 | } | ||
36 | } | ||
37 | |||
29 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { | 38 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { |
30 | VALIDATORS: [ ], | 39 | VALIDATORS: [ ], |
31 | MESSAGES: {} | 40 | MESSAGES: {} |
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts index 2c3226f68..8b6cd091a 100644 --- a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts +++ b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' | 5 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' |
6 | import { ItemSelectCheckboxValue } from './select-checkbox.component' | 6 | import { ItemSelectCheckboxValue } from './select-checkbox.component' |
7 | 7 | ||
@@ -80,9 +80,9 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor { | |||
80 | 80 | ||
81 | if (outputItems.length >= this.maxItems) { | 81 | if (outputItems.length >= this.maxItems) { |
82 | this.notifier.error( | 82 | this.notifier.error( |
83 | prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)( | 83 | formatICU( |
84 | { maxItems: this.maxItems }, | 84 | $localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`, |
85 | $localize`You can't select more than ${this.maxItems} items` | 85 | { maxItems: this.maxItems } |
86 | ) | 86 | ) |
87 | ) | 87 | ) |
88 | 88 | ||
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index 2e63f6c17..ab1b1458a 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
@@ -71,17 +71,17 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
71 | const hours = Math.floor(seconds / 3600) | 71 | const hours = Math.floor(seconds / 3600) |
72 | 72 | ||
73 | if (hours !== 0) { | 73 | if (hours !== 0) { |
74 | return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)( | 74 | return formatICU( |
75 | { hours }, | 75 | $localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`, |
76 | $localize`~ ${hours} hours` | 76 | { hours } |
77 | ) | 77 | ) |
78 | } | 78 | } |
79 | 79 | ||
80 | const minutes = Math.floor(seconds % 3600 / 60) | 80 | const minutes = Math.floor(seconds % 3600 / 60) |
81 | 81 | ||
82 | return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)( | 82 | return formatICU( |
83 | { minutes }, | 83 | $localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`, |
84 | $localize`~ ${minutes} minutes` | 84 | { minutes } |
85 | ) | 85 | ) |
86 | } | 86 | } |
87 | 87 | ||
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts index dc6a25e83..4ff244bbb 100644 --- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -1,14 +1,9 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | 1 | import { Pipe, PipeTransform } from '@angular/core' |
2 | import { prepareIcu } from '@app/helpers' | 2 | import { formatICU } from '@app/helpers' |
3 | 3 | ||
4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | 4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site |
5 | @Pipe({ name: 'myFromNow' }) | 5 | @Pipe({ name: 'myFromNow' }) |
6 | export class FromNowPipe implements PipeTransform { | 6 | export class FromNowPipe implements PipeTransform { |
7 | private yearICU = prepareIcu($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`) | ||
8 | private monthICU = prepareIcu($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`) | ||
9 | private weekICU = prepareIcu($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`) | ||
10 | private dayICU = prepareIcu($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`) | ||
11 | private hourICU = prepareIcu($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`) | ||
12 | 7 | ||
13 | transform (arg: number | Date | string) { | 8 | transform (arg: number | Date | string) { |
14 | const argDate = new Date(arg) | 9 | const argDate = new Date(arg) |
@@ -16,7 +11,7 @@ export class FromNowPipe implements PipeTransform { | |||
16 | 11 | ||
17 | let interval = Math.floor(seconds / 31536000) | 12 | let interval = Math.floor(seconds / 31536000) |
18 | if (interval >= 1) { | 13 | if (interval >= 1) { |
19 | return this.yearICU({ interval }, $localize`${interval} year(s) ago`) | 14 | return formatICU($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`, { interval }) |
20 | } | 15 | } |
21 | 16 | ||
22 | interval = Math.floor(seconds / 2419200) | 17 | interval = Math.floor(seconds / 2419200) |
@@ -25,7 +20,7 @@ export class FromNowPipe implements PipeTransform { | |||
25 | if (interval >= 12) return $localize`1 year ago` | 20 | if (interval >= 12) return $localize`1 year ago` |
26 | 21 | ||
27 | if (interval >= 1) { | 22 | if (interval >= 1) { |
28 | return this.monthICU({ interval }, $localize`${interval} month(s) ago`) | 23 | return formatICU($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`, { interval }) |
29 | } | 24 | } |
30 | 25 | ||
31 | interval = Math.floor(seconds / 604800) | 26 | interval = Math.floor(seconds / 604800) |
@@ -34,17 +29,17 @@ export class FromNowPipe implements PipeTransform { | |||
34 | if (interval >= 4) return $localize`1 month ago` | 29 | if (interval >= 4) return $localize`1 month ago` |
35 | 30 | ||
36 | if (interval >= 1) { | 31 | if (interval >= 1) { |
37 | return this.weekICU({ interval }, $localize`${interval} week(s) ago`) | 32 | return formatICU($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`, { interval }) |
38 | } | 33 | } |
39 | 34 | ||
40 | interval = Math.floor(seconds / 86400) | 35 | interval = Math.floor(seconds / 86400) |
41 | if (interval >= 1) { | 36 | if (interval >= 1) { |
42 | return this.dayICU({ interval }, $localize`${interval} day(s) ago`) | 37 | return formatICU($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`, { interval }) |
43 | } | 38 | } |
44 | 39 | ||
45 | interval = Math.floor(seconds / 3600) | 40 | interval = Math.floor(seconds / 3600) |
46 | if (interval >= 1) { | 41 | if (interval >= 1) { |
47 | return this.hourICU({ interval }, $localize`${interval} hour(s) ago`) | 42 | return formatICU($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`, { interval }) |
48 | } | 43 | } |
49 | 44 | ||
50 | interval = Math.floor(seconds / 60) | 45 | interval = Math.floor(seconds / 60) |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index d3ec31d6e..480277450 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -52,6 +52,7 @@ import { | |||
52 | VideoFileTokenService, | 52 | VideoFileTokenService, |
53 | VideoImportService, | 53 | VideoImportService, |
54 | VideoOwnershipService, | 54 | VideoOwnershipService, |
55 | VideoPasswordService, | ||
55 | VideoResolver, | 56 | VideoResolver, |
56 | VideoService | 57 | VideoService |
57 | } from './video' | 58 | } from './video' |
@@ -210,6 +211,8 @@ import { VideoChannelService } from './video-channel' | |||
210 | 211 | ||
211 | VideoChannelService, | 212 | VideoChannelService, |
212 | 213 | ||
214 | VideoPasswordService, | ||
215 | |||
213 | CustomPageService, | 216 | CustomPageService, |
214 | 217 | ||
215 | ActorRedirectGuard | 218 | ActorRedirectGuard |
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts index 0f3afd116..21f31a717 100644 --- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts +++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts | |||
@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http' | |||
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { RestExtractor, ServerService } from '@app/core' | 5 | import { RestExtractor, ServerService } from '@app/core' |
6 | import { objectToFormData, sortBy } from '@app/helpers' | 6 | import { objectToFormData, sortBy } from '@app/helpers' |
7 | import { VideoService } from '@app/shared/shared-main/video' | 7 | import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video' |
8 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 8 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
9 | import { ResultList, VideoCaption } from '@shared/models' | 9 | import { ResultList, VideoCaption } from '@shared/models' |
10 | import { environment } from '../../../../environments/environment' | 10 | import { environment } from '../../../../environments/environment' |
@@ -18,8 +18,10 @@ export class VideoCaptionService { | |||
18 | private restExtractor: RestExtractor | 18 | private restExtractor: RestExtractor |
19 | ) {} | 19 | ) {} |
20 | 20 | ||
21 | listCaptions (videoId: string): Observable<ResultList<VideoCaption>> { | 21 | listCaptions (videoId: string, videoPassword?: string): Observable<ResultList<VideoCaption>> { |
22 | return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`) | 22 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) |
23 | |||
24 | return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`, { headers }) | ||
23 | .pipe( | 25 | .pipe( |
24 | switchMap(captionsResult => { | 26 | switchMap(captionsResult => { |
25 | return this.serverService.getServerLocale() | 27 | return this.serverService.getServerLocale() |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index a2e47883e..07d40b117 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -5,6 +5,7 @@ export * from './video-edit.model' | |||
5 | export * from './video-file-token.service' | 5 | export * from './video-file-token.service' |
6 | export * from './video-import.service' | 6 | export * from './video-import.service' |
7 | export * from './video-ownership.service' | 7 | export * from './video-ownership.service' |
8 | export * from './video-password.service' | ||
8 | export * from './video.model' | 9 | export * from './video.model' |
9 | export * from './video.resolver' | 10 | export * from './video.resolver' |
10 | export * from './video.service' | 11 | export * from './video.service' |
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts index 47eee80d8..1b8b67ee2 100644 --- a/client/src/app/shared/shared-main/video/video-edit.model.ts +++ b/client/src/app/shared/shared-main/video/video-edit.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { getAbsoluteAPIUrl } from '@app/helpers' | 1 | import { getAbsoluteAPIUrl } from '@app/helpers' |
2 | import { VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' | 2 | import { VideoPassword, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' |
3 | import { VideoDetails } from './video-details.model' | 3 | import { VideoDetails } from './video-details.model' |
4 | import { objectKeysTyped } from '@shared/core-utils' | 4 | import { objectKeysTyped } from '@shared/core-utils' |
5 | 5 | ||
@@ -18,6 +18,7 @@ export class VideoEdit implements VideoUpdate { | |||
18 | waitTranscoding: boolean | 18 | waitTranscoding: boolean |
19 | channelId: number | 19 | channelId: number |
20 | privacy: VideoPrivacy | 20 | privacy: VideoPrivacy |
21 | videoPassword?: string | ||
21 | support: string | 22 | support: string |
22 | thumbnailfile?: any | 23 | thumbnailfile?: any |
23 | previewfile?: any | 24 | previewfile?: any |
@@ -32,7 +33,7 @@ export class VideoEdit implements VideoUpdate { | |||
32 | 33 | ||
33 | pluginData?: any | 34 | pluginData?: any |
34 | 35 | ||
35 | constructor (video?: VideoDetails) { | 36 | constructor (video?: VideoDetails, videoPassword?: VideoPassword) { |
36 | if (!video) return | 37 | if (!video) return |
37 | 38 | ||
38 | this.id = video.id | 39 | this.id = video.id |
@@ -63,6 +64,8 @@ export class VideoEdit implements VideoUpdate { | |||
63 | : null | 64 | : null |
64 | 65 | ||
65 | this.pluginData = video.pluginData | 66 | this.pluginData = video.pluginData |
67 | |||
68 | if (videoPassword) this.videoPassword = videoPassword.password | ||
66 | } | 69 | } |
67 | 70 | ||
68 | patch (values: { [ id: string ]: any }) { | 71 | patch (values: { [ id: string ]: any }) { |
@@ -112,6 +115,7 @@ export class VideoEdit implements VideoUpdate { | |||
112 | waitTranscoding: this.waitTranscoding, | 115 | waitTranscoding: this.waitTranscoding, |
113 | channelId: this.channelId, | 116 | channelId: this.channelId, |
114 | privacy: this.privacy, | 117 | privacy: this.privacy, |
118 | videoPassword: this.videoPassword, | ||
115 | originallyPublishedAt: this.originallyPublishedAt | 119 | originallyPublishedAt: this.originallyPublishedAt |
116 | } | 120 | } |
117 | 121 | ||
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts index 791607249..9bca5b9ec 100644 --- a/client/src/app/shared/shared-main/video/video-file-token.service.ts +++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts | |||
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core' | |||
4 | import { RestExtractor } from '@app/core' | 4 | import { RestExtractor } from '@app/core' |
5 | import { VideoToken } from '@shared/models' | 5 | import { VideoToken } from '@shared/models' |
6 | import { VideoService } from './video.service' | 6 | import { VideoService } from './video.service' |
7 | import { VideoPasswordService } from './video-password.service' | ||
7 | 8 | ||
8 | @Injectable() | 9 | @Injectable() |
9 | export class VideoFileTokenService { | 10 | export class VideoFileTokenService { |
@@ -15,16 +16,18 @@ export class VideoFileTokenService { | |||
15 | private restExtractor: RestExtractor | 16 | private restExtractor: RestExtractor |
16 | ) {} | 17 | ) {} |
17 | 18 | ||
18 | getVideoFileToken (videoUUID: string) { | 19 | getVideoFileToken ({ videoUUID, videoPassword }: { videoUUID: string, videoPassword?: string }) { |
19 | const existing = this.store.get(videoUUID) | 20 | const existing = this.store.get(videoUUID) |
20 | if (existing) return of(existing) | 21 | if (existing) return of(existing) |
21 | 22 | ||
22 | return this.createVideoFileToken(videoUUID) | 23 | return this.createVideoFileToken(videoUUID, videoPassword) |
23 | .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) | 24 | .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) |
24 | } | 25 | } |
25 | 26 | ||
26 | private createVideoFileToken (videoUUID: string) { | 27 | private createVideoFileToken (videoUUID: string, videoPassword?: string) { |
27 | return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) | 28 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) |
29 | |||
30 | return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}, { headers }) | ||
28 | .pipe( | 31 | .pipe( |
29 | map(({ files }) => files), | 32 | map(({ files }) => files), |
30 | catchError(err => this.restExtractor.handleError(err)) | 33 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/shared-main/video/video-password.service.ts b/client/src/app/shared/shared-main/video/video-password.service.ts new file mode 100644 index 000000000..d5b0406f8 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-password.service.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import { ResultList, VideoPassword } from '@shared/models' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { catchError, switchMap } from 'rxjs' | ||
4 | import { HttpClient, HttpHeaders } from '@angular/common/http' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { VideoService } from './video.service' | ||
7 | |||
8 | @Injectable() | ||
9 | export class VideoPasswordService { | ||
10 | |||
11 | constructor ( | ||
12 | private authHttp: HttpClient, | ||
13 | private restExtractor: RestExtractor | ||
14 | ) {} | ||
15 | |||
16 | static buildVideoPasswordHeader (videoPassword: string) { | ||
17 | return videoPassword | ||
18 | ? new HttpHeaders().set('x-peertube-video-password', videoPassword) | ||
19 | : undefined | ||
20 | } | ||
21 | |||
22 | getVideoPasswords (options: { videoUUID: string }) { | ||
23 | return this.authHttp.get<ResultList<VideoPassword>>(`${VideoService.BASE_VIDEO_URL}/${options.videoUUID}/passwords`) | ||
24 | .pipe( | ||
25 | switchMap(res => res.data), | ||
26 | catchError(err => this.restExtractor.handleError(err)) | ||
27 | ) | ||
28 | } | ||
29 | } | ||
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 6fdffb394..1ffc40411 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { AuthUser } from '@app/core' | 1 | import { AuthUser } from '@app/core' |
2 | import { User } from '@app/core/users/user.model' | 2 | import { User } from '@app/core/users/user.model' |
3 | import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl, prepareIcu } from '@app/helpers' | 3 | import { durationToString, formatICU, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' |
4 | import { Actor } from '@app/shared/shared-main/account/actor.model' | 4 | import { Actor } from '@app/shared/shared-main/account/actor.model' |
5 | import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils' | 5 | import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils' |
6 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 6 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
@@ -19,9 +19,6 @@ import { | |||
19 | } from '@shared/models' | 19 | } from '@shared/models' |
20 | 20 | ||
21 | export class Video implements VideoServerModel { | 21 | export class Video implements VideoServerModel { |
22 | private static readonly viewsICU = prepareIcu($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`) | ||
23 | private static readonly viewersICU = prepareIcu($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`) | ||
24 | |||
25 | byVideoChannel: string | 22 | byVideoChannel: string |
26 | byAccount: string | 23 | byAccount: string |
27 | 24 | ||
@@ -255,7 +252,7 @@ export class Video implements VideoServerModel { | |||
255 | user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) && | 252 | user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) && |
256 | this.state.id !== VideoState.TO_TRANSCODE && | 253 | this.state.id !== VideoState.TO_TRANSCODE && |
257 | this.hasHLS() && | 254 | this.hasHLS() && |
258 | this.hasWebTorrent() | 255 | this.hasWebVideos() |
259 | } | 256 | } |
260 | 257 | ||
261 | canRunTranscoding (user: AuthUser) { | 258 | canRunTranscoding (user: AuthUser) { |
@@ -268,7 +265,7 @@ export class Video implements VideoServerModel { | |||
268 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) | 265 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) |
269 | } | 266 | } |
270 | 267 | ||
271 | hasWebTorrent () { | 268 | hasWebVideos () { |
272 | return this.files && this.files.length !== 0 | 269 | return this.files && this.files.length !== 0 |
273 | } | 270 | } |
274 | 271 | ||
@@ -281,11 +278,18 @@ export class Video implements VideoServerModel { | |||
281 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) | 278 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) |
282 | } | 279 | } |
283 | 280 | ||
281 | canAccessPasswordProtectedVideoWithoutPassword (user: AuthUser) { | ||
282 | return this.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && | ||
283 | user && | ||
284 | this.isLocal === true && | ||
285 | (this.account.name === user.username || user.hasRight(UserRight.SEE_ALL_VIDEOS)) | ||
286 | } | ||
287 | |||
284 | getExactNumberOfViews () { | 288 | getExactNumberOfViews () { |
285 | if (this.isLive) { | 289 | if (this.isLive) { |
286 | return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`) | 290 | return formatICU($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`, { viewers: this.viewers }) |
287 | } | 291 | } |
288 | 292 | ||
289 | return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`) | 293 | return formatICU($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`, { views: this.views }) |
290 | } | 294 | } |
291 | } | 295 | } |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 78a49567f..20145b9c5 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | FeedFormat, | 11 | FeedFormat, |
12 | NSFWPolicyType, | 12 | NSFWPolicyType, |
13 | ResultList, | 13 | ResultList, |
14 | Storyboard, | ||
14 | UserVideoRate, | 15 | UserVideoRate, |
15 | UserVideoRateType, | 16 | UserVideoRateType, |
16 | UserVideoRateUpdate, | 17 | UserVideoRateUpdate, |
@@ -33,6 +34,7 @@ import { VideoChannel, VideoChannelService } from '../video-channel' | |||
33 | import { VideoDetails } from './video-details.model' | 34 | import { VideoDetails } from './video-details.model' |
34 | import { VideoEdit } from './video-edit.model' | 35 | import { VideoEdit } from './video-edit.model' |
35 | import { Video } from './video.model' | 36 | import { Video } from './video.model' |
37 | import { VideoPasswordService } from './video-password.service' | ||
36 | 38 | ||
37 | export type CommonVideoParams = { | 39 | export type CommonVideoParams = { |
38 | videoPagination?: ComponentPaginationLight | 40 | videoPagination?: ComponentPaginationLight |
@@ -69,16 +71,17 @@ export class VideoService { | |||
69 | return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` | 71 | return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` |
70 | } | 72 | } |
71 | 73 | ||
72 | getVideo (options: { videoId: string }): Observable<VideoDetails> { | 74 | getVideo (options: { videoId: string, videoPassword?: string }): Observable<VideoDetails> { |
73 | return this.serverService.getServerLocale() | 75 | const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword) |
74 | .pipe( | 76 | |
75 | switchMap(translations => { | 77 | return this.serverService.getServerLocale().pipe( |
76 | return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`) | 78 | switchMap(translations => { |
77 | .pipe(map(videoHash => ({ videoHash, translations }))) | 79 | return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`, { headers }) |
78 | }), | 80 | .pipe(map(videoHash => ({ videoHash, translations }))) |
79 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), | 81 | }), |
80 | catchError(err => this.restExtractor.handleError(err)) | 82 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), |
81 | ) | 83 | catchError(err => this.restExtractor.handleError(err)) |
84 | ) | ||
82 | } | 85 | } |
83 | 86 | ||
84 | updateVideo (video: VideoEdit) { | 87 | updateVideo (video: VideoEdit) { |
@@ -99,6 +102,9 @@ export class VideoService { | |||
99 | description, | 102 | description, |
100 | channelId: video.channelId, | 103 | channelId: video.channelId, |
101 | privacy: video.privacy, | 104 | privacy: video.privacy, |
105 | videoPasswords: video.privacy === VideoPrivacy.PASSWORD_PROTECTED | ||
106 | ? [ video.videoPassword ] | ||
107 | : undefined, | ||
102 | tags: video.tags, | 108 | tags: video.tags, |
103 | nsfw: video.nsfw, | 109 | nsfw: video.nsfw, |
104 | waitTranscoding: video.waitTranscoding, | 110 | waitTranscoding: video.waitTranscoding, |
@@ -305,7 +311,7 @@ export class VideoService { | |||
305 | ) | 311 | ) |
306 | } | 312 | } |
307 | 313 | ||
308 | removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { | 314 | removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'web-videos') { |
309 | return from(videoIds) | 315 | return from(videoIds) |
310 | .pipe( | 316 | .pipe( |
311 | concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)), | 317 | concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)), |
@@ -314,12 +320,12 @@ export class VideoService { | |||
314 | ) | 320 | ) |
315 | } | 321 | } |
316 | 322 | ||
317 | removeFile (videoId: number | string, fileId: number, type: 'hls' | 'webtorrent') { | 323 | removeFile (videoId: number | string, fileId: number, type: 'hls' | 'web-videos') { |
318 | return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId) | 324 | return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId) |
319 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 325 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
320 | } | 326 | } |
321 | 327 | ||
322 | runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { | 328 | runTranscoding (videoIds: (number | string)[], type: 'hls' | 'web-video') { |
323 | const body: VideoTranscodingCreate = { transcodingType: type } | 329 | const body: VideoTranscodingCreate = { transcodingType: type } |
324 | 330 | ||
325 | return from(videoIds) | 331 | return from(videoIds) |
@@ -339,6 +345,27 @@ export class VideoService { | |||
339 | ) | 345 | ) |
340 | } | 346 | } |
341 | 347 | ||
348 | // --------------------------------------------------------------------------- | ||
349 | |||
350 | getStoryboards (videoId: string | number, videoPassword: string) { | ||
351 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
352 | |||
353 | return this.authHttp | ||
354 | .get<{ storyboards: Storyboard[] }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/storyboards', { headers }) | ||
355 | .pipe( | ||
356 | map(({ storyboards }) => storyboards), | ||
357 | catchError(err => { | ||
358 | if (err.status === 404) { | ||
359 | return of([]) | ||
360 | } | ||
361 | |||
362 | this.restExtractor.handleError(err) | ||
363 | }) | ||
364 | ) | ||
365 | } | ||
366 | |||
367 | // --------------------------------------------------------------------------- | ||
368 | |||
342 | getSource (videoId: number) { | 369 | getSource (videoId: number) { |
343 | return this.authHttp | 370 | return this.authHttp |
344 | .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source') | 371 | .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source') |
@@ -353,18 +380,22 @@ export class VideoService { | |||
353 | ) | 380 | ) |
354 | } | 381 | } |
355 | 382 | ||
356 | setVideoLike (id: string) { | 383 | // --------------------------------------------------------------------------- |
357 | return this.setVideoRate(id, 'like') | 384 | |
385 | setVideoLike (id: string, videoPassword: string) { | ||
386 | return this.setVideoRate(id, 'like', videoPassword) | ||
358 | } | 387 | } |
359 | 388 | ||
360 | setVideoDislike (id: string) { | 389 | setVideoDislike (id: string, videoPassword: string) { |
361 | return this.setVideoRate(id, 'dislike') | 390 | return this.setVideoRate(id, 'dislike', videoPassword) |
362 | } | 391 | } |
363 | 392 | ||
364 | unsetVideoLike (id: string) { | 393 | unsetVideoLike (id: string, videoPassword: string) { |
365 | return this.setVideoRate(id, 'none') | 394 | return this.setVideoRate(id, 'none', videoPassword) |
366 | } | 395 | } |
367 | 396 | ||
397 | // --------------------------------------------------------------------------- | ||
398 | |||
368 | getUserVideoRating (id: string) { | 399 | getUserVideoRating (id: string) { |
369 | const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' | 400 | const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' |
370 | 401 | ||
@@ -394,7 +425,8 @@ export class VideoService { | |||
394 | [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, | 425 | [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, |
395 | [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, | 426 | [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, |
396 | [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, | 427 | [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, |
397 | [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video` | 428 | [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`, |
429 | [VideoPrivacy.PASSWORD_PROTECTED]: $localize`Only users with the appropriate password can see this video` | ||
398 | } | 430 | } |
399 | 431 | ||
400 | const videoPrivacies = serverPrivacies.map(p => { | 432 | const videoPrivacies = serverPrivacies.map(p => { |
@@ -412,7 +444,13 @@ export class VideoService { | |||
412 | } | 444 | } |
413 | 445 | ||
414 | getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) { | 446 | getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) { |
415 | const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ] | 447 | // We do not add a password as this requires additional configuration. |
448 | const order = [ | ||
449 | VideoPrivacy.PRIVATE, | ||
450 | VideoPrivacy.INTERNAL, | ||
451 | VideoPrivacy.UNLISTED, | ||
452 | VideoPrivacy.PUBLIC | ||
453 | ] | ||
416 | 454 | ||
417 | for (const privacy of order) { | 455 | for (const privacy of order) { |
418 | if (serverPrivacies.find(p => p.id === privacy)) { | 456 | if (serverPrivacies.find(p => p.id === privacy)) { |
@@ -499,14 +537,15 @@ export class VideoService { | |||
499 | } | 537 | } |
500 | } | 538 | } |
501 | 539 | ||
502 | private setVideoRate (id: string, rateType: UserVideoRateType) { | 540 | private setVideoRate (id: string, rateType: UserVideoRateType, videoPassword?: string) { |
503 | const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` | 541 | const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` |
504 | const body: UserVideoRateUpdate = { | 542 | const body: UserVideoRateUpdate = { |
505 | rating: rateType | 543 | rating: rateType |
506 | } | 544 | } |
545 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
507 | 546 | ||
508 | return this.authHttp | 547 | return this.authHttp |
509 | .put(url, body) | 548 | .put(url, body, { headers }) |
510 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 549 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
511 | } | 550 | } |
512 | } | 551 | } |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts index 27dcf043a..34295c34a 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin } from 'rxjs' |
2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -67,9 +67,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
67 | let message: string | 67 | let message: string |
68 | 68 | ||
69 | if (Array.isArray(this.usersToBan)) { | 69 | if (Array.isArray(this.usersToBan)) { |
70 | message = prepareIcu($localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`)( | 70 | message = formatICU( |
71 | { count: this.usersToBan.length }, | 71 | $localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`, |
72 | $localize`${this.usersToBan.length} users banned.` | 72 | { count: this.usersToBan.length } |
73 | ) | 73 | ) |
74 | } else { | 74 | } else { |
75 | message = $localize`User ${this.usersToBan.username} banned.` | 75 | message = $localize`User ${this.usersToBan.username} banned.` |
@@ -88,9 +88,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
88 | 88 | ||
89 | getModalTitle () { | 89 | getModalTitle () { |
90 | if (Array.isArray(this.usersToBan)) { | 90 | if (Array.isArray(this.usersToBan)) { |
91 | return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)( | 91 | return formatICU( |
92 | { count: this.usersToBan.length }, | 92 | $localize`Ban {count, plural, =1 {1 user} other {{count} users}}`, |
93 | $localize`Ban ${this.usersToBan.length} users` | 93 | { count: this.usersToBan.length } |
94 | ) | 94 | ) |
95 | } | 95 | } |
96 | 96 | ||
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts index 3ff53443a..0137def89 100644 --- a/client/src/app/shared/shared-moderation/video-block.component.ts +++ b/client/src/app/shared/shared-moderation/video-block.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { Video } from '@app/shared/shared-main' | 5 | import { Video } from '@app/shared/shared-main' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
@@ -81,9 +81,9 @@ export class VideoBlockComponent extends FormReactive implements OnInit { | |||
81 | this.videoBlocklistService.blockVideo(options) | 81 | this.videoBlocklistService.blockVideo(options) |
82 | .subscribe({ | 82 | .subscribe({ |
83 | next: () => { | 83 | next: () => { |
84 | const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`)( | 84 | const message = formatICU( |
85 | { count: this.videos.length, videoName: this.getSingleVideo().name }, | 85 | $localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`, |
86 | $localize`Blocked ${this.videos.length} videos.` | 86 | { count: this.videos.length, videoName: this.getSingleVideo().name } |
87 | ) | 87 | ) |
88 | 88 | ||
89 | this.notifier.success(message) | 89 | this.notifier.success(message) |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.html b/client/src/app/shared/shared-share-modal/video-share.component.html index 5650fa948..9f1455561 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.html +++ b/client/src/app/shared/shared-share-modal/video-share.component.html | |||
@@ -107,6 +107,10 @@ | |||
107 | </a> | 107 | </a> |
108 | </div> | 108 | </div> |
109 | 109 | ||
110 | <div i18n *ngIf="isPasswordProtectedVideo()" class="alert-private alert alert-warning"> | ||
111 | This video is password protected, please note that recipients will require the corresponding password to access the content. | ||
112 | </div> | ||
113 | |||
110 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> | 114 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> |
111 | 115 | ||
112 | <ng-container ngbNavItem="url"> | 116 | <ng-container ngbNavItem="url"> |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts index 32f900f15..da4f2a4b4 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.ts +++ b/client/src/app/shared/shared-share-modal/video-share.component.ts | |||
@@ -243,6 +243,10 @@ export class VideoShareComponent { | |||
243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE | 243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE |
244 | } | 244 | } |
245 | 245 | ||
246 | isPasswordProtectedVideo () { | ||
247 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
248 | } | ||
249 | |||
246 | private getPlaylistOptions (baseUrl?: string) { | 250 | private getPlaylistOptions (baseUrl?: string) { |
247 | return { | 251 | return { |
248 | baseUrl, | 252 | baseUrl, |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index 8d2deedf7..3906652be 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | import { environment } from '../../../environments/environment' | 18 | import { environment } from '../../../environments/environment' |
19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | 19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' |
20 | import { VideoComment } from './video-comment.model' | 20 | import { VideoComment } from './video-comment.model' |
21 | import { VideoPasswordService } from '../shared-main' | ||
21 | 22 | ||
22 | @Injectable() | 23 | @Injectable() |
23 | export class VideoCommentService { | 24 | export class VideoCommentService { |
@@ -31,22 +32,25 @@ export class VideoCommentService { | |||
31 | private restService: RestService | 32 | private restService: RestService |
32 | ) {} | 33 | ) {} |
33 | 34 | ||
34 | addCommentThread (videoId: string, comment: VideoCommentCreate) { | 35 | addCommentThread (videoId: string, comment: VideoCommentCreate, videoPassword?: string) { |
36 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
35 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 37 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
36 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 38 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
37 | 39 | ||
38 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 40 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
39 | .pipe( | 41 | .pipe( |
40 | map(data => this.extractVideoComment(data.comment)), | 42 | map(data => this.extractVideoComment(data.comment)), |
41 | catchError(err => this.restExtractor.handleError(err)) | 43 | catchError(err => this.restExtractor.handleError(err)) |
42 | ) | 44 | ) |
43 | } | 45 | } |
44 | 46 | ||
45 | addCommentReply (videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate) { | 47 | addCommentReply (options: { videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate, videoPassword?: string }) { |
48 | const { videoId, inReplyToCommentId, comment, videoPassword } = options | ||
49 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
46 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId | 50 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId |
47 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 51 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
48 | 52 | ||
49 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 53 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
50 | .pipe( | 54 | .pipe( |
51 | map(data => this.extractVideoComment(data.comment)), | 55 | map(data => this.extractVideoComment(data.comment)), |
52 | catchError(err => this.restExtractor.handleError(err)) | 56 | catchError(err => this.restExtractor.handleError(err)) |
@@ -76,10 +80,13 @@ export class VideoCommentService { | |||
76 | 80 | ||
77 | getVideoCommentThreads (parameters: { | 81 | getVideoCommentThreads (parameters: { |
78 | videoId: string | 82 | videoId: string |
83 | videoPassword: string | ||
79 | componentPagination: ComponentPaginationLight | 84 | componentPagination: ComponentPaginationLight |
80 | sort: string | 85 | sort: string |
81 | }): Observable<ThreadsResultList<VideoComment>> { | 86 | }): Observable<ThreadsResultList<VideoComment>> { |
82 | const { videoId, componentPagination, sort } = parameters | 87 | const { videoId, videoPassword, componentPagination, sort } = parameters |
88 | |||
89 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
83 | 90 | ||
84 | const pagination = this.restService.componentToRestPagination(componentPagination) | 91 | const pagination = this.restService.componentToRestPagination(componentPagination) |
85 | 92 | ||
@@ -87,7 +94,7 @@ export class VideoCommentService { | |||
87 | params = this.restService.addRestGetParams(params, pagination, sort) | 94 | params = this.restService.addRestGetParams(params, pagination, sort) |
88 | 95 | ||
89 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 96 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
90 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params }) | 97 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params, headers }) |
91 | .pipe( | 98 | .pipe( |
92 | map(result => this.extractVideoComments(result)), | 99 | map(result => this.extractVideoComments(result)), |
93 | catchError(err => this.restExtractor.handleError(err)) | 100 | catchError(err => this.restExtractor.handleError(err)) |
@@ -97,12 +104,14 @@ export class VideoCommentService { | |||
97 | getVideoThreadComments (parameters: { | 104 | getVideoThreadComments (parameters: { |
98 | videoId: string | 105 | videoId: string |
99 | threadId: number | 106 | threadId: number |
107 | videoPassword?: string | ||
100 | }): Observable<VideoCommentThreadTree> { | 108 | }): Observable<VideoCommentThreadTree> { |
101 | const { videoId, threadId } = parameters | 109 | const { videoId, threadId, videoPassword } = parameters |
102 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` | 110 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` |
111 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
103 | 112 | ||
104 | return this.authHttp | 113 | return this.authHttp |
105 | .get<VideoCommentThreadTreeServerModel>(url) | 114 | .get<VideoCommentThreadTreeServerModel>(url, { headers }) |
106 | .pipe( | 115 | .pipe( |
107 | map(tree => this.extractVideoCommentTree(tree)), | 116 | map(tree => this.extractVideoCommentTree(tree)), |
108 | catchError(err => this.restExtractor.handleError(err)) | 117 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index 56527ddfa..0a3ada711 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts | |||
@@ -273,7 +273,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
273 | }) | 273 | }) |
274 | } | 274 | } |
275 | 275 | ||
276 | async removeVideoFiles (video: Video, type: 'hls' | 'webtorrent') { | 276 | async removeVideoFiles (video: Video, type: 'hls' | 'web-videos') { |
277 | const confirmMessage = $localize`Do you really want to remove "${this.video.name}" files?` | 277 | const confirmMessage = $localize`Do you really want to remove "${this.video.name}" files?` |
278 | 278 | ||
279 | const res = await this.confirmService.confirm(confirmMessage, $localize`Remove "${this.video.name}" files`) | 279 | const res = await this.confirmService.confirm(confirmMessage, $localize`Remove "${this.video.name}" files`) |
@@ -290,7 +290,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
290 | }) | 290 | }) |
291 | } | 291 | } |
292 | 292 | ||
293 | runTranscoding (video: Video, type: 'hls' | 'webtorrent') { | 293 | runTranscoding (video: Video, type: 'hls' | 'web-video') { |
294 | this.videoService.runTranscoding([ video.id ], type) | 294 | this.videoService.runTranscoding([ video.id ], type) |
295 | .subscribe({ | 295 | .subscribe({ |
296 | next: () => { | 296 | next: () => { |
@@ -394,8 +394,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
394 | iconName: 'cog' | 394 | iconName: 'cog' |
395 | }, | 395 | }, |
396 | { | 396 | { |
397 | label: $localize`Run WebTorrent transcoding`, | 397 | label: $localize`Run Web Video transcoding`, |
398 | handler: ({ video }) => this.runTranscoding(video, 'webtorrent'), | 398 | handler: ({ video }) => this.runTranscoding(video, 'web-video'), |
399 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), | 399 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), |
400 | iconName: 'cog' | 400 | iconName: 'cog' |
401 | }, | 401 | }, |
@@ -406,8 +406,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
406 | iconName: 'delete' | 406 | iconName: 'delete' |
407 | }, | 407 | }, |
408 | { | 408 | { |
409 | label: $localize`Delete WebTorrent files`, | 409 | label: $localize`Delete Web Video files`, |
410 | handler: ({ video }) => this.removeVideoFiles(video, 'webtorrent'), | 410 | handler: ({ video }) => this.removeVideoFiles(video, 'web-videos'), |
411 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), | 411 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), |
412 | iconName: 'delete' | 412 | iconName: 'delete' |
413 | } | 413 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts index cac82d8d0..146ea7dfe 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { mapValues } from 'lodash-es' | 1 | import { mapValues } from 'lodash-es' |
2 | import { firstValueFrom } from 'rxjs' | 2 | import { firstValueFrom } from 'rxjs' |
3 | import { tap } from 'rxjs/operators' | 3 | import { tap } from 'rxjs/operators' |
4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core' |
5 | import { HooksService } from '@app/core' | 5 | import { HooksService } from '@app/core' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { logger } from '@root-helpers/logger' |
8 | import { videoRequiresAuth } from '@root-helpers/video' | 8 | import { videoRequiresFileToken } from '@root-helpers/video' |
9 | import { objectKeysTyped, pick } from '@shared/core-utils' | 9 | import { objectKeysTyped, pick } from '@shared/core-utils' |
10 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 10 | import { VideoCaption, VideoFile } from '@shared/models' |
11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' | 11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' |
12 | 12 | ||
13 | type DownloadType = 'video' | 'subtitles' | 13 | type DownloadType = 'video' | 'subtitles' |
@@ -21,6 +21,8 @@ type FileMetadata = { [key: string]: { label: string, value: string | number } } | |||
21 | export class VideoDownloadComponent { | 21 | export class VideoDownloadComponent { |
22 | @ViewChild('modal', { static: true }) modal: ElementRef | 22 | @ViewChild('modal', { static: true }) modal: ElementRef |
23 | 23 | ||
24 | @Input() videoPassword: string | ||
25 | |||
24 | downloadType: 'direct' | 'torrent' = 'direct' | 26 | downloadType: 'direct' | 'torrent' = 'direct' |
25 | 27 | ||
26 | resolutionId: number | string = -1 | 28 | resolutionId: number | string = -1 |
@@ -89,8 +91,8 @@ export class VideoDownloadComponent { | |||
89 | this.subtitleLanguageId = this.videoCaptions[0].language.id | 91 | this.subtitleLanguageId = this.videoCaptions[0].language.id |
90 | } | 92 | } |
91 | 93 | ||
92 | if (videoRequiresAuth(this.video)) { | 94 | if (this.isConfidentialVideo()) { |
93 | this.videoFileTokenService.getVideoFileToken(this.video.uuid) | 95 | this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword }) |
94 | .subscribe(({ token }) => this.videoFileToken = token) | 96 | .subscribe(({ token }) => this.videoFileToken = token) |
95 | } | 97 | } |
96 | 98 | ||
@@ -201,7 +203,8 @@ export class VideoDownloadComponent { | |||
201 | } | 203 | } |
202 | 204 | ||
203 | isConfidentialVideo () { | 205 | isConfidentialVideo () { |
204 | return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL | 206 | return videoRequiresFileToken(this.video) |
207 | |||
205 | } | 208 | } |
206 | 209 | ||
207 | switchToType (type: DownloadType) { | 210 | switchToType (type: DownloadType) { |
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html index 3d39c6fdc..3fbfaed28 100644 --- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html +++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html | |||
@@ -125,7 +125,7 @@ | |||
125 | <my-peertube-checkbox | 125 | <my-peertube-checkbox |
126 | formControlName="allVideos" | 126 | formControlName="allVideos" |
127 | inputName="allVideos" | 127 | inputName="allVideos" |
128 | i18n-labelText labelText="Display all videos (private, unlisted or not yet published)" | 128 | i18n-labelText labelText="Display all videos (private, unlisted, password protected or not yet published)" |
129 | ></my-peertube-checkbox> | 129 | ></my-peertube-checkbox> |
130 | </div> | 130 | </div> |
131 | </div> | 131 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 3f0180695..9e0a4f79b 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -5,6 +5,7 @@ | |||
5 | > | 5 | > |
6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> | 6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> |
7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> | 7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> |
8 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPasswordProtectedVideo()" i18n>Password protected</ng-container> | ||
8 | </my-video-thumbnail> | 9 | </my-video-thumbnail> |
9 | 10 | ||
10 | <div class="video-bottom"> | 11 | <div class="video-bottom"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 2384b34d7..d453f37a1 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -171,6 +171,10 @@ export class VideoMiniatureComponent implements OnInit { | |||
171 | return this.video.privacy.id === VideoPrivacy.PRIVATE | 171 | return this.video.privacy.id === VideoPrivacy.PRIVATE |
172 | } | 172 | } |
173 | 173 | ||
174 | isPasswordProtectedVideo () { | ||
175 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
176 | } | ||
177 | |||
174 | getStateLabel (video: Video) { | 178 | getStateLabel (video: Video) { |
175 | if (!video.state) return '' | 179 | if (!video.state) return '' |
176 | 180 | ||
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index 7b832263e..14a5abd7a 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -419,6 +419,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
419 | this.lastQueryLength = data.length | 419 | this.lastQueryLength = data.length |
420 | 420 | ||
421 | if (reset) this.videos = [] | 421 | if (reset) this.videos = [] |
422 | |||
422 | this.videos = this.videos.concat(data) | 423 | this.videos = this.videos.concat(data) |
423 | 424 | ||
424 | if (this.groupByDate) this.buildGroupedDateLabels() | 425 | if (this.groupByDate) this.buildGroupedDateLabels() |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html index 75afa0709..882b14c5e 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html | |||
@@ -21,7 +21,8 @@ | |||
21 | [attr.title]="playlistElement.video.name" | 21 | [attr.title]="playlistElement.video.name" |
22 | >{{ playlistElement.video.name }}</a> | 22 | >{{ playlistElement.video.name }}</a> |
23 | 23 | ||
24 | <span *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> | 24 | <span i18n *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> |
25 | <span i18n *ngIf="isVideoPasswordProtected()" class="pt-badge badge-yellow">Password protected</span> | ||
25 | </div> | 26 | </div> |
26 | 27 | ||
27 | <span class="video-miniature-created-at-views"> | 28 | <span class="video-miniature-created-at-views"> |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts index 552ea742b..b9a1d9623 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts | |||
@@ -60,6 +60,10 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit { | |||
60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE | 60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE |
61 | } | 61 | } |
62 | 62 | ||
63 | isVideoPasswordProtected () { | ||
64 | return this.playlistElement.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
65 | } | ||
66 | |||
63 | isUnavailable (e: VideoPlaylistElement) { | 67 | isUnavailable (e: VideoPlaylistElement) { |
64 | return e.type === VideoPlaylistElementType.UNAVAILABLE | 68 | return e.type === VideoPlaylistElementType.UNAVAILABLE |
65 | } | 69 | } |
diff --git a/client/src/assets/player/index.ts b/client/src/assets/player/index.ts index 9b87afc4a..d34188ea7 100644 --- a/client/src/assets/player/index.ts +++ b/client/src/assets/player/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './peertube-player-manager' | 1 | export * from './peertube-player' |
2 | export * from './types' | 2 | export * from './types' |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts deleted file mode 100644 index 2781850b9..000000000 --- a/client/src/assets/player/peertube-player-manager.ts +++ /dev/null | |||
@@ -1,266 +0,0 @@ | |||
1 | import '@peertube/videojs-contextmenu' | ||
2 | import './shared/upnext/end-card' | ||
3 | import './shared/upnext/upnext-plugin' | ||
4 | import './shared/stats/stats-card' | ||
5 | import './shared/stats/stats-plugin' | ||
6 | import './shared/bezels/bezels-plugin' | ||
7 | import './shared/peertube/peertube-plugin' | ||
8 | import './shared/resolutions/peertube-resolutions-plugin' | ||
9 | import './shared/control-bar/next-previous-video-button' | ||
10 | import './shared/control-bar/p2p-info-button' | ||
11 | import './shared/control-bar/peertube-link-button' | ||
12 | import './shared/control-bar/peertube-load-progress-bar' | ||
13 | import './shared/control-bar/theater-button' | ||
14 | import './shared/control-bar/peertube-live-display' | ||
15 | import './shared/settings/resolution-menu-button' | ||
16 | import './shared/settings/resolution-menu-item' | ||
17 | import './shared/settings/settings-dialog' | ||
18 | import './shared/settings/settings-menu-button' | ||
19 | import './shared/settings/settings-menu-item' | ||
20 | import './shared/settings/settings-panel' | ||
21 | import './shared/settings/settings-panel-child' | ||
22 | import './shared/playlist/playlist-plugin' | ||
23 | import './shared/mobile/peertube-mobile-plugin' | ||
24 | import './shared/mobile/peertube-mobile-buttons' | ||
25 | import './shared/hotkeys/peertube-hotkeys-plugin' | ||
26 | import './shared/metrics/metrics-plugin' | ||
27 | import videojs from 'video.js' | ||
28 | import { logger } from '@root-helpers/logger' | ||
29 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
30 | import { isMobile } from '@root-helpers/web-browser' | ||
31 | import { saveAverageBandwidth } from './peertube-player-local-storage' | ||
32 | import { ManagerOptionsBuilder } from './shared/manager-options' | ||
33 | import { TranslationsManager } from './translations-manager' | ||
34 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode, PlayerNetworkInfo } from './types' | ||
35 | |||
36 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
37 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | ||
38 | |||
39 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
40 | // Change Captions to Subtitles/CC | ||
41 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' | ||
42 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
43 | CaptionsButton.prototype.label_ = ' ' | ||
44 | |||
45 | export class PeertubePlayerManager { | ||
46 | private static playerElementClassName: string | ||
47 | private static playerElementAttributes: { name: string, value: string }[] = [] | ||
48 | |||
49 | private static onPlayerChange: (player: videojs.Player) => void | ||
50 | private static alreadyPlayed = false | ||
51 | private static pluginsManager: PluginsManager | ||
52 | |||
53 | private static videojsDecodeErrors = 0 | ||
54 | |||
55 | private static p2pMediaLoaderModule: any | ||
56 | |||
57 | static initState () { | ||
58 | this.alreadyPlayed = false | ||
59 | } | ||
60 | |||
61 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) { | ||
62 | this.pluginsManager = options.pluginsManager | ||
63 | |||
64 | this.onPlayerChange = onPlayerChange | ||
65 | |||
66 | this.playerElementClassName = options.common.playerElement.className | ||
67 | |||
68 | for (const name of options.common.playerElement.getAttributeNames()) { | ||
69 | this.playerElementAttributes.push({ name, value: options.common.playerElement.getAttribute(name) }) | ||
70 | } | ||
71 | |||
72 | if (mode === 'webtorrent') await import('./shared/webtorrent/webtorrent-plugin') | ||
73 | if (mode === 'p2p-media-loader') { | ||
74 | const [ p2pMediaLoaderModule ] = await Promise.all([ | ||
75 | import('@peertube/p2p-media-loader-hlsjs'), | ||
76 | import('./shared/p2p-media-loader/p2p-media-loader-plugin') | ||
77 | ]) | ||
78 | |||
79 | this.p2pMediaLoaderModule = p2pMediaLoaderModule | ||
80 | } | ||
81 | |||
82 | await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs) | ||
83 | |||
84 | return this.buildPlayer(mode, options) | ||
85 | } | ||
86 | |||
87 | private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> { | ||
88 | const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule) | ||
89 | |||
90 | const videojsOptions = await this.pluginsManager.runHook( | ||
91 | 'filter:internal.player.videojs.options.result', | ||
92 | videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed) | ||
93 | ) | ||
94 | |||
95 | const self = this | ||
96 | return new Promise(res => { | ||
97 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | ||
98 | const player = this | ||
99 | |||
100 | if (!isNaN(+options.common.playbackRate)) { | ||
101 | player.playbackRate(+options.common.playbackRate) | ||
102 | } | ||
103 | |||
104 | let alreadyFallback = false | ||
105 | |||
106 | const handleError = () => { | ||
107 | if (alreadyFallback) return | ||
108 | alreadyFallback = true | ||
109 | |||
110 | if (mode === 'p2p-media-loader') { | ||
111 | self.tryToRecoverHLSError(player.error(), player, options) | ||
112 | } else { | ||
113 | self.maybeFallbackToWebTorrent(mode, player, options) | ||
114 | } | ||
115 | } | ||
116 | |||
117 | player.one('error', () => handleError()) | ||
118 | |||
119 | player.one('play', () => { | ||
120 | self.alreadyPlayed = true | ||
121 | }) | ||
122 | |||
123 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | ||
124 | |||
125 | if (isMobile()) player.peertubeMobile() | ||
126 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) | ||
127 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | ||
128 | |||
129 | player.bezels() | ||
130 | |||
131 | player.stats({ | ||
132 | videoUUID: options.common.videoUUID, | ||
133 | videoIsLive: options.common.isLive, | ||
134 | mode, | ||
135 | p2pEnabled: options.common.p2pEnabled | ||
136 | }) | ||
137 | |||
138 | player.on('p2pInfo', (_, data: PlayerNetworkInfo) => { | ||
139 | if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return | ||
140 | |||
141 | saveAverageBandwidth(data.bandwidthEstimate) | ||
142 | }) | ||
143 | |||
144 | const offlineNotificationElem = document.createElement('div') | ||
145 | offlineNotificationElem.classList.add('vjs-peertube-offline-notification') | ||
146 | offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work') | ||
147 | |||
148 | let offlineNotificationElemAdded = false | ||
149 | |||
150 | const handleOnline = () => { | ||
151 | if (!offlineNotificationElemAdded) return | ||
152 | |||
153 | player.el().removeChild(offlineNotificationElem) | ||
154 | offlineNotificationElemAdded = false | ||
155 | |||
156 | logger.info('The browser is online') | ||
157 | } | ||
158 | |||
159 | const handleOffline = () => { | ||
160 | if (offlineNotificationElemAdded) return | ||
161 | |||
162 | player.el().appendChild(offlineNotificationElem) | ||
163 | offlineNotificationElemAdded = true | ||
164 | |||
165 | logger.info('The browser is offline') | ||
166 | } | ||
167 | |||
168 | window.addEventListener('online', handleOnline) | ||
169 | window.addEventListener('offline', handleOffline) | ||
170 | |||
171 | player.on('dispose', () => { | ||
172 | window.removeEventListener('online', handleOnline) | ||
173 | window.removeEventListener('offline', handleOffline) | ||
174 | }) | ||
175 | |||
176 | return res(player) | ||
177 | }) | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) { | ||
182 | if (err.code === MediaError.MEDIA_ERR_DECODE) { | ||
183 | |||
184 | // Display a notification to user | ||
185 | if (this.videojsDecodeErrors === 0) { | ||
186 | options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.')) | ||
187 | } | ||
188 | |||
189 | if (this.videojsDecodeErrors === 20) { | ||
190 | this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options) | ||
191 | return | ||
192 | } | ||
193 | |||
194 | logger.info('Fast forwarding HLS to recover from an error.') | ||
195 | |||
196 | this.videojsDecodeErrors++ | ||
197 | |||
198 | options.common.startTime = currentPlayer.currentTime() + 2 | ||
199 | options.common.autoplay = true | ||
200 | this.rebuildAndUpdateVideoElement(currentPlayer, options.common) | ||
201 | |||
202 | const newPlayer = await this.buildPlayer('p2p-media-loader', options) | ||
203 | this.onPlayerChange(newPlayer) | ||
204 | } else { | ||
205 | this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options) | ||
206 | } | ||
207 | } | ||
208 | |||
209 | private static async maybeFallbackToWebTorrent ( | ||
210 | currentMode: PlayerMode, | ||
211 | currentPlayer: videojs.Player, | ||
212 | options: PeertubePlayerManagerOptions | ||
213 | ) { | ||
214 | if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') { | ||
215 | currentPlayer.peertube().displayFatalError() | ||
216 | return | ||
217 | } | ||
218 | |||
219 | logger.info('Fallback to webtorrent.') | ||
220 | |||
221 | this.rebuildAndUpdateVideoElement(currentPlayer, options.common) | ||
222 | |||
223 | await import('./shared/webtorrent/webtorrent-plugin') | ||
224 | |||
225 | const newPlayer = await this.buildPlayer('webtorrent', options) | ||
226 | this.onPlayerChange(newPlayer) | ||
227 | } | ||
228 | |||
229 | private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) { | ||
230 | const newVideoElement = document.createElement('video') | ||
231 | |||
232 | // Reset class | ||
233 | newVideoElement.className = this.playerElementClassName | ||
234 | |||
235 | // Reapply attributes | ||
236 | for (const { name, value } of this.playerElementAttributes) { | ||
237 | newVideoElement.setAttribute(name, value) | ||
238 | } | ||
239 | |||
240 | // VideoJS wraps our video element inside a div | ||
241 | let currentParentPlayerElement = commonOptions.playerElement.parentNode | ||
242 | // Fix on IOS, don't ask me why | ||
243 | if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode | ||
244 | |||
245 | currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement) | ||
246 | |||
247 | commonOptions.playerElement = newVideoElement | ||
248 | commonOptions.onPlayerElementChange(newVideoElement) | ||
249 | |||
250 | player.dispose() | ||
251 | |||
252 | return newVideoElement | ||
253 | } | ||
254 | |||
255 | private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) { | ||
256 | const options = optionsBuilder.getContextMenuOptions(player, commonOptions) | ||
257 | |||
258 | player.contextmenuUI(options) | ||
259 | } | ||
260 | } | ||
261 | |||
262 | // ############################################################################ | ||
263 | |||
264 | export { | ||
265 | videojs | ||
266 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts new file mode 100644 index 000000000..a7a2b4065 --- /dev/null +++ b/client/src/assets/player/peertube-player.ts | |||
@@ -0,0 +1,522 @@ | |||
1 | import '@peertube/videojs-contextmenu' | ||
2 | import './shared/upnext/end-card' | ||
3 | import './shared/upnext/upnext-plugin' | ||
4 | import './shared/stats/stats-card' | ||
5 | import './shared/stats/stats-plugin' | ||
6 | import './shared/bezels/bezels-plugin' | ||
7 | import './shared/peertube/peertube-plugin' | ||
8 | import './shared/resolutions/peertube-resolutions-plugin' | ||
9 | import './shared/control-bar/storyboard-plugin' | ||
10 | import './shared/control-bar/next-previous-video-button' | ||
11 | import './shared/control-bar/p2p-info-button' | ||
12 | import './shared/control-bar/peertube-link-button' | ||
13 | import './shared/control-bar/theater-button' | ||
14 | import './shared/control-bar/peertube-live-display' | ||
15 | import './shared/settings/resolution-menu-button' | ||
16 | import './shared/settings/resolution-menu-item' | ||
17 | import './shared/settings/settings-dialog' | ||
18 | import './shared/settings/settings-menu-button' | ||
19 | import './shared/settings/settings-menu-item' | ||
20 | import './shared/settings/settings-panel' | ||
21 | import './shared/settings/settings-panel-child' | ||
22 | import './shared/playlist/playlist-plugin' | ||
23 | import './shared/mobile/peertube-mobile-plugin' | ||
24 | import './shared/mobile/peertube-mobile-buttons' | ||
25 | import './shared/hotkeys/peertube-hotkeys-plugin' | ||
26 | import './shared/metrics/metrics-plugin' | ||
27 | import videojs, { VideoJsPlayer } from 'video.js' | ||
28 | import { logger } from '@root-helpers/logger' | ||
29 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
30 | import { copyToClipboard } from '@root-helpers/utils' | ||
31 | import { buildVideoOrPlaylistEmbed } from '@root-helpers/video' | ||
32 | import { isMobile } from '@root-helpers/web-browser' | ||
33 | import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@shared/core-utils' | ||
34 | import { saveAverageBandwidth } from './peertube-player-local-storage' | ||
35 | import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder' | ||
36 | import { TranslationsManager } from './translations-manager' | ||
37 | import { PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types' | ||
38 | |||
39 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
40 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' | ||
41 | |||
42 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
43 | // Change Captions to Subtitles/CC | ||
44 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' | ||
45 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
46 | CaptionsButton.prototype.label_ = ' ' | ||
47 | |||
48 | // TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged | ||
49 | const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any | ||
50 | if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) { | ||
51 | PlayProgressBar.prototype.options_.children.push('timeTooltip') | ||
52 | } | ||
53 | |||
54 | export class PeerTubePlayer { | ||
55 | private pluginsManager: PluginsManager | ||
56 | |||
57 | private videojsDecodeErrors = 0 | ||
58 | |||
59 | private p2pMediaLoaderModule: any | ||
60 | |||
61 | private player: VideoJsPlayer | ||
62 | |||
63 | private currentLoadOptions: PeerTubePlayerLoadOptions | ||
64 | |||
65 | private moduleLoaded = { | ||
66 | webVideo: false, | ||
67 | p2pMediaLoader: false | ||
68 | } | ||
69 | |||
70 | constructor (private options: PeerTubePlayerContructorOptions) { | ||
71 | this.pluginsManager = options.pluginsManager | ||
72 | } | ||
73 | |||
74 | unload () { | ||
75 | if (!this.player) return | ||
76 | |||
77 | this.disposeDynamicPluginsIfNeeded() | ||
78 | |||
79 | this.player.reset() | ||
80 | } | ||
81 | |||
82 | async load (loadOptions: PeerTubePlayerLoadOptions) { | ||
83 | this.currentLoadOptions = loadOptions | ||
84 | |||
85 | this.setPoster('') | ||
86 | |||
87 | this.disposeDynamicPluginsIfNeeded() | ||
88 | |||
89 | await this.lazyLoadModulesIfNeeded() | ||
90 | await this.buildPlayerIfNeeded() | ||
91 | |||
92 | if (this.currentLoadOptions.mode === 'p2p-media-loader') { | ||
93 | await this.loadP2PMediaLoader() | ||
94 | } else { | ||
95 | this.loadWebVideo() | ||
96 | } | ||
97 | |||
98 | this.loadDynamicPlugins() | ||
99 | |||
100 | if (this.options.controlBar === false) this.player.controlBar.hide() | ||
101 | else this.player.controlBar.show() | ||
102 | |||
103 | this.player.autoplay(this.getAutoPlayValue(this.currentLoadOptions.autoplay)) | ||
104 | |||
105 | this.player.trigger('video-change') | ||
106 | } | ||
107 | |||
108 | getPlayer () { | ||
109 | return this.player | ||
110 | } | ||
111 | |||
112 | destroy () { | ||
113 | if (this.player) this.player.dispose() | ||
114 | } | ||
115 | |||
116 | setPoster (url: string) { | ||
117 | this.player?.poster(url) | ||
118 | this.options.playerElement().poster = url | ||
119 | } | ||
120 | |||
121 | enable () { | ||
122 | if (!this.player) return | ||
123 | |||
124 | (this.player.el() as HTMLElement).style.pointerEvents = 'auto' | ||
125 | } | ||
126 | |||
127 | disable () { | ||
128 | if (!this.player) return | ||
129 | |||
130 | if (this.player.isFullscreen()) { | ||
131 | this.player.exitFullscreen() | ||
132 | } | ||
133 | |||
134 | // Disable player | ||
135 | this.player.hasStarted(false) | ||
136 | this.player.removeClass('vjs-has-autoplay') | ||
137 | this.player.bigPlayButton.hide(); | ||
138 | |||
139 | (this.player.el() as HTMLElement).style.pointerEvents = 'none' | ||
140 | } | ||
141 | |||
142 | private async loadP2PMediaLoader () { | ||
143 | const hlsOptionsBuilder = new HLSOptionsBuilder({ | ||
144 | ...pick(this.options, [ 'pluginsManager', 'serverUrl', 'authorizationHeader' ]), | ||
145 | ...pick(this.currentLoadOptions, [ | ||
146 | 'videoPassword', | ||
147 | 'requiresUserAuth', | ||
148 | 'videoFileToken', | ||
149 | 'requiresPassword', | ||
150 | 'isLive', | ||
151 | 'p2pEnabled', | ||
152 | 'liveOptions', | ||
153 | 'hls' | ||
154 | ]) | ||
155 | }, this.p2pMediaLoaderModule) | ||
156 | |||
157 | const { hlsjs, p2pMediaLoader } = await hlsOptionsBuilder.getPluginOptions() | ||
158 | |||
159 | this.player.hlsjs(hlsjs) | ||
160 | this.player.p2pMediaLoader(p2pMediaLoader) | ||
161 | } | ||
162 | |||
163 | private loadWebVideo () { | ||
164 | const webVideoOptionsBuilder = new WebVideoOptionsBuilder(pick(this.currentLoadOptions, [ | ||
165 | 'videoFileToken', | ||
166 | 'webVideo', | ||
167 | 'hls', | ||
168 | 'startTime' | ||
169 | ])) | ||
170 | |||
171 | this.player.webVideo(webVideoOptionsBuilder.getPluginOptions()) | ||
172 | } | ||
173 | |||
174 | private async buildPlayerIfNeeded () { | ||
175 | if (this.player) return | ||
176 | |||
177 | await TranslationsManager.loadLocaleInVideoJS(this.options.serverUrl, this.options.language, videojs) | ||
178 | |||
179 | const videojsOptions = await this.pluginsManager.runHook( | ||
180 | 'filter:internal.player.videojs.options.result', | ||
181 | this.getVideojsOptions() | ||
182 | ) | ||
183 | |||
184 | this.player = videojs(this.options.playerElement(), videojsOptions) | ||
185 | |||
186 | this.player.ready(() => { | ||
187 | if (!isNaN(+this.options.playbackRate)) { | ||
188 | this.player.playbackRate(+this.options.playbackRate) | ||
189 | } | ||
190 | |||
191 | let alreadyFallback = false | ||
192 | |||
193 | const handleError = () => { | ||
194 | if (alreadyFallback) return | ||
195 | alreadyFallback = true | ||
196 | |||
197 | if (this.currentLoadOptions.mode === 'p2p-media-loader') { | ||
198 | this.tryToRecoverHLSError(this.player.error()) | ||
199 | } else { | ||
200 | this.maybeFallbackToWebVideo() | ||
201 | } | ||
202 | } | ||
203 | |||
204 | this.player.one('error', () => handleError()) | ||
205 | |||
206 | this.player.on('p2p-info', (_, data: PlayerNetworkInfo) => { | ||
207 | if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return | ||
208 | |||
209 | saveAverageBandwidth(data.bandwidthEstimate) | ||
210 | }) | ||
211 | |||
212 | this.player.contextmenuUI(this.getContextMenuOptions()) | ||
213 | |||
214 | this.displayNotificationWhenOffline() | ||
215 | }) | ||
216 | } | ||
217 | |||
218 | private disposeDynamicPluginsIfNeeded () { | ||
219 | if (!this.player) return | ||
220 | |||
221 | if (this.player.usingPlugin('peertubeMobile')) this.player.peertubeMobile().dispose() | ||
222 | if (this.player.usingPlugin('peerTubeHotkeysPlugin')) this.player.peerTubeHotkeysPlugin().dispose() | ||
223 | if (this.player.usingPlugin('playlist')) this.player.playlist().dispose() | ||
224 | if (this.player.usingPlugin('bezels')) this.player.bezels().dispose() | ||
225 | if (this.player.usingPlugin('upnext')) this.player.upnext().dispose() | ||
226 | if (this.player.usingPlugin('stats')) this.player.stats().dispose() | ||
227 | if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose() | ||
228 | |||
229 | if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose() | ||
230 | |||
231 | if (this.player.usingPlugin('p2pMediaLoader')) this.player.p2pMediaLoader().dispose() | ||
232 | if (this.player.usingPlugin('hlsjs')) this.player.hlsjs().dispose() | ||
233 | |||
234 | if (this.player.usingPlugin('webVideo')) this.player.webVideo().dispose() | ||
235 | } | ||
236 | |||
237 | private loadDynamicPlugins () { | ||
238 | if (isMobile()) this.player.peertubeMobile() | ||
239 | |||
240 | this.player.bezels() | ||
241 | |||
242 | this.player.stats({ | ||
243 | videoUUID: this.currentLoadOptions.videoUUID, | ||
244 | videoIsLive: this.currentLoadOptions.isLive, | ||
245 | mode: this.currentLoadOptions.mode, | ||
246 | p2pEnabled: this.currentLoadOptions.p2pEnabled | ||
247 | }) | ||
248 | |||
249 | if (this.options.enableHotkeys === true) { | ||
250 | this.player.peerTubeHotkeysPlugin({ isLive: this.currentLoadOptions.isLive }) | ||
251 | } | ||
252 | |||
253 | if (this.currentLoadOptions.playlist) { | ||
254 | this.player.playlist(this.currentLoadOptions.playlist) | ||
255 | } | ||
256 | |||
257 | if (this.currentLoadOptions.upnext) { | ||
258 | this.player.upnext({ | ||
259 | timeout: this.currentLoadOptions.upnext.timeout, | ||
260 | |||
261 | getTitle: () => this.currentLoadOptions.nextVideo.getVideoTitle(), | ||
262 | |||
263 | next: () => this.currentLoadOptions.nextVideo.handler(), | ||
264 | isDisplayed: () => this.currentLoadOptions.nextVideo.enabled && this.currentLoadOptions.upnext.isEnabled(), | ||
265 | |||
266 | isSuspended: () => this.currentLoadOptions.upnext.isSuspended(this.player) | ||
267 | }) | ||
268 | } | ||
269 | |||
270 | if (this.currentLoadOptions.storyboard) { | ||
271 | this.player.storyboard(this.currentLoadOptions.storyboard) | ||
272 | } | ||
273 | |||
274 | if (this.currentLoadOptions.dock) { | ||
275 | this.player.peertubeDock(this.currentLoadOptions.dock) | ||
276 | } | ||
277 | } | ||
278 | |||
279 | private async lazyLoadModulesIfNeeded () { | ||
280 | if (this.currentLoadOptions.mode === 'web-video' && this.moduleLoaded.webVideo !== true) { | ||
281 | await import('./shared/web-video/web-video-plugin') | ||
282 | } | ||
283 | |||
284 | if (this.currentLoadOptions.mode === 'p2p-media-loader' && this.moduleLoaded.p2pMediaLoader !== true) { | ||
285 | const [ p2pMediaLoaderModule ] = await Promise.all([ | ||
286 | import('@peertube/p2p-media-loader-hlsjs'), | ||
287 | import('./shared/p2p-media-loader/hls-plugin'), | ||
288 | import('./shared/p2p-media-loader/p2p-media-loader-plugin') | ||
289 | ]) | ||
290 | |||
291 | this.p2pMediaLoaderModule = p2pMediaLoaderModule | ||
292 | } | ||
293 | } | ||
294 | |||
295 | private async tryToRecoverHLSError (err: any) { | ||
296 | if (err.code === MediaError.MEDIA_ERR_DECODE) { | ||
297 | |||
298 | // Display a notification to user | ||
299 | if (this.videojsDecodeErrors === 0) { | ||
300 | this.options.errorNotifier(this.player.localize('The video failed to play, will try to fast forward.')) | ||
301 | } | ||
302 | |||
303 | if (this.videojsDecodeErrors === 20) { | ||
304 | this.maybeFallbackToWebVideo() | ||
305 | return | ||
306 | } | ||
307 | |||
308 | logger.info('Fast forwarding HLS to recover from an error.') | ||
309 | |||
310 | this.videojsDecodeErrors++ | ||
311 | |||
312 | await this.load({ | ||
313 | ...this.currentLoadOptions, | ||
314 | |||
315 | mode: 'p2p-media-loader', | ||
316 | startTime: this.player.currentTime() + 2, | ||
317 | autoplay: true | ||
318 | }) | ||
319 | } else { | ||
320 | this.maybeFallbackToWebVideo() | ||
321 | } | ||
322 | } | ||
323 | |||
324 | private async maybeFallbackToWebVideo () { | ||
325 | if (this.currentLoadOptions.webVideo.videoFiles.length === 0 || this.currentLoadOptions.mode === 'web-video') { | ||
326 | this.player.peertube().displayFatalError() | ||
327 | return | ||
328 | } | ||
329 | |||
330 | logger.info('Fallback to web-video.') | ||
331 | |||
332 | await this.load({ | ||
333 | ...this.currentLoadOptions, | ||
334 | |||
335 | mode: 'web-video', | ||
336 | startTime: this.player.currentTime(), | ||
337 | autoplay: true | ||
338 | }) | ||
339 | } | ||
340 | |||
341 | getVideojsOptions (): videojs.PlayerOptions { | ||
342 | const html5 = { | ||
343 | preloadTextTracks: false | ||
344 | } | ||
345 | |||
346 | const plugins: VideoJSPluginOptions = { | ||
347 | peertube: { | ||
348 | hasAutoplay: () => this.getAutoPlayValue(this.currentLoadOptions.autoplay), | ||
349 | |||
350 | videoViewUrl: () => this.currentLoadOptions.videoViewUrl, | ||
351 | videoViewIntervalMs: this.options.videoViewIntervalMs, | ||
352 | |||
353 | authorizationHeader: this.options.authorizationHeader, | ||
354 | |||
355 | videoDuration: () => this.currentLoadOptions.duration, | ||
356 | |||
357 | startTime: () => this.currentLoadOptions.startTime, | ||
358 | stopTime: () => this.currentLoadOptions.stopTime, | ||
359 | |||
360 | videoCaptions: () => this.currentLoadOptions.videoCaptions, | ||
361 | isLive: () => this.currentLoadOptions.isLive, | ||
362 | videoUUID: () => this.currentLoadOptions.videoUUID, | ||
363 | subtitle: () => this.currentLoadOptions.subtitle | ||
364 | }, | ||
365 | metrics: { | ||
366 | mode: () => this.currentLoadOptions.mode, | ||
367 | |||
368 | metricsUrl: () => this.options.metricsUrl, | ||
369 | videoUUID: () => this.currentLoadOptions.videoUUID | ||
370 | } | ||
371 | } | ||
372 | |||
373 | const controlBarOptionsBuilder = new ControlBarOptionsBuilder({ | ||
374 | ...this.options, | ||
375 | |||
376 | videoShortUUID: () => this.currentLoadOptions.videoShortUUID, | ||
377 | p2pEnabled: () => this.currentLoadOptions.p2pEnabled, | ||
378 | |||
379 | nextVideo: () => this.currentLoadOptions.nextVideo, | ||
380 | previousVideo: () => this.currentLoadOptions.previousVideo | ||
381 | }) | ||
382 | |||
383 | const videojsOptions = { | ||
384 | html5, | ||
385 | |||
386 | // We don't use text track settings for now | ||
387 | textTrackSettings: false as any, // FIXME: typings | ||
388 | controls: this.options.controls !== undefined ? this.options.controls : true, | ||
389 | loop: this.options.loop !== undefined ? this.options.loop : false, | ||
390 | |||
391 | muted: this.options.muted !== undefined | ||
392 | ? this.options.muted | ||
393 | : undefined, // Undefined so the player knows it has to check the local storage | ||
394 | |||
395 | autoplay: this.getAutoPlayValue(this.currentLoadOptions.autoplay), | ||
396 | |||
397 | poster: this.currentLoadOptions.poster, | ||
398 | inactivityTimeout: this.options.inactivityTimeout, | ||
399 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], | ||
400 | |||
401 | plugins, | ||
402 | |||
403 | controlBar: { | ||
404 | children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings | ||
405 | }, | ||
406 | |||
407 | language: this.options.language && !isDefaultLocale(this.options.language) | ||
408 | ? this.options.language | ||
409 | : undefined | ||
410 | } | ||
411 | |||
412 | return videojsOptions | ||
413 | } | ||
414 | |||
415 | private getAutoPlayValue (autoplay: boolean): videojs.Autoplay { | ||
416 | if (autoplay !== true) return false | ||
417 | |||
418 | return this.currentLoadOptions.forceAutoplay | ||
419 | ? 'any' | ||
420 | : 'play' | ||
421 | } | ||
422 | |||
423 | private displayNotificationWhenOffline () { | ||
424 | const offlineNotificationElem = document.createElement('div') | ||
425 | offlineNotificationElem.classList.add('vjs-peertube-offline-notification') | ||
426 | offlineNotificationElem.innerText = this.player.localize('You seem to be offline and the video may not work') | ||
427 | |||
428 | let offlineNotificationElemAdded = false | ||
429 | |||
430 | const handleOnline = () => { | ||
431 | if (!offlineNotificationElemAdded) return | ||
432 | |||
433 | this.player.el().removeChild(offlineNotificationElem) | ||
434 | offlineNotificationElemAdded = false | ||
435 | |||
436 | logger.info('The browser is online') | ||
437 | } | ||
438 | |||
439 | const handleOffline = () => { | ||
440 | if (offlineNotificationElemAdded) return | ||
441 | |||
442 | this.player.el().appendChild(offlineNotificationElem) | ||
443 | offlineNotificationElemAdded = true | ||
444 | |||
445 | logger.info('The browser is offline') | ||
446 | } | ||
447 | |||
448 | window.addEventListener('online', handleOnline) | ||
449 | window.addEventListener('offline', handleOffline) | ||
450 | |||
451 | this.player.on('dispose', () => { | ||
452 | window.removeEventListener('online', handleOnline) | ||
453 | window.removeEventListener('offline', handleOffline) | ||
454 | }) | ||
455 | } | ||
456 | |||
457 | private getContextMenuOptions () { | ||
458 | |||
459 | const content = () => { | ||
460 | const self = this | ||
461 | const player = this.player | ||
462 | |||
463 | const shortUUID = self.currentLoadOptions.videoShortUUID | ||
464 | const isLoopEnabled = player.options_['loop'] | ||
465 | |||
466 | const items = [ | ||
467 | { | ||
468 | icon: 'repeat', | ||
469 | label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''), | ||
470 | listener: function () { | ||
471 | player.options_['loop'] = !isLoopEnabled | ||
472 | } | ||
473 | }, | ||
474 | { | ||
475 | label: player.localize('Copy the video URL'), | ||
476 | listener: function () { | ||
477 | copyToClipboard(buildVideoLink({ shortUUID })) | ||
478 | } | ||
479 | }, | ||
480 | { | ||
481 | label: player.localize('Copy the video URL at the current time'), | ||
482 | listener: function () { | ||
483 | const url = buildVideoLink({ shortUUID }) | ||
484 | |||
485 | copyToClipboard(decorateVideoLink({ url, startTime: player.currentTime() })) | ||
486 | } | ||
487 | }, | ||
488 | { | ||
489 | icon: 'code', | ||
490 | label: player.localize('Copy embed code'), | ||
491 | listener: () => { | ||
492 | copyToClipboard(buildVideoOrPlaylistEmbed({ | ||
493 | embedUrl: self.currentLoadOptions.embedUrl, | ||
494 | embedTitle: self.currentLoadOptions.embedTitle | ||
495 | })) | ||
496 | } | ||
497 | } | ||
498 | ] | ||
499 | |||
500 | items.push({ | ||
501 | icon: 'info', | ||
502 | label: player.localize('Stats for nerds'), | ||
503 | listener: () => { | ||
504 | player.stats().show() | ||
505 | } | ||
506 | }) | ||
507 | |||
508 | return items.map(i => ({ | ||
509 | ...i, | ||
510 | label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label | ||
511 | })) | ||
512 | } | ||
513 | |||
514 | return { content } | ||
515 | } | ||
516 | } | ||
517 | |||
518 | // ############################################################################ | ||
519 | |||
520 | export { | ||
521 | videojs | ||
522 | } | ||
diff --git a/client/src/assets/player/shared/bezels/bezels-plugin.ts b/client/src/assets/player/shared/bezels/bezels-plugin.ts index ca88bc1f9..6afb2c6a3 100644 --- a/client/src/assets/player/shared/bezels/bezels-plugin.ts +++ b/client/src/assets/player/shared/bezels/bezels-plugin.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import './pause-bezel' | 2 | import { PauseBezel } from './pause-bezel' |
3 | 3 | ||
4 | const Plugin = videojs.getPlugin('plugin') | 4 | const Plugin = videojs.getPlugin('plugin') |
5 | 5 | ||
@@ -12,7 +12,7 @@ class BezelsPlugin extends Plugin { | |||
12 | player.addClass('vjs-bezels') | 12 | player.addClass('vjs-bezels') |
13 | }) | 13 | }) |
14 | 14 | ||
15 | player.addChild('PauseBezel', options) | 15 | player.addChild(new PauseBezel(player, options)) |
16 | } | 16 | } |
17 | } | 17 | } |
18 | 18 | ||
diff --git a/client/src/assets/player/shared/bezels/pause-bezel.ts b/client/src/assets/player/shared/bezels/pause-bezel.ts index e35c39a5f..d364ad0dd 100644 --- a/client/src/assets/player/shared/bezels/pause-bezel.ts +++ b/client/src/assets/player/shared/bezels/pause-bezel.ts | |||
@@ -32,26 +32,61 @@ function getPlayBezel () { | |||
32 | } | 32 | } |
33 | 33 | ||
34 | const Component = videojs.getComponent('Component') | 34 | const Component = videojs.getComponent('Component') |
35 | class PauseBezel extends Component { | 35 | export class PauseBezel extends Component { |
36 | container: HTMLDivElement | 36 | container: HTMLDivElement |
37 | 37 | ||
38 | private firstPlayDone = false | ||
39 | private paused = false | ||
40 | |||
41 | private playerPauseHandler: () => void | ||
42 | private playerPlayHandler: () => void | ||
43 | private videoChangeHandler: () => void | ||
44 | |||
38 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { | 45 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { |
39 | super(player, options) | 46 | super(player, options) |
40 | 47 | ||
41 | // Hide bezels on mobile since we already have our mobile overlay | 48 | // Hide bezels on mobile since we already have our mobile overlay |
42 | if (isMobile()) return | 49 | if (isMobile()) return |
43 | 50 | ||
44 | player.on('pause', (_: any) => { | 51 | this.playerPauseHandler = () => { |
45 | if (player.seeking() || player.ended()) return | 52 | if (player.seeking()) return |
53 | |||
54 | this.paused = true | ||
55 | |||
56 | if (player.ended()) return | ||
57 | |||
46 | this.container.innerHTML = getPauseBezel() | 58 | this.container.innerHTML = getPauseBezel() |
47 | this.showBezel() | 59 | this.showBezel() |
48 | }) | 60 | } |
61 | |||
62 | this.playerPlayHandler = () => { | ||
63 | if (player.seeking() || !this.firstPlayDone || !this.paused) { | ||
64 | this.firstPlayDone = true | ||
65 | return | ||
66 | } | ||
67 | |||
68 | this.paused = false | ||
69 | this.firstPlayDone = true | ||
49 | 70 | ||
50 | player.on('play', (_: any) => { | ||
51 | if (player.seeking()) return | ||
52 | this.container.innerHTML = getPlayBezel() | 71 | this.container.innerHTML = getPlayBezel() |
53 | this.showBezel() | 72 | this.showBezel() |
54 | }) | 73 | } |
74 | |||
75 | this.videoChangeHandler = () => { | ||
76 | this.firstPlayDone = false | ||
77 | } | ||
78 | |||
79 | player.on('video-change', () => this.videoChangeHandler) | ||
80 | player.on('pause', this.playerPauseHandler) | ||
81 | player.on('play', this.playerPlayHandler) | ||
82 | } | ||
83 | |||
84 | dispose () { | ||
85 | if (this.playerPauseHandler) this.player().off('pause', this.playerPauseHandler) | ||
86 | if (this.playerPlayHandler) this.player().off('play', this.playerPlayHandler) | ||
87 | if (this.videoChangeHandler) this.player().off('video-change', this.videoChangeHandler) | ||
88 | |||
89 | super.dispose() | ||
55 | } | 90 | } |
56 | 91 | ||
57 | createEl () { | 92 | createEl () { |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index e71e90713..9307027f6 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -2,5 +2,5 @@ export * from './next-previous-video-button' | |||
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | 4 | export * from './peertube-live-display' |
5 | export * from './peertube-load-progress-bar' | 5 | export * from './storyboard-plugin' |
6 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts index b7b986806..18a107f52 100644 --- a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts +++ b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts | |||
@@ -4,14 +4,18 @@ import { NextPreviousVideoButtonOptions } from '../../types' | |||
4 | const Button = videojs.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
5 | 5 | ||
6 | class NextPreviousVideoButton extends Button { | 6 | class NextPreviousVideoButton extends Button { |
7 | private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions | 7 | options_: NextPreviousVideoButtonOptions & videojs.ComponentOptions |
8 | 8 | ||
9 | constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) { | 9 | constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions & videojs.ComponentOptions) { |
10 | super(player, options as any) | 10 | super(player, options) |
11 | 11 | ||
12 | this.nextPreviousVideoButtonOptions = options | 12 | this.player().on('video-change', () => { |
13 | this.updateDisabled() | ||
14 | this.updateShowing() | ||
15 | }) | ||
13 | 16 | ||
14 | this.update() | 17 | this.updateDisabled() |
18 | this.updateShowing() | ||
15 | } | 19 | } |
16 | 20 | ||
17 | createEl () { | 21 | createEl () { |
@@ -35,15 +39,20 @@ class NextPreviousVideoButton extends Button { | |||
35 | } | 39 | } |
36 | 40 | ||
37 | handleClick () { | 41 | handleClick () { |
38 | this.nextPreviousVideoButtonOptions.handler() | 42 | this.options_.handler() |
39 | } | 43 | } |
40 | 44 | ||
41 | update () { | 45 | updateDisabled () { |
42 | const disabled = this.nextPreviousVideoButtonOptions.isDisabled() | 46 | const disabled = this.options_.isDisabled() |
43 | 47 | ||
44 | if (disabled) this.addClass('vjs-disabled') | 48 | if (disabled) this.addClass('vjs-disabled') |
45 | else this.removeClass('vjs-disabled') | 49 | else this.removeClass('vjs-disabled') |
46 | } | 50 | } |
51 | |||
52 | updateShowing () { | ||
53 | if (this.options_.isDisplayed()) this.show() | ||
54 | else this.hide() | ||
55 | } | ||
47 | } | 56 | } |
48 | 57 | ||
49 | videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) | 58 | videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) |
diff --git a/client/src/assets/player/shared/control-bar/p2p-info-button.ts b/client/src/assets/player/shared/control-bar/p2p-info-button.ts index 1979654ad..4177b3280 100644 --- a/client/src/assets/player/shared/control-bar/p2p-info-button.ts +++ b/client/src/assets/player/shared/control-bar/p2p-info-button.ts | |||
@@ -1,71 +1,44 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { PeerTubeP2PInfoButtonOptions, PlayerNetworkInfo } from '../../types' | 2 | import { PlayerNetworkInfo } from '../../types' |
3 | import { bytes } from '../common' | 3 | import { bytes } from '../common' |
4 | 4 | ||
5 | const Button = videojs.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
6 | class P2pInfoButton extends Button { | 6 | class P2PInfoButton extends Button { |
7 | 7 | el_: HTMLElement | |
8 | constructor (player: videojs.Player, options?: PeerTubeP2PInfoButtonOptions) { | ||
9 | super(player, options as any) | ||
10 | } | ||
11 | 8 | ||
12 | createEl () { | 9 | createEl () { |
13 | const div = videojs.dom.createEl('div', { | 10 | const div = videojs.dom.createEl('div', { className: 'vjs-peertube' }) |
14 | className: 'vjs-peertube' | 11 | const subDivP2P = videojs.dom.createEl('div', { |
15 | }) | ||
16 | const subDivWebtorrent = videojs.dom.createEl('div', { | ||
17 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info | 12 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info |
18 | }) as HTMLDivElement | 13 | }) as HTMLDivElement |
19 | div.appendChild(subDivWebtorrent) | 14 | div.appendChild(subDivP2P) |
20 | 15 | ||
21 | // Stop here if P2P is not enabled | 16 | const downloadIcon = videojs.dom.createEl('span', { className: 'icon icon-download' }) |
22 | const p2pEnabled = (this.options_ as PeerTubeP2PInfoButtonOptions).p2pEnabled | 17 | subDivP2P.appendChild(downloadIcon) |
23 | if (!p2pEnabled) return div as HTMLButtonElement | ||
24 | 18 | ||
25 | const downloadIcon = videojs.dom.createEl('span', { | 19 | const downloadSpeedText = videojs.dom.createEl('span', { className: 'download-speed-text' }) |
26 | className: 'icon icon-download' | 20 | const downloadSpeedNumber = videojs.dom.createEl('span', { className: 'download-speed-number' }) |
27 | }) | ||
28 | subDivWebtorrent.appendChild(downloadIcon) | ||
29 | |||
30 | const downloadSpeedText = videojs.dom.createEl('span', { | ||
31 | className: 'download-speed-text' | ||
32 | }) | ||
33 | const downloadSpeedNumber = videojs.dom.createEl('span', { | ||
34 | className: 'download-speed-number' | ||
35 | }) | ||
36 | const downloadSpeedUnit = videojs.dom.createEl('span') | 21 | const downloadSpeedUnit = videojs.dom.createEl('span') |
37 | downloadSpeedText.appendChild(downloadSpeedNumber) | 22 | downloadSpeedText.appendChild(downloadSpeedNumber) |
38 | downloadSpeedText.appendChild(downloadSpeedUnit) | 23 | downloadSpeedText.appendChild(downloadSpeedUnit) |
39 | subDivWebtorrent.appendChild(downloadSpeedText) | 24 | subDivP2P.appendChild(downloadSpeedText) |
40 | 25 | ||
41 | const uploadIcon = videojs.dom.createEl('span', { | 26 | const uploadIcon = videojs.dom.createEl('span', { className: 'icon icon-upload' }) |
42 | className: 'icon icon-upload' | 27 | subDivP2P.appendChild(uploadIcon) |
43 | }) | ||
44 | subDivWebtorrent.appendChild(uploadIcon) | ||
45 | 28 | ||
46 | const uploadSpeedText = videojs.dom.createEl('span', { | 29 | const uploadSpeedText = videojs.dom.createEl('span', { className: 'upload-speed-text' }) |
47 | className: 'upload-speed-text' | 30 | const uploadSpeedNumber = videojs.dom.createEl('span', { className: 'upload-speed-number' }) |
48 | }) | ||
49 | const uploadSpeedNumber = videojs.dom.createEl('span', { | ||
50 | className: 'upload-speed-number' | ||
51 | }) | ||
52 | const uploadSpeedUnit = videojs.dom.createEl('span') | 31 | const uploadSpeedUnit = videojs.dom.createEl('span') |
53 | uploadSpeedText.appendChild(uploadSpeedNumber) | 32 | uploadSpeedText.appendChild(uploadSpeedNumber) |
54 | uploadSpeedText.appendChild(uploadSpeedUnit) | 33 | uploadSpeedText.appendChild(uploadSpeedUnit) |
55 | subDivWebtorrent.appendChild(uploadSpeedText) | 34 | subDivP2P.appendChild(uploadSpeedText) |
56 | 35 | ||
57 | const peersText = videojs.dom.createEl('span', { | 36 | const peersText = videojs.dom.createEl('span', { className: 'peers-text' }) |
58 | className: 'peers-text' | 37 | const peersNumber = videojs.dom.createEl('span', { className: 'peers-number' }) |
59 | }) | 38 | subDivP2P.appendChild(peersNumber) |
60 | const peersNumber = videojs.dom.createEl('span', { | 39 | subDivP2P.appendChild(peersText) |
61 | className: 'peers-number' | ||
62 | }) | ||
63 | subDivWebtorrent.appendChild(peersNumber) | ||
64 | subDivWebtorrent.appendChild(peersText) | ||
65 | 40 | ||
66 | const subDivHttp = videojs.dom.createEl('div', { | 41 | const subDivHttp = videojs.dom.createEl('div', { className: 'vjs-peertube-hidden' }) as HTMLElement |
67 | className: 'vjs-peertube-hidden' | ||
68 | }) | ||
69 | const subDivHttpText = videojs.dom.createEl('span', { | 42 | const subDivHttpText = videojs.dom.createEl('span', { |
70 | className: 'http-fallback', | 43 | className: 'http-fallback', |
71 | textContent: 'HTTP' | 44 | textContent: 'HTTP' |
@@ -74,14 +47,9 @@ class P2pInfoButton extends Button { | |||
74 | subDivHttp.appendChild(subDivHttpText) | 47 | subDivHttp.appendChild(subDivHttpText) |
75 | div.appendChild(subDivHttp) | 48 | div.appendChild(subDivHttp) |
76 | 49 | ||
77 | this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { | 50 | this.player_.on('p2p-info', (_event: any, data: PlayerNetworkInfo) => { |
78 | // We are in HTTP fallback | 51 | subDivP2P.className = 'vjs-peertube-displayed' |
79 | if (!data) { | 52 | subDivHttp.className = 'vjs-peertube-hidden' |
80 | subDivHttp.className = 'vjs-peertube-displayed' | ||
81 | subDivWebtorrent.className = 'vjs-peertube-hidden' | ||
82 | |||
83 | return | ||
84 | } | ||
85 | 53 | ||
86 | const p2pStats = data.p2p | 54 | const p2pStats = data.p2p |
87 | const httpStats = data.http | 55 | const httpStats = data.http |
@@ -92,17 +60,17 @@ class P2pInfoButton extends Button { | |||
92 | const totalUploaded = bytes(p2pStats.uploaded) | 60 | const totalUploaded = bytes(p2pStats.uploaded) |
93 | const numPeers = p2pStats.numPeers | 61 | const numPeers = p2pStats.numPeers |
94 | 62 | ||
95 | subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' | 63 | subDivP2P.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' |
96 | 64 | ||
97 | if (data.source === 'p2p-media-loader') { | 65 | if (data.source === 'p2p-media-loader') { |
98 | const downloadedFromServer = bytes(httpStats.downloaded).join(' ') | 66 | const downloadedFromServer = bytes(httpStats.downloaded).join(' ') |
99 | const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | 67 | const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') |
100 | 68 | ||
101 | subDivWebtorrent.title += | 69 | subDivP2P.title += |
102 | ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + | 70 | ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + |
103 | ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' | 71 | ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' |
104 | } | 72 | } |
105 | subDivWebtorrent.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') | 73 | subDivP2P.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') |
106 | 74 | ||
107 | downloadSpeedNumber.textContent = downloadSpeed[0] | 75 | downloadSpeedNumber.textContent = downloadSpeed[0] |
108 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] | 76 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] |
@@ -114,11 +82,24 @@ class P2pInfoButton extends Button { | |||
114 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) | 82 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) |
115 | 83 | ||
116 | subDivHttp.className = 'vjs-peertube-hidden' | 84 | subDivHttp.className = 'vjs-peertube-hidden' |
117 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 85 | subDivP2P.className = 'vjs-peertube-displayed' |
86 | }) | ||
87 | |||
88 | this.player_.on('http-info', (_event, data: PlayerNetworkInfo) => { | ||
89 | // We are in HTTP fallback | ||
90 | subDivHttp.className = 'vjs-peertube-displayed' | ||
91 | subDivP2P.className = 'vjs-peertube-hidden' | ||
92 | |||
93 | subDivHttp.title = this.player().localize('Total downloaded: ') + bytes(data.http.downloaded).join(' ') | ||
94 | }) | ||
95 | |||
96 | this.player_.on('video-change', () => { | ||
97 | subDivP2P.className = 'vjs-peertube-hidden' | ||
98 | subDivHttp.className = 'vjs-peertube-hidden' | ||
118 | }) | 99 | }) |
119 | 100 | ||
120 | return div as HTMLButtonElement | 101 | return div as HTMLButtonElement |
121 | } | 102 | } |
122 | } | 103 | } |
123 | 104 | ||
124 | videojs.registerComponent('P2PInfoButton', P2pInfoButton) | 105 | videojs.registerComponent('P2PInfoButton', P2PInfoButton) |
diff --git a/client/src/assets/player/shared/control-bar/peertube-link-button.ts b/client/src/assets/player/shared/control-bar/peertube-link-button.ts index 45d7ac42f..8242b9cea 100644 --- a/client/src/assets/player/shared/control-bar/peertube-link-button.ts +++ b/client/src/assets/player/shared/control-bar/peertube-link-button.ts | |||
@@ -3,37 +3,58 @@ import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | |||
3 | import { PeerTubeLinkButtonOptions } from '../../types' | 3 | import { PeerTubeLinkButtonOptions } from '../../types' |
4 | 4 | ||
5 | const Component = videojs.getComponent('Component') | 5 | const Component = videojs.getComponent('Component') |
6 | |||
6 | class PeerTubeLinkButton extends Component { | 7 | class PeerTubeLinkButton extends Component { |
8 | private mouseEnterHandler: () => void | ||
9 | private clickHandler: () => void | ||
7 | 10 | ||
8 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | 11 | options_: PeerTubeLinkButtonOptions & videojs.ComponentOptions |
9 | super(player, options as any) | ||
10 | } | ||
11 | 12 | ||
12 | createEl () { | 13 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions & videojs.ComponentOptions) { |
13 | return this.buildElement() | 14 | super(player, options) |
15 | |||
16 | this.updateShowing() | ||
17 | this.player().on('video-change', () => this.updateShowing()) | ||
14 | } | 18 | } |
15 | 19 | ||
16 | updateHref () { | 20 | dispose () { |
17 | this.el().setAttribute('href', this.buildLink()) | 21 | if (this.el()) return |
22 | |||
23 | this.el().removeEventListener('mouseenter', this.mouseEnterHandler) | ||
24 | this.el().removeEventListener('click', this.clickHandler) | ||
25 | |||
26 | super.dispose() | ||
18 | } | 27 | } |
19 | 28 | ||
20 | private buildElement () { | 29 | createEl () { |
21 | const el = videojs.dom.createEl('a', { | 30 | const el = videojs.dom.createEl('a', { |
22 | href: this.buildLink(), | 31 | href: this.buildLink(), |
23 | innerHTML: (this.options_ as PeerTubeLinkButtonOptions).instanceName, | 32 | innerHTML: this.options_.instanceName, |
24 | title: this.player().localize('Video page (new window)'), | 33 | title: this.player().localize('Video page (new window)'), |
25 | className: 'vjs-peertube-link', | 34 | className: 'vjs-peertube-link', |
26 | target: '_blank' | 35 | target: '_blank' |
27 | }) | 36 | }) |
28 | 37 | ||
29 | el.addEventListener('mouseenter', () => this.updateHref()) | 38 | this.mouseEnterHandler = () => this.updateHref() |
30 | el.addEventListener('click', () => this.player().pause()) | 39 | this.clickHandler = () => this.player().pause() |
40 | |||
41 | el.addEventListener('mouseenter', this.mouseEnterHandler) | ||
42 | el.addEventListener('click', this.clickHandler) | ||
43 | |||
44 | return el | ||
45 | } | ||
46 | |||
47 | updateShowing () { | ||
48 | if (this.options_.isDisplayed()) this.show() | ||
49 | else this.hide() | ||
50 | } | ||
31 | 51 | ||
32 | return el as HTMLButtonElement | 52 | updateHref () { |
53 | this.el().setAttribute('href', this.buildLink()) | ||
33 | } | 54 | } |
34 | 55 | ||
35 | private buildLink () { | 56 | private buildLink () { |
36 | const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID }) | 57 | const url = buildVideoLink({ shortUUID: this.options_.shortUUID() }) |
37 | 58 | ||
38 | return decorateVideoLink({ url, startTime: this.player().currentTime() }) | 59 | return decorateVideoLink({ url, startTime: this.player().currentTime() }) |
39 | } | 60 | } |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts index 649eb0b00..f9f6bf12f 100644 --- a/client/src/assets/player/shared/control-bar/peertube-live-display.ts +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -13,7 +13,6 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
13 | 13 | ||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | 14 | this.interval = this.setInterval(() => this.updateClass(), 1000) |
15 | 15 | ||
16 | this.show() | ||
17 | this.updateSync(true) | 16 | this.updateSync(true) |
18 | } | 17 | } |
19 | 18 | ||
@@ -30,7 +29,7 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
30 | 29 | ||
31 | createEl () { | 30 | createEl () { |
32 | const el = super.createEl('div', { | 31 | const el = super.createEl('div', { |
33 | className: 'vjs-live-control vjs-control' | 32 | className: 'vjs-pt-live-control vjs-control' |
34 | }) | 33 | }) |
35 | 34 | ||
36 | this.contentEl_ = videojs.dom.createEl('div', { | 35 | this.contentEl_ = videojs.dom.createEl('div', { |
@@ -83,10 +82,9 @@ class PeerTubeLiveDisplay extends ClickableComponent { | |||
83 | } | 82 | } |
84 | 83 | ||
85 | private getHLSJS () { | 84 | private getHLSJS () { |
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | 85 | if (!this.player()?.usingPlugin('p2pMediaLoader')) return |
87 | if (!p2pMediaLoader) return undefined | ||
88 | 86 | ||
89 | return p2pMediaLoader().getHLSJS() | 87 | return this.player().p2pMediaLoader().getHLSJS() |
90 | } | 88 | } |
91 | } | 89 | } |
92 | 90 | ||
diff --git a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts b/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts deleted file mode 100644 index 623e70eb2..000000000 --- a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class PeerTubeLoadProgressBar extends Component { | ||
6 | |||
7 | constructor (player: videojs.Player, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | |||
10 | this.on(player, 'progress', this.update) | ||
11 | } | ||
12 | |||
13 | createEl () { | ||
14 | return super.createEl('div', { | ||
15 | className: 'vjs-load-progress', | ||
16 | innerHTML: `<span class="vjs-control-text"><span>${this.localize('Loaded')}</span>: 0%</span>` | ||
17 | }) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | super.dispose() | ||
22 | } | ||
23 | |||
24 | update () { | ||
25 | const torrent = this.player().webtorrent().getTorrent() | ||
26 | if (!torrent) return | ||
27 | |||
28 | (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' | ||
29 | } | ||
30 | |||
31 | } | ||
32 | |||
33 | Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar) | ||
diff --git a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts new file mode 100644 index 000000000..80c69b5f2 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts | |||
@@ -0,0 +1,197 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { StoryboardOptions } from '../../types' | ||
3 | |||
4 | // Big thanks to this beautiful plugin: https://github.com/phloxic/videojs-sprite-thumbnails | ||
5 | // Adapted to respect peertube player style | ||
6 | |||
7 | const Plugin = videojs.getPlugin('plugin') | ||
8 | |||
9 | class StoryboardPlugin extends Plugin { | ||
10 | private url: string | ||
11 | private height: number | ||
12 | private width: number | ||
13 | private interval: number | ||
14 | |||
15 | private cached: boolean | ||
16 | |||
17 | private mouseTimeTooltip: videojs.MouseTimeDisplay | ||
18 | private seekBar: { el(): HTMLElement, mouseTimeDisplay: any, playProgressBar: any } | ||
19 | private progress: any | ||
20 | |||
21 | private spritePlaceholder: HTMLElement | ||
22 | |||
23 | private readonly sprites: { [id: string]: HTMLImageElement } = {} | ||
24 | |||
25 | private readonly boundedHijackMouseTooltip: typeof StoryboardPlugin.prototype.hijackMouseTooltip | ||
26 | |||
27 | private onReadyOrLoadstartHandler: (event: { type: 'ready' }) => void | ||
28 | |||
29 | constructor (player: videojs.Player, options: videojs.ComponentOptions & StoryboardOptions) { | ||
30 | super(player, options) | ||
31 | |||
32 | this.url = options.url | ||
33 | this.height = options.height | ||
34 | this.width = options.width | ||
35 | this.interval = options.interval | ||
36 | |||
37 | this.boundedHijackMouseTooltip = this.hijackMouseTooltip.bind(this) | ||
38 | |||
39 | this.init() | ||
40 | |||
41 | this.player.ready(() => { | ||
42 | player.addClass('vjs-storyboard') | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | init () { | ||
47 | const controls = this.player.controlBar as any | ||
48 | |||
49 | // default control bar component tree is expected | ||
50 | // https://docs.videojs.com/tutorial-components.html#default-component-tree | ||
51 | this.progress = controls?.progressControl | ||
52 | this.seekBar = this.progress?.seekBar | ||
53 | |||
54 | this.mouseTimeTooltip = this.seekBar?.mouseTimeDisplay?.timeTooltip | ||
55 | |||
56 | this.spritePlaceholder = videojs.dom.createEl('div', { className: 'vjs-storyboard-sprite-placeholder' }) as HTMLElement | ||
57 | this.seekBar?.el()?.appendChild(this.spritePlaceholder) | ||
58 | |||
59 | this.onReadyOrLoadstartHandler = event => { | ||
60 | if (event.type !== 'ready') { | ||
61 | const spriteSource = this.player.currentSources().find(source => { | ||
62 | return Object.prototype.hasOwnProperty.call(source, 'storyboard') | ||
63 | }) as any | ||
64 | const spriteOpts = spriteSource?.['storyboard'] as StoryboardOptions | ||
65 | |||
66 | if (spriteOpts) { | ||
67 | this.url = spriteOpts.url | ||
68 | this.height = spriteOpts.height | ||
69 | this.width = spriteOpts.width | ||
70 | this.interval = spriteOpts.interval | ||
71 | } | ||
72 | } | ||
73 | |||
74 | this.cached = !!this.sprites[this.url] | ||
75 | |||
76 | this.load() | ||
77 | } | ||
78 | |||
79 | this.player.on([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler) | ||
80 | } | ||
81 | |||
82 | dispose () { | ||
83 | if (this.onReadyOrLoadstartHandler) this.player.off([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler) | ||
84 | if (this.progress) this.progress.off([ 'mousemove', 'touchmove' ], this.boundedHijackMouseTooltip) | ||
85 | |||
86 | this.seekBar?.el()?.removeChild(this.spritePlaceholder) | ||
87 | |||
88 | super.dispose() | ||
89 | } | ||
90 | |||
91 | private load () { | ||
92 | const spriteEvents = [ 'mousemove', 'touchmove' ] | ||
93 | |||
94 | if (this.isReady()) { | ||
95 | if (!this.cached) { | ||
96 | this.sprites[this.url] = videojs.dom.createEl('img', { | ||
97 | src: this.url | ||
98 | }) | ||
99 | } | ||
100 | this.progress.on(spriteEvents, this.boundedHijackMouseTooltip) | ||
101 | } else { | ||
102 | this.progress.off(spriteEvents, this.boundedHijackMouseTooltip) | ||
103 | |||
104 | this.resetMouseTooltip() | ||
105 | } | ||
106 | } | ||
107 | |||
108 | private hijackMouseTooltip (evt: Event) { | ||
109 | const sprite = this.sprites[this.url] | ||
110 | const imgWidth = sprite.naturalWidth | ||
111 | const imgHeight = sprite.naturalHeight | ||
112 | const seekBarEl = this.seekBar.el() | ||
113 | |||
114 | if (!sprite.complete || !imgWidth || !imgHeight) { | ||
115 | this.resetMouseTooltip() | ||
116 | return | ||
117 | } | ||
118 | |||
119 | this.player.requestNamedAnimationFrame('StoryBoardPlugin#hijackMouseTooltip', () => { | ||
120 | const seekBarRect = videojs.dom.getBoundingClientRect(seekBarEl) | ||
121 | const playerRect = videojs.dom.getBoundingClientRect(this.player.el()) | ||
122 | |||
123 | if (!seekBarRect || !playerRect) return | ||
124 | |||
125 | const seekBarX = videojs.dom.getPointerPosition(seekBarEl, evt).x | ||
126 | let position = seekBarX * this.player.duration() | ||
127 | |||
128 | const maxPosition = Math.round((imgHeight / this.height) * (imgWidth / this.width)) - 1 | ||
129 | position = Math.min(position / this.interval, maxPosition) | ||
130 | |||
131 | const responsive = 600 | ||
132 | const playerWidth = this.player.currentWidth() | ||
133 | const scaleFactor = responsive && playerWidth < responsive | ||
134 | ? playerWidth / responsive | ||
135 | : 1 | ||
136 | const columns = imgWidth / this.width | ||
137 | |||
138 | const scaledWidth = this.width * scaleFactor | ||
139 | const scaledHeight = this.height * scaleFactor | ||
140 | const cleft = Math.floor(position % columns) * -scaledWidth | ||
141 | const ctop = Math.floor(position / columns) * -scaledHeight | ||
142 | |||
143 | const bgSize = `${imgWidth * scaleFactor}px ${imgHeight * scaleFactor}px` | ||
144 | const topOffset = -scaledHeight - 60 | ||
145 | |||
146 | const previewHalfSize = Math.round(scaledWidth / 2) | ||
147 | let left = seekBarRect.width * seekBarX - previewHalfSize | ||
148 | |||
149 | // Seek bar doesn't take all the player width, so we can add/minus a few more pixels | ||
150 | const minLeft = playerRect.left - seekBarRect.left | ||
151 | const maxLeft = seekBarRect.width - scaledWidth + (playerRect.right - seekBarRect.right) | ||
152 | |||
153 | if (left < minLeft) left = minLeft | ||
154 | if (left > maxLeft) left = maxLeft | ||
155 | |||
156 | const tooltipStyle: { [id: string]: string } = { | ||
157 | 'background-image': `url("${this.url}")`, | ||
158 | 'background-repeat': 'no-repeat', | ||
159 | 'background-position': `${cleft}px ${ctop}px`, | ||
160 | 'background-size': bgSize, | ||
161 | |||
162 | 'color': '#fff', | ||
163 | 'text-shadow': '1px 1px #000', | ||
164 | |||
165 | 'position': 'relative', | ||
166 | |||
167 | 'top': `${topOffset}px`, | ||
168 | |||
169 | 'border': '1px solid #000', | ||
170 | |||
171 | // border should not overlay thumbnail area | ||
172 | 'width': `${scaledWidth + 2}px`, | ||
173 | 'height': `${scaledHeight + 2}px` | ||
174 | } | ||
175 | |||
176 | tooltipStyle.left = `${left}px` | ||
177 | |||
178 | for (const [ key, value ] of Object.entries(tooltipStyle)) { | ||
179 | this.spritePlaceholder.style.setProperty(key, value) | ||
180 | } | ||
181 | }) | ||
182 | } | ||
183 | |||
184 | private resetMouseTooltip () { | ||
185 | if (this.spritePlaceholder) { | ||
186 | this.spritePlaceholder.style.cssText = '' | ||
187 | } | ||
188 | } | ||
189 | |||
190 | private isReady () { | ||
191 | return this.mouseTimeTooltip && this.width && this.height && this.url | ||
192 | } | ||
193 | } | ||
194 | |||
195 | videojs.registerPlugin('storyboard', StoryboardPlugin) | ||
196 | |||
197 | export { StoryboardPlugin } | ||
diff --git a/client/src/assets/player/shared/control-bar/theater-button.ts b/client/src/assets/player/shared/control-bar/theater-button.ts index 56c349d6b..a5feb56ee 100644 --- a/client/src/assets/player/shared/control-bar/theater-button.ts +++ b/client/src/assets/player/shared/control-bar/theater-button.ts | |||
@@ -1,14 +1,19 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { getStoredTheater, saveTheaterInStore } from '../../peertube-player-local-storage' | 2 | import { getStoredTheater, saveTheaterInStore } from '../../peertube-player-local-storage' |
3 | import { TheaterButtonOptions } from '../../types' | ||
3 | 4 | ||
4 | const Button = videojs.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
5 | class TheaterButton extends Button { | 6 | class TheaterButton extends Button { |
6 | 7 | ||
7 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' | 8 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' |
8 | 9 | ||
9 | constructor (player: videojs.Player, options: videojs.ComponentOptions) { | 10 | private theaterButtonOptions: TheaterButtonOptions |
11 | |||
12 | constructor (player: videojs.Player, options: TheaterButtonOptions & videojs.ComponentOptions) { | ||
10 | super(player, options) | 13 | super(player, options) |
11 | 14 | ||
15 | this.theaterButtonOptions = options | ||
16 | |||
12 | const enabled = getStoredTheater() | 17 | const enabled = getStoredTheater() |
13 | if (enabled === true) { | 18 | if (enabled === true) { |
14 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) | 19 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) |
@@ -19,6 +24,9 @@ class TheaterButton extends Button { | |||
19 | this.controlText('Theater mode') | 24 | this.controlText('Theater mode') |
20 | 25 | ||
21 | this.player().theaterEnabled = enabled | 26 | this.player().theaterEnabled = enabled |
27 | |||
28 | this.updateShowing() | ||
29 | this.player().on('video-change', () => this.updateShowing()) | ||
22 | } | 30 | } |
23 | 31 | ||
24 | buildCSSClass () { | 32 | buildCSSClass () { |
@@ -36,7 +44,7 @@ class TheaterButton extends Button { | |||
36 | 44 | ||
37 | saveTheaterInStore(theaterEnabled) | 45 | saveTheaterInStore(theaterEnabled) |
38 | 46 | ||
39 | this.player_.trigger('theaterChange', theaterEnabled) | 47 | this.player_.trigger('theater-change', theaterEnabled) |
40 | } | 48 | } |
41 | 49 | ||
42 | handleClick () { | 50 | handleClick () { |
@@ -48,6 +56,11 @@ class TheaterButton extends Button { | |||
48 | private isTheaterEnabled () { | 56 | private isTheaterEnabled () { |
49 | return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) | 57 | return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) |
50 | } | 58 | } |
59 | |||
60 | private updateShowing () { | ||
61 | if (this.theaterButtonOptions.isDisplayed()) this.show() | ||
62 | else this.hide() | ||
63 | } | ||
51 | } | 64 | } |
52 | 65 | ||
53 | videojs.registerComponent('TheaterButton', TheaterButton) | 66 | videojs.registerComponent('TheaterButton', TheaterButton) |
diff --git a/client/src/assets/player/shared/dock/peertube-dock-component.ts b/client/src/assets/player/shared/dock/peertube-dock-component.ts index 183c7a00f..c13ca647b 100644 --- a/client/src/assets/player/shared/dock/peertube-dock-component.ts +++ b/client/src/assets/player/shared/dock/peertube-dock-component.ts | |||
@@ -10,17 +10,20 @@ export type PeerTubeDockComponentOptions = { | |||
10 | 10 | ||
11 | class PeerTubeDockComponent extends Component { | 11 | class PeerTubeDockComponent extends Component { |
12 | 12 | ||
13 | createEl () { | 13 | options_: videojs.ComponentOptions & PeerTubeDockComponentOptions |
14 | const options = this.options_ as PeerTubeDockComponentOptions | ||
15 | 14 | ||
16 | const el = super.createEl('div', { | 15 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor |
17 | className: 'peertube-dock' | 16 | constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeDockComponentOptions) { |
18 | }) | 17 | super(player, options) |
18 | } | ||
19 | |||
20 | createEl () { | ||
21 | const el = super.createEl('div', { className: 'peertube-dock' }) | ||
19 | 22 | ||
20 | if (options.avatarUrl) { | 23 | if (this.options_.avatarUrl) { |
21 | const avatar = videojs.dom.createEl('img', { | 24 | const avatar = videojs.dom.createEl('img', { |
22 | className: 'peertube-dock-avatar', | 25 | className: 'peertube-dock-avatar', |
23 | src: options.avatarUrl | 26 | src: this.options_.avatarUrl |
24 | }) | 27 | }) |
25 | 28 | ||
26 | el.appendChild(avatar) | 29 | el.appendChild(avatar) |
@@ -30,27 +33,27 @@ class PeerTubeDockComponent extends Component { | |||
30 | className: 'peertube-dock-title-description' | 33 | className: 'peertube-dock-title-description' |
31 | }) | 34 | }) |
32 | 35 | ||
33 | if (options.title) { | 36 | if (this.options_.title) { |
34 | const title = videojs.dom.createEl('div', { | 37 | const title = videojs.dom.createEl('div', { |
35 | className: 'peertube-dock-title', | 38 | className: 'peertube-dock-title', |
36 | title: options.title, | 39 | title: this.options_.title, |
37 | innerHTML: options.title | 40 | innerHTML: this.options_.title |
38 | }) | 41 | }) |
39 | 42 | ||
40 | elWrapperTitleDescription.appendChild(title) | 43 | elWrapperTitleDescription.appendChild(title) |
41 | } | 44 | } |
42 | 45 | ||
43 | if (options.description) { | 46 | if (this.options_.description) { |
44 | const description = videojs.dom.createEl('div', { | 47 | const description = videojs.dom.createEl('div', { |
45 | className: 'peertube-dock-description', | 48 | className: 'peertube-dock-description', |
46 | title: options.description, | 49 | title: this.options_.description, |
47 | innerHTML: options.description | 50 | innerHTML: this.options_.description |
48 | }) | 51 | }) |
49 | 52 | ||
50 | elWrapperTitleDescription.appendChild(description) | 53 | elWrapperTitleDescription.appendChild(description) |
51 | } | 54 | } |
52 | 55 | ||
53 | if (options.title || options.description) { | 56 | if (this.options_.title || this.options_.description) { |
54 | el.appendChild(elWrapperTitleDescription) | 57 | el.appendChild(elWrapperTitleDescription) |
55 | } | 58 | } |
56 | 59 | ||
diff --git a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts index 245981692..fc71a8c4b 100644 --- a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts +++ b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts | |||
@@ -10,14 +10,25 @@ export type PeerTubeDockPluginOptions = { | |||
10 | } | 10 | } |
11 | 11 | ||
12 | class PeerTubeDockPlugin extends Plugin { | 12 | class PeerTubeDockPlugin extends Plugin { |
13 | private dockComponent: PeerTubeDockComponent | ||
14 | |||
13 | constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeDockPluginOptions) { | 15 | constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeDockPluginOptions) { |
14 | super(player, options) | 16 | super(player, options) |
15 | 17 | ||
16 | this.player.addClass('peertube-dock') | 18 | player.ready(() => { |
17 | 19 | player.addClass('peertube-dock') | |
18 | this.player.ready(() => { | ||
19 | this.player.addChild('PeerTubeDockComponent', options) as PeerTubeDockComponent | ||
20 | }) | 20 | }) |
21 | |||
22 | this.dockComponent = new PeerTubeDockComponent(player, options) | ||
23 | player.addChild(this.dockComponent) | ||
24 | } | ||
25 | |||
26 | dispose () { | ||
27 | this.dockComponent?.dispose() | ||
28 | this.player.removeChild(this.dockComponent) | ||
29 | this.player.removeClass('peertube-dock') | ||
30 | |||
31 | super.dispose() | ||
21 | } | 32 | } |
22 | } | 33 | } |
23 | 34 | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index 2742b21a1..e77b7dc6d 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -31,6 +31,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
31 | 31 | ||
32 | dispose () { | 32 | dispose () { |
33 | document.removeEventListener('keydown', this.handleKeyFunction) | 33 | document.removeEventListener('keydown', this.handleKeyFunction) |
34 | |||
35 | super.dispose() | ||
34 | } | 36 | } |
35 | 37 | ||
36 | private onKeyDown (event: KeyboardEvent) { | 38 | private onKeyDown (event: KeyboardEvent) { |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts deleted file mode 100644 index 26f923e92..000000000 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ /dev/null | |||
@@ -1,155 +0,0 @@ | |||
1 | import { | ||
2 | CommonOptions, | ||
3 | NextPreviousVideoButtonOptions, | ||
4 | PeerTubeLinkButtonOptions, | ||
5 | PeertubePlayerManagerOptions, | ||
6 | PlayerMode | ||
7 | } from '../../types' | ||
8 | |||
9 | export class ControlBarOptionsBuilder { | ||
10 | private options: CommonOptions | ||
11 | |||
12 | constructor ( | ||
13 | globalOptions: PeertubePlayerManagerOptions, | ||
14 | private mode: PlayerMode | ||
15 | ) { | ||
16 | this.options = globalOptions.common | ||
17 | } | ||
18 | |||
19 | getChildrenOptions () { | ||
20 | const children = {} | ||
21 | |||
22 | if (this.options.previousVideo) { | ||
23 | Object.assign(children, this.getPreviousVideo()) | ||
24 | } | ||
25 | |||
26 | Object.assign(children, { playToggle: {} }) | ||
27 | |||
28 | if (this.options.nextVideo) { | ||
29 | Object.assign(children, this.getNextVideo()) | ||
30 | } | ||
31 | |||
32 | Object.assign(children, { | ||
33 | ...this.getTimeControls(), | ||
34 | |||
35 | flexibleWidthSpacer: {}, | ||
36 | |||
37 | ...this.getProgressControl(), | ||
38 | |||
39 | p2PInfoButton: { | ||
40 | p2pEnabled: this.options.p2pEnabled | ||
41 | }, | ||
42 | |||
43 | muteToggle: {}, | ||
44 | volumeControl: {}, | ||
45 | |||
46 | ...this.getSettingsButton() | ||
47 | }) | ||
48 | |||
49 | if (this.options.peertubeLink === true) { | ||
50 | Object.assign(children, { | ||
51 | peerTubeLinkButton: { | ||
52 | shortUUID: this.options.videoShortUUID, | ||
53 | instanceName: this.options.instanceName | ||
54 | } as PeerTubeLinkButtonOptions | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | if (this.options.theaterButton === true) { | ||
59 | Object.assign(children, { | ||
60 | theaterButton: {} | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | Object.assign(children, { | ||
65 | fullscreenToggle: {} | ||
66 | }) | ||
67 | |||
68 | return children | ||
69 | } | ||
70 | |||
71 | private getSettingsButton () { | ||
72 | const settingEntries: string[] = [] | ||
73 | |||
74 | if (!this.options.isLive) { | ||
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
77 | |||
78 | if (this.options.captions === true) settingEntries.push('captionsButton') | ||
79 | |||
80 | settingEntries.push('resolutionMenuButton') | ||
81 | |||
82 | return { | ||
83 | settingsButton: { | ||
84 | setup: { | ||
85 | maxHeightOffset: 40 | ||
86 | }, | ||
87 | entries: settingEntries | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private getProgressControl () { | ||
107 | if (this.options.isLive) return {} | ||
108 | |||
109 | const loadProgressBar = this.mode === 'webtorrent' | ||
110 | ? 'peerTubeLoadProgressBar' | ||
111 | : 'loadProgressBar' | ||
112 | |||
113 | return { | ||
114 | progressControl: { | ||
115 | children: { | ||
116 | seekBar: { | ||
117 | children: { | ||
118 | [loadProgressBar]: {}, | ||
119 | mouseTimeDisplay: {}, | ||
120 | playProgressBar: {} | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | |||
128 | private getPreviousVideo () { | ||
129 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
130 | type: 'previous', | ||
131 | handler: this.options.previousVideo, | ||
132 | isDisabled: () => { | ||
133 | if (!this.options.hasPreviousVideo) return false | ||
134 | |||
135 | return !this.options.hasPreviousVideo() | ||
136 | } | ||
137 | } | ||
138 | |||
139 | return { previousVideoButton: buttonOptions } | ||
140 | } | ||
141 | |||
142 | private getNextVideo () { | ||
143 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
144 | type: 'next', | ||
145 | handler: this.options.nextVideo, | ||
146 | isDisabled: () => { | ||
147 | if (!this.options.hasNextVideo) return false | ||
148 | |||
149 | return !this.options.hasNextVideo() | ||
150 | } | ||
151 | } | ||
152 | |||
153 | return { nextVideoButton: buttonOptions } | ||
154 | } | ||
155 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/index.ts b/client/src/assets/player/shared/manager-options/index.ts deleted file mode 100644 index 4934d8302..000000000 --- a/client/src/assets/player/shared/manager-options/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './manager-options-builder' | ||
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts deleted file mode 100644 index 5d3ee4c4a..000000000 --- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts +++ /dev/null | |||
@@ -1,186 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { copyToClipboard } from '@root-helpers/utils' | ||
3 | import { buildVideoOrPlaylistEmbed } from '@root-helpers/video' | ||
4 | import { isIOS, isSafari } from '@root-helpers/web-browser' | ||
5 | import { buildVideoLink, decorateVideoLink, pick } from '@shared/core-utils' | ||
6 | import { isDefaultLocale } from '@shared/core-utils/i18n' | ||
7 | import { VideoJSPluginOptions } from '../../types' | ||
8 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../types/manager-options' | ||
9 | import { ControlBarOptionsBuilder } from './control-bar-options-builder' | ||
10 | import { HLSOptionsBuilder } from './hls-options-builder' | ||
11 | import { WebTorrentOptionsBuilder } from './webtorrent-options-builder' | ||
12 | |||
13 | export class ManagerOptionsBuilder { | ||
14 | |||
15 | constructor ( | ||
16 | private mode: PlayerMode, | ||
17 | private options: PeertubePlayerManagerOptions, | ||
18 | private p2pMediaLoaderModule?: any | ||
19 | ) { | ||
20 | |||
21 | } | ||
22 | |||
23 | async getVideojsOptions (alreadyPlayed: boolean): Promise<videojs.PlayerOptions> { | ||
24 | const commonOptions = this.options.common | ||
25 | |||
26 | let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) | ||
27 | const html5 = { | ||
28 | preloadTextTracks: false | ||
29 | } | ||
30 | |||
31 | const plugins: VideoJSPluginOptions = { | ||
32 | peertube: { | ||
33 | mode: this.mode, | ||
34 | autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent | ||
35 | |||
36 | ...pick(commonOptions, [ | ||
37 | 'videoViewUrl', | ||
38 | 'videoViewIntervalMs', | ||
39 | 'authorizationHeader', | ||
40 | 'startTime', | ||
41 | 'videoDuration', | ||
42 | 'subtitle', | ||
43 | 'videoCaptions', | ||
44 | 'stopTime', | ||
45 | 'isLive', | ||
46 | 'videoUUID' | ||
47 | ]) | ||
48 | }, | ||
49 | metrics: { | ||
50 | mode: this.mode, | ||
51 | |||
52 | ...pick(commonOptions, [ | ||
53 | 'metricsUrl', | ||
54 | 'videoUUID' | ||
55 | ]) | ||
56 | } | ||
57 | } | ||
58 | |||
59 | if (commonOptions.playlist) { | ||
60 | plugins.playlist = commonOptions.playlist | ||
61 | } | ||
62 | |||
63 | if (this.mode === 'p2p-media-loader') { | ||
64 | const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule) | ||
65 | const options = await hlsOptionsBuilder.getPluginOptions() | ||
66 | |||
67 | Object.assign(plugins, pick(options, [ 'hlsjs', 'p2pMediaLoader' ])) | ||
68 | Object.assign(html5, options.html5) | ||
69 | } else if (this.mode === 'webtorrent') { | ||
70 | const webtorrentOptionsBuilder = new WebTorrentOptionsBuilder(this.options, this.getAutoPlayValue(autoplay, alreadyPlayed)) | ||
71 | |||
72 | Object.assign(plugins, webtorrentOptionsBuilder.getPluginOptions()) | ||
73 | |||
74 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
75 | autoplay = false | ||
76 | } | ||
77 | |||
78 | const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode) | ||
79 | |||
80 | const videojsOptions = { | ||
81 | html5, | ||
82 | |||
83 | // We don't use text track settings for now | ||
84 | textTrackSettings: false as any, // FIXME: typings | ||
85 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
86 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
87 | |||
88 | muted: commonOptions.muted !== undefined | ||
89 | ? commonOptions.muted | ||
90 | : undefined, // Undefined so the player knows it has to check the local storage | ||
91 | |||
92 | autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed), | ||
93 | |||
94 | poster: commonOptions.poster, | ||
95 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
96 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], | ||
97 | |||
98 | plugins, | ||
99 | |||
100 | controlBar: { | ||
101 | children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings | ||
102 | } | ||
103 | } | ||
104 | |||
105 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
106 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
107 | } | ||
108 | |||
109 | return videojsOptions | ||
110 | } | ||
111 | |||
112 | private getAutoPlayValue (autoplay: videojs.Autoplay, alreadyPlayed: boolean) { | ||
113 | if (autoplay !== true) return autoplay | ||
114 | |||
115 | // On first play, disable autoplay to avoid issues | ||
116 | // But if the player already played videos, we can safely autoplay next ones | ||
117 | if (isIOS() || isSafari()) { | ||
118 | return alreadyPlayed ? 'play' : false | ||
119 | } | ||
120 | |||
121 | return this.options.common.forceAutoplay | ||
122 | ? 'any' | ||
123 | : 'play' | ||
124 | } | ||
125 | |||
126 | getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) { | ||
127 | const content = () => { | ||
128 | const isLoopEnabled = player.options_['loop'] | ||
129 | |||
130 | const items = [ | ||
131 | { | ||
132 | icon: 'repeat', | ||
133 | label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''), | ||
134 | listener: function () { | ||
135 | player.options_['loop'] = !isLoopEnabled | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | label: player.localize('Copy the video URL'), | ||
140 | listener: function () { | ||
141 | copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID })) | ||
142 | } | ||
143 | }, | ||
144 | { | ||
145 | label: player.localize('Copy the video URL at the current time'), | ||
146 | listener: function (this: videojs.Player) { | ||
147 | const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID }) | ||
148 | |||
149 | copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() })) | ||
150 | } | ||
151 | }, | ||
152 | { | ||
153 | icon: 'code', | ||
154 | label: player.localize('Copy embed code'), | ||
155 | listener: () => { | ||
156 | copyToClipboard(buildVideoOrPlaylistEmbed({ embedUrl: commonOptions.embedUrl, embedTitle: commonOptions.embedTitle })) | ||
157 | } | ||
158 | } | ||
159 | ] | ||
160 | |||
161 | if (this.mode === 'webtorrent') { | ||
162 | items.push({ | ||
163 | label: player.localize('Copy magnet URI'), | ||
164 | listener: function (this: videojs.Player) { | ||
165 | copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri) | ||
166 | } | ||
167 | }) | ||
168 | } | ||
169 | |||
170 | items.push({ | ||
171 | icon: 'info', | ||
172 | label: player.localize('Stats for nerds'), | ||
173 | listener: () => { | ||
174 | player.stats().show() | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | return items.map(i => ({ | ||
179 | ...i, | ||
180 | label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label | ||
181 | })) | ||
182 | } | ||
183 | |||
184 | return { content } | ||
185 | } | ||
186 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts deleted file mode 100644 index b5bdcd4e6..000000000 --- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts +++ /dev/null | |||
@@ -1,47 +0,0 @@ | |||
1 | import { addQueryParams } from '../../../../../../shared/core-utils' | ||
2 | import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types' | ||
3 | |||
4 | export class WebTorrentOptionsBuilder { | ||
5 | |||
6 | constructor ( | ||
7 | private options: PeertubePlayerManagerOptions, | ||
8 | private autoPlayValue: any | ||
9 | ) { | ||
10 | |||
11 | } | ||
12 | |||
13 | getPluginOptions () { | ||
14 | const commonOptions = this.options.common | ||
15 | const webtorrentOptions = this.options.webtorrent | ||
16 | const p2pMediaLoaderOptions = this.options.p2pMediaLoader | ||
17 | |||
18 | const autoplay = this.autoPlayValue === 'play' | ||
19 | |||
20 | const webtorrent: WebtorrentPluginOptions = { | ||
21 | autoplay, | ||
22 | |||
23 | playerRefusedP2P: commonOptions.p2pEnabled === false, | ||
24 | videoDuration: commonOptions.videoDuration, | ||
25 | playerElement: commonOptions.playerElement, | ||
26 | |||
27 | videoFileToken: commonOptions.videoFileToken, | ||
28 | |||
29 | requiresAuth: commonOptions.requiresAuth, | ||
30 | |||
31 | buildWebSeedUrls: file => { | ||
32 | if (!commonOptions.requiresAuth) return [] | ||
33 | |||
34 | return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ] | ||
35 | }, | ||
36 | |||
37 | videoFiles: webtorrentOptions.videoFiles.length !== 0 | ||
38 | ? webtorrentOptions.videoFiles | ||
39 | // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode | ||
40 | : p2pMediaLoaderOptions?.videoFiles || [], | ||
41 | |||
42 | startTime: commonOptions.startTime | ||
43 | } | ||
44 | |||
45 | return { webtorrent } | ||
46 | } | ||
47 | } | ||
diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts index 2aae3e90a..48363a724 100644 --- a/client/src/assets/player/shared/metrics/metrics-plugin.ts +++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts | |||
@@ -1,14 +1,15 @@ | |||
1 | import debug from 'debug' | ||
1 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
2 | import { PlaybackMetricCreate } from '../../../../../../shared/models' | ||
3 | import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types' | ||
4 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { PlaybackMetricCreate } from '../../../../../../shared/models' | ||
5 | import { MetricsPluginOptions, PlayerNetworkInfo } from '../../types' | ||
6 | |||
7 | const debugLogger = debug('peertube:player:metrics') | ||
5 | 8 | ||
6 | const Plugin = videojs.getPlugin('plugin') | 9 | const Plugin = videojs.getPlugin('plugin') |
7 | 10 | ||
8 | class MetricsPlugin extends Plugin { | 11 | class MetricsPlugin extends Plugin { |
9 | private readonly metricsUrl: string | 12 | options_: MetricsPluginOptions |
10 | private readonly videoUUID: string | ||
11 | private readonly mode: PlayerMode | ||
12 | 13 | ||
13 | private downloadedBytesP2P = 0 | 14 | private downloadedBytesP2P = 0 |
14 | private downloadedBytesHTTP = 0 | 15 | private downloadedBytesHTTP = 0 |
@@ -28,29 +29,54 @@ class MetricsPlugin extends Plugin { | |||
28 | constructor (player: videojs.Player, options: MetricsPluginOptions) { | 29 | constructor (player: videojs.Player, options: MetricsPluginOptions) { |
29 | super(player) | 30 | super(player) |
30 | 31 | ||
31 | this.metricsUrl = options.metricsUrl | 32 | this.options_ = options |
32 | this.videoUUID = options.videoUUID | ||
33 | this.mode = options.mode | ||
34 | 33 | ||
35 | this.player.one('play', () => { | 34 | this.trackBytes() |
36 | this.runMetricsInterval() | 35 | this.trackResolutionChange() |
36 | this.trackErrors() | ||
37 | 37 | ||
38 | this.trackBytes() | 38 | this.one('play', () => { |
39 | this.trackResolutionChange() | 39 | this.player.on('video-change', () => { |
40 | this.trackErrors() | 40 | this.runMetricsIntervalOnPlay() |
41 | }) | ||
41 | }) | 42 | }) |
43 | |||
44 | this.runMetricsIntervalOnPlay() | ||
42 | } | 45 | } |
43 | 46 | ||
44 | dispose () { | 47 | dispose () { |
45 | if (this.metricsInterval) clearInterval(this.metricsInterval) | 48 | if (this.metricsInterval) clearInterval(this.metricsInterval) |
49 | |||
50 | super.dispose() | ||
51 | } | ||
52 | |||
53 | private runMetricsIntervalOnPlay () { | ||
54 | this.downloadedBytesP2P = 0 | ||
55 | this.downloadedBytesHTTP = 0 | ||
56 | this.uploadedBytesP2P = 0 | ||
57 | |||
58 | this.resolutionChanges = 0 | ||
59 | this.errors = 0 | ||
60 | |||
61 | this.lastPlayerNetworkInfo = undefined | ||
62 | |||
63 | debugLogger('Will track metrics on next play') | ||
64 | |||
65 | this.player.one('play', () => { | ||
66 | debugLogger('Tracking metrics') | ||
67 | |||
68 | this.runMetricsInterval() | ||
69 | }) | ||
46 | } | 70 | } |
47 | 71 | ||
48 | private runMetricsInterval () { | 72 | private runMetricsInterval () { |
73 | if (this.metricsInterval) clearInterval(this.metricsInterval) | ||
74 | |||
49 | this.metricsInterval = setInterval(() => { | 75 | this.metricsInterval = setInterval(() => { |
50 | let resolution: number | 76 | let resolution: number |
51 | let fps: number | 77 | let fps: number |
52 | 78 | ||
53 | if (this.mode === 'p2p-media-loader') { | 79 | if (this.player.usingPlugin('p2pMediaLoader')) { |
54 | const level = this.player.p2pMediaLoader().getCurrentLevel() | 80 | const level = this.player.p2pMediaLoader().getCurrentLevel() |
55 | if (!level) return | 81 | if (!level) return |
56 | 82 | ||
@@ -60,21 +86,23 @@ class MetricsPlugin extends Plugin { | |||
60 | fps = framerate | 86 | fps = framerate |
61 | ? parseInt(framerate, 10) | 87 | ? parseInt(framerate, 10) |
62 | : undefined | 88 | : undefined |
63 | } else { // webtorrent | 89 | } else if (this.player.usingPlugin('webVideo')) { |
64 | const videoFile = this.player.webtorrent().getCurrentVideoFile() | 90 | const videoFile = this.player.webVideo().getCurrentVideoFile() |
65 | if (!videoFile) return | 91 | if (!videoFile) return |
66 | 92 | ||
67 | resolution = videoFile.resolution.id | 93 | resolution = videoFile.resolution.id |
68 | fps = videoFile.fps && videoFile.fps !== -1 | 94 | fps = videoFile.fps && videoFile.fps !== -1 |
69 | ? videoFile.fps | 95 | ? videoFile.fps |
70 | : undefined | 96 | : undefined |
97 | } else { | ||
98 | return | ||
71 | } | 99 | } |
72 | 100 | ||
73 | const body: PlaybackMetricCreate = { | 101 | const body: PlaybackMetricCreate = { |
74 | resolution, | 102 | resolution, |
75 | fps, | 103 | fps, |
76 | 104 | ||
77 | playerMode: this.mode, | 105 | playerMode: this.options_.mode(), |
78 | 106 | ||
79 | resolutionChanges: this.resolutionChanges, | 107 | resolutionChanges: this.resolutionChanges, |
80 | 108 | ||
@@ -85,7 +113,7 @@ class MetricsPlugin extends Plugin { | |||
85 | 113 | ||
86 | uploadedBytesP2P: this.uploadedBytesP2P, | 114 | uploadedBytesP2P: this.uploadedBytesP2P, |
87 | 115 | ||
88 | videoId: this.videoUUID | 116 | videoId: this.options_.videoUUID() |
89 | } | 117 | } |
90 | 118 | ||
91 | this.resolutionChanges = 0 | 119 | this.resolutionChanges = 0 |
@@ -99,15 +127,13 @@ class MetricsPlugin extends Plugin { | |||
99 | 127 | ||
100 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) | 128 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) |
101 | 129 | ||
102 | return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers }) | 130 | return fetch(this.options_.metricsUrl(), { method: 'POST', body: JSON.stringify(body), headers }) |
103 | .catch(err => logger.error('Cannot send metrics to the server.', err)) | 131 | .catch(err => logger.error('Cannot send metrics to the server.', err)) |
104 | }, this.CONSTANTS.METRICS_INTERVAL) | 132 | }, this.CONSTANTS.METRICS_INTERVAL) |
105 | } | 133 | } |
106 | 134 | ||
107 | private trackBytes () { | 135 | private trackBytes () { |
108 | this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => { | 136 | this.player.on('p2p-info', (_event, data: PlayerNetworkInfo) => { |
109 | if (!data) return | ||
110 | |||
111 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) | 137 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) |
112 | this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) | 138 | this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) |
113 | 139 | ||
@@ -115,10 +141,18 @@ class MetricsPlugin extends Plugin { | |||
115 | 141 | ||
116 | this.lastPlayerNetworkInfo = data | 142 | this.lastPlayerNetworkInfo = data |
117 | }) | 143 | }) |
144 | |||
145 | this.player.on('http-info', (_event, data: PlayerNetworkInfo) => { | ||
146 | this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) | ||
147 | }) | ||
118 | } | 148 | } |
119 | 149 | ||
120 | private trackResolutionChange () { | 150 | private trackResolutionChange () { |
121 | this.player.on('engineResolutionChange', () => { | 151 | this.player.on('engine-resolution-change', () => { |
152 | this.resolutionChanges++ | ||
153 | }) | ||
154 | |||
155 | this.player.on('user-resolution-change', () => { | ||
122 | this.resolutionChanges++ | 156 | this.resolutionChanges++ |
123 | }) | 157 | }) |
124 | } | 158 | } |
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts index 09cb98f2e..1bc3ca38d 100644 --- a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts +++ b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts | |||
@@ -2,22 +2,20 @@ import videojs from 'video.js' | |||
2 | 2 | ||
3 | const Component = videojs.getComponent('Component') | 3 | const Component = videojs.getComponent('Component') |
4 | class PeerTubeMobileButtons extends Component { | 4 | class PeerTubeMobileButtons extends Component { |
5 | private mainButton: HTMLDivElement | ||
5 | 6 | ||
6 | private rewind: Element | 7 | private rewind: Element |
7 | private forward: Element | 8 | private forward: Element |
8 | private rewindText: Element | 9 | private rewindText: Element |
9 | private forwardText: Element | 10 | private forwardText: Element |
10 | 11 | ||
11 | createEl () { | 12 | private touchStartHandler: (e: TouchEvent) => void |
12 | const container = super.createEl('div', { | ||
13 | className: 'vjs-mobile-buttons-overlay' | ||
14 | }) as HTMLDivElement | ||
15 | 13 | ||
16 | const mainButton = super.createEl('div', { | 14 | createEl () { |
17 | className: 'main-button' | 15 | const container = super.createEl('div', { className: 'vjs-mobile-buttons-overlay' }) as HTMLDivElement |
18 | }) as HTMLDivElement | 16 | this.mainButton = super.createEl('div', { className: 'main-button' }) as HTMLDivElement |
19 | 17 | ||
20 | mainButton.addEventListener('touchstart', e => { | 18 | this.touchStartHandler = e => { |
21 | e.stopPropagation() | 19 | e.stopPropagation() |
22 | 20 | ||
23 | if (this.player_.paused() || this.player_.ended()) { | 21 | if (this.player_.paused() || this.player_.ended()) { |
@@ -26,7 +24,9 @@ class PeerTubeMobileButtons extends Component { | |||
26 | } | 24 | } |
27 | 25 | ||
28 | this.player_.pause() | 26 | this.player_.pause() |
29 | }) | 27 | } |
28 | |||
29 | this.mainButton.addEventListener('touchstart', this.touchStartHandler, { passive: true }) | ||
30 | 30 | ||
31 | this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) | 31 | this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) |
32 | this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) | 32 | this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) |
@@ -40,12 +40,18 @@ class PeerTubeMobileButtons extends Component { | |||
40 | this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) | 40 | this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) |
41 | 41 | ||
42 | container.appendChild(this.rewind) | 42 | container.appendChild(this.rewind) |
43 | container.appendChild(mainButton) | 43 | container.appendChild(this.mainButton) |
44 | container.appendChild(this.forward) | 44 | container.appendChild(this.forward) |
45 | 45 | ||
46 | return container | 46 | return container |
47 | } | 47 | } |
48 | 48 | ||
49 | dispose () { | ||
50 | if (this.touchStartHandler) this.mainButton.removeEventListener('touchstart', this.touchStartHandler) | ||
51 | |||
52 | super.dispose() | ||
53 | } | ||
54 | |||
49 | displayFastSeek (amount: number) { | 55 | displayFastSeek (amount: number) { |
50 | if (amount === 0) { | 56 | if (amount === 0) { |
51 | this.hideRewind() | 57 | this.hideRewind() |
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts index 646e9f8c6..f31fa7ddb 100644 --- a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts +++ b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts | |||
@@ -21,6 +21,15 @@ class PeerTubeMobilePlugin extends Plugin { | |||
21 | 21 | ||
22 | private setCurrentTimeTimeout: ReturnType<typeof setTimeout> | 22 | private setCurrentTimeTimeout: ReturnType<typeof setTimeout> |
23 | 23 | ||
24 | private onPlayHandler: () => void | ||
25 | private onFullScreenChangeHandler: () => void | ||
26 | private onTouchStartHandler: (event: TouchEvent) => void | ||
27 | private onMobileButtonTouchStartHandler: (event: TouchEvent) => void | ||
28 | private sliderActiveHandler: () => void | ||
29 | private sliderInactiveHandler: () => void | ||
30 | |||
31 | private seekBar: videojs.Component | ||
32 | |||
24 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 33 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { |
25 | super(player, options) | 34 | super(player, options) |
26 | 35 | ||
@@ -36,18 +45,38 @@ class PeerTubeMobilePlugin extends Plugin { | |||
36 | (this.player.options_.userActions as any).click = false | 45 | (this.player.options_.userActions as any).click = false |
37 | this.player.options_.userActions.doubleClick = false | 46 | this.player.options_.userActions.doubleClick = false |
38 | 47 | ||
39 | this.player.one('play', () => { | 48 | this.onPlayHandler = () => this.initTouchStartEvents() |
40 | this.initTouchStartEvents() | 49 | this.player.one('play', this.onPlayHandler) |
41 | }) | 50 | |
51 | this.seekBar = this.player.getDescendant([ 'controlBar', 'progressControl', 'seekBar' ]) | ||
52 | |||
53 | this.sliderActiveHandler = () => this.player.addClass('vjs-mobile-sliding') | ||
54 | this.sliderInactiveHandler = () => this.player.removeClass('vjs-mobile-sliding') | ||
55 | |||
56 | this.seekBar.on('slideractive', this.sliderActiveHandler) | ||
57 | this.seekBar.on('sliderinactive', this.sliderInactiveHandler) | ||
58 | } | ||
59 | |||
60 | dispose () { | ||
61 | if (this.onPlayHandler) this.player.off('play', this.onPlayHandler) | ||
62 | if (this.onFullScreenChangeHandler) this.player.off('fullscreenchange', this.onFullScreenChangeHandler) | ||
63 | if (this.onTouchStartHandler) this.player.off('touchstart', this.onFullScreenChangeHandler) | ||
64 | if (this.onMobileButtonTouchStartHandler) { | ||
65 | this.peerTubeMobileButtons?.el().removeEventListener('touchstart', this.onMobileButtonTouchStartHandler) | ||
66 | } | ||
67 | |||
68 | super.dispose() | ||
42 | } | 69 | } |
43 | 70 | ||
44 | private handleFullscreenRotation () { | 71 | private handleFullscreenRotation () { |
45 | this.player.on('fullscreenchange', () => { | 72 | this.onFullScreenChangeHandler = () => { |
46 | if (!this.player.isFullscreen() || this.isPortraitVideo()) return | 73 | if (!this.player.isFullscreen() || this.isPortraitVideo()) return |
47 | 74 | ||
48 | screen.orientation.lock('landscape') | 75 | screen.orientation.lock('landscape') |
49 | .catch(err => logger.error('Cannot lock screen to landscape.', err)) | 76 | .catch(err => logger.error('Cannot lock screen to landscape.', err)) |
50 | }) | 77 | } |
78 | |||
79 | this.player.on('fullscreenchange', this.onFullScreenChangeHandler) | ||
51 | } | 80 | } |
52 | 81 | ||
53 | private isPortraitVideo () { | 82 | private isPortraitVideo () { |
@@ -80,19 +109,22 @@ class PeerTubeMobilePlugin extends Plugin { | |||
80 | this.lastTapEvent = event | 109 | this.lastTapEvent = event |
81 | } | 110 | } |
82 | 111 | ||
83 | this.player.on('touchstart', (event: TouchEvent) => { | 112 | this.onTouchStartHandler = event => { |
84 | // Only enable user active on player touch, we listen event on peertube mobile buttons to disable it | 113 | // Only enable user active on player touch, we listen event on peertube mobile buttons to disable it |
85 | if (this.player.userActive()) return | 114 | if (this.player.userActive()) return |
86 | 115 | ||
87 | handleTouchStart(event) | 116 | handleTouchStart(event) |
88 | }) | 117 | } |
118 | this.player.on('touchstart', this.onTouchStartHandler) | ||
89 | 119 | ||
90 | this.peerTubeMobileButtons.el().addEventListener('touchstart', (event: TouchEvent) => { | 120 | this.onMobileButtonTouchStartHandler = event => { |
91 | // Prevent mousemove/click events firing on the player, that conflict with our user active logic | 121 | // Prevent mousemove/click events firing on the player, that conflict with our user active logic |
92 | event.preventDefault() | 122 | event.preventDefault() |
93 | 123 | ||
94 | handleTouchStart(event) | 124 | handleTouchStart(event) |
95 | }, { passive: false }) | 125 | } |
126 | |||
127 | this.peerTubeMobileButtons.el().addEventListener('touchstart', this.onMobileButtonTouchStartHandler, { passive: false }) | ||
96 | } | 128 | } |
97 | 129 | ||
98 | private onDoubleTap (event: TouchEvent) { | 130 | private onDoubleTap (event: TouchEvent) { |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index d05d6193c..d83ec625a 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -14,6 +14,10 @@ type Metadata = { | |||
14 | levels: Level[] | 14 | levels: Level[] |
15 | } | 15 | } |
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | ||
18 | // Source handler registration | ||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
17 | type HookFn = (player: videojs.Player, hljs: Hlsjs) => void | 21 | type HookFn = (player: videojs.Player, hljs: Hlsjs) => void |
18 | 22 | ||
19 | const registerSourceHandler = function (vjs: typeof videojs) { | 23 | const registerSourceHandler = function (vjs: typeof videojs) { |
@@ -25,10 +29,13 @@ const registerSourceHandler = function (vjs: typeof videojs) { | |||
25 | const html5 = vjs.getTech('Html5') | 29 | const html5 = vjs.getTech('Html5') |
26 | 30 | ||
27 | if (!html5) { | 31 | if (!html5) { |
28 | logger.error('No Hml5 tech found in videojs') | 32 | logger.error('No "Html5" tech found in videojs') |
29 | return | 33 | return |
30 | } | 34 | } |
31 | 35 | ||
36 | // Already registered | ||
37 | if ((html5 as any).canPlaySource({ type: 'application/x-mpegURL' })) return | ||
38 | |||
32 | // FIXME: typings | 39 | // FIXME: typings |
33 | (html5 as any).registerSourceHandler({ | 40 | (html5 as any).registerSourceHandler({ |
34 | canHandleSource: function (source: videojs.Tech.SourceObject) { | 41 | canHandleSource: function (source: videojs.Tech.SourceObject) { |
@@ -56,32 +63,55 @@ const registerSourceHandler = function (vjs: typeof videojs) { | |||
56 | (vjs as any).Html5Hlsjs = Html5Hlsjs | 63 | (vjs as any).Html5Hlsjs = Html5Hlsjs |
57 | } | 64 | } |
58 | 65 | ||
59 | function hlsjsConfigHandler (this: videojs.Player, options: HlsjsConfigHandlerOptions) { | 66 | // --------------------------------------------------------------------------- |
60 | const player = this | 67 | // HLS options plugin |
68 | // --------------------------------------------------------------------------- | ||
61 | 69 | ||
62 | if (!options) return | 70 | const Plugin = videojs.getPlugin('plugin') |
63 | 71 | ||
64 | if (!player.srOptions_) { | 72 | class HLSJSConfigHandler extends Plugin { |
65 | player.srOptions_ = {} | 73 | |
66 | } | 74 | constructor (player: videojs.Player, options: HlsjsConfigHandlerOptions) { |
75 | super(player, options) | ||
76 | |||
77 | if (!options) return | ||
78 | |||
79 | if (!player.srOptions_) { | ||
80 | player.srOptions_ = {} | ||
81 | } | ||
82 | |||
83 | if (!player.srOptions_.hlsjsConfig) { | ||
84 | player.srOptions_.hlsjsConfig = options.hlsjsConfig | ||
85 | } | ||
67 | 86 | ||
68 | if (!player.srOptions_.hlsjsConfig) { | 87 | if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) { |
69 | player.srOptions_.hlsjsConfig = options.hlsjsConfig | 88 | player.srOptions_.levelLabelHandler = options.levelLabelHandler |
89 | } | ||
90 | |||
91 | registerSourceHandler(videojs) | ||
70 | } | 92 | } |
71 | 93 | ||
72 | if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) { | 94 | dispose () { |
73 | player.srOptions_.levelLabelHandler = options.levelLabelHandler | 95 | this.player.srOptions_ = undefined |
96 | |||
97 | const tech = this.player.tech(true) as any | ||
98 | if (tech.hlsProvider) { | ||
99 | tech.hlsProvider.dispose() | ||
100 | tech.hlsProvider = undefined | ||
101 | } | ||
102 | |||
103 | super.dispose() | ||
74 | } | 104 | } |
75 | } | 105 | } |
76 | 106 | ||
77 | const registerConfigPlugin = function (vjs: typeof videojs) { | 107 | videojs.registerPlugin('hlsjs', HLSJSConfigHandler) |
78 | // Used in Brightcove since we don't pass options directly there | 108 | |
79 | const registerVjsPlugin = vjs.registerPlugin || vjs.plugin | 109 | // --------------------------------------------------------------------------- |
80 | registerVjsPlugin('hlsjs', hlsjsConfigHandler) | 110 | // HLS JS source handler |
81 | } | 111 | // --------------------------------------------------------------------------- |
82 | 112 | ||
83 | class Html5Hlsjs { | 113 | export class Html5Hlsjs { |
84 | private static readonly hooks: { [id: string]: HookFn[] } = {} | 114 | private static hooks: { [id: string]: HookFn[] } = {} |
85 | 115 | ||
86 | private readonly videoElement: HTMLVideoElement | 116 | private readonly videoElement: HTMLVideoElement |
87 | private readonly errorCounts: ErrorCounts = {} | 117 | private readonly errorCounts: ErrorCounts = {} |
@@ -101,8 +131,9 @@ class Html5Hlsjs { | |||
101 | private dvrDuration: number = null | 131 | private dvrDuration: number = null |
102 | private edgeMargin: number = null | 132 | private edgeMargin: number = null |
103 | 133 | ||
104 | private handlers: { [ id in 'play' ]: EventListener } = { | 134 | private handlers: { [ id in 'play' | 'error' ]: EventListener } = { |
105 | play: null | 135 | play: null, |
136 | error: null | ||
106 | } | 137 | } |
107 | 138 | ||
108 | constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { | 139 | constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { |
@@ -115,7 +146,7 @@ class Html5Hlsjs { | |||
115 | this.videoElement = tech.el() as HTMLVideoElement | 146 | this.videoElement = tech.el() as HTMLVideoElement |
116 | this.player = vjs((tech.options_ as any).playerId) | 147 | this.player = vjs((tech.options_ as any).playerId) |
117 | 148 | ||
118 | this.videoElement.addEventListener('error', event => { | 149 | this.handlers.error = event => { |
119 | let errorTxt: string | 150 | let errorTxt: string |
120 | const mediaError = ((event.currentTarget || event.target) as HTMLVideoElement).error | 151 | const mediaError = ((event.currentTarget || event.target) as HTMLVideoElement).error |
121 | 152 | ||
@@ -143,7 +174,8 @@ class Html5Hlsjs { | |||
143 | } | 174 | } |
144 | 175 | ||
145 | logger.error(`MEDIA_ERROR: ${errorTxt}`) | 176 | logger.error(`MEDIA_ERROR: ${errorTxt}`) |
146 | }) | 177 | } |
178 | this.videoElement.addEventListener('error', this.handlers.error) | ||
147 | 179 | ||
148 | this.initialize() | 180 | this.initialize() |
149 | } | 181 | } |
@@ -174,6 +206,7 @@ class Html5Hlsjs { | |||
174 | // See comment for `initialize` method. | 206 | // See comment for `initialize` method. |
175 | dispose () { | 207 | dispose () { |
176 | this.videoElement.removeEventListener('play', this.handlers.play) | 208 | this.videoElement.removeEventListener('play', this.handlers.play) |
209 | this.videoElement.removeEventListener('error', this.handlers.error) | ||
177 | 210 | ||
178 | // FIXME: https://github.com/video-dev/hls.js/issues/4092 | 211 | // FIXME: https://github.com/video-dev/hls.js/issues/4092 |
179 | const untypedHLS = this.hls as any | 212 | const untypedHLS = this.hls as any |
@@ -200,6 +233,10 @@ class Html5Hlsjs { | |||
200 | return true | 233 | return true |
201 | } | 234 | } |
202 | 235 | ||
236 | static removeAllHooks () { | ||
237 | Html5Hlsjs.hooks = {} | ||
238 | } | ||
239 | |||
203 | private _executeHooksFor (type: string) { | 240 | private _executeHooksFor (type: string) { |
204 | if (Html5Hlsjs.hooks[type] === undefined) { | 241 | if (Html5Hlsjs.hooks[type] === undefined) { |
205 | return | 242 | return |
@@ -421,7 +458,7 @@ class Html5Hlsjs { | |||
421 | ? data.level | 458 | ? data.level |
422 | : -1 | 459 | : -1 |
423 | 460 | ||
424 | this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, byEngine: true }) | 461 | this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, fireCallback: false }) |
425 | }) | 462 | }) |
426 | 463 | ||
427 | this.hls.attachMedia(this.videoElement) | 464 | this.hls.attachMedia(this.videoElement) |
@@ -433,9 +470,3 @@ class Html5Hlsjs { | |||
433 | this._initHlsjs() | 470 | this._initHlsjs() |
434 | } | 471 | } |
435 | } | 472 | } |
436 | |||
437 | export { | ||
438 | Html5Hlsjs, | ||
439 | registerSourceHandler, | ||
440 | registerConfigPlugin | ||
441 | } | ||
diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts index e6f525fea..fe967a730 100644 --- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -3,19 +3,12 @@ import videojs from 'video.js' | |||
3 | import { Events, Segment } from '@peertube/p2p-media-loader-core' | 3 | import { Events, Segment } from '@peertube/p2p-media-loader-core' |
4 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' | 4 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' |
5 | import { logger } from '@root-helpers/logger' | 5 | import { logger } from '@root-helpers/logger' |
6 | import { addQueryParams, timeToInt } from '@shared/core-utils' | 6 | import { addQueryParams } from '@shared/core-utils' |
7 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' | 7 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' |
8 | import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' | 8 | import { SettingsButton } from '../settings/settings-menu-button' |
9 | |||
10 | registerConfigPlugin(videojs) | ||
11 | registerSourceHandler(videojs) | ||
12 | 9 | ||
13 | const Plugin = videojs.getPlugin('plugin') | 10 | const Plugin = videojs.getPlugin('plugin') |
14 | class P2pMediaLoaderPlugin extends Plugin { | 11 | class P2pMediaLoaderPlugin extends Plugin { |
15 | |||
16 | private readonly CONSTANTS = { | ||
17 | INFO_SCHEDULER: 1000 // Don't change this | ||
18 | } | ||
19 | private readonly options: P2PMediaLoaderPluginOptions | 12 | private readonly options: P2PMediaLoaderPluginOptions |
20 | 13 | ||
21 | private hlsjs: Hlsjs | 14 | private hlsjs: Hlsjs |
@@ -31,7 +24,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
31 | pendingDownload: [] as number[], | 24 | pendingDownload: [] as number[], |
32 | totalDownload: 0 | 25 | totalDownload: 0 |
33 | } | 26 | } |
34 | private startTime: number | ||
35 | 27 | ||
36 | private networkInfoInterval: any | 28 | private networkInfoInterval: any |
37 | 29 | ||
@@ -39,7 +31,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
39 | super(player) | 31 | super(player) |
40 | 32 | ||
41 | this.options = options | 33 | this.options = options |
42 | this.startTime = timeToInt(options.startTime) | ||
43 | 34 | ||
44 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 | 35 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
45 | if (!(videojs as any).Html5Hlsjs) { | 36 | if (!(videojs as any).Html5Hlsjs) { |
@@ -77,17 +68,22 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
77 | }) | 68 | }) |
78 | 69 | ||
79 | player.ready(() => { | 70 | player.ready(() => { |
80 | this.initializeCore() | ||
81 | |||
82 | this.initializePlugin() | 71 | this.initializePlugin() |
83 | }) | 72 | }) |
84 | } | 73 | } |
85 | 74 | ||
86 | dispose () { | 75 | dispose () { |
87 | if (this.hlsjs) this.hlsjs.destroy() | 76 | this.p2pEngine?.removeAllListeners() |
88 | if (this.p2pEngine) this.p2pEngine.destroy() | 77 | this.p2pEngine?.destroy() |
78 | |||
79 | this.hlsjs?.destroy() | ||
80 | this.options.segmentValidator?.destroy(); | ||
81 | |||
82 | (videojs as any).Html5Hlsjs?.removeAllHooks() | ||
89 | 83 | ||
90 | clearInterval(this.networkInfoInterval) | 84 | clearInterval(this.networkInfoInterval) |
85 | |||
86 | super.dispose() | ||
91 | } | 87 | } |
92 | 88 | ||
93 | getCurrentLevel () { | 89 | getCurrentLevel () { |
@@ -104,18 +100,6 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
104 | return this.hlsjs | 100 | return this.hlsjs |
105 | } | 101 | } |
106 | 102 | ||
107 | private initializeCore () { | ||
108 | this.player.one('play', () => { | ||
109 | this.player.addClass('vjs-has-big-play-button-clicked') | ||
110 | }) | ||
111 | |||
112 | this.player.one('canplay', () => { | ||
113 | if (this.startTime) { | ||
114 | this.player.currentTime(this.startTime) | ||
115 | } | ||
116 | }) | ||
117 | } | ||
118 | |||
119 | private initializePlugin () { | 103 | private initializePlugin () { |
120 | initHlsJsPlayer(this.hlsjs) | 104 | initHlsJsPlayer(this.hlsjs) |
121 | 105 | ||
@@ -133,7 +117,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
133 | 117 | ||
134 | this.runStats() | 118 | this.runStats() |
135 | 119 | ||
136 | this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange')) | 120 | this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engine-resolution-change')) |
137 | } | 121 | } |
138 | 122 | ||
139 | private runStats () { | 123 | private runStats () { |
@@ -167,7 +151,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
167 | this.statsP2PBytes.pendingUpload = [] | 151 | this.statsP2PBytes.pendingUpload = [] |
168 | this.statsHTTPBytes.pendingDownload = [] | 152 | this.statsHTTPBytes.pendingDownload = [] |
169 | 153 | ||
170 | return this.player.trigger('p2pInfo', { | 154 | return this.player.trigger('p2p-info', { |
171 | source: 'p2p-media-loader', | 155 | source: 'p2p-media-loader', |
172 | http: { | 156 | http: { |
173 | downloadSpeed: httpDownloadSpeed, | 157 | downloadSpeed: httpDownloadSpeed, |
@@ -182,7 +166,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
182 | }, | 166 | }, |
183 | bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8 | 167 | bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8 |
184 | } as PlayerNetworkInfo) | 168 | } as PlayerNetworkInfo) |
185 | }, this.CONSTANTS.INFO_SCHEDULER) | 169 | }, 1000) |
186 | } | 170 | } |
187 | 171 | ||
188 | private arraySum (data: number[]) { | 172 | private arraySum (data: number[]) { |
@@ -190,10 +174,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
190 | } | 174 | } |
191 | 175 | ||
192 | private fallbackToBuiltInIOS () { | 176 | private fallbackToBuiltInIOS () { |
193 | logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.'); | 177 | logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.') |
194 | |||
195 | // Workaround to force video.js to not re create a video element | ||
196 | (this.player as any).playerElIngest_ = this.player.el().parentNode | ||
197 | 178 | ||
198 | this.player.src({ | 179 | this.player.src({ |
199 | type: this.options.type, | 180 | type: this.options.type, |
@@ -203,9 +184,14 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
203 | }) | 184 | }) |
204 | }) | 185 | }) |
205 | 186 | ||
206 | this.player.ready(() => { | 187 | // Resolution button is not supported in built-in HLS player |
207 | this.initializeCore() | 188 | this.getResolutionButton().hide() |
208 | }) | 189 | } |
190 | |||
191 | private getResolutionButton () { | ||
192 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
193 | |||
194 | return settingsButton.menu.getChild('resolutionMenuButton') | ||
209 | } | 195 | } |
210 | } | 196 | } |
211 | 197 | ||
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts index 44a31bfb4..a2f7e676d 100644 --- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts +++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts | |||
@@ -9,21 +9,29 @@ type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string | |||
9 | 9 | ||
10 | const maxRetries = 10 | 10 | const maxRetries = 10 |
11 | 11 | ||
12 | function segmentValidatorFactory (options: { | 12 | export class SegmentValidator { |
13 | serverUrl: string | 13 | |
14 | segmentsSha256Url: string | 14 | private readonly bytesRangeRegex = /bytes=(\d+)-(\d+)/ |
15 | authorizationHeader: () => string | 15 | |
16 | requiresAuth: boolean | 16 | private destroyed = false |
17 | }) { | 17 | |
18 | const { serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth } = options | 18 | constructor (private readonly options: { |
19 | 19 | serverUrl: string | |
20 | let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) | 20 | segmentsSha256Url: string |
21 | const regex = /bytes=(\d+)-(\d+)/ | 21 | authorizationHeader: () => string |
22 | 22 | requiresUserAuth: boolean | |
23 | return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { | 23 | requiresPassword: boolean |
24 | videoPassword: () => string | ||
25 | }) { | ||
26 | |||
27 | } | ||
28 | |||
29 | async validate (segment: Segment, _method: string, _peerId: string, retry = 1) { | ||
30 | if (this.destroyed) return | ||
31 | |||
24 | const filename = basename(removeQueryParams(segment.url)) | 32 | const filename = basename(removeQueryParams(segment.url)) |
25 | 33 | ||
26 | const segmentValue = (await segmentsJSON)[filename] | 34 | const segmentValue = (await this.fetchSha256Segments())[filename] |
27 | 35 | ||
28 | if (!segmentValue && retry > maxRetries) { | 36 | if (!segmentValue && retry > maxRetries) { |
29 | throw new Error(`Unknown segment name ${filename} in segment validator`) | 37 | throw new Error(`Unknown segment name ${filename} in segment validator`) |
@@ -34,8 +42,7 @@ function segmentValidatorFactory (options: { | |||
34 | 42 | ||
35 | await wait(500) | 43 | await wait(500) |
36 | 44 | ||
37 | segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) | 45 | await this.validate(segment, _method, _peerId, retry + 1) |
38 | await segmentValidator(segment, _method, _peerId, retry + 1) | ||
39 | 46 | ||
40 | return | 47 | return |
41 | } | 48 | } |
@@ -46,7 +53,7 @@ function segmentValidatorFactory (options: { | |||
46 | if (typeof segmentValue === 'string') { | 53 | if (typeof segmentValue === 'string') { |
47 | hashShouldBe = segmentValue | 54 | hashShouldBe = segmentValue |
48 | } else { | 55 | } else { |
49 | const captured = regex.exec(segment.range) | 56 | const captured = this.bytesRangeRegex.exec(segment.range) |
50 | range = captured[1] + '-' + captured[2] | 57 | range = captured[1] + '-' + captured[2] |
51 | 58 | ||
52 | hashShouldBe = segmentValue[range] | 59 | hashShouldBe = segmentValue[range] |
@@ -56,7 +63,7 @@ function segmentValidatorFactory (options: { | |||
56 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) | 63 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) |
57 | } | 64 | } |
58 | 65 | ||
59 | const calculatedSha = await sha256Hex(segment.data) | 66 | const calculatedSha = await this.sha256Hex(segment.data) |
60 | if (calculatedSha !== hashShouldBe) { | 67 | if (calculatedSha !== hashShouldBe) { |
61 | throw new Error( | 68 | throw new Error( |
62 | `Hashes does not correspond for segment ${filename}/${range}` + | 69 | `Hashes does not correspond for segment ${filename}/${range}` + |
@@ -64,61 +71,53 @@ function segmentValidatorFactory (options: { | |||
64 | ) | 71 | ) |
65 | } | 72 | } |
66 | } | 73 | } |
67 | } | ||
68 | 74 | ||
69 | // --------------------------------------------------------------------------- | 75 | destroy () { |
76 | this.destroyed = true | ||
77 | } | ||
70 | 78 | ||
71 | export { | 79 | private fetchSha256Segments (): Promise<SegmentsJSON> { |
72 | segmentValidatorFactory | 80 | let headers: { [ id: string ]: string } = {} |
73 | } | ||
74 | 81 | ||
75 | // --------------------------------------------------------------------------- | 82 | if (isSameOrigin(this.options.serverUrl, this.options.segmentsSha256Url)) { |
76 | 83 | if (this.options.requiresPassword) headers = { 'x-peertube-video-password': this.options.videoPassword() } | |
77 | function fetchSha256Segments (options: { | 84 | else if (this.options.requiresUserAuth) headers = { Authorization: this.options.authorizationHeader() } |
78 | serverUrl: string | 85 | } |
79 | segmentsSha256Url: string | ||
80 | authorizationHeader: () => string | ||
81 | requiresAuth: boolean | ||
82 | }): Promise<SegmentsJSON> { | ||
83 | const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options | ||
84 | |||
85 | const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url) | ||
86 | ? { Authorization: authorizationHeader() } | ||
87 | : {} | ||
88 | |||
89 | return fetch(segmentsSha256Url, { headers }) | ||
90 | .then(res => res.json() as Promise<SegmentsJSON>) | ||
91 | .catch(err => { | ||
92 | logger.error('Cannot get sha256 segments', err) | ||
93 | return {} | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | async function sha256Hex (data?: ArrayBuffer) { | ||
98 | if (!data) return undefined | ||
99 | 86 | ||
100 | if (window.crypto.subtle) { | 87 | return fetch(this.options.segmentsSha256Url, { headers }) |
101 | return window.crypto.subtle.digest('SHA-256', data) | 88 | .then(res => res.json() as Promise<SegmentsJSON>) |
102 | .then(data => bufferToHex(data)) | 89 | .catch(err => { |
90 | logger.error('Cannot get sha256 segments', err) | ||
91 | return {} | ||
92 | }) | ||
103 | } | 93 | } |
104 | 94 | ||
105 | // Fallback for non HTTPS context | 95 | private async sha256Hex (data?: ArrayBuffer) { |
106 | const shaModule = (await import('sha.js') as any).default | 96 | if (!data) return undefined |
107 | // eslint-disable-next-line new-cap | 97 | |
108 | return new shaModule.sha256().update(Buffer.from(data)).digest('hex') | 98 | if (window.crypto.subtle) { |
109 | } | 99 | return window.crypto.subtle.digest('SHA-256', data) |
100 | .then(data => this.bufferToHex(data)) | ||
101 | } | ||
110 | 102 | ||
111 | // Thanks: https://stackoverflow.com/a/53307879 | 103 | // Fallback for non HTTPS context |
112 | function bufferToHex (buffer?: ArrayBuffer) { | 104 | const shaModule = (await import('sha.js') as any).default |
113 | if (!buffer) return '' | 105 | // eslint-disable-next-line new-cap |
106 | return new shaModule.sha256().update(Buffer.from(data)).digest('hex') | ||
107 | } | ||
114 | 108 | ||
115 | let s = '' | 109 | // Thanks: https://stackoverflow.com/a/53307879 |
116 | const h = '0123456789abcdef' | 110 | private bufferToHex (buffer?: ArrayBuffer) { |
117 | const o = new Uint8Array(buffer) | 111 | if (!buffer) return '' |
118 | 112 | ||
119 | o.forEach((v: any) => { | 113 | let s = '' |
120 | s += h[v >> 4] + h[v & 15] | 114 | const h = '0123456789abcdef' |
121 | }) | 115 | const o = new Uint8Array(buffer) |
122 | 116 | ||
123 | return s | 117 | o.forEach((v: any) => { |
118 | s += h[v >> 4] + h[v & 15] | ||
119 | }) | ||
120 | |||
121 | return s | ||
122 | } | ||
124 | } | 123 | } |
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index af2147749..f52ec75f4 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import debug from 'debug' | 1 | import debug from 'debug' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { isMobile } from '@root-helpers/web-browser' | 4 | import { isIOS, isMobile } from '@root-helpers/web-browser' |
5 | import { timeToInt } from '@shared/core-utils' | 5 | import { timeToInt } from '@shared/core-utils' |
6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' | 6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' |
7 | import { | 7 | import { |
@@ -13,7 +13,7 @@ import { | |||
13 | saveVideoWatchHistory, | 13 | saveVideoWatchHistory, |
14 | saveVolumeInStore | 14 | saveVolumeInStore |
15 | } from '../../peertube-player-local-storage' | 15 | } from '../../peertube-player-local-storage' |
16 | import { PeerTubePluginOptions, VideoJSCaption } from '../../types' | 16 | import { PeerTubePluginOptions } from '../../types' |
17 | import { SettingsButton } from '../settings/settings-menu-button' | 17 | import { SettingsButton } from '../settings/settings-menu-button' |
18 | 18 | ||
19 | const debugLogger = debug('peertube:player:peertube') | 19 | const debugLogger = debug('peertube:player:peertube') |
@@ -21,43 +21,59 @@ const debugLogger = debug('peertube:player:peertube') | |||
21 | const Plugin = videojs.getPlugin('plugin') | 21 | const Plugin = videojs.getPlugin('plugin') |
22 | 22 | ||
23 | class PeerTubePlugin extends Plugin { | 23 | class PeerTubePlugin extends Plugin { |
24 | private readonly videoViewUrl: string | 24 | private readonly videoViewUrl: () => string |
25 | private readonly authorizationHeader: () => string | 25 | private readonly authorizationHeader: () => string |
26 | private readonly initialInactivityTimeout: number | ||
26 | 27 | ||
27 | private readonly videoUUID: string | 28 | private readonly hasAutoplay: () => videojs.Autoplay |
28 | private readonly startTime: number | ||
29 | |||
30 | private readonly videoViewIntervalMs: number | ||
31 | 29 | ||
32 | private videoCaptions: VideoJSCaption[] | 30 | private currentSubtitle: string |
33 | private defaultSubtitle: string | 31 | private currentPlaybackRate: number |
34 | 32 | ||
35 | private videoViewInterval: any | 33 | private videoViewInterval: any |
36 | 34 | ||
37 | private menuOpened = false | 35 | private menuOpened = false |
38 | private mouseInControlBar = false | 36 | private mouseInControlBar = false |
39 | private mouseInSettings = false | 37 | private mouseInSettings = false |
40 | private readonly initialInactivityTimeout: number | ||
41 | 38 | ||
42 | constructor (player: videojs.Player, options?: PeerTubePluginOptions) { | 39 | private videoViewOnPlayHandler: (...args: any[]) => void |
40 | private videoViewOnSeekedHandler: (...args: any[]) => void | ||
41 | private videoViewOnEndedHandler: (...args: any[]) => void | ||
42 | |||
43 | private stopTimeHandler: (...args: any[]) => void | ||
44 | |||
45 | constructor (player: videojs.Player, private readonly options: PeerTubePluginOptions) { | ||
43 | super(player) | 46 | super(player) |
44 | 47 | ||
45 | this.videoViewUrl = options.videoViewUrl | 48 | this.videoViewUrl = options.videoViewUrl |
46 | this.authorizationHeader = options.authorizationHeader | 49 | this.authorizationHeader = options.authorizationHeader |
47 | this.videoUUID = options.videoUUID | 50 | this.hasAutoplay = options.hasAutoplay |
48 | this.startTime = timeToInt(options.startTime) | ||
49 | this.videoViewIntervalMs = options.videoViewIntervalMs | ||
50 | 51 | ||
51 | this.videoCaptions = options.videoCaptions | ||
52 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout | 52 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout |
53 | 53 | ||
54 | if (options.autoplay !== false) this.player.addClass('vjs-has-autoplay') | 54 | this.currentSubtitle = this.options.subtitle() || getStoredLastSubtitle() |
55 | |||
56 | this.initializePlayer() | ||
57 | this.initOnVideoChange() | ||
58 | |||
59 | this.deleteLegacyIndexedDB() | ||
55 | 60 | ||
56 | this.player.on('autoplay-failure', () => { | 61 | this.player.on('autoplay-failure', () => { |
62 | debugLogger('Autoplay failed') | ||
63 | |||
57 | this.player.removeClass('vjs-has-autoplay') | 64 | this.player.removeClass('vjs-has-autoplay') |
65 | |||
66 | // Fix a bug on iOS where the big play button is not displayed when autoplay fails | ||
67 | if (isIOS()) this.player.hasStarted(false) | ||
58 | }) | 68 | }) |
59 | 69 | ||
60 | this.player.ready(() => { | 70 | this.player.on('ratechange', () => { |
71 | this.currentPlaybackRate = this.player.playbackRate() | ||
72 | |||
73 | this.player.defaultPlaybackRate(this.currentPlaybackRate) | ||
74 | }) | ||
75 | |||
76 | this.player.one('canplay', () => { | ||
61 | const playerOptions = this.player.options_ | 77 | const playerOptions = this.player.options_ |
62 | 78 | ||
63 | const volume = getStoredVolume() | 79 | const volume = getStoredVolume() |
@@ -65,28 +81,15 @@ class PeerTubePlugin extends Plugin { | |||
65 | 81 | ||
66 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | 82 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() |
67 | if (muted !== undefined) this.player.muted(muted) | 83 | if (muted !== undefined) this.player.muted(muted) |
84 | }) | ||
68 | 85 | ||
69 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | 86 | this.player.ready(() => { |
70 | 87 | ||
71 | this.player.on('volumechange', () => { | 88 | this.player.on('volumechange', () => { |
72 | saveVolumeInStore(this.player.volume()) | 89 | saveVolumeInStore(this.player.volume()) |
73 | saveMuteInStore(this.player.muted()) | 90 | saveMuteInStore(this.player.muted()) |
74 | }) | 91 | }) |
75 | 92 | ||
76 | if (options.stopTime) { | ||
77 | const stopTime = timeToInt(options.stopTime) | ||
78 | const self = this | ||
79 | |||
80 | this.player.on('timeupdate', function onTimeUpdate () { | ||
81 | if (self.player.currentTime() > stopTime) { | ||
82 | self.player.pause() | ||
83 | self.player.trigger('stopped') | ||
84 | |||
85 | self.player.off('timeupdate', onTimeUpdate) | ||
86 | } | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | this.player.textTracks().addEventListener('change', () => { | 93 | this.player.textTracks().addEventListener('change', () => { |
91 | const showing = this.player.textTracks().tracks_.find(t => { | 94 | const showing = this.player.textTracks().tracks_.find(t => { |
92 | return t.kind === 'captions' && t.mode === 'showing' | 95 | return t.kind === 'captions' && t.mode === 'showing' |
@@ -94,23 +97,24 @@ class PeerTubePlugin extends Plugin { | |||
94 | 97 | ||
95 | if (!showing) { | 98 | if (!showing) { |
96 | saveLastSubtitle('off') | 99 | saveLastSubtitle('off') |
100 | this.currentSubtitle = undefined | ||
97 | return | 101 | return |
98 | } | 102 | } |
99 | 103 | ||
104 | this.currentSubtitle = showing.language | ||
100 | saveLastSubtitle(showing.language) | 105 | saveLastSubtitle(showing.language) |
101 | }) | 106 | }) |
102 | 107 | ||
103 | this.player.on('sourcechange', () => this.initCaptions()) | 108 | this.player.on('video-change', () => { |
104 | 109 | this.initOnVideoChange() | |
105 | this.player.duration(options.videoDuration) | 110 | }) |
106 | |||
107 | this.initializePlayer() | ||
108 | this.runUserViewing() | ||
109 | }) | 111 | }) |
110 | } | 112 | } |
111 | 113 | ||
112 | dispose () { | 114 | dispose () { |
113 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) | 115 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
116 | |||
117 | super.dispose() | ||
114 | } | 118 | } |
115 | 119 | ||
116 | onMenuOpened () { | 120 | onMenuOpened () { |
@@ -162,40 +166,70 @@ class PeerTubePlugin extends Plugin { | |||
162 | 166 | ||
163 | this.initSmoothProgressBar() | 167 | this.initSmoothProgressBar() |
164 | 168 | ||
165 | this.initCaptions() | 169 | this.player.ready(() => { |
166 | 170 | this.listenControlBarMouse() | |
167 | this.listenControlBarMouse() | 171 | }) |
168 | 172 | ||
169 | this.listenFullScreenChange() | 173 | this.listenFullScreenChange() |
170 | } | 174 | } |
171 | 175 | ||
176 | private initOnVideoChange () { | ||
177 | if (this.hasAutoplay() !== false) this.player.addClass('vjs-has-autoplay') | ||
178 | else this.player.removeClass('vjs-has-autoplay') | ||
179 | |||
180 | if (this.currentPlaybackRate && this.currentPlaybackRate !== 1) { | ||
181 | debugLogger('Setting playback rate to ' + this.currentPlaybackRate) | ||
182 | |||
183 | this.player.playbackRate(this.currentPlaybackRate) | ||
184 | } | ||
185 | |||
186 | this.player.ready(() => { | ||
187 | this.initCaptions() | ||
188 | this.updateControlBar() | ||
189 | }) | ||
190 | |||
191 | this.handleStartStopTime() | ||
192 | this.runUserViewing() | ||
193 | } | ||
194 | |||
172 | // --------------------------------------------------------------------------- | 195 | // --------------------------------------------------------------------------- |
173 | 196 | ||
174 | private runUserViewing () { | 197 | private runUserViewing () { |
175 | let lastCurrentTime = this.startTime | 198 | const startTime = timeToInt(this.options.startTime()) |
199 | |||
200 | let lastCurrentTime = startTime | ||
176 | let lastViewEvent: VideoViewEvent | 201 | let lastViewEvent: VideoViewEvent |
177 | 202 | ||
178 | this.player.one('play', () => { | 203 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
179 | this.notifyUserIsWatching(this.startTime, lastViewEvent) | 204 | if (this.videoViewOnPlayHandler) this.player.off('play', this.videoViewOnPlayHandler) |
180 | }) | 205 | if (this.videoViewOnSeekedHandler) this.player.off('seeked', this.videoViewOnSeekedHandler) |
206 | if (this.videoViewOnEndedHandler) this.player.off('ended', this.videoViewOnEndedHandler) | ||
181 | 207 | ||
182 | this.player.on('seeked', () => { | 208 | this.videoViewOnPlayHandler = () => { |
209 | this.notifyUserIsWatching(startTime, lastViewEvent) | ||
210 | } | ||
211 | |||
212 | this.videoViewOnSeekedHandler = () => { | ||
183 | const diff = Math.floor(this.player.currentTime()) - lastCurrentTime | 213 | const diff = Math.floor(this.player.currentTime()) - lastCurrentTime |
184 | 214 | ||
185 | // Don't take into account small forwards | 215 | // Don't take into account small forwards |
186 | if (diff > 0 && diff < 3) return | 216 | if (diff > 0 && diff < 3) return |
187 | 217 | ||
188 | lastViewEvent = 'seek' | 218 | lastViewEvent = 'seek' |
189 | }) | 219 | } |
190 | 220 | ||
191 | this.player.one('ended', () => { | 221 | this.videoViewOnEndedHandler = () => { |
192 | const currentTime = Math.floor(this.player.duration()) | 222 | const currentTime = Math.floor(this.player.duration()) |
193 | lastCurrentTime = currentTime | 223 | lastCurrentTime = currentTime |
194 | 224 | ||
195 | this.notifyUserIsWatching(currentTime, lastViewEvent) | 225 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
196 | 226 | ||
197 | lastViewEvent = undefined | 227 | lastViewEvent = undefined |
198 | }) | 228 | } |
229 | |||
230 | this.player.one('play', this.videoViewOnPlayHandler) | ||
231 | this.player.on('seeked', this.videoViewOnSeekedHandler) | ||
232 | this.player.one('ended', this.videoViewOnEndedHandler) | ||
199 | 233 | ||
200 | this.videoViewInterval = setInterval(() => { | 234 | this.videoViewInterval = setInterval(() => { |
201 | const currentTime = Math.floor(this.player.currentTime()) | 235 | const currentTime = Math.floor(this.player.currentTime()) |
@@ -209,13 +243,13 @@ class PeerTubePlugin extends Plugin { | |||
209 | .catch(err => logger.error('Cannot notify user is watching.', err)) | 243 | .catch(err => logger.error('Cannot notify user is watching.', err)) |
210 | 244 | ||
211 | lastViewEvent = undefined | 245 | lastViewEvent = undefined |
212 | }, this.videoViewIntervalMs) | 246 | }, this.options.videoViewIntervalMs) |
213 | } | 247 | } |
214 | 248 | ||
215 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { | 249 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { |
216 | // Server won't save history, so save the video position in local storage | 250 | // Server won't save history, so save the video position in local storage |
217 | if (!this.authorizationHeader()) { | 251 | if (!this.authorizationHeader()) { |
218 | saveVideoWatchHistory(this.videoUUID, currentTime) | 252 | saveVideoWatchHistory(this.options.videoUUID(), currentTime) |
219 | } | 253 | } |
220 | 254 | ||
221 | if (!this.videoViewUrl) return Promise.resolve(true) | 255 | if (!this.videoViewUrl) return Promise.resolve(true) |
@@ -225,7 +259,7 @@ class PeerTubePlugin extends Plugin { | |||
225 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) | 259 | const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) |
226 | if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader()) | 260 | if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader()) |
227 | 261 | ||
228 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) | 262 | return fetch(this.videoViewUrl(), { method: 'POST', body: JSON.stringify(body), headers }) |
229 | } | 263 | } |
230 | 264 | ||
231 | // --------------------------------------------------------------------------- | 265 | // --------------------------------------------------------------------------- |
@@ -279,18 +313,89 @@ class PeerTubePlugin extends Plugin { | |||
279 | } | 313 | } |
280 | 314 | ||
281 | private initCaptions () { | 315 | private initCaptions () { |
282 | for (const caption of this.videoCaptions) { | 316 | debugLogger('Init captions with current subtitle ' + this.currentSubtitle) |
317 | |||
318 | this.player.tech(true).clearTracks('text') | ||
319 | |||
320 | for (const caption of this.options.videoCaptions()) { | ||
283 | this.player.addRemoteTextTrack({ | 321 | this.player.addRemoteTextTrack({ |
284 | kind: 'captions', | 322 | kind: 'captions', |
285 | label: caption.label, | 323 | label: caption.label, |
286 | language: caption.language, | 324 | language: caption.language, |
287 | id: caption.language, | 325 | id: caption.language, |
288 | src: caption.src, | 326 | src: caption.src, |
289 | default: this.defaultSubtitle === caption.language | 327 | default: this.currentSubtitle === caption.language |
290 | }, false) | 328 | }, true) |
329 | } | ||
330 | |||
331 | this.player.trigger('captions-changed') | ||
332 | } | ||
333 | |||
334 | private updateControlBar () { | ||
335 | debugLogger('Updating control bar') | ||
336 | |||
337 | if (this.options.isLive()) { | ||
338 | this.getPlaybackRateButton().hide() | ||
339 | |||
340 | this.player.controlBar.getChild('progressControl').hide() | ||
341 | this.player.controlBar.getChild('currentTimeDisplay').hide() | ||
342 | this.player.controlBar.getChild('timeDivider').hide() | ||
343 | this.player.controlBar.getChild('durationDisplay').hide() | ||
344 | |||
345 | this.player.controlBar.getChild('peerTubeLiveDisplay').show() | ||
346 | } else { | ||
347 | this.getPlaybackRateButton().show() | ||
348 | |||
349 | this.player.controlBar.getChild('progressControl').show() | ||
350 | this.player.controlBar.getChild('currentTimeDisplay').show() | ||
351 | this.player.controlBar.getChild('timeDivider').show() | ||
352 | this.player.controlBar.getChild('durationDisplay').show() | ||
353 | |||
354 | this.player.controlBar.getChild('peerTubeLiveDisplay').hide() | ||
291 | } | 355 | } |
292 | 356 | ||
293 | this.player.trigger('captionsChanged') | 357 | if (this.options.videoCaptions().length === 0) { |
358 | this.getCaptionsButton().hide() | ||
359 | } else { | ||
360 | this.getCaptionsButton().show() | ||
361 | } | ||
362 | } | ||
363 | |||
364 | private handleStartStopTime () { | ||
365 | this.player.duration(this.options.videoDuration()) | ||
366 | |||
367 | if (this.stopTimeHandler) { | ||
368 | this.player.off('timeupdate', this.stopTimeHandler) | ||
369 | this.stopTimeHandler = undefined | ||
370 | } | ||
371 | |||
372 | // Prefer canplaythrough instead of canplay because Chrome has issues with the second one | ||
373 | this.player.one('canplaythrough', () => { | ||
374 | if (this.options.startTime()) { | ||
375 | debugLogger('Start the video at ' + this.options.startTime()) | ||
376 | |||
377 | this.player.currentTime(timeToInt(this.options.startTime())) | ||
378 | } | ||
379 | |||
380 | if (this.options.stopTime()) { | ||
381 | const stopTime = timeToInt(this.options.stopTime()) | ||
382 | |||
383 | this.stopTimeHandler = () => { | ||
384 | if (this.player.currentTime() <= stopTime) return | ||
385 | |||
386 | debugLogger('Stopping the video at ' + this.options.stopTime()) | ||
387 | |||
388 | // Time top stop | ||
389 | this.player.pause() | ||
390 | this.player.trigger('auto-stopped') | ||
391 | |||
392 | this.player.off('timeupdate', this.stopTimeHandler) | ||
393 | this.stopTimeHandler = undefined | ||
394 | } | ||
395 | |||
396 | this.player.on('timeupdate', this.stopTimeHandler) | ||
397 | } | ||
398 | }) | ||
294 | } | 399 | } |
295 | 400 | ||
296 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 401 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
@@ -314,6 +419,37 @@ class PeerTubePlugin extends Plugin { | |||
314 | this.update() | 419 | this.update() |
315 | } | 420 | } |
316 | } | 421 | } |
422 | |||
423 | private getCaptionsButton () { | ||
424 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
425 | |||
426 | return settingsButton.menu.getChild('captionsButton') as videojs.CaptionsButton | ||
427 | } | ||
428 | |||
429 | private getPlaybackRateButton () { | ||
430 | const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton | ||
431 | |||
432 | return settingsButton.menu.getChild('playbackRateMenuButton') | ||
433 | } | ||
434 | |||
435 | // We don't use webtorrent anymore, so we can safely remove old chunks from IndexedDB | ||
436 | private deleteLegacyIndexedDB () { | ||
437 | try { | ||
438 | if (typeof window.indexedDB === 'undefined') return | ||
439 | if (!window.indexedDB) return | ||
440 | if (typeof window.indexedDB.databases !== 'function') return | ||
441 | |||
442 | window.indexedDB.databases() | ||
443 | .then(databases => { | ||
444 | for (const db of databases) { | ||
445 | window.indexedDB.deleteDatabase(db.name) | ||
446 | } | ||
447 | }) | ||
448 | } catch (err) { | ||
449 | debugLogger('Cannot delete legacy indexed DB', err) | ||
450 | // Nothing to do | ||
451 | } | ||
452 | } | ||
317 | } | 453 | } |
318 | 454 | ||
319 | videojs.registerPlugin('peertube', PeerTubePlugin) | 455 | videojs.registerPlugin('peertube', PeerTubePlugin) |
diff --git a/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts new file mode 100644 index 000000000..b467e3637 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts | |||
@@ -0,0 +1,136 @@ | |||
1 | import { | ||
2 | NextPreviousVideoButtonOptions, | ||
3 | PeerTubeLinkButtonOptions, | ||
4 | PeerTubePlayerContructorOptions, | ||
5 | PeerTubePlayerLoadOptions, | ||
6 | TheaterButtonOptions | ||
7 | } from '../../types' | ||
8 | |||
9 | type ControlBarOptionsBuilderConstructorOptions = | ||
10 | Pick<PeerTubePlayerContructorOptions, 'peertubeLink' | 'instanceName' | 'theaterButton'> & | ||
11 | { | ||
12 | videoShortUUID: () => string | ||
13 | p2pEnabled: () => boolean | ||
14 | |||
15 | previousVideo: () => PeerTubePlayerLoadOptions['previousVideo'] | ||
16 | nextVideo: () => PeerTubePlayerLoadOptions['nextVideo'] | ||
17 | } | ||
18 | |||
19 | export class ControlBarOptionsBuilder { | ||
20 | |||
21 | constructor (private options: ControlBarOptionsBuilderConstructorOptions) { | ||
22 | } | ||
23 | |||
24 | getChildrenOptions () { | ||
25 | const children = { | ||
26 | ...this.getPreviousVideo(), | ||
27 | |||
28 | playToggle: {}, | ||
29 | |||
30 | ...this.getNextVideo(), | ||
31 | |||
32 | ...this.getTimeControls(), | ||
33 | |||
34 | ...this.getProgressControl(), | ||
35 | |||
36 | p2PInfoButton: {}, | ||
37 | muteToggle: {}, | ||
38 | volumeControl: {}, | ||
39 | |||
40 | ...this.getSettingsButton(), | ||
41 | |||
42 | ...this.getPeerTubeLinkButton(), | ||
43 | |||
44 | ...this.getTheaterButton(), | ||
45 | |||
46 | fullscreenToggle: {} | ||
47 | } | ||
48 | |||
49 | return children | ||
50 | } | ||
51 | |||
52 | private getSettingsButton () { | ||
53 | const settingEntries: string[] = [] | ||
54 | |||
55 | settingEntries.push('playbackRateMenuButton') | ||
56 | settingEntries.push('captionsButton') | ||
57 | settingEntries.push('resolutionMenuButton') | ||
58 | |||
59 | return { | ||
60 | settingsButton: { | ||
61 | setup: { | ||
62 | maxHeightOffset: 40 | ||
63 | }, | ||
64 | entries: settingEntries | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | private getTimeControls () { | ||
70 | return { | ||
71 | peerTubeLiveDisplay: {}, | ||
72 | |||
73 | currentTimeDisplay: {}, | ||
74 | timeDivider: {}, | ||
75 | durationDisplay: {} | ||
76 | } | ||
77 | } | ||
78 | |||
79 | private getProgressControl () { | ||
80 | return { | ||
81 | progressControl: { | ||
82 | children: { | ||
83 | seekBar: { | ||
84 | children: { | ||
85 | loadProgressBar: {}, | ||
86 | mouseTimeDisplay: {}, | ||
87 | playProgressBar: {} | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | |||
95 | private getPreviousVideo () { | ||
96 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
97 | type: 'previous', | ||
98 | handler: () => this.options.previousVideo().handler(), | ||
99 | isDisabled: () => !this.options.previousVideo().enabled, | ||
100 | isDisplayed: () => this.options.previousVideo().displayControlBarButton | ||
101 | } | ||
102 | |||
103 | return { previousVideoButton: buttonOptions } | ||
104 | } | ||
105 | |||
106 | private getNextVideo () { | ||
107 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
108 | type: 'next', | ||
109 | handler: () => this.options.nextVideo().handler(), | ||
110 | isDisabled: () => !this.options.nextVideo().enabled, | ||
111 | isDisplayed: () => this.options.nextVideo().displayControlBarButton | ||
112 | } | ||
113 | |||
114 | return { nextVideoButton: buttonOptions } | ||
115 | } | ||
116 | |||
117 | private getPeerTubeLinkButton () { | ||
118 | const options: PeerTubeLinkButtonOptions = { | ||
119 | isDisplayed: this.options.peertubeLink, | ||
120 | shortUUID: this.options.videoShortUUID, | ||
121 | instanceName: this.options.instanceName | ||
122 | } | ||
123 | |||
124 | return { peerTubeLinkButton: options } | ||
125 | } | ||
126 | |||
127 | private getTheaterButton () { | ||
128 | const options: TheaterButtonOptions = { | ||
129 | isDisplayed: () => this.options.theaterButton | ||
130 | } | ||
131 | |||
132 | return { | ||
133 | theaterButton: options | ||
134 | } | ||
135 | } | ||
136 | } | ||
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts index 194991fa4..10df2db5d 100644 --- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts +++ b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts | |||
@@ -3,49 +3,61 @@ import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | |||
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { LiveVideoLatencyMode } from '@shared/models' | 4 | import { LiveVideoLatencyMode } from '@shared/models' |
5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' | 5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' |
6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' | 6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions } from '../../types' |
7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' | ||
8 | import { getRtcConfig, isSameOrigin } from '../common' | 7 | import { getRtcConfig, isSameOrigin } from '../common' |
9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' | 8 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' |
10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' | 9 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' |
11 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' | 10 | import { SegmentValidator } from '../p2p-media-loader/segment-validator' |
11 | |||
12 | type ConstructorOptions = | ||
13 | Pick<PeerTubePlayerContructorOptions, 'pluginsManager' | 'serverUrl' | 'authorizationHeader'> & | ||
14 | Pick<PeerTubePlayerLoadOptions, 'videoPassword' | 'requiresUserAuth' | 'videoFileToken' | 'requiresPassword' | | ||
15 | 'isLive' | 'liveOptions' | 'p2pEnabled' | 'hls'> | ||
12 | 16 | ||
13 | export class HLSOptionsBuilder { | 17 | export class HLSOptionsBuilder { |
14 | 18 | ||
15 | constructor ( | 19 | constructor ( |
16 | private options: PeertubePlayerManagerOptions, | 20 | private options: ConstructorOptions, |
17 | private p2pMediaLoaderModule?: any | 21 | private p2pMediaLoaderModule?: any |
18 | ) { | 22 | ) { |
19 | 23 | ||
20 | } | 24 | } |
21 | 25 | ||
22 | async getPluginOptions () { | 26 | async getPluginOptions () { |
23 | const commonOptions = this.options.common | 27 | const redundancyUrlManager = new RedundancyUrlManager(this.options.hls.redundancyBaseUrls) |
24 | 28 | const segmentValidator = new SegmentValidator({ | |
25 | const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls) | 29 | segmentsSha256Url: this.options.hls.segmentsSha256Url, |
30 | authorizationHeader: this.options.authorizationHeader, | ||
31 | requiresUserAuth: this.options.requiresUserAuth, | ||
32 | serverUrl: this.options.serverUrl, | ||
33 | requiresPassword: this.options.requiresPassword, | ||
34 | videoPassword: this.options.videoPassword | ||
35 | }) | ||
26 | 36 | ||
27 | const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook( | 37 | const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook( |
28 | 'filter:internal.player.p2p-media-loader.options.result', | 38 | 'filter:internal.player.p2p-media-loader.options.result', |
29 | this.getP2PMediaLoaderOptions(redundancyUrlManager) | 39 | this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator }) |
30 | ) | 40 | ) |
31 | const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader | 41 | const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader |
32 | 42 | ||
33 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | 43 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { |
34 | requiresAuth: commonOptions.requiresAuth, | 44 | requiresUserAuth: this.options.requiresUserAuth, |
35 | videoFileToken: commonOptions.videoFileToken, | 45 | videoFileToken: this.options.videoFileToken, |
36 | 46 | ||
37 | redundancyUrlManager, | 47 | redundancyUrlManager, |
38 | type: 'application/x-mpegURL', | 48 | type: 'application/x-mpegURL', |
39 | startTime: commonOptions.startTime, | 49 | src: this.options.hls.playlistUrl, |
40 | src: this.options.p2pMediaLoader.playlistUrl, | 50 | segmentValidator, |
41 | loader | 51 | loader |
42 | } | 52 | } |
43 | 53 | ||
44 | const hlsjs = { | 54 | const hlsjs = { |
55 | hlsjsConfig: this.getHLSJSOptions(loader), | ||
56 | |||
45 | levelLabelHandler: (level: { height: number, width: number }) => { | 57 | levelLabelHandler: (level: { height: number, width: number }) => { |
46 | const resolution = Math.min(level.height || 0, level.width || 0) | 58 | const resolution = Math.min(level.height || 0, level.width || 0) |
47 | 59 | ||
48 | const file = this.options.p2pMediaLoader.videoFiles.find(f => f.resolution.id === resolution) | 60 | const file = this.options.hls.videoFiles.find(f => f.resolution.id === resolution) |
49 | // We don't have files for live videos | 61 | // We don't have files for live videos |
50 | if (!file) return level.height | 62 | if (!file) return level.height |
51 | 63 | ||
@@ -56,26 +68,27 @@ export class HLSOptionsBuilder { | |||
56 | } | 68 | } |
57 | } | 69 | } |
58 | 70 | ||
59 | const html5 = { | 71 | return { p2pMediaLoader, hlsjs } |
60 | hlsjsConfig: this.getHLSJSOptions(loader) | ||
61 | } | ||
62 | |||
63 | return { p2pMediaLoader, hlsjs, html5 } | ||
64 | } | 72 | } |
65 | 73 | ||
66 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
67 | 75 | ||
68 | private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings { | 76 | private getP2PMediaLoaderOptions (options: { |
77 | redundancyUrlManager: RedundancyUrlManager | ||
78 | segmentValidator: SegmentValidator | ||
79 | }): HlsJsEngineSettings { | ||
80 | const { redundancyUrlManager, segmentValidator } = options | ||
81 | |||
69 | let consumeOnly = false | 82 | let consumeOnly = false |
70 | if ((navigator as any)?.connection?.type === 'cellular') { | 83 | if ((navigator as any)?.connection?.type === 'cellular') { |
71 | logger.info('We are on a cellular connection: disabling seeding.') | 84 | logger.info('We are on a cellular connection: disabling seeding.') |
72 | consumeOnly = true | 85 | consumeOnly = true |
73 | } | 86 | } |
74 | 87 | ||
75 | const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce | 88 | const trackerAnnounce = this.options.hls.trackerAnnounce |
76 | .filter(t => t.startsWith('ws')) | 89 | .filter(t => t.startsWith('ws')) |
77 | 90 | ||
78 | const specificLiveOrVODOptions = this.options.common.isLive | 91 | const specificLiveOrVODOptions = this.options.isLive |
79 | ? this.getP2PMediaLoaderLiveOptions() | 92 | ? this.getP2PMediaLoaderLiveOptions() |
80 | : this.getP2PMediaLoaderVODOptions() | 93 | : this.getP2PMediaLoaderVODOptions() |
81 | 94 | ||
@@ -88,28 +101,28 @@ export class HLSOptionsBuilder { | |||
88 | httpFailedSegmentTimeout: 1000, | 101 | httpFailedSegmentTimeout: 1000, |
89 | 102 | ||
90 | xhrSetup: (xhr, url) => { | 103 | xhrSetup: (xhr, url) => { |
91 | if (!this.options.common.requiresAuth) return | 104 | const { requiresUserAuth, requiresPassword } = this.options |
92 | if (!isSameOrigin(this.options.common.serverUrl, url)) return | 105 | |
106 | if (!(requiresUserAuth || requiresPassword)) return | ||
107 | |||
108 | if (!isSameOrigin(this.options.serverUrl, url)) return | ||
109 | |||
110 | if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword()) | ||
93 | 111 | ||
94 | xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader()) | 112 | else xhr.setRequestHeader('Authorization', this.options.authorizationHeader()) |
95 | }, | 113 | }, |
96 | 114 | ||
97 | segmentValidator: segmentValidatorFactory({ | 115 | segmentValidator: segmentValidator.validate.bind(segmentValidator), |
98 | segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url, | ||
99 | authorizationHeader: this.options.common.authorizationHeader, | ||
100 | requiresAuth: this.options.common.requiresAuth, | ||
101 | serverUrl: this.options.common.serverUrl | ||
102 | }), | ||
103 | 116 | ||
104 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), | 117 | segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), |
105 | 118 | ||
106 | useP2P: this.options.common.p2pEnabled, | 119 | useP2P: this.options.p2pEnabled, |
107 | consumeOnly, | 120 | consumeOnly, |
108 | 121 | ||
109 | ...specificLiveOrVODOptions | 122 | ...specificLiveOrVODOptions |
110 | }, | 123 | }, |
111 | segments: { | 124 | segments: { |
112 | swarmId: this.options.p2pMediaLoader.playlistUrl, | 125 | swarmId: this.options.hls.playlistUrl, |
113 | forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20 | 126 | forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20 |
114 | } | 127 | } |
115 | } | 128 | } |
@@ -120,7 +133,7 @@ export class HLSOptionsBuilder { | |||
120 | requiredSegmentsPriority: 1 | 133 | requiredSegmentsPriority: 1 |
121 | } | 134 | } |
122 | 135 | ||
123 | const latencyMode = this.options.common.liveOptions.latencyMode | 136 | const latencyMode = this.options.liveOptions.latencyMode |
124 | 137 | ||
125 | switch (latencyMode) { | 138 | switch (latencyMode) { |
126 | case LiveVideoLatencyMode.SMALL_LATENCY: | 139 | case LiveVideoLatencyMode.SMALL_LATENCY: |
@@ -158,7 +171,7 @@ export class HLSOptionsBuilder { | |||
158 | // --------------------------------------------------------------------------- | 171 | // --------------------------------------------------------------------------- |
159 | 172 | ||
160 | private getHLSJSOptions (loader: P2PMediaLoader) { | 173 | private getHLSJSOptions (loader: P2PMediaLoader) { |
161 | const specificLiveOrVODOptions = this.options.common.isLive | 174 | const specificLiveOrVODOptions = this.options.isLive |
162 | ? this.getHLSLiveOptions() | 175 | ? this.getHLSLiveOptions() |
163 | : this.getHLSVODOptions() | 176 | : this.getHLSVODOptions() |
164 | 177 | ||
@@ -186,7 +199,7 @@ export class HLSOptionsBuilder { | |||
186 | } | 199 | } |
187 | 200 | ||
188 | private getHLSLiveOptions () { | 201 | private getHLSLiveOptions () { |
189 | const latencyMode = this.options.common.liveOptions.latencyMode | 202 | const latencyMode = this.options.liveOptions.latencyMode |
190 | 203 | ||
191 | switch (latencyMode) { | 204 | switch (latencyMode) { |
192 | case LiveVideoLatencyMode.SMALL_LATENCY: | 205 | case LiveVideoLatencyMode.SMALL_LATENCY: |
diff --git a/client/src/assets/player/shared/player-options-builder/index.ts b/client/src/assets/player/shared/player-options-builder/index.ts new file mode 100644 index 000000000..674754a94 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './control-bar-options-builder' | ||
2 | export * from './hls-options-builder' | ||
3 | export * from './web-video-options-builder' | ||
diff --git a/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts new file mode 100644 index 000000000..a3c3c3f27 --- /dev/null +++ b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { PeerTubePlayerLoadOptions, WebVideoPluginOptions } from '../../types' | ||
2 | |||
3 | type ConstructorOptions = Pick<PeerTubePlayerLoadOptions, 'videoFileToken' | 'webVideo' | 'hls' | 'startTime'> | ||
4 | |||
5 | export class WebVideoOptionsBuilder { | ||
6 | |||
7 | constructor (private options: ConstructorOptions) { | ||
8 | |||
9 | } | ||
10 | |||
11 | getPluginOptions (): WebVideoPluginOptions { | ||
12 | return { | ||
13 | videoFileToken: this.options.videoFileToken, | ||
14 | |||
15 | videoFiles: this.options.webVideo.videoFiles.length !== 0 | ||
16 | ? this.options.webVideo.videoFiles | ||
17 | : this.options?.hls.videoFiles || [], | ||
18 | |||
19 | startTime: this.options.startTime | ||
20 | } | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-button.ts b/client/src/assets/player/shared/playlist/playlist-button.ts index 6cfaf4158..45cbb4899 100644 --- a/client/src/assets/player/shared/playlist/playlist-button.ts +++ b/client/src/assets/player/shared/playlist/playlist-button.ts | |||
@@ -8,8 +8,15 @@ class PlaylistButton extends ClickableComponent { | |||
8 | private playlistInfoElement: HTMLElement | 8 | private playlistInfoElement: HTMLElement |
9 | private wrapper: HTMLElement | 9 | private wrapper: HTMLElement |
10 | 10 | ||
11 | constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) { | 11 | options_: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions |
12 | super(player, options as any) | 12 | |
13 | // FIXME: eslint -> it's not a useless constructor, we need to extend constructor options typings | ||
14 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor | ||
15 | constructor ( | ||
16 | player: videojs.Player, | ||
17 | options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions | ||
18 | ) { | ||
19 | super(player, options) | ||
13 | } | 20 | } |
14 | 21 | ||
15 | createEl () { | 22 | createEl () { |
@@ -40,20 +47,15 @@ class PlaylistButton extends ClickableComponent { | |||
40 | } | 47 | } |
41 | 48 | ||
42 | update () { | 49 | update () { |
43 | const options = this.options_ as PlaylistPluginOptions | 50 | this.playlistInfoElement.innerHTML = this.options_.getCurrentPosition() + '/' + this.options_.playlist.videosLength |
44 | 51 | ||
45 | this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength | 52 | this.wrapper.title = this.player().localize('Playlist: {1}', [ this.options_.playlist.displayName ]) |
46 | this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ]) | ||
47 | } | 53 | } |
48 | 54 | ||
49 | handleClick () { | 55 | handleClick () { |
50 | const playlistMenu = this.getPlaylistMenu() | 56 | const playlistMenu = this.options_.playlistMenu |
51 | playlistMenu.open() | 57 | playlistMenu.open() |
52 | } | 58 | } |
53 | |||
54 | private getPlaylistMenu () { | ||
55 | return (this.options_ as any).playlistMenu as PlaylistMenu | ||
56 | } | ||
57 | } | 59 | } |
58 | 60 | ||
59 | videojs.registerComponent('PlaylistButton', PlaylistButton) | 61 | videojs.registerComponent('PlaylistButton', PlaylistButton) |
diff --git a/client/src/assets/player/shared/playlist/playlist-menu-item.ts b/client/src/assets/player/shared/playlist/playlist-menu-item.ts index 81b5acf30..f9366332d 100644 --- a/client/src/assets/player/shared/playlist/playlist-menu-item.ts +++ b/client/src/assets/player/shared/playlist/playlist-menu-item.ts | |||
@@ -8,6 +8,11 @@ const Component = videojs.getComponent('Component') | |||
8 | class PlaylistMenuItem extends Component { | 8 | class PlaylistMenuItem extends Component { |
9 | private element: VideoPlaylistElement | 9 | private element: VideoPlaylistElement |
10 | 10 | ||
11 | private clickHandler: () => void | ||
12 | private keyDownHandler: (event: KeyboardEvent) => void | ||
13 | |||
14 | options_: videojs.ComponentOptions & PlaylistItemOptions | ||
15 | |||
11 | constructor (player: videojs.Player, options?: PlaylistItemOptions) { | 16 | constructor (player: videojs.Player, options?: PlaylistItemOptions) { |
12 | super(player, options as any) | 17 | super(player, options as any) |
13 | 18 | ||
@@ -15,19 +20,27 @@ class PlaylistMenuItem extends Component { | |||
15 | 20 | ||
16 | this.element = options.element | 21 | this.element = options.element |
17 | 22 | ||
18 | this.on([ 'click', 'tap' ], () => this.switchPlaylistItem()) | 23 | this.clickHandler = () => this.switchPlaylistItem() |
19 | this.on('keydown', event => this.handleKeyDown(event)) | 24 | this.keyDownHandler = event => this.handleKeyDown(event) |
25 | |||
26 | this.on([ 'click', 'tap' ], this.clickHandler) | ||
27 | this.on('keydown', this.keyDownHandler) | ||
20 | } | 28 | } |
21 | 29 | ||
22 | createEl () { | 30 | dispose () { |
23 | const options = this.options_ as PlaylistItemOptions | 31 | this.off([ 'click', 'tap' ], this.clickHandler) |
32 | this.off('keydown', this.keyDownHandler) | ||
24 | 33 | ||
34 | super.dispose() | ||
35 | } | ||
36 | |||
37 | createEl () { | ||
25 | const li = super.createEl('li', { | 38 | const li = super.createEl('li', { |
26 | className: 'vjs-playlist-menu-item', | 39 | className: 'vjs-playlist-menu-item', |
27 | innerHTML: '' | 40 | innerHTML: '' |
28 | }) as HTMLElement | 41 | }) as HTMLElement |
29 | 42 | ||
30 | if (!options.element.video) { | 43 | if (!this.options_.element.video) { |
31 | li.classList.add('vjs-disabled') | 44 | li.classList.add('vjs-disabled') |
32 | } | 45 | } |
33 | 46 | ||
@@ -37,14 +50,14 @@ class PlaylistMenuItem extends Component { | |||
37 | 50 | ||
38 | const position = super.createEl('div', { | 51 | const position = super.createEl('div', { |
39 | className: 'item-position', | 52 | className: 'item-position', |
40 | innerHTML: options.element.position | 53 | innerHTML: this.options_.element.position |
41 | }) | 54 | }) |
42 | 55 | ||
43 | positionBlock.appendChild(position) | 56 | positionBlock.appendChild(position) |
44 | li.appendChild(positionBlock) | 57 | li.appendChild(positionBlock) |
45 | 58 | ||
46 | if (options.element.video) { | 59 | if (this.options_.element.video) { |
47 | this.buildAvailableVideo(li, positionBlock, options) | 60 | this.buildAvailableVideo(li, positionBlock, this.options_) |
48 | } else { | 61 | } else { |
49 | this.buildUnavailableVideo(li) | 62 | this.buildUnavailableVideo(li) |
50 | } | 63 | } |
@@ -125,9 +138,7 @@ class PlaylistMenuItem extends Component { | |||
125 | } | 138 | } |
126 | 139 | ||
127 | private switchPlaylistItem () { | 140 | private switchPlaylistItem () { |
128 | const options = this.options_ as PlaylistItemOptions | 141 | this.options_.onClicked() |
129 | |||
130 | options.onClicked() | ||
131 | } | 142 | } |
132 | } | 143 | } |
133 | 144 | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-menu.ts b/client/src/assets/player/shared/playlist/playlist-menu.ts index 1ec9ac804..53a5a7274 100644 --- a/client/src/assets/player/shared/playlist/playlist-menu.ts +++ b/client/src/assets/player/shared/playlist/playlist-menu.ts | |||
@@ -6,26 +6,32 @@ import { PlaylistMenuItem } from './playlist-menu-item' | |||
6 | const Component = videojs.getComponent('Component') | 6 | const Component = videojs.getComponent('Component') |
7 | 7 | ||
8 | class PlaylistMenu extends Component { | 8 | class PlaylistMenu extends Component { |
9 | private menuItems: PlaylistMenuItem[] | 9 | private menuItems: PlaylistMenuItem[] = [] |
10 | 10 | ||
11 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { | 11 | private readonly userInactiveHandler: () => void |
12 | super(player, options as any) | 12 | private readonly onMouseEnter: () => void |
13 | private readonly onMouseLeave: () => void | ||
13 | 14 | ||
14 | const self = this | 15 | private readonly onPlayerCick: (event: Event) => void |
15 | 16 | ||
16 | function userInactiveHandler () { | 17 | options_: PlaylistPluginOptions & videojs.ComponentOptions |
17 | self.close() | 18 | |
19 | constructor (player: videojs.Player, options?: PlaylistPluginOptions & videojs.ComponentOptions) { | ||
20 | super(player, options) | ||
21 | |||
22 | this.userInactiveHandler = () => { | ||
23 | this.close() | ||
18 | } | 24 | } |
19 | 25 | ||
20 | this.el().addEventListener('mouseenter', () => { | 26 | this.onMouseEnter = () => { |
21 | this.player().off('userinactive', userInactiveHandler) | 27 | this.player().off('userinactive', this.userInactiveHandler) |
22 | }) | 28 | } |
23 | 29 | ||
24 | this.el().addEventListener('mouseleave', () => { | 30 | this.onMouseLeave = () => { |
25 | this.player().one('userinactive', userInactiveHandler) | 31 | this.player().one('userinactive', this.userInactiveHandler) |
26 | }) | 32 | } |
27 | 33 | ||
28 | this.player().on('click', event => { | 34 | this.onPlayerCick = event => { |
29 | let current = event.target as HTMLElement | 35 | let current = event.target as HTMLElement |
30 | 36 | ||
31 | do { | 37 | do { |
@@ -40,14 +46,31 @@ class PlaylistMenu extends Component { | |||
40 | } while (current) | 46 | } while (current) |
41 | 47 | ||
42 | this.close() | 48 | this.close() |
43 | }) | 49 | } |
50 | |||
51 | this.el().addEventListener('mouseenter', this.onMouseEnter) | ||
52 | this.el().addEventListener('mouseleave', this.onMouseLeave) | ||
53 | |||
54 | this.player().on('click', this.onPlayerCick) | ||
55 | } | ||
56 | |||
57 | dispose () { | ||
58 | this.el().removeEventListener('mouseenter', this.onMouseEnter) | ||
59 | this.el().removeEventListener('mouseleave', this.onMouseLeave) | ||
60 | |||
61 | this.player().off('userinactive', this.userInactiveHandler) | ||
62 | this.player().off('click', this.onPlayerCick) | ||
63 | |||
64 | for (const item of this.menuItems) { | ||
65 | item.dispose() | ||
66 | } | ||
67 | |||
68 | super.dispose() | ||
44 | } | 69 | } |
45 | 70 | ||
46 | createEl () { | 71 | createEl () { |
47 | this.menuItems = [] | 72 | this.menuItems = [] |
48 | 73 | ||
49 | const options = this.getOptions() | ||
50 | |||
51 | const menu = super.createEl('div', { | 74 | const menu = super.createEl('div', { |
52 | className: 'vjs-playlist-menu', | 75 | className: 'vjs-playlist-menu', |
53 | innerHTML: '', | 76 | innerHTML: '', |
@@ -61,11 +84,11 @@ class PlaylistMenu extends Component { | |||
61 | const headerLeft = super.createEl('div') | 84 | const headerLeft = super.createEl('div') |
62 | 85 | ||
63 | const leftTitle = super.createEl('div', { | 86 | const leftTitle = super.createEl('div', { |
64 | innerHTML: options.playlist.displayName, | 87 | innerHTML: this.options_.playlist.displayName, |
65 | className: 'title' | 88 | className: 'title' |
66 | }) | 89 | }) |
67 | 90 | ||
68 | const playlistChannel = options.playlist.videoChannel | 91 | const playlistChannel = this.options_.playlist.videoChannel |
69 | const leftSubtitle = super.createEl('div', { | 92 | const leftSubtitle = super.createEl('div', { |
70 | innerHTML: playlistChannel | 93 | innerHTML: playlistChannel |
71 | ? this.player().localize('By {1}', [ playlistChannel.displayName ]) | 94 | ? this.player().localize('By {1}', [ playlistChannel.displayName ]) |
@@ -86,7 +109,7 @@ class PlaylistMenu extends Component { | |||
86 | 109 | ||
87 | const list = super.createEl('ol') | 110 | const list = super.createEl('ol') |
88 | 111 | ||
89 | for (const playlistElement of options.elements) { | 112 | for (const playlistElement of this.options_.elements) { |
90 | const item = new PlaylistMenuItem(this.player(), { | 113 | const item = new PlaylistMenuItem(this.player(), { |
91 | element: playlistElement, | 114 | element: playlistElement, |
92 | onClicked: () => this.onItemClicked(playlistElement) | 115 | onClicked: () => this.onItemClicked(playlistElement) |
@@ -100,13 +123,13 @@ class PlaylistMenu extends Component { | |||
100 | menu.appendChild(header) | 123 | menu.appendChild(header) |
101 | menu.appendChild(list) | 124 | menu.appendChild(list) |
102 | 125 | ||
126 | this.update() | ||
127 | |||
103 | return menu | 128 | return menu |
104 | } | 129 | } |
105 | 130 | ||
106 | update () { | 131 | update () { |
107 | const options = this.getOptions() | 132 | this.updateSelected(this.options_.getCurrentPosition()) |
108 | |||
109 | this.updateSelected(options.getCurrentPosition()) | ||
110 | } | 133 | } |
111 | 134 | ||
112 | open () { | 135 | open () { |
@@ -123,12 +146,8 @@ class PlaylistMenu extends Component { | |||
123 | } | 146 | } |
124 | } | 147 | } |
125 | 148 | ||
126 | private getOptions () { | ||
127 | return this.options_ as PlaylistPluginOptions | ||
128 | } | ||
129 | |||
130 | private onItemClicked (element: VideoPlaylistElement) { | 149 | private onItemClicked (element: VideoPlaylistElement) { |
131 | this.getOptions().onItemClicked(element) | 150 | this.options_.onItemClicked(element) |
132 | } | 151 | } |
133 | } | 152 | } |
134 | 153 | ||
diff --git a/client/src/assets/player/shared/playlist/playlist-plugin.ts b/client/src/assets/player/shared/playlist/playlist-plugin.ts index 44de0da5a..c00e45843 100644 --- a/client/src/assets/player/shared/playlist/playlist-plugin.ts +++ b/client/src/assets/player/shared/playlist/playlist-plugin.ts | |||
@@ -8,17 +8,10 @@ const Plugin = videojs.getPlugin('plugin') | |||
8 | class PlaylistPlugin extends Plugin { | 8 | class PlaylistPlugin extends Plugin { |
9 | private playlistMenu: PlaylistMenu | 9 | private playlistMenu: PlaylistMenu |
10 | private playlistButton: PlaylistButton | 10 | private playlistButton: PlaylistButton |
11 | private options: PlaylistPluginOptions | ||
12 | 11 | ||
13 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { | 12 | constructor (player: videojs.Player, options?: PlaylistPluginOptions) { |
14 | super(player, options) | 13 | super(player, options) |
15 | 14 | ||
16 | this.options = options | ||
17 | |||
18 | this.player.ready(() => { | ||
19 | player.addClass('vjs-playlist') | ||
20 | }) | ||
21 | |||
22 | this.playlistMenu = new PlaylistMenu(player, options) | 15 | this.playlistMenu = new PlaylistMenu(player, options) |
23 | this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu }) | 16 | this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu }) |
24 | 17 | ||
@@ -26,8 +19,16 @@ class PlaylistPlugin extends Plugin { | |||
26 | player.addChild(this.playlistButton, options) | 19 | player.addChild(this.playlistButton, options) |
27 | } | 20 | } |
28 | 21 | ||
29 | updateSelected () { | 22 | dispose () { |
30 | this.playlistMenu.updateSelected(this.options.getCurrentPosition()) | 23 | this.player.removeClass('vjs-playlist') |
24 | |||
25 | this.playlistMenu.dispose() | ||
26 | this.playlistButton.dispose() | ||
27 | |||
28 | this.player.removeChild(this.playlistMenu) | ||
29 | this.player.removeChild(this.playlistButton) | ||
30 | |||
31 | super.dispose() | ||
31 | } | 32 | } |
32 | } | 33 | } |
33 | 34 | ||
diff --git a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts index 4fafd27b1..4d6701003 100644 --- a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts +++ b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts | |||
@@ -8,7 +8,16 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
8 | private resolutions: PeerTubeResolution[] = [] | 8 | private resolutions: PeerTubeResolution[] = [] |
9 | 9 | ||
10 | private autoResolutionChosenId: number | 10 | private autoResolutionChosenId: number |
11 | private autoResolutionEnabled = true | 11 | |
12 | constructor (player: videojs.Player) { | ||
13 | super(player) | ||
14 | |||
15 | player.on('video-change', () => { | ||
16 | this.resolutions = [] | ||
17 | |||
18 | this.trigger('resolutions-removed') | ||
19 | }) | ||
20 | } | ||
12 | 21 | ||
13 | add (resolutions: PeerTubeResolution[]) { | 22 | add (resolutions: PeerTubeResolution[]) { |
14 | for (const r of resolutions) { | 23 | for (const r of resolutions) { |
@@ -18,12 +27,12 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
18 | this.currentSelection = this.getSelected() | 27 | this.currentSelection = this.getSelected() |
19 | 28 | ||
20 | this.sort() | 29 | this.sort() |
21 | this.trigger('resolutionsAdded') | 30 | this.trigger('resolutions-added') |
22 | } | 31 | } |
23 | 32 | ||
24 | remove (resolutionIndex: number) { | 33 | remove (resolutionIndex: number) { |
25 | this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex) | 34 | this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex) |
26 | this.trigger('resolutionRemoved') | 35 | this.trigger('resolutions-removed') |
27 | } | 36 | } |
28 | 37 | ||
29 | getResolutions () { | 38 | getResolutions () { |
@@ -40,10 +49,10 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
40 | 49 | ||
41 | select (options: { | 50 | select (options: { |
42 | id: number | 51 | id: number |
43 | byEngine: boolean | 52 | fireCallback: boolean |
44 | autoResolutionChosenId?: number | 53 | autoResolutionChosenId?: number |
45 | }) { | 54 | }) { |
46 | const { id, autoResolutionChosenId, byEngine } = options | 55 | const { id, autoResolutionChosenId, fireCallback } = options |
47 | 56 | ||
48 | if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return | 57 | if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return |
49 | 58 | ||
@@ -55,25 +64,11 @@ class PeerTubeResolutionsPlugin extends Plugin { | |||
55 | if (r.selected) { | 64 | if (r.selected) { |
56 | this.currentSelection = r | 65 | this.currentSelection = r |
57 | 66 | ||
58 | if (!byEngine) r.selectCallback() | 67 | if (fireCallback) r.selectCallback() |
59 | } | 68 | } |
60 | } | 69 | } |
61 | 70 | ||
62 | this.trigger('resolutionChanged') | 71 | this.trigger('resolutions-changed') |
63 | } | ||
64 | |||
65 | disableAutoResolution () { | ||
66 | this.autoResolutionEnabled = false | ||
67 | this.trigger('autoResolutionEnabledChanged') | ||
68 | } | ||
69 | |||
70 | enabledAutoResolution () { | ||
71 | this.autoResolutionEnabled = true | ||
72 | this.trigger('autoResolutionEnabledChanged') | ||
73 | } | ||
74 | |||
75 | isAutoResolutionEnabeld () { | ||
76 | return this.autoResolutionEnabled | ||
77 | } | 72 | } |
78 | 73 | ||
79 | private sort () { | 74 | private sort () { |
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts index 672411c11..c39894284 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-button.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts | |||
@@ -11,12 +11,12 @@ class ResolutionMenuButton extends MenuButton { | |||
11 | 11 | ||
12 | this.controlText('Quality') | 12 | this.controlText('Quality') |
13 | 13 | ||
14 | player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) | 14 | player.peertubeResolutions().on('resolutions-added', () => this.update()) |
15 | player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities()) | 15 | player.peertubeResolutions().on('resolutions-removed', () => this.update()) |
16 | 16 | ||
17 | // For parent | 17 | // For parent |
18 | player.peertubeResolutions().on('resolutionChanged', () => { | 18 | player.peertubeResolutions().on('resolutions-changed', () => { |
19 | setTimeout(() => this.trigger('labelUpdated')) | 19 | setTimeout(() => this.trigger('label-updated')) |
20 | }) | 20 | }) |
21 | } | 21 | } |
22 | 22 | ||
@@ -37,69 +37,42 @@ class ResolutionMenuButton extends MenuButton { | |||
37 | } | 37 | } |
38 | 38 | ||
39 | createMenu () { | 39 | createMenu () { |
40 | return new Menu(this.player_) | 40 | const menu: videojs.Menu = new Menu(this.player_, { menuButton: this }) |
41 | } | 41 | const resolutions = this.player().peertubeResolutions().getResolutions() |
42 | |||
43 | buildCSSClass () { | ||
44 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
45 | } | ||
46 | |||
47 | buildWrapperCSSClass () { | ||
48 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
49 | } | ||
50 | |||
51 | private addClickListener (component: any) { | ||
52 | component.on('click', () => { | ||
53 | const children = this.menu.children() | ||
54 | |||
55 | for (const child of children) { | ||
56 | if (component !== child) { | ||
57 | (child as videojs.MenuItem).selected(false) | ||
58 | } | ||
59 | } | ||
60 | }) | ||
61 | } | ||
62 | 42 | ||
63 | private buildQualities () { | 43 | for (const r of resolutions) { |
64 | for (const d of this.player().peertubeResolutions().getResolutions()) { | 44 | const label = r.label === '0p' |
65 | const label = d.label === '0p' | ||
66 | ? this.player().localize('Audio-only') | 45 | ? this.player().localize('Audio-only') |
67 | : d.label | 46 | : r.label |
68 | 47 | ||
69 | this.menu.addChild(new ResolutionMenuItem( | 48 | const component = new ResolutionMenuItem( |
70 | this.player_, | 49 | this.player_, |
71 | { | 50 | { |
72 | id: d.id + '', | 51 | id: r.id + '', |
73 | resolutionId: d.id, | 52 | resolutionId: r.id, |
74 | label, | 53 | label, |
75 | selected: d.selected | 54 | selected: r.selected |
76 | }) | 55 | } |
77 | ) | 56 | ) |
78 | } | ||
79 | 57 | ||
80 | for (const m of this.menu.children()) { | 58 | menu.addItem(component) |
81 | this.addClickListener(m) | ||
82 | } | 59 | } |
83 | 60 | ||
84 | this.trigger('menuChanged') | 61 | return menu |
85 | } | 62 | } |
86 | 63 | ||
87 | private cleanupQualities () { | 64 | update () { |
88 | const resolutions = this.player().peertubeResolutions().getResolutions() | 65 | super.update() |
89 | |||
90 | this.menu.children().forEach((children: ResolutionMenuItem) => { | ||
91 | if (children.resolutionId === undefined) { | ||
92 | return | ||
93 | } | ||
94 | 66 | ||
95 | if (resolutions.find(r => r.id === children.resolutionId)) { | 67 | this.trigger('menu-changed') |
96 | return | 68 | } |
97 | } | ||
98 | 69 | ||
99 | this.menu.removeChild(children) | 70 | buildCSSClass () { |
100 | }) | 71 | return super.buildCSSClass() + ' vjs-resolution-button' |
72 | } | ||
101 | 73 | ||
102 | this.trigger('menuChanged') | 74 | buildWrapperCSSClass () { |
75 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
103 | } | 76 | } |
104 | } | 77 | } |
105 | 78 | ||
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts index c59b8b891..86387f533 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-item.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts | |||
@@ -10,35 +10,32 @@ class ResolutionMenuItem extends MenuItem { | |||
10 | readonly resolutionId: number | 10 | readonly resolutionId: number |
11 | private readonly label: string | 11 | private readonly label: string |
12 | 12 | ||
13 | private autoResolutionEnabled: boolean | ||
14 | private autoResolutionChosen: string | 13 | private autoResolutionChosen: string |
15 | 14 | ||
16 | constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { | 15 | private updateSelectionHandler: () => void |
17 | options.selectable = true | ||
18 | 16 | ||
19 | super(player, options) | 17 | constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { |
18 | super(player, { ...options, selectable: true }) | ||
20 | 19 | ||
21 | this.autoResolutionEnabled = true | ||
22 | this.autoResolutionChosen = '' | 20 | this.autoResolutionChosen = '' |
23 | 21 | ||
24 | this.resolutionId = options.resolutionId | 22 | this.resolutionId = options.resolutionId |
25 | this.label = options.label | 23 | this.label = options.label |
26 | 24 | ||
27 | player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection()) | 25 | this.updateSelectionHandler = () => this.updateSelection() |
26 | player.peertubeResolutions().on('resolutions-changed', this.updateSelectionHandler) | ||
27 | } | ||
28 | |||
29 | dispose () { | ||
30 | this.player().peertubeResolutions().off('resolutions-changed', this.updateSelectionHandler) | ||
28 | 31 | ||
29 | // We only want to disable the "Auto" item | 32 | super.dispose() |
30 | if (this.resolutionId === -1) { | ||
31 | player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution()) | ||
32 | } | ||
33 | } | 33 | } |
34 | 34 | ||
35 | handleClick (event: any) { | 35 | handleClick (event: any) { |
36 | // Auto button disabled? | ||
37 | if (this.autoResolutionEnabled === false && this.resolutionId === -1) return | ||
38 | |||
39 | super.handleClick(event) | 36 | super.handleClick(event) |
40 | 37 | ||
41 | this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false }) | 38 | this.player().peertubeResolutions().select({ id: this.resolutionId, fireCallback: true }) |
42 | } | 39 | } |
43 | 40 | ||
44 | updateSelection () { | 41 | updateSelection () { |
@@ -51,19 +48,6 @@ class ResolutionMenuItem extends MenuItem { | |||
51 | this.selected(this.resolutionId === selectedResolution.id) | 48 | this.selected(this.resolutionId === selectedResolution.id) |
52 | } | 49 | } |
53 | 50 | ||
54 | updateAutoResolution () { | ||
55 | const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld() | ||
56 | |||
57 | // Check if the auto resolution is enabled or not | ||
58 | if (enabled === false) { | ||
59 | this.addClass('disabled') | ||
60 | } else { | ||
61 | this.removeClass('disabled') | ||
62 | } | ||
63 | |||
64 | this.autoResolutionEnabled = enabled | ||
65 | } | ||
66 | |||
67 | getLabel () { | 51 | getLabel () { |
68 | if (this.resolutionId === -1) { | 52 | if (this.resolutionId === -1) { |
69 | return this.label + ' <small>' + this.autoResolutionChosen + '</small>' | 53 | return this.label + ' <small>' + this.autoResolutionChosen + '</small>' |
diff --git a/client/src/assets/player/shared/settings/settings-dialog.ts b/client/src/assets/player/shared/settings/settings-dialog.ts index f5fbbe7ad..ba39d0f45 100644 --- a/client/src/assets/player/shared/settings/settings-dialog.ts +++ b/client/src/assets/player/shared/settings/settings-dialog.ts | |||
@@ -28,6 +28,18 @@ class SettingsDialog extends Component { | |||
28 | 'aria-describedby': dialogDescriptionId | 28 | 'aria-describedby': dialogDescriptionId |
29 | }) | 29 | }) |
30 | } | 30 | } |
31 | |||
32 | show () { | ||
33 | this.player().addClass('vjs-settings-dialog-opened') | ||
34 | |||
35 | super.show() | ||
36 | } | ||
37 | |||
38 | hide () { | ||
39 | this.player().removeClass('vjs-settings-dialog-opened') | ||
40 | |||
41 | super.hide() | ||
42 | } | ||
31 | } | 43 | } |
32 | 44 | ||
33 | Component.registerComponent('SettingsDialog', SettingsDialog) | 45 | Component.registerComponent('SettingsDialog', SettingsDialog) |
diff --git a/client/src/assets/player/shared/settings/settings-menu-button.ts b/client/src/assets/player/shared/settings/settings-menu-button.ts index 4cf29866b..9499a43eb 100644 --- a/client/src/assets/player/shared/settings/settings-menu-button.ts +++ b/client/src/assets/player/shared/settings/settings-menu-button.ts | |||
@@ -71,7 +71,7 @@ class SettingsButton extends Button { | |||
71 | } | 71 | } |
72 | } | 72 | } |
73 | 73 | ||
74 | onDisposeSettingsItem (event: any, name: string) { | 74 | onDisposeSettingsItem (_event: any, name: string) { |
75 | if (name === undefined) { | 75 | if (name === undefined) { |
76 | const children = this.menu.children() | 76 | const children = this.menu.children() |
77 | 77 | ||
@@ -103,6 +103,8 @@ class SettingsButton extends Button { | |||
103 | if (this.isInIframe()) { | 103 | if (this.isInIframe()) { |
104 | window.removeEventListener('blur', this.documentClickHandler) | 104 | window.removeEventListener('blur', this.documentClickHandler) |
105 | } | 105 | } |
106 | |||
107 | super.dispose() | ||
106 | } | 108 | } |
107 | 109 | ||
108 | onAddSettingsItem (event: any, data: any) { | 110 | onAddSettingsItem (event: any, data: any) { |
@@ -249,8 +251,8 @@ class SettingsButton extends Button { | |||
249 | } | 251 | } |
250 | 252 | ||
251 | resetChildren () { | 253 | resetChildren () { |
252 | for (const menuChild of this.menu.children()) { | 254 | for (const menuChild of this.menu.children() as SettingsMenuItem[]) { |
253 | (menuChild as SettingsMenuItem).reset() | 255 | menuChild.reset() |
254 | } | 256 | } |
255 | } | 257 | } |
256 | 258 | ||
@@ -258,8 +260,8 @@ class SettingsButton extends Button { | |||
258 | * Hide all the sub menus | 260 | * Hide all the sub menus |
259 | */ | 261 | */ |
260 | hideChildren () { | 262 | hideChildren () { |
261 | for (const menuChild of this.menu.children()) { | 263 | for (const menuChild of this.menu.children() as SettingsMenuItem[]) { |
262 | (menuChild as SettingsMenuItem).hideSubMenu() | 264 | menuChild.hideSubMenu() |
263 | } | 265 | } |
264 | } | 266 | } |
265 | 267 | ||
diff --git a/client/src/assets/player/shared/settings/settings-menu-item.ts b/client/src/assets/player/shared/settings/settings-menu-item.ts index 288e3b233..9916ae27f 100644 --- a/client/src/assets/player/shared/settings/settings-menu-item.ts +++ b/client/src/assets/player/shared/settings/settings-menu-item.ts | |||
@@ -70,17 +70,22 @@ class SettingsMenuItem extends MenuItem { | |||
70 | this.build() | 70 | this.build() |
71 | 71 | ||
72 | // Update on rate change | 72 | // Update on rate change |
73 | player.on('ratechange', this.submenuClickHandler) | 73 | if (subMenuName === 'PlaybackRateMenuButton') { |
74 | player.on('ratechange', this.submenuClickHandler) | ||
75 | } | ||
74 | 76 | ||
75 | if (subMenuName === 'CaptionsButton') { | 77 | if (subMenuName === 'CaptionsButton') { |
76 | // Hack to regenerate captions on HTTP fallback | 78 | player.on('captions-changed', () => { |
77 | player.on('captionsChanged', () => { | 79 | // Wait menu component rebuild |
78 | setTimeout(() => { | 80 | setTimeout(() => { |
79 | this.settingsSubMenuEl_.innerHTML = '' | 81 | this.rebuildAfterMenuChange() |
80 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | 82 | }, 150) |
81 | this.update() | 83 | }) |
82 | this.bindClickEvents() | 84 | } |
83 | }, 0) | 85 | |
86 | if (subMenuName === 'ResolutionMenuButton') { | ||
87 | this.subMenu.on('menu-changed', () => { | ||
88 | this.rebuildAfterMenuChange() | ||
84 | }) | 89 | }) |
85 | } | 90 | } |
86 | 91 | ||
@@ -89,6 +94,12 @@ class SettingsMenuItem extends MenuItem { | |||
89 | }) | 94 | }) |
90 | } | 95 | } |
91 | 96 | ||
97 | dispose () { | ||
98 | this.settingsSubMenuEl_.removeEventListener('transitionend', this.transitionEndHandler) | ||
99 | |||
100 | super.dispose() | ||
101 | } | ||
102 | |||
92 | eventHandlers () { | 103 | eventHandlers () { |
93 | this.submenuClickHandler = this.onSubmenuClick.bind(this) | 104 | this.submenuClickHandler = this.onSubmenuClick.bind(this) |
94 | this.transitionEndHandler = this.onTransitionEnd.bind(this) | 105 | this.transitionEndHandler = this.onTransitionEnd.bind(this) |
@@ -190,27 +201,6 @@ class SettingsMenuItem extends MenuItem { | |||
190 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) | 201 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) |
191 | } | 202 | } |
192 | 203 | ||
193 | /** | ||
194 | * Add/remove prefixed event listener for CSS Transition | ||
195 | * | ||
196 | * @method PrefixedEvent | ||
197 | */ | ||
198 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { | ||
199 | const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] | ||
200 | |||
201 | for (let p = 0; p < prefix.length; p++) { | ||
202 | if (!prefix[p]) { | ||
203 | type = type.toLowerCase() | ||
204 | } | ||
205 | |||
206 | if (action === 'addEvent') { | ||
207 | element.addEventListener(prefix[p] + type, callback, false) | ||
208 | } else if (action === 'removeEvent') { | ||
209 | element.removeEventListener(prefix[p] + type, callback, false) | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | |||
214 | onTransitionEnd (event: any) { | 204 | onTransitionEnd (event: any) { |
215 | if (event.propertyName !== 'margin-right') { | 205 | if (event.propertyName !== 'margin-right') { |
216 | return | 206 | return |
@@ -254,12 +244,7 @@ class SettingsMenuItem extends MenuItem { | |||
254 | } | 244 | } |
255 | 245 | ||
256 | build () { | 246 | build () { |
257 | this.subMenu.on('labelUpdated', () => { | 247 | this.subMenu.on('label-updated', () => { |
258 | this.update() | ||
259 | }) | ||
260 | this.subMenu.on('menuChanged', () => { | ||
261 | this.bindClickEvents() | ||
262 | this.setSize() | ||
263 | this.update() | 248 | this.update() |
264 | }) | 249 | }) |
265 | 250 | ||
@@ -272,25 +257,12 @@ class SettingsMenuItem extends MenuItem { | |||
272 | this.setSize() | 257 | this.setSize() |
273 | this.bindClickEvents() | 258 | this.bindClickEvents() |
274 | 259 | ||
275 | // prefixed event listeners for CSS TransitionEnd | 260 | this.settingsSubMenuEl_.addEventListener('transitionend', this.transitionEndHandler, false) |
276 | this.PrefixedEvent( | ||
277 | this.settingsSubMenuEl_, | ||
278 | 'TransitionEnd', | ||
279 | this.transitionEndHandler, | ||
280 | 'addEvent' | ||
281 | ) | ||
282 | } | 261 | } |
283 | 262 | ||
284 | update (event?: any) { | 263 | update (event?: any) { |
285 | let target: HTMLElement = null | ||
286 | const subMenu = this.subMenu.name() | 264 | const subMenu = this.subMenu.name() |
287 | 265 | ||
288 | if (event && event.type === 'tap') { | ||
289 | target = event.target | ||
290 | } else if (event) { | ||
291 | target = event.currentTarget | ||
292 | } | ||
293 | |||
294 | // Playback rate menu button doesn't get a vjs-selected class | 266 | // Playback rate menu button doesn't get a vjs-selected class |
295 | // or sets options_['selected'] on the selected playback rate. | 267 | // or sets options_['selected'] on the selected playback rate. |
296 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | 268 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton |
@@ -321,6 +293,13 @@ class SettingsMenuItem extends MenuItem { | |||
321 | } | 293 | } |
322 | } | 294 | } |
323 | 295 | ||
296 | let target: HTMLElement = null | ||
297 | if (event && event.type === 'tap') { | ||
298 | target = event.target | ||
299 | } else if (event) { | ||
300 | target = event.currentTarget | ||
301 | } | ||
302 | |||
324 | if (target && !target.classList.contains('vjs-back-button')) { | 303 | if (target && !target.classList.contains('vjs-back-button')) { |
325 | this.settingsButton.hideDialog() | 304 | this.settingsButton.hideDialog() |
326 | } | 305 | } |
@@ -369,6 +348,15 @@ class SettingsMenuItem extends MenuItem { | |||
369 | } | 348 | } |
370 | } | 349 | } |
371 | 350 | ||
351 | private rebuildAfterMenuChange () { | ||
352 | this.settingsSubMenuEl_.innerHTML = '' | ||
353 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | ||
354 | this.update() | ||
355 | this.createBackButton() | ||
356 | this.setSize() | ||
357 | this.bindClickEvents() | ||
358 | } | ||
359 | |||
372 | } | 360 | } |
373 | 361 | ||
374 | (SettingsMenuItem as any).prototype.contentElType = 'button' | 362 | (SettingsMenuItem as any).prototype.contentElType = 'button' |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index 471a5e46c..fad68cec9 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -7,7 +7,7 @@ import { bytes } from '../common' | |||
7 | interface StatsCardOptions extends videojs.ComponentOptions { | 7 | interface StatsCardOptions extends videojs.ComponentOptions { |
8 | videoUUID: string | 8 | videoUUID: string |
9 | videoIsLive: boolean | 9 | videoIsLive: boolean |
10 | mode: 'webtorrent' | 'p2p-media-loader' | 10 | mode: 'web-video' | 'p2p-media-loader' |
11 | p2pEnabled: boolean | 11 | p2pEnabled: boolean |
12 | } | 12 | } |
13 | 13 | ||
@@ -34,7 +34,7 @@ class StatsCard extends Component { | |||
34 | 34 | ||
35 | updateInterval: any | 35 | updateInterval: any |
36 | 36 | ||
37 | mode: 'webtorrent' | 'p2p-media-loader' | 37 | mode: 'web-video' | 'p2p-media-loader' |
38 | 38 | ||
39 | metadataStore: any = {} | 39 | metadataStore: any = {} |
40 | 40 | ||
@@ -63,6 +63,9 @@ class StatsCard extends Component { | |||
63 | 63 | ||
64 | private liveLatency: InfoElement | 64 | private liveLatency: InfoElement |
65 | 65 | ||
66 | private onP2PInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void | ||
67 | private onHTTPInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void | ||
68 | |||
66 | createEl () { | 69 | createEl () { |
67 | this.containerEl = videojs.dom.createEl('div', { | 70 | this.containerEl = videojs.dom.createEl('div', { |
68 | className: 'vjs-stats-content' | 71 | className: 'vjs-stats-content' |
@@ -86,9 +89,7 @@ class StatsCard extends Component { | |||
86 | 89 | ||
87 | this.populateInfoBlocks() | 90 | this.populateInfoBlocks() |
88 | 91 | ||
89 | this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => { | 92 | this.onP2PInfoHandler = (_event, data) => { |
90 | if (!data) return // HTTP fallback | ||
91 | |||
92 | this.mode = data.source | 93 | this.mode = data.source |
93 | 94 | ||
94 | const p2pStats = data.p2p | 95 | const p2pStats = data.p2p |
@@ -105,11 +106,29 @@ class StatsCard extends Component { | |||
105 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') | 106 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') |
106 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | 107 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') |
107 | } | 108 | } |
108 | }) | 109 | } |
110 | |||
111 | this.onHTTPInfoHandler = (_event, data) => { | ||
112 | this.mode = data.source | ||
113 | |||
114 | this.playerNetworkInfo.totalDownloaded = bytes(data.http.downloaded).join(' ') | ||
115 | } | ||
116 | |||
117 | this.player().on('p2p-info', this.onP2PInfoHandler) | ||
118 | this.player().on('http-info', this.onHTTPInfoHandler) | ||
109 | 119 | ||
110 | return this.containerEl | 120 | return this.containerEl |
111 | } | 121 | } |
112 | 122 | ||
123 | dispose () { | ||
124 | if (this.updateInterval) clearInterval(this.updateInterval) | ||
125 | |||
126 | this.player().off('p2p-info', this.onP2PInfoHandler) | ||
127 | this.player().off('http-info', this.onHTTPInfoHandler) | ||
128 | |||
129 | super.dispose() | ||
130 | } | ||
131 | |||
113 | toggle () { | 132 | toggle () { |
114 | if (this.updateInterval) this.hide() | 133 | if (this.updateInterval) this.hide() |
115 | else this.show() | 134 | else this.show() |
@@ -122,7 +141,7 @@ class StatsCard extends Component { | |||
122 | try { | 141 | try { |
123 | const options = this.mode === 'p2p-media-loader' | 142 | const options = this.mode === 'p2p-media-loader' |
124 | ? this.buildHLSOptions() | 143 | ? this.buildHLSOptions() |
125 | : await this.buildWebTorrentOptions() // Default | 144 | : await this.buildWebVideoOptions() // Default |
126 | 145 | ||
127 | this.populateInfoValues(options) | 146 | this.populateInfoValues(options) |
128 | } catch (err) { | 147 | } catch (err) { |
@@ -170,8 +189,8 @@ class StatsCard extends Component { | |||
170 | } | 189 | } |
171 | } | 190 | } |
172 | 191 | ||
173 | private async buildWebTorrentOptions () { | 192 | private async buildWebVideoOptions () { |
174 | const videoFile = this.player_.webtorrent().getCurrentVideoFile() | 193 | const videoFile = this.player_.webVideo().getCurrentVideoFile() |
175 | 194 | ||
176 | if (!this.metadataStore[videoFile.fileUrl]) { | 195 | if (!this.metadataStore[videoFile.fileUrl]) { |
177 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) | 196 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) |
@@ -194,7 +213,7 @@ class StatsCard extends Component { | |||
194 | 213 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 214 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player_.buffered()) | 215 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 216 | const progress = this.player_.bufferedPercent() |
198 | 217 | ||
199 | return { | 218 | return { |
200 | playerNetworkInfo: this.playerNetworkInfo, | 219 | playerNetworkInfo: this.playerNetworkInfo, |
@@ -284,8 +303,10 @@ class StatsCard extends Component { | |||
284 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` | 303 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` |
285 | : undefined | 304 | : undefined |
286 | 305 | ||
287 | this.setInfoValue(this.playerMode, this.mode || 'HTTP') | 306 | const p2pEnabled = this.options_.p2pEnabled && this.mode === 'p2p-media-loader' |
288 | this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled')) | 307 | |
308 | this.setInfoValue(this.playerMode, this.mode) | ||
309 | this.setInfoValue(this.p2p, player.localize(p2pEnabled ? 'enabled' : 'disabled')) | ||
289 | this.setInfoValue(this.uuid, this.options_.videoUUID) | 310 | this.setInfoValue(this.uuid, this.options_.videoUUID) |
290 | 311 | ||
291 | this.setInfoValue(this.viewport, frames) | 312 | this.setInfoValue(this.viewport, frames) |
diff --git a/client/src/assets/player/shared/stats/stats-plugin.ts b/client/src/assets/player/shared/stats/stats-plugin.ts index 8aad80e8a..86684a78c 100644 --- a/client/src/assets/player/shared/stats/stats-plugin.ts +++ b/client/src/assets/player/shared/stats/stats-plugin.ts | |||
@@ -7,10 +7,6 @@ class StatsForNerdsPlugin extends Plugin { | |||
7 | private statsCard: StatsCard | 7 | private statsCard: StatsCard |
8 | 8 | ||
9 | constructor (player: videojs.Player, options: StatsCardOptions) { | 9 | constructor (player: videojs.Player, options: StatsCardOptions) { |
10 | const settings = { | ||
11 | ...options | ||
12 | } | ||
13 | |||
14 | super(player) | 10 | super(player) |
15 | 11 | ||
16 | this.player.ready(() => { | 12 | this.player.ready(() => { |
@@ -19,7 +15,17 @@ class StatsForNerdsPlugin extends Plugin { | |||
19 | 15 | ||
20 | this.statsCard = new StatsCard(player, options) | 16 | this.statsCard = new StatsCard(player, options) |
21 | 17 | ||
22 | player.addChild(this.statsCard, settings) | 18 | // Copy options |
19 | player.addChild(this.statsCard) | ||
20 | } | ||
21 | |||
22 | dispose () { | ||
23 | if (this.statsCard) { | ||
24 | this.statsCard.dispose() | ||
25 | this.player.removeChild(this.statsCard) | ||
26 | } | ||
27 | |||
28 | super.dispose() | ||
23 | } | 29 | } |
24 | 30 | ||
25 | show () { | 31 | show () { |
diff --git a/client/src/assets/player/shared/upnext/end-card.ts b/client/src/assets/player/shared/upnext/end-card.ts index 61668e407..3589e1fd8 100644 --- a/client/src/assets/player/shared/upnext/end-card.ts +++ b/client/src/assets/player/shared/upnext/end-card.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { UpNextPluginOptions } from '../../types' | ||
2 | 3 | ||
3 | function getMainTemplate (options: any) { | 4 | function getMainTemplate (options: EndCardOptions) { |
4 | return ` | 5 | return ` |
5 | <div class="vjs-upnext-top"> | 6 | <div class="vjs-upnext-top"> |
6 | <span class="vjs-upnext-headtext">${options.headText}</span> | 7 | <span class="vjs-upnext-headtext">${options.headText}</span> |
@@ -23,15 +24,10 @@ function getMainTemplate (options: any) { | |||
23 | ` | 24 | ` |
24 | } | 25 | } |
25 | 26 | ||
26 | export interface EndCardOptions extends videojs.ComponentOptions { | 27 | export interface EndCardOptions extends videojs.ComponentOptions, UpNextPluginOptions { |
27 | next: () => void | ||
28 | getTitle: () => string | ||
29 | timeout: number | ||
30 | cancelText: string | 28 | cancelText: string |
31 | headText: string | 29 | headText: string |
32 | suspendedText: string | 30 | suspendedText: string |
33 | condition: () => boolean | ||
34 | suspended: () => boolean | ||
35 | } | 31 | } |
36 | 32 | ||
37 | const Component = videojs.getComponent('Component') | 33 | const Component = videojs.getComponent('Component') |
@@ -52,27 +48,43 @@ class EndCard extends Component { | |||
52 | suspendedMessage: HTMLElement | 48 | suspendedMessage: HTMLElement |
53 | nextButton: HTMLElement | 49 | nextButton: HTMLElement |
54 | 50 | ||
51 | private onEndedHandler: () => void | ||
52 | private onPlayingHandler: () => void | ||
53 | |||
55 | constructor (player: videojs.Player, options: EndCardOptions) { | 54 | constructor (player: videojs.Player, options: EndCardOptions) { |
56 | super(player, options) | 55 | super(player, options) |
57 | 56 | ||
58 | this.totalTicks = this.options_.timeout / this.interval | 57 | this.totalTicks = this.options_.timeout / this.interval |
59 | 58 | ||
60 | player.on('ended', (_: any) => { | 59 | this.onEndedHandler = () => { |
61 | if (!this.options_.condition()) return | 60 | if (!this.options_.isDisplayed()) return |
62 | 61 | ||
63 | player.addClass('vjs-upnext--showing') | 62 | player.addClass('vjs-upnext--showing') |
64 | this.showCard((canceled: boolean) => { | 63 | |
64 | this.showCard(canceled => { | ||
65 | player.removeClass('vjs-upnext--showing') | 65 | player.removeClass('vjs-upnext--showing') |
66 | |||
66 | this.container.style.display = 'none' | 67 | this.container.style.display = 'none' |
68 | |||
67 | if (!canceled) { | 69 | if (!canceled) { |
68 | this.options_.next() | 70 | this.options_.next() |
69 | } | 71 | } |
70 | }) | 72 | }) |
71 | }) | 73 | } |
72 | 74 | ||
73 | player.on('playing', () => { | 75 | this.onPlayingHandler = () => { |
74 | this.upNextEvents.trigger('playing') | 76 | this.upNextEvents.trigger('playing') |
75 | }) | 77 | } |
78 | |||
79 | player.on([ 'auto-stopped', 'ended' ], this.onEndedHandler) | ||
80 | player.on('playing', this.onPlayingHandler) | ||
81 | } | ||
82 | |||
83 | dispose () { | ||
84 | if (this.onEndedHandler) this.player().off([ 'auto-stopped', 'ended' ], this.onEndedHandler) | ||
85 | if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler) | ||
86 | |||
87 | super.dispose() | ||
76 | } | 88 | } |
77 | 89 | ||
78 | createEl () { | 90 | createEl () { |
@@ -101,7 +113,7 @@ class EndCard extends Component { | |||
101 | return container | 113 | return container |
102 | } | 114 | } |
103 | 115 | ||
104 | showCard (cb: (value: boolean) => void) { | 116 | showCard (cb: (canceled: boolean) => void) { |
105 | let timeout: any | 117 | let timeout: any |
106 | 118 | ||
107 | this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`) | 119 | this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`) |
@@ -109,6 +121,10 @@ class EndCard extends Component { | |||
109 | 121 | ||
110 | this.title.innerHTML = this.options_.getTitle() | 122 | this.title.innerHTML = this.options_.getTitle() |
111 | 123 | ||
124 | if (this.totalTicks === 0) { | ||
125 | return cb(false) | ||
126 | } | ||
127 | |||
112 | this.upNextEvents.one('cancel', () => { | 128 | this.upNextEvents.one('cancel', () => { |
113 | clearTimeout(timeout) | 129 | clearTimeout(timeout) |
114 | cb(true) | 130 | cb(true) |
@@ -134,7 +150,7 @@ class EndCard extends Component { | |||
134 | } | 150 | } |
135 | 151 | ||
136 | const update = () => { | 152 | const update = () => { |
137 | if (this.options_.suspended()) { | 153 | if (this.options_.isSuspended()) { |
138 | this.suspendedMessage.innerText = this.options_.suspendedText | 154 | this.suspendedMessage.innerText = this.options_.suspendedText |
139 | goToPercent(0) | 155 | goToPercent(0) |
140 | this.ticks = 0 | 156 | this.ticks = 0 |
diff --git a/client/src/assets/player/shared/upnext/upnext-plugin.ts b/client/src/assets/player/shared/upnext/upnext-plugin.ts index e12e8c503..0badcd68c 100644 --- a/client/src/assets/player/shared/upnext/upnext-plugin.ts +++ b/client/src/assets/player/shared/upnext/upnext-plugin.ts | |||
@@ -1,27 +1,25 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { UpNextPluginOptions } from '../../types' | ||
2 | import { EndCardOptions } from './end-card' | 3 | import { EndCardOptions } from './end-card' |
3 | 4 | ||
4 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
5 | 6 | ||
6 | class UpNextPlugin extends Plugin { | 7 | class UpNextPlugin extends Plugin { |
7 | 8 | ||
8 | constructor (player: videojs.Player, options: Partial<EndCardOptions> = {}) { | 9 | constructor (player: videojs.Player, options: UpNextPluginOptions) { |
9 | const settings = { | 10 | super(player) |
11 | |||
12 | const settings: EndCardOptions = { | ||
10 | next: options.next, | 13 | next: options.next, |
11 | getTitle: options.getTitle, | 14 | getTitle: options.getTitle, |
12 | timeout: options.timeout || 5000, | 15 | timeout: options.timeout, |
13 | cancelText: options.cancelText || 'Cancel', | 16 | cancelText: player.localize('Cancel'), |
14 | headText: options.headText || 'Up Next', | 17 | headText: player.localize('Up Next'), |
15 | suspendedText: options.suspendedText || 'Autoplay is suspended', | 18 | suspendedText: player.localize('Autoplay is suspended'), |
16 | condition: options.condition, | 19 | isDisplayed: options.isDisplayed, |
17 | suspended: options.suspended | 20 | isSuspended: options.isSuspended |
18 | } | 21 | } |
19 | 22 | ||
20 | super(player) | ||
21 | |||
22 | // UpNext plugin can be called later, so ensure the player is not disposed | ||
23 | if (this.player.isDisposed()) return | ||
24 | |||
25 | this.player.ready(() => { | 23 | this.player.ready(() => { |
26 | player.addClass('vjs-upnext') | 24 | player.addClass('vjs-upnext') |
27 | }) | 25 | }) |
diff --git a/client/src/assets/player/shared/web-video/web-video-plugin.ts b/client/src/assets/player/shared/web-video/web-video-plugin.ts new file mode 100644 index 000000000..80e56795b --- /dev/null +++ b/client/src/assets/player/shared/web-video/web-video-plugin.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | import debug from 'debug' | ||
2 | import videojs from 'video.js' | ||
3 | import { logger } from '@root-helpers/logger' | ||
4 | import { addQueryParams } from '@shared/core-utils' | ||
5 | import { VideoFile } from '@shared/models' | ||
6 | import { PeerTubeResolution, PlayerNetworkInfo, WebVideoPluginOptions } from '../../types' | ||
7 | |||
8 | const debugLogger = debug('peertube:player:web-video-plugin') | ||
9 | |||
10 | const Plugin = videojs.getPlugin('plugin') | ||
11 | |||
12 | class WebVideoPlugin extends Plugin { | ||
13 | private readonly videoFiles: VideoFile[] | ||
14 | |||
15 | private currentVideoFile: VideoFile | ||
16 | private videoFileToken: () => string | ||
17 | |||
18 | private networkInfoInterval: any | ||
19 | |||
20 | private onErrorHandler: () => void | ||
21 | private onPlayHandler: () => void | ||
22 | |||
23 | constructor (player: videojs.Player, options?: WebVideoPluginOptions) { | ||
24 | super(player, options) | ||
25 | |||
26 | this.videoFiles = options.videoFiles | ||
27 | this.videoFileToken = options.videoFileToken | ||
28 | |||
29 | this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false }) | ||
30 | |||
31 | player.ready(() => { | ||
32 | this.buildQualities() | ||
33 | |||
34 | this.setupNetworkInfoInterval() | ||
35 | |||
36 | if (this.videoFiles.length === 0) { | ||
37 | this.player.addClass('disabled') | ||
38 | return | ||
39 | } | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | dispose () { | ||
44 | clearInterval(this.networkInfoInterval) | ||
45 | |||
46 | if (this.onErrorHandler) this.player.off('error', this.onErrorHandler) | ||
47 | if (this.onPlayHandler) this.player.off('canplay', this.onPlayHandler) | ||
48 | |||
49 | super.dispose() | ||
50 | } | ||
51 | |||
52 | getCurrentResolutionId () { | ||
53 | return this.currentVideoFile.resolution.id | ||
54 | } | ||
55 | |||
56 | updateVideoFile (options: { | ||
57 | videoFile: VideoFile | ||
58 | isUserResolutionChange: boolean | ||
59 | }) { | ||
60 | this.currentVideoFile = options.videoFile | ||
61 | |||
62 | debugLogger('Updating web video file to ' + this.currentVideoFile.fileUrl) | ||
63 | |||
64 | const paused = this.player.paused() | ||
65 | const playbackRate = this.player.playbackRate() | ||
66 | const currentTime = this.player.currentTime() | ||
67 | |||
68 | // Enable error display now this is our last fallback | ||
69 | this.onErrorHandler = () => this.player.peertube().displayFatalError() | ||
70 | this.player.one('error', this.onErrorHandler) | ||
71 | |||
72 | let httpUrl = this.currentVideoFile.fileUrl | ||
73 | |||
74 | if (this.videoFileToken()) { | ||
75 | httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) | ||
76 | } | ||
77 | |||
78 | const oldAutoplayValue = this.player.autoplay() | ||
79 | if (options.isUserResolutionChange) { | ||
80 | this.player.autoplay(false) | ||
81 | this.player.addClass('vjs-updating-resolution') | ||
82 | } | ||
83 | |||
84 | this.player.src(httpUrl) | ||
85 | |||
86 | this.onPlayHandler = () => { | ||
87 | this.player.playbackRate(playbackRate) | ||
88 | this.player.currentTime(currentTime) | ||
89 | |||
90 | this.adaptPosterForAudioOnly() | ||
91 | |||
92 | if (options.isUserResolutionChange) { | ||
93 | this.player.trigger('user-resolution-change') | ||
94 | this.player.trigger('web-video-source-change') | ||
95 | |||
96 | this.tryToPlay() | ||
97 | .then(() => { | ||
98 | if (paused) this.player.pause() | ||
99 | |||
100 | this.player.autoplay(oldAutoplayValue) | ||
101 | }) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | this.player.one('canplay', this.onPlayHandler) | ||
106 | } | ||
107 | |||
108 | getCurrentVideoFile () { | ||
109 | return this.currentVideoFile | ||
110 | } | ||
111 | |||
112 | private adaptPosterForAudioOnly () { | ||
113 | // Audio-only (resolutionId === 0) gets special treatment | ||
114 | if (this.currentVideoFile.resolution.id === 0) { | ||
115 | this.player.audioPosterMode(true) | ||
116 | } else { | ||
117 | this.player.audioPosterMode(false) | ||
118 | } | ||
119 | } | ||
120 | |||
121 | private tryToPlay () { | ||
122 | debugLogger('Try to play manually the video') | ||
123 | |||
124 | const playPromise = this.player.play() | ||
125 | if (playPromise === undefined) return | ||
126 | |||
127 | return playPromise | ||
128 | .catch((err: Error) => { | ||
129 | if (err.message.includes('The play() request was interrupted by a call to pause()')) { | ||
130 | return | ||
131 | } | ||
132 | |||
133 | logger.warn(err) | ||
134 | this.player.pause() | ||
135 | this.player.posterImage.show() | ||
136 | this.player.removeClass('vjs-has-autoplay') | ||
137 | this.player.removeClass('vjs-playing-audio-only-content') | ||
138 | }) | ||
139 | .finally(() => { | ||
140 | this.player.removeClass('vjs-updating-resolution') | ||
141 | }) | ||
142 | } | ||
143 | |||
144 | private pickAverageVideoFile () { | ||
145 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
146 | |||
147 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
148 | return files[Math.floor(files.length / 2)] | ||
149 | } | ||
150 | |||
151 | private buildQualities () { | ||
152 | const resolutions: PeerTubeResolution[] = this.videoFiles.map(videoFile => ({ | ||
153 | id: videoFile.resolution.id, | ||
154 | label: this.buildQualityLabel(videoFile), | ||
155 | height: videoFile.resolution.id, | ||
156 | selected: videoFile.id === this.currentVideoFile.id, | ||
157 | selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true }) | ||
158 | })) | ||
159 | |||
160 | this.player.peertubeResolutions().add(resolutions) | ||
161 | } | ||
162 | |||
163 | private buildQualityLabel (file: VideoFile) { | ||
164 | let label = file.resolution.label | ||
165 | |||
166 | if (file.fps && file.fps >= 50) { | ||
167 | label += file.fps | ||
168 | } | ||
169 | |||
170 | return label | ||
171 | } | ||
172 | |||
173 | private setupNetworkInfoInterval () { | ||
174 | this.networkInfoInterval = setInterval(() => { | ||
175 | return this.player.trigger('http-info', { | ||
176 | source: 'web-video', | ||
177 | http: { | ||
178 | downloaded: this.player.bufferedPercent() * this.currentVideoFile.size | ||
179 | } | ||
180 | } as PlayerNetworkInfo) | ||
181 | }, 1000) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | videojs.registerPlugin('webVideo', WebVideoPlugin) | ||
186 | export { WebVideoPlugin } | ||
diff --git a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts b/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts deleted file mode 100644 index 74ae17704..000000000 --- a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts +++ /dev/null | |||
@@ -1,234 +0,0 @@ | |||
1 | // From https://github.com/MinEduTDF/idb-chunk-store | ||
2 | // We use temporary IndexDB (all data are removed on destroy) to avoid RAM issues | ||
3 | // Thanks @santiagogil and @Feross | ||
4 | |||
5 | import Dexie from 'dexie' | ||
6 | import { EventEmitter } from 'events' | ||
7 | import { logger } from '@root-helpers/logger' | ||
8 | |||
9 | class ChunkDatabase extends Dexie { | ||
10 | chunks: Dexie.Table<{ id: number, buf: Buffer }, number> | ||
11 | |||
12 | constructor (dbname: string) { | ||
13 | super(dbname) | ||
14 | |||
15 | this.version(1).stores({ | ||
16 | chunks: 'id' | ||
17 | }) | ||
18 | } | ||
19 | } | ||
20 | |||
21 | class ExpirationDatabase extends Dexie { | ||
22 | databases: Dexie.Table<{ name: string, expiration: number }, number> | ||
23 | |||
24 | constructor () { | ||
25 | super('webtorrent-expiration') | ||
26 | |||
27 | this.version(1).stores({ | ||
28 | databases: 'name,expiration' | ||
29 | }) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | export class PeertubeChunkStore extends EventEmitter { | ||
34 | private static readonly BUFFERING_PUT_MS = 1000 | ||
35 | private static readonly CLEANER_INTERVAL_MS = 1000 * 60 // 1 minute | ||
36 | private static readonly CLEANER_EXPIRATION_MS = 1000 * 60 * 5 // 5 minutes | ||
37 | |||
38 | chunkLength: number | ||
39 | |||
40 | private pendingPut: { id: number, buf: Buffer, cb: (err?: Error) => void }[] = [] | ||
41 | // If the store is full | ||
42 | private memoryChunks: { [ id: number ]: Buffer | true } = {} | ||
43 | private databaseName: string | ||
44 | private putBulkTimeout: any | ||
45 | private cleanerInterval: any | ||
46 | private db: ChunkDatabase | ||
47 | private expirationDB: ExpirationDatabase | ||
48 | private readonly length: number | ||
49 | private readonly lastChunkLength: number | ||
50 | private readonly lastChunkIndex: number | ||
51 | |||
52 | constructor (chunkLength: number, opts: any) { | ||
53 | super() | ||
54 | |||
55 | this.databaseName = 'webtorrent-chunks-' | ||
56 | |||
57 | if (!opts) opts = {} | ||
58 | if (opts.torrent?.infoHash) this.databaseName += opts.torrent.infoHash | ||
59 | else this.databaseName += '-default' | ||
60 | |||
61 | this.setMaxListeners(100) | ||
62 | |||
63 | this.chunkLength = Number(chunkLength) | ||
64 | if (!this.chunkLength) throw new Error('First argument must be a chunk length') | ||
65 | |||
66 | this.length = Number(opts.length) || Infinity | ||
67 | |||
68 | if (this.length !== Infinity) { | ||
69 | this.lastChunkLength = (this.length % this.chunkLength) || this.chunkLength | ||
70 | this.lastChunkIndex = Math.ceil(this.length / this.chunkLength) - 1 | ||
71 | } | ||
72 | |||
73 | this.db = new ChunkDatabase(this.databaseName) | ||
74 | // Track databases that expired | ||
75 | this.expirationDB = new ExpirationDatabase() | ||
76 | |||
77 | this.runCleaner() | ||
78 | } | ||
79 | |||
80 | put (index: number, buf: Buffer, cb: (err?: Error) => void) { | ||
81 | const isLastChunk = (index === this.lastChunkIndex) | ||
82 | if (isLastChunk && buf.length !== this.lastChunkLength) { | ||
83 | return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength)) | ||
84 | } | ||
85 | if (!isLastChunk && buf.length !== this.chunkLength) { | ||
86 | return this.nextTick(cb, new Error('Chunk length must be ' + this.chunkLength)) | ||
87 | } | ||
88 | |||
89 | // Specify we have this chunk | ||
90 | this.memoryChunks[index] = true | ||
91 | |||
92 | // Add it to the pending put | ||
93 | this.pendingPut.push({ id: index, buf, cb }) | ||
94 | // If it's already planned, return | ||
95 | if (this.putBulkTimeout) return | ||
96 | |||
97 | // Plan a future bulk insert | ||
98 | this.putBulkTimeout = setTimeout(async () => { | ||
99 | const processing = this.pendingPut | ||
100 | this.pendingPut = [] | ||
101 | this.putBulkTimeout = undefined | ||
102 | |||
103 | try { | ||
104 | await this.db.transaction('rw', this.db.chunks, () => { | ||
105 | return this.db.chunks.bulkPut(processing.map(p => ({ id: p.id, buf: p.buf }))) | ||
106 | }) | ||
107 | } catch (err) { | ||
108 | logger.info('Cannot bulk insert chunks. Store them in memory.', err) | ||
109 | |||
110 | processing.forEach(p => { | ||
111 | this.memoryChunks[p.id] = p.buf | ||
112 | }) | ||
113 | } finally { | ||
114 | processing.forEach(p => p.cb()) | ||
115 | } | ||
116 | }, PeertubeChunkStore.BUFFERING_PUT_MS) | ||
117 | } | ||
118 | |||
119 | get (index: number, opts: any, cb: (err?: Error, buf?: Buffer) => void): void { | ||
120 | if (typeof opts === 'function') return this.get(index, null, opts) | ||
121 | |||
122 | // IndexDB could be slow, use our memory index first | ||
123 | const memoryChunk = this.memoryChunks[index] | ||
124 | if (memoryChunk === undefined) { | ||
125 | const err = new Error('Chunk not found') as any | ||
126 | err['notFound'] = true | ||
127 | |||
128 | return process.nextTick(() => cb(err)) | ||
129 | } | ||
130 | |||
131 | // Chunk in memory | ||
132 | if (memoryChunk !== true) return cb(null, memoryChunk) | ||
133 | |||
134 | // Chunk in store | ||
135 | this.db.transaction('r', this.db.chunks, async () => { | ||
136 | const result = await this.db.chunks.get({ id: index }) | ||
137 | if (result === undefined) return cb(null, Buffer.alloc(0)) | ||
138 | |||
139 | const buf = result.buf | ||
140 | if (!opts) return this.nextTick(cb, null, buf) | ||
141 | |||
142 | const offset = opts.offset || 0 | ||
143 | const len = opts.length || (buf.length - offset) | ||
144 | return cb(null, buf.slice(offset, len + offset)) | ||
145 | }) | ||
146 | .catch(err => { | ||
147 | logger.error(err) | ||
148 | return cb(err) | ||
149 | }) | ||
150 | } | ||
151 | |||
152 | close (cb: (err?: Error) => void) { | ||
153 | return this.destroy(cb) | ||
154 | } | ||
155 | |||
156 | async destroy (cb: (err?: Error) => void) { | ||
157 | try { | ||
158 | if (this.pendingPut) { | ||
159 | clearTimeout(this.putBulkTimeout) | ||
160 | this.pendingPut = null | ||
161 | } | ||
162 | if (this.cleanerInterval) { | ||
163 | clearInterval(this.cleanerInterval) | ||
164 | this.cleanerInterval = null | ||
165 | } | ||
166 | |||
167 | if (this.db) { | ||
168 | this.db.close() | ||
169 | |||
170 | await this.dropDatabase(this.databaseName) | ||
171 | } | ||
172 | |||
173 | if (this.expirationDB) { | ||
174 | this.expirationDB.close() | ||
175 | this.expirationDB = null | ||
176 | } | ||
177 | |||
178 | return cb() | ||
179 | } catch (err) { | ||
180 | logger.error('Cannot destroy peertube chunk store.', err) | ||
181 | return cb(err) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | private runCleaner () { | ||
186 | this.checkExpiration() | ||
187 | |||
188 | this.cleanerInterval = setInterval(() => { | ||
189 | this.checkExpiration() | ||
190 | }, PeertubeChunkStore.CLEANER_INTERVAL_MS) | ||
191 | } | ||
192 | |||
193 | private async checkExpiration () { | ||
194 | let databasesToDeleteInfo: { name: string }[] = [] | ||
195 | |||
196 | try { | ||
197 | await this.expirationDB.transaction('rw', this.expirationDB.databases, async () => { | ||
198 | // Update our database expiration since we are alive | ||
199 | await this.expirationDB.databases.put({ | ||
200 | name: this.databaseName, | ||
201 | expiration: new Date().getTime() + PeertubeChunkStore.CLEANER_EXPIRATION_MS | ||
202 | }) | ||
203 | |||
204 | const now = new Date().getTime() | ||
205 | databasesToDeleteInfo = await this.expirationDB.databases.where('expiration').below(now).toArray() | ||
206 | }) | ||
207 | } catch (err) { | ||
208 | logger.error('Cannot update expiration of fetch expired databases.', err) | ||
209 | } | ||
210 | |||
211 | for (const databaseToDeleteInfo of databasesToDeleteInfo) { | ||
212 | await this.dropDatabase(databaseToDeleteInfo.name) | ||
213 | } | ||
214 | } | ||
215 | |||
216 | private async dropDatabase (databaseName: string) { | ||
217 | const dbToDelete = new ChunkDatabase(databaseName) | ||
218 | logger.info(`Destroying IndexDB database ${databaseName}`) | ||
219 | |||
220 | try { | ||
221 | await dbToDelete.delete() | ||
222 | |||
223 | await this.expirationDB.transaction('rw', this.expirationDB.databases, () => { | ||
224 | return this.expirationDB.databases.where({ name: databaseName }).delete() | ||
225 | }) | ||
226 | } catch (err) { | ||
227 | logger.error(`Cannot delete ${databaseName}.`, err) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | private nextTick <T> (cb: (err?: Error, val?: T) => void, err: Error, val?: T) { | ||
232 | process.nextTick(() => cb(err, val), undefined) | ||
233 | } | ||
234 | } | ||
diff --git a/client/src/assets/player/shared/webtorrent/video-renderer.ts b/client/src/assets/player/shared/webtorrent/video-renderer.ts deleted file mode 100644 index a85d7a838..000000000 --- a/client/src/assets/player/shared/webtorrent/video-renderer.ts +++ /dev/null | |||
@@ -1,134 +0,0 @@ | |||
1 | // Thanks: https://github.com/feross/render-media | ||
2 | |||
3 | const MediaElementWrapper = require('mediasource') | ||
4 | import { logger } from '@root-helpers/logger' | ||
5 | import { extname } from 'path' | ||
6 | const Videostream = require('videostream') | ||
7 | |||
8 | const VIDEOSTREAM_EXTS = [ | ||
9 | '.m4a', | ||
10 | '.m4v', | ||
11 | '.mp4' | ||
12 | ] | ||
13 | |||
14 | type RenderMediaOptions = { | ||
15 | controls: boolean | ||
16 | autoplay: boolean | ||
17 | } | ||
18 | |||
19 | function renderVideo ( | ||
20 | file: any, | ||
21 | elem: HTMLVideoElement, | ||
22 | opts: RenderMediaOptions, | ||
23 | callback: (err: Error, renderer: any) => void | ||
24 | ) { | ||
25 | validateFile(file) | ||
26 | |||
27 | return renderMedia(file, elem, opts, callback) | ||
28 | } | ||
29 | |||
30 | function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { | ||
31 | const extension = extname(file.name).toLowerCase() | ||
32 | let preparedElem: any | ||
33 | let currentTime = 0 | ||
34 | let renderer: any | ||
35 | |||
36 | try { | ||
37 | if (VIDEOSTREAM_EXTS.includes(extension)) { | ||
38 | renderer = useVideostream() | ||
39 | } else { | ||
40 | renderer = useMediaSource() | ||
41 | } | ||
42 | } catch (err) { | ||
43 | return callback(err) | ||
44 | } | ||
45 | |||
46 | function useVideostream () { | ||
47 | prepareElem() | ||
48 | preparedElem.addEventListener('error', function onError (err: Error) { | ||
49 | preparedElem.removeEventListener('error', onError) | ||
50 | |||
51 | return callback(err) | ||
52 | }) | ||
53 | preparedElem.addEventListener('loadstart', onLoadStart) | ||
54 | return new Videostream(file, preparedElem) | ||
55 | } | ||
56 | |||
57 | function useMediaSource (useVP9 = false) { | ||
58 | const codecs = getCodec(file.name, useVP9) | ||
59 | |||
60 | prepareElem() | ||
61 | preparedElem.addEventListener('error', function onError (err: Error) { | ||
62 | preparedElem.removeEventListener('error', onError) | ||
63 | |||
64 | // Try with vp9 before returning an error | ||
65 | if (codecs.includes('vp8')) return fallbackToMediaSource(true) | ||
66 | |||
67 | return callback(err) | ||
68 | }) | ||
69 | preparedElem.addEventListener('loadstart', onLoadStart) | ||
70 | |||
71 | const wrapper = new MediaElementWrapper(preparedElem) | ||
72 | const writable = wrapper.createWriteStream(codecs) | ||
73 | file.createReadStream().pipe(writable) | ||
74 | |||
75 | if (currentTime) preparedElem.currentTime = currentTime | ||
76 | |||
77 | return wrapper | ||
78 | } | ||
79 | |||
80 | function fallbackToMediaSource (useVP9 = false) { | ||
81 | if (useVP9 === true) logger.info('Falling back to media source with VP9 enabled.') | ||
82 | else logger.info('Falling back to media source..') | ||
83 | |||
84 | useMediaSource(useVP9) | ||
85 | } | ||
86 | |||
87 | function prepareElem () { | ||
88 | if (preparedElem === undefined) { | ||
89 | preparedElem = elem | ||
90 | |||
91 | preparedElem.addEventListener('progress', function () { | ||
92 | currentTime = elem.currentTime | ||
93 | }) | ||
94 | } | ||
95 | } | ||
96 | |||
97 | function onLoadStart () { | ||
98 | preparedElem.removeEventListener('loadstart', onLoadStart) | ||
99 | if (opts.autoplay) preparedElem.play() | ||
100 | |||
101 | callback(null, renderer) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | function validateFile (file: any) { | ||
106 | if (file == null) { | ||
107 | throw new Error('file cannot be null or undefined') | ||
108 | } | ||
109 | if (typeof file.name !== 'string') { | ||
110 | throw new Error('missing or invalid file.name property') | ||
111 | } | ||
112 | if (typeof file.createReadStream !== 'function') { | ||
113 | throw new Error('missing or invalid file.createReadStream property') | ||
114 | } | ||
115 | } | ||
116 | |||
117 | function getCodec (name: string, useVP9 = false) { | ||
118 | const ext = extname(name).toLowerCase() | ||
119 | if (ext === '.mp4') { | ||
120 | return 'video/mp4; codecs="avc1.640029, mp4a.40.5"' | ||
121 | } | ||
122 | |||
123 | if (ext === '.webm') { | ||
124 | if (useVP9 === true) return 'video/webm; codecs="vp9, opus"' | ||
125 | |||
126 | return 'video/webm; codecs="vp8, vorbis"' | ||
127 | } | ||
128 | |||
129 | return undefined | ||
130 | } | ||
131 | |||
132 | export { | ||
133 | renderVideo | ||
134 | } | ||
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts deleted file mode 100644 index 3dde44a60..000000000 --- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts +++ /dev/null | |||
@@ -1,663 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import * as WebTorrent from 'webtorrent' | ||
3 | import { logger } from '@root-helpers/logger' | ||
4 | import { isIOS } from '@root-helpers/web-browser' | ||
5 | import { addQueryParams, timeToInt } from '@shared/core-utils' | ||
6 | import { VideoFile } from '@shared/models' | ||
7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' | ||
8 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' | ||
9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../common' | ||
10 | import { PeertubeChunkStore } from './peertube-chunk-store' | ||
11 | import { renderVideo } from './video-renderer' | ||
12 | |||
13 | const CacheChunkStore = require('cache-chunk-store') | ||
14 | |||
15 | type PlayOptions = { | ||
16 | forcePlay?: boolean | ||
17 | seek?: number | ||
18 | delay?: number | ||
19 | } | ||
20 | |||
21 | const Plugin = videojs.getPlugin('plugin') | ||
22 | |||
23 | class WebTorrentPlugin extends Plugin { | ||
24 | readonly videoFiles: VideoFile[] | ||
25 | |||
26 | private readonly playerElement: HTMLVideoElement | ||
27 | |||
28 | private readonly autoplay: boolean | string = false | ||
29 | private readonly startTime: number = 0 | ||
30 | private readonly savePlayerSrcFunction: videojs.Player['src'] | ||
31 | private readonly videoDuration: number | ||
32 | private readonly CONSTANTS = { | ||
33 | INFO_SCHEDULER: 1000, // Don't change this | ||
34 | AUTO_QUALITY_SCHEDULER: 3000, // Check quality every 3 seconds | ||
35 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it | ||
36 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check | ||
37 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds | ||
38 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth | ||
39 | } | ||
40 | |||
41 | private readonly buildWebSeedUrls: (file: VideoFile) => string[] | ||
42 | |||
43 | private readonly webtorrent = new WebTorrent({ | ||
44 | tracker: { | ||
45 | rtcConfig: getRtcConfig() | ||
46 | }, | ||
47 | dht: false | ||
48 | }) | ||
49 | |||
50 | private currentVideoFile: VideoFile | ||
51 | private torrent: WebTorrent.Torrent | ||
52 | |||
53 | private renderer: any | ||
54 | private fakeRenderer: any | ||
55 | private destroyingFakeRenderer = false | ||
56 | |||
57 | private autoResolution = true | ||
58 | private autoResolutionPossible = true | ||
59 | private isAutoResolutionObservation = false | ||
60 | private playerRefusedP2P = false | ||
61 | |||
62 | private requiresAuth: boolean | ||
63 | private videoFileToken: () => string | ||
64 | |||
65 | private torrentInfoInterval: any | ||
66 | private autoQualityInterval: any | ||
67 | private addTorrentDelay: any | ||
68 | private qualityObservationTimer: any | ||
69 | private runAutoQualitySchedulerTimer: any | ||
70 | |||
71 | private downloadSpeeds: number[] = [] | ||
72 | |||
73 | constructor (player: videojs.Player, options?: WebtorrentPluginOptions) { | ||
74 | super(player) | ||
75 | |||
76 | this.startTime = timeToInt(options.startTime) | ||
77 | |||
78 | // Custom autoplay handled by webtorrent because we lazy play the video | ||
79 | this.autoplay = options.autoplay | ||
80 | |||
81 | this.playerRefusedP2P = options.playerRefusedP2P | ||
82 | |||
83 | this.videoFiles = options.videoFiles | ||
84 | this.videoDuration = options.videoDuration | ||
85 | |||
86 | this.savePlayerSrcFunction = this.player.src | ||
87 | this.playerElement = options.playerElement | ||
88 | |||
89 | this.requiresAuth = options.requiresAuth | ||
90 | this.videoFileToken = options.videoFileToken | ||
91 | |||
92 | this.buildWebSeedUrls = options.buildWebSeedUrls | ||
93 | |||
94 | this.player.ready(() => { | ||
95 | const playerOptions = this.player.options_ | ||
96 | |||
97 | const volume = getStoredVolume() | ||
98 | if (volume !== undefined) this.player.volume(volume) | ||
99 | |||
100 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
101 | if (muted !== undefined) this.player.muted(muted) | ||
102 | |||
103 | this.player.duration(options.videoDuration) | ||
104 | |||
105 | this.initializePlayer() | ||
106 | this.runTorrentInfoScheduler() | ||
107 | |||
108 | this.player.one('play', () => { | ||
109 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | ||
110 | this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER) | ||
111 | }) | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | dispose () { | ||
116 | clearTimeout(this.addTorrentDelay) | ||
117 | clearTimeout(this.qualityObservationTimer) | ||
118 | clearTimeout(this.runAutoQualitySchedulerTimer) | ||
119 | |||
120 | clearInterval(this.torrentInfoInterval) | ||
121 | clearInterval(this.autoQualityInterval) | ||
122 | |||
123 | // Don't need to destroy renderer, video player will be destroyed | ||
124 | this.flushVideoFile(this.currentVideoFile, false) | ||
125 | |||
126 | this.destroyFakeRenderer() | ||
127 | } | ||
128 | |||
129 | getCurrentResolutionId () { | ||
130 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | ||
131 | } | ||
132 | |||
133 | updateVideoFile ( | ||
134 | videoFile?: VideoFile, | ||
135 | options: { | ||
136 | forcePlay?: boolean | ||
137 | seek?: number | ||
138 | delay?: number | ||
139 | } = {}, | ||
140 | done: () => void = () => { /* empty */ } | ||
141 | ) { | ||
142 | // Automatically choose the adapted video file | ||
143 | if (!videoFile) { | ||
144 | const savedAverageBandwidth = getAverageBandwidthInStore() | ||
145 | videoFile = savedAverageBandwidth | ||
146 | ? this.getAppropriateFile(savedAverageBandwidth) | ||
147 | : this.pickAverageVideoFile() | ||
148 | } | ||
149 | |||
150 | if (!videoFile) { | ||
151 | throw Error(`Can't update video file since videoFile is undefined.`) | ||
152 | } | ||
153 | |||
154 | // Don't add the same video file once again | ||
155 | if (this.currentVideoFile !== undefined && this.currentVideoFile.magnetUri === videoFile.magnetUri) { | ||
156 | return | ||
157 | } | ||
158 | |||
159 | // Do not display error to user because we will have multiple fallback | ||
160 | this.player.peertube().hideFatalError(); | ||
161 | |||
162 | // Hack to "simulate" src link in video.js >= 6 | ||
163 | // Without this, we can't play the video after pausing it | ||
164 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 | ||
165 | (this.player as any).src = () => true | ||
166 | const oldPlaybackRate = this.player.playbackRate() | ||
167 | |||
168 | const previousVideoFile = this.currentVideoFile | ||
169 | this.currentVideoFile = videoFile | ||
170 | |||
171 | // Don't try on iOS that does not support MediaSource | ||
172 | // Or don't use P2P if webtorrent is disabled | ||
173 | if (isIOS() || this.playerRefusedP2P) { | ||
174 | return this.fallbackToHttp(options, () => { | ||
175 | this.player.playbackRate(oldPlaybackRate) | ||
176 | return done() | ||
177 | }) | ||
178 | } | ||
179 | |||
180 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => { | ||
181 | this.player.playbackRate(oldPlaybackRate) | ||
182 | return done() | ||
183 | }) | ||
184 | |||
185 | this.selectAppropriateResolution(true) | ||
186 | } | ||
187 | |||
188 | updateEngineResolution (resolutionId: number, delay = 0) { | ||
189 | // Remember player state | ||
190 | const currentTime = this.player.currentTime() | ||
191 | const isPaused = this.player.paused() | ||
192 | |||
193 | // Hide bigPlayButton | ||
194 | if (!isPaused) { | ||
195 | this.player.bigPlayButton.hide() | ||
196 | } | ||
197 | |||
198 | // Audio-only (resolutionId === 0) gets special treatment | ||
199 | if (resolutionId === 0) { | ||
200 | // Audio-only: show poster, do not auto-hide controls | ||
201 | this.player.addClass('vjs-playing-audio-only-content') | ||
202 | this.player.posterImage.show() | ||
203 | } else { | ||
204 | // Hide poster to have black background | ||
205 | this.player.removeClass('vjs-playing-audio-only-content') | ||
206 | this.player.posterImage.hide() | ||
207 | } | ||
208 | |||
209 | const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) | ||
210 | const options = { | ||
211 | forcePlay: false, | ||
212 | delay, | ||
213 | seek: currentTime + (delay / 1000) | ||
214 | } | ||
215 | |||
216 | this.updateVideoFile(newVideoFile, options) | ||
217 | |||
218 | this.player.trigger('engineResolutionChange') | ||
219 | } | ||
220 | |||
221 | flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { | ||
222 | if (videoFile !== undefined && this.webtorrent.get(videoFile.magnetUri)) { | ||
223 | if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy() | ||
224 | |||
225 | this.webtorrent.remove(videoFile.magnetUri) | ||
226 | logger.info(`Removed ${videoFile.magnetUri}`) | ||
227 | } | ||
228 | } | ||
229 | |||
230 | disableAutoResolution () { | ||
231 | this.autoResolution = false | ||
232 | this.autoResolutionPossible = false | ||
233 | this.player.peertubeResolutions().disableAutoResolution() | ||
234 | } | ||
235 | |||
236 | isAutoResolutionPossible () { | ||
237 | return this.autoResolutionPossible | ||
238 | } | ||
239 | |||
240 | getTorrent () { | ||
241 | return this.torrent | ||
242 | } | ||
243 | |||
244 | getCurrentVideoFile () { | ||
245 | return this.currentVideoFile | ||
246 | } | ||
247 | |||
248 | changeQuality (id: number) { | ||
249 | if (id === -1) { | ||
250 | if (this.autoResolutionPossible === true) { | ||
251 | this.autoResolution = true | ||
252 | |||
253 | this.selectAppropriateResolution(false) | ||
254 | } | ||
255 | |||
256 | return | ||
257 | } | ||
258 | |||
259 | this.autoResolution = false | ||
260 | this.updateEngineResolution(id) | ||
261 | this.selectAppropriateResolution(false) | ||
262 | } | ||
263 | |||
264 | private addTorrent ( | ||
265 | magnetOrTorrentUrl: string, | ||
266 | previousVideoFile: VideoFile, | ||
267 | options: PlayOptions, | ||
268 | done: (err?: Error) => void | ||
269 | ) { | ||
270 | if (!magnetOrTorrentUrl) return this.fallbackToHttp(options, done) | ||
271 | |||
272 | logger.info(`Adding ${magnetOrTorrentUrl}.`) | ||
273 | |||
274 | const oldTorrent = this.torrent | ||
275 | const torrentOptions = { | ||
276 | // Don't use arrow function: it breaks webtorrent (that uses `new` keyword) | ||
277 | store: function (chunkLength: number, storeOpts: any) { | ||
278 | return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { | ||
279 | max: 100 | ||
280 | }) | ||
281 | }, | ||
282 | urlList: this.buildWebSeedUrls(this.currentVideoFile) | ||
283 | } | ||
284 | |||
285 | this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { | ||
286 | logger.info(`Added ${magnetOrTorrentUrl}.`) | ||
287 | |||
288 | if (oldTorrent) { | ||
289 | // Pause the old torrent | ||
290 | this.stopTorrent(oldTorrent) | ||
291 | |||
292 | // We use a fake renderer so we download correct pieces of the next file | ||
293 | if (options.delay) this.renderFileInFakeElement(torrent.files[0], options.delay) | ||
294 | } | ||
295 | |||
296 | // Render the video in a few seconds? (on resolution change for example, we wait some seconds of the new video resolution) | ||
297 | this.addTorrentDelay = setTimeout(() => { | ||
298 | // We don't need the fake renderer anymore | ||
299 | this.destroyFakeRenderer() | ||
300 | |||
301 | const paused = this.player.paused() | ||
302 | |||
303 | this.flushVideoFile(previousVideoFile) | ||
304 | |||
305 | // Update progress bar (just for the UI), do not wait rendering | ||
306 | if (options.seek) this.player.currentTime(options.seek) | ||
307 | |||
308 | const renderVideoOptions = { autoplay: false, controls: true } | ||
309 | renderVideo(torrent.files[0], this.playerElement, renderVideoOptions, (err, renderer) => { | ||
310 | this.renderer = renderer | ||
311 | |||
312 | if (err) return this.fallbackToHttp(options, done) | ||
313 | |||
314 | return this.tryToPlay(err => { | ||
315 | if (err) return done(err) | ||
316 | |||
317 | if (options.seek) this.seek(options.seek) | ||
318 | if (options.forcePlay === false && paused === true) this.player.pause() | ||
319 | |||
320 | return done() | ||
321 | }) | ||
322 | }) | ||
323 | }, options.delay || 0) | ||
324 | }) | ||
325 | |||
326 | this.torrent.on('error', (err: any) => logger.error(err)) | ||
327 | |||
328 | this.torrent.on('warning', (err: any) => { | ||
329 | // We don't support HTTP tracker but we don't care -> we use the web socket tracker | ||
330 | if (err.message.indexOf('Unsupported tracker protocol') !== -1) return | ||
331 | |||
332 | // Users don't care about issues with WebRTC, but developers do so log it in the console | ||
333 | if (err.message.indexOf('Ice connection failed') !== -1) { | ||
334 | logger.info(err) | ||
335 | return | ||
336 | } | ||
337 | |||
338 | // Magnet hash is not up to date with the torrent file, add directly the torrent file | ||
339 | if (err.message.indexOf('incorrect info hash') !== -1) { | ||
340 | logger.error('Incorrect info hash detected, falling back to torrent file.') | ||
341 | const newOptions = { forcePlay: true, seek: options.seek } | ||
342 | return this.addTorrent((this.torrent as any)['xs'], previousVideoFile, newOptions, done) | ||
343 | } | ||
344 | |||
345 | // Remote instance is down | ||
346 | if (err.message.indexOf('from xs param') !== -1) { | ||
347 | this.handleError(err) | ||
348 | } | ||
349 | |||
350 | logger.warn(err) | ||
351 | }) | ||
352 | } | ||
353 | |||
354 | private tryToPlay (done?: (err?: Error) => void) { | ||
355 | if (!done) done = function () { /* empty */ } | ||
356 | |||
357 | const playPromise = this.player.play() | ||
358 | if (playPromise !== undefined) { | ||
359 | return playPromise.then(() => done()) | ||
360 | .catch((err: Error) => { | ||
361 | if (err.message.includes('The play() request was interrupted by a call to pause()')) { | ||
362 | return | ||
363 | } | ||
364 | |||
365 | logger.warn(err) | ||
366 | this.player.pause() | ||
367 | this.player.posterImage.show() | ||
368 | this.player.removeClass('vjs-has-autoplay') | ||
369 | this.player.removeClass('vjs-has-big-play-button-clicked') | ||
370 | this.player.removeClass('vjs-playing-audio-only-content') | ||
371 | |||
372 | return done() | ||
373 | }) | ||
374 | } | ||
375 | |||
376 | return done() | ||
377 | } | ||
378 | |||
379 | private seek (time: number) { | ||
380 | this.player.currentTime(time) | ||
381 | this.player.handleTechSeeked_() | ||
382 | } | ||
383 | |||
384 | private getAppropriateFile (averageDownloadSpeed?: number): VideoFile { | ||
385 | if (this.videoFiles === undefined) return undefined | ||
386 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
387 | |||
388 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
389 | if (files.length === 0) return undefined | ||
390 | |||
391 | // Don't change the torrent if the player ended | ||
392 | if (this.torrent && this.torrent.progress === 1 && this.player.ended()) return this.currentVideoFile | ||
393 | |||
394 | if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed() | ||
395 | |||
396 | // Limit resolution according to player height | ||
397 | const playerHeight = this.playerElement.offsetHeight | ||
398 | |||
399 | // We take the first resolution just above the player height | ||
400 | // Example: player height is 530px, we want the 720p file instead of 480p | ||
401 | let maxResolution = files[0].resolution.id | ||
402 | for (let i = files.length - 1; i >= 0; i--) { | ||
403 | const resolutionId = files[i].resolution.id | ||
404 | if (resolutionId !== 0 && resolutionId >= playerHeight) { | ||
405 | maxResolution = resolutionId | ||
406 | break | ||
407 | } | ||
408 | } | ||
409 | |||
410 | // Filter videos we can play according to our screen resolution and bandwidth | ||
411 | const filteredFiles = files.filter(f => f.resolution.id <= maxResolution) | ||
412 | .filter(f => { | ||
413 | const fileBitrate = (f.size / this.videoDuration) | ||
414 | let threshold = fileBitrate | ||
415 | |||
416 | // If this is for a higher resolution or an initial load: add a margin | ||
417 | if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) { | ||
418 | threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100) | ||
419 | } | ||
420 | |||
421 | return averageDownloadSpeed > threshold | ||
422 | }) | ||
423 | |||
424 | // If the download speed is too bad, return the lowest resolution we have | ||
425 | if (filteredFiles.length === 0) return videoFileMinByResolution(files) | ||
426 | |||
427 | return videoFileMaxByResolution(filteredFiles) | ||
428 | } | ||
429 | |||
430 | private getAndSaveActualDownloadSpeed () { | ||
431 | const start = Math.max(this.downloadSpeeds.length - this.CONSTANTS.BANDWIDTH_AVERAGE_NUMBER_OF_VALUES, 0) | ||
432 | const lastDownloadSpeeds = this.downloadSpeeds.slice(start, this.downloadSpeeds.length) | ||
433 | if (lastDownloadSpeeds.length === 0) return -1 | ||
434 | |||
435 | const sum = lastDownloadSpeeds.reduce((a, b) => a + b) | ||
436 | const averageBandwidth = Math.round(sum / lastDownloadSpeeds.length) | ||
437 | |||
438 | // Save the average bandwidth for future use | ||
439 | saveAverageBandwidth(averageBandwidth) | ||
440 | |||
441 | return averageBandwidth | ||
442 | } | ||
443 | |||
444 | private initializePlayer () { | ||
445 | this.buildQualities() | ||
446 | |||
447 | if (this.videoFiles.length === 0) { | ||
448 | this.player.addClass('disabled') | ||
449 | return | ||
450 | } | ||
451 | |||
452 | if (this.autoplay !== false) { | ||
453 | this.player.posterImage.hide() | ||
454 | |||
455 | return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) | ||
456 | } | ||
457 | |||
458 | // Proxy first play | ||
459 | const oldPlay = this.player.play.bind(this.player); | ||
460 | (this.player as any).play = () => { | ||
461 | this.player.addClass('vjs-has-big-play-button-clicked') | ||
462 | this.player.play = oldPlay | ||
463 | |||
464 | this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) | ||
465 | } | ||
466 | } | ||
467 | |||
468 | private runAutoQualityScheduler () { | ||
469 | this.autoQualityInterval = setInterval(() => { | ||
470 | |||
471 | // Not initialized or in HTTP fallback | ||
472 | if (this.torrent === undefined || this.torrent === null) return | ||
473 | if (this.autoResolution === false) return | ||
474 | if (this.isAutoResolutionObservation === true) return | ||
475 | |||
476 | const file = this.getAppropriateFile() | ||
477 | let changeResolution = false | ||
478 | let changeResolutionDelay = 0 | ||
479 | |||
480 | // Lower resolution | ||
481 | if (this.isPlayerWaiting() && file.resolution.id < this.currentVideoFile.resolution.id) { | ||
482 | logger.info(`Downgrading automatically the resolution to: ${file.resolution.label}`) | ||
483 | changeResolution = true | ||
484 | } else if (file.resolution.id > this.currentVideoFile.resolution.id) { // Higher resolution | ||
485 | logger.info(`Upgrading automatically the resolution to: ${file.resolution.label}`) | ||
486 | changeResolution = true | ||
487 | changeResolutionDelay = this.CONSTANTS.AUTO_QUALITY_HIGHER_RESOLUTION_DELAY | ||
488 | } | ||
489 | |||
490 | if (changeResolution === true) { | ||
491 | this.updateEngineResolution(file.resolution.id, changeResolutionDelay) | ||
492 | |||
493 | // Wait some seconds in observation of our new resolution | ||
494 | this.isAutoResolutionObservation = true | ||
495 | |||
496 | this.qualityObservationTimer = setTimeout(() => { | ||
497 | this.isAutoResolutionObservation = false | ||
498 | }, this.CONSTANTS.AUTO_QUALITY_OBSERVATION_TIME) | ||
499 | } | ||
500 | }, this.CONSTANTS.AUTO_QUALITY_SCHEDULER) | ||
501 | } | ||
502 | |||
503 | private isPlayerWaiting () { | ||
504 | return this.player?.hasClass('vjs-waiting') | ||
505 | } | ||
506 | |||
507 | private runTorrentInfoScheduler () { | ||
508 | this.torrentInfoInterval = setInterval(() => { | ||
509 | // Not initialized yet | ||
510 | if (this.torrent === undefined) return | ||
511 | |||
512 | // Http fallback | ||
513 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) | ||
514 | |||
515 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | ||
516 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | ||
517 | |||
518 | return this.player.trigger('p2pInfo', { | ||
519 | source: 'webtorrent', | ||
520 | http: { | ||
521 | downloadSpeed: 0, | ||
522 | downloaded: 0 | ||
523 | }, | ||
524 | p2p: { | ||
525 | downloadSpeed: this.torrent.downloadSpeed, | ||
526 | numPeers: this.torrent.numPeers, | ||
527 | uploadSpeed: this.torrent.uploadSpeed, | ||
528 | downloaded: this.torrent.downloaded, | ||
529 | uploaded: this.torrent.uploaded | ||
530 | }, | ||
531 | bandwidthEstimate: this.webtorrent.downloadSpeed | ||
532 | } as PlayerNetworkInfo) | ||
533 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
534 | } | ||
535 | |||
536 | private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) { | ||
537 | const paused = this.player.paused() | ||
538 | |||
539 | this.disableAutoResolution() | ||
540 | |||
541 | this.flushVideoFile(this.currentVideoFile, true) | ||
542 | this.torrent = null | ||
543 | |||
544 | // Enable error display now this is our last fallback | ||
545 | this.player.one('error', () => this.player.peertube().displayFatalError()) | ||
546 | |||
547 | let httpUrl = this.currentVideoFile.fileUrl | ||
548 | |||
549 | if (this.requiresAuth && this.videoFileToken) { | ||
550 | httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) | ||
551 | } | ||
552 | |||
553 | this.player.src = this.savePlayerSrcFunction | ||
554 | this.player.src(httpUrl) | ||
555 | |||
556 | this.selectAppropriateResolution(true) | ||
557 | |||
558 | // We changed the source, so reinit captions | ||
559 | this.player.trigger('sourcechange') | ||
560 | |||
561 | return this.tryToPlay(err => { | ||
562 | if (err && done) return done(err) | ||
563 | |||
564 | if (options.seek) this.seek(options.seek) | ||
565 | if (options.forcePlay === false && paused === true) this.player.pause() | ||
566 | |||
567 | if (done) return done() | ||
568 | }) | ||
569 | } | ||
570 | |||
571 | private handleError (err: Error | string) { | ||
572 | return this.player.trigger('customError', { err }) | ||
573 | } | ||
574 | |||
575 | private pickAverageVideoFile () { | ||
576 | if (this.videoFiles.length === 1) return this.videoFiles[0] | ||
577 | |||
578 | const files = this.videoFiles.filter(f => f.resolution.id !== 0) | ||
579 | return files[Math.floor(files.length / 2)] | ||
580 | } | ||
581 | |||
582 | private stopTorrent (torrent: WebTorrent.Torrent) { | ||
583 | torrent.pause() | ||
584 | // Pause does not remove actual peers (in particular the webseed peer) | ||
585 | torrent.removePeer((torrent as any)['ws']) | ||
586 | } | ||
587 | |||
588 | private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) { | ||
589 | this.destroyingFakeRenderer = false | ||
590 | |||
591 | const fakeVideoElem = document.createElement('video') | ||
592 | renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => { | ||
593 | this.fakeRenderer = renderer | ||
594 | |||
595 | // The renderer returns an error when we destroy it, so skip them | ||
596 | if (this.destroyingFakeRenderer === false && err) { | ||
597 | logger.error('Cannot render new torrent in fake video element.', err) | ||
598 | } | ||
599 | |||
600 | // Load the future file at the correct time (in delay MS - 2 seconds) | ||
601 | fakeVideoElem.currentTime = this.player.currentTime() + (delay - 2000) | ||
602 | }) | ||
603 | } | ||
604 | |||
605 | private destroyFakeRenderer () { | ||
606 | if (this.fakeRenderer) { | ||
607 | this.destroyingFakeRenderer = true | ||
608 | |||
609 | if (this.fakeRenderer.destroy) { | ||
610 | try { | ||
611 | this.fakeRenderer.destroy() | ||
612 | } catch (err) { | ||
613 | logger.info('Cannot destroy correctly fake renderer.', err) | ||
614 | } | ||
615 | } | ||
616 | this.fakeRenderer = undefined | ||
617 | } | ||
618 | } | ||
619 | |||
620 | private buildQualities () { | ||
621 | const resolutions: PeerTubeResolution[] = this.videoFiles.map(file => ({ | ||
622 | id: file.resolution.id, | ||
623 | label: this.buildQualityLabel(file), | ||
624 | height: file.resolution.id, | ||
625 | selected: false, | ||
626 | selectCallback: () => this.changeQuality(file.resolution.id) | ||
627 | })) | ||
628 | |||
629 | resolutions.push({ | ||
630 | id: -1, | ||
631 | label: this.player.localize('Auto'), | ||
632 | selected: true, | ||
633 | selectCallback: () => this.changeQuality(-1) | ||
634 | }) | ||
635 | |||
636 | this.player.peertubeResolutions().add(resolutions) | ||
637 | } | ||
638 | |||
639 | private buildQualityLabel (file: VideoFile) { | ||
640 | let label = file.resolution.label | ||
641 | |||
642 | if (file.fps && file.fps >= 50) { | ||
643 | label += file.fps | ||
644 | } | ||
645 | |||
646 | return label | ||
647 | } | ||
648 | |||
649 | private selectAppropriateResolution (byEngine: boolean) { | ||
650 | const resolution = this.autoResolution | ||
651 | ? -1 | ||
652 | : this.getCurrentResolutionId() | ||
653 | |||
654 | const autoResolutionChosen = this.autoResolution | ||
655 | ? this.getCurrentResolutionId() | ||
656 | : undefined | ||
657 | |||
658 | this.player.peertubeResolutions().select({ id: resolution, autoResolutionChosenId: autoResolutionChosen, byEngine }) | ||
659 | } | ||
660 | } | ||
661 | |||
662 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) | ||
663 | export { WebTorrentPlugin } | ||
diff --git a/client/src/assets/player/types/index.ts b/client/src/assets/player/types/index.ts index b73e0b3cb..4bf49f65c 100644 --- a/client/src/assets/player/types/index.ts +++ b/client/src/assets/player/types/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './manager-options' | 1 | export * from './peertube-player-options' |
2 | export * from './peertube-videojs-typings' | 2 | export * from './peertube-videojs-typings' |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts deleted file mode 100644 index c14fd7e99..000000000 --- a/client/src/assets/player/types/manager-options.ts +++ /dev/null | |||
@@ -1,98 +0,0 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | ||
3 | import { PlaylistPluginOptions, VideoJSCaption } from './peertube-videojs-typings' | ||
4 | |||
5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
6 | |||
7 | export type WebtorrentOptions = { | ||
8 | videoFiles: VideoFile[] | ||
9 | } | ||
10 | |||
11 | export type P2PMediaLoaderOptions = { | ||
12 | playlistUrl: string | ||
13 | segmentsSha256Url: string | ||
14 | trackerAnnounce: string[] | ||
15 | redundancyBaseUrls: string[] | ||
16 | videoFiles: VideoFile[] | ||
17 | } | ||
18 | |||
19 | export interface CustomizationOptions { | ||
20 | startTime: number | string | ||
21 | stopTime: number | string | ||
22 | |||
23 | controls?: boolean | ||
24 | controlBar?: boolean | ||
25 | |||
26 | muted?: boolean | ||
27 | loop?: boolean | ||
28 | subtitle?: string | ||
29 | resume?: string | ||
30 | |||
31 | peertubeLink: boolean | ||
32 | |||
33 | playbackRate?: number | string | ||
34 | } | ||
35 | |||
36 | export interface CommonOptions extends CustomizationOptions { | ||
37 | playerElement: HTMLVideoElement | ||
38 | onPlayerElementChange: (element: HTMLVideoElement) => void | ||
39 | |||
40 | autoplay: boolean | ||
41 | forceAutoplay: boolean | ||
42 | |||
43 | p2pEnabled: boolean | ||
44 | |||
45 | nextVideo?: () => void | ||
46 | hasNextVideo?: () => boolean | ||
47 | |||
48 | previousVideo?: () => void | ||
49 | hasPreviousVideo?: () => boolean | ||
50 | |||
51 | playlist?: PlaylistPluginOptions | ||
52 | |||
53 | videoDuration: number | ||
54 | enableHotkeys: boolean | ||
55 | inactivityTimeout: number | ||
56 | poster: string | ||
57 | |||
58 | videoViewIntervalMs: number | ||
59 | |||
60 | instanceName: string | ||
61 | |||
62 | theaterButton: boolean | ||
63 | captions: boolean | ||
64 | |||
65 | videoViewUrl: string | ||
66 | authorizationHeader?: () => string | ||
67 | |||
68 | metricsUrl: string | ||
69 | |||
70 | embedUrl: string | ||
71 | embedTitle: string | ||
72 | |||
73 | isLive: boolean | ||
74 | liveOptions?: { | ||
75 | latencyMode: LiveVideoLatencyMode | ||
76 | } | ||
77 | |||
78 | language?: string | ||
79 | |||
80 | videoCaptions: VideoJSCaption[] | ||
81 | |||
82 | videoUUID: string | ||
83 | videoShortUUID: string | ||
84 | |||
85 | serverUrl: string | ||
86 | requiresAuth: boolean | ||
87 | videoFileToken: () => string | ||
88 | |||
89 | errorNotifier: (message: string) => void | ||
90 | } | ||
91 | |||
92 | export type PeertubePlayerManagerOptions = { | ||
93 | common: CommonOptions | ||
94 | webtorrent: WebtorrentOptions | ||
95 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
96 | |||
97 | pluginsManager: PluginsManager | ||
98 | } | ||
diff --git a/client/src/assets/player/types/peertube-player-options.ts b/client/src/assets/player/types/peertube-player-options.ts new file mode 100644 index 000000000..e1b8c7fab --- /dev/null +++ b/client/src/assets/player/types/peertube-player-options.ts | |||
@@ -0,0 +1,117 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | ||
3 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | ||
4 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' | ||
5 | |||
6 | export type PlayerMode = 'web-video' | 'p2p-media-loader' | ||
7 | |||
8 | export type PeerTubePlayerContructorOptions = { | ||
9 | playerElement: () => HTMLVideoElement | ||
10 | |||
11 | controls: boolean | ||
12 | controlBar: boolean | ||
13 | |||
14 | muted: boolean | ||
15 | loop: boolean | ||
16 | |||
17 | peertubeLink: () => boolean | ||
18 | |||
19 | playbackRate?: number | string | ||
20 | |||
21 | enableHotkeys: boolean | ||
22 | inactivityTimeout: number | ||
23 | |||
24 | videoViewIntervalMs: number | ||
25 | |||
26 | instanceName: string | ||
27 | |||
28 | theaterButton: boolean | ||
29 | |||
30 | authorizationHeader: () => string | ||
31 | |||
32 | metricsUrl: string | ||
33 | serverUrl: string | ||
34 | |||
35 | errorNotifier: (message: string) => void | ||
36 | |||
37 | // Current web browser language | ||
38 | language: string | ||
39 | |||
40 | pluginsManager: PluginsManager | ||
41 | } | ||
42 | |||
43 | export type PeerTubePlayerLoadOptions = { | ||
44 | mode: PlayerMode | ||
45 | |||
46 | startTime?: number | string | ||
47 | stopTime?: number | string | ||
48 | |||
49 | autoplay: boolean | ||
50 | forceAutoplay: boolean | ||
51 | |||
52 | poster: string | ||
53 | subtitle?: string | ||
54 | videoViewUrl: string | ||
55 | |||
56 | embedUrl: string | ||
57 | embedTitle: string | ||
58 | |||
59 | isLive: boolean | ||
60 | |||
61 | liveOptions?: { | ||
62 | latencyMode: LiveVideoLatencyMode | ||
63 | } | ||
64 | |||
65 | videoCaptions: VideoJSCaption[] | ||
66 | storyboard: VideoJSStoryboard | ||
67 | |||
68 | videoUUID: string | ||
69 | videoShortUUID: string | ||
70 | |||
71 | duration: number | ||
72 | |||
73 | requiresUserAuth: boolean | ||
74 | videoFileToken: () => string | ||
75 | requiresPassword: boolean | ||
76 | videoPassword: () => string | ||
77 | |||
78 | nextVideo: { | ||
79 | enabled: boolean | ||
80 | getVideoTitle: () => string | ||
81 | handler?: () => void | ||
82 | displayControlBarButton: boolean | ||
83 | } | ||
84 | |||
85 | previousVideo: { | ||
86 | enabled: boolean | ||
87 | handler?: () => void | ||
88 | displayControlBarButton: boolean | ||
89 | } | ||
90 | |||
91 | upnext?: { | ||
92 | isEnabled: () => boolean | ||
93 | isSuspended: (player: videojs.VideoJsPlayer) => boolean | ||
94 | timeout: number | ||
95 | } | ||
96 | |||
97 | dock?: PeerTubeDockPluginOptions | ||
98 | |||
99 | playlist?: PlaylistPluginOptions | ||
100 | |||
101 | p2pEnabled: boolean | ||
102 | |||
103 | hls?: HLSOptions | ||
104 | webVideo?: WebVideoOptions | ||
105 | } | ||
106 | |||
107 | export type WebVideoOptions = { | ||
108 | videoFiles: VideoFile[] | ||
109 | } | ||
110 | |||
111 | export type HLSOptions = { | ||
112 | playlistUrl: string | ||
113 | segmentsSha256Url: string | ||
114 | trackerAnnounce: string[] | ||
115 | redundancyBaseUrls: string[] | ||
116 | videoFiles: VideoFile[] | ||
117 | } | ||
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index eadf56cfa..f10fc03a8 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -2,8 +2,11 @@ import { HlsConfig, Level } from 'hls.js' | |||
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { BezelsPlugin } from '../shared/bezels/bezels-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | 6 | import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin' |
7 | import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | ||
8 | import { HotkeysOptions, PeerTubeHotkeysPlugin } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
9 | import { PeerTubeMobilePlugin } from '../shared/mobile/peertube-mobile-plugin' | ||
7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 10 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 11 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 12 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -12,9 +15,10 @@ import { PlaylistPlugin } from '../shared/playlist/playlist-plugin' | |||
12 | import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin' | 15 | import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin' |
13 | import { StatsCardOptions } from '../shared/stats/stats-card' | 16 | import { StatsCardOptions } from '../shared/stats/stats-card' |
14 | import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin' | 17 | import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin' |
15 | import { EndCardOptions } from '../shared/upnext/end-card' | 18 | import { UpNextPlugin } from '../shared/upnext/upnext-plugin' |
16 | import { WebTorrentPlugin } from '../shared/webtorrent/webtorrent-plugin' | 19 | import { WebVideoPlugin } from '../shared/web-video/web-video-plugin' |
17 | import { PlayerMode } from './manager-options' | 20 | import { PlayerMode } from './peertube-player-options' |
21 | import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator' | ||
18 | 22 | ||
19 | declare module 'video.js' { | 23 | declare module 'video.js' { |
20 | 24 | ||
@@ -31,33 +35,36 @@ declare module 'video.js' { | |||
31 | 35 | ||
32 | handleTechSeeked_ (): void | 36 | handleTechSeeked_ (): void |
33 | 37 | ||
38 | textTracks (): TextTrackList & { | ||
39 | tracks_: (TextTrack & { id: string, label: string, src: string })[] | ||
40 | } | ||
41 | |||
34 | // Plugins | 42 | // Plugins |
35 | 43 | ||
36 | peertube (): PeerTubePlugin | 44 | peertube (): PeerTubePlugin |
37 | 45 | ||
38 | webtorrent (): WebTorrentPlugin | 46 | webVideo (options?: any): WebVideoPlugin |
39 | 47 | ||
40 | p2pMediaLoader (): P2pMediaLoaderPlugin | 48 | p2pMediaLoader (options?: any): P2pMediaLoaderPlugin |
49 | hlsjs (options?: any): any | ||
41 | 50 | ||
42 | peertubeResolutions (): PeerTubeResolutionsPlugin | 51 | peertubeResolutions (): PeerTubeResolutionsPlugin |
43 | 52 | ||
44 | contextmenuUI (options: any): any | 53 | contextmenuUI (options?: any): any |
45 | 54 | ||
46 | bezels (): void | 55 | bezels (): BezelsPlugin |
47 | peertubeMobile (): void | 56 | peertubeMobile (): PeerTubeMobilePlugin |
48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void | 57 | peerTubeHotkeysPlugin (options?: HotkeysOptions): PeerTubeHotkeysPlugin |
49 | 58 | ||
50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 59 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
51 | 60 | ||
52 | textTracks (): TextTrackList & { | 61 | storyboard (options?: StoryboardOptions): StoryboardPlugin |
53 | tracks_: (TextTrack & { id: string, label: string, src: string })[] | ||
54 | } | ||
55 | 62 | ||
56 | peertubeDock (options: PeerTubeDockPluginOptions): void | 63 | peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin |
57 | 64 | ||
58 | upnext (options: Partial<EndCardOptions>): void | 65 | upnext (options?: UpNextPluginOptions): UpNextPlugin |
59 | 66 | ||
60 | playlist (): PlaylistPlugin | 67 | playlist (options?: PlaylistPluginOptions): PlaylistPlugin |
61 | } | 68 | } |
62 | } | 69 | } |
63 | 70 | ||
@@ -89,33 +96,43 @@ type VideoJSCaption = { | |||
89 | src: string | 96 | src: string |
90 | } | 97 | } |
91 | 98 | ||
99 | type VideoJSStoryboard = { | ||
100 | url: string | ||
101 | width: number | ||
102 | height: number | ||
103 | interval: number | ||
104 | } | ||
105 | |||
92 | type PeerTubePluginOptions = { | 106 | type PeerTubePluginOptions = { |
93 | mode: PlayerMode | 107 | hasAutoplay: () => videojs.Autoplay |
94 | 108 | ||
95 | autoplay: videojs.Autoplay | 109 | videoViewUrl: () => string |
96 | videoDuration: number | 110 | videoViewIntervalMs: number |
97 | 111 | ||
98 | videoViewUrl: string | ||
99 | authorizationHeader?: () => string | 112 | authorizationHeader?: () => string |
100 | 113 | ||
101 | subtitle?: string | 114 | videoDuration: () => number |
102 | 115 | ||
103 | videoCaptions: VideoJSCaption[] | 116 | startTime: () => number | string |
104 | 117 | stopTime: () => number | string | |
105 | startTime: number | string | ||
106 | stopTime: number | string | ||
107 | 118 | ||
108 | isLive: boolean | 119 | videoCaptions: () => VideoJSCaption[] |
109 | 120 | isLive: () => boolean | |
110 | videoUUID: string | 121 | videoUUID: () => string |
111 | 122 | subtitle: () => string | |
112 | videoViewIntervalMs: number | ||
113 | } | 123 | } |
114 | 124 | ||
115 | type MetricsPluginOptions = { | 125 | type MetricsPluginOptions = { |
116 | mode: PlayerMode | 126 | mode: () => PlayerMode |
117 | metricsUrl: string | 127 | metricsUrl: () => string |
118 | videoUUID: string | 128 | videoUUID: () => string |
129 | } | ||
130 | |||
131 | type StoryboardOptions = { | ||
132 | url: string | ||
133 | width: number | ||
134 | height: number | ||
135 | interval: number | ||
119 | } | 136 | } |
120 | 137 | ||
121 | type PlaylistPluginOptions = { | 138 | type PlaylistPluginOptions = { |
@@ -128,37 +145,36 @@ type PlaylistPluginOptions = { | |||
128 | onItemClicked: (element: VideoPlaylistElement) => void | 145 | onItemClicked: (element: VideoPlaylistElement) => void |
129 | } | 146 | } |
130 | 147 | ||
148 | type UpNextPluginOptions = { | ||
149 | timeout: number | ||
150 | |||
151 | next: () => void | ||
152 | getTitle: () => string | ||
153 | isDisplayed: () => boolean | ||
154 | isSuspended: () => boolean | ||
155 | } | ||
156 | |||
131 | type NextPreviousVideoButtonOptions = { | 157 | type NextPreviousVideoButtonOptions = { |
132 | type: 'next' | 'previous' | 158 | type: 'next' | 'previous' |
133 | handler: () => void | 159 | handler?: () => void |
160 | isDisplayed: () => boolean | ||
134 | isDisabled: () => boolean | 161 | isDisabled: () => boolean |
135 | } | 162 | } |
136 | 163 | ||
137 | type PeerTubeLinkButtonOptions = { | 164 | type PeerTubeLinkButtonOptions = { |
138 | shortUUID: string | 165 | isDisplayed: () => boolean |
166 | shortUUID: () => string | ||
139 | instanceName: string | 167 | instanceName: string |
140 | } | 168 | } |
141 | 169 | ||
142 | type PeerTubeP2PInfoButtonOptions = { | 170 | type TheaterButtonOptions = { |
143 | p2pEnabled: boolean | 171 | isDisplayed: () => boolean |
144 | } | 172 | } |
145 | 173 | ||
146 | type WebtorrentPluginOptions = { | 174 | type WebVideoPluginOptions = { |
147 | playerElement: HTMLVideoElement | ||
148 | |||
149 | autoplay: videojs.Autoplay | ||
150 | videoDuration: number | ||
151 | |||
152 | videoFiles: VideoFile[] | 175 | videoFiles: VideoFile[] |
153 | |||
154 | startTime: number | string | 176 | startTime: number | string |
155 | |||
156 | playerRefusedP2P: boolean | ||
157 | |||
158 | requiresAuth: boolean | ||
159 | videoFileToken: () => string | 177 | videoFileToken: () => string |
160 | |||
161 | buildWebSeedUrls: (file: VideoFile) => string[] | ||
162 | } | 178 | } |
163 | 179 | ||
164 | type P2PMediaLoaderPluginOptions = { | 180 | type P2PMediaLoaderPluginOptions = { |
@@ -166,16 +182,17 @@ type P2PMediaLoaderPluginOptions = { | |||
166 | type: string | 182 | type: string |
167 | src: string | 183 | src: string |
168 | 184 | ||
169 | startTime: number | string | ||
170 | |||
171 | loader: P2PMediaLoader | 185 | loader: P2PMediaLoader |
186 | segmentValidator: SegmentValidator | ||
172 | 187 | ||
173 | requiresAuth: boolean | 188 | requiresUserAuth: boolean |
174 | videoFileToken: () => string | 189 | videoFileToken: () => string |
175 | } | 190 | } |
176 | 191 | ||
177 | export type P2PMediaLoader = { | 192 | export type P2PMediaLoader = { |
178 | getEngine(): Engine | 193 | getEngine(): Engine |
194 | |||
195 | destroy: () => void | ||
179 | } | 196 | } |
180 | 197 | ||
181 | type VideoJSPluginOptions = { | 198 | type VideoJSPluginOptions = { |
@@ -184,7 +201,7 @@ type VideoJSPluginOptions = { | |||
184 | peertube: PeerTubePluginOptions | 201 | peertube: PeerTubePluginOptions |
185 | metrics: MetricsPluginOptions | 202 | metrics: MetricsPluginOptions |
186 | 203 | ||
187 | webtorrent?: WebtorrentPluginOptions | 204 | webVideo?: WebVideoPluginOptions |
188 | 205 | ||
189 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | 206 | p2pMediaLoader?: P2PMediaLoaderPluginOptions |
190 | } | 207 | } |
@@ -211,14 +228,14 @@ type AutoResolutionUpdateData = { | |||
211 | } | 228 | } |
212 | 229 | ||
213 | type PlayerNetworkInfo = { | 230 | type PlayerNetworkInfo = { |
214 | source: 'webtorrent' | 'p2p-media-loader' | 231 | source: 'web-video' | 'p2p-media-loader' |
215 | 232 | ||
216 | http: { | 233 | http: { |
217 | downloadSpeed: number | 234 | downloadSpeed?: number |
218 | downloaded: number | 235 | downloaded: number |
219 | } | 236 | } |
220 | 237 | ||
221 | p2p: { | 238 | p2p?: { |
222 | downloadSpeed: number | 239 | downloadSpeed: number |
223 | uploadSpeed: number | 240 | uploadSpeed: number |
224 | downloaded: number | 241 | downloaded: number |
@@ -227,7 +244,7 @@ type PlayerNetworkInfo = { | |||
227 | } | 244 | } |
228 | 245 | ||
229 | // In bytes | 246 | // In bytes |
230 | bandwidthEstimate: number | 247 | bandwidthEstimate?: number |
231 | } | 248 | } |
232 | 249 | ||
233 | type PlaylistItemOptions = { | 250 | type PlaylistItemOptions = { |
@@ -238,6 +255,8 @@ type PlaylistItemOptions = { | |||
238 | 255 | ||
239 | export { | 256 | export { |
240 | PlayerNetworkInfo, | 257 | PlayerNetworkInfo, |
258 | TheaterButtonOptions, | ||
259 | VideoJSStoryboard, | ||
241 | PlaylistItemOptions, | 260 | PlaylistItemOptions, |
242 | NextPreviousVideoButtonOptions, | 261 | NextPreviousVideoButtonOptions, |
243 | ResolutionUpdateData, | 262 | ResolutionUpdateData, |
@@ -246,11 +265,12 @@ export { | |||
246 | MetricsPluginOptions, | 265 | MetricsPluginOptions, |
247 | VideoJSCaption, | 266 | VideoJSCaption, |
248 | PeerTubePluginOptions, | 267 | PeerTubePluginOptions, |
249 | WebtorrentPluginOptions, | 268 | WebVideoPluginOptions, |
250 | P2PMediaLoaderPluginOptions, | 269 | P2PMediaLoaderPluginOptions, |
251 | PeerTubeResolution, | 270 | PeerTubeResolution, |
252 | VideoJSPluginOptions, | 271 | VideoJSPluginOptions, |
272 | UpNextPluginOptions, | ||
253 | LoadedQualityData, | 273 | LoadedQualityData, |
254 | PeerTubeLinkButtonOptions, | 274 | StoryboardOptions, |
255 | PeerTubeP2PInfoButtonOptions | 275 | PeerTubeLinkButtonOptions |
256 | } | 276 | } |
diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts index 9022b908b..4a44615fb 100644 --- a/client/src/root-helpers/video.ts +++ b/client/src/root-helpers/video.ts | |||
@@ -41,14 +41,21 @@ function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: b | |||
41 | return userP2PEnabled | 41 | return userP2PEnabled |
42 | } | 42 | } |
43 | 43 | ||
44 | function videoRequiresAuth (video: Video) { | 44 | function videoRequiresUserAuth (video: Video, videoPassword?: string) { |
45 | return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) | 45 | return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) || |
46 | (video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && !videoPassword) | ||
47 | |||
48 | } | ||
49 | |||
50 | function videoRequiresFileToken (video: Video, videoPassword?: string) { | ||
51 | return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy.id) | ||
46 | } | 52 | } |
47 | 53 | ||
48 | export { | 54 | export { |
49 | buildVideoOrPlaylistEmbed, | 55 | buildVideoOrPlaylistEmbed, |
50 | isP2PEnabled, | 56 | isP2PEnabled, |
51 | videoRequiresAuth | 57 | videoRequiresUserAuth, |
58 | videoRequiresFileToken | ||
52 | } | 59 | } |
53 | 60 | ||
54 | // --------------------------------------------------------------------------- | 61 | // --------------------------------------------------------------------------- |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 96b3adf66..09a75e2fd 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -12,11 +12,8 @@ | |||
12 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); | 12 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); |
13 | transition: visibility 0.3s, opacity 0.3s !important; | 13 | transition: visibility 0.3s, opacity 0.3s !important; |
14 | 14 | ||
15 | &.control-bar-hidden { | 15 | > button:not(.vjs-hidden):first-child, |
16 | display: none !important; | 16 | > button.vjs-hidden + button:not(.vjs-hidden) { |
17 | } | ||
18 | |||
19 | > button:first-child { | ||
20 | @include margin-left($first-control-bar-element-margin-left); | 17 | @include margin-left($first-control-bar-element-margin-left); |
21 | } | 18 | } |
22 | 19 | ||
@@ -79,6 +76,7 @@ | |||
79 | top: -0.3em; | 76 | top: -0.3em; |
80 | } | 77 | } |
81 | 78 | ||
79 | // Only used on mobile | ||
82 | .vjs-time-tooltip { | 80 | .vjs-time-tooltip { |
83 | display: none; | 81 | display: none; |
84 | } | 82 | } |
@@ -152,7 +150,7 @@ | |||
152 | } | 150 | } |
153 | } | 151 | } |
154 | 152 | ||
155 | .vjs-live-control { | 153 | .vjs-pt-live-control { |
156 | padding: 5px 7px; | 154 | padding: 5px 7px; |
157 | border-radius: 3px; | 155 | border-radius: 3px; |
158 | height: fit-content; | 156 | height: fit-content; |
@@ -230,6 +228,7 @@ | |||
230 | .vjs-next-video, | 228 | .vjs-next-video, |
231 | .vjs-previous-video { | 229 | .vjs-previous-video { |
232 | width: $control-bar-button-width - 4px; | 230 | width: $control-bar-button-width - 4px; |
231 | cursor: pointer; | ||
233 | 232 | ||
234 | &.vjs-disabled { | 233 | &.vjs-disabled { |
235 | cursor: default; | 234 | cursor: default; |
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss index 5d0307d95..4bfd67a26 100644 --- a/client/src/sass/player/index.scss +++ b/client/src/sass/player/index.scss | |||
@@ -10,3 +10,4 @@ | |||
10 | @use './playlist'; | 10 | @use './playlist'; |
11 | @use './stats'; | 11 | @use './stats'; |
12 | @use './offline-notification'; | 12 | @use './offline-notification'; |
13 | @use './storyboard.scss'; | ||
diff --git a/client/src/sass/player/mobile.scss b/client/src/sass/player/mobile.scss index 84d7a00f1..b0019d2c9 100644 --- a/client/src/sass/player/mobile.scss +++ b/client/src/sass/player/mobile.scss | |||
@@ -6,6 +6,31 @@ | |||
6 | /* Special mobile style */ | 6 | /* Special mobile style */ |
7 | 7 | ||
8 | .video-js.vjs-peertube-skin.vjs-is-mobile { | 8 | .video-js.vjs-peertube-skin.vjs-is-mobile { |
9 | // No hover means we can't display the storyboard/time tooltip on mouse hover | ||
10 | // Use the time tooltip in progress control instead | ||
11 | .vjs-mouse-display { | ||
12 | display: none !important; | ||
13 | } | ||
14 | |||
15 | .vjs-storyboard-sprite-placeholder { | ||
16 | display: none; | ||
17 | } | ||
18 | |||
19 | .vjs-progress-control .vjs-sliding { | ||
20 | |||
21 | .vjs-time-tooltip, | ||
22 | .vjs-storyboard-sprite-placeholder { | ||
23 | display: block !important; | ||
24 | |||
25 | visibility: visible !important; | ||
26 | } | ||
27 | |||
28 | .vjs-time-tooltip { | ||
29 | color: #fff; | ||
30 | background-color: rgba(0, 0, 0, 0.8); | ||
31 | } | ||
32 | } | ||
33 | |||
9 | .vjs-control-bar { | 34 | .vjs-control-bar { |
10 | .vjs-progress-control .vjs-slider .vjs-play-progress { | 35 | .vjs-progress-control .vjs-slider .vjs-play-progress { |
11 | // Always display the circle on mobile | 36 | // Always display the circle on mobile |
@@ -145,7 +170,8 @@ | |||
145 | } | 170 | } |
146 | } | 171 | } |
147 | 172 | ||
148 | &.vjs-scrubbing { | 173 | &.vjs-scrubbing, |
174 | &.vjs-mobile-sliding { | ||
149 | .vjs-mobile-buttons-overlay { | 175 | .vjs-mobile-buttons-overlay { |
150 | display: none; | 176 | display: none; |
151 | } | 177 | } |
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index 4df8dbaf0..572ae7050 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -84,7 +84,9 @@ body { | |||
84 | } | 84 | } |
85 | 85 | ||
86 | // Do not display poster when video is starting | 86 | // Do not display poster when video is starting |
87 | &.vjs-has-autoplay:not(.vjs-has-started) { | 87 | // Or if we change resolution manually |
88 | &.vjs-has-autoplay:not(.vjs-has-started), | ||
89 | &.vjs-updating-resolution { | ||
88 | .vjs-poster { | 90 | .vjs-poster { |
89 | opacity: 0; | 91 | opacity: 0; |
90 | visibility: hidden; | 92 | visibility: hidden; |
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss index d2346c126..369c827f7 100644 --- a/client/src/sass/player/settings-menu.scss +++ b/client/src/sass/player/settings-menu.scss | |||
@@ -75,6 +75,7 @@ $setting-transition-easing: ease-out; | |||
75 | > .vjs-menu { | 75 | > .vjs-menu { |
76 | flex: 1; | 76 | flex: 1; |
77 | min-width: 200px; | 77 | min-width: 200px; |
78 | padding: 5px 0; | ||
78 | } | 79 | } |
79 | 80 | ||
80 | > .vjs-menu, | 81 | > .vjs-menu, |
@@ -90,14 +91,6 @@ $setting-transition-easing: ease-out; | |||
90 | background-color: rgba(255, 255, 255, 0.2); | 91 | background-color: rgba(255, 255, 255, 0.2); |
91 | } | 92 | } |
92 | 93 | ||
93 | &:first-child { | ||
94 | margin-top: 5px; | ||
95 | } | ||
96 | |||
97 | &:last-child { | ||
98 | margin-bottom: 5px; | ||
99 | } | ||
100 | |||
101 | &.disabled { | 94 | &.disabled { |
102 | opacity: 0.5; | 95 | opacity: 0.5; |
103 | cursor: default !important; | 96 | cursor: default !important; |
diff --git a/client/src/sass/player/storyboard.scss b/client/src/sass/player/storyboard.scss new file mode 100644 index 000000000..c80d1b59d --- /dev/null +++ b/client/src/sass/player/storyboard.scss | |||
@@ -0,0 +1,26 @@ | |||
1 | @use 'sass:math'; | ||
2 | @use '_variables' as *; | ||
3 | @use '_mixins' as *; | ||
4 | @use './_player-variables' as *; | ||
5 | |||
6 | // Like the time tooltip | ||
7 | .video-js .vjs-progress-holder .vjs-storyboard-sprite-placeholder { | ||
8 | display: none; | ||
9 | } | ||
10 | |||
11 | .video-js .vjs-progress-control:hover .vjs-storyboard-sprite-placeholder, | ||
12 | .video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-storyboard-sprite-placeholder { | ||
13 | display: block; | ||
14 | |||
15 | // Ensure that we maintain a font-size of ~10px. | ||
16 | font-size: 0.6em; | ||
17 | visibility: visible; | ||
18 | } | ||
19 | |||
20 | .video-js.vjs-settings-dialog-opened { | ||
21 | .vjs-storyboard-sprite-placeholder, | ||
22 | .vjs-time-tooltip, | ||
23 | .vjs-mouse-display { | ||
24 | display: none !important; | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/shims/http.ts b/client/src/shims/http.ts deleted file mode 100644 index 1b1767aab..000000000 --- a/client/src/shims/http.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('stream-http') | ||
diff --git a/client/src/shims/https.ts b/client/src/shims/https.ts deleted file mode 100644 index f5ef70430..000000000 --- a/client/src/shims/https.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('https-browserify') | ||
diff --git a/client/src/shims/stream.ts b/client/src/shims/stream.ts deleted file mode 100644 index 977fd05a0..000000000 --- a/client/src/shims/stream.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | module.exports = require('stream-browserify') | ||
diff --git a/client/src/standalone/player/.npmignore b/client/src/standalone/embed-player-api/.npmignore index 870b6315b..870b6315b 100644 --- a/client/src/standalone/player/.npmignore +++ b/client/src/standalone/embed-player-api/.npmignore | |||
diff --git a/client/src/standalone/player/README.md b/client/src/standalone/embed-player-api/README.md index 7b47e8f02..7b47e8f02 100644 --- a/client/src/standalone/player/README.md +++ b/client/src/standalone/embed-player-api/README.md | |||
diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/embed-player-api/definitions.ts index 495f1a98c..495f1a98c 100644 --- a/client/src/standalone/player/definitions.ts +++ b/client/src/standalone/embed-player-api/definitions.ts | |||
diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/embed-player-api/events.ts index 77d21c78c..77d21c78c 100644 --- a/client/src/standalone/player/events.ts +++ b/client/src/standalone/embed-player-api/events.ts | |||
diff --git a/client/src/standalone/player/package.json b/client/src/standalone/embed-player-api/package.json index b549fbf52..b549fbf52 100644 --- a/client/src/standalone/player/package.json +++ b/client/src/standalone/embed-player-api/package.json | |||
diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/embed-player-api/player.ts index 75487258b..75487258b 100644 --- a/client/src/standalone/player/player.ts +++ b/client/src/standalone/embed-player-api/player.ts | |||
diff --git a/client/src/standalone/player/tsconfig.json b/client/src/standalone/embed-player-api/tsconfig.json index eecc63dfb..eecc63dfb 100644 --- a/client/src/standalone/player/tsconfig.json +++ b/client/src/standalone/embed-player-api/tsconfig.json | |||
diff --git a/client/src/standalone/player/webpack.config.js b/client/src/standalone/embed-player-api/webpack.config.js index 48d350edf..48d350edf 100644 --- a/client/src/standalone/player/webpack.config.js +++ b/client/src/standalone/embed-player-api/webpack.config.js | |||
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts index a99f1edae..6227c378e 100644 --- a/client/src/standalone/videos/embed-api.ts +++ b/client/src/standalone/videos/embed-api.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | import * as Channel from 'jschannel' | 2 | import * as Channel from 'jschannel' |
3 | import { logger } from '../../root-helpers' | 3 | import { logger } from '../../root-helpers' |
4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../player/definitions' | 4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../embed-player-api/definitions' |
5 | import { PeerTubeEmbed } from './embed' | 5 | import { PeerTubeEmbed } from './embed' |
6 | 6 | ||
7 | /** | 7 | /** |
@@ -72,15 +72,12 @@ export class PeerTubeEmbedApi { | |||
72 | private setResolution (resolutionId: number) { | 72 | private setResolution (resolutionId: number) { |
73 | logger.info(`Set resolution ${resolutionId}`) | 73 | logger.info(`Set resolution ${resolutionId}`) |
74 | 74 | ||
75 | if (this.isWebtorrent()) { | 75 | if (this.isWebVideo() && resolutionId === -1) { |
76 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return | 76 | logger.error('Auto resolution cannot be set in web video player mode') |
77 | |||
78 | this.embed.player.webtorrent().changeQuality(resolutionId) | ||
79 | |||
80 | return | 77 | return |
81 | } | 78 | } |
82 | 79 | ||
83 | this.embed.player.p2pMediaLoader().getHLSJS().currentLevel = resolutionId | 80 | this.embed.player.peertubeResolutions().select({ id: resolutionId, fireCallback: true }) |
84 | } | 81 | } |
85 | 82 | ||
86 | private getCaptions (): PeerTubeTextTrack[] { | 83 | private getCaptions (): PeerTubeTextTrack[] { |
@@ -152,8 +149,8 @@ export class PeerTubeEmbedApi { | |||
152 | // --------------------------------------------------------------------------- | 149 | // --------------------------------------------------------------------------- |
153 | 150 | ||
154 | // PeerTube specific capabilities | 151 | // PeerTube specific capabilities |
155 | this.embed.player.peertubeResolutions().on('resolutionsAdded', () => this.loadResolutions()) | 152 | this.embed.player.peertubeResolutions().on('resolutions-added', () => this.loadResolutions()) |
156 | this.embed.player.peertubeResolutions().on('resolutionChanged', () => this.loadResolutions()) | 153 | this.embed.player.peertubeResolutions().on('resolutions-changed', () => this.loadResolutions()) |
157 | 154 | ||
158 | this.loadResolutions() | 155 | this.loadResolutions() |
159 | 156 | ||
@@ -193,7 +190,7 @@ export class PeerTubeEmbedApi { | |||
193 | }) | 190 | }) |
194 | } | 191 | } |
195 | 192 | ||
196 | private isWebtorrent () { | 193 | private isWebVideo () { |
197 | return !!this.embed.player.webtorrent | 194 | return !!this.embed.player.webVideo |
198 | } | 195 | } |
199 | } | 196 | } |
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index 32bf5f655..e2dc02b60 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -41,9 +41,24 @@ | |||
41 | <div id="error-content"></div> | 41 | <div id="error-content"></div> |
42 | </div> | 42 | </div> |
43 | 43 | ||
44 | <div id="video-wrapper"></div> | 44 | <div id="video-password-block"> |
45 | <!-- eslint-disable-next-line @angular-eslint/template/elements-content --> | ||
46 | <h1 id="video-password-title"></h1> | ||
47 | |||
48 | <div id="video-password-content"></div> | ||
49 | |||
50 | <form id="video-password-form"> | ||
51 | <input type="password" id="video-password-input" name="video-password" autocomplete="user-password" required> | ||
52 | <button type="submit" id="video-password-submit"> </button> | ||
53 | </form> | ||
45 | 54 | ||
46 | <div id="placeholder-preview"></div> | 55 | <div id="video-password-error"></div> |
56 | <svg xmlns="http://www.w3.org/2000/svg" width="4rem" height="4rem" viewBox="0 0 24 24"> | ||
57 | <g fill="none" stroke="#c4c4c4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></g> | ||
58 | </svg> | ||
59 | </div> | ||
60 | |||
61 | <div id="video-wrapper"></div> | ||
47 | 62 | ||
48 | <script type="text/javascript"> | 63 | <script type="text/javascript"> |
49 | // Can be called in embed.ts | 64 | // Can be called in embed.ts |
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index 3631ea7e6..d15887478 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss | |||
@@ -24,7 +24,7 @@ html, | |||
24 | body { | 24 | body { |
25 | height: 100%; | 25 | height: 100%; |
26 | margin: 0; | 26 | margin: 0; |
27 | background-color: #000; | 27 | background-color: #0f0f10; |
28 | } | 28 | } |
29 | 29 | ||
30 | #video-wrapper { | 30 | #video-wrapper { |
@@ -42,8 +42,10 @@ body { | |||
42 | } | 42 | } |
43 | } | 43 | } |
44 | 44 | ||
45 | #error-block { | 45 | #error-block, |
46 | #video-password-block { | ||
46 | display: none; | 47 | display: none; |
48 | user-select: none; | ||
47 | 49 | ||
48 | flex-direction: column; | 50 | flex-direction: column; |
49 | align-content: center; | 51 | align-content: center; |
@@ -86,6 +88,43 @@ body { | |||
86 | text-align: center; | 88 | text-align: center; |
87 | } | 89 | } |
88 | 90 | ||
91 | #video-password-content { | ||
92 | @include margin(1rem, 0, 2rem); | ||
93 | } | ||
94 | |||
95 | #video-password-input, | ||
96 | #video-password-submit { | ||
97 | line-height: 23px; | ||
98 | padding: 1rem; | ||
99 | margin: 1rem 0.5rem; | ||
100 | border: 0; | ||
101 | font-weight: 600; | ||
102 | border-radius: 3px!important; | ||
103 | font-size: 18px; | ||
104 | display: inline-block; | ||
105 | } | ||
106 | |||
107 | #video-password-submit { | ||
108 | color: #fff; | ||
109 | background-color: #f2690d; | ||
110 | cursor: pointer; | ||
111 | } | ||
112 | |||
113 | #video-password-submit:hover { | ||
114 | background-color: #f47825; | ||
115 | } | ||
116 | #video-password-error { | ||
117 | margin-top: 10px; | ||
118 | margin-bottom: 10px; | ||
119 | height: 2rem; | ||
120 | font-weight: bolder; | ||
121 | } | ||
122 | |||
123 | #video-password-block svg { | ||
124 | margin-left: auto; | ||
125 | margin-right: auto; | ||
126 | } | ||
127 | |||
89 | @media screen and (max-width: 300px) { | 128 | @media screen and (max-width: 300px) { |
90 | #error-block { | 129 | #error-block { |
91 | font-size: 36px; | 130 | font-size: 36px; |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index cc4274b99..78b812ffd 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -1,18 +1,26 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | import '../../assets/player/shared/dock/peertube-dock-component' | 2 | import '../../assets/player/shared/dock/peertube-dock-component' |
3 | import '../../assets/player/shared/dock/peertube-dock-plugin' | 3 | import '../../assets/player/shared/dock/peertube-dock-plugin' |
4 | import { PeerTubeServerError } from 'src/types' | ||
4 | import videojs from 'video.js' | 5 | import videojs from 'video.js' |
5 | import { peertubeTranslate } from '../../../../shared/core-utils/i18n' | 6 | import { |
6 | import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' | 7 | HTMLServerConfig, |
7 | import { PeertubePlayerManager } from '../../assets/player' | 8 | ResultList, |
9 | ServerErrorCode, | ||
10 | VideoDetails, | ||
11 | VideoPlaylist, | ||
12 | VideoPlaylistElement, | ||
13 | VideoState | ||
14 | } from '../../../../shared/models' | ||
15 | import { PeerTubePlayer } from '../../assets/player/peertube-player' | ||
8 | import { TranslationsManager } from '../../assets/player/translations-manager' | 16 | import { TranslationsManager } from '../../assets/player/translations-manager' |
9 | import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' | 17 | import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers' |
10 | import { PeerTubeEmbedApi } from './embed-api' | 18 | import { PeerTubeEmbedApi } from './embed-api' |
11 | import { | 19 | import { |
12 | AuthHTTP, | 20 | AuthHTTP, |
13 | LiveManager, | 21 | LiveManager, |
14 | PeerTubePlugin, | 22 | PeerTubePlugin, |
15 | PlayerManagerOptions, | 23 | PlayerOptionsBuilder, |
16 | PlaylistFetcher, | 24 | PlaylistFetcher, |
17 | PlaylistTracker, | 25 | PlaylistTracker, |
18 | Translations, | 26 | Translations, |
@@ -27,18 +35,26 @@ export class PeerTubeEmbed { | |||
27 | config: HTMLServerConfig | 35 | config: HTMLServerConfig |
28 | 36 | ||
29 | private translationsPromise: Promise<{ [id: string]: string }> | 37 | private translationsPromise: Promise<{ [id: string]: string }> |
30 | private PeertubePlayerManagerModulePromise: Promise<any> | 38 | private PeerTubePlayerManagerModulePromise: Promise<any> |
31 | 39 | ||
32 | private readonly http: AuthHTTP | 40 | private readonly http: AuthHTTP |
33 | private readonly videoFetcher: VideoFetcher | 41 | private readonly videoFetcher: VideoFetcher |
34 | private readonly playlistFetcher: PlaylistFetcher | 42 | private readonly playlistFetcher: PlaylistFetcher |
35 | private readonly peertubePlugin: PeerTubePlugin | 43 | private readonly peertubePlugin: PeerTubePlugin |
36 | private readonly playerHTML: PlayerHTML | 44 | private readonly playerHTML: PlayerHTML |
37 | private readonly playerManagerOptions: PlayerManagerOptions | 45 | private readonly playerOptionsBuilder: PlayerOptionsBuilder |
38 | private readonly liveManager: LiveManager | 46 | private readonly liveManager: LiveManager |
39 | 47 | ||
48 | private peertubePlayer: PeerTubePlayer | ||
49 | |||
40 | private playlistTracker: PlaylistTracker | 50 | private playlistTracker: PlaylistTracker |
41 | 51 | ||
52 | private alreadyInitialized = false | ||
53 | private alreadyPlayed = false | ||
54 | |||
55 | private videoPassword: string | ||
56 | private requiresPassword: boolean | ||
57 | |||
42 | constructor (videoWrapperId: string) { | 58 | constructor (videoWrapperId: string) { |
43 | logger.registerServerSending(window.location.origin) | 59 | logger.registerServerSending(window.location.origin) |
44 | 60 | ||
@@ -48,8 +64,9 @@ export class PeerTubeEmbed { | |||
48 | this.playlistFetcher = new PlaylistFetcher(this.http) | 64 | this.playlistFetcher = new PlaylistFetcher(this.http) |
49 | this.peertubePlugin = new PeerTubePlugin(this.http) | 65 | this.peertubePlugin = new PeerTubePlugin(this.http) |
50 | this.playerHTML = new PlayerHTML(videoWrapperId) | 66 | this.playerHTML = new PlayerHTML(videoWrapperId) |
51 | this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) | 67 | this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin) |
52 | this.liveManager = new LiveManager(this.playerHTML) | 68 | this.liveManager = new LiveManager(this.playerHTML) |
69 | this.requiresPassword = false | ||
53 | 70 | ||
54 | try { | 71 | try { |
55 | this.config = JSON.parse((window as any)['PeerTubeServerConfig']) | 72 | this.config = JSON.parse((window as any)['PeerTubeServerConfig']) |
@@ -69,14 +86,14 @@ export class PeerTubeEmbed { | |||
69 | } | 86 | } |
70 | 87 | ||
71 | getScope () { | 88 | getScope () { |
72 | return this.playerManagerOptions.getScope() | 89 | return this.playerOptionsBuilder.getScope() |
73 | } | 90 | } |
74 | 91 | ||
75 | // --------------------------------------------------------------------------- | 92 | // --------------------------------------------------------------------------- |
76 | 93 | ||
77 | async init () { | 94 | async init () { |
78 | this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) | 95 | this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) |
79 | this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') | 96 | this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player') |
80 | 97 | ||
81 | // Issue when we parsed config from HTML, fallback to API | 98 | // Issue when we parsed config from HTML, fallback to API |
82 | if (!this.config) { | 99 | if (!this.config) { |
@@ -90,7 +107,7 @@ export class PeerTubeEmbed { | |||
90 | 107 | ||
91 | if (!videoId) return | 108 | if (!videoId) return |
92 | 109 | ||
93 | return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) | 110 | return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false }) |
94 | } | 111 | } |
95 | 112 | ||
96 | private async initPlaylist () { | 113 | private async initPlaylist () { |
@@ -125,7 +142,7 @@ export class PeerTubeEmbed { | |||
125 | } | 142 | } |
126 | 143 | ||
127 | private initializeApi () { | 144 | private initializeApi () { |
128 | if (this.playerManagerOptions.hasAPIEnabled()) { | 145 | if (this.playerOptionsBuilder.hasAPIEnabled()) { |
129 | if (this.api) { | 146 | if (this.api) { |
130 | this.api.reInit() | 147 | this.api.reInit() |
131 | return | 148 | return |
@@ -147,7 +164,7 @@ export class PeerTubeEmbed { | |||
147 | 164 | ||
148 | this.playlistTracker.setCurrentElement(next) | 165 | this.playlistTracker.setCurrentElement(next) |
149 | 166 | ||
150 | return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) | 167 | return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false }) |
151 | } | 168 | } |
152 | 169 | ||
153 | async playPreviousPlaylistVideo () { | 170 | async playPreviousPlaylistVideo () { |
@@ -159,7 +176,7 @@ export class PeerTubeEmbed { | |||
159 | 176 | ||
160 | this.playlistTracker.setCurrentElement(previous) | 177 | this.playlistTracker.setCurrentElement(previous) |
161 | 178 | ||
162 | await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) | 179 | await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false }) |
163 | } | 180 | } |
164 | 181 | ||
165 | getCurrentPlaylistPosition () { | 182 | getCurrentPlaylistPosition () { |
@@ -170,123 +187,124 @@ export class PeerTubeEmbed { | |||
170 | 187 | ||
171 | private async loadVideoAndBuildPlayer (options: { | 188 | private async loadVideoAndBuildPlayer (options: { |
172 | uuid: string | 189 | uuid: string |
173 | autoplayFromPreviousVideo: boolean | ||
174 | forceAutoplay: boolean | 190 | forceAutoplay: boolean |
175 | }) { | 191 | }) { |
176 | const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options | 192 | const { uuid, forceAutoplay } = options |
177 | 193 | ||
178 | try { | 194 | try { |
179 | const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid) | 195 | const { |
196 | videoResponse, | ||
197 | captionsPromise, | ||
198 | storyboardsPromise | ||
199 | } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword }) | ||
180 | 200 | ||
181 | return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay }) | 201 | return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, forceAutoplay }) |
182 | } catch (err) { | 202 | } catch (err) { |
183 | this.playerHTML.displayError(err.message, await this.translationsPromise) | 203 | |
204 | if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options }) | ||
205 | else this.playerHTML.displayError(err.message, await this.translationsPromise) | ||
184 | } | 206 | } |
185 | } | 207 | } |
186 | 208 | ||
187 | private async buildVideoPlayer (options: { | 209 | private async buildVideoPlayer (options: { |
188 | videoResponse: Response | 210 | videoResponse: Response |
211 | storyboardsPromise: Promise<Response> | ||
189 | captionsPromise: Promise<Response> | 212 | captionsPromise: Promise<Response> |
190 | autoplayFromPreviousVideo: boolean | ||
191 | forceAutoplay: boolean | 213 | forceAutoplay: boolean |
192 | }) { | 214 | }) { |
193 | const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options | 215 | const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options |
194 | |||
195 | this.resetPlayerElement() | ||
196 | 216 | ||
197 | const videoInfoPromise = videoResponse.json() | 217 | const videoInfoPromise = videoResponse.json() |
198 | .then(async (videoInfo: VideoDetails) => { | 218 | .then(async (videoInfo: VideoDetails) => { |
199 | this.playerManagerOptions.loadParams(this.config, videoInfo) | 219 | this.playerOptionsBuilder.loadParams(this.config, videoInfo) |
200 | 220 | ||
201 | if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) { | ||
202 | this.playerHTML.buildPlaceholder(videoInfo) | ||
203 | } | ||
204 | const live = videoInfo.isLive | 221 | const live = videoInfo.isLive |
205 | ? await this.videoFetcher.loadLive(videoInfo) | 222 | ? await this.videoFetcher.loadLive(videoInfo) |
206 | : undefined | 223 | : undefined |
207 | 224 | ||
208 | const videoFileToken = videoRequiresAuth(videoInfo) | 225 | const videoFileToken = videoRequiresFileToken(videoInfo) |
209 | ? await this.videoFetcher.loadVideoToken(videoInfo) | 226 | ? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword) |
210 | : undefined | 227 | : undefined |
211 | 228 | ||
212 | return { live, video: videoInfo, videoFileToken } | 229 | return { live, video: videoInfo, videoFileToken } |
213 | }) | 230 | }) |
214 | 231 | ||
215 | const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ | 232 | const [ |
233 | { video, live, videoFileToken }, | ||
234 | translations, | ||
235 | captionsResponse, | ||
236 | storyboardsResponse | ||
237 | ] = await Promise.all([ | ||
216 | videoInfoPromise, | 238 | videoInfoPromise, |
217 | this.translationsPromise, | 239 | this.translationsPromise, |
218 | captionsPromise, | 240 | captionsPromise, |
219 | this.PeertubePlayerManagerModulePromise | 241 | storyboardsPromise, |
242 | this.buildPlayerIfNeeded() | ||
220 | ]) | 243 | ]) |
221 | 244 | ||
222 | await this.peertubePlugin.loadPlugins(this.config, translations) | 245 | // If already played, we are in a playlist so we don't want to display the poster between videos |
246 | if (!this.alreadyPlayed) { | ||
247 | this.peertubePlayer.setPoster(window.location.origin + video.previewPath) | ||
248 | } | ||
249 | |||
250 | const playlist = this.playlistTracker | ||
251 | ? { | ||
252 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }), | ||
223 | 253 | ||
224 | const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager | 254 | playlistTracker: this.playlistTracker, |
255 | playNext: () => this.playNextPlaylistVideo(), | ||
256 | playPrevious: () => this.playPreviousPlaylistVideo() | ||
257 | } | ||
258 | : undefined | ||
225 | 259 | ||
226 | const playerOptions = await this.playerManagerOptions.getPlayerOptions({ | 260 | const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({ |
227 | video, | 261 | video, |
228 | captionsResponse, | 262 | captionsResponse, |
229 | autoplayFromPreviousVideo, | ||
230 | translations, | 263 | translations, |
231 | serverConfig: this.config, | ||
232 | 264 | ||
233 | authorizationHeader: () => this.http.getHeaderTokenValue(), | 265 | storyboardsResponse, |
234 | videoFileToken: () => videoFileToken, | ||
235 | 266 | ||
236 | onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), | 267 | videoFileToken: () => videoFileToken, |
268 | videoPassword: () => this.videoPassword, | ||
269 | requiresPassword: this.requiresPassword, | ||
237 | 270 | ||
238 | playlistTracker: this.playlistTracker, | 271 | playlist, |
239 | playNextPlaylistVideo: () => this.playNextPlaylistVideo(), | ||
240 | playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(), | ||
241 | 272 | ||
242 | live, | 273 | live, |
243 | forceAutoplay | 274 | forceAutoplay, |
275 | alreadyPlayed: this.alreadyPlayed | ||
244 | }) | 276 | }) |
277 | await this.peertubePlayer.load(loadOptions) | ||
245 | 278 | ||
246 | this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { | 279 | if (!this.alreadyInitialized) { |
247 | this.player = player | 280 | this.player = this.peertubePlayer.getPlayer(); |
248 | }) | ||
249 | 281 | ||
250 | this.player.on('customError', (event: any, data: any) => { | 282 | (window as any)['videojsPlayer'] = this.player |
251 | const message = data?.err?.message || '' | ||
252 | if (!message.includes('from xs param')) return | ||
253 | 283 | ||
254 | this.player.dispose() | 284 | this.buildCSS() |
255 | this.playerHTML.removePlayerElement() | 285 | this.initializeApi() |
256 | this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations) | 286 | } |
257 | }); | ||
258 | |||
259 | (window as any)['videojsPlayer'] = this.player | ||
260 | |||
261 | this.buildCSS() | ||
262 | this.buildPlayerDock(video) | ||
263 | this.initializeApi() | ||
264 | 287 | ||
265 | this.playerHTML.removePlaceholder() | 288 | this.alreadyInitialized = true |
266 | 289 | ||
267 | if (this.isPlaylistEmbed()) { | 290 | this.player.one('play', () => { |
268 | await this.buildPlayerPlaylistUpnext() | 291 | this.alreadyPlayed = true |
292 | }) | ||
269 | 293 | ||
270 | this.player.playlist().updateSelected() | 294 | if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock() |
271 | |||
272 | this.player.on('stopped', () => { | ||
273 | this.playNextPlaylistVideo() | ||
274 | }) | ||
275 | } | ||
276 | 295 | ||
277 | if (video.isLive) { | 296 | if (video.isLive) { |
278 | this.liveManager.listenForChanges({ | 297 | this.liveManager.listenForChanges({ |
279 | video, | 298 | video, |
280 | onPublishedVideo: () => { | 299 | onPublishedVideo: () => { |
281 | this.liveManager.stopListeningForChanges(video) | 300 | this.liveManager.stopListeningForChanges(video) |
282 | this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) | 301 | this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true }) |
283 | } | 302 | } |
284 | }) | 303 | }) |
285 | 304 | ||
286 | if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { | 305 | if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { |
287 | this.liveManager.displayInfo({ state: video.state.id, translations }) | 306 | this.liveManager.displayInfo({ state: video.state.id, translations }) |
288 | 307 | this.peertubePlayer.disable() | |
289 | this.disablePlayer() | ||
290 | } else { | 308 | } else { |
291 | this.correctlyHandleLiveEnding(translations) | 309 | this.correctlyHandleLiveEnding(translations) |
292 | } | 310 | } |
@@ -295,74 +313,15 @@ export class PeerTubeEmbed { | |||
295 | this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) | 313 | this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) |
296 | } | 314 | } |
297 | 315 | ||
298 | private resetPlayerElement () { | ||
299 | if (this.player) { | ||
300 | this.player.dispose() | ||
301 | this.player = undefined | ||
302 | } | ||
303 | |||
304 | const playerElement = document.createElement('video') | ||
305 | playerElement.className = 'video-js vjs-peertube-skin' | ||
306 | playerElement.setAttribute('playsinline', 'true') | ||
307 | |||
308 | this.playerHTML.setPlayerElement(playerElement) | ||
309 | this.playerHTML.addPlayerElementToDOM() | ||
310 | } | ||
311 | |||
312 | private async buildPlayerPlaylistUpnext () { | ||
313 | const translations = await this.translationsPromise | ||
314 | |||
315 | this.player.upnext({ | ||
316 | timeout: 10000, // 10s | ||
317 | headText: peertubeTranslate('Up Next', translations), | ||
318 | cancelText: peertubeTranslate('Cancel', translations), | ||
319 | suspendedText: peertubeTranslate('Autoplay is suspended', translations), | ||
320 | getTitle: () => this.playlistTracker.nextVideoTitle(), | ||
321 | next: () => this.playNextPlaylistVideo(), | ||
322 | condition: () => !!this.playlistTracker.getNextPlaylistElement(), | ||
323 | suspended: () => false | ||
324 | }) | ||
325 | } | ||
326 | |||
327 | private buildPlayerDock (videoInfo: VideoDetails) { | ||
328 | if (!this.playerManagerOptions.hasControls()) return | ||
329 | |||
330 | // On webtorrent fallback, player may have been disposed | ||
331 | if (!this.player.player_) return | ||
332 | |||
333 | const title = this.playerManagerOptions.hasTitle() | ||
334 | ? videoInfo.name | ||
335 | : undefined | ||
336 | |||
337 | const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled() | ||
338 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | ||
339 | : undefined | ||
340 | |||
341 | if (!title && !description) return | ||
342 | |||
343 | const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) | ||
344 | const avatar = availableAvatars.length !== 0 | ||
345 | ? availableAvatars[0] | ||
346 | : undefined | ||
347 | |||
348 | this.player.peertubeDock({ | ||
349 | title, | ||
350 | description, | ||
351 | avatarUrl: title && avatar | ||
352 | ? avatar.path | ||
353 | : undefined | ||
354 | }) | ||
355 | } | ||
356 | |||
357 | private buildCSS () { | 316 | private buildCSS () { |
358 | const body = document.getElementById('custom-css') | 317 | const body = document.getElementById('custom-css') |
359 | 318 | ||
360 | if (this.playerManagerOptions.hasBigPlayBackgroundColor()) { | 319 | if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) { |
361 | body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor()) | 320 | body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor()) |
362 | } | 321 | } |
363 | 322 | ||
364 | if (this.playerManagerOptions.hasForegroundColor()) { | 323 | if (this.playerOptionsBuilder.hasForegroundColor()) { |
365 | body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor()) | 324 | body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor()) |
366 | } | 325 | } |
367 | } | 326 | } |
368 | 327 | ||
@@ -384,23 +343,52 @@ export class PeerTubeEmbed { | |||
384 | // Display the live ended information | 343 | // Display the live ended information |
385 | this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) | 344 | this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) |
386 | 345 | ||
387 | this.disablePlayer() | 346 | this.peertubePlayer.disable() |
388 | }) | 347 | }) |
389 | } | 348 | } |
390 | 349 | ||
391 | private disablePlayer () { | 350 | private async handlePasswordError (err: PeerTubeServerError) { |
392 | if (this.player.isFullscreen()) { | 351 | let incorrectPassword: boolean = null |
393 | this.player.exitFullscreen() | 352 | if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false |
394 | } | 353 | else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true |
395 | 354 | ||
396 | // Disable player | 355 | if (incorrectPassword === null) return false |
397 | this.player.hasStarted(false) | ||
398 | this.player.removeClass('vjs-has-autoplay') | ||
399 | this.player.bigPlayButton.hide(); | ||
400 | 356 | ||
401 | (this.player.el() as HTMLElement).style.pointerEvents = 'none' | 357 | this.requiresPassword = true |
358 | this.videoPassword = await this.playerHTML.askVideoPassword({ | ||
359 | incorrectPassword, | ||
360 | translations: await this.translationsPromise | ||
361 | }) | ||
362 | return true | ||
402 | } | 363 | } |
403 | 364 | ||
365 | private async buildPlayerIfNeeded () { | ||
366 | if (this.peertubePlayer) { | ||
367 | this.peertubePlayer.enable() | ||
368 | |||
369 | return | ||
370 | } | ||
371 | |||
372 | const playerElement = document.createElement('video') | ||
373 | playerElement.className = 'video-js vjs-peertube-skin' | ||
374 | playerElement.setAttribute('playsinline', 'true') | ||
375 | |||
376 | this.playerHTML.setPlayerElement(playerElement) | ||
377 | this.playerHTML.addPlayerElementToDOM() | ||
378 | |||
379 | const [ { PeerTubePlayer } ] = await Promise.all([ | ||
380 | this.PeerTubePlayerManagerModulePromise, | ||
381 | this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise) | ||
382 | ]) | ||
383 | |||
384 | const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({ | ||
385 | serverConfig: this.config, | ||
386 | authorizationHeader: () => this.http.getHeaderTokenValue() | ||
387 | }) | ||
388 | this.peertubePlayer = new PeerTubePlayer(constructorOptions) | ||
389 | |||
390 | this.player = this.peertubePlayer.getPlayer() | ||
391 | } | ||
404 | } | 392 | } |
405 | 393 | ||
406 | PeerTubeEmbed.main() | 394 | PeerTubeEmbed.main() |
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts index 95e3b029e..c1e9f7750 100644 --- a/client/src/standalone/videos/shared/auth-http.ts +++ b/client/src/standalone/videos/shared/auth-http.ts | |||
@@ -18,10 +18,12 @@ export class AuthHTTP { | |||
18 | if (this.userOAuthTokens) this.setHeadersFromTokens() | 18 | if (this.userOAuthTokens) this.setHeadersFromTokens() |
19 | } | 19 | } |
20 | 20 | ||
21 | fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) { | 21 | fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }, videoPassword?: string) { |
22 | const refreshFetchOptions = optionalAuth | 22 | let refreshFetchOptions: { headers?: Headers } = {} |
23 | ? { headers: this.headers } | 23 | |
24 | : {} | 24 | if (videoPassword) this.headers.set('x-peertube-video-password', videoPassword) |
25 | |||
26 | if (videoPassword || optionalAuth) refreshFetchOptions = { headers: this.headers } | ||
25 | 27 | ||
26 | return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method }) | 28 | return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method }) |
27 | } | 29 | } |
diff --git a/client/src/standalone/videos/shared/index.ts b/client/src/standalone/videos/shared/index.ts index 928b8e270..dcc522ac6 100644 --- a/client/src/standalone/videos/shared/index.ts +++ b/client/src/standalone/videos/shared/index.ts | |||
@@ -2,7 +2,7 @@ export * from './auth-http' | |||
2 | export * from './peertube-plugin' | 2 | export * from './peertube-plugin' |
3 | export * from './live-manager' | 3 | export * from './live-manager' |
4 | export * from './player-html' | 4 | export * from './player-html' |
5 | export * from './player-manager-options' | 5 | export * from './player-options-builder' |
6 | export * from './playlist-fetcher' | 6 | export * from './playlist-fetcher' |
7 | export * from './playlist-tracker' | 7 | export * from './playlist-tracker' |
8 | export * from './translations' | 8 | export * from './translations' |
diff --git a/client/src/standalone/videos/shared/player-html.ts b/client/src/standalone/videos/shared/player-html.ts index d93678c10..0defa0d70 100644 --- a/client/src/standalone/videos/shared/player-html.ts +++ b/client/src/standalone/videos/shared/player-html.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' | 1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' |
2 | import { VideoDetails } from '../../../../../shared/models' | ||
3 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
4 | import { Translations } from './translations' | 3 | import { Translations } from './translations' |
5 | 4 | ||
@@ -55,17 +54,55 @@ export class PlayerHTML { | |||
55 | this.wrapperElement.style.display = 'none' | 54 | this.wrapperElement.style.display = 'none' |
56 | } | 55 | } |
57 | 56 | ||
58 | buildPlaceholder (video: VideoDetails) { | 57 | async askVideoPassword (options: { incorrectPassword: boolean, translations: Translations }): Promise<string> { |
59 | const placeholder = this.getPlaceholderElement() | 58 | const { incorrectPassword, translations } = options |
59 | return new Promise((resolve) => { | ||
60 | 60 | ||
61 | const url = window.location.origin + video.previewPath | 61 | this.wrapperElement.style.display = 'none' |
62 | placeholder.style.backgroundImage = `url("${url}")` | 62 | |
63 | placeholder.style.display = 'block' | 63 | const translatedTitle = peertubeTranslate('This video is password protected', translations) |
64 | const translatedMessage = peertubeTranslate('You need a password to watch this video.', translations) | ||
65 | |||
66 | document.title = translatedTitle | ||
67 | |||
68 | const videoPasswordBlock = document.getElementById('video-password-block') | ||
69 | videoPasswordBlock.style.display = 'flex' | ||
70 | |||
71 | const videoPasswordTitle = document.getElementById('video-password-title') | ||
72 | videoPasswordTitle.innerHTML = translatedTitle | ||
73 | |||
74 | const videoPasswordMessage = document.getElementById('video-password-content') | ||
75 | videoPasswordMessage.innerHTML = translatedMessage | ||
76 | |||
77 | if (incorrectPassword) { | ||
78 | const videoPasswordError = document.getElementById('video-password-error') | ||
79 | videoPasswordError.innerHTML = peertubeTranslate('Incorrect password, please enter a correct password', translations) | ||
80 | videoPasswordError.style.transform = 'scale(1.2)' | ||
81 | |||
82 | setTimeout(() => { | ||
83 | videoPasswordError.style.transform = 'scale(1)' | ||
84 | }, 500) | ||
85 | } | ||
86 | |||
87 | const videoPasswordSubmitButton = document.getElementById('video-password-submit') | ||
88 | videoPasswordSubmitButton.innerHTML = peertubeTranslate('Watch Video', translations) | ||
89 | |||
90 | const videoPasswordInput = document.getElementById('video-password-input') as HTMLInputElement | ||
91 | videoPasswordInput.placeholder = peertubeTranslate('Password', translations) | ||
92 | |||
93 | const videoPasswordForm = document.getElementById('video-password-form') | ||
94 | videoPasswordForm.addEventListener('submit', (event) => { | ||
95 | event.preventDefault() | ||
96 | const videoPassword = videoPasswordInput.value | ||
97 | resolve(videoPassword) | ||
98 | }) | ||
99 | }) | ||
64 | } | 100 | } |
65 | 101 | ||
66 | removePlaceholder () { | 102 | removeVideoPasswordBlock () { |
67 | const placeholder = this.getPlaceholderElement() | 103 | const videoPasswordBlock = document.getElementById('video-password-block') |
68 | placeholder.style.display = 'none' | 104 | videoPasswordBlock.style.display = 'none' |
105 | this.wrapperElement.style.display = 'block' | ||
69 | } | 106 | } |
70 | 107 | ||
71 | displayInformation (text: string, translations: Translations) { | 108 | displayInformation (text: string, translations: Translations) { |
@@ -85,10 +122,6 @@ export class PlayerHTML { | |||
85 | this.informationElement = undefined | 122 | this.informationElement = undefined |
86 | } | 123 | } |
87 | 124 | ||
88 | private getPlaceholderElement () { | ||
89 | return document.getElementById('placeholder-preview') | ||
90 | } | ||
91 | |||
92 | private removeElement (element: HTMLElement) { | 125 | private removeElement (element: HTMLElement) { |
93 | element.parentElement.removeChild(element) | 126 | element.parentElement.removeChild(element) |
94 | } | 127 | } |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-options-builder.ts index 43ae22a3b..8a4e32444 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-options-builder.ts | |||
@@ -2,6 +2,7 @@ import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' | |||
2 | import { | 2 | import { |
3 | HTMLServerConfig, | 3 | HTMLServerConfig, |
4 | LiveVideo, | 4 | LiveVideo, |
5 | Storyboard, | ||
5 | Video, | 6 | Video, |
6 | VideoCaption, | 7 | VideoCaption, |
7 | VideoDetails, | 8 | VideoDetails, |
@@ -9,7 +10,7 @@ import { | |||
9 | VideoState, | 10 | VideoState, |
10 | VideoStreamingPlaylistType | 11 | VideoStreamingPlaylistType |
11 | } from '../../../../../shared/models' | 12 | } from '../../../../../shared/models' |
12 | import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' | 13 | import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' |
13 | import { | 14 | import { |
14 | getBoolOrDefault, | 15 | getBoolOrDefault, |
15 | getParamString, | 16 | getParamString, |
@@ -18,7 +19,7 @@ import { | |||
18 | logger, | 19 | logger, |
19 | peertubeLocalStorage, | 20 | peertubeLocalStorage, |
20 | UserLocalStorageKeys, | 21 | UserLocalStorageKeys, |
21 | videoRequiresAuth | 22 | videoRequiresUserAuth |
22 | } from '../../../root-helpers' | 23 | } from '../../../root-helpers' |
23 | import { PeerTubePlugin } from './peertube-plugin' | 24 | import { PeerTubePlugin } from './peertube-plugin' |
24 | import { PlayerHTML } from './player-html' | 25 | import { PlayerHTML } from './player-html' |
@@ -26,7 +27,7 @@ import { PlaylistTracker } from './playlist-tracker' | |||
26 | import { Translations } from './translations' | 27 | import { Translations } from './translations' |
27 | import { VideoFetcher } from './video-fetcher' | 28 | import { VideoFetcher } from './video-fetcher' |
28 | 29 | ||
29 | export class PlayerManagerOptions { | 30 | export class PlayerOptionsBuilder { |
30 | private autoplay: boolean | 31 | private autoplay: boolean |
31 | 32 | ||
32 | private controls: boolean | 33 | private controls: boolean |
@@ -140,10 +141,10 @@ export class PlayerManagerOptions { | |||
140 | 141 | ||
141 | if (modeParam) { | 142 | if (modeParam) { |
142 | if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' | 143 | if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' |
143 | else this.mode = 'webtorrent' | 144 | else this.mode = 'web-video' |
144 | } else { | 145 | } else { |
145 | if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' | 146 | if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' |
146 | else this.mode = 'webtorrent' | 147 | else this.mode = 'web-video' |
147 | } | 148 | } |
148 | } catch (err) { | 149 | } catch (err) { |
149 | logger.error('Cannot get params from URL.', err) | 150 | logger.error('Cannot get params from URL.', err) |
@@ -152,119 +153,140 @@ export class PlayerManagerOptions { | |||
152 | 153 | ||
153 | // --------------------------------------------------------------------------- | 154 | // --------------------------------------------------------------------------- |
154 | 155 | ||
155 | async getPlayerOptions (options: { | 156 | getPlayerConstructorOptions (options: { |
157 | serverConfig: HTMLServerConfig | ||
158 | authorizationHeader: () => string | ||
159 | }): PeerTubePlayerContructorOptions { | ||
160 | const { serverConfig, authorizationHeader } = options | ||
161 | |||
162 | return { | ||
163 | controls: this.controls, | ||
164 | controlBar: this.controlBar, | ||
165 | |||
166 | muted: this.muted, | ||
167 | loop: this.loop, | ||
168 | |||
169 | playbackRate: this.playbackRate, | ||
170 | |||
171 | inactivityTimeout: 2500, | ||
172 | videoViewIntervalMs: 5000, | ||
173 | metricsUrl: window.location.origin + '/api/v1/metrics/playback', | ||
174 | |||
175 | authorizationHeader, | ||
176 | |||
177 | playerElement: () => this.playerHTML.getPlayerElement(), | ||
178 | enableHotkeys: true, | ||
179 | |||
180 | peertubeLink: () => this.peertubeLink, | ||
181 | instanceName: serverConfig.instance.name, | ||
182 | |||
183 | theaterButton: false, | ||
184 | |||
185 | serverUrl: window.location.origin, | ||
186 | language: navigator.language, | ||
187 | |||
188 | pluginsManager: this.peertubePlugin.getPluginsManager(), | ||
189 | |||
190 | errorNotifier: () => { | ||
191 | // Empty, we don't have a notifier in the embed | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | async getPlayerLoadOptions (options: { | ||
156 | video: VideoDetails | 197 | video: VideoDetails |
157 | captionsResponse: Response | 198 | captionsResponse: Response |
199 | |||
200 | storyboardsResponse: Response | ||
201 | |||
158 | live?: LiveVideo | 202 | live?: LiveVideo |
159 | 203 | ||
204 | alreadyPlayed: boolean | ||
160 | forceAutoplay: boolean | 205 | forceAutoplay: boolean |
161 | 206 | ||
162 | authorizationHeader: () => string | ||
163 | videoFileToken: () => string | 207 | videoFileToken: () => string |
164 | 208 | ||
165 | serverConfig: HTMLServerConfig | 209 | videoPassword: () => string |
166 | 210 | requiresPassword: boolean | |
167 | autoplayFromPreviousVideo: boolean | ||
168 | 211 | ||
169 | translations: Translations | 212 | translations: Translations |
170 | 213 | ||
171 | playlistTracker?: PlaylistTracker | 214 | playlist?: { |
172 | playNextPlaylistVideo?: () => any | 215 | playlistTracker: PlaylistTracker |
173 | playPreviousPlaylistVideo?: () => any | 216 | playNext: () => any |
174 | onVideoUpdate?: (uuid: string) => any | 217 | playPrevious: () => any |
175 | }) { | 218 | onVideoUpdate: (uuid: string) => any |
219 | } | ||
220 | }): Promise<PeerTubePlayerLoadOptions> { | ||
176 | const { | 221 | const { |
177 | video, | 222 | video, |
178 | captionsResponse, | 223 | captionsResponse, |
179 | autoplayFromPreviousVideo, | ||
180 | videoFileToken, | 224 | videoFileToken, |
225 | videoPassword, | ||
226 | requiresPassword, | ||
181 | translations, | 227 | translations, |
228 | alreadyPlayed, | ||
182 | forceAutoplay, | 229 | forceAutoplay, |
183 | playlistTracker, | 230 | playlist, |
184 | live, | 231 | live, |
185 | authorizationHeader, | 232 | storyboardsResponse |
186 | serverConfig | ||
187 | } = options | 233 | } = options |
188 | 234 | ||
189 | const videoCaptions = await this.buildCaptions(captionsResponse, translations) | 235 | const [ videoCaptions, storyboard ] = await Promise.all([ |
190 | 236 | this.buildCaptions(captionsResponse, translations), | |
191 | const playerOptions: PeertubePlayerManagerOptions = { | 237 | this.buildStoryboard(storyboardsResponse) |
192 | common: { | 238 | ]) |
193 | // Autoplay in playlist mode | ||
194 | autoplay: autoplayFromPreviousVideo ? true : this.autoplay, | ||
195 | forceAutoplay, | ||
196 | 239 | ||
197 | controls: this.controls, | 240 | return { |
198 | controlBar: this.controlBar, | 241 | mode: this.mode, |
199 | |||
200 | muted: this.muted, | ||
201 | loop: this.loop, | ||
202 | 242 | ||
203 | p2pEnabled: this.p2pEnabled, | 243 | autoplay: forceAutoplay || alreadyPlayed || this.autoplay, |
244 | forceAutoplay, | ||
204 | 245 | ||
205 | captions: videoCaptions.length !== 0, | 246 | p2pEnabled: this.p2pEnabled, |
206 | subtitle: this.subtitle, | ||
207 | 247 | ||
208 | startTime: playlistTracker | 248 | subtitle: this.subtitle, |
209 | ? playlistTracker.getCurrentElement().startTimestamp | ||
210 | : this.startTime, | ||
211 | stopTime: playlistTracker | ||
212 | ? playlistTracker.getCurrentElement().stopTimestamp | ||
213 | : this.stopTime, | ||
214 | 249 | ||
215 | playbackRate: this.playbackRate, | 250 | storyboard, |
216 | 251 | ||
217 | videoCaptions, | 252 | startTime: playlist |
218 | inactivityTimeout: 2500, | 253 | ? playlist.playlistTracker.getCurrentElement().startTimestamp |
219 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | 254 | : this.startTime, |
220 | videoViewIntervalMs: 5000, | 255 | stopTime: playlist |
221 | metricsUrl: window.location.origin + '/api/v1/metrics/playback', | 256 | ? playlist.playlistTracker.getCurrentElement().stopTimestamp |
257 | : this.stopTime, | ||
222 | 258 | ||
223 | videoShortUUID: video.shortUUID, | 259 | videoCaptions, |
224 | videoUUID: video.uuid, | 260 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |
225 | 261 | ||
226 | playerElement: this.playerHTML.getPlayerElement(), | 262 | videoShortUUID: video.shortUUID, |
227 | onPlayerElementChange: (element: HTMLVideoElement) => { | 263 | videoUUID: video.uuid, |
228 | this.playerHTML.setPlayerElement(element) | ||
229 | }, | ||
230 | 264 | ||
231 | videoDuration: video.duration, | 265 | duration: video.duration, |
232 | enableHotkeys: true, | ||
233 | 266 | ||
234 | peertubeLink: this.peertubeLink, | 267 | poster: window.location.origin + video.previewPath, |
235 | instanceName: serverConfig.instance.name, | ||
236 | 268 | ||
237 | poster: window.location.origin + video.previewPath, | 269 | embedUrl: window.location.origin + video.embedPath, |
238 | theaterButton: false, | 270 | embedTitle: video.name, |
239 | 271 | ||
240 | serverUrl: window.location.origin, | 272 | requiresUserAuth: videoRequiresUserAuth(video), |
241 | language: navigator.language, | 273 | videoFileToken, |
242 | embedUrl: window.location.origin + video.embedPath, | ||
243 | embedTitle: video.name, | ||
244 | 274 | ||
245 | requiresAuth: videoRequiresAuth(video), | 275 | requiresPassword, |
246 | authorizationHeader, | 276 | videoPassword, |
247 | videoFileToken, | ||
248 | 277 | ||
249 | errorNotifier: () => { | 278 | ...this.buildLiveOptions(video, live), |
250 | // Empty, we don't have a notifier in the embed | ||
251 | }, | ||
252 | 279 | ||
253 | ...this.buildLiveOptions(video, live), | 280 | ...this.buildPlaylistOptions(playlist), |
254 | 281 | ||
255 | ...this.buildPlaylistOptions(options) | 282 | dock: this.buildDockOptions(video), |
256 | }, | ||
257 | 283 | ||
258 | webtorrent: { | 284 | webVideo: { |
259 | videoFiles: video.files | 285 | videoFiles: video.files |
260 | }, | 286 | }, |
261 | 287 | ||
262 | ...this.buildP2PMediaLoaderOptions(video), | 288 | hls: this.buildHLSOptions(video) |
263 | |||
264 | pluginsManager: this.peertubePlugin.getPluginsManager() | ||
265 | } | 289 | } |
266 | |||
267 | return playerOptions | ||
268 | } | 290 | } |
269 | 291 | ||
270 | private buildLiveOptions (video: VideoDetails, live: LiveVideo) { | 292 | private buildLiveOptions (video: VideoDetails, live: LiveVideo) { |
@@ -278,15 +300,39 @@ export class PlayerManagerOptions { | |||
278 | } | 300 | } |
279 | } | 301 | } |
280 | 302 | ||
281 | private buildPlaylistOptions (options: { | 303 | private async buildStoryboard (storyboardsResponse: Response) { |
282 | playlistTracker?: PlaylistTracker | 304 | const { storyboards } = await storyboardsResponse.json() as { storyboards: Storyboard[] } |
283 | playNextPlaylistVideo?: () => any | 305 | if (!storyboards || storyboards.length === 0) return undefined |
284 | playPreviousPlaylistVideo?: () => any | 306 | |
285 | onVideoUpdate?: (uuid: string) => any | 307 | return { |
308 | url: window.location.origin + storyboards[0].storyboardPath, | ||
309 | height: storyboards[0].spriteHeight, | ||
310 | width: storyboards[0].spriteWidth, | ||
311 | interval: storyboards[0].spriteDuration | ||
312 | } | ||
313 | } | ||
314 | |||
315 | private buildPlaylistOptions (options?: { | ||
316 | playlistTracker: PlaylistTracker | ||
317 | playNext: () => any | ||
318 | playPrevious: () => any | ||
319 | onVideoUpdate: (uuid: string) => any | ||
286 | }) { | 320 | }) { |
287 | const { playlistTracker, playNextPlaylistVideo, playPreviousPlaylistVideo, onVideoUpdate } = options | 321 | if (!options) { |
322 | return { | ||
323 | nextVideo: { | ||
324 | enabled: false, | ||
325 | displayControlBarButton: false, | ||
326 | getVideoTitle: () => '' | ||
327 | }, | ||
328 | previousVideo: { | ||
329 | enabled: false, | ||
330 | displayControlBarButton: false | ||
331 | } | ||
332 | } | ||
333 | } | ||
288 | 334 | ||
289 | if (!playlistTracker) return {} | 335 | const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options |
290 | 336 | ||
291 | return { | 337 | return { |
292 | playlist: { | 338 | playlist: { |
@@ -302,27 +348,37 @@ export class PlayerManagerOptions { | |||
302 | } | 348 | } |
303 | }, | 349 | }, |
304 | 350 | ||
305 | nextVideo: () => playNextPlaylistVideo(), | 351 | previousVideo: { |
306 | hasNextVideo: () => playlistTracker.hasNextPlaylistElement(), | 352 | enabled: playlistTracker.hasPreviousPlaylistElement(), |
353 | handler: () => playPrevious(), | ||
354 | displayControlBarButton: true | ||
355 | }, | ||
307 | 356 | ||
308 | previousVideo: () => playPreviousPlaylistVideo(), | 357 | nextVideo: { |
309 | hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement() | 358 | enabled: playlistTracker.hasNextPlaylistElement(), |
359 | handler: () => playNext(), | ||
360 | getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name, | ||
361 | displayControlBarButton: true | ||
362 | }, | ||
363 | |||
364 | upnext: { | ||
365 | isEnabled: () => true, | ||
366 | isSuspended: () => false, | ||
367 | timeout: 0 | ||
368 | } | ||
310 | } | 369 | } |
311 | } | 370 | } |
312 | 371 | ||
313 | private buildP2PMediaLoaderOptions (video: VideoDetails) { | 372 | private buildHLSOptions (video: VideoDetails): HLSOptions { |
314 | if (this.mode !== 'p2p-media-loader') return {} | ||
315 | |||
316 | const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 373 | const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
374 | if (!hlsPlaylist) return undefined | ||
317 | 375 | ||
318 | return { | 376 | return { |
319 | p2pMediaLoader: { | 377 | playlistUrl: hlsPlaylist.playlistUrl, |
320 | playlistUrl: hlsPlaylist.playlistUrl, | 378 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, |
321 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | 379 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), |
322 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | 380 | trackerAnnounce: video.trackerUrls, |
323 | trackerAnnounce: video.trackerUrls, | 381 | videoFiles: hlsPlaylist.files |
324 | videoFiles: hlsPlaylist.files | ||
325 | } as P2PMediaLoaderOptions | ||
326 | } | 382 | } |
327 | } | 383 | } |
328 | 384 | ||
@@ -344,6 +400,35 @@ export class PlayerManagerOptions { | |||
344 | 400 | ||
345 | // --------------------------------------------------------------------------- | 401 | // --------------------------------------------------------------------------- |
346 | 402 | ||
403 | private buildDockOptions (videoInfo: VideoDetails) { | ||
404 | if (!this.hasControls()) return undefined | ||
405 | |||
406 | const title = this.hasTitle() | ||
407 | ? videoInfo.name | ||
408 | : undefined | ||
409 | |||
410 | const description = this.hasWarningTitle() && this.hasP2PEnabled() | ||
411 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | ||
412 | : undefined | ||
413 | |||
414 | if (!title && !description) return | ||
415 | |||
416 | const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) | ||
417 | const avatar = availableAvatars.length !== 0 | ||
418 | ? availableAvatars[0] | ||
419 | : undefined | ||
420 | |||
421 | return { | ||
422 | title, | ||
423 | description, | ||
424 | avatarUrl: title && avatar | ||
425 | ? avatar.path | ||
426 | : undefined | ||
427 | } | ||
428 | } | ||
429 | |||
430 | // --------------------------------------------------------------------------- | ||
431 | |||
347 | private isP2PEnabled (config: HTMLServerConfig, video: Video) { | 432 | private isP2PEnabled (config: HTMLServerConfig, video: Video) { |
348 | const userP2PEnabled = getBoolOrDefault( | 433 | const userP2PEnabled = getBoolOrDefault( |
349 | peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), | 434 | peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), |
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts index cf6d12831..7fb94fbf3 100644 --- a/client/src/standalone/videos/shared/video-fetcher.ts +++ b/client/src/standalone/videos/shared/video-fetcher.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' | 1 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' |
2 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
3 | import { PeerTubeServerError } from '../../../types' | ||
3 | import { AuthHTTP } from './auth-http' | 4 | import { AuthHTTP } from './auth-http' |
4 | 5 | ||
5 | export class VideoFetcher { | 6 | export class VideoFetcher { |
@@ -8,8 +9,8 @@ export class VideoFetcher { | |||
8 | 9 | ||
9 | } | 10 | } |
10 | 11 | ||
11 | async loadVideo (videoId: string) { | 12 | async loadVideo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }) { |
12 | const videoPromise = this.loadVideoInfo(videoId) | 13 | const videoPromise = this.loadVideoInfo({ videoId, videoPassword }) |
13 | 14 | ||
14 | let videoResponse: Response | 15 | let videoResponse: Response |
15 | let isResponseOk: boolean | 16 | let isResponseOk: boolean |
@@ -27,13 +28,17 @@ export class VideoFetcher { | |||
27 | if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) { | 28 | if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) { |
28 | throw new Error('This video does not exist.') | 29 | throw new Error('This video does not exist.') |
29 | } | 30 | } |
30 | 31 | if (videoResponse?.status === HttpStatusCode.FORBIDDEN_403) { | |
32 | const res = await videoResponse.json() | ||
33 | throw new PeerTubeServerError(res.message, res.code) | ||
34 | } | ||
31 | throw new Error('We cannot fetch the video. Please try again later.') | 35 | throw new Error('We cannot fetch the video. Please try again later.') |
32 | } | 36 | } |
33 | 37 | ||
34 | const captionsPromise = this.loadVideoCaptions(videoId) | 38 | const captionsPromise = this.loadVideoCaptions({ videoId, videoPassword }) |
39 | const storyboardsPromise = this.loadStoryboards(videoId) | ||
35 | 40 | ||
36 | return { captionsPromise, videoResponse } | 41 | return { captionsPromise, storyboardsPromise, videoResponse } |
37 | } | 42 | } |
38 | 43 | ||
39 | loadLive (video: VideoDetails) { | 44 | loadLive (video: VideoDetails) { |
@@ -41,8 +46,8 @@ export class VideoFetcher { | |||
41 | .then(res => res.json() as Promise<LiveVideo>) | 46 | .then(res => res.json() as Promise<LiveVideo>) |
42 | } | 47 | } |
43 | 48 | ||
44 | loadVideoToken (video: VideoDetails) { | 49 | loadVideoToken (video: VideoDetails, videoPassword?: string) { |
45 | return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }) | 50 | return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }, videoPassword) |
46 | .then(res => res.json() as Promise<VideoToken>) | 51 | .then(res => res.json() as Promise<VideoToken>) |
47 | .then(token => token.files.token) | 52 | .then(token => token.files.token) |
48 | } | 53 | } |
@@ -51,12 +56,12 @@ export class VideoFetcher { | |||
51 | return this.getVideoUrl(videoUUID) + '/views' | 56 | return this.getVideoUrl(videoUUID) + '/views' |
52 | } | 57 | } |
53 | 58 | ||
54 | private loadVideoInfo (videoId: string): Promise<Response> { | 59 | private loadVideoInfo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> { |
55 | return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }) | 60 | return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }, videoPassword) |
56 | } | 61 | } |
57 | 62 | ||
58 | private loadVideoCaptions (videoId: string): Promise<Response> { | 63 | private loadVideoCaptions ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> { |
59 | return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }) | 64 | return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }, videoPassword) |
60 | } | 65 | } |
61 | 66 | ||
62 | private getVideoUrl (id: string) { | 67 | private getVideoUrl (id: string) { |
@@ -67,6 +72,14 @@ export class VideoFetcher { | |||
67 | return window.location.origin + '/api/v1/videos/live/' + videoId | 72 | return window.location.origin + '/api/v1/videos/live/' + videoId |
68 | } | 73 | } |
69 | 74 | ||
75 | private loadStoryboards (videoUUID: string): Promise<Response> { | ||
76 | return this.http.fetch(this.getStoryboardsUrl(videoUUID), { optionalAuth: true }) | ||
77 | } | ||
78 | |||
79 | private getStoryboardsUrl (videoId: string) { | ||
80 | return window.location.origin + '/api/v1/videos/' + videoId + '/storyboards' | ||
81 | } | ||
82 | |||
70 | private getVideoTokenUrl (id: string) { | 83 | private getVideoTokenUrl (id: string) { |
71 | return this.getVideoUrl(id) + '/token' | 84 | return this.getVideoUrl(id) + '/token' |
72 | } | 85 | } |
diff --git a/client/src/standalone/videos/test-embed.ts b/client/src/standalone/videos/test-embed.ts index b34df11ee..b7a283c4d 100644 --- a/client/src/standalone/videos/test-embed.ts +++ b/client/src/standalone/videos/test-embed.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import './test-embed.scss' | 1 | import './test-embed.scss' |
2 | import { PeerTubeResolution, PlayerEventType } from '../player/definitions' | 2 | import { PeerTubeResolution, PlayerEventType } from '../embed-player-api/definitions' |
3 | import { PeerTubePlayer } from '../player/player' | 3 | import { PeerTubePlayer } from '../embed-player-api/player' |
4 | import { logger } from '../../root-helpers' | 4 | import { logger } from '../../root-helpers' |
5 | 5 | ||
6 | window.addEventListener('load', async () => { | 6 | window.addEventListener('load', async () => { |
diff --git a/client/src/types/index.ts b/client/src/types/index.ts index 5508515fd..60564496c 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './client-script.model' | 1 | export * from './client-script.model' |
2 | export * from './server-error.model' | ||
2 | export * from './job-state-client.type' | 3 | export * from './job-state-client.type' |
3 | export * from './job-type-client.type' | 4 | export * from './job-type-client.type' |
4 | export * from './link.type' | 5 | export * from './link.type' |
diff --git a/client/src/types/server-error.model.ts b/client/src/types/server-error.model.ts new file mode 100644 index 000000000..4a57287fe --- /dev/null +++ b/client/src/types/server-error.model.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { ServerErrorCode } from '@shared/models/index' | ||
2 | |||
3 | export class PeerTubeServerError extends Error { | ||
4 | serverCode: ServerErrorCode | ||
5 | |||
6 | constructor (message: string, serverCode: ServerErrorCode) { | ||
7 | super(message) | ||
8 | this.name = 'CustomError' | ||
9 | this.serverCode = serverCode | ||
10 | } | ||
11 | } | ||
diff --git a/client/tsconfig.json b/client/tsconfig.json index 785ed1c6c..5dee39362 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json | |||
@@ -61,18 +61,9 @@ | |||
61 | "fs": [ | 61 | "fs": [ |
62 | "src/shims/noop.ts" | 62 | "src/shims/noop.ts" |
63 | ], | 63 | ], |
64 | "http": [ | ||
65 | "src/shims/http.ts" | ||
66 | ], | ||
67 | "https": [ | ||
68 | "src/shims/https.ts" | ||
69 | ], | ||
70 | "path": [ | 64 | "path": [ |
71 | "src/shims/path.ts" | 65 | "src/shims/path.ts" |
72 | ], | 66 | ], |
73 | "stream": [ | ||
74 | "src/shims/stream.ts" | ||
75 | ], | ||
76 | "crypto": [ | 67 | "crypto": [ |
77 | "src/shims/noop.ts" | 68 | "src/shims/noop.ts" |
78 | ] | 69 | ] |
@@ -89,8 +80,7 @@ | |||
89 | ], | 80 | ], |
90 | "exclude": [ | 81 | "exclude": [ |
91 | "../node_modules", | 82 | "../node_modules", |
92 | "../server", | 83 | "../server" |
93 | "node_modules" | ||
94 | ], | 84 | ], |
95 | "angularCompilerOptions": { | 85 | "angularCompilerOptions": { |
96 | "strictInjectionParameters": true, | 86 | "strictInjectionParameters": true, |
diff --git a/client/webpack/webpack.video-embed.js b/client/webpack/webpack.video-embed.js index e25677872..47d440c25 100644 --- a/client/webpack/webpack.video-embed.js +++ b/client/webpack/webpack.video-embed.js | |||
@@ -10,7 +10,7 @@ module.exports = function () { | |||
10 | const configuration = { | 10 | const configuration = { |
11 | entry: { | 11 | entry: { |
12 | 'video-embed': './src/standalone/videos/embed.ts', | 12 | 'video-embed': './src/standalone/videos/embed.ts', |
13 | 'player': './src/standalone/player/player.ts', | 13 | 'player': './src/standalone/embed-player-api/player.ts', |
14 | 'test-embed': './src/standalone/videos/test-embed.ts' | 14 | 'test-embed': './src/standalone/videos/test-embed.ts' |
15 | }, | 15 | }, |
16 | 16 | ||
@@ -36,10 +36,7 @@ module.exports = function () { | |||
36 | 36 | ||
37 | fallback: { | 37 | fallback: { |
38 | fs: [ path.resolve('src/shims/noop.ts') ], | 38 | fs: [ path.resolve('src/shims/noop.ts') ], |
39 | http: [ path.resolve('src/shims/http.ts') ], | ||
40 | https: [ path.resolve('src/shims/https.ts') ], | ||
41 | path: [ path.resolve('src/shims/path.ts') ], | 39 | path: [ path.resolve('src/shims/path.ts') ], |
42 | stream: [ path.resolve('src/shims/stream.ts') ], | ||
43 | crypto: [ path.resolve('src/shims/noop.ts') ] | 40 | crypto: [ path.resolve('src/shims/noop.ts') ] |
44 | } | 41 | } |
45 | }, | 42 | }, |
diff --git a/client/yarn.lock b/client/yarn.lock index aeb13a7b5..5c9f4bf42 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -1647,6 +1647,14 @@ | |||
1647 | "@formatjs/intl-localematcher" "0.2.32" | 1647 | "@formatjs/intl-localematcher" "0.2.32" |
1648 | tslib "^2.4.0" | 1648 | tslib "^2.4.0" |
1649 | 1649 | ||
1650 | "@formatjs/ecma402-abstract@1.16.0": | ||
1651 | version "1.16.0" | ||
1652 | resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.16.0.tgz#15a0baa8401880d4010eb93440d996e896ca251c" | ||
1653 | integrity sha512-qIH2cmG/oHGrVdApbqDf6/YR+B2A4NdkBjKLeq369OMVkqMFsC5oPSP1xpiyL1cAn+PbNEZHxwOVMYD/C76c6g== | ||
1654 | dependencies: | ||
1655 | "@formatjs/intl-localematcher" "0.3.0" | ||
1656 | tslib "^2.4.0" | ||
1657 | |||
1650 | "@formatjs/fast-memoize@2.0.1": | 1658 | "@formatjs/fast-memoize@2.0.1": |
1651 | version "2.0.1" | 1659 | version "2.0.1" |
1652 | resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b" | 1660 | resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b" |
@@ -1671,6 +1679,30 @@ | |||
1671 | "@formatjs/ecma402-abstract" "1.15.0" | 1679 | "@formatjs/ecma402-abstract" "1.15.0" |
1672 | tslib "^2.4.0" | 1680 | tslib "^2.4.0" |
1673 | 1681 | ||
1682 | "@formatjs/intl-enumerator@1.3.1": | ||
1683 | version "1.3.1" | ||
1684 | resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.3.1.tgz#32f0a3b5aece244977aad16fe1cd5c205defc7f8" | ||
1685 | integrity sha512-UMuD1tNRb8JC+mZo0KeuSntJNie0+TLxXl/1QxRIRMR7z2UuJgphrK/UTUibAx9hjywL1qGdNNhD6QX//pvNyA== | ||
1686 | dependencies: | ||
1687 | tslib "^2.4.0" | ||
1688 | |||
1689 | "@formatjs/intl-getcanonicallocales@2.2.1": | ||
1690 | version "2.2.1" | ||
1691 | resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.2.1.tgz#0d70251b16bec06c1c4a40b37892ea5b51e3a2bb" | ||
1692 | integrity sha512-KooqmyY+Mhq3ioASPzoU6p6Cy9Mx+cWSVQSP6lF+vEW2tiaN90ti08cp82p1dzFschenduOYgPKrNcBpsDi6+g== | ||
1693 | dependencies: | ||
1694 | tslib "^2.4.0" | ||
1695 | |||
1696 | "@formatjs/intl-locale@^3.3.1": | ||
1697 | version "3.3.1" | ||
1698 | resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.3.1.tgz#2b86e85319913e0bedfcd64884be33bc4bcef73e" | ||
1699 | integrity sha512-Rg3BLIjMzVxBcZCsPhvwIrcfc+UVEzPZPKnvoOLh6KNNWrzWnRo0ORQEE/KRDyvYZxAmALV/GHCcRl+qlchKuw== | ||
1700 | dependencies: | ||
1701 | "@formatjs/ecma402-abstract" "1.16.0" | ||
1702 | "@formatjs/intl-enumerator" "1.3.1" | ||
1703 | "@formatjs/intl-getcanonicallocales" "2.2.1" | ||
1704 | tslib "^2.4.0" | ||
1705 | |||
1674 | "@formatjs/intl-localematcher@0.2.32": | 1706 | "@formatjs/intl-localematcher@0.2.32": |
1675 | version "0.2.32" | 1707 | version "0.2.32" |
1676 | resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" | 1708 | resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" |
@@ -1678,6 +1710,22 @@ | |||
1678 | dependencies: | 1710 | dependencies: |
1679 | tslib "^2.4.0" | 1711 | tslib "^2.4.0" |
1680 | 1712 | ||
1713 | "@formatjs/intl-localematcher@0.3.0": | ||
1714 | version "0.3.0" | ||
1715 | resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.3.0.tgz#9ad570d90d302b60bcbe78efd5fcd7593c440579" | ||
1716 | integrity sha512-NFoxXX3dtZ6B53NlCErq181NxN/noMZOWKHfcEPQRNfV0a19THxyjxu2RTSNS3532wGm6fOdid5qsBQWg0Rhtw== | ||
1717 | dependencies: | ||
1718 | tslib "^2.4.0" | ||
1719 | |||
1720 | "@formatjs/intl-pluralrules@^5.2.2": | ||
1721 | version "5.2.2" | ||
1722 | resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.2.tgz#6322d20a6d0172459e4faf4b0f06603c931673aa" | ||
1723 | integrity sha512-mEbnbRzsSCIYqaBmrmUlOsPu5MG6KfMcnzekPzUrUucX2dNiI1KWBGHK6IoXl5c8zx60L1NXJ6cSQ7akoc15SQ== | ||
1724 | dependencies: | ||
1725 | "@formatjs/ecma402-abstract" "1.15.0" | ||
1726 | "@formatjs/intl-localematcher" "0.2.32" | ||
1727 | tslib "^2.4.0" | ||
1728 | |||
1681 | "@gar/promisify@^1.1.3": | 1729 | "@gar/promisify@^1.1.3": |
1682 | version "1.1.3" | 1730 | version "1.1.3" |
1683 | resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" | 1731 | resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" |
@@ -2169,13 +2217,6 @@ | |||
2169 | "@tufjs/canonical-json" "1.0.0" | 2217 | "@tufjs/canonical-json" "1.0.0" |
2170 | minimatch "^9.0.0" | 2218 | minimatch "^9.0.0" |
2171 | 2219 | ||
2172 | "@types/bittorrent-protocol@*": | ||
2173 | version "3.1.2" | ||
2174 | resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-3.1.2.tgz#884cf1589fa8b1f7a6cc39bd516922a96ce08221" | ||
2175 | integrity sha512-7k9nivNeG7Sc8wVuBs+XjBp2u7pH8tqW3BB93/SAg3xht/cZEK+Rqkj79xSyJqyj86eA0F6n85EKkkyGki8afg== | ||
2176 | dependencies: | ||
2177 | "@types/node" "*" | ||
2178 | |||
2179 | "@types/body-parser@*": | 2220 | "@types/body-parser@*": |
2180 | version "1.19.2" | 2221 | version "1.19.2" |
2181 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" | 2222 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" |
@@ -2346,13 +2387,6 @@ | |||
2346 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" | 2387 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" |
2347 | integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== | 2388 | integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== |
2348 | 2389 | ||
2349 | "@types/magnet-uri@*": | ||
2350 | version "5.1.3" | ||
2351 | resolved "https://registry.yarnpkg.com/@types/magnet-uri/-/magnet-uri-5.1.3.tgz#cdf974721012bd758c0f559cabcad7bab87f9008" | ||
2352 | integrity sha512-FvJN1yYdLhvU6zWJ2YnWQ2GnpFLsA8bt+85WY0tLh6ehzGNrvBorjlcc53/zY43r/IKn+ctFs1nt7andwGnQCQ== | ||
2353 | dependencies: | ||
2354 | "@types/node" "*" | ||
2355 | |||
2356 | "@types/markdown-it@^12.0.1": | 2390 | "@types/markdown-it@^12.0.1": |
2357 | version "12.2.3" | 2391 | version "12.2.3" |
2358 | resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" | 2392 | resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" |
@@ -2411,22 +2445,6 @@ | |||
2411 | resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" | 2445 | resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" |
2412 | integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== | 2446 | integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== |
2413 | 2447 | ||
2414 | "@types/parse-torrent-file@*": | ||
2415 | version "4.0.3" | ||
2416 | resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.3.tgz#045b023426d168e0253c932cb782b231b1ee2d62" | ||
2417 | integrity sha512-dFkPnJPKiFWiGX+HXmyTVt2js3k0d9dThmUxX8nfGC22hbyZ5BTmetsEl45sQhHLcFo43njVrIKMXM3F1ahXRw== | ||
2418 | dependencies: | ||
2419 | "@types/node" "*" | ||
2420 | |||
2421 | "@types/parse-torrent@*": | ||
2422 | version "5.8.4" | ||
2423 | resolved "https://registry.yarnpkg.com/@types/parse-torrent/-/parse-torrent-5.8.4.tgz#c095834a9a815507c59014a79517ad403e4329d0" | ||
2424 | integrity sha512-FdKs5yN5iYO5Cu9gVz1Zl30CbZe6HTsqloWmCf+LfbImgSzlsUkov2+npQWCQSQ3zi/a2G5C824K0UpZ2sRufA== | ||
2425 | dependencies: | ||
2426 | "@types/magnet-uri" "*" | ||
2427 | "@types/node" "*" | ||
2428 | "@types/parse-torrent-file" "*" | ||
2429 | |||
2430 | "@types/prop-types@*": | 2448 | "@types/prop-types@*": |
2431 | version "15.7.5" | 2449 | version "15.7.5" |
2432 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" | 2450 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" |
@@ -2510,13 +2528,6 @@ | |||
2510 | dependencies: | 2528 | dependencies: |
2511 | "@types/node" "*" | 2529 | "@types/node" "*" |
2512 | 2530 | ||
2513 | "@types/simple-peer@*": | ||
2514 | version "9.11.5" | ||
2515 | resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.5.tgz#6baa00edbbd0f632f8561e8fb03b4d21d62f076e" | ||
2516 | integrity sha512-haXgWcAa3Y3Sn+T8lzkE4ErQUpYzhW6Cz2lh00RhQTyWt+xZ3s87wJPztUxlqSdFRqGhe2MQIBd0XsyHP3No4w== | ||
2517 | dependencies: | ||
2518 | "@types/node" "*" | ||
2519 | |||
2520 | "@types/sockjs@^0.3.33": | 2531 | "@types/sockjs@^0.3.33": |
2521 | version "0.3.33" | 2532 | version "0.3.33" |
2522 | resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" | 2533 | resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" |
@@ -2534,16 +2545,6 @@ | |||
2534 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.51.tgz#ce69e02681ed6ed8abe61bb3802dd032a74d63e8" | 2545 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.51.tgz#ce69e02681ed6ed8abe61bb3802dd032a74d63e8" |
2535 | integrity sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ== | 2546 | integrity sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ== |
2536 | 2547 | ||
2537 | "@types/webtorrent@^0.109.0": | ||
2538 | version "0.109.3" | ||
2539 | resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.109.3.tgz#95df708d98bcea235b37f49a9a348b11f3511670" | ||
2540 | integrity sha512-EJLsxMEcEjPXHcBqL6TRAbUwIpxAul5ULrXHJ0zwig7Oe70FS6dAzCWLq4MBafX3QrQG1DzGAS0fS8iJEOjD0g== | ||
2541 | dependencies: | ||
2542 | "@types/bittorrent-protocol" "*" | ||
2543 | "@types/node" "*" | ||
2544 | "@types/parse-torrent" "*" | ||
2545 | "@types/simple-peer" "*" | ||
2546 | |||
2547 | "@types/which@^2.0.1": | 2548 | "@types/which@^2.0.1": |
2548 | version "2.0.2" | 2549 | version "2.0.2" |
2549 | resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" | 2550 | resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" |
@@ -3076,14 +3077,6 @@ | |||
3076 | resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f" | 3077 | resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f" |
3077 | integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A== | 3078 | integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A== |
3078 | 3079 | ||
3079 | "@webtorrent/http-node@^1.3.0": | ||
3080 | version "1.3.0" | ||
3081 | resolved "https://registry.yarnpkg.com/@webtorrent/http-node/-/http-node-1.3.0.tgz#bd8aacf13f08bb19ee25b5f5364e8d261eaa5c3c" | ||
3082 | integrity sha512-GWZQKroPES4z91Ijx6zsOsb7+USOxjy66s8AoTWg0HiBBdfnbtf9aeh3Uav0MgYn4BL8Q7tVSUpd0gGpngKGEQ== | ||
3083 | dependencies: | ||
3084 | freelist "^1.0.3" | ||
3085 | http-parser-js "^0.4.3" | ||
3086 | |||
3087 | "@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.7": | 3080 | "@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.7": |
3088 | version "0.8.7" | 3081 | version "0.8.7" |
3089 | resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" | 3082 | resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" |
@@ -3157,7 +3150,7 @@ acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: | |||
3157 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" | 3150 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" |
3158 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== | 3151 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== |
3159 | 3152 | ||
3160 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.4: | 3153 | addr-to-ip-port@^1.0.1: |
3161 | version "1.5.4" | 3154 | version "1.5.4" |
3162 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" | 3155 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" |
3163 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== | 3156 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== |
@@ -3535,11 +3528,6 @@ axobject-query@3.1.1: | |||
3535 | dependencies: | 3528 | dependencies: |
3536 | deep-equal "^2.0.5" | 3529 | deep-equal "^2.0.5" |
3537 | 3530 | ||
3538 | b4a@^1.3.1: | ||
3539 | version "1.6.4" | ||
3540 | resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" | ||
3541 | integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== | ||
3542 | |||
3543 | babel-loader@9.1.2, babel-loader@^9.1.0: | 3531 | babel-loader@9.1.2, babel-loader@^9.1.0: |
3544 | version "9.1.2" | 3532 | version "9.1.2" |
3545 | resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" | 3533 | resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" |
@@ -3608,16 +3596,11 @@ batch@0.6.1: | |||
3608 | resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" | 3596 | resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" |
3609 | integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== | 3597 | integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== |
3610 | 3598 | ||
3611 | bencode@^2.0.0, bencode@^2.0.1, bencode@^2.0.2, bencode@^2.0.3: | 3599 | bencode@^2.0.1: |
3612 | version "2.0.3" | 3600 | version "2.0.3" |
3613 | resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.3.tgz#89b9c80ea1b8573554915a7d0c15f62b0aa7fc52" | 3601 | resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.3.tgz#89b9c80ea1b8573554915a7d0c15f62b0aa7fc52" |
3614 | integrity sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w== | 3602 | integrity sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w== |
3615 | 3603 | ||
3616 | bep53-range@^1.1.0: | ||
3617 | version "1.1.1" | ||
3618 | resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-1.1.1.tgz#20fd125b00a413254a77d42f63a43750ca7e64ac" | ||
3619 | integrity sha512-ct6s33iiwRCUPp9KXnJ4QMWDgHIgaw36caK/5XEQ9L8dCzSQlJt1Vk6VmHh1VD4AlGCAI4C2zmtfItifBBPrhQ== | ||
3620 | |||
3621 | big-integer@^1.6.17: | 3604 | big-integer@^1.6.17: |
3622 | version "1.6.51" | 3605 | version "1.6.51" |
3623 | resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" | 3606 | resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" |
@@ -3633,11 +3616,6 @@ binary-extensions@^2.0.0: | |||
3633 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" | 3616 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" |
3634 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== | 3617 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== |
3635 | 3618 | ||
3636 | binary-search@^1.3.4: | ||
3637 | version "1.3.6" | ||
3638 | resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" | ||
3639 | integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== | ||
3640 | |||
3641 | binary@~0.3.0: | 3619 | binary@~0.3.0: |
3642 | version "0.3.0" | 3620 | version "0.3.0" |
3643 | resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" | 3621 | resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" |
@@ -3646,54 +3624,11 @@ binary@~0.3.0: | |||
3646 | buffers "~0.1.1" | 3624 | buffers "~0.1.1" |
3647 | chainsaw "~0.1.0" | 3625 | chainsaw "~0.1.0" |
3648 | 3626 | ||
3649 | bitfield@^4.0.0, bitfield@^4.1.0: | ||
3650 | version "4.1.0" | ||
3651 | resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.1.0.tgz#77f3ef4e915e58adaf758b23cbff156959e0fd8e" | ||
3652 | integrity sha512-6cEDG3K+PK9f+B7WyhWYjp09bqSa+uaAaecVA7Y5giFixyVe1s6HKGnvOqYNR4Mi4fBMjfDPLBpHkKvzzgP7kg== | ||
3653 | |||
3654 | bittorrent-dht@^10.0.4, bittorrent-dht@^10.0.7: | ||
3655 | version "10.0.7" | ||
3656 | resolved "https://registry.yarnpkg.com/bittorrent-dht/-/bittorrent-dht-10.0.7.tgz#fbe0f56349e7aab951d6d8625e0f78495ad74684" | ||
3657 | integrity sha512-o6elCANGteECXz82LFqG1Ov2fG4uNzfUU7pBMx9ixxKUh99ZXNrhbiNLRNN2F2vBnqKSN7SHlUW4LJ5Z2u1eKw== | ||
3658 | dependencies: | ||
3659 | bencode "^2.0.3" | ||
3660 | debug "^4.3.4" | ||
3661 | k-bucket "^5.1.0" | ||
3662 | k-rpc "^5.1.0" | ||
3663 | last-one-wins "^1.0.4" | ||
3664 | lru "^3.1.0" | ||
3665 | randombytes "^2.1.0" | ||
3666 | record-cache "^1.2.0" | ||
3667 | simple-sha1 "^3.1.0" | ||
3668 | |||
3669 | bittorrent-lsd@^1.1.1: | ||
3670 | version "1.1.1" | ||
3671 | resolved "https://registry.yarnpkg.com/bittorrent-lsd/-/bittorrent-lsd-1.1.1.tgz#427044bfcc05d0c2f286b6d1db70a91c04daa0c9" | ||
3672 | integrity sha512-dWxU2Mr2lU6jzIKgZrTsXgeXDCIcYpR1b6f2n89fn7juwPAYbNU04OgWjcQPLiNliY0filsX5CQAWntVErpk+Q== | ||
3673 | dependencies: | ||
3674 | chrome-dgram "^3.0.6" | ||
3675 | debug "^4.2.0" | ||
3676 | |||
3677 | bittorrent-peerid@^1.3.3: | 3627 | bittorrent-peerid@^1.3.3: |
3678 | version "1.3.6" | 3628 | version "1.3.6" |
3679 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.6.tgz#3688705a64937a8176ac2ded1178fc7bd91b61db" | 3629 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.6.tgz#3688705a64937a8176ac2ded1178fc7bd91b61db" |
3680 | integrity sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg== | 3630 | integrity sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg== |
3681 | 3631 | ||
3682 | bittorrent-protocol@^3.5.5: | ||
3683 | version "3.5.5" | ||
3684 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.5.5.tgz#d89233da11996d8978146f8b80ed91fec9e0e9b8" | ||
3685 | integrity sha512-cfzO//WtJGNLHXS58a4exJCSq1U0dkP2DZCQxgADInYFPdOfV1EmtpEN9toLOluVCXJRYAdwW5H6Li/hrn697A== | ||
3686 | dependencies: | ||
3687 | bencode "^2.0.2" | ||
3688 | bitfield "^4.0.0" | ||
3689 | debug "^4.3.4" | ||
3690 | randombytes "^2.1.0" | ||
3691 | rc4 "^0.1.5" | ||
3692 | readable-stream "^3.6.0" | ||
3693 | simple-sha1 "^3.1.0" | ||
3694 | speedometer "^1.1.0" | ||
3695 | unordered-array-remove "^1.0.2" | ||
3696 | |||
3697 | bittorrent-tracker@^9.19.0: | 3632 | bittorrent-tracker@^9.19.0: |
3698 | version "9.19.0" | 3633 | version "9.19.0" |
3699 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.19.0.tgz#2266bfa8a45a57b09f8d8b184710ba531712d8ef" | 3634 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.19.0.tgz#2266bfa8a45a57b09f8d8b184710ba531712d8ef" |
@@ -3735,23 +3670,6 @@ bl@^4.0.3, bl@^4.1.0: | |||
3735 | inherits "^2.0.4" | 3670 | inherits "^2.0.4" |
3736 | readable-stream "^3.4.0" | 3671 | readable-stream "^3.4.0" |
3737 | 3672 | ||
3738 | blob-to-buffer@^1.2.9: | ||
3739 | version "1.2.9" | ||
3740 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" | ||
3741 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== | ||
3742 | |||
3743 | block-iterator@^1.0.1: | ||
3744 | version "1.1.1" | ||
3745 | resolved "https://registry.yarnpkg.com/block-iterator/-/block-iterator-1.1.1.tgz#3c8a94e083febf8da59d8baad1006ffee1a74694" | ||
3746 | integrity sha512-DrjdVWZemVO4iBf4tiOXjUrY5cNesjzy0t7sIiu2rdl8cOCHRxAgKjSJFc3vBZYYMMmshUAxajl8QQh/uxXTKQ== | ||
3747 | |||
3748 | block-stream2@^2.0.0: | ||
3749 | version "2.1.0" | ||
3750 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-2.1.0.tgz#ac0c5ef4298b3857796e05be8ebed72196fa054b" | ||
3751 | integrity sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg== | ||
3752 | dependencies: | ||
3753 | readable-stream "^3.4.0" | ||
3754 | |||
3755 | bluebird@~3.4.1: | 3673 | bluebird@~3.4.1: |
3756 | version "3.4.7" | 3674 | version "3.4.7" |
3757 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" | 3675 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" |
@@ -3827,11 +3745,6 @@ browser-stdout@1.3.1: | |||
3827 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" | 3745 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" |
3828 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== | 3746 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== |
3829 | 3747 | ||
3830 | browserify-package-json@^1.0.0: | ||
3831 | version "1.0.1" | ||
3832 | resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea" | ||
3833 | integrity sha512-CikZxJGNyNOBERbeALo0NUUeJgHs5NyEvuYChX/PcsBV91TAvEq4hYDaWSenSieT8XwAutNnS3FGvyzIMOughQ== | ||
3834 | |||
3835 | browserslist@4.21.5, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.5: | 3748 | browserslist@4.21.5, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.5: |
3836 | version "4.21.5" | 3749 | version "4.21.5" |
3837 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" | 3750 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" |
@@ -3853,29 +3766,11 @@ browserstack-local@^1.5.1: | |||
3853 | ps-tree "=1.2.0" | 3766 | ps-tree "=1.2.0" |
3854 | temp-fs "^0.9.9" | 3767 | temp-fs "^0.9.9" |
3855 | 3768 | ||
3856 | buffer-alloc-unsafe@^1.1.0: | ||
3857 | version "1.1.0" | ||
3858 | resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" | ||
3859 | integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== | ||
3860 | |||
3861 | buffer-alloc@^1.1.0: | ||
3862 | version "1.2.0" | ||
3863 | resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" | ||
3864 | integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== | ||
3865 | dependencies: | ||
3866 | buffer-alloc-unsafe "^1.1.0" | ||
3867 | buffer-fill "^1.0.0" | ||
3868 | |||
3869 | buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: | 3769 | buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: |
3870 | version "0.2.13" | 3770 | version "0.2.13" |
3871 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" | 3771 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" |
3872 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== | 3772 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== |
3873 | 3773 | ||
3874 | buffer-fill@^1.0.0: | ||
3875 | version "1.0.0" | ||
3876 | resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" | ||
3877 | integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== | ||
3878 | |||
3879 | buffer-from@^1.0.0: | 3774 | buffer-from@^1.0.0: |
3880 | version "1.1.2" | 3775 | version "1.1.2" |
3881 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" | 3776 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" |
@@ -3914,11 +3809,6 @@ bufferutil@^4.0.3: | |||
3914 | dependencies: | 3809 | dependencies: |
3915 | node-gyp-build "^4.3.0" | 3810 | node-gyp-build "^4.3.0" |
3916 | 3811 | ||
3917 | builtin-status-codes@^3.0.0: | ||
3918 | version "3.0.0" | ||
3919 | resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" | ||
3920 | integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== | ||
3921 | |||
3922 | builtins@^5.0.0: | 3812 | builtins@^5.0.0: |
3923 | version "5.0.1" | 3813 | version "5.0.1" |
3924 | resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" | 3814 | resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" |
@@ -4010,14 +3900,6 @@ cacache@^17.0.0: | |||
4010 | tar "^6.1.11" | 3900 | tar "^6.1.11" |
4011 | unique-filename "^3.0.0" | 3901 | unique-filename "^3.0.0" |
4012 | 3902 | ||
4013 | cache-chunk-store@^3.0.0, cache-chunk-store@^3.2.2: | ||
4014 | version "3.2.2" | ||
4015 | resolved "https://registry.yarnpkg.com/cache-chunk-store/-/cache-chunk-store-3.2.2.tgz#19bb55d61252cd2174da4686548d52bc2dd44120" | ||
4016 | integrity sha512-2lJdWbgHFFxcSth9s2wpId3CR3v1YC63KjP4T9WhpW7LWlY7Hiiei3QwwqzkWqlJTfR8lSy9F5kRQECeyj+yQA== | ||
4017 | dependencies: | ||
4018 | lru "^3.1.0" | ||
4019 | queue-microtask "^1.2.3" | ||
4020 | |||
4021 | cacheable-lookup@^7.0.0: | 3903 | cacheable-lookup@^7.0.0: |
4022 | version "7.0.0" | 3904 | version "7.0.0" |
4023 | resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" | 3905 | resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" |
@@ -4183,7 +4065,7 @@ chownr@^2.0.0: | |||
4183 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" | 4065 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" |
4184 | integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== | 4066 | integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== |
4185 | 4067 | ||
4186 | chrome-dgram@^3.0.2, chrome-dgram@^3.0.6: | 4068 | chrome-dgram@^3.0.6: |
4187 | version "3.0.6" | 4069 | version "3.0.6" |
4188 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" | 4070 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" |
4189 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== | 4071 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== |
@@ -4191,13 +4073,6 @@ chrome-dgram@^3.0.2, chrome-dgram@^3.0.6: | |||
4191 | inherits "^2.0.4" | 4073 | inherits "^2.0.4" |
4192 | run-series "^1.1.9" | 4074 | run-series "^1.1.9" |
4193 | 4075 | ||
4194 | chrome-dns@^1.0.0: | ||
4195 | version "1.0.1" | ||
4196 | resolved "https://registry.yarnpkg.com/chrome-dns/-/chrome-dns-1.0.1.tgz#6870af680a40d2c4b2efc2154a378793f5a4ce4b" | ||
4197 | integrity sha512-HqsYJgIc8ljJJOqOzLphjAs79EUuWSX3nzZi2LNkzlw3GIzAeZbaSektC8iT/tKvLqZq8yl1GJu5o6doA4TRbg== | ||
4198 | dependencies: | ||
4199 | chrome-net "^3.3.2" | ||
4200 | |||
4201 | chrome-launcher@^0.15.0: | 4076 | chrome-launcher@^0.15.0: |
4202 | version "0.15.2" | 4077 | version "0.15.2" |
4203 | resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" | 4078 | resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" |
@@ -4208,13 +4083,6 @@ chrome-launcher@^0.15.0: | |||
4208 | is-wsl "^2.2.0" | 4083 | is-wsl "^2.2.0" |
4209 | lighthouse-logger "^1.0.0" | 4084 | lighthouse-logger "^1.0.0" |
4210 | 4085 | ||
4211 | chrome-net@^3.3.2, chrome-net@^3.3.4: | ||
4212 | version "3.3.4" | ||
4213 | resolved "https://registry.yarnpkg.com/chrome-net/-/chrome-net-3.3.4.tgz#0e604a31d226ebfb8d2d1c381cab47d35309825d" | ||
4214 | integrity sha512-Jzy2EnzmE+ligqIZUsmWnck9RBXLuUy6CaKyuNMtowFG3ZvLt8d+WBJCTPEludV0DHpIKjAOlwjFmTaEdfdWCw== | ||
4215 | dependencies: | ||
4216 | inherits "^2.0.1" | ||
4217 | |||
4218 | chrome-trace-event@^1.0.2: | 4086 | chrome-trace-event@^1.0.2: |
4219 | version "1.0.3" | 4087 | version "1.0.3" |
4220 | resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" | 4088 | resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" |
@@ -4240,14 +4108,6 @@ chromium-bidi@0.4.9: | |||
4240 | dependencies: | 4108 | dependencies: |
4241 | mitt "3.0.0" | 4109 | mitt "3.0.0" |
4242 | 4110 | ||
4243 | chunk-store-stream@^4.3.0: | ||
4244 | version "4.3.0" | ||
4245 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" | ||
4246 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== | ||
4247 | dependencies: | ||
4248 | block-stream2 "^2.0.0" | ||
4249 | readable-stream "^3.6.0" | ||
4250 | |||
4251 | ci-info@^3.2.0: | 4111 | ci-info@^3.2.0: |
4252 | version "3.8.0" | 4112 | version "3.8.0" |
4253 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" | 4113 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" |
@@ -4580,11 +4440,6 @@ cosmiconfig@^8.1.3: | |||
4580 | parse-json "^5.0.0" | 4440 | parse-json "^5.0.0" |
4581 | path-type "^4.0.0" | 4441 | path-type "^4.0.0" |
4582 | 4442 | ||
4583 | cpus@^1.0.3: | ||
4584 | version "1.0.3" | ||
4585 | resolved "https://registry.yarnpkg.com/cpus/-/cpus-1.0.3.tgz#4ef6deea461968d6329d07dd01205685df2934a2" | ||
4586 | integrity sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA== | ||
4587 | |||
4588 | crc-32@^1.2.0: | 4443 | crc-32@^1.2.0: |
4589 | version "1.2.2" | 4444 | version "1.2.2" |
4590 | resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" | 4445 | resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" |
@@ -4598,23 +4453,6 @@ crc32-stream@^4.0.2: | |||
4598 | crc-32 "^1.2.0" | 4453 | crc-32 "^1.2.0" |
4599 | readable-stream "^3.4.0" | 4454 | readable-stream "^3.4.0" |
4600 | 4455 | ||
4601 | create-torrent@^5.0.4: | ||
4602 | version "5.0.9" | ||
4603 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-5.0.9.tgz#850f198f7568e3d0e1e73b6858d43d44659a69d0" | ||
4604 | integrity sha512-WQ/bMe+aCBSa5EonIkgw7CTM/1JnJDQuLJhA78omSWvuEbXDwaUy0rG3a+IYt+EiO+rdTLxdsBwrsn/wfWOMQA== | ||
4605 | dependencies: | ||
4606 | bencode "^2.0.3" | ||
4607 | block-iterator "^1.0.1" | ||
4608 | fast-readable-async-iterator "^1.1.1" | ||
4609 | is-file "^1.0.0" | ||
4610 | join-async-iterator "^1.1.1" | ||
4611 | junk "^3.1.0" | ||
4612 | minimist "^1.2.7" | ||
4613 | piece-length "^2.0.1" | ||
4614 | queue-microtask "^1.2.3" | ||
4615 | run-parallel "^1.2.0" | ||
4616 | simple-sha1 "^3.1.0" | ||
4617 | |||
4618 | critters@0.0.16: | 4456 | critters@0.0.16: |
4619 | version "0.0.16" | 4457 | version "0.0.16" |
4620 | resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" | 4458 | resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" |
@@ -4764,7 +4602,7 @@ debug@2.6.9, debug@^2.6.9: | |||
4764 | dependencies: | 4602 | dependencies: |
4765 | ms "2.0.0" | 4603 | ms "2.0.0" |
4766 | 4604 | ||
4767 | debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: | 4605 | debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: |
4768 | version "4.3.4" | 4606 | version "4.3.4" |
4769 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" | 4607 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" |
4770 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== | 4608 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== |
@@ -4951,11 +4789,6 @@ devtools@8.10.5: | |||
4951 | uuid "^9.0.0" | 4789 | uuid "^9.0.0" |
4952 | which "^3.0.0" | 4790 | which "^3.0.0" |
4953 | 4791 | ||
4954 | dexie@^3.2.2: | ||
4955 | version "3.2.3" | ||
4956 | resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.3.tgz#f35c91ca797599df8e771b998e9ae9669c877f8c" | ||
4957 | integrity sha512-iHayBd4UYryDCVUNa3PMsJMEnd8yjyh5p7a+RFeC8i8n476BC9wMhVvqiImq5zJZJf5Tuer+s4SSj+AA3x+ZbQ== | ||
4958 | |||
4959 | diff-sequences@^29.4.3: | 4792 | diff-sequences@^29.4.3: |
4960 | version "29.4.3" | 4793 | version "29.4.3" |
4961 | resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" | 4794 | resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" |
@@ -5172,7 +5005,7 @@ encoding@^0.1.13: | |||
5172 | dependencies: | 5005 | dependencies: |
5173 | iconv-lite "^0.6.2" | 5006 | iconv-lite "^0.6.2" |
5174 | 5007 | ||
5175 | end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: | 5008 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: |
5176 | version "1.4.4" | 5009 | version "1.4.4" |
5177 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" | 5010 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" |
5178 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== | 5011 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== |
@@ -5410,7 +5243,7 @@ escalade@^3.1.1: | |||
5410 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" | 5243 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" |
5411 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== | 5244 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== |
5412 | 5245 | ||
5413 | escape-html@^1.0.3, escape-html@~1.0.3: | 5246 | escape-html@~1.0.3: |
5414 | version "1.0.3" | 5247 | version "1.0.3" |
5415 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" | 5248 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" |
5416 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== | 5249 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== |
@@ -5770,11 +5603,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: | |||
5770 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" | 5603 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" |
5771 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== | 5604 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== |
5772 | 5605 | ||
5773 | fast-fifo@^1.1.0: | ||
5774 | version "1.2.0" | ||
5775 | resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.2.0.tgz#2ee038da2468e8623066dee96958b0c1763aa55a" | ||
5776 | integrity sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg== | ||
5777 | |||
5778 | fast-glob@3.2.7: | 5606 | fast-glob@3.2.7: |
5779 | version "3.2.7" | 5607 | version "3.2.7" |
5780 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" | 5608 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" |
@@ -5807,11 +5635,6 @@ fast-levenshtein@^2.0.6: | |||
5807 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" | 5635 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" |
5808 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== | 5636 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== |
5809 | 5637 | ||
5810 | fast-readable-async-iterator@^1.1.1: | ||
5811 | version "1.1.1" | ||
5812 | resolved "https://registry.yarnpkg.com/fast-readable-async-iterator/-/fast-readable-async-iterator-1.1.1.tgz#77dfbb5262b278bb123c4d8d3219b1bb881b857c" | ||
5813 | integrity sha512-xEHkLUEmStETI+15zhglJLO9TjXxNkkp2ldEfYVZdcqxFhM172EfGl1irI6mVlTxXspYKH1/kjevnt/XSsPeFA== | ||
5814 | |||
5815 | fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: | 5638 | fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: |
5816 | version "1.0.16" | 5639 | version "1.0.16" |
5817 | resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" | 5640 | resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" |
@@ -6017,11 +5840,6 @@ fraction.js@^4.2.0: | |||
6017 | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" | 5840 | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" |
6018 | integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== | 5841 | integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== |
6019 | 5842 | ||
6020 | freelist@^1.0.3: | ||
6021 | version "1.0.3" | ||
6022 | resolved "https://registry.yarnpkg.com/freelist/-/freelist-1.0.3.tgz#006775509f3935701784d3ed2fc9f12c9df1bab2" | ||
6023 | integrity sha512-Ji7fEnMdZDGbS5oXElpRJsn9jPvBR8h/037D3bzreNmS8809cISq/2D9//JbA/TaZmkkN8cmecXwmQHmM+NHhg== | ||
6024 | |||
6025 | fresh@0.5.2: | 5843 | fresh@0.5.2: |
6026 | version "0.5.2" | 5844 | version "0.5.2" |
6027 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" | 5845 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" |
@@ -6032,18 +5850,6 @@ from@~0: | |||
6032 | resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" | 5850 | resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" |
6033 | integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== | 5851 | integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== |
6034 | 5852 | ||
6035 | fs-chunk-store@^2.0.5: | ||
6036 | version "2.0.5" | ||
6037 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.5.tgz#1dd4bdbb371239ac6f7234af6cd4386c72315059" | ||
6038 | integrity sha512-z3c2BmyaHdQTtIVXJDQOvwZVWN2gNU//0IYKK2LuPr+cZyGoIrgDwI4iDASaTUyQbOBtyg/k6GuDZepB6jQIPw== | ||
6039 | dependencies: | ||
6040 | queue-microtask "^1.2.2" | ||
6041 | random-access-file "^2.0.1" | ||
6042 | randombytes "^2.0.3" | ||
6043 | rimraf "^3.0.0" | ||
6044 | run-parallel "^1.1.2" | ||
6045 | thunky "^1.0.1" | ||
6046 | |||
6047 | fs-constants@^1.0.0: | 5853 | fs-constants@^1.0.0: |
6048 | version "1.0.0" | 5854 | version "1.0.0" |
6049 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" | 5855 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" |
@@ -6192,11 +5998,6 @@ get-port@^6.1.2: | |||
6192 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" | 5998 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" |
6193 | integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== | 5999 | integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== |
6194 | 6000 | ||
6195 | get-stdin@^8.0.0: | ||
6196 | version "8.0.0" | ||
6197 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" | ||
6198 | integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== | ||
6199 | |||
6200 | get-stream@^3.0.0: | 6001 | get-stream@^3.0.0: |
6201 | version "3.0.0" | 6002 | version "3.0.0" |
6202 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" | 6003 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" |
@@ -6686,11 +6487,6 @@ http-parser-js@>=0.5.1: | |||
6686 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" | 6487 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" |
6687 | integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== | 6488 | integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== |
6688 | 6489 | ||
6689 | http-parser-js@^0.4.3: | ||
6690 | version "0.4.13" | ||
6691 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.13.tgz#3bd6d6fde6e3172c9334c3b33b6c193d80fe1137" | ||
6692 | integrity sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA== | ||
6693 | |||
6694 | http-proxy-agent@5.0.0, http-proxy-agent@^5.0.0: | 6490 | http-proxy-agent@5.0.0, http-proxy-agent@^5.0.0: |
6695 | version "5.0.0" | 6491 | version "5.0.0" |
6696 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" | 6492 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" |
@@ -6736,11 +6532,6 @@ http2-wrapper@^2.1.10: | |||
6736 | quick-lru "^5.1.1" | 6532 | quick-lru "^5.1.1" |
6737 | resolve-alpn "^1.2.0" | 6533 | resolve-alpn "^1.2.0" |
6738 | 6534 | ||
6739 | https-browserify@^1.0.0: | ||
6740 | version "1.0.0" | ||
6741 | resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" | ||
6742 | integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== | ||
6743 | |||
6744 | https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: | 6535 | https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: |
6745 | version "5.0.1" | 6536 | version "5.0.1" |
6746 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" | 6537 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" |
@@ -6815,13 +6606,6 @@ image-size@~0.5.0: | |||
6815 | resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" | 6606 | resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" |
6816 | integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== | 6607 | integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== |
6817 | 6608 | ||
6818 | immediate-chunk-store@^2.2.0: | ||
6819 | version "2.2.0" | ||
6820 | resolved "https://registry.yarnpkg.com/immediate-chunk-store/-/immediate-chunk-store-2.2.0.tgz#f56d30ecc7171f6cfcf632b0eb8395a89f92c03c" | ||
6821 | integrity sha512-1bHBna0hCa6arRXicu91IiL9RvvkbNYLVq+mzWdaLGZC3hXvX4doh8e1dLhMKez5siu63CYgO5NrGJbRX5lbPA== | ||
6822 | dependencies: | ||
6823 | queue-microtask "^1.2.3" | ||
6824 | |||
6825 | immutable@^4.0.0: | 6609 | immutable@^4.0.0: |
6826 | version "4.3.0" | 6610 | version "4.3.0" |
6827 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" | 6611 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" |
@@ -6886,7 +6670,7 @@ inflight@^1.0.4: | |||
6886 | once "^1.3.0" | 6670 | once "^1.3.0" |
6887 | wrappy "1" | 6671 | wrappy "1" |
6888 | 6672 | ||
6889 | inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4: | 6673 | inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: |
6890 | version "2.0.4" | 6674 | version "2.0.4" |
6891 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" | 6675 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" |
6892 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== | 6676 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== |
@@ -6982,13 +6766,6 @@ ip-regex@^4.1.0: | |||
6982 | resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" | 6766 | resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" |
6983 | integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== | 6767 | integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== |
6984 | 6768 | ||
6985 | ip-set@^2.1.0: | ||
6986 | version "2.1.0" | ||
6987 | resolved "https://registry.yarnpkg.com/ip-set/-/ip-set-2.1.0.tgz#9a47b9f5d220c38bc7fe5db8efc4baa45b0a0a35" | ||
6988 | integrity sha512-JdHz4tSMx1IeFj8yEcQU0i58qiSkOlmZXkZ8+HJ0ROV5KcgLRDO9F703oJ1GeZCvqggrcCbmagD/V7hghY62wA== | ||
6989 | dependencies: | ||
6990 | ip "^1.1.5" | ||
6991 | |||
6992 | ip@^1.1.5: | 6769 | ip@^1.1.5: |
6993 | version "1.1.8" | 6770 | version "1.1.8" |
6994 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" | 6771 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" |
@@ -7036,11 +6813,6 @@ is-arrayish@^0.2.1: | |||
7036 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | 6813 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" |
7037 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== | 6814 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== |
7038 | 6815 | ||
7039 | is-ascii@^1.0.0: | ||
7040 | version "1.0.0" | ||
7041 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" | ||
7042 | integrity sha512-CXMaB/+EWCSGlLPs7ZlXRBpaPRRSRnrOfq0N3+RGeCZfqQaHQtiDLlkPCn63+LCkRUc1iRE0AXiI+sm2/Hi3qQ== | ||
7043 | |||
7044 | is-bigint@^1.0.1: | 6816 | is-bigint@^1.0.1: |
7045 | version "1.0.4" | 6817 | version "1.0.4" |
7046 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" | 6818 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" |
@@ -7092,11 +6864,6 @@ is-extglob@^2.1.1: | |||
7092 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" | 6864 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" |
7093 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== | 6865 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== |
7094 | 6866 | ||
7095 | is-file@^1.0.0: | ||
7096 | version "1.0.0" | ||
7097 | resolved "https://registry.yarnpkg.com/is-file/-/is-file-1.0.0.tgz#28a44cfbd9d3db193045f22b65fce8edf9620596" | ||
7098 | integrity sha512-ZGMuc+xA8mRnrXtmtf2l/EkIW2zaD2LSBWlaOVEF6yH4RTndHob65V4SwWWdtGKVthQfXPVKsXqw4TDUjbVxVQ== | ||
7099 | |||
7100 | is-fullwidth-code-point@^1.0.0: | 6867 | is-fullwidth-code-point@^1.0.0: |
7101 | version "1.0.0" | 6868 | version "1.0.0" |
7102 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" | 6869 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" |
@@ -7437,11 +7204,6 @@ jest-worker@^27.4.5: | |||
7437 | merge-stream "^2.0.0" | 7204 | merge-stream "^2.0.0" |
7438 | supports-color "^8.0.0" | 7205 | supports-color "^8.0.0" |
7439 | 7206 | ||
7440 | join-async-iterator@^1.1.1: | ||
7441 | version "1.1.1" | ||
7442 | resolved "https://registry.yarnpkg.com/join-async-iterator/-/join-async-iterator-1.1.1.tgz#7d2857d7f4066267861888d264769e842110d07e" | ||
7443 | integrity sha512-ATse+nuNeKZ9K1y27LKdvPe/GCe9R/u9dw9vI248e+vILeRK3IcJP4JUPAlSmKRCDK0cKhEwfmiw4Skqx7UnGQ== | ||
7444 | |||
7445 | js-tokens@^4.0.0: | 7207 | js-tokens@^4.0.0: |
7446 | version "4.0.0" | 7208 | version "4.0.0" |
7447 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | 7209 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" |
@@ -7543,37 +7305,6 @@ jsonparse@^1.3.1: | |||
7543 | resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" | 7305 | resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" |
7544 | integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== | 7306 | integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== |
7545 | 7307 | ||
7546 | junk@^3.1.0: | ||
7547 | version "3.1.0" | ||
7548 | resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" | ||
7549 | integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== | ||
7550 | |||
7551 | k-bucket@^5.0.0, k-bucket@^5.1.0: | ||
7552 | version "5.1.0" | ||
7553 | resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.1.0.tgz#db2c9e72bd168b432e3f3e8fc092e2ccb61bff89" | ||
7554 | integrity sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg== | ||
7555 | dependencies: | ||
7556 | randombytes "^2.1.0" | ||
7557 | |||
7558 | k-rpc-socket@^1.7.2: | ||
7559 | version "1.11.1" | ||
7560 | resolved "https://registry.yarnpkg.com/k-rpc-socket/-/k-rpc-socket-1.11.1.tgz#f14b4b240a716c6cad7b6434b21716dbd7c7b0e8" | ||
7561 | integrity sha512-8xtA8oqbZ6v1Niryp2/g4GxW16EQh5MvrUylQoOG+zcrDff5CKttON2XUXvMwlIHq4/2zfPVFiinAccJ+WhxoA== | ||
7562 | dependencies: | ||
7563 | bencode "^2.0.0" | ||
7564 | chrome-dgram "^3.0.2" | ||
7565 | chrome-dns "^1.0.0" | ||
7566 | chrome-net "^3.3.2" | ||
7567 | |||
7568 | k-rpc@^5.1.0: | ||
7569 | version "5.1.0" | ||
7570 | resolved "https://registry.yarnpkg.com/k-rpc/-/k-rpc-5.1.0.tgz#af2052de2e84994d55da3032175da5dad8640174" | ||
7571 | integrity sha512-FGc+n70Hcjoa/X2JTwP+jMIOpBz+pkRffHnSl9yrYiwUxg3FIgD50+u1ePfJUOnRCnx6pbjmVk5aAeB1wIijuQ== | ||
7572 | dependencies: | ||
7573 | k-bucket "^5.0.0" | ||
7574 | k-rpc-socket "^1.7.2" | ||
7575 | randombytes "^2.0.5" | ||
7576 | |||
7577 | karma-source-map-support@1.4.0: | 7308 | karma-source-map-support@1.4.0: |
7578 | version "1.4.0" | 7309 | version "1.4.0" |
7579 | resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" | 7310 | resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" |
@@ -7613,11 +7344,6 @@ ky@^0.33.0: | |||
7613 | resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" | 7344 | resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" |
7614 | integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== | 7345 | integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== |
7615 | 7346 | ||
7616 | last-one-wins@^1.0.4: | ||
7617 | version "1.0.4" | ||
7618 | resolved "https://registry.yarnpkg.com/last-one-wins/-/last-one-wins-1.0.4.tgz#c1bfd0cbcb46790ec9156b8d1aee8fcb86cda22a" | ||
7619 | integrity sha512-t+KLJFkHPQk8lfN6WBOiGkiUXoub+gnb2XTYI2P3aiISL+94xgZ1vgz1SXN/N4hthuOoLXarXfBZPUruyjQtfA== | ||
7620 | |||
7621 | launch-editor@^2.6.0: | 7347 | launch-editor@^2.6.0: |
7622 | version "2.6.0" | 7348 | version "2.6.0" |
7623 | resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" | 7349 | resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" |
@@ -7687,11 +7413,6 @@ lighthouse-logger@^1.0.0: | |||
7687 | debug "^2.6.9" | 7413 | debug "^2.6.9" |
7688 | marky "^1.2.2" | 7414 | marky "^1.2.2" |
7689 | 7415 | ||
7690 | limiter@^1.1.5: | ||
7691 | version "1.1.5" | ||
7692 | resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" | ||
7693 | integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== | ||
7694 | |||
7695 | lines-and-columns@^1.1.6: | 7416 | lines-and-columns@^1.1.6: |
7696 | version "1.2.4" | 7417 | version "1.2.4" |
7697 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" | 7418 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" |
@@ -7724,17 +7445,6 @@ listenercount@~1.0.1: | |||
7724 | resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" | 7445 | resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" |
7725 | integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== | 7446 | integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== |
7726 | 7447 | ||
7727 | load-ip-set@^2.2.1: | ||
7728 | version "2.2.1" | ||
7729 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.2.1.tgz#9496ab8aa14ebf81aeb7c8bb38e7abdf50af3563" | ||
7730 | integrity sha512-G3hQXehU2LTOp52e+lPffpK4EvidfjwbvHaGqmFcp4ptiZagR4xFdL+D08kMX906dxeqZyWhfonEjdUxrWcldg== | ||
7731 | dependencies: | ||
7732 | ip-set "^2.1.0" | ||
7733 | netmask "^2.0.1" | ||
7734 | once "^1.4.0" | ||
7735 | simple-get "^4.0.0" | ||
7736 | split "^1.0.1" | ||
7737 | |||
7738 | load-json-file@^1.0.0: | 7448 | load-json-file@^1.0.0: |
7739 | version "1.1.0" | 7449 | version "1.1.0" |
7740 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" | 7450 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" |
@@ -7943,14 +7653,6 @@ lru@^3.1.0: | |||
7943 | dependencies: | 7653 | dependencies: |
7944 | inherits "^2.0.1" | 7654 | inherits "^2.0.1" |
7945 | 7655 | ||
7946 | lt_donthave@^1.0.1: | ||
7947 | version "1.0.1" | ||
7948 | resolved "https://registry.yarnpkg.com/lt_donthave/-/lt_donthave-1.0.1.tgz#a160e08bdf15b9e092172063688855a6c031d8b3" | ||
7949 | integrity sha512-PfOXfDN9GnUjlNHjjxKQuMxPC8s12iSrnmg+Ff1BU1uLn7S1BFAKzpZCu6Gwg3WsCUvTZrZoDSHvy6B/j+N4/Q== | ||
7950 | dependencies: | ||
7951 | debug "^4.2.0" | ||
7952 | unordered-array-remove "^1.0.2" | ||
7953 | |||
7954 | m3u8-parser@4.8.0, m3u8-parser@^4.7.1: | 7656 | m3u8-parser@4.8.0, m3u8-parser@^4.7.1: |
7955 | version "4.8.0" | 7657 | version "4.8.0" |
7956 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" | 7658 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" |
@@ -7967,14 +7669,6 @@ magic-string@0.30.0: | |||
7967 | dependencies: | 7669 | dependencies: |
7968 | "@jridgewell/sourcemap-codec" "^1.4.13" | 7670 | "@jridgewell/sourcemap-codec" "^1.4.13" |
7969 | 7671 | ||
7970 | magnet-uri@^6.2.0: | ||
7971 | version "6.2.0" | ||
7972 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.2.0.tgz#10f7be050bf23452df210838239b118463c3eeff" | ||
7973 | integrity sha512-O9AgdDwT771fnUj0giPYu/rACpz8173y8UXCSOdLITjOVfBenZ9H9q3FqQmveK+ORUMuD+BkKNSZP8C3+IMAKQ== | ||
7974 | dependencies: | ||
7975 | bep53-range "^1.1.0" | ||
7976 | thirty-two "^1.0.2" | ||
7977 | |||
7978 | mailparser-mit@^1.0.0: | 7672 | mailparser-mit@^1.0.0: |
7979 | version "1.0.0" | 7673 | version "1.0.0" |
7980 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" | 7674 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" |
@@ -8101,15 +7795,6 @@ media-typer@0.3.0: | |||
8101 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" | 7795 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" |
8102 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== | 7796 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== |
8103 | 7797 | ||
8104 | mediasource@^2.2.2, mediasource@^2.4.0: | ||
8105 | version "2.4.0" | ||
8106 | resolved "https://registry.yarnpkg.com/mediasource/-/mediasource-2.4.0.tgz#7b03378054c41400374e9bade50aa0d7a758c39b" | ||
8107 | integrity sha512-SKUMrbFMHgiCUZFOWZcL0aiF/KgHx9SPIKzxrl6+7nMUMDK/ZnOmJdY/9wKzYeM0g3mybt3ueg+W+/mrYfmeFQ== | ||
8108 | dependencies: | ||
8109 | inherits "^2.0.4" | ||
8110 | readable-stream "^3.6.0" | ||
8111 | to-arraybuffer "^1.0.1" | ||
8112 | |||
8113 | mem@^1.1.0: | 7798 | mem@^1.1.0: |
8114 | version "1.1.0" | 7799 | version "1.1.0" |
8115 | resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" | 7800 | resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" |
@@ -8124,13 +7809,6 @@ memfs@^3.4.12, memfs@^3.4.3: | |||
8124 | dependencies: | 7809 | dependencies: |
8125 | fs-monkey "^1.0.3" | 7810 | fs-monkey "^1.0.3" |
8126 | 7811 | ||
8127 | memory-chunk-store@^1.3.5: | ||
8128 | version "1.3.5" | ||
8129 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.5.tgz#700f712415895600bc5466007333efa19f1de07c" | ||
8130 | integrity sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g== | ||
8131 | dependencies: | ||
8132 | queue-microtask "^1.2.3" | ||
8133 | |||
8134 | meow@^9.0.0: | 7812 | meow@^9.0.0: |
8135 | version "9.0.0" | 7813 | version "9.0.0" |
8136 | resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" | 7814 | resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" |
@@ -8194,11 +7872,6 @@ mime@1.6.0, mime@^1.4.1, mime@^1.6.0: | |||
8194 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 7872 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
8195 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 7873 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
8196 | 7874 | ||
8197 | mime@^3.0.0: | ||
8198 | version "3.0.0" | ||
8199 | resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" | ||
8200 | integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== | ||
8201 | |||
8202 | mimic-fn@^1.0.0: | 7875 | mimic-fn@^1.0.0: |
8203 | version "1.2.0" | 7876 | version "1.2.0" |
8204 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" | 7877 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" |
@@ -8306,7 +7979,7 @@ minimist-options@4.1.0: | |||
8306 | is-plain-obj "^1.1.0" | 7979 | is-plain-obj "^1.1.0" |
8307 | kind-of "^6.0.3" | 7980 | kind-of "^6.0.3" |
8308 | 7981 | ||
8309 | minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.7: | 7982 | minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: |
8310 | version "1.2.8" | 7983 | version "1.2.8" |
8311 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" | 7984 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" |
8312 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== | 7985 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== |
@@ -8453,23 +8126,6 @@ mousetrap@^1.6.5: | |||
8453 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" | 8126 | resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" |
8454 | integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== | 8127 | integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== |
8455 | 8128 | ||
8456 | mp4-box-encoding@^1.3.0: | ||
8457 | version "1.4.1" | ||
8458 | resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.4.1.tgz#19b31804c896bc1adf1c21b497bcf951aa3b9098" | ||
8459 | integrity sha512-2/PRtGGiqPc/VEhbm7xAQ+gbb7yzHjjMAv6MpAifr5pCpbh3fQUdj93uNgwPiTppAGu8HFKe3PeU+OdRyAxStA== | ||
8460 | dependencies: | ||
8461 | uint64be "^2.0.2" | ||
8462 | |||
8463 | mp4-stream@^3.0.0: | ||
8464 | version "3.1.3" | ||
8465 | resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-3.1.3.tgz#79b8a19900337203a9bd607a02eccc64419a379c" | ||
8466 | integrity sha512-DUT8f0x2jHbZjNMdqe9h6lZdt6RENWTTdGn8z3TXa4uEsoltuNY9lCCij84mdm0q7xcV0E2W25WRxlKBMo4hSw== | ||
8467 | dependencies: | ||
8468 | mp4-box-encoding "^1.3.0" | ||
8469 | next-event "^1.0.0" | ||
8470 | queue-microtask "^1.2.2" | ||
8471 | readable-stream "^3.0.6" | ||
8472 | |||
8473 | mpd-parser@0.22.1, mpd-parser@^0.22.1: | 8129 | mpd-parser@0.22.1, mpd-parser@^0.22.1: |
8474 | version "0.22.1" | 8130 | version "0.22.1" |
8475 | resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" | 8131 | resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" |
@@ -8508,14 +8164,6 @@ multicast-dns@^7.2.5: | |||
8508 | dns-packet "^5.2.2" | 8164 | dns-packet "^5.2.2" |
8509 | thunky "^1.0.2" | 8165 | thunky "^1.0.2" |
8510 | 8166 | ||
8511 | multistream@^4.1.0: | ||
8512 | version "4.1.0" | ||
8513 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" | ||
8514 | integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== | ||
8515 | dependencies: | ||
8516 | once "^1.4.0" | ||
8517 | readable-stream "^3.6.0" | ||
8518 | |||
8519 | mute-stream@0.0.8: | 8167 | mute-stream@0.0.8: |
8520 | version "0.0.8" | 8168 | version "0.0.8" |
8521 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" | 8169 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" |
@@ -8544,11 +8192,6 @@ nanoid@^3.3.6: | |||
8544 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" | 8192 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" |
8545 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== | 8193 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== |
8546 | 8194 | ||
8547 | napi-macros@^2.0.0: | ||
8548 | version "2.2.2" | ||
8549 | resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.2.2.tgz#817fef20c3e0e40a963fbf7b37d1600bd0201044" | ||
8550 | integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== | ||
8551 | |||
8552 | natural-compare-lite@^1.4.0: | 8195 | natural-compare-lite@^1.4.0: |
8553 | version "1.4.0" | 8196 | version "1.4.0" |
8554 | resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" | 8197 | resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" |
@@ -8578,16 +8221,6 @@ neo-async@^2.6.2: | |||
8578 | resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" | 8221 | resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" |
8579 | integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== | 8222 | integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== |
8580 | 8223 | ||
8581 | netmask@^2.0.1: | ||
8582 | version "2.0.2" | ||
8583 | resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" | ||
8584 | integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== | ||
8585 | |||
8586 | next-event@^1.0.0: | ||
8587 | version "1.0.0" | ||
8588 | resolved "https://registry.yarnpkg.com/next-event/-/next-event-1.0.0.tgz#e7778acde2e55802e0ad1879c39cf6f75eda61d8" | ||
8589 | integrity sha512-IXGPhl/yAiUU597gz+k5OYxYZkmLSWTcPPcpQjWABud9OK6m/ZNLrVdcEu4e7NgmOObFIhgZVg1jecPYT/6AoA== | ||
8590 | |||
8591 | ngx-uploadx@^6.1.0: | 8224 | ngx-uploadx@^6.1.0: |
8592 | version "6.1.0" | 8225 | version "6.1.0" |
8593 | resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-6.1.0.tgz#40f00c352ba5a1af5b4bbe78a6a7572518314f8c" | 8226 | resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-6.1.0.tgz#40f00c352ba5a1af5b4bbe78a6a7572518314f8c" |
@@ -8642,7 +8275,7 @@ node-forge@^1: | |||
8642 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" | 8275 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" |
8643 | integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== | 8276 | integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== |
8644 | 8277 | ||
8645 | node-gyp-build@^4.2.0, node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: | 8278 | node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: |
8646 | version "4.6.0" | 8279 | version "4.6.0" |
8647 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" | 8280 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" |
8648 | integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== | 8281 | integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== |
@@ -9103,13 +8736,6 @@ p-try@^2.0.0: | |||
9103 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" | 8736 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" |
9104 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== | 8737 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== |
9105 | 8738 | ||
9106 | package-json-versionify@^1.0.4: | ||
9107 | version "1.0.4" | ||
9108 | resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17" | ||
9109 | integrity sha512-mtKKtCeSZMtWcc5hHJS6OlEGP7J9g7WN6vWCCZi2hCXFag/Zmjokh6WFFTQb9TuMnBcZpRjhhMQyOyglPCAahw== | ||
9110 | dependencies: | ||
9111 | browserify-package-json "^1.0.0" | ||
9112 | |||
9113 | pacote@15.1.3: | 8739 | pacote@15.1.3: |
9114 | version "15.1.3" | 8740 | version "15.1.3" |
9115 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.3.tgz#4c0e7fb5e7ab3b27fb3f86514b451ad4c4f64e9d" | 8741 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.1.3.tgz#4c0e7fb5e7ab3b27fb3f86514b451ad4c4f64e9d" |
@@ -9186,19 +8812,6 @@ parse-srcset@^1.0.2: | |||
9186 | resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" | 8812 | resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" |
9187 | integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== | 8813 | integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== |
9188 | 8814 | ||
9189 | parse-torrent@^9.1.5: | ||
9190 | version "9.1.5" | ||
9191 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.5.tgz#fcae5f360d9baf617d9a2de68e74d5de4c8099fd" | ||
9192 | integrity sha512-K8FXRwTOaZMI0/xuv0dpng1MVHZRtMJ0jRWBJ3qZWVNTrC1MzWUxm9QwaXDz/2qPhV2XC4UIHI92IGHwseAwaA== | ||
9193 | dependencies: | ||
9194 | bencode "^2.0.2" | ||
9195 | blob-to-buffer "^1.2.9" | ||
9196 | get-stdin "^8.0.0" | ||
9197 | magnet-uri "^6.2.0" | ||
9198 | queue-microtask "^1.2.3" | ||
9199 | simple-get "^4.0.1" | ||
9200 | simple-sha1 "^3.1.0" | ||
9201 | |||
9202 | parse5-html-rewriting-stream@7.0.0: | 8815 | parse5-html-rewriting-stream@7.0.0: |
9203 | version "7.0.0" | 8816 | version "7.0.0" |
9204 | resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" | 8817 | resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" |
@@ -9355,11 +8968,6 @@ picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch | |||
9355 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" | 8968 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" |
9356 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== | 8969 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== |
9357 | 8970 | ||
9358 | piece-length@^2.0.1: | ||
9359 | version "2.0.1" | ||
9360 | resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-2.0.1.tgz#dbed4e78976955f34466d0a65304d0cb21914ac9" | ||
9361 | integrity sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug== | ||
9362 | |||
9363 | pify@^2.0.0: | 8971 | pify@^2.0.0: |
9364 | version "2.3.0" | 8972 | version "2.3.0" |
9365 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" | 8973 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" |
@@ -9612,11 +9220,6 @@ pump@^3.0.0: | |||
9612 | end-of-stream "^1.1.0" | 9220 | end-of-stream "^1.1.0" |
9613 | once "^1.3.1" | 9221 | once "^1.3.1" |
9614 | 9222 | ||
9615 | punycode@1.3.2: | ||
9616 | version "1.3.2" | ||
9617 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" | ||
9618 | integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== | ||
9619 | |||
9620 | punycode@^2.1.0: | 9223 | punycode@^2.1.0: |
9621 | version "2.3.0" | 9224 | version "2.3.0" |
9622 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" | 9225 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" |
@@ -9667,26 +9270,11 @@ query-selector-shadow-dom@^1.0.0: | |||
9667 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" | 9270 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" |
9668 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== | 9271 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== |
9669 | 9272 | ||
9670 | querystring@0.2.0: | ||
9671 | version "0.2.0" | ||
9672 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" | ||
9673 | integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== | ||
9674 | |||
9675 | querystring@^0.2.1: | ||
9676 | version "0.2.1" | ||
9677 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" | ||
9678 | integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== | ||
9679 | |||
9680 | queue-microtask@^1.2.2, queue-microtask@^1.2.3: | 9273 | queue-microtask@^1.2.2, queue-microtask@^1.2.3: |
9681 | version "1.2.3" | 9274 | version "1.2.3" |
9682 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" | 9275 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
9683 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== | 9276 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
9684 | 9277 | ||
9685 | queue-tick@^1.0.0, queue-tick@^1.0.1: | ||
9686 | version "1.0.1" | ||
9687 | resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" | ||
9688 | integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== | ||
9689 | |||
9690 | quick-lru@^4.0.1: | 9278 | quick-lru@^4.0.1: |
9691 | version "4.0.1" | 9279 | version "4.0.1" |
9692 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" | 9280 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" |
@@ -9697,29 +9285,12 @@ quick-lru@^5.1.1: | |||
9697 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" | 9285 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" |
9698 | integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== | 9286 | integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== |
9699 | 9287 | ||
9700 | random-access-file@^2.0.1: | ||
9701 | version "2.2.1" | ||
9702 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.1.tgz#071d086d8a92cc65abbd32b42aeba6d1d845d68d" | ||
9703 | integrity sha512-RGU0xmDqdOyEiynob1KYSeh8+9c9Td1MJ74GT1viMEYAn8SJ9oBtWCXLsYZukCF46yududHOdM449uRYbzBrZQ== | ||
9704 | dependencies: | ||
9705 | mkdirp-classic "^0.5.2" | ||
9706 | random-access-storage "^1.1.1" | ||
9707 | |||
9708 | random-access-storage@^1.1.1: | ||
9709 | version "1.4.3" | ||
9710 | resolved "https://registry.yarnpkg.com/random-access-storage/-/random-access-storage-1.4.3.tgz#277d07005107562dfea84798eb9a6acd47d64b7f" | ||
9711 | integrity sha512-D5e2iIC5dNENWyBxsjhEnNOMCwZZ64TARK6dyMN+3g4OTC4MJxyjh9hKLjTGoNhDOPrgjI+YlFEHFnrp/cSnzQ== | ||
9712 | dependencies: | ||
9713 | events "^3.3.0" | ||
9714 | inherits "^2.0.3" | ||
9715 | queue-tick "^1.0.0" | ||
9716 | |||
9717 | random-iterate@^1.0.1: | 9288 | random-iterate@^1.0.1: |
9718 | version "1.0.1" | 9289 | version "1.0.1" |
9719 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" | 9290 | resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99" |
9720 | integrity sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA== | 9291 | integrity sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA== |
9721 | 9292 | ||
9722 | randombytes@^2.0.3, randombytes@^2.0.5, randombytes@^2.1.0: | 9293 | randombytes@^2.1.0: |
9723 | version "2.1.0" | 9294 | version "2.1.0" |
9724 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" | 9295 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" |
9725 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== | 9296 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== |
@@ -9731,13 +9302,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: | |||
9731 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" | 9302 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" |
9732 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== | 9303 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== |
9733 | 9304 | ||
9734 | range-slice-stream@^2.0.0: | ||
9735 | version "2.0.0" | ||
9736 | resolved "https://registry.yarnpkg.com/range-slice-stream/-/range-slice-stream-2.0.0.tgz#1f25fc7a2cacf9ccd140c46f9cf670a1a7fe3ce6" | ||
9737 | integrity sha512-PPYLwZ63lXi6Tv2EZ8w3M4FzC0rVqvxivaOVS8pXSp5FMIHFnvi4MWHL3UdFLhwSy50aNtJsgjY0mBC6oFL26Q== | ||
9738 | dependencies: | ||
9739 | readable-stream "^3.0.2" | ||
9740 | |||
9741 | raw-body@2.5.1: | 9305 | raw-body@2.5.1: |
9742 | version "2.5.1" | 9306 | version "2.5.1" |
9743 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" | 9307 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" |
@@ -9756,11 +9320,6 @@ raw-loader@^4.0.2: | |||
9756 | loader-utils "^2.0.0" | 9320 | loader-utils "^2.0.0" |
9757 | schema-utils "^3.0.0" | 9321 | schema-utils "^3.0.0" |
9758 | 9322 | ||
9759 | rc4@^0.1.5: | ||
9760 | version "0.1.5" | ||
9761 | resolved "https://registry.yarnpkg.com/rc4/-/rc4-0.1.5.tgz#08c6e04a0168f6eb621c22ab6cb1151bd9f4a64d" | ||
9762 | integrity sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA== | ||
9763 | |||
9764 | react-is@^18.0.0: | 9323 | react-is@^18.0.0: |
9765 | version "18.2.0" | 9324 | version "18.2.0" |
9766 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" | 9325 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" |
@@ -9869,7 +9428,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable | |||
9869 | string_decoder "~1.1.1" | 9428 | string_decoder "~1.1.1" |
9870 | util-deprecate "~1.0.1" | 9429 | util-deprecate "~1.0.1" |
9871 | 9430 | ||
9872 | readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: | 9431 | readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: |
9873 | version "3.6.2" | 9432 | version "3.6.2" |
9874 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" | 9433 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" |
9875 | integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== | 9434 | integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== |
@@ -9899,13 +9458,6 @@ rechoir@^0.8.0: | |||
9899 | dependencies: | 9458 | dependencies: |
9900 | resolve "^1.20.0" | 9459 | resolve "^1.20.0" |
9901 | 9460 | ||
9902 | record-cache@^1.2.0: | ||
9903 | version "1.2.0" | ||
9904 | resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.2.0.tgz#e601bc4f164d58330cc00055e27aa4682291c882" | ||
9905 | integrity sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw== | ||
9906 | dependencies: | ||
9907 | b4a "^1.3.1" | ||
9908 | |||
9909 | recursive-readdir@^2.2.3: | 9461 | recursive-readdir@^2.2.3: |
9910 | version "2.2.3" | 9462 | version "2.2.3" |
9911 | resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" | 9463 | resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" |
@@ -9988,17 +9540,6 @@ relateurl@^0.2.7: | |||
9988 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" | 9540 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" |
9989 | integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== | 9541 | integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== |
9990 | 9542 | ||
9991 | render-media@^4.1.0: | ||
9992 | version "4.1.0" | ||
9993 | resolved "https://registry.yarnpkg.com/render-media/-/render-media-4.1.0.tgz#9188376822653d7e56c2d789d157c81e74fee0cb" | ||
9994 | integrity sha512-F5BMWDmgATEoyPCtKjmGNTGN1ghoZlfRQ3MJh8dS/MrvIUIxupiof/Y9uahChipXcqQ57twVbgMmyQmuO1vokw== | ||
9995 | dependencies: | ||
9996 | debug "^4.2.0" | ||
9997 | is-ascii "^1.0.0" | ||
9998 | mediasource "^2.4.0" | ||
9999 | stream-to-blob-url "^3.0.2" | ||
10000 | videostream "^3.2.2" | ||
10001 | |||
10002 | renderkid@^3.0.0: | 9543 | renderkid@^3.0.0: |
10003 | version "3.0.0" | 9544 | version "3.0.0" |
10004 | resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" | 9545 | resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" |
@@ -10170,14 +9711,7 @@ run-async@^3.0.0: | |||
10170 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" | 9711 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" |
10171 | integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== | 9712 | integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== |
10172 | 9713 | ||
10173 | run-parallel-limit@^1.1.0: | 9714 | run-parallel@^1.1.9, run-parallel@^1.2.0: |
10174 | version "1.1.0" | ||
10175 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" | ||
10176 | integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== | ||
10177 | dependencies: | ||
10178 | queue-microtask "^1.2.2" | ||
10179 | |||
10180 | run-parallel@^1.1.2, run-parallel@^1.1.9, run-parallel@^1.2.0: | ||
10181 | version "1.2.0" | 9715 | version "1.2.0" |
10182 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" | 9716 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" |
10183 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== | 9717 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== |
@@ -10189,11 +9723,6 @@ run-series@^1.1.9: | |||
10189 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" | 9723 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" |
10190 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== | 9724 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== |
10191 | 9725 | ||
10192 | rusha@^0.8.13: | ||
10193 | version "0.8.14" | ||
10194 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68" | ||
10195 | integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA== | ||
10196 | |||
10197 | rust-result@^1.0.0: | 9726 | rust-result@^1.0.0: |
10198 | version "1.0.0" | 9727 | version "1.0.0" |
10199 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" | 9728 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" |
@@ -10498,12 +10027,12 @@ sigstore@^1.3.0: | |||
10498 | make-fetch-happen "^11.0.1" | 10027 | make-fetch-happen "^11.0.1" |
10499 | tuf-js "^1.1.3" | 10028 | tuf-js "^1.1.3" |
10500 | 10029 | ||
10501 | simple-concat@^1.0.0, simple-concat@^1.0.1: | 10030 | simple-concat@^1.0.0: |
10502 | version "1.0.1" | 10031 | version "1.0.1" |
10503 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" | 10032 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" |
10504 | integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== | 10033 | integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== |
10505 | 10034 | ||
10506 | simple-get@^4.0.0, simple-get@^4.0.1: | 10035 | simple-get@^4.0.0: |
10507 | version "4.0.1" | 10036 | version "4.0.1" |
10508 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" | 10037 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" |
10509 | integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== | 10038 | integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== |
@@ -10525,14 +10054,6 @@ simple-peer@^9.11.0, simple-peer@^9.11.1: | |||
10525 | randombytes "^2.1.0" | 10054 | randombytes "^2.1.0" |
10526 | readable-stream "^3.6.0" | 10055 | readable-stream "^3.6.0" |
10527 | 10056 | ||
10528 | simple-sha1@^3.0.1, simple-sha1@^3.1.0: | ||
10529 | version "3.1.0" | ||
10530 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" | ||
10531 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== | ||
10532 | dependencies: | ||
10533 | queue-microtask "^1.2.2" | ||
10534 | rusha "^0.8.13" | ||
10535 | |||
10536 | simple-websocket@^9.1.0: | 10057 | simple-websocket@^9.1.0: |
10537 | version "9.1.0" | 10058 | version "9.1.0" |
10538 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" | 10059 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" |
@@ -10727,19 +10248,6 @@ spdy@^4.0.2: | |||
10727 | select-hose "^2.0.0" | 10248 | select-hose "^2.0.0" |
10728 | spdy-transport "^3.0.0" | 10249 | spdy-transport "^3.0.0" |
10729 | 10250 | ||
10730 | speed-limiter@^1.0.2: | ||
10731 | version "1.0.2" | ||
10732 | resolved "https://registry.yarnpkg.com/speed-limiter/-/speed-limiter-1.0.2.tgz#e4632f476a1d25d32557aad7bd089b3a0d948116" | ||
10733 | integrity sha512-Ax+TbUOho84bWUc3AKqWtkIvAIVws7d6QI4oJkgH4yQ5Yil+lR3vjd/7qd51dHKGzS5bFxg0++QwyNRN7s6rZA== | ||
10734 | dependencies: | ||
10735 | limiter "^1.1.5" | ||
10736 | streamx "^2.10.3" | ||
10737 | |||
10738 | speedometer@^1.1.0: | ||
10739 | version "1.1.0" | ||
10740 | resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.1.0.tgz#a30b13abda45687a1a76977012c060f2ac8a7934" | ||
10741 | integrity sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ== | ||
10742 | |||
10743 | split2@^4.1.0: | 10251 | split2@^4.1.0: |
10744 | version "4.2.0" | 10252 | version "4.2.0" |
10745 | resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" | 10253 | resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" |
@@ -10752,13 +10260,6 @@ split@0.3: | |||
10752 | dependencies: | 10260 | dependencies: |
10753 | through "2" | 10261 | through "2" |
10754 | 10262 | ||
10755 | split@^1.0.1: | ||
10756 | version "1.0.1" | ||
10757 | resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" | ||
10758 | integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== | ||
10759 | dependencies: | ||
10760 | through "2" | ||
10761 | |||
10762 | sprintf-js@~1.0.2: | 10263 | sprintf-js@~1.0.2: |
10763 | version "1.0.3" | 10264 | version "1.0.3" |
10764 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" | 10265 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" |
@@ -10802,14 +10303,6 @@ stop-iteration-iterator@^1.0.0: | |||
10802 | dependencies: | 10303 | dependencies: |
10803 | internal-slot "^1.0.4" | 10304 | internal-slot "^1.0.4" |
10804 | 10305 | ||
10805 | stream-browserify@^3.0.0: | ||
10806 | version "3.0.0" | ||
10807 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" | ||
10808 | integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== | ||
10809 | dependencies: | ||
10810 | inherits "~2.0.4" | ||
10811 | readable-stream "^3.5.0" | ||
10812 | |||
10813 | stream-buffers@^3.0.2: | 10306 | stream-buffers@^3.0.2: |
10814 | version "3.0.2" | 10307 | version "3.0.2" |
10815 | resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" | 10308 | resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" |
@@ -10822,43 +10315,6 @@ stream-combiner@~0.0.4: | |||
10822 | dependencies: | 10315 | dependencies: |
10823 | duplexer "~0.1.1" | 10316 | duplexer "~0.1.1" |
10824 | 10317 | ||
10825 | stream-http@^3.0.0: | ||
10826 | version "3.2.0" | ||
10827 | resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" | ||
10828 | integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== | ||
10829 | dependencies: | ||
10830 | builtin-status-codes "^3.0.0" | ||
10831 | inherits "^2.0.4" | ||
10832 | readable-stream "^3.6.0" | ||
10833 | xtend "^4.0.2" | ||
10834 | |||
10835 | stream-to-blob-url@^3.0.2: | ||
10836 | version "3.0.2" | ||
10837 | resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-3.0.2.tgz#5574d139e2a6d1435945476f0a9469947f2da4fb" | ||
10838 | integrity sha512-PS6wT2ZyyR38Cy+lE6PBEI1ZmO2HdzZoLeDGG0zZbYikCZd0dh8FUoSeFzgWLItpBYw1WJmPVRLpykRV+lAWLQ== | ||
10839 | dependencies: | ||
10840 | stream-to-blob "^2.0.0" | ||
10841 | |||
10842 | stream-to-blob@^2.0.0, stream-to-blob@^2.0.1: | ||
10843 | version "2.0.1" | ||
10844 | resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-2.0.1.tgz#59ab71d7a7f0bfb899570e886e44d39f4ac4381a" | ||
10845 | integrity sha512-GXlqXt3svqwIVWoICenix5Poxi4KbCF0BdXXUbpU1X4vq1V8wmjiEIU3aFJzCGNFpKxfbnG0uoowS3nKUgSPYg== | ||
10846 | |||
10847 | stream-with-known-length-to-buffer@^1.0.4: | ||
10848 | version "1.0.4" | ||
10849 | resolved "https://registry.yarnpkg.com/stream-with-known-length-to-buffer/-/stream-with-known-length-to-buffer-1.0.4.tgz#6a8aec53f27b8f481f962337c951aa3916fb60d1" | ||
10850 | integrity sha512-ztP79ug6S+I7td0Nd2GBeIKCm+vA54c+e60FY87metz5n/l6ydPELd2lxsljz8OpIhsRM9HkIiAwz85+S5G5/A== | ||
10851 | dependencies: | ||
10852 | once "^1.4.0" | ||
10853 | |||
10854 | streamx@^2.10.3: | ||
10855 | version "2.13.2" | ||
10856 | resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.13.2.tgz#9de43569a1cd54980d128673b3c1429b79afff1c" | ||
10857 | integrity sha512-+TWqixPhGDXEG9L/XczSbhfkmwAtGs3BJX5QNU6cvno+pOLKeszByWcnaTu6dg8efsTYqR8ZZuXWHhZfgrxMvA== | ||
10858 | dependencies: | ||
10859 | fast-fifo "^1.1.0" | ||
10860 | queue-tick "^1.0.1" | ||
10861 | |||
10862 | "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | 10318 | "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: |
10863 | version "4.2.3" | 10319 | version "4.2.3" |
10864 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | 10320 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" |
@@ -10921,7 +10377,7 @@ string.prototype.trimstart@^1.0.6: | |||
10921 | define-properties "^1.1.4" | 10377 | define-properties "^1.1.4" |
10922 | es-abstract "^1.20.4" | 10378 | es-abstract "^1.20.4" |
10923 | 10379 | ||
10924 | string2compact@^1.3.0, string2compact@^1.3.2: | 10380 | string2compact@^1.3.0: |
10925 | version "1.3.2" | 10381 | version "1.3.2" |
10926 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.2.tgz#c9d11a13f368404b8025425cc53f9916de1d0b8b" | 10382 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.2.tgz#c9d11a13f368404b8025425cc53f9916de1d0b8b" |
10927 | integrity sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw== | 10383 | integrity sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw== |
@@ -11258,31 +10714,16 @@ text-table@0.2.0, text-table@^0.2.0: | |||
11258 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" | 10714 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" |
11259 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== | 10715 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== |
11260 | 10716 | ||
11261 | thirty-two@^1.0.2: | ||
11262 | version "1.0.2" | ||
11263 | resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" | ||
11264 | integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA== | ||
11265 | |||
11266 | through@2, through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: | 10717 | through@2, through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: |
11267 | version "2.3.8" | 10718 | version "2.3.8" |
11268 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" | 10719 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" |
11269 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== | 10720 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== |
11270 | 10721 | ||
11271 | throughput@^1.0.1: | 10722 | thunky@^1.0.2: |
11272 | version "1.0.1" | ||
11273 | resolved "https://registry.yarnpkg.com/throughput/-/throughput-1.0.1.tgz#f8474cfc8f2f0eb740410bc23fa920b0bdba6d53" | ||
11274 | integrity sha512-4Mvv5P4xyVz6RM07wS3tGyZ/kPAiKtLeqznq3hK4pxDiTUSyQ5xeFlBiWxflCWexvSnxo2aAfedzKajJqihz4Q== | ||
11275 | |||
11276 | thunky@^1.0.1, thunky@^1.0.2: | ||
11277 | version "1.1.0" | 10723 | version "1.1.0" |
11278 | resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" | 10724 | resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" |
11279 | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== | 10725 | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== |
11280 | 10726 | ||
11281 | timeout-refresh@^1.0.0: | ||
11282 | version "1.0.3" | ||
11283 | resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-1.0.3.tgz#7024a8ce0a09a57acc2ea86002048e6c0bff7375" | ||
11284 | integrity sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA== | ||
11285 | |||
11286 | tmp@0.2.1, tmp@~0.2.1: | 10727 | tmp@0.2.1, tmp@~0.2.1: |
11287 | version "0.2.1" | 10728 | version "0.2.1" |
11288 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" | 10729 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" |
@@ -11297,11 +10738,6 @@ tmp@^0.0.33: | |||
11297 | dependencies: | 10738 | dependencies: |
11298 | os-tmpdir "~1.0.2" | 10739 | os-tmpdir "~1.0.2" |
11299 | 10740 | ||
11300 | to-arraybuffer@^1.0.1: | ||
11301 | version "1.0.1" | ||
11302 | resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" | ||
11303 | integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== | ||
11304 | |||
11305 | to-fast-properties@^2.0.0: | 10741 | to-fast-properties@^2.0.0: |
11306 | version "2.0.0" | 10742 | version "2.0.0" |
11307 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" | 10743 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" |
@@ -11324,22 +10760,6 @@ tokenizr@^1.6.4: | |||
11324 | resolved "https://registry.yarnpkg.com/tokenizr/-/tokenizr-1.6.9.tgz#67e7fc575fb73ae1145afe166e5e21a27d725b0d" | 10760 | resolved "https://registry.yarnpkg.com/tokenizr/-/tokenizr-1.6.9.tgz#67e7fc575fb73ae1145afe166e5e21a27d725b0d" |
11325 | integrity sha512-JeEey5bD1S0hsUEANaEKqqa4HDRfZRy8I1Enx+Rpb7wD1nDMqN53g6I9nhAOejPCfG5m0gOE73H4jT3NuK29Cw== | 10761 | integrity sha512-JeEey5bD1S0hsUEANaEKqqa4HDRfZRy8I1Enx+Rpb7wD1nDMqN53g6I9nhAOejPCfG5m0gOE73H4jT3NuK29Cw== |
11326 | 10762 | ||
11327 | torrent-discovery@^9.4.13: | ||
11328 | version "9.4.15" | ||
11329 | resolved "https://registry.yarnpkg.com/torrent-discovery/-/torrent-discovery-9.4.15.tgz#95f983543d3e5259857116532cecca4aa979e494" | ||
11330 | integrity sha512-71nx+TpLaF27mbsSj/tZTr588Dfk7XVzx+Rf1+nrxfXqe8qn5dIlRhgA+yY4cg8Ib69vWwkKFhAzbRqg8z42aw== | ||
11331 | dependencies: | ||
11332 | bittorrent-dht "^10.0.7" | ||
11333 | bittorrent-lsd "^1.1.1" | ||
11334 | bittorrent-tracker "^9.19.0" | ||
11335 | debug "^4.3.4" | ||
11336 | run-parallel "^1.2.0" | ||
11337 | |||
11338 | torrent-piece@^2.0.1: | ||
11339 | version "2.0.1" | ||
11340 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.1.tgz#a1a50fffa589d9bf9560e38837230708bc3afdc6" | ||
11341 | integrity sha512-JLSOyvQVLI6JTWqioY4vFL0JkEUKQcaHQsU3loxkCvPTSttw8ePs2tFwsP4XIjw99Fz8EdOzt/4faykcbnPbCQ== | ||
11342 | |||
11343 | totalist@^1.0.0: | 10763 | totalist@^1.0.0: |
11344 | version "1.1.0" | 10764 | version "1.1.0" |
11345 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" | 10765 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" |
@@ -11511,13 +10931,6 @@ uglify-js@^3.0.6: | |||
11511 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" | 10931 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" |
11512 | integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== | 10932 | integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== |
11513 | 10933 | ||
11514 | uint64be@^2.0.2: | ||
11515 | version "2.0.2" | ||
11516 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" | ||
11517 | integrity sha512-9QqdvpGQTXgxthP+lY4e/gIBy+RuqcBaC6JVwT5I3bDLgT/btL6twZMR0pI3/Fgah9G/pdwzIprE5gL6v9UvyQ== | ||
11518 | dependencies: | ||
11519 | buffer-alloc "^1.1.0" | ||
11520 | |||
11521 | unbox-primitive@^1.0.2: | 10934 | unbox-primitive@^1.0.2: |
11522 | version "1.0.2" | 10935 | version "1.0.2" |
11523 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" | 10936 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" |
@@ -11597,11 +11010,6 @@ unordered-array-remove@^1.0.2: | |||
11597 | resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef" | 11010 | resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef" |
11598 | integrity sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw== | 11011 | integrity sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw== |
11599 | 11012 | ||
11600 | unordered-set@^2.0.1: | ||
11601 | version "2.0.1" | ||
11602 | resolved "https://registry.yarnpkg.com/unordered-set/-/unordered-set-2.0.1.tgz#4cd0fe27b8814bcf5d6073e5f0966ec7a50841e6" | ||
11603 | integrity sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg== | ||
11604 | |||
11605 | unpipe@1.0.0, unpipe@~1.0.0: | 11013 | unpipe@1.0.0, unpipe@~1.0.0: |
11606 | version "1.0.0" | 11014 | version "1.0.0" |
11607 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" | 11015 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" |
@@ -11648,33 +11056,6 @@ url-toolkit@^2.2.1: | |||
11648 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" | 11056 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" |
11649 | integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== | 11057 | integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== |
11650 | 11058 | ||
11651 | url@^0.11.0: | ||
11652 | version "0.11.0" | ||
11653 | resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" | ||
11654 | integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ== | ||
11655 | dependencies: | ||
11656 | punycode "1.3.2" | ||
11657 | querystring "0.2.0" | ||
11658 | |||
11659 | ut_metadata@^3.5.2: | ||
11660 | version "3.5.2" | ||
11661 | resolved "https://registry.yarnpkg.com/ut_metadata/-/ut_metadata-3.5.2.tgz#2351c9348759e929978fa6a08d56ef6f584749e7" | ||
11662 | integrity sha512-3XZZuJSeoIUyMYSuDbTbVtP4KAVGHPfU8nmHFkr8LJc+THCaUXwnu/2AV+LCSLarET/hL9IlbNfYTGrt6fOVuQ== | ||
11663 | dependencies: | ||
11664 | bencode "^2.0.1" | ||
11665 | bitfield "^4.0.0" | ||
11666 | debug "^4.2.0" | ||
11667 | simple-sha1 "^3.0.1" | ||
11668 | |||
11669 | ut_pex@^3.0.2: | ||
11670 | version "3.0.2" | ||
11671 | resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-3.0.2.tgz#cd794d4fe02ebfa82704d41854c76c8d8187eea0" | ||
11672 | integrity sha512-3xM88t+AVU5GR0sIY3tmRMLUS+YKiwStc7U7+ZFQ+UHQpX7BjVJOomhmtm0Bs+8R2n812Dt2ymXm01EqDrOOpQ== | ||
11673 | dependencies: | ||
11674 | bencode "^2.0.2" | ||
11675 | compact2string "^1.4.1" | ||
11676 | string2compact "^1.3.2" | ||
11677 | |||
11678 | utf-8-validate@^5.0.5: | 11059 | utf-8-validate@^5.0.5: |
11679 | version "5.0.10" | 11060 | version "5.0.10" |
11680 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" | 11061 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" |
@@ -11697,17 +11078,6 @@ utils-merge@1.0.1: | |||
11697 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" | 11078 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" |
11698 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== | 11079 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== |
11699 | 11080 | ||
11700 | utp-native@^2.5.3: | ||
11701 | version "2.5.3" | ||
11702 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.3.tgz#7c04c2a8c2858716555a77d10adb9819e3119b25" | ||
11703 | integrity sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w== | ||
11704 | dependencies: | ||
11705 | napi-macros "^2.0.0" | ||
11706 | node-gyp-build "^4.2.0" | ||
11707 | readable-stream "^3.0.2" | ||
11708 | timeout-refresh "^1.0.0" | ||
11709 | unordered-set "^2.0.1" | ||
11710 | |||
11711 | uue@^3.1.0: | 11081 | uue@^3.1.0: |
11712 | version "3.1.2" | 11082 | version "3.1.2" |
11713 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" | 11083 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" |
@@ -11782,18 +11152,6 @@ videojs-vtt.js@^0.15.4: | |||
11782 | dependencies: | 11152 | dependencies: |
11783 | global "^4.3.1" | 11153 | global "^4.3.1" |
11784 | 11154 | ||
11785 | videostream@^3.2.2, videostream@~3.2.1: | ||
11786 | version "3.2.2" | ||
11787 | resolved "https://registry.yarnpkg.com/videostream/-/videostream-3.2.2.tgz#e3e8d44f5159892f8f31ad35cbf9302d7a6e6afc" | ||
11788 | integrity sha512-4tz23yGGeATmbzj/ZnUm6wgQ4E1lzmMXu2mUA/c0G6adtWKxm1Di5YejdZdRsK6SdkLjKjhplFFYT7r+UUDKvA== | ||
11789 | dependencies: | ||
11790 | binary-search "^1.3.4" | ||
11791 | mediasource "^2.2.2" | ||
11792 | mp4-box-encoding "^1.3.0" | ||
11793 | mp4-stream "^3.0.0" | ||
11794 | pump "^3.0.0" | ||
11795 | range-slice-stream "^2.0.0" | ||
11796 | |||
11797 | vite@4.3.1: | 11155 | vite@4.3.1: |
11798 | version "4.3.1" | 11156 | version "4.3.1" |
11799 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a" | 11157 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a" |
@@ -12110,63 +11468,6 @@ websocket-extensions@>=0.1.1: | |||
12110 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" | 11468 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" |
12111 | integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== | 11469 | integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== |
12112 | 11470 | ||
12113 | webtorrent@1.8.26: | ||
12114 | version "1.8.26" | ||
12115 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-1.8.26.tgz#c40313f3329d2bdfe8ae23365c17dd77825a829d" | ||
12116 | integrity sha512-1bbCIDtbk4OA7xXmT87t6jDhnng6RNC9d7HNpRyvxF0GQTrIz1fB3oDnNcbOim9Upjy1GDqxAOe0Mejmc86TUg== | ||
12117 | dependencies: | ||
12118 | "@webtorrent/http-node" "^1.3.0" | ||
12119 | addr-to-ip-port "^1.5.4" | ||
12120 | bitfield "^4.1.0" | ||
12121 | bittorrent-dht "^10.0.4" | ||
12122 | bittorrent-protocol "^3.5.5" | ||
12123 | cache-chunk-store "^3.2.2" | ||
12124 | chrome-net "^3.3.4" | ||
12125 | chunk-store-stream "^4.3.0" | ||
12126 | cpus "^1.0.3" | ||
12127 | create-torrent "^5.0.4" | ||
12128 | debug "^4.3.4" | ||
12129 | end-of-stream "^1.4.4" | ||
12130 | escape-html "^1.0.3" | ||
12131 | fs-chunk-store "^2.0.5" | ||
12132 | immediate-chunk-store "^2.2.0" | ||
12133 | load-ip-set "^2.2.1" | ||
12134 | lt_donthave "^1.0.1" | ||
12135 | memory-chunk-store "^1.3.5" | ||
12136 | mime "^3.0.0" | ||
12137 | multistream "^4.1.0" | ||
12138 | package-json-versionify "^1.0.4" | ||
12139 | parse-torrent "^9.1.5" | ||
12140 | pump "^3.0.0" | ||
12141 | queue-microtask "^1.2.3" | ||
12142 | random-iterate "^1.0.1" | ||
12143 | randombytes "^2.1.0" | ||
12144 | range-parser "^1.2.1" | ||
12145 | render-media "^4.1.0" | ||
12146 | run-parallel "^1.2.0" | ||
12147 | run-parallel-limit "^1.1.0" | ||
12148 | simple-concat "^1.0.1" | ||
12149 | simple-get "^4.0.1" | ||
12150 | simple-peer "^9.11.1" | ||
12151 | simple-sha1 "^3.1.0" | ||
12152 | speed-limiter "^1.0.2" | ||
12153 | stream-to-blob "^2.0.1" | ||
12154 | stream-to-blob-url "^3.0.2" | ||
12155 | stream-with-known-length-to-buffer "^1.0.4" | ||
12156 | throughput "^1.0.1" | ||
12157 | torrent-discovery "^9.4.13" | ||
12158 | torrent-piece "^2.0.1" | ||
12159 | unordered-array-remove "^1.0.2" | ||
12160 | ut_metadata "^3.5.2" | ||
12161 | ut_pex "^3.0.2" | ||
12162 | optionalDependencies: | ||
12163 | utp-native "^2.5.3" | ||
12164 | |||
12165 | whatwg-fetch@^3.0.0: | ||
12166 | version "3.6.2" | ||
12167 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" | ||
12168 | integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== | ||
12169 | |||
12170 | whatwg-url@^5.0.0: | 11471 | whatwg-url@^5.0.0: |
12171 | version "5.0.0" | 11472 | version "5.0.0" |
12172 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" | 11473 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" |
@@ -12329,11 +11630,6 @@ xmlhttprequest-ssl@~2.0.0: | |||
12329 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" | 11630 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" |
12330 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== | 11631 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== |
12331 | 11632 | ||
12332 | xtend@^4.0.2: | ||
12333 | version "4.0.2" | ||
12334 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" | ||
12335 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== | ||
12336 | |||
12337 | y18n@^3.2.1: | 11633 | y18n@^3.2.1: |
12338 | version "3.2.2" | 11634 | version "3.2.2" |
12339 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" | 11635 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" |
diff --git a/config/default.yaml b/config/default.yaml index 5d0eab4f5..e590ab300 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -130,12 +130,13 @@ storage: | |||
130 | tmp_persistent: 'storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts | 130 | tmp_persistent: 'storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts |
131 | bin: 'storage/bin/' | 131 | bin: 'storage/bin/' |
132 | avatars: 'storage/avatars/' | 132 | avatars: 'storage/avatars/' |
133 | videos: 'storage/videos/' | 133 | web_videos: 'storage/web-videos/' |
134 | streaming_playlists: 'storage/streaming-playlists/' | 134 | streaming_playlists: 'storage/streaming-playlists/' |
135 | redundancy: 'storage/redundancy/' | 135 | redundancy: 'storage/redundancy/' |
136 | logs: 'storage/logs/' | 136 | logs: 'storage/logs/' |
137 | previews: 'storage/previews/' | 137 | previews: 'storage/previews/' |
138 | thumbnails: 'storage/thumbnails/' | 138 | thumbnails: 'storage/thumbnails/' |
139 | storyboards: 'storage/storyboards/' | ||
139 | torrents: 'storage/torrents/' | 140 | torrents: 'storage/torrents/' |
140 | captions: 'storage/captions/' | 141 | captions: 'storage/captions/' |
141 | cache: 'storage/cache/' | 142 | cache: 'storage/cache/' |
@@ -200,9 +201,9 @@ object_storage: | |||
200 | # Useful when you want to use a CDN/external proxy | 201 | # Useful when you want to use a CDN/external proxy |
201 | base_url: '' # Example: 'https://mirror.example.com' | 202 | base_url: '' # Example: 'https://mirror.example.com' |
202 | 203 | ||
203 | # Same settings but for webtorrent videos | 204 | # Same settings but for web videos |
204 | videos: | 205 | web_videos: |
205 | bucket_name: 'videos' | 206 | bucket_name: 'web-videos' |
206 | prefix: '' | 207 | prefix: '' |
207 | base_url: '' | 208 | base_url: '' |
208 | 209 | ||
@@ -396,6 +397,8 @@ cache: | |||
396 | size: 500 # Max number of video captions/subtitles you want to cache | 397 | size: 500 # Max number of video captions/subtitles you want to cache |
397 | torrents: | 398 | torrents: |
398 | size: 500 # Max number of video torrents you want to cache | 399 | size: 500 # Max number of video torrents you want to cache |
400 | storyboards: | ||
401 | size: 500 # Max number of video storyboards you want to cache | ||
399 | 402 | ||
400 | admin: | 403 | admin: |
401 | # Used to generate the root user at first startup | 404 | # Used to generate the root user at first startup |
@@ -477,18 +480,18 @@ transcoding: | |||
477 | # Transcode and keep original resolution, even if it's above your maximum enabled resolution | 480 | # Transcode and keep original resolution, even if it's above your maximum enabled resolution |
478 | always_transcode_original_resolution: true | 481 | always_transcode_original_resolution: true |
479 | 482 | ||
480 | # Generate videos in a WebTorrent format (what we do since the first PeerTube release) | 483 | # Generate videos in a web compatible format |
481 | # If you also enabled the hls format, it will multiply videos storage by 2 | 484 | # If you also enabled the hls format, it will multiply videos storage by 2 |
482 | # If disabled, breaks federation with PeerTube instances < 2.1 | 485 | # If disabled, breaks federation with PeerTube instances < 2.1 |
483 | webtorrent: | 486 | web_videos: |
484 | enabled: false | 487 | enabled: false |
485 | 488 | ||
486 | # /!\ Requires ffmpeg >= 4.1 | 489 | # /!\ Requires ffmpeg >= 4.1 |
487 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: | 490 | # Generate HLS playlists and fragmented MP4 files. Better playback than with Web Videos: |
488 | # * Resolution change is smoother | 491 | # * Resolution change is smoother |
489 | # * Faster playback in particular with long videos | 492 | # * Faster playback in particular with long videos |
490 | # * More stable playback (less bugs/infinite loading) | 493 | # * More stable playback (less bugs/infinite loading) |
491 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 | 494 | # If you also enabled the web videos format, it will multiply videos storage by 2 |
492 | hls: | 495 | hls: |
493 | enabled: true | 496 | enabled: true |
494 | 497 | ||
diff --git a/config/production.yaml.example b/config/production.yaml.example index 5514f1af6..884300ddb 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -128,12 +128,13 @@ storage: | |||
128 | tmp_persistent: '/var/www/peertube/storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts | 128 | tmp_persistent: '/var/www/peertube/storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts |
129 | bin: '/var/www/peertube/storage/bin/' | 129 | bin: '/var/www/peertube/storage/bin/' |
130 | avatars: '/var/www/peertube/storage/avatars/' | 130 | avatars: '/var/www/peertube/storage/avatars/' |
131 | videos: '/var/www/peertube/storage/videos/' | 131 | web_videos: '/var/www/peertube/storage/web-videos/' |
132 | streaming_playlists: '/var/www/peertube/storage/streaming-playlists/' | 132 | streaming_playlists: '/var/www/peertube/storage/streaming-playlists/' |
133 | redundancy: '/var/www/peertube/storage/redundancy/' | 133 | redundancy: '/var/www/peertube/storage/redundancy/' |
134 | logs: '/var/www/peertube/storage/logs/' | 134 | logs: '/var/www/peertube/storage/logs/' |
135 | previews: '/var/www/peertube/storage/previews/' | 135 | previews: '/var/www/peertube/storage/previews/' |
136 | thumbnails: '/var/www/peertube/storage/thumbnails/' | 136 | thumbnails: '/var/www/peertube/storage/thumbnails/' |
137 | storyboards: '/var/www/peertube/storage/storyboards/' | ||
137 | torrents: '/var/www/peertube/storage/torrents/' | 138 | torrents: '/var/www/peertube/storage/torrents/' |
138 | captions: '/var/www/peertube/storage/captions/' | 139 | captions: '/var/www/peertube/storage/captions/' |
139 | cache: '/var/www/peertube/storage/cache/' | 140 | cache: '/var/www/peertube/storage/cache/' |
@@ -198,9 +199,9 @@ object_storage: | |||
198 | # Useful when you want to use a CDN/external proxy | 199 | # Useful when you want to use a CDN/external proxy |
199 | base_url: '' # Example: 'https://mirror.example.com' | 200 | base_url: '' # Example: 'https://mirror.example.com' |
200 | 201 | ||
201 | # Same settings but for webtorrent videos | 202 | # Same settings but for web videos |
202 | videos: | 203 | web_videos: |
203 | bucket_name: 'videos' | 204 | bucket_name: 'web-videos' |
204 | prefix: '' | 205 | prefix: '' |
205 | base_url: '' | 206 | base_url: '' |
206 | 207 | ||
@@ -406,6 +407,8 @@ cache: | |||
406 | size: 500 # Max number of video captions/subtitles you want to cache | 407 | size: 500 # Max number of video captions/subtitles you want to cache |
407 | torrents: | 408 | torrents: |
408 | size: 500 # Max number of video torrents you want to cache | 409 | size: 500 # Max number of video torrents you want to cache |
410 | storyboards: | ||
411 | size: 500 # Max number of video storyboards you want to cache | ||
409 | 412 | ||
410 | admin: | 413 | admin: |
411 | # Used to generate the root user at first startup | 414 | # Used to generate the root user at first startup |
@@ -487,18 +490,18 @@ transcoding: | |||
487 | # Transcode and keep original resolution, even if it's above your maximum enabled resolution | 490 | # Transcode and keep original resolution, even if it's above your maximum enabled resolution |
488 | always_transcode_original_resolution: true | 491 | always_transcode_original_resolution: true |
489 | 492 | ||
490 | # Generate videos in a WebTorrent format (what we do since the first PeerTube release) | 493 | # Generate videos in a web compatible format |
491 | # If you also enabled the hls format, it will multiply videos storage by 2 | 494 | # If you also enabled the hls format, it will multiply videos storage by 2 |
492 | # If disabled, breaks federation with PeerTube instances < 2.1 | 495 | # If disabled, breaks federation with PeerTube instances < 2.1 |
493 | webtorrent: | 496 | web_videos: |
494 | enabled: false | 497 | enabled: false |
495 | 498 | ||
496 | # /!\ Requires ffmpeg >= 4.1 | 499 | # /!\ Requires ffmpeg >= 4.1 |
497 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: | 500 | # Generate HLS playlists and fragmented MP4 files. Better playback than with Web Videos: |
498 | # * Resolution change is smoother | 501 | # * Resolution change is smoother |
499 | # * Faster playback in particular with long videos | 502 | # * Faster playback in particular with long videos |
500 | # * More stable playback (less bugs/infinite loading) | 503 | # * More stable playback (less bugs/infinite loading) |
501 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 | 504 | # If you also enabled the web videos format, it will multiply videos storage by 2 |
502 | hls: | 505 | hls: |
503 | enabled: true | 506 | enabled: true |
504 | 507 | ||
@@ -596,7 +599,6 @@ video_studio: | |||
596 | # If enabled, users can create transcoding tasks as they wish | 599 | # If enabled, users can create transcoding tasks as they wish |
597 | enabled: false | 600 | enabled: false |
598 | 601 | ||
599 | |||
600 | # Enable remote runners to transcode studio tasks | 602 | # Enable remote runners to transcode studio tasks |
601 | # If enabled, your instance won't transcode the videos itself | 603 | # If enabled, your instance won't transcode the videos itself |
602 | # At least 1 remote runner must be configured to transcode your videos | 604 | # At least 1 remote runner must be configured to transcode your videos |
diff --git a/config/test-1.yaml b/config/test-1.yaml index 7b62e3d0c..b86b48438 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test1/tmp-persistent/' | 13 | tmp_persistent: 'test1/tmp-persistent/' |
14 | bin: 'test1/bin/' | 14 | bin: 'test1/bin/' |
15 | avatars: 'test1/avatars/' | 15 | avatars: 'test1/avatars/' |
16 | videos: 'test1/videos/' | 16 | web_videos: 'test1/web-videos/' |
17 | streaming_playlists: 'test1/streaming-playlists/' | 17 | streaming_playlists: 'test1/streaming-playlists/' |
18 | redundancy: 'test1/redundancy/' | 18 | redundancy: 'test1/redundancy/' |
19 | logs: 'test1/logs/' | 19 | logs: 'test1/logs/' |
20 | previews: 'test1/previews/' | 20 | previews: 'test1/previews/' |
21 | thumbnails: 'test1/thumbnails/' | 21 | thumbnails: 'test1/thumbnails/' |
22 | storyboards: 'test1/storyboards/' | ||
22 | torrents: 'test1/torrents/' | 23 | torrents: 'test1/torrents/' |
23 | captions: 'test1/captions/' | 24 | captions: 'test1/captions/' |
24 | cache: 'test1/cache/' | 25 | cache: 'test1/cache/' |
diff --git a/config/test-2.yaml b/config/test-2.yaml index ba36369a6..266e44c17 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test2/tmp-persistent/' | 13 | tmp_persistent: 'test2/tmp-persistent/' |
14 | bin: 'test2/bin/' | 14 | bin: 'test2/bin/' |
15 | avatars: 'test2/avatars/' | 15 | avatars: 'test2/avatars/' |
16 | videos: 'test2/videos/' | 16 | web_videos: 'test2/web-videos/' |
17 | streaming_playlists: 'test2/streaming-playlists/' | 17 | streaming_playlists: 'test2/streaming-playlists/' |
18 | redundancy: 'test2/redundancy/' | 18 | redundancy: 'test2/redundancy/' |
19 | logs: 'test2/logs/' | 19 | logs: 'test2/logs/' |
20 | previews: 'test2/previews/' | 20 | previews: 'test2/previews/' |
21 | thumbnails: 'test2/thumbnails/' | 21 | thumbnails: 'test2/thumbnails/' |
22 | storyboards: 'test2/storyboards/' | ||
22 | torrents: 'test2/torrents/' | 23 | torrents: 'test2/torrents/' |
23 | captions: 'test2/captions/' | 24 | captions: 'test2/captions/' |
24 | cache: 'test2/cache/' | 25 | cache: 'test2/cache/' |
diff --git a/config/test-3.yaml b/config/test-3.yaml index 6adec7953..b31d37765 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test3/tmp-persistent/' | 13 | tmp_persistent: 'test3/tmp-persistent/' |
14 | bin: 'test3/bin/' | 14 | bin: 'test3/bin/' |
15 | avatars: 'test3/avatars/' | 15 | avatars: 'test3/avatars/' |
16 | videos: 'test3/videos/' | 16 | web_videos: 'test3/web-videos/' |
17 | streaming_playlists: 'test3/streaming-playlists/' | 17 | streaming_playlists: 'test3/streaming-playlists/' |
18 | redundancy: 'test3/redundancy/' | 18 | redundancy: 'test3/redundancy/' |
19 | logs: 'test3/logs/' | 19 | logs: 'test3/logs/' |
20 | previews: 'test3/previews/' | 20 | previews: 'test3/previews/' |
21 | thumbnails: 'test3/thumbnails/' | 21 | thumbnails: 'test3/thumbnails/' |
22 | storyboards: 'test3/storyboards/' | ||
22 | torrents: 'test3/torrents/' | 23 | torrents: 'test3/torrents/' |
23 | captions: 'test3/captions/' | 24 | captions: 'test3/captions/' |
24 | cache: 'test3/cache/' | 25 | cache: 'test3/cache/' |
diff --git a/config/test-4.yaml b/config/test-4.yaml index f042aee46..d73b09b56 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test4/tmp-persistent/' | 13 | tmp_persistent: 'test4/tmp-persistent/' |
14 | bin: 'test4/bin/' | 14 | bin: 'test4/bin/' |
15 | avatars: 'test4/avatars/' | 15 | avatars: 'test4/avatars/' |
16 | videos: 'test4/videos/' | 16 | web_videos: 'test4/web-videos/' |
17 | streaming_playlists: 'test4/streaming-playlists/' | 17 | streaming_playlists: 'test4/streaming-playlists/' |
18 | redundancy: 'test4/redundancy/' | 18 | redundancy: 'test4/redundancy/' |
19 | logs: 'test4/logs/' | 19 | logs: 'test4/logs/' |
20 | previews: 'test4/previews/' | 20 | previews: 'test4/previews/' |
21 | thumbnails: 'test4/thumbnails/' | 21 | thumbnails: 'test4/thumbnails/' |
22 | storyboards: 'test4/storyboards/' | ||
22 | torrents: 'test4/torrents/' | 23 | torrents: 'test4/torrents/' |
23 | captions: 'test4/captions/' | 24 | captions: 'test4/captions/' |
24 | cache: 'test4/cache/' | 25 | cache: 'test4/cache/' |
diff --git a/config/test-5.yaml b/config/test-5.yaml index ad90fec04..56cdc5242 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test5/tmp-persistent/' | 13 | tmp_persistent: 'test5/tmp-persistent/' |
14 | bin: 'test5/bin/' | 14 | bin: 'test5/bin/' |
15 | avatars: 'test5/avatars/' | 15 | avatars: 'test5/avatars/' |
16 | videos: 'test5/videos/' | 16 | web_videos: 'test5/web-videos/' |
17 | streaming_playlists: 'test5/streaming-playlists/' | 17 | streaming_playlists: 'test5/streaming-playlists/' |
18 | redundancy: 'test5/redundancy/' | 18 | redundancy: 'test5/redundancy/' |
19 | logs: 'test5/logs/' | 19 | logs: 'test5/logs/' |
20 | previews: 'test5/previews/' | 20 | previews: 'test5/previews/' |
21 | thumbnails: 'test5/thumbnails/' | 21 | thumbnails: 'test5/thumbnails/' |
22 | storyboards: 'test5/storyboards/' | ||
22 | torrents: 'test5/torrents/' | 23 | torrents: 'test5/torrents/' |
23 | captions: 'test5/captions/' | 24 | captions: 'test5/captions/' |
24 | cache: 'test5/cache/' | 25 | cache: 'test5/cache/' |
diff --git a/config/test-6.yaml b/config/test-6.yaml index a579f1f01..0e212c699 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml | |||
@@ -13,12 +13,13 @@ storage: | |||
13 | tmp_persistent: 'test6/tmp-persistent/' | 13 | tmp_persistent: 'test6/tmp-persistent/' |
14 | bin: 'test6/bin/' | 14 | bin: 'test6/bin/' |
15 | avatars: 'test6/avatars/' | 15 | avatars: 'test6/avatars/' |
16 | videos: 'test6/videos/' | 16 | web_videos: 'test6/web-videos/' |
17 | streaming_playlists: 'test6/streaming-playlists/' | 17 | streaming_playlists: 'test6/streaming-playlists/' |
18 | redundancy: 'test6/redundancy/' | 18 | redundancy: 'test6/redundancy/' |
19 | logs: 'test6/logs/' | 19 | logs: 'test6/logs/' |
20 | previews: 'test6/previews/' | 20 | previews: 'test6/previews/' |
21 | thumbnails: 'test6/thumbnails/' | 21 | thumbnails: 'test6/thumbnails/' |
22 | storyboards: 'test6/storyboards/' | ||
22 | torrents: 'test6/torrents/' | 23 | torrents: 'test6/torrents/' |
23 | captions: 'test6/captions/' | 24 | captions: 'test6/captions/' |
24 | cache: 'test6/cache/' | 25 | cache: 'test6/cache/' |
diff --git a/config/test.yaml b/config/test.yaml index 361064af1..cc642327c 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -73,6 +73,8 @@ cache: | |||
73 | size: 1 | 73 | size: 1 |
74 | torrents: | 74 | torrents: |
75 | size: 1 | 75 | size: 1 |
76 | storyboards: | ||
77 | size: 1 | ||
76 | 78 | ||
77 | signup: | 79 | signup: |
78 | enabled: true | 80 | enabled: true |
@@ -95,7 +97,7 @@ transcoding: | |||
95 | 1080p: true | 97 | 1080p: true |
96 | 1440p: true | 98 | 1440p: true |
97 | 2160p: true | 99 | 2160p: true |
98 | webtorrent: | 100 | web_videos: |
99 | enabled: true | 101 | enabled: true |
100 | hls: | 102 | hls: |
101 | enabled: true | 103 | enabled: true |
diff --git a/package.json b/package.json index 223156098..7573232a1 100644 --- a/package.json +++ b/package.json | |||
@@ -49,6 +49,7 @@ | |||
49 | "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js", | 49 | "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js", |
50 | "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", | 50 | "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", |
51 | "create-move-video-storage-job": "node ./dist/scripts/create-move-video-storage-job.js", | 51 | "create-move-video-storage-job": "node ./dist/scripts/create-move-video-storage-job.js", |
52 | "create-generate-storyboard-job": "node ./dist/scripts/create-generate-storyboard-job.js", | ||
52 | "test": "bash ./scripts/test.sh", | 53 | "test": "bash ./scripts/test.sh", |
53 | "generate-cli-doc": "bash ./scripts/generate-cli-doc.sh", | 54 | "generate-cli-doc": "bash ./scripts/generate-cli-doc.sh", |
54 | "generate-types-package": "ts-node ./packages/types/generate-package.ts", | 55 | "generate-types-package": "ts-node ./packages/types/generate-package.ts", |
@@ -77,6 +78,7 @@ | |||
77 | "@aws-sdk/client-s3": "^3.190.0", | 78 | "@aws-sdk/client-s3": "^3.190.0", |
78 | "@aws-sdk/lib-storage": "^3.190.0", | 79 | "@aws-sdk/lib-storage": "^3.190.0", |
79 | "@aws-sdk/node-http-handler": "^3.190.0", | 80 | "@aws-sdk/node-http-handler": "^3.190.0", |
81 | "@aws-sdk/s3-request-presigner": "^3.345.0", | ||
80 | "@babel/parser": "^7.17.8", | 82 | "@babel/parser": "^7.17.8", |
81 | "@node-oauth/oauth2-server": "^4.2.0", | 83 | "@node-oauth/oauth2-server": "^4.2.0", |
82 | "@opentelemetry/api": "^1.1.0", | 84 | "@opentelemetry/api": "^1.1.0", |
diff --git a/packages/peertube-runner/package.json b/packages/peertube-runner/package.json index a57acf189..1c525691a 100644 --- a/packages/peertube-runner/package.json +++ b/packages/peertube-runner/package.json | |||
@@ -1,6 +1,6 @@ | |||
1 | { | 1 | { |
2 | "name": "@peertube/peertube-runner", | 2 | "name": "@peertube/peertube-runner", |
3 | "version": "0.0.4", | 3 | "version": "0.0.5", |
4 | "main": "dist/peertube-runner.js", | 4 | "main": "dist/peertube-runner.js", |
5 | "bin": "dist/peertube-runner.js", | 5 | "bin": "dist/peertube-runner.js", |
6 | "license": "AGPL-3.0", | 6 | "license": "AGPL-3.0", |
diff --git a/packages/peertube-runner/server/process/shared/common.ts b/packages/peertube-runner/server/process/shared/common.ts index dbeb9dfc1..a9b37bbc4 100644 --- a/packages/peertube-runner/server/process/shared/common.ts +++ b/packages/peertube-runner/server/process/shared/common.ts | |||
@@ -35,49 +35,48 @@ export async function downloadInputFile (options: { | |||
35 | return destination | 35 | return destination |
36 | } | 36 | } |
37 | 37 | ||
38 | export async function updateTranscodingProgress (options: { | 38 | export function scheduleTranscodingProgress (options: { |
39 | server: PeerTubeServer | 39 | server: PeerTubeServer |
40 | runnerToken: string | 40 | runnerToken: string |
41 | job: JobWithToken | 41 | job: JobWithToken |
42 | progress: number | 42 | progressGetter: () => number |
43 | }) { | 43 | }) { |
44 | const { server, job, runnerToken, progress } = options | 44 | const { job, server, progressGetter, runnerToken } = options |
45 | |||
46 | return server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress }) | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export function buildFFmpegVOD (options: { | ||
52 | server: PeerTubeServer | ||
53 | runnerToken: string | ||
54 | job: JobWithToken | ||
55 | }) { | ||
56 | const { server, job, runnerToken } = options | ||
57 | 45 | ||
58 | const updateInterval = ConfigManager.Instance.isTestInstance() | 46 | const updateInterval = ConfigManager.Instance.isTestInstance() |
59 | ? 500 | 47 | ? 500 |
60 | : 60000 | 48 | : 60000 |
61 | 49 | ||
62 | let progress: number | 50 | const update = () => { |
51 | server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress: progressGetter() }) | ||
52 | .catch(err => logger.error({ err }, 'Cannot send job progress')) | ||
53 | } | ||
63 | 54 | ||
64 | const interval = setInterval(() => { | 55 | const interval = setInterval(() => { |
65 | updateTranscodingProgress({ server, job, runnerToken, progress }) | 56 | update() |
66 | .catch(err => logger.error({ err }, 'Cannot send job progress')) | ||
67 | }, updateInterval) | 57 | }, updateInterval) |
68 | 58 | ||
59 | update() | ||
60 | |||
61 | return interval | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | export function buildFFmpegVOD (options: { | ||
67 | onJobProgress: (progress: number) => void | ||
68 | }) { | ||
69 | const { onJobProgress } = options | ||
70 | |||
69 | return new FFmpegVOD({ | 71 | return new FFmpegVOD({ |
70 | ...getCommonFFmpegOptions(), | 72 | ...getCommonFFmpegOptions(), |
71 | 73 | ||
72 | onError: () => clearInterval(interval), | ||
73 | onEnd: () => clearInterval(interval), | ||
74 | |||
75 | updateJobProgress: arg => { | 74 | updateJobProgress: arg => { |
76 | if (arg < 0 || arg > 100) { | 75 | const progress = arg < 0 || arg > 100 |
77 | progress = undefined | 76 | ? undefined |
78 | } else { | 77 | : arg |
79 | progress = arg | 78 | |
80 | } | 79 | onJobProgress(progress) |
81 | } | 80 | } |
82 | }) | 81 | }) |
83 | } | 82 | } |
diff --git a/packages/peertube-runner/server/process/shared/process-studio.ts b/packages/peertube-runner/server/process/shared/process-studio.ts index 9c745d031..afd9347fe 100644 --- a/packages/peertube-runner/server/process/shared/process-studio.ts +++ b/packages/peertube-runner/server/process/shared/process-studio.ts | |||
@@ -5,26 +5,42 @@ import { join } from 'path' | |||
5 | import { buildUUID } from '@shared/extra-utils' | 5 | import { buildUUID } from '@shared/extra-utils' |
6 | import { | 6 | import { |
7 | RunnerJobStudioTranscodingPayload, | 7 | RunnerJobStudioTranscodingPayload, |
8 | VideoStudioTranscodingSuccess, | ||
9 | VideoStudioTask, | 8 | VideoStudioTask, |
10 | VideoStudioTaskCutPayload, | 9 | VideoStudioTaskCutPayload, |
11 | VideoStudioTaskIntroPayload, | 10 | VideoStudioTaskIntroPayload, |
12 | VideoStudioTaskOutroPayload, | 11 | VideoStudioTaskOutroPayload, |
13 | VideoStudioTaskPayload, | 12 | VideoStudioTaskPayload, |
14 | VideoStudioTaskWatermarkPayload | 13 | VideoStudioTaskWatermarkPayload, |
14 | VideoStudioTranscodingSuccess | ||
15 | } from '@shared/models' | 15 | } from '@shared/models' |
16 | import { ConfigManager } from '../../../shared/config-manager' | 16 | import { ConfigManager } from '../../../shared/config-manager' |
17 | import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions } from './common' | 17 | import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions, scheduleTranscodingProgress } from './common' |
18 | 18 | ||
19 | export async function processStudioTranscoding (options: ProcessOptions<RunnerJobStudioTranscodingPayload>) { | 19 | export async function processStudioTranscoding (options: ProcessOptions<RunnerJobStudioTranscodingPayload>) { |
20 | const { server, job, runnerToken } = options | 20 | const { server, job, runnerToken } = options |
21 | const payload = job.payload | 21 | const payload = job.payload |
22 | 22 | ||
23 | let inputPath: string | ||
23 | let outputPath: string | 24 | let outputPath: string |
24 | const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | 25 | let tmpInputFilePath: string |
25 | let tmpInputFilePath = inputPath | 26 | |
27 | let tasksProgress = 0 | ||
28 | |||
29 | const updateProgressInterval = scheduleTranscodingProgress({ | ||
30 | job, | ||
31 | server, | ||
32 | runnerToken, | ||
33 | progressGetter: () => tasksProgress | ||
34 | }) | ||
26 | 35 | ||
27 | try { | 36 | try { |
37 | logger.info(`Downloading input file ${payload.input.videoFileUrl} for job ${job.jobToken}`) | ||
38 | |||
39 | inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | ||
40 | tmpInputFilePath = inputPath | ||
41 | |||
42 | logger.info(`Input file ${payload.input.videoFileUrl} downloaded for job ${job.jobToken}. Running studio transcoding tasks.`) | ||
43 | |||
28 | for (const task of payload.tasks) { | 44 | for (const task of payload.tasks) { |
29 | const outputFilename = 'output-edition-' + buildUUID() + '.mp4' | 45 | const outputFilename = 'output-edition-' + buildUUID() + '.mp4' |
30 | outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), outputFilename) | 46 | outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), outputFilename) |
@@ -41,6 +57,8 @@ export async function processStudioTranscoding (options: ProcessOptions<RunnerJo | |||
41 | 57 | ||
42 | // For the next iteration | 58 | // For the next iteration |
43 | tmpInputFilePath = outputPath | 59 | tmpInputFilePath = outputPath |
60 | |||
61 | tasksProgress += Math.floor(100 / payload.tasks.length) | ||
44 | } | 62 | } |
45 | 63 | ||
46 | const successBody: VideoStudioTranscodingSuccess = { | 64 | const successBody: VideoStudioTranscodingSuccess = { |
@@ -54,8 +72,9 @@ export async function processStudioTranscoding (options: ProcessOptions<RunnerJo | |||
54 | payload: successBody | 72 | payload: successBody |
55 | }) | 73 | }) |
56 | } finally { | 74 | } finally { |
57 | await remove(tmpInputFilePath) | 75 | if (tmpInputFilePath) await remove(tmpInputFilePath) |
58 | await remove(outputPath) | 76 | if (outputPath) await remove(outputPath) |
77 | if (updateProgressInterval) clearInterval(updateProgressInterval) | ||
59 | } | 78 | } |
60 | } | 79 | } |
61 | 80 | ||
diff --git a/packages/peertube-runner/server/process/shared/process-vod.ts b/packages/peertube-runner/server/process/shared/process-vod.ts index 22489afd5..f7c076b27 100644 --- a/packages/peertube-runner/server/process/shared/process-vod.ts +++ b/packages/peertube-runner/server/process/shared/process-vod.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { logger } from 'packages/peertube-runner/shared' | ||
2 | import { join } from 'path' | 3 | import { join } from 'path' |
3 | import { buildUUID } from '@shared/extra-utils' | 4 | import { buildUUID } from '@shared/extra-utils' |
4 | import { | 5 | import { |
@@ -10,19 +11,36 @@ import { | |||
10 | VODWebVideoTranscodingSuccess | 11 | VODWebVideoTranscodingSuccess |
11 | } from '@shared/models' | 12 | } from '@shared/models' |
12 | import { ConfigManager } from '../../../shared/config-manager' | 13 | import { ConfigManager } from '../../../shared/config-manager' |
13 | import { buildFFmpegVOD, downloadInputFile, ProcessOptions } from './common' | 14 | import { buildFFmpegVOD, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common' |
14 | 15 | ||
15 | export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) { | 16 | export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) { |
16 | const { server, job, runnerToken } = options | 17 | const { server, job, runnerToken } = options |
17 | const payload = job.payload | ||
18 | 18 | ||
19 | const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | 19 | const payload = job.payload |
20 | 20 | ||
21 | const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken }) | 21 | let ffmpegProgress: number |
22 | let inputPath: string | ||
22 | 23 | ||
23 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`) | 24 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`) |
24 | 25 | ||
26 | const updateProgressInterval = scheduleTranscodingProgress({ | ||
27 | job, | ||
28 | server, | ||
29 | runnerToken, | ||
30 | progressGetter: () => ffmpegProgress | ||
31 | }) | ||
32 | |||
25 | try { | 33 | try { |
34 | logger.info(`Downloading input file ${payload.input.videoFileUrl} for web video transcoding job ${job.jobToken}`) | ||
35 | |||
36 | inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | ||
37 | |||
38 | logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running web video transcoding.`) | ||
39 | |||
40 | const ffmpegVod = buildFFmpegVOD({ | ||
41 | onJobProgress: progress => { ffmpegProgress = progress } | ||
42 | }) | ||
43 | |||
26 | await ffmpegVod.transcode({ | 44 | await ffmpegVod.transcode({ |
27 | type: 'video', | 45 | type: 'video', |
28 | 46 | ||
@@ -47,8 +65,9 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner | |||
47 | payload: successBody | 65 | payload: successBody |
48 | }) | 66 | }) |
49 | } finally { | 67 | } finally { |
50 | await remove(inputPath) | 68 | if (inputPath) await remove(inputPath) |
51 | await remove(outputPath) | 69 | if (outputPath) await remove(outputPath) |
70 | if (updateProgressInterval) clearInterval(updateProgressInterval) | ||
52 | } | 71 | } |
53 | } | 72 | } |
54 | 73 | ||
@@ -56,16 +75,32 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO | |||
56 | const { server, job, runnerToken } = options | 75 | const { server, job, runnerToken } = options |
57 | const payload = job.payload | 76 | const payload = job.payload |
58 | 77 | ||
59 | const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | 78 | let ffmpegProgress: number |
60 | const uuid = buildUUID() | 79 | let inputPath: string |
61 | 80 | ||
81 | const uuid = buildUUID() | ||
62 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `${uuid}-${payload.output.resolution}.m3u8`) | 82 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `${uuid}-${payload.output.resolution}.m3u8`) |
63 | const videoFilename = `${uuid}-${payload.output.resolution}-fragmented.mp4` | 83 | const videoFilename = `${uuid}-${payload.output.resolution}-fragmented.mp4` |
64 | const videoPath = join(join(ConfigManager.Instance.getTranscodingDirectory(), videoFilename)) | 84 | const videoPath = join(join(ConfigManager.Instance.getTranscodingDirectory(), videoFilename)) |
65 | 85 | ||
66 | const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken }) | 86 | const updateProgressInterval = scheduleTranscodingProgress({ |
87 | job, | ||
88 | server, | ||
89 | runnerToken, | ||
90 | progressGetter: () => ffmpegProgress | ||
91 | }) | ||
67 | 92 | ||
68 | try { | 93 | try { |
94 | logger.info(`Downloading input file ${payload.input.videoFileUrl} for HLS transcoding job ${job.jobToken}`) | ||
95 | |||
96 | inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job }) | ||
97 | |||
98 | logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running HLS transcoding.`) | ||
99 | |||
100 | const ffmpegVod = buildFFmpegVOD({ | ||
101 | onJobProgress: progress => { ffmpegProgress = progress } | ||
102 | }) | ||
103 | |||
69 | await ffmpegVod.transcode({ | 104 | await ffmpegVod.transcode({ |
70 | type: 'hls', | 105 | type: 'hls', |
71 | copyCodecs: false, | 106 | copyCodecs: false, |
@@ -91,9 +126,10 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO | |||
91 | payload: successBody | 126 | payload: successBody |
92 | }) | 127 | }) |
93 | } finally { | 128 | } finally { |
94 | await remove(inputPath) | 129 | if (inputPath) await remove(inputPath) |
95 | await remove(outputPath) | 130 | if (outputPath) await remove(outputPath) |
96 | await remove(videoPath) | 131 | if (videoPath) await remove(videoPath) |
132 | if (updateProgressInterval) clearInterval(updateProgressInterval) | ||
97 | } | 133 | } |
98 | } | 134 | } |
99 | 135 | ||
@@ -101,14 +137,37 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn | |||
101 | const { server, job, runnerToken } = options | 137 | const { server, job, runnerToken } = options |
102 | const payload = job.payload | 138 | const payload = job.payload |
103 | 139 | ||
104 | const audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job }) | 140 | let ffmpegProgress: number |
105 | const inputPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job }) | 141 | let audioPath: string |
142 | let inputPath: string | ||
106 | 143 | ||
107 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`) | 144 | const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`) |
108 | 145 | ||
109 | const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken }) | 146 | const updateProgressInterval = scheduleTranscodingProgress({ |
147 | job, | ||
148 | server, | ||
149 | runnerToken, | ||
150 | progressGetter: () => ffmpegProgress | ||
151 | }) | ||
110 | 152 | ||
111 | try { | 153 | try { |
154 | logger.info( | ||
155 | `Downloading input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` + | ||
156 | `for audio merge transcoding job ${job.jobToken}` | ||
157 | ) | ||
158 | |||
159 | audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job }) | ||
160 | inputPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job }) | ||
161 | |||
162 | logger.info( | ||
163 | `Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` + | ||
164 | `for job ${job.jobToken}. Running audio merge transcoding.` | ||
165 | ) | ||
166 | |||
167 | const ffmpegVod = buildFFmpegVOD({ | ||
168 | onJobProgress: progress => { ffmpegProgress = progress } | ||
169 | }) | ||
170 | |||
112 | await ffmpegVod.transcode({ | 171 | await ffmpegVod.transcode({ |
113 | type: 'merge-audio', | 172 | type: 'merge-audio', |
114 | 173 | ||
@@ -134,8 +193,9 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn | |||
134 | payload: successBody | 193 | payload: successBody |
135 | }) | 194 | }) |
136 | } finally { | 195 | } finally { |
137 | await remove(audioPath) | 196 | if (audioPath) await remove(audioPath) |
138 | await remove(inputPath) | 197 | if (inputPath) await remove(inputPath) |
139 | await remove(outputPath) | 198 | if (outputPath) await remove(outputPath) |
199 | if (updateProgressInterval) clearInterval(updateProgressInterval) | ||
140 | } | 200 | } |
141 | } | 201 | } |
diff --git a/packages/peertube-runner/server/server.ts b/packages/peertube-runner/server/server.ts index e76131c74..5fa86fa1a 100644 --- a/packages/peertube-runner/server/server.ts +++ b/packages/peertube-runner/server/server.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { ensureDir, readdir, remove } from 'fs-extra' | 1 | import { ensureDir, readdir, remove } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { io, Socket } from 'socket.io-client' | 3 | import { io, Socket } from 'socket.io-client' |
4 | import { pick, wait } from '@shared/core-utils' | 4 | import { pick, shuffle, wait } from '@shared/core-utils' |
5 | import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' | 5 | import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' |
6 | import { PeerTubeServer as PeerTubeServerCommand } from '@shared/server-commands' | 6 | import { PeerTubeServer as PeerTubeServerCommand } from '@shared/server-commands' |
7 | import { ConfigManager } from '../shared' | 7 | import { ConfigManager } from '../shared' |
@@ -175,7 +175,7 @@ export class RunnerServer { | |||
175 | 175 | ||
176 | let hadAvailableJob = false | 176 | let hadAvailableJob = false |
177 | 177 | ||
178 | for (const server of this.servers) { | 178 | for (const server of shuffle([ ...this.servers ])) { |
179 | try { | 179 | try { |
180 | logger.info('Checking available jobs on ' + server.url) | 180 | logger.info('Checking available jobs on ' + server.url) |
181 | 181 | ||
diff --git a/scripts/create-generate-storyboard-job.ts b/scripts/create-generate-storyboard-job.ts new file mode 100644 index 000000000..47c08edac --- /dev/null +++ b/scripts/create-generate-storyboard-job.ts | |||
@@ -0,0 +1,85 @@ | |||
1 | import { program } from 'commander' | ||
2 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
3 | import { initDatabaseModels } from '@server/initializers/database' | ||
4 | import { JobQueue } from '@server/lib/job-queue' | ||
5 | import { VideoModel } from '@server/models/video/video' | ||
6 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
7 | |||
8 | program | ||
9 | .description('Generate videos storyboard') | ||
10 | .option('-v, --video [videoUUID]', 'Generate the storyboard of a specific video') | ||
11 | .option('-a, --all-videos', 'Generate missing storyboards of local videos') | ||
12 | .parse(process.argv) | ||
13 | |||
14 | const options = program.opts() | ||
15 | |||
16 | if (!options['video'] && !options['allVideos']) { | ||
17 | console.error('You need to choose videos for storyboard generation.') | ||
18 | process.exit(-1) | ||
19 | } | ||
20 | |||
21 | run() | ||
22 | .then(() => process.exit(0)) | ||
23 | .catch(err => { | ||
24 | console.error(err) | ||
25 | process.exit(-1) | ||
26 | }) | ||
27 | |||
28 | async function run () { | ||
29 | await initDatabaseModels(true) | ||
30 | |||
31 | JobQueue.Instance.init() | ||
32 | |||
33 | let ids: number[] = [] | ||
34 | |||
35 | if (options['video']) { | ||
36 | const video = await VideoModel.load(toCompleteUUID(options['video'])) | ||
37 | |||
38 | if (!video) { | ||
39 | console.error('Unknown video ' + options['video']) | ||
40 | process.exit(-1) | ||
41 | } | ||
42 | |||
43 | if (video.remote === true) { | ||
44 | console.error('Cannot process a remote video') | ||
45 | process.exit(-1) | ||
46 | } | ||
47 | |||
48 | if (video.isLive) { | ||
49 | console.error('Cannot process live video') | ||
50 | process.exit(-1) | ||
51 | } | ||
52 | |||
53 | ids.push(video.id) | ||
54 | } else { | ||
55 | ids = await listLocalMissingStoryboards() | ||
56 | } | ||
57 | |||
58 | for (const id of ids) { | ||
59 | const videoFull = await VideoModel.load(id) | ||
60 | |||
61 | if (videoFull.isLive) continue | ||
62 | |||
63 | await JobQueue.Instance.createJob({ | ||
64 | type: 'generate-video-storyboard', | ||
65 | payload: { | ||
66 | videoUUID: videoFull.uuid, | ||
67 | federate: true | ||
68 | } | ||
69 | }) | ||
70 | |||
71 | console.log(`Created generate-storyboard job for ${videoFull.name}.`) | ||
72 | } | ||
73 | } | ||
74 | |||
75 | async function listLocalMissingStoryboards () { | ||
76 | const ids = await VideoModel.listLocalIds() | ||
77 | const results: number[] = [] | ||
78 | |||
79 | for (const id of ids) { | ||
80 | const storyboard = await StoryboardModel.loadByVideo(id) | ||
81 | if (!storyboard) results.push(id) | ||
82 | } | ||
83 | |||
84 | return results | ||
85 | } | ||
diff --git a/scripts/create-move-video-storage-job.ts b/scripts/create-move-video-storage-job.ts index c402115f0..8537114eb 100644 --- a/scripts/create-move-video-storage-job.ts +++ b/scripts/create-move-video-storage-job.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { program } from 'commander' | 1 | import { program } from 'commander' |
2 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
2 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
3 | import { initDatabaseModels } from '@server/initializers/database' | 4 | import { initDatabaseModels } from '@server/initializers/database' |
4 | import { JobQueue } from '@server/lib/job-queue' | 5 | import { JobQueue } from '@server/lib/job-queue' |
@@ -32,7 +33,10 @@ if (options['toObjectStorage'] && !CONFIG.OBJECT_STORAGE.ENABLED) { | |||
32 | 33 | ||
33 | run() | 34 | run() |
34 | .then(() => process.exit(0)) | 35 | .then(() => process.exit(0)) |
35 | .catch(err => console.error(err)) | 36 | .catch(err => { |
37 | console.error(err) | ||
38 | process.exit(-1) | ||
39 | }) | ||
36 | 40 | ||
37 | async function run () { | 41 | async function run () { |
38 | await initDatabaseModels(true) | 42 | await initDatabaseModels(true) |
@@ -42,7 +46,7 @@ async function run () { | |||
42 | let ids: number[] = [] | 46 | let ids: number[] = [] |
43 | 47 | ||
44 | if (options['video']) { | 48 | if (options['video']) { |
45 | const video = await VideoModel.load(options['video']) | 49 | const video = await VideoModel.load(toCompleteUUID(options['video'])) |
46 | 50 | ||
47 | if (!video) { | 51 | if (!video) { |
48 | console.error('Unknown video ' + options['video']) | 52 | console.error('Unknown video ' + options['video']) |
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index 72136614c..696a097b1 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts | |||
@@ -69,7 +69,13 @@ const playerKeys = { | |||
69 | '{1} from servers · {2} from peers': '{1} from servers · {2} from peers', | 69 | '{1} from servers · {2} from peers': '{1} from servers · {2} from peers', |
70 | 'Previous video': 'Previous video', | 70 | 'Previous video': 'Previous video', |
71 | 'Video page (new window)': 'Video page (new window)', | 71 | 'Video page (new window)': 'Video page (new window)', |
72 | 'Next video': 'Next video' | 72 | 'Next video': 'Next video', |
73 | 'This video is password protected': 'This video is password protected', | ||
74 | 'You need a password to watch this video.': 'You need a password to watch this video.', | ||
75 | 'Incorrect password, please enter a correct password': 'Incorrect password, please enter a correct password', | ||
76 | 'Cancel': 'Cancel', | ||
77 | 'Up Next': 'Up Next', | ||
78 | 'Autoplay is suspended': 'Autoplay is suspended' | ||
73 | } | 79 | } |
74 | Object.assign(playerKeys, videojs) | 80 | Object.assign(playerKeys, videojs) |
75 | 81 | ||
diff --git a/scripts/migrations/peertube-4.2.ts b/scripts/migrations/peertube-4.2.ts index 513c629ef..d8929692b 100644 --- a/scripts/migrations/peertube-4.2.ts +++ b/scripts/migrations/peertube-4.2.ts | |||
@@ -78,7 +78,7 @@ async function fillAvatarSizeIfNeeded (accountOrChannel: MAccountDefault | MChan | |||
78 | 78 | ||
79 | console.log('Filling size of avatars of %s.', accountOrChannel.name) | 79 | console.log('Filling size of avatars of %s.', accountOrChannel.name) |
80 | 80 | ||
81 | const { width, height } = await getImageSize(join(CONFIG.STORAGE.ACTOR_IMAGES, avatar.filename)) | 81 | const { width, height } = await getImageSize(join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, avatar.filename)) |
82 | avatar.width = width | 82 | avatar.width = width |
83 | avatar.height = height | 83 | avatar.height = height |
84 | 84 | ||
@@ -107,8 +107,8 @@ async function generateSmallerAvatar (actor: MActorDefault) { | |||
107 | const sourceFilename = bigAvatar.filename | 107 | const sourceFilename = bigAvatar.filename |
108 | 108 | ||
109 | const newImageName = buildUUID() + getLowercaseExtension(sourceFilename) | 109 | const newImageName = buildUUID() + getLowercaseExtension(sourceFilename) |
110 | const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename) | 110 | const source = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, sourceFilename) |
111 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName) | 111 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, newImageName) |
112 | 112 | ||
113 | await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true }) | 113 | await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true }) |
114 | 114 | ||
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts index d19594a60..9a73a8600 100755 --- a/scripts/prune-storage.ts +++ b/scripts/prune-storage.ts | |||
@@ -37,8 +37,8 @@ async function run () { | |||
37 | console.log('Detecting files to remove, it could take a while...') | 37 | console.log('Detecting files to remove, it could take a while...') |
38 | 38 | ||
39 | toDelete = toDelete.concat( | 39 | toDelete = toDelete.concat( |
40 | await pruneDirectory(DIRECTORIES.VIDEOS.PUBLIC, doesWebTorrentFileExist()), | 40 | await pruneDirectory(DIRECTORIES.VIDEOS.PUBLIC, doesWebVideoFileExist()), |
41 | await pruneDirectory(DIRECTORIES.VIDEOS.PRIVATE, doesWebTorrentFileExist()), | 41 | await pruneDirectory(DIRECTORIES.VIDEOS.PRIVATE, doesWebVideoFileExist()), |
42 | 42 | ||
43 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, doesHLSPlaylistExist()), | 43 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, doesHLSPlaylistExist()), |
44 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, doesHLSPlaylistExist()), | 44 | await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, doesHLSPlaylistExist()), |
@@ -50,7 +50,7 @@ async function run () { | |||
50 | await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), | 50 | await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), |
51 | await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), | 51 | await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), |
52 | 52 | ||
53 | await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist) | 53 | await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES_DIR, doesActorImageExist) |
54 | ) | 54 | ) |
55 | 55 | ||
56 | const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) | 56 | const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) |
@@ -93,12 +93,12 @@ async function pruneDirectory (directory: string, existFun: ExistFun) { | |||
93 | return toDelete | 93 | return toDelete |
94 | } | 94 | } |
95 | 95 | ||
96 | function doesWebTorrentFileExist () { | 96 | function doesWebVideoFileExist () { |
97 | return (filePath: string) => { | 97 | return (filePath: string) => { |
98 | // Don't delete private directory | 98 | // Don't delete private directory |
99 | if (filePath === DIRECTORIES.VIDEOS.PRIVATE) return true | 99 | if (filePath === DIRECTORIES.VIDEOS.PRIVATE) return true |
100 | 100 | ||
101 | return VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath)) | 101 | return VideoFileModel.doesOwnedWebVideoFileExist(basename(filePath)) |
102 | } | 102 | } |
103 | } | 103 | } |
104 | 104 | ||
diff --git a/scripts/release-embed-api.sh b/scripts/release-embed-api.sh index ae76a65f5..41c84ed38 100755 --- a/scripts/release-embed-api.sh +++ b/scripts/release-embed-api.sh | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | set -eu | 3 | set -eu |
4 | 4 | ||
5 | cd client/src/standalone/player | 5 | cd client/src/standalone/embed-player-api |
6 | 6 | ||
7 | rm -rf dist build && tsc -p . && ../../../node_modules/.bin/webpack --config ./webpack.config.js | 7 | rm -rf dist build && tsc -p . && ../../../node_modules/.bin/webpack --config ./webpack.config.js |
8 | 8 | ||
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 86135fcc9..64c7e1581 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh | |||
@@ -43,10 +43,11 @@ if [ -x "$(command -v pg_dump)" ]; then | |||
43 | DB_USER=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['username'])") | 43 | DB_USER=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['username'])") |
44 | DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])") | 44 | DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])") |
45 | DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])") | 45 | DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])") |
46 | DB_PORT=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['port'])") | ||
46 | DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])") | 47 | DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])") |
47 | DB_NAME=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['name'] || '')") | 48 | DB_NAME=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['name'] || '')") |
48 | 49 | ||
49 | PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -h $DB_HOST -F c "${DB_NAME:-peertube${DB_SUFFIX}}" -f "$SQL_BACKUP_PATH" | 50 | PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -p $DB_PORT -h $DB_HOST -F c "${DB_NAME:-peertube${DB_SUFFIX}}" -f "$SQL_BACKUP_PATH" |
50 | else | 51 | else |
51 | echo "pg_dump not found. Cannot make a SQL backup!" | 52 | echo "pg_dump not found. Cannot make a SQL backup!" |
52 | fi | 53 | fi |
@@ -21,7 +21,7 @@ import { checkMissedConfig, checkFFmpeg, checkNodeVersion } from './server/initi | |||
21 | 21 | ||
22 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) | 22 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) |
23 | import { CONFIG } from './server/initializers/config' | 23 | import { CONFIG } from './server/initializers/config' |
24 | import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants' | 24 | import { API_VERSION, WEBSERVER, loadLanguages } from './server/initializers/constants' |
25 | import { logger } from './server/helpers/logger' | 25 | import { logger } from './server/helpers/logger' |
26 | 26 | ||
27 | const missed = checkMissedConfig() | 27 | const missed = checkMissedConfig() |
@@ -101,7 +101,6 @@ loadLanguages() | |||
101 | import { installApplication } from './server/initializers/installer' | 101 | import { installApplication } from './server/initializers/installer' |
102 | import { Emailer } from './server/lib/emailer' | 102 | import { Emailer } from './server/lib/emailer' |
103 | import { JobQueue } from './server/lib/job-queue' | 103 | import { JobQueue } from './server/lib/job-queue' |
104 | import { VideosPreviewCache, VideosCaptionCache } from './server/lib/files-cache' | ||
105 | import { | 104 | import { |
106 | activityPubRouter, | 105 | activityPubRouter, |
107 | apiRouter, | 106 | apiRouter, |
@@ -143,7 +142,6 @@ import { Hooks } from './server/lib/plugins/hooks' | |||
143 | import { PluginManager } from './server/lib/plugins/plugin-manager' | 142 | import { PluginManager } from './server/lib/plugins/plugin-manager' |
144 | import { LiveManager } from './server/lib/live' | 143 | import { LiveManager } from './server/lib/live' |
145 | import { HttpStatusCode } from './shared/models/http/http-error-codes' | 144 | import { HttpStatusCode } from './shared/models/http/http-error-codes' |
146 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | ||
147 | import { ServerConfigManager } from '@server/lib/server-config-manager' | 145 | import { ServerConfigManager } from '@server/lib/server-config-manager' |
148 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | 146 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' |
149 | import { isTestOrDevInstance } from './server/helpers/core-utils' | 147 | import { isTestOrDevInstance } from './server/helpers/core-utils' |
@@ -312,11 +310,6 @@ async function startApplication () { | |||
312 | ServerConfigManager.Instance.init() | 310 | ServerConfigManager.Instance.init() |
313 | ]) | 311 | ]) |
314 | 312 | ||
315 | // Caches initializations | ||
316 | VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) | ||
317 | VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) | ||
318 | VideosTorrentCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) | ||
319 | |||
320 | // Enable Schedulers | 313 | // Enable Schedulers |
321 | ActorFollowScheduler.Instance.enable() | 314 | ActorFollowScheduler.Instance.enable() |
322 | RemoveOldJobsScheduler.Instance.enable() | 315 | RemoveOldJobsScheduler.Instance.enable() |
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 750e3091c..c47c61f52 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -4,6 +4,7 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect | |||
4 | import { activityPubContextify } from '@server/lib/activitypub/context' | 4 | import { activityPubContextify } from '@server/lib/activitypub/context' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' | 6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' |
7 | import { VideoCommentObject } from '@shared/models' | ||
7 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 8 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
8 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 9 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' | 10 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' |
@@ -33,7 +34,6 @@ import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from ' | |||
33 | import { AccountModel } from '../../models/account/account' | 34 | import { AccountModel } from '../../models/account/account' |
34 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 35 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
35 | import { ActorFollowModel } from '../../models/actor/actor-follow' | 36 | import { ActorFollowModel } from '../../models/actor/actor-follow' |
36 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
37 | import { VideoCommentModel } from '../../models/video/video-comment' | 37 | import { VideoCommentModel } from '../../models/video/video-comment' |
38 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 38 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
39 | import { VideoShareModel } from '../../models/video/video-share' | 39 | import { VideoShareModel } from '../../models/video/video-share' |
@@ -242,14 +242,13 @@ async function videoController (req: express.Request, res: express.Response) { | |||
242 | if (redirectIfNotOwned(video.url, res)) return | 242 | if (redirectIfNotOwned(video.url, res)) return |
243 | 243 | ||
244 | // We need captions to render AP object | 244 | // We need captions to render AP object |
245 | const captions = await VideoCaptionModel.listVideoCaptions(video.id) | 245 | const videoAP = await video.lightAPToFullAP(undefined) |
246 | const videoWithCaptions = Object.assign(video, { VideoCaptions: captions }) | ||
247 | 246 | ||
248 | const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC) | 247 | const audience = getAudience(videoAP.VideoChannel.Account.Actor, videoAP.privacy === VideoPrivacy.PUBLIC) |
249 | const videoObject = audiencify(await videoWithCaptions.toActivityPubObject(), audience) | 248 | const videoObject = audiencify(await videoAP.toActivityPubObject(), audience) |
250 | 249 | ||
251 | if (req.path.endsWith('/activity')) { | 250 | if (req.path.endsWith('/activity')) { |
252 | const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience) | 251 | const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience) |
253 | return activityPubResponse(activityPubContextify(data, 'Video'), res) | 252 | return activityPubResponse(activityPubContextify(data, 'Video'), res) |
254 | } | 253 | } |
255 | 254 | ||
@@ -355,7 +354,7 @@ async function videoCommentController (req: express.Request, res: express.Respon | |||
355 | videoCommentObject = audiencify(videoCommentObject, audience) | 354 | videoCommentObject = audiencify(videoCommentObject, audience) |
356 | 355 | ||
357 | if (req.path.endsWith('/activity')) { | 356 | if (req.path.endsWith('/activity')) { |
358 | const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) | 357 | const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience) |
359 | return activityPubResponse(activityPubContextify(data, 'Comment'), res) | 358 | return activityPubResponse(activityPubContextify(data, 'Comment'), res) |
360 | } | 359 | } |
361 | } | 360 | } |
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 681a5660c..4175cf276 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts | |||
@@ -63,6 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number | |||
63 | 63 | ||
64 | activities.push(announceActivity) | 64 | activities.push(announceActivity) |
65 | } else { | 65 | } else { |
66 | // FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0 | ||
66 | const videoObject = await video.toActivityPubObject() | 67 | const videoObject = await video.toActivityPubObject() |
67 | const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) | 68 | const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) |
68 | 69 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 96f36bf6f..49cd7559a 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -2,7 +2,6 @@ import express from 'express' | |||
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
6 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | 5 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' |
7 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 6 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
8 | import { getFormattedObjects } from '../../helpers/utils' | 7 | import { getFormattedObjects } from '../../helpers/utils' |
@@ -36,6 +35,7 @@ import { | |||
36 | import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists' | 35 | import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists' |
37 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
38 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 37 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
38 | import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' | ||
39 | import { VideoModel } from '../../models/video/video' | 39 | import { VideoModel } from '../../models/video/video' |
40 | import { VideoChannelModel } from '../../models/video/video-channel' | 40 | import { VideoChannelModel } from '../../models/video/video-channel' |
41 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 41 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 228eae109..0980ec10a 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -190,6 +190,9 @@ function customConfig (): CustomConfig { | |||
190 | }, | 190 | }, |
191 | torrents: { | 191 | torrents: { |
192 | size: CONFIG.CACHE.TORRENTS.SIZE | 192 | size: CONFIG.CACHE.TORRENTS.SIZE |
193 | }, | ||
194 | storyboards: { | ||
195 | size: CONFIG.CACHE.STORYBOARDS.SIZE | ||
193 | } | 196 | } |
194 | }, | 197 | }, |
195 | signup: { | 198 | signup: { |
@@ -239,8 +242,8 @@ function customConfig (): CustomConfig { | |||
239 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] | 242 | '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] |
240 | }, | 243 | }, |
241 | alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, | 244 | alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, |
242 | webtorrent: { | 245 | webVideos: { |
243 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 246 | enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
244 | }, | 247 | }, |
245 | hls: { | 248 | hls: { |
246 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 249 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 31f1a56f9..38bd135d0 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | 3 | import { logger } from '@server/helpers/logger' | |
4 | import { HttpStatusCode } from '../../../shared/models' | 4 | import { HttpStatusCode } from '../../../shared/models' |
5 | import { badRequest } from '../../helpers/express-utils' | ||
6 | import { abuseRouter } from './abuse' | 5 | import { abuseRouter } from './abuse' |
7 | import { accountsRouter } from './accounts' | 6 | import { accountsRouter } from './accounts' |
8 | import { blocklistRouter } from './blocklist' | 7 | import { blocklistRouter } from './blocklist' |
@@ -64,3 +63,11 @@ export { apiRouter } | |||
64 | function pong (req: express.Request, res: express.Response) { | 63 | function pong (req: express.Request, res: express.Response) { |
65 | return res.send('pong').status(HttpStatusCode.OK_200).end() | 64 | return res.send('pong').status(HttpStatusCode.OK_200).end() |
66 | } | 65 | } |
66 | |||
67 | function badRequest (req: express.Request, res: express.Response) { | ||
68 | logger.debug(`API express handler not found: bad PeerTube request for ${req.method} - ${req.originalUrl}`) | ||
69 | |||
70 | return res.type('json') | ||
71 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
72 | .end() | ||
73 | } | ||
diff --git a/server/controllers/api/runners/jobs-files.ts b/server/controllers/api/runners/jobs-files.ts index 4e69fb902..cb4eff570 100644 --- a/server/controllers/api/runners/jobs-files.ts +++ b/server/controllers/api/runners/jobs-files.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
3 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' | 3 | import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' |
4 | import { VideoPathManager } from '@server/lib/video-path-manager' | 4 | import { VideoPathManager } from '@server/lib/video-path-manager' |
5 | import { getStudioTaskFilePath } from '@server/lib/video-studio' | 5 | import { getStudioTaskFilePath } from '@server/lib/video-studio' |
6 | import { apiRateLimiter, asyncMiddleware } from '@server/middlewares' | 6 | import { apiRateLimiter, asyncMiddleware } from '@server/middlewares' |
@@ -70,7 +70,7 @@ async function getMaxQualityVideoFile (req: express.Request, res: express.Respon | |||
70 | } | 70 | } |
71 | 71 | ||
72 | // Web video | 72 | // Web video |
73 | return proxifyWebTorrentFile({ | 73 | return proxifyWebVideoFile({ |
74 | req, | 74 | req, |
75 | res, | 75 | res, |
76 | filename: file.filename | 76 | filename: file.filename |
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts index 1d7a7b7bc..034a63ace 100644 --- a/server/controllers/api/search/search-videos.ts +++ b/server/controllers/api/search/search-videos.ts | |||
@@ -8,7 +8,6 @@ import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | |||
8 | import { Hooks } from '@server/lib/plugins/hooks' | 8 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | 9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' |
10 | import { getServerActor } from '@server/models/application/application' | 10 | import { getServerActor } from '@server/models/application/application' |
11 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
12 | import { HttpStatusCode, ResultList, Video } from '@shared/models' | 11 | import { HttpStatusCode, ResultList, Video } from '@shared/models' |
13 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' | 12 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' |
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | 13 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' |
@@ -25,6 +24,7 @@ import { | |||
25 | videosSearchSortValidator, | 24 | videosSearchSortValidator, |
26 | videosSearchValidator | 25 | videosSearchValidator |
27 | } from '../../../middlewares' | 26 | } from '../../../middlewares' |
27 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
28 | import { VideoModel } from '../../../models/video/video' | 28 | import { VideoModel } from '../../../models/video/video' |
29 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 29 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
30 | import { searchLocalUrl } from './shared' | 30 | import { searchLocalUrl } from './shared' |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 218091d91..4753308e8 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -213,19 +213,14 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
213 | 'noInstanceConfigWarningModal', | 213 | 'noInstanceConfigWarningModal', |
214 | 'noAccountSetupWarningModal', | 214 | 'noAccountSetupWarningModal', |
215 | 'noWelcomeModal', | 215 | 'noWelcomeModal', |
216 | 'emailPublic' | 216 | 'emailPublic', |
217 | 'p2pEnabled' | ||
217 | ] | 218 | ] |
218 | 219 | ||
219 | for (const key of keysToUpdate) { | 220 | for (const key of keysToUpdate) { |
220 | if (body[key] !== undefined) user.set(key, body[key]) | 221 | if (body[key] !== undefined) user.set(key, body[key]) |
221 | } | 222 | } |
222 | 223 | ||
223 | if (body.p2pEnabled !== undefined) { | ||
224 | user.set('p2pEnabled', body.p2pEnabled) | ||
225 | } else if (body.webTorrentEnabled !== undefined) { // FIXME: deprecated in 4.1 | ||
226 | user.set('p2pEnabled', body.webTorrentEnabled) | ||
227 | } | ||
228 | |||
229 | if (body.email !== undefined) { | 224 | if (body.email !== undefined) { |
230 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | 225 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { |
231 | user.pendingEmail = body.email | 226 | user.pendingEmail = body.email |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index 6e2aa3711..c4360f59d 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -3,7 +3,7 @@ import express from 'express' | |||
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | 3 | import { handlesToNameAndHost } from '@server/helpers/actors' |
4 | import { pickCommonVideoQuery } from '@server/helpers/query' | 4 | import { pickCommonVideoQuery } from '@server/helpers/query' |
5 | import { sendUndoFollow } from '@server/lib/activitypub/send' | 5 | import { sendUndoFollow } from '@server/lib/activitypub/send' |
6 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | 6 | import { Hooks } from '@server/lib/plugins/hooks' |
7 | import { VideoChannelModel } from '@server/models/video/video-channel' | 7 | import { VideoChannelModel } from '@server/models/video/video-channel' |
8 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 8 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
9 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 9 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
@@ -29,8 +29,8 @@ import { | |||
29 | videosSortValidator | 29 | videosSortValidator |
30 | } from '../../../middlewares/validators' | 30 | } from '../../../middlewares/validators' |
31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | 31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
32 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
32 | import { VideoModel } from '../../../models/video/video' | 33 | import { VideoModel } from '../../../models/video/video' |
33 | import { Hooks } from '@server/lib/plugins/hooks' | ||
34 | 34 | ||
35 | const mySubscriptionsRouter = express.Router() | 35 | const mySubscriptionsRouter = express.Router() |
36 | 36 | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index cdafa31dc..3d7ef31ee 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -4,7 +4,6 @@ import { getBiggestActorImage } from '@server/lib/actor-image' | |||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
6 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
7 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
8 | import { MChannelBannerAccountDefault } from '@server/types/models' | 7 | import { MChannelBannerAccountDefault } from '@server/types/models' |
9 | import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' | 8 | import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' |
10 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 9 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
@@ -48,6 +47,7 @@ import { | |||
48 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | 47 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
49 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 48 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
50 | import { AccountModel } from '../../models/account/account' | 49 | import { AccountModel } from '../../models/account/account' |
50 | import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' | ||
51 | import { VideoModel } from '../../models/video/video' | 51 | import { VideoModel } from '../../models/video/video' |
52 | import { VideoChannelModel } from '../../models/video/video-channel' | 52 | import { VideoChannelModel } from '../../models/video/video-channel' |
53 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 53 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index fe00034ed..73362e1e3 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { join } from 'path' | ||
3 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' | 2 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' |
3 | import { VideoMiniaturePermanentFileCache } from '@server/lib/files-cache' | ||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' | 6 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' |
@@ -18,12 +18,11 @@ import { resetSequelizeInstance } from '../../helpers/database-utils' | |||
18 | import { createReqFiles } from '../../helpers/express-utils' | 18 | import { createReqFiles } from '../../helpers/express-utils' |
19 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
20 | import { getFormattedObjects } from '../../helpers/utils' | 20 | import { getFormattedObjects } from '../../helpers/utils' |
21 | import { CONFIG } from '../../initializers/config' | ||
22 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' | 21 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' |
23 | import { sequelizeTypescript } from '../../initializers/database' | 22 | import { sequelizeTypescript } from '../../initializers/database' |
24 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 23 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
25 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 24 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
26 | import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' | 25 | import { updateLocalPlaylistMiniatureFromExisting } from '../../lib/thumbnail' |
27 | import { | 26 | import { |
28 | apiRateLimiter, | 27 | apiRateLimiter, |
29 | asyncMiddleware, | 28 | asyncMiddleware, |
@@ -178,7 +177,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
178 | 177 | ||
179 | const thumbnailField = req.files['thumbnailfile'] | 178 | const thumbnailField = req.files['thumbnailfile'] |
180 | const thumbnailModel = thumbnailField | 179 | const thumbnailModel = thumbnailField |
181 | ? await updatePlaylistMiniatureFromExisting({ | 180 | ? await updateLocalPlaylistMiniatureFromExisting({ |
182 | inputPath: thumbnailField[0].path, | 181 | inputPath: thumbnailField[0].path, |
183 | playlist: videoPlaylist, | 182 | playlist: videoPlaylist, |
184 | automaticallyGenerated: false | 183 | automaticallyGenerated: false |
@@ -220,7 +219,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
220 | 219 | ||
221 | const thumbnailField = req.files['thumbnailfile'] | 220 | const thumbnailField = req.files['thumbnailfile'] |
222 | const thumbnailModel = thumbnailField | 221 | const thumbnailModel = thumbnailField |
223 | ? await updatePlaylistMiniatureFromExisting({ | 222 | ? await updateLocalPlaylistMiniatureFromExisting({ |
224 | inputPath: thumbnailField[0].path, | 223 | inputPath: thumbnailField[0].path, |
225 | playlist: videoPlaylistInstance, | 224 | playlist: videoPlaylistInstance, |
226 | automaticallyGenerated: false | 225 | automaticallyGenerated: false |
@@ -496,8 +495,13 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn | |||
496 | return | 495 | return |
497 | } | 496 | } |
498 | 497 | ||
499 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) | 498 | // Ensure the file is on disk |
500 | const thumbnailModel = await updatePlaylistMiniatureFromExisting({ | 499 | const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() |
500 | const inputPath = videoMiniature.isOwned() | ||
501 | ? videoMiniature.getPath() | ||
502 | : await videoMiniaturePermanentFileCache.downloadRemoteFile(videoMiniature) | ||
503 | |||
504 | const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({ | ||
501 | inputPath, | 505 | inputPath, |
502 | playlist: videoPlaylist, | 506 | playlist: videoPlaylist, |
503 | automaticallyGenerated: true, | 507 | automaticallyGenerated: true, |
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts index 6d9c0b843..67b60ff63 100644 --- a/server/controllers/api/videos/files.ts +++ b/server/controllers/api/videos/files.ts | |||
@@ -2,7 +2,8 @@ import express from 'express' | |||
2 | import toInt from 'validator/lib/toInt' | 2 | import toInt from 'validator/lib/toInt' |
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
5 | import { removeAllWebTorrentFiles, removeHLSFile, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file' | 5 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' |
6 | import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file' | ||
6 | import { VideoFileModel } from '@server/models/video/video-file' | 7 | import { VideoFileModel } from '@server/models/video/video-file' |
7 | import { HttpStatusCode, UserRight } from '@shared/models' | 8 | import { HttpStatusCode, UserRight } from '@shared/models' |
8 | import { | 9 | import { |
@@ -12,11 +13,10 @@ import { | |||
12 | videoFileMetadataGetValidator, | 13 | videoFileMetadataGetValidator, |
13 | videoFilesDeleteHLSFileValidator, | 14 | videoFilesDeleteHLSFileValidator, |
14 | videoFilesDeleteHLSValidator, | 15 | videoFilesDeleteHLSValidator, |
15 | videoFilesDeleteWebTorrentFileValidator, | 16 | videoFilesDeleteWebVideoFileValidator, |
16 | videoFilesDeleteWebTorrentValidator, | 17 | videoFilesDeleteWebVideoValidator, |
17 | videosGetValidator | 18 | videosGetValidator |
18 | } from '../../../middlewares' | 19 | } from '../../../middlewares' |
19 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' | ||
20 | 20 | ||
21 | const lTags = loggerTagsFactory('api', 'video') | 21 | const lTags = loggerTagsFactory('api', 'video') |
22 | const filesRouter = express.Router() | 22 | const filesRouter = express.Router() |
@@ -40,17 +40,19 @@ filesRouter.delete('/:id/hls/:videoFileId', | |||
40 | asyncMiddleware(removeHLSFileController) | 40 | asyncMiddleware(removeHLSFileController) |
41 | ) | 41 | ) |
42 | 42 | ||
43 | filesRouter.delete('/:id/webtorrent', | 43 | filesRouter.delete( |
44 | [ '/:id/webtorrent', '/:id/web-videos' ], // TODO: remove webtorrent in V7 | ||
44 | authenticate, | 45 | authenticate, |
45 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 46 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
46 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), | 47 | asyncMiddleware(videoFilesDeleteWebVideoValidator), |
47 | asyncMiddleware(removeAllWebTorrentFilesController) | 48 | asyncMiddleware(removeAllWebVideoFilesController) |
48 | ) | 49 | ) |
49 | filesRouter.delete('/:id/webtorrent/:videoFileId', | 50 | filesRouter.delete( |
51 | [ '/:id/webtorrent/:videoFileId', '/:id/web-videos/:videoFileId' ], // TODO: remove webtorrent in V7 | ||
50 | authenticate, | 52 | authenticate, |
51 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | 53 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), |
52 | asyncMiddleware(videoFilesDeleteWebTorrentFileValidator), | 54 | asyncMiddleware(videoFilesDeleteWebVideoFileValidator), |
53 | asyncMiddleware(removeWebTorrentFileController) | 55 | asyncMiddleware(removeWebVideoFileController) |
54 | ) | 56 | ) |
55 | 57 | ||
56 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
@@ -96,24 +98,24 @@ async function removeHLSFileController (req: express.Request, res: express.Respo | |||
96 | 98 | ||
97 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
98 | 100 | ||
99 | async function removeAllWebTorrentFilesController (req: express.Request, res: express.Response) { | 101 | async function removeAllWebVideoFilesController (req: express.Request, res: express.Response) { |
100 | const video = res.locals.videoAll | 102 | const video = res.locals.videoAll |
101 | 103 | ||
102 | logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid)) | 104 | logger.info('Deleting Web Video files of %s.', video.url, lTags(video.uuid)) |
103 | 105 | ||
104 | await removeAllWebTorrentFiles(video) | 106 | await removeAllWebVideoFiles(video) |
105 | await federateVideoIfNeeded(video, false, undefined) | 107 | await federateVideoIfNeeded(video, false, undefined) |
106 | 108 | ||
107 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 109 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
108 | } | 110 | } |
109 | 111 | ||
110 | async function removeWebTorrentFileController (req: express.Request, res: express.Response) { | 112 | async function removeWebVideoFileController (req: express.Request, res: express.Response) { |
111 | const video = res.locals.videoAll | 113 | const video = res.locals.videoAll |
112 | 114 | ||
113 | const videoFileId = +req.params.videoFileId | 115 | const videoFileId = +req.params.videoFileId |
114 | logger.info('Deleting WebTorrent file %d of %s.', videoFileId, video.url, lTags(video.uuid)) | 116 | logger.info('Deleting Web Video file %d of %s.', videoFileId, video.url, lTags(video.uuid)) |
115 | 117 | ||
116 | await removeWebTorrentFile(video, videoFileId) | 118 | await removeWebVideoFile(video, videoFileId) |
117 | await federateVideoIfNeeded(video, false, undefined) | 119 | await federateVideoIfNeeded(video, false, undefined) |
118 | 120 | ||
119 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 121 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 6a50aaf4e..defe9efd4 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -14,7 +14,7 @@ import { getSecureTorrentName } from '../../../helpers/utils' | |||
14 | import { CONFIG } from '../../../initializers/config' | 14 | import { CONFIG } from '../../../initializers/config' |
15 | import { MIMETYPES } from '../../../initializers/constants' | 15 | import { MIMETYPES } from '../../../initializers/constants' |
16 | import { JobQueue } from '../../../lib/job-queue/job-queue' | 16 | import { JobQueue } from '../../../lib/job-queue/job-queue' |
17 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 17 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
18 | import { | 18 | import { |
19 | asyncMiddleware, | 19 | asyncMiddleware, |
20 | asyncRetryTransactionMiddleware, | 20 | asyncRetryTransactionMiddleware, |
@@ -120,6 +120,7 @@ async function handleTorrentImport (req: express.Request, res: express.Response, | |||
120 | videoChannel: res.locals.videoChannel, | 120 | videoChannel: res.locals.videoChannel, |
121 | tags: body.tags || undefined, | 121 | tags: body.tags || undefined, |
122 | user, | 122 | user, |
123 | videoPasswords: body.videoPasswords, | ||
123 | videoImportAttributes: { | 124 | videoImportAttributes: { |
124 | magnetUri, | 125 | magnetUri, |
125 | torrentName, | 126 | torrentName, |
@@ -192,7 +193,7 @@ async function processThumbnail (req: express.Request, video: MVideoThumbnail) { | |||
192 | if (thumbnailField) { | 193 | if (thumbnailField) { |
193 | const thumbnailPhysicalFile = thumbnailField[0] | 194 | const thumbnailPhysicalFile = thumbnailField[0] |
194 | 195 | ||
195 | return updateVideoMiniatureFromExisting({ | 196 | return updateLocalVideoMiniatureFromExisting({ |
196 | inputPath: thumbnailPhysicalFile.path, | 197 | inputPath: thumbnailPhysicalFile.path, |
197 | video, | 198 | video, |
198 | type: ThumbnailType.MINIATURE, | 199 | type: ThumbnailType.MINIATURE, |
@@ -208,7 +209,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr | |||
208 | if (previewField) { | 209 | if (previewField) { |
209 | const previewPhysicalFile = previewField[0] | 210 | const previewPhysicalFile = previewField[0] |
210 | 211 | ||
211 | return updateVideoMiniatureFromExisting({ | 212 | return updateLocalVideoMiniatureFromExisting({ |
212 | inputPath: previewPhysicalFile.path, | 213 | inputPath: previewPhysicalFile.path, |
213 | video, | 214 | video, |
214 | type: ThumbnailType.PREVIEW, | 215 | type: ThumbnailType.PREVIEW, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index a34325e79..520d8cbbb 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -3,7 +3,6 @@ import { pickCommonVideoQuery } from '@server/helpers/query' | |||
3 | import { doJSONRequest } from '@server/helpers/requests' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { openapiOperationDoc } from '@server/middlewares/doc' | 4 | import { openapiOperationDoc } from '@server/middlewares/doc' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
7 | import { MVideoAccountLight } from '@server/types/models' | 6 | import { MVideoAccountLight } from '@server/types/models' |
8 | import { HttpStatusCode } from '../../../../shared/models' | 7 | import { HttpStatusCode } from '../../../../shared/models' |
9 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 8 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
@@ -31,6 +30,7 @@ import { | |||
31 | videosRemoveValidator, | 30 | videosRemoveValidator, |
32 | videosSortValidator | 31 | videosSortValidator |
33 | } from '../../../middlewares' | 32 | } from '../../../middlewares' |
33 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
34 | import { VideoModel } from '../../../models/video/video' | 34 | import { VideoModel } from '../../../models/video/video' |
35 | import { blacklistRouter } from './blacklist' | 35 | import { blacklistRouter } from './blacklist' |
36 | import { videoCaptionsRouter } from './captions' | 36 | import { videoCaptionsRouter } from './captions' |
@@ -41,12 +41,14 @@ import { liveRouter } from './live' | |||
41 | import { ownershipVideoRouter } from './ownership' | 41 | import { ownershipVideoRouter } from './ownership' |
42 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { statsRouter } from './stats' | 43 | import { statsRouter } from './stats' |
44 | import { storyboardRouter } from './storyboard' | ||
44 | import { studioRouter } from './studio' | 45 | import { studioRouter } from './studio' |
45 | import { tokenRouter } from './token' | 46 | import { tokenRouter } from './token' |
46 | import { transcodingRouter } from './transcoding' | 47 | import { transcodingRouter } from './transcoding' |
47 | import { updateRouter } from './update' | 48 | import { updateRouter } from './update' |
48 | import { uploadRouter } from './upload' | 49 | import { uploadRouter } from './upload' |
49 | import { viewRouter } from './view' | 50 | import { viewRouter } from './view' |
51 | import { videoPasswordRouter } from './passwords' | ||
50 | 52 | ||
51 | const auditLogger = auditLoggerFactory('videos') | 53 | const auditLogger = auditLoggerFactory('videos') |
52 | const videosRouter = express.Router() | 54 | const videosRouter = express.Router() |
@@ -68,6 +70,8 @@ videosRouter.use('/', updateRouter) | |||
68 | videosRouter.use('/', filesRouter) | 70 | videosRouter.use('/', filesRouter) |
69 | videosRouter.use('/', transcodingRouter) | 71 | videosRouter.use('/', transcodingRouter) |
70 | videosRouter.use('/', tokenRouter) | 72 | videosRouter.use('/', tokenRouter) |
73 | videosRouter.use('/', videoPasswordRouter) | ||
74 | videosRouter.use('/', storyboardRouter) | ||
71 | 75 | ||
72 | videosRouter.get('/categories', | 76 | videosRouter.get('/categories', |
73 | openapiOperationDoc({ operationId: 'getCategories' }), | 77 | openapiOperationDoc({ operationId: 'getCategories' }), |
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index de047d4ec..e19e8c652 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts | |||
@@ -18,13 +18,14 @@ import { VideoLiveModel } from '@server/models/video/video-live' | |||
18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | 18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' |
19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' | 19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' |
20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models' | 21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { sequelizeTypescript } from '../../../initializers/database' | 23 | import { sequelizeTypescript } from '../../../initializers/database' |
24 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 24 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' | 25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' |
26 | import { VideoModel } from '../../../models/video/video' | 26 | import { VideoModel } from '../../../models/video/video' |
27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | 27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' |
28 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
28 | 29 | ||
29 | const liveRouter = express.Router() | 30 | const liveRouter = express.Router() |
30 | 31 | ||
@@ -165,7 +166,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
165 | video, | 166 | video, |
166 | files: req.files, | 167 | files: req.files, |
167 | fallback: type => { | 168 | fallback: type => { |
168 | return updateVideoMiniatureFromExisting({ | 169 | return updateLocalVideoMiniatureFromExisting({ |
169 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, | 170 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, |
170 | video, | 171 | video, |
171 | type, | 172 | type, |
@@ -202,6 +203,10 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
202 | 203 | ||
203 | await federateVideoIfNeeded(videoCreated, true, t) | 204 | await federateVideoIfNeeded(videoCreated, true, t) |
204 | 205 | ||
206 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
207 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
208 | } | ||
209 | |||
205 | logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) | 210 | logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) |
206 | 211 | ||
207 | return { videoCreated } | 212 | return { videoCreated } |
diff --git a/server/controllers/api/videos/passwords.ts b/server/controllers/api/videos/passwords.ts new file mode 100644 index 000000000..d11cf5bcc --- /dev/null +++ b/server/controllers/api/videos/passwords.ts | |||
@@ -0,0 +1,105 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { getFormattedObjects } from '../../../helpers/utils' | ||
5 | import { | ||
6 | asyncMiddleware, | ||
7 | asyncRetryTransactionMiddleware, | ||
8 | authenticate, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort | ||
11 | } from '../../../middlewares' | ||
12 | import { | ||
13 | listVideoPasswordValidator, | ||
14 | paginationValidator, | ||
15 | removeVideoPasswordValidator, | ||
16 | updateVideoPasswordListValidator, | ||
17 | videoPasswordsSortValidator | ||
18 | } from '../../../middlewares/validators' | ||
19 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
20 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
21 | import { Transaction } from 'sequelize' | ||
22 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
23 | |||
24 | const lTags = loggerTagsFactory('api', 'video') | ||
25 | const videoPasswordRouter = express.Router() | ||
26 | |||
27 | videoPasswordRouter.get('/:videoId/passwords', | ||
28 | authenticate, | ||
29 | paginationValidator, | ||
30 | videoPasswordsSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | asyncMiddleware(listVideoPasswordValidator), | ||
34 | asyncMiddleware(listVideoPasswords) | ||
35 | ) | ||
36 | |||
37 | videoPasswordRouter.put('/:videoId/passwords', | ||
38 | authenticate, | ||
39 | asyncMiddleware(updateVideoPasswordListValidator), | ||
40 | asyncMiddleware(updateVideoPasswordList) | ||
41 | ) | ||
42 | |||
43 | videoPasswordRouter.delete('/:videoId/passwords/:passwordId', | ||
44 | authenticate, | ||
45 | asyncMiddleware(removeVideoPasswordValidator), | ||
46 | asyncRetryTransactionMiddleware(removeVideoPassword) | ||
47 | ) | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | videoPasswordRouter | ||
53 | } | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | async function listVideoPasswords (req: express.Request, res: express.Response) { | ||
58 | const options = { | ||
59 | videoId: res.locals.videoAll.id, | ||
60 | start: req.query.start, | ||
61 | count: req.query.count, | ||
62 | sort: req.query.sort | ||
63 | } | ||
64 | |||
65 | const resultList = await VideoPasswordModel.listPasswords(options) | ||
66 | |||
67 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
68 | } | ||
69 | |||
70 | async function updateVideoPasswordList (req: express.Request, res: express.Response) { | ||
71 | const videoInstance = getVideoWithAttributes(res) | ||
72 | const videoId = videoInstance.id | ||
73 | |||
74 | const passwordArray = req.body.passwords as string[] | ||
75 | |||
76 | await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => { | ||
77 | await VideoPasswordModel.deleteAllPasswords(videoId, t) | ||
78 | await VideoPasswordModel.addPasswords(passwordArray, videoId, t) | ||
79 | }) | ||
80 | |||
81 | logger.info( | ||
82 | `Video passwords for video with name %s and uuid %s have been updated`, | ||
83 | videoInstance.name, | ||
84 | videoInstance.uuid, | ||
85 | lTags(videoInstance.uuid) | ||
86 | ) | ||
87 | |||
88 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
89 | } | ||
90 | |||
91 | async function removeVideoPassword (req: express.Request, res: express.Response) { | ||
92 | const videoInstance = getVideoWithAttributes(res) | ||
93 | const password = res.locals.videoPassword | ||
94 | |||
95 | await VideoPasswordModel.deletePassword(password.id) | ||
96 | logger.info( | ||
97 | 'Password with id %d of video named %s and uuid %s has been deleted.', | ||
98 | password.id, | ||
99 | videoInstance.name, | ||
100 | videoInstance.uuid, | ||
101 | lTags(videoInstance.uuid) | ||
102 | ) | ||
103 | |||
104 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
105 | } | ||
diff --git a/server/controllers/api/videos/storyboard.ts b/server/controllers/api/videos/storyboard.ts new file mode 100644 index 000000000..47a22011d --- /dev/null +++ b/server/controllers/api/videos/storyboard.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import express from 'express' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
3 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
4 | import { asyncMiddleware, videosGetValidator } from '../../../middlewares' | ||
5 | |||
6 | const storyboardRouter = express.Router() | ||
7 | |||
8 | storyboardRouter.get('/:id/storyboards', | ||
9 | asyncMiddleware(videosGetValidator), | ||
10 | asyncMiddleware(listStoryboards) | ||
11 | ) | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | storyboardRouter | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | async function listStoryboards (req: express.Request, res: express.Response) { | ||
22 | const video = getVideoWithAttributes(res) | ||
23 | |||
24 | const storyboards = await StoryboardModel.listStoryboardsOf(video) | ||
25 | |||
26 | return res.json({ | ||
27 | storyboards: storyboards.map(s => s.toFormattedJSON()) | ||
28 | }) | ||
29 | } | ||
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts index 22387c3e8..e961ffd9e 100644 --- a/server/controllers/api/videos/token.ts +++ b/server/controllers/api/videos/token.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | 2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' |
3 | import { VideoToken } from '@shared/models' | 3 | import { VideoPrivacy, VideoToken } from '@shared/models' |
4 | import { asyncMiddleware, authenticate, videosCustomGetValidator } from '../../../middlewares' | 4 | import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares' |
5 | 5 | ||
6 | const tokenRouter = express.Router() | 6 | const tokenRouter = express.Router() |
7 | 7 | ||
8 | tokenRouter.post('/:id/token', | 8 | tokenRouter.post('/:id/token', |
9 | authenticate, | 9 | optionalAuthenticate, |
10 | asyncMiddleware(videosCustomGetValidator('only-video')), | 10 | asyncMiddleware(videosCustomGetValidator('only-video')), |
11 | videoFileTokenValidator, | ||
11 | generateToken | 12 | generateToken |
12 | ) | 13 | ) |
13 | 14 | ||
@@ -22,12 +23,11 @@ export { | |||
22 | function generateToken (req: express.Request, res: express.Response) { | 23 | function generateToken (req: express.Request, res: express.Response) { |
23 | const video = res.locals.onlyVideo | 24 | const video = res.locals.onlyVideo |
24 | 25 | ||
25 | const { token, expires } = VideoTokensManager.Instance.create({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) | 26 | const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED |
27 | ? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid }) | ||
28 | : VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) | ||
26 | 29 | ||
27 | return res.json({ | 30 | return res.json({ |
28 | files: { | 31 | files |
29 | token, | ||
30 | expires | ||
31 | } | ||
32 | } as VideoToken) | 32 | } as VideoToken) |
33 | } | 33 | } |
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts index ddab428d4..28ec2cf37 100644 --- a/server/controllers/api/videos/update.ts +++ b/server/controllers/api/videos/update.ts | |||
@@ -2,13 +2,12 @@ import express from 'express' | |||
2 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | 3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' |
4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
5 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
6 | import { setVideoPrivacy } from '@server/lib/video-privacy' | 5 | import { setVideoPrivacy } from '@server/lib/video-privacy' |
7 | import { openapiOperationDoc } from '@server/middlewares/doc' | 6 | import { openapiOperationDoc } from '@server/middlewares/doc' |
8 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
9 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
10 | import { forceNumber } from '@shared/core-utils' | 9 | import { forceNumber } from '@shared/core-utils' |
11 | import { HttpStatusCode, VideoUpdate } from '@shared/models' | 10 | import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models' |
12 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
13 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 12 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
14 | import { createReqFiles } from '../../../helpers/express-utils' | 13 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -20,6 +19,9 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | |||
20 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | 19 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' |
21 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 20 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
22 | import { VideoModel } from '../../../models/video/video' | 21 | import { VideoModel } from '../../../models/video/video' |
22 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
23 | 25 | ||
24 | const lTags = loggerTagsFactory('api', 'video') | 26 | const lTags = loggerTagsFactory('api', 'video') |
25 | const auditLogger = auditLoggerFactory('videos') | 27 | const auditLogger = auditLoggerFactory('videos') |
@@ -176,6 +178,16 @@ async function updateVideoPrivacy (options: { | |||
176 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) | 178 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) |
177 | setVideoPrivacy(videoInstance, newPrivacy) | 179 | setVideoPrivacy(videoInstance, newPrivacy) |
178 | 180 | ||
181 | // Delete passwords if video is not anymore password protected | ||
182 | if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
183 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
184 | } | ||
185 | |||
186 | if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) { | ||
187 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
188 | await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction) | ||
189 | } | ||
190 | |||
179 | // Unfederate the video if the new privacy is not compatible with federation | 191 | // Unfederate the video if the new privacy is not compatible with federation |
180 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | 192 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { |
181 | await VideoModel.sendDelete(videoInstance, { transaction }) | 193 | await VideoModel.sendDelete(videoInstance, { transaction }) |
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 885ac8b81..27fef0b1a 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -14,14 +14,14 @@ import { openapiOperationDoc } from '@server/middlewares/doc' | |||
14 | import { VideoSourceModel } from '@server/models/video/video-source' | 14 | import { VideoSourceModel } from '@server/models/video/video-source' |
15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' | 15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' |
16 | import { uuidToShort } from '@shared/extra-utils' | 16 | import { uuidToShort } from '@shared/extra-utils' |
17 | import { HttpStatusCode, VideoCreate, VideoState } from '@shared/models' | 17 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
19 | import { createReqFiles } from '../../../helpers/express-utils' | 19 | import { createReqFiles } from '../../../helpers/express-utils' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { MIMETYPES } from '../../../initializers/constants' | 21 | import { MIMETYPES } from '../../../initializers/constants' |
22 | import { sequelizeTypescript } from '../../../initializers/database' | 22 | import { sequelizeTypescript } from '../../../initializers/database' |
23 | import { Hooks } from '../../../lib/plugins/hooks' | 23 | import { Hooks } from '../../../lib/plugins/hooks' |
24 | import { generateVideoMiniature } from '../../../lib/thumbnail' | 24 | import { generateLocalVideoMiniature } from '../../../lib/thumbnail' |
25 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 25 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
26 | import { | 26 | import { |
27 | asyncMiddleware, | 27 | asyncMiddleware, |
@@ -33,6 +33,7 @@ import { | |||
33 | } from '../../../middlewares' | 33 | } from '../../../middlewares' |
34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
35 | import { VideoModel } from '../../../models/video/video' | 35 | import { VideoModel } from '../../../models/video/video' |
36 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
36 | 37 | ||
37 | const lTags = loggerTagsFactory('api', 'video') | 38 | const lTags = loggerTagsFactory('api', 'video') |
38 | const auditLogger = auditLoggerFactory('videos') | 39 | const auditLogger = auditLoggerFactory('videos') |
@@ -62,13 +63,13 @@ uploadRouter.post('/upload-resumable', | |||
62 | authenticate, | 63 | authenticate, |
63 | reqVideoFileAddResumable, | 64 | reqVideoFileAddResumable, |
64 | asyncMiddleware(videosAddResumableInitValidator), | 65 | asyncMiddleware(videosAddResumableInitValidator), |
65 | uploadx.upload | 66 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end |
66 | ) | 67 | ) |
67 | 68 | ||
68 | uploadRouter.delete('/upload-resumable', | 69 | uploadRouter.delete('/upload-resumable', |
69 | authenticate, | 70 | authenticate, |
70 | asyncMiddleware(deleteUploadResumableCache), | 71 | asyncMiddleware(deleteUploadResumableCache), |
71 | uploadx.upload | 72 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end |
72 | ) | 73 | ) |
73 | 74 | ||
74 | uploadRouter.put('/upload-resumable', | 75 | uploadRouter.put('/upload-resumable', |
@@ -110,7 +111,7 @@ async function addVideoLegacy (req: express.Request, res: express.Response) { | |||
110 | async function addVideoResumable (req: express.Request, res: express.Response) { | 111 | async function addVideoResumable (req: express.Request, res: express.Response) { |
111 | const videoPhysicalFile = res.locals.videoFileResumable | 112 | const videoPhysicalFile = res.locals.videoFileResumable |
112 | const videoInfo = videoPhysicalFile.metadata | 113 | const videoInfo = videoPhysicalFile.metadata |
113 | const files = { previewfile: videoInfo.previewfile } | 114 | const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } |
114 | 115 | ||
115 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) | 116 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) |
116 | await Redis.Instance.setUploadSession(req.query.upload_id, response) | 117 | await Redis.Instance.setUploadSession(req.query.upload_id, response) |
@@ -152,7 +153,7 @@ async function addVideo (options: { | |||
152 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 153 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
153 | video, | 154 | video, |
154 | files, | 155 | files, |
155 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | 156 | fallback: type => generateLocalVideoMiniature({ video, videoFile, type }) |
156 | }) | 157 | }) |
157 | 158 | ||
158 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | 159 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { |
@@ -195,6 +196,10 @@ async function addVideo (options: { | |||
195 | transaction: t | 196 | transaction: t |
196 | }) | 197 | }) |
197 | 198 | ||
199 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
200 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
201 | } | ||
202 | |||
198 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | 203 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) |
199 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | 204 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) |
200 | 205 | ||
@@ -230,6 +235,15 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide | |||
230 | }, | 235 | }, |
231 | 236 | ||
232 | { | 237 | { |
238 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
239 | payload: { | ||
240 | videoUUID: video.uuid, | ||
241 | // No need to federate, we process these jobs sequentially | ||
242 | federate: false | ||
243 | } | ||
244 | }, | ||
245 | |||
246 | { | ||
233 | type: 'notify', | 247 | type: 'notify', |
234 | payload: { | 248 | payload: { |
235 | action: 'new-video', | 249 | action: 'new-video', |
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index d675a2d6c..4b94e34bd 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 4 | import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache' |
5 | import { generateHLSFilePresignedUrl, generateWebVideoPresignedUrl } from '@server/lib/object-storage' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | 6 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { VideoPathManager } from '@server/lib/video-path-manager' | 7 | import { VideoPathManager } from '@server/lib/video-path-manager' |
7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 8 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
8 | import { addQueryParams, forceNumber } from '@shared/core-utils' | 9 | import { forceNumber } from '@shared/core-utils' |
9 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' | 10 | import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' |
10 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 11 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
11 | import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' | 12 | import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' |
@@ -42,7 +43,7 @@ export { | |||
42 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
43 | 44 | ||
44 | async function downloadTorrent (req: express.Request, res: express.Response) { | 45 | async function downloadTorrent (req: express.Request, res: express.Response) { |
45 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 46 | const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) |
46 | if (!result) { | 47 | if (!result) { |
47 | return res.fail({ | 48 | return res.fail({ |
48 | status: HttpStatusCode.NOT_FOUND_404, | 49 | status: HttpStatusCode.NOT_FOUND_404, |
@@ -94,16 +95,16 @@ async function downloadVideoFile (req: express.Request, res: express.Response) { | |||
94 | 95 | ||
95 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | 96 | if (!checkAllowResult(res, allowParameters, allowedResult)) return |
96 | 97 | ||
98 | // Express uses basename on filename parameter | ||
99 | const videoName = video.name.replace(/[/\\]/g, '_') | ||
100 | const downloadFilename = `${videoName}-${videoFile.resolution}p${videoFile.extname}` | ||
101 | |||
97 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 102 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
98 | return redirectToObjectStorage({ req, res, video, file: videoFile }) | 103 | return redirectToObjectStorage({ req, res, video, file: videoFile, downloadFilename }) |
99 | } | 104 | } |
100 | 105 | ||
101 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { | 106 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { |
102 | // Express uses basename on filename parameter | 107 | return res.download(path, downloadFilename) |
103 | const videoName = video.name.replace(/[/\\]/g, '_') | ||
104 | const filename = `${videoName}-${videoFile.resolution}p${videoFile.extname}` | ||
105 | |||
106 | return res.download(path, filename) | ||
107 | }) | 108 | }) |
108 | } | 109 | } |
109 | 110 | ||
@@ -136,14 +137,14 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response | |||
136 | 137 | ||
137 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | 138 | if (!checkAllowResult(res, allowParameters, allowedResult)) return |
138 | 139 | ||
140 | const downloadFilename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | ||
141 | |||
139 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 142 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
140 | return redirectToObjectStorage({ req, res, video, file: videoFile }) | 143 | return redirectToObjectStorage({ req, res, video, streamingPlaylist, file: videoFile, downloadFilename }) |
141 | } | 144 | } |
142 | 145 | ||
143 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { | 146 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { |
144 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | 147 | return res.download(path, downloadFilename) |
145 | |||
146 | return res.download(path, filename) | ||
147 | }) | 148 | }) |
148 | } | 149 | } |
149 | 150 | ||
@@ -192,19 +193,21 @@ function checkAllowResult (res: express.Response, allowParameters: any, result?: | |||
192 | return true | 193 | return true |
193 | } | 194 | } |
194 | 195 | ||
195 | function redirectToObjectStorage (options: { | 196 | async function redirectToObjectStorage (options: { |
196 | req: express.Request | 197 | req: express.Request |
197 | res: express.Response | 198 | res: express.Response |
198 | video: MVideo | 199 | video: MVideo |
199 | file: MVideoFile | 200 | file: MVideoFile |
201 | streamingPlaylist?: MStreamingPlaylistVideo | ||
202 | downloadFilename: string | ||
200 | }) { | 203 | }) { |
201 | const { req, res, video, file } = options | 204 | const { res, video, streamingPlaylist, file, downloadFilename } = options |
202 | 205 | ||
203 | const baseUrl = file.getObjectStorageUrl(video) | 206 | const url = streamingPlaylist |
207 | ? await generateHLSFilePresignedUrl({ streamingPlaylist, file, downloadFilename }) | ||
208 | : await generateWebVideoPresignedUrl({ file, downloadFilename }) | ||
204 | 209 | ||
205 | const url = video.hasPrivateStaticPath() && req.query.videoFileToken | 210 | logger.debug('Generating pre-signed URL %s for video %s', url, video.uuid) |
206 | ? addQueryParams(baseUrl, { videoFileToken: req.query.videoFileToken }) | ||
207 | : baseUrl | ||
208 | 211 | ||
209 | return res.redirect(url) | 212 | return res.redirect(url) |
210 | } | 213 | } |
diff --git a/server/controllers/feeds/shared/video-feed-utils.ts b/server/controllers/feeds/shared/video-feed-utils.ts index 3175cea59..b154e04fa 100644 --- a/server/controllers/feeds/shared/video-feed-utils.ts +++ b/server/controllers/feeds/shared/video-feed-utils.ts | |||
@@ -2,7 +2,7 @@ import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' | |||
2 | import { CONFIG } from '@server/initializers/config' | 2 | import { CONFIG } from '@server/initializers/config' |
3 | import { WEBSERVER } from '@server/initializers/constants' | 3 | import { WEBSERVER } from '@server/initializers/constants' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | 5 | import { getCategoryLabel } from '@server/models/video/formatter' |
6 | import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video' | 6 | import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video' |
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { MThumbnail, MUserDefault } from '@server/types/models' | 8 | import { MThumbnail, MUserDefault } from '@server/types/models' |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index b082e41f6..dad30365c 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -1,14 +1,28 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { MActorImage } from '@server/types/models' | ||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
6 | import { logger } from '../helpers/logger' | 5 | import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { |
8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 7 | AvatarPermanentFileCache, |
9 | import { actorImagePathUnsafeCache, downloadActorImageFromWorker } from '../lib/local-actor' | 8 | VideoCaptionsSimpleFileCache, |
9 | VideoMiniaturePermanentFileCache, | ||
10 | VideoPreviewsSimpleFileCache, | ||
11 | VideoStoryboardsSimpleFileCache, | ||
12 | VideoTorrentsSimpleFileCache | ||
13 | } from '../lib/files-cache' | ||
10 | import { asyncMiddleware, handleStaticError } from '../middlewares' | 14 | import { asyncMiddleware, handleStaticError } from '../middlewares' |
11 | import { ActorImageModel } from '../models/actor/actor-image' | 15 | |
16 | // --------------------------------------------------------------------------- | ||
17 | // Cache initializations | ||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | VideoPreviewsSimpleFileCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) | ||
21 | VideoCaptionsSimpleFileCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) | ||
22 | VideoTorrentsSimpleFileCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) | ||
23 | VideoStoryboardsSimpleFileCache.Instance.init(CONFIG.CACHE.STORYBOARDS.SIZE, FILES_CACHE.STORYBOARDS.MAX_AGE) | ||
24 | |||
25 | // --------------------------------------------------------------------------- | ||
12 | 26 | ||
13 | const lazyStaticRouter = express.Router() | 27 | const lazyStaticRouter = express.Router() |
14 | 28 | ||
@@ -27,12 +41,24 @@ lazyStaticRouter.use( | |||
27 | ) | 41 | ) |
28 | 42 | ||
29 | lazyStaticRouter.use( | 43 | lazyStaticRouter.use( |
44 | LAZY_STATIC_PATHS.THUMBNAILS + ':filename', | ||
45 | asyncMiddleware(getThumbnail), | ||
46 | handleStaticError | ||
47 | ) | ||
48 | |||
49 | lazyStaticRouter.use( | ||
30 | LAZY_STATIC_PATHS.PREVIEWS + ':filename', | 50 | LAZY_STATIC_PATHS.PREVIEWS + ':filename', |
31 | asyncMiddleware(getPreview), | 51 | asyncMiddleware(getPreview), |
32 | handleStaticError | 52 | handleStaticError |
33 | ) | 53 | ) |
34 | 54 | ||
35 | lazyStaticRouter.use( | 55 | lazyStaticRouter.use( |
56 | LAZY_STATIC_PATHS.STORYBOARDS + ':filename', | ||
57 | asyncMiddleware(getStoryboard), | ||
58 | handleStaticError | ||
59 | ) | ||
60 | |||
61 | lazyStaticRouter.use( | ||
36 | LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename', | 62 | LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename', |
37 | asyncMiddleware(getVideoCaption), | 63 | asyncMiddleware(getVideoCaption), |
38 | handleStaticError | 64 | handleStaticError |
@@ -53,88 +79,48 @@ export { | |||
53 | } | 79 | } |
54 | 80 | ||
55 | // --------------------------------------------------------------------------- | 81 | // --------------------------------------------------------------------------- |
82 | const avatarPermanentFileCache = new AvatarPermanentFileCache() | ||
56 | 83 | ||
57 | async function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { | 84 | function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { |
58 | const filename = req.params.filename | 85 | const filename = req.params.filename |
59 | 86 | ||
60 | if (actorImagePathUnsafeCache.has(filename)) { | 87 | return avatarPermanentFileCache.lazyServe({ filename, res, next }) |
61 | return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | 88 | } |
62 | } | ||
63 | |||
64 | const image = await ActorImageModel.loadByName(filename) | ||
65 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
66 | |||
67 | if (image.onDisk === false) { | ||
68 | if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
69 | |||
70 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | ||
71 | |||
72 | try { | ||
73 | await downloadActorImageFromWorker({ | ||
74 | filename: image.filename, | ||
75 | fileUrl: image.fileUrl, | ||
76 | size: getActorImageSize(image), | ||
77 | type: image.type | ||
78 | }) | ||
79 | } catch (err) { | ||
80 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) | ||
81 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
82 | } | ||
83 | |||
84 | image.onDisk = true | ||
85 | image.save() | ||
86 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) | ||
87 | } | ||
88 | |||
89 | const path = image.getPath() | ||
90 | |||
91 | actorImagePathUnsafeCache.set(filename, path) | ||
92 | |||
93 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
94 | if (!err) return | ||
95 | |||
96 | // It seems this actor image is not on the disk anymore | ||
97 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
98 | logger.error('Cannot lazy serve actor image %s.', filename, { err }) | ||
99 | 89 | ||
100 | actorImagePathUnsafeCache.delete(filename) | 90 | // --------------------------------------------------------------------------- |
91 | const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() | ||
101 | 92 | ||
102 | image.onDisk = false | 93 | function getThumbnail (req: express.Request, res: express.Response, next: express.NextFunction) { |
103 | image.save() | 94 | const filename = req.params.filename |
104 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) | ||
105 | } | ||
106 | 95 | ||
107 | return next(err) | 96 | return videoMiniaturePermanentFileCache.lazyServe({ filename, res, next }) |
108 | }) | ||
109 | } | 97 | } |
110 | 98 | ||
111 | function getActorImageSize (image: MActorImage): { width: number, height: number } { | 99 | // --------------------------------------------------------------------------- |
112 | if (image.width && image.height) { | 100 | |
113 | return { | 101 | async function getPreview (req: express.Request, res: express.Response) { |
114 | height: image.height, | 102 | const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename) |
115 | width: image.width | 103 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
116 | } | ||
117 | } | ||
118 | 104 | ||
119 | return ACTOR_IMAGES_SIZE[image.type][0] | 105 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
120 | } | 106 | } |
121 | 107 | ||
122 | async function getPreview (req: express.Request, res: express.Response) { | 108 | async function getStoryboard (req: express.Request, res: express.Response) { |
123 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) | 109 | const result = await VideoStoryboardsSimpleFileCache.Instance.getFilePath(req.params.filename) |
124 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 110 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
125 | 111 | ||
126 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 112 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
127 | } | 113 | } |
128 | 114 | ||
129 | async function getVideoCaption (req: express.Request, res: express.Response) { | 115 | async function getVideoCaption (req: express.Request, res: express.Response) { |
130 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) | 116 | const result = await VideoCaptionsSimpleFileCache.Instance.getFilePath(req.params.filename) |
131 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 117 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
132 | 118 | ||
133 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 119 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
134 | } | 120 | } |
135 | 121 | ||
136 | async function getTorrent (req: express.Request, res: express.Response) { | 122 | async function getTorrent (req: express.Request, res: express.Response) { |
137 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 123 | const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) |
138 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 124 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
139 | 125 | ||
140 | // Torrents still use the old naming convention (video uuid + .torrent) | 126 | // Torrents still use the old naming convention (video uuid + .torrent) |
diff --git a/server/controllers/misc.ts b/server/controllers/misc.ts index 4c8af2adc..163352ac5 100644 --- a/server/controllers/misc.ts +++ b/server/controllers/misc.ts | |||
@@ -120,8 +120,8 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
120 | hls: { | 120 | hls: { |
121 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 121 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
122 | }, | 122 | }, |
123 | webtorrent: { | 123 | web_videos: { |
124 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 124 | enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
125 | }, | 125 | }, |
126 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') | 126 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') |
127 | }, | 127 | }, |
diff --git a/server/controllers/object-storage-proxy.ts b/server/controllers/object-storage-proxy.ts index 8e2cc4af9..d0c59bf93 100644 --- a/server/controllers/object-storage-proxy.ts +++ b/server/controllers/object-storage-proxy.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' | 3 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' |
4 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' | 4 | import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' |
5 | import { | 5 | import { |
6 | asyncMiddleware, | 6 | asyncMiddleware, |
7 | ensureCanAccessPrivateVideoHLSFiles, | 7 | ensureCanAccessPrivateVideoHLSFiles, |
8 | ensureCanAccessVideoPrivateWebTorrentFiles, | 8 | ensureCanAccessVideoPrivateWebVideoFiles, |
9 | ensurePrivateObjectStorageProxyIsEnabled, | 9 | ensurePrivateObjectStorageProxyIsEnabled, |
10 | optionalAuthenticate | 10 | optionalAuthenticate |
11 | } from '@server/middlewares' | 11 | } from '@server/middlewares' |
@@ -15,11 +15,12 @@ const objectStorageProxyRouter = express.Router() | |||
15 | 15 | ||
16 | objectStorageProxyRouter.use(cors()) | 16 | objectStorageProxyRouter.use(cors()) |
17 | 17 | ||
18 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + ':filename', | 18 | objectStorageProxyRouter.get( |
19 | [ OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + ':filename', OBJECT_STORAGE_PROXY_PATHS.LEGACY_PRIVATE_WEB_VIDEOS + ':filename' ], | ||
19 | ensurePrivateObjectStorageProxyIsEnabled, | 20 | ensurePrivateObjectStorageProxyIsEnabled, |
20 | optionalAuthenticate, | 21 | optionalAuthenticate, |
21 | asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles), | 22 | asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles), |
22 | asyncMiddleware(proxifyWebTorrentController) | 23 | asyncMiddleware(proxifyWebVideoController) |
23 | ) | 24 | ) |
24 | 25 | ||
25 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', | 26 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', |
@@ -35,10 +36,10 @@ export { | |||
35 | objectStorageProxyRouter | 36 | objectStorageProxyRouter |
36 | } | 37 | } |
37 | 38 | ||
38 | function proxifyWebTorrentController (req: express.Request, res: express.Response) { | 39 | function proxifyWebVideoController (req: express.Request, res: express.Response) { |
39 | const filename = req.params.filename | 40 | const filename = req.params.filename |
40 | 41 | ||
41 | return proxifyWebTorrentFile({ req, res, filename }) | 42 | return proxifyWebVideoFile({ req, res, filename }) |
42 | } | 43 | } |
43 | 44 | ||
44 | function proxifyHLSController (req: express.Request, res: express.Response) { | 45 | function proxifyHLSController (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 9baff94c0..97caa8292 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -6,7 +6,7 @@ import { injectQueryToPlaylistUrls } from '@server/lib/hls' | |||
6 | import { | 6 | import { |
7 | asyncMiddleware, | 7 | asyncMiddleware, |
8 | ensureCanAccessPrivateVideoHLSFiles, | 8 | ensureCanAccessPrivateVideoHLSFiles, |
9 | ensureCanAccessVideoPrivateWebTorrentFiles, | 9 | ensureCanAccessVideoPrivateWebVideoFiles, |
10 | handleStaticError, | 10 | handleStaticError, |
11 | optionalAuthenticate | 11 | optionalAuthenticate |
12 | } from '@server/middlewares' | 12 | } from '@server/middlewares' |
@@ -21,21 +21,21 @@ const staticRouter = express.Router() | |||
21 | staticRouter.use(cors()) | 21 | staticRouter.use(cors()) |
22 | 22 | ||
23 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
24 | // WebTorrent/Classic videos | 24 | // Web videos/Classic videos |
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const privateWebTorrentStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true | 27 | const privateWebVideoStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true |
28 | ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles) ] | 28 | ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles) ] |
29 | : [] | 29 | : [] |
30 | 30 | ||
31 | staticRouter.use( | 31 | staticRouter.use( |
32 | STATIC_PATHS.PRIVATE_WEBSEED, | 32 | [ STATIC_PATHS.PRIVATE_WEB_VIDEOS, STATIC_PATHS.LEGACY_PRIVATE_WEB_VIDEOS ], |
33 | ...privateWebTorrentStaticMiddlewares, | 33 | ...privateWebVideoStaticMiddlewares, |
34 | express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), | 34 | express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), |
35 | handleStaticError | 35 | handleStaticError |
36 | ) | 36 | ) |
37 | staticRouter.use( | 37 | staticRouter.use( |
38 | STATIC_PATHS.WEBSEED, | 38 | [ STATIC_PATHS.WEB_VIDEOS, STATIC_PATHS.LEGACY_WEB_VIDEOS ], |
39 | express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), | 39 | express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), |
40 | handleStaticError | 40 | handleStaticError |
41 | ) | 41 | ) |
@@ -72,7 +72,7 @@ staticRouter.use( | |||
72 | handleStaticError | 72 | handleStaticError |
73 | ) | 73 | ) |
74 | 74 | ||
75 | // Thumbnails path for express | 75 | // FIXME: deprecated in v6, to remove |
76 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR | 76 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR |
77 | staticRouter.use( | 77 | staticRouter.use( |
78 | STATIC_PATHS.THUMBNAILS, | 78 | STATIC_PATHS.THUMBNAILS, |
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 279ad83dc..7df47cf15 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -51,7 +51,8 @@ function setValidAttributedTo (obj: any) { | |||
51 | } | 51 | } |
52 | 52 | ||
53 | obj.attributedTo = obj.attributedTo.filter(a => { | 53 | obj.attributedTo = obj.attributedTo.filter(a => { |
54 | return (a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id) | 54 | return isActivityPubUrlValid(a) || |
55 | ((a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id)) | ||
55 | }) | 56 | }) |
56 | 57 | ||
57 | return true | 58 | return true |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 97b3577af..573a29754 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' | 3 | import { ActivityPubStoryboard, ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject, VideoObject } from '@shared/models' |
4 | import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' | 4 | import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos' |
5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
6 | import { peertubeTruncate } from '../../core-utils' | 6 | import { peertubeTruncate } from '../../core-utils' |
@@ -48,6 +48,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
48 | logger.debug('Video has invalid icons', { video }) | 48 | logger.debug('Video has invalid icons', { video }) |
49 | return false | 49 | return false |
50 | } | 50 | } |
51 | if (!setValidStoryboard(video)) { | ||
52 | logger.debug('Video has invalid preview (storyboard)', { video }) | ||
53 | return false | ||
54 | } | ||
51 | 55 | ||
52 | // Default attributes | 56 | // Default attributes |
53 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED | 57 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED |
@@ -201,3 +205,36 @@ function setRemoteVideoContent (video: any) { | |||
201 | 205 | ||
202 | return true | 206 | return true |
203 | } | 207 | } |
208 | |||
209 | function setValidStoryboard (video: VideoObject) { | ||
210 | if (!video.preview) return true | ||
211 | if (!Array.isArray(video.preview)) return false | ||
212 | |||
213 | video.preview = video.preview.filter(p => isStorybordValid(p)) | ||
214 | |||
215 | return true | ||
216 | } | ||
217 | |||
218 | function isStorybordValid (preview: ActivityPubStoryboard) { | ||
219 | if (!preview) return false | ||
220 | |||
221 | if ( | ||
222 | preview.type !== 'Image' || | ||
223 | !isArray(preview.rel) || | ||
224 | !preview.rel.includes('storyboard') | ||
225 | ) { | ||
226 | return false | ||
227 | } | ||
228 | |||
229 | preview.url = preview.url.filter(u => { | ||
230 | return u.mediaType === 'image/jpeg' && | ||
231 | isActivityPubUrlValid(u.href) && | ||
232 | validator.isInt(u.width + '', { min: 0 }) && | ||
233 | validator.isInt(u.height + '', { min: 0 }) && | ||
234 | validator.isInt(u.tileWidth + '', { min: 0 }) && | ||
235 | validator.isInt(u.tileHeight + '', { min: 0 }) && | ||
236 | isActivityPubVideoDurationValid(u.tileDuration) | ||
237 | }) | ||
238 | |||
239 | return preview.url.length !== 0 | ||
240 | } | ||
diff --git a/server/helpers/custom-validators/metrics.ts b/server/helpers/custom-validators/metrics.ts index 533f8988d..44a863630 100644 --- a/server/helpers/custom-validators/metrics.ts +++ b/server/helpers/custom-validators/metrics.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | function isValidPlayerMode (value: any) { | 1 | function isValidPlayerMode (value: any) { |
2 | return value === 'webtorrent' || value === 'p2p-media-loader' | 2 | // TODO: remove webtorrent in v7 |
3 | return value === 'webtorrent' || value === 'web-video' || value === 'p2p-media-loader' | ||
3 | } | 4 | } |
4 | 5 | ||
5 | // --------------------------------------------------------------------------- | 6 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/video-transcoding.ts b/server/helpers/custom-validators/video-transcoding.ts index cf792f996..220530de4 100644 --- a/server/helpers/custom-validators/video-transcoding.ts +++ b/server/helpers/custom-validators/video-transcoding.ts | |||
@@ -2,7 +2,7 @@ import { exists } from './misc' | |||
2 | 2 | ||
3 | function isValidCreateTranscodingType (value: any) { | 3 | function isValidCreateTranscodingType (value: any) { |
4 | return exists(value) && | 4 | return exists(value) && |
5 | (value === 'hls' || value === 'webtorrent') | 5 | (value === 'hls' || value === 'webtorrent' || value === 'web-video') // TODO: remove webtorrent in v7 |
6 | } | 6 | } |
7 | 7 | ||
8 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 5f75ec27c..91109217c 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | 1 | import { Response, Request, UploadFilesForCheck } from 'express' |
2 | import { decode as magnetUriDecode } from 'magnet-uri' | 2 | import { decode as magnetUriDecode } from 'magnet-uri' |
3 | import validator from 'validator' | 3 | import validator from 'validator' |
4 | import { VideoFilter, VideoInclude, VideoPrivacy, VideoRateType } from '@shared/models' | 4 | import { HttpStatusCode, VideoFilter, VideoInclude, VideoPrivacy, VideoRateType } from '@shared/models' |
5 | import { | 5 | import { |
6 | CONSTRAINTS_FIELDS, | 6 | CONSTRAINTS_FIELDS, |
7 | MIMETYPES, | 7 | MIMETYPES, |
@@ -13,6 +13,7 @@ import { | |||
13 | VIDEO_STATES | 13 | VIDEO_STATES |
14 | } from '../../initializers/constants' | 14 | } from '../../initializers/constants' |
15 | import { exists, isArray, isDateValid, isFileValid } from './misc' | 15 | import { exists, isArray, isDateValid, isFileValid } from './misc' |
16 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
16 | 17 | ||
17 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
18 | 19 | ||
@@ -110,6 +111,10 @@ function isVideoPrivacyValid (value: number) { | |||
110 | return VIDEO_PRIVACIES[value] !== undefined | 111 | return VIDEO_PRIVACIES[value] !== undefined |
111 | } | 112 | } |
112 | 113 | ||
114 | function isVideoReplayPrivacyValid (value: number) { | ||
115 | return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED | ||
116 | } | ||
117 | |||
113 | function isScheduleVideoUpdatePrivacyValid (value: number) { | 118 | function isScheduleVideoUpdatePrivacyValid (value: number) { |
114 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL | 119 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL |
115 | } | 120 | } |
@@ -141,6 +146,49 @@ function isVideoMagnetUriValid (value: string) { | |||
141 | return parsed && isVideoFileInfoHashValid(parsed.infoHash) | 146 | return parsed && isVideoFileInfoHashValid(parsed.infoHash) |
142 | } | 147 | } |
143 | 148 | ||
149 | function isPasswordValid (password: string) { | ||
150 | return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min && | ||
151 | password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max | ||
152 | } | ||
153 | |||
154 | function isValidPasswordProtectedPrivacy (req: Request, res: Response) { | ||
155 | const fail = (message: string) => { | ||
156 | res.fail({ | ||
157 | status: HttpStatusCode.BAD_REQUEST_400, | ||
158 | message | ||
159 | }) | ||
160 | return false | ||
161 | } | ||
162 | |||
163 | let privacy: VideoPrivacy | ||
164 | const video = getVideoWithAttributes(res) | ||
165 | |||
166 | if (exists(req.body?.privacy)) privacy = req.body.privacy | ||
167 | else if (exists(video?.privacy)) privacy = video.privacy | ||
168 | |||
169 | if (privacy !== VideoPrivacy.PASSWORD_PROTECTED) return true | ||
170 | |||
171 | if (!exists(req.body.videoPasswords) && !exists(req.body.passwords)) return fail('Video passwords are missing.') | ||
172 | |||
173 | const passwords = req.body.videoPasswords || req.body.passwords | ||
174 | |||
175 | if (passwords.length === 0) return fail('At least one video password is required.') | ||
176 | |||
177 | if (new Set(passwords).size !== passwords.length) return fail('Duplicate video passwords are not allowed.') | ||
178 | |||
179 | for (const password of passwords) { | ||
180 | if (typeof password !== 'string') { | ||
181 | return fail('Video password should be a string.') | ||
182 | } | ||
183 | |||
184 | if (!isPasswordValid(password)) { | ||
185 | return fail('Invalid video password. Password length should be at least 2 characters and no more than 100 characters.') | ||
186 | } | ||
187 | } | ||
188 | |||
189 | return true | ||
190 | } | ||
191 | |||
144 | // --------------------------------------------------------------------------- | 192 | // --------------------------------------------------------------------------- |
145 | 193 | ||
146 | export { | 194 | export { |
@@ -164,9 +212,12 @@ export { | |||
164 | isVideoDurationValid, | 212 | isVideoDurationValid, |
165 | isVideoTagValid, | 213 | isVideoTagValid, |
166 | isVideoPrivacyValid, | 214 | isVideoPrivacyValid, |
215 | isVideoReplayPrivacyValid, | ||
167 | isVideoFileResolutionValid, | 216 | isVideoFileResolutionValid, |
168 | isVideoFileSizeValid, | 217 | isVideoFileSizeValid, |
169 | isVideoImageValid, | 218 | isVideoImageValid, |
170 | isVideoSupportValid, | 219 | isVideoSupportValid, |
171 | isVideoFilterValid | 220 | isVideoFilterValid, |
221 | isPasswordValid, | ||
222 | isValidPasswordProtectedPrivacy | ||
172 | } | 223 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 82dd4c178..783097e55 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import express, { RequestHandler } from 'express' | 1 | import express, { RequestHandler } from 'express' |
2 | import multer, { diskStorage } from 'multer' | 2 | import multer, { diskStorage } from 'multer' |
3 | import { getLowercaseExtension } from '@shared/core-utils' | 3 | import { getLowercaseExtension } from '@shared/core-utils' |
4 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | ||
5 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG } from '../initializers/config' |
6 | import { REMOTE_SCHEME } from '../initializers/constants' | 5 | import { REMOTE_SCHEME } from '../initializers/constants' |
7 | import { isArray } from './custom-validators/misc' | 6 | import { isArray } from './custom-validators/misc' |
@@ -59,12 +58,6 @@ function getHostWithPort (host: string) { | |||
59 | return host | 58 | return host |
60 | } | 59 | } |
61 | 60 | ||
62 | function badRequest (_req: express.Request, res: express.Response) { | ||
63 | return res.type('json') | ||
64 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
65 | .end() | ||
66 | } | ||
67 | |||
68 | function createReqFiles ( | 61 | function createReqFiles ( |
69 | fieldNames: string[], | 62 | fieldNames: string[], |
70 | mimeTypes: { [id: string]: string | string[] }, | 63 | mimeTypes: { [id: string]: string | string[] }, |
@@ -126,7 +119,6 @@ export { | |||
126 | getHostWithPort, | 119 | getHostWithPort, |
127 | createAnyReqFiles, | 120 | createAnyReqFiles, |
128 | isUserAbleToSearchRemoteURI, | 121 | isUserAbleToSearchRemoteURI, |
129 | badRequest, | ||
130 | createReqFiles, | 122 | createReqFiles, |
131 | cleanUpReqFiles, | 123 | cleanUpReqFiles, |
132 | getCountVideos | 124 | getCountVideos |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index f86f7216d..7b77e694a 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -51,7 +51,7 @@ async function generateImageFromVideoFile (options: { | |||
51 | const pendingImagePath = join(folder, pendingImageName) | 51 | const pendingImagePath = join(folder, pendingImageName) |
52 | 52 | ||
53 | try { | 53 | try { |
54 | await generateThumbnailFromVideo({ fromPath, folder, imageName }) | 54 | await generateThumbnailFromVideo({ fromPath, output: pendingImagePath }) |
55 | 55 | ||
56 | const destination = join(folder, imageName) | 56 | const destination = join(folder, imageName) |
57 | await processImage({ path: pendingImagePath, destination, newSize: size }) | 57 | await processImage({ path: pendingImagePath, destination, newSize: size }) |
diff --git a/server/helpers/promise-cache.ts b/server/helpers/promise-cache.ts index 07e8a9962..303bab976 100644 --- a/server/helpers/promise-cache.ts +++ b/server/helpers/promise-cache.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export class PromiseCache <A, R> { | 1 | export class CachePromiseFactory <A, R> { |
2 | private readonly running = new Map<string, Promise<R>>() | 2 | private readonly running = new Map<string, Promise<R>>() |
3 | 3 | ||
4 | constructor ( | 4 | constructor ( |
@@ -8,14 +8,32 @@ export class PromiseCache <A, R> { | |||
8 | } | 8 | } |
9 | 9 | ||
10 | run (arg: A) { | 10 | run (arg: A) { |
11 | return this.runWithContext(null, arg) | ||
12 | } | ||
13 | |||
14 | runWithContext (ctx: any, arg: A) { | ||
11 | const key = this.keyBuilder(arg) | 15 | const key = this.keyBuilder(arg) |
12 | 16 | ||
13 | if (this.running.has(key)) return this.running.get(key) | 17 | if (this.running.has(key)) return this.running.get(key) |
14 | 18 | ||
15 | const p = this.fn(arg) | 19 | const p = this.fn.apply(ctx || this, [ arg ]) |
16 | 20 | ||
17 | this.running.set(key, p) | 21 | this.running.set(key, p) |
18 | 22 | ||
19 | return p.finally(() => this.running.delete(key)) | 23 | return p.finally(() => this.running.delete(key)) |
20 | } | 24 | } |
21 | } | 25 | } |
26 | |||
27 | export function CachePromise (options: { | ||
28 | keyBuilder: (...args: any[]) => string | ||
29 | }) { | ||
30 | return function (_target, _key, descriptor: PropertyDescriptor) { | ||
31 | const promiseCache = new CachePromiseFactory(descriptor.value, options.keyBuilder) | ||
32 | |||
33 | descriptor.value = function () { | ||
34 | if (arguments.length !== 1) throw new Error('Cache promise only support methods with 1 argument') | ||
35 | |||
36 | return promiseCache.runWithContext(this, arguments[0]) | ||
37 | } | ||
38 | } | ||
39 | } | ||
diff --git a/server/helpers/query.ts b/server/helpers/query.ts index 10efae41c..c0f78368f 100644 --- a/server/helpers/query.ts +++ b/server/helpers/query.ts | |||
@@ -23,7 +23,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) { | |||
23 | 'include', | 23 | 'include', |
24 | 'skipCount', | 24 | 'skipCount', |
25 | 'hasHLSFiles', | 25 | 'hasHLSFiles', |
26 | 'hasWebtorrentFiles', | 26 | 'hasWebtorrentFiles', // TODO: Remove in v7 |
27 | 'hasWebVideoFiles', | ||
27 | 'search', | 28 | 'search', |
28 | 'excludeAlreadyWatched' | 29 | 'excludeAlreadyWatched' |
29 | ]) | 30 | ]) |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index 68dea909d..5ef72058b 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import config from 'config' | 1 | import config from 'config' |
2 | import { readFileSync, writeFileSync } from 'fs-extra' | ||
2 | import { URL } from 'url' | 3 | import { URL } from 'url' |
3 | import { uniqify } from '@shared/core-utils' | 4 | import { uniqify } from '@shared/core-utils' |
4 | import { getFFmpegVersion } from '@shared/ffmpeg' | 5 | import { getFFmpegVersion } from '@shared/ffmpeg' |
@@ -10,7 +11,7 @@ import { logger } from '../helpers/logger' | |||
10 | import { ApplicationModel, getServerActor } from '../models/application/application' | 11 | import { ApplicationModel, getServerActor } from '../models/application/application' |
11 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 12 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
12 | import { UserModel } from '../models/user/user' | 13 | import { UserModel } from '../models/user/user' |
13 | import { CONFIG, isEmailEnabled } from './config' | 14 | import { CONFIG, getLocalConfigFilePath, isEmailEnabled, reloadConfig } from './config' |
14 | import { WEBSERVER } from './constants' | 15 | import { WEBSERVER } from './constants' |
15 | 16 | ||
16 | async function checkActivityPubUrls () { | 17 | async function checkActivityPubUrls () { |
@@ -37,10 +38,7 @@ function checkConfig () { | |||
37 | const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ') | 38 | const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ') |
38 | logger.info('Using following configuration file hierarchy: %s.', configFiles) | 39 | logger.info('Using following configuration file hierarchy: %s.', configFiles) |
39 | 40 | ||
40 | // Moved configuration keys | 41 | checkRemovedConfigKeys() |
41 | if (config.has('services.csp-logger')) { | ||
42 | logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.') | ||
43 | } | ||
44 | 42 | ||
45 | checkSecretsConfig() | 43 | checkSecretsConfig() |
46 | checkEmailConfig() | 44 | checkEmailConfig() |
@@ -104,6 +102,34 @@ export { | |||
104 | 102 | ||
105 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
106 | 104 | ||
105 | function checkRemovedConfigKeys () { | ||
106 | // Moved configuration keys | ||
107 | if (config.has('services.csp-logger')) { | ||
108 | logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.') | ||
109 | } | ||
110 | |||
111 | if (config.has('transcoding.webtorrent.enabled')) { | ||
112 | const localConfigPath = getLocalConfigFilePath() | ||
113 | |||
114 | const content = readFileSync(localConfigPath, { encoding: 'utf-8' }) | ||
115 | if (!content.includes('"webtorrent"')) { | ||
116 | throw new Error('Please rename transcoding.webtorrent.enabled key to transcoding.web_videos.enabled in your configuration file') | ||
117 | } | ||
118 | |||
119 | try { | ||
120 | logger.info( | ||
121 | 'Replacing "transcoding.webtorrent.enabled" key to "transcoding.web_videos.enabled" in your local configuration ' + localConfigPath | ||
122 | ) | ||
123 | |||
124 | writeFileSync(localConfigPath, content.replace('"webtorrent"', '"web_videos"'), { encoding: 'utf-8' }) | ||
125 | |||
126 | reloadConfig() | ||
127 | } catch (err) { | ||
128 | logger.error('Cannot write new configuration to file ' + localConfigPath, { err }) | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
107 | function checkSecretsConfig () { | 133 | function checkSecretsConfig () { |
108 | if (!CONFIG.SECRETS.PEERTUBE) { | 134 | if (!CONFIG.SECRETS.PEERTUBE) { |
109 | throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`') | 135 | throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`') |
@@ -191,15 +217,15 @@ function checkStorageConfig () { | |||
191 | } | 217 | } |
192 | } | 218 | } |
193 | 219 | ||
194 | if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { | 220 | if (CONFIG.STORAGE.WEB_VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { |
195 | logger.warn('Redundancy directory should be different than the videos folder.') | 221 | logger.warn('Redundancy directory should be different than the videos folder.') |
196 | } | 222 | } |
197 | } | 223 | } |
198 | 224 | ||
199 | function checkTranscodingConfig () { | 225 | function checkTranscodingConfig () { |
200 | if (CONFIG.TRANSCODING.ENABLED) { | 226 | if (CONFIG.TRANSCODING.ENABLED) { |
201 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { | 227 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { |
202 | throw new Error('You need to enable at least WebTorrent transcoding or HLS transcoding.') | 228 | throw new Error('You need to enable at least Web Video transcoding or HLS transcoding.') |
203 | } | 229 | } |
204 | 230 | ||
205 | if (CONFIG.TRANSCODING.CONCURRENCY <= 0) { | 231 | if (CONFIG.TRANSCODING.CONCURRENCY <= 0) { |
@@ -264,7 +290,7 @@ function checkLiveConfig () { | |||
264 | function checkObjectStorageConfig () { | 290 | function checkObjectStorageConfig () { |
265 | if (CONFIG.OBJECT_STORAGE.ENABLED === true) { | 291 | if (CONFIG.OBJECT_STORAGE.ENABLED === true) { |
266 | 292 | ||
267 | if (!CONFIG.OBJECT_STORAGE.VIDEOS.BUCKET_NAME) { | 293 | if (!CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME) { |
268 | throw new Error('videos_bucket should be set when object storage support is enabled.') | 294 | throw new Error('videos_bucket should be set when object storage support is enabled.') |
269 | } | 295 | } |
270 | 296 | ||
@@ -273,10 +299,10 @@ function checkObjectStorageConfig () { | |||
273 | } | 299 | } |
274 | 300 | ||
275 | if ( | 301 | if ( |
276 | CONFIG.OBJECT_STORAGE.VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME && | 302 | CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME && |
277 | CONFIG.OBJECT_STORAGE.VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX | 303 | CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX |
278 | ) { | 304 | ) { |
279 | if (CONFIG.OBJECT_STORAGE.VIDEOS.PREFIX === '') { | 305 | if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') { |
280 | throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.') | 306 | throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.') |
281 | } | 307 | } |
282 | 308 | ||
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 0a315ea70..a872fcba3 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -18,7 +18,7 @@ function checkMissedConfig () { | |||
18 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', | 18 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', |
19 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 19 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
20 | 'email.body.signature', 'email.subject.prefix', | 20 | 'email.body.signature', 'email.subject.prefix', |
21 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 21 | 'storage.avatars', 'storage.web_videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
22 | 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 'storage.plugins', 'storage.well_known', | 22 | 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 'storage.plugins', 'storage.well_known', |
23 | 'log.level', 'log.rotation.enabled', 'log.rotation.max_file_size', 'log.rotation.max_files', 'log.anonymize_ip', | 23 | 'log.level', 'log.rotation.enabled', 'log.rotation.max_file_size', 'log.rotation.max_files', 'log.anonymize_ip', |
24 | 'log.log_ping_requests', 'log.log_tracker_unknown_infohash', 'log.prettify_sql', 'log.accept_client_log', | 24 | 'log.log_ping_requests', 'log.log_tracker_unknown_infohash', 'log.prettify_sql', 'log.accept_client_log', |
@@ -29,12 +29,13 @@ function checkMissedConfig () { | |||
29 | 'video_channels.max_per_user', | 29 | 'video_channels.max_per_user', |
30 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', | 30 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', |
31 | 'security.frameguard.enabled', 'security.powered_by_header.enabled', | 31 | 'security.frameguard.enabled', 'security.powered_by_header.enabled', |
32 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', | 32 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'cache.storyboards.size', |
33 | 'admin.email', 'contact_form.enabled', | ||
33 | 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age', | 34 | 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age', |
34 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 35 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
35 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', | 36 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', |
36 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', | 37 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.web_videos.enabled', |
37 | 'transcoding.profile', 'transcoding.concurrency', | 38 | 'transcoding.hls.enabled', 'transcoding.profile', 'transcoding.concurrency', |
38 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', | 39 | 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', |
39 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', | 40 | 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', |
40 | 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled', | 41 | 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled', |
@@ -59,8 +60,8 @@ function checkMissedConfig () { | |||
59 | 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public', | 60 | 'object_storage.enabled', 'object_storage.endpoint', 'object_storage.region', 'object_storage.upload_acl.public', |
60 | 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id', | 61 | 'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id', |
61 | 'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name', | 62 | 'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name', |
62 | 'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.videos.bucket_name', | 63 | 'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.web_videos.bucket_name', |
63 | 'object_storage.videos.prefix', 'object_storage.videos.base_url', | 64 | 'object_storage.web_videos.prefix', 'object_storage.web_videos.base_url', |
64 | 'theme.default', | 65 | 'theme.default', |
65 | 'feeds.videos.count', 'feeds.comments.count', | 66 | 'feeds.videos.count', 'feeds.comments.count', |
66 | 'geo_ip.enabled', 'geo_ip.country.database_url', | 67 | 'geo_ip.enabled', 'geo_ip.country.database_url', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 51ac5d0ce..37cd852f1 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -106,12 +106,13 @@ const CONFIG = { | |||
106 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | 106 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), |
107 | TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')), | 107 | TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')), |
108 | BIN_DIR: buildPath(config.get<string>('storage.bin')), | 108 | BIN_DIR: buildPath(config.get<string>('storage.bin')), |
109 | ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), | 109 | ACTOR_IMAGES_DIR: buildPath(config.get<string>('storage.avatars')), |
110 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 110 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
111 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 111 | WEB_VIDEOS_DIR: buildPath(config.get<string>('storage.web_videos')), |
112 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), | 112 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), |
113 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | 113 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), |
114 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 114 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
115 | STORYBOARDS_DIR: buildPath(config.get<string>('storage.storyboards')), | ||
115 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 116 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
116 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 117 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
117 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), | 118 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), |
@@ -139,10 +140,10 @@ const CONFIG = { | |||
139 | PROXY: { | 140 | PROXY: { |
140 | PROXIFY_PRIVATE_FILES: config.get<boolean>('object_storage.proxy.proxify_private_files') | 141 | PROXIFY_PRIVATE_FILES: config.get<boolean>('object_storage.proxy.proxify_private_files') |
141 | }, | 142 | }, |
142 | VIDEOS: { | 143 | WEB_VIDEOS: { |
143 | BUCKET_NAME: config.get<string>('object_storage.videos.bucket_name'), | 144 | BUCKET_NAME: config.get<string>('object_storage.web_videos.bucket_name'), |
144 | PREFIX: config.get<string>('object_storage.videos.prefix'), | 145 | PREFIX: config.get<string>('object_storage.web_videos.prefix'), |
145 | BASE_URL: config.get<string>('object_storage.videos.base_url') | 146 | BASE_URL: config.get<string>('object_storage.web_videos.base_url') |
146 | }, | 147 | }, |
147 | STREAMING_PLAYLISTS: { | 148 | STREAMING_PLAYLISTS: { |
148 | BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'), | 149 | BUCKET_NAME: config.get<string>('object_storage.streaming_playlists.bucket_name'), |
@@ -370,8 +371,8 @@ const CONFIG = { | |||
370 | HLS: { | 371 | HLS: { |
371 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | 372 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } |
372 | }, | 373 | }, |
373 | WEBTORRENT: { | 374 | WEB_VIDEOS: { |
374 | get ENABLED () { return config.get<boolean>('transcoding.webtorrent.enabled') } | 375 | get ENABLED () { return config.get<boolean>('transcoding.web_videos.enabled') } |
375 | }, | 376 | }, |
376 | REMOTE_RUNNERS: { | 377 | REMOTE_RUNNERS: { |
377 | get ENABLED () { return config.get<boolean>('transcoding.remote_runners.enabled') } | 378 | get ENABLED () { return config.get<boolean>('transcoding.remote_runners.enabled') } |
@@ -482,6 +483,9 @@ const CONFIG = { | |||
482 | }, | 483 | }, |
483 | TORRENTS: { | 484 | TORRENTS: { |
484 | get SIZE () { return config.get<number>('cache.torrents.size') } | 485 | get SIZE () { return config.get<number>('cache.torrents.size') } |
486 | }, | ||
487 | STORYBOARDS: { | ||
488 | get SIZE () { return config.get<number>('cache.storyboards.size') } | ||
485 | } | 489 | } |
486 | }, | 490 | }, |
487 | INSTANCE: { | 491 | INSTANCE: { |
@@ -580,16 +584,6 @@ function isEmailEnabled () { | |||
580 | return false | 584 | return false |
581 | } | 585 | } |
582 | 586 | ||
583 | // --------------------------------------------------------------------------- | ||
584 | |||
585 | export { | ||
586 | CONFIG, | ||
587 | registerConfigChangedHandler, | ||
588 | isEmailEnabled | ||
589 | } | ||
590 | |||
591 | // --------------------------------------------------------------------------- | ||
592 | |||
593 | function getLocalConfigFilePath () { | 587 | function getLocalConfigFilePath () { |
594 | const localConfigDir = getLocalConfigDir() | 588 | const localConfigDir = getLocalConfigDir() |
595 | 589 | ||
@@ -600,6 +594,17 @@ function getLocalConfigFilePath () { | |||
600 | return join(localConfigDir, filename + '.json') | 594 | return join(localConfigDir, filename + '.json') |
601 | } | 595 | } |
602 | 596 | ||
597 | // --------------------------------------------------------------------------- | ||
598 | |||
599 | export { | ||
600 | CONFIG, | ||
601 | getLocalConfigFilePath, | ||
602 | registerConfigChangedHandler, | ||
603 | isEmailEnabled | ||
604 | } | ||
605 | |||
606 | // --------------------------------------------------------------------------- | ||
607 | |||
603 | function getLocalConfigDir () { | 608 | function getLocalConfigDir () { |
604 | if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG | 609 | if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG |
605 | 610 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index a92fd22d6..03ae94d35 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -27,7 +27,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
27 | 27 | ||
28 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
29 | 29 | ||
30 | const LAST_MIGRATION_VERSION = 780 | 30 | const LAST_MIGRATION_VERSION = 790 |
31 | 31 | ||
32 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
33 | 33 | ||
@@ -76,6 +76,8 @@ const SORTABLE_COLUMNS = { | |||
76 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], | 76 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], |
77 | VIDEO_COMMENTS: [ 'createdAt' ], | 77 | VIDEO_COMMENTS: [ 'createdAt' ], |
78 | 78 | ||
79 | VIDEO_PASSWORDS: [ 'createdAt' ], | ||
80 | |||
79 | VIDEO_RATES: [ 'createdAt' ], | 81 | VIDEO_RATES: [ 'createdAt' ], |
80 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], | 82 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], |
81 | 83 | ||
@@ -172,6 +174,7 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = { | |||
172 | 'after-video-channel-import': 1, | 174 | 'after-video-channel-import': 1, |
173 | 'move-to-object-storage': 3, | 175 | 'move-to-object-storage': 3, |
174 | 'transcoding-job-builder': 1, | 176 | 'transcoding-job-builder': 1, |
177 | 'generate-video-storyboard': 1, | ||
175 | 'notify': 1, | 178 | 'notify': 1, |
176 | 'federate-video': 1 | 179 | 'federate-video': 1 |
177 | } | 180 | } |
@@ -196,6 +199,7 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im | |||
196 | 'video-channel-import': 1, | 199 | 'video-channel-import': 1, |
197 | 'after-video-channel-import': 1, | 200 | 'after-video-channel-import': 1, |
198 | 'transcoding-job-builder': 1, | 201 | 'transcoding-job-builder': 1, |
202 | 'generate-video-storyboard': 1, | ||
199 | 'notify': 5, | 203 | 'notify': 5, |
200 | 'federate-video': 3 | 204 | 'federate-video': 3 |
201 | } | 205 | } |
@@ -216,6 +220,7 @@ const JOB_TTL: { [id in JobType]: number } = { | |||
216 | 'activitypub-refresher': 60000 * 10, // 10 minutes | 220 | 'activitypub-refresher': 60000 * 10, // 10 minutes |
217 | 'video-redundancy': 1000 * 3600 * 3, // 3 hours | 221 | 'video-redundancy': 1000 * 3600 * 3, // 3 hours |
218 | 'video-live-ending': 1000 * 60 * 10, // 10 minutes | 222 | 'video-live-ending': 1000 * 60 * 10, // 10 minutes |
223 | 'generate-video-storyboard': 1000 * 60 * 10, // 10 minutes | ||
219 | 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours | 224 | 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours |
220 | 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours | 225 | 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours |
221 | 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours | 226 | 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours |
@@ -444,6 +449,9 @@ const CONSTRAINTS_FIELDS = { | |||
444 | REASON: { min: 1, max: 5000 }, // Length | 449 | REASON: { min: 1, max: 5000 }, // Length |
445 | ERROR_MESSAGE: { min: 1, max: 5000 }, // Length | 450 | ERROR_MESSAGE: { min: 1, max: 5000 }, // Length |
446 | PROGRESS: { min: 0, max: 100 } // Value | 451 | PROGRESS: { min: 0, max: 100 } // Value |
452 | }, | ||
453 | VIDEO_PASSWORD: { | ||
454 | LENGTH: { min: 2, max: 100 } | ||
447 | } | 455 | } |
448 | } | 456 | } |
449 | 457 | ||
@@ -520,7 +528,8 @@ const VIDEO_PRIVACIES: { [ id in VideoPrivacy ]: string } = { | |||
520 | [VideoPrivacy.PUBLIC]: 'Public', | 528 | [VideoPrivacy.PUBLIC]: 'Public', |
521 | [VideoPrivacy.UNLISTED]: 'Unlisted', | 529 | [VideoPrivacy.UNLISTED]: 'Unlisted', |
522 | [VideoPrivacy.PRIVATE]: 'Private', | 530 | [VideoPrivacy.PRIVATE]: 'Private', |
523 | [VideoPrivacy.INTERNAL]: 'Internal' | 531 | [VideoPrivacy.INTERNAL]: 'Internal', |
532 | [VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected' | ||
524 | } | 533 | } |
525 | 534 | ||
526 | const VIDEO_STATES: { [ id in VideoState ]: string } = { | 535 | const VIDEO_STATES: { [ id in VideoState ]: string } = { |
@@ -738,10 +747,16 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = { | |||
738 | 747 | ||
739 | // Express static paths (router) | 748 | // Express static paths (router) |
740 | const STATIC_PATHS = { | 749 | const STATIC_PATHS = { |
750 | // TODO: deprecated in v6, to remove | ||
741 | THUMBNAILS: '/static/thumbnails/', | 751 | THUMBNAILS: '/static/thumbnails/', |
742 | 752 | ||
743 | WEBSEED: '/static/webseed/', | 753 | // Need to keep this legacy path for previously generated torrents |
744 | PRIVATE_WEBSEED: '/static/webseed/private/', | 754 | LEGACY_WEB_VIDEOS: '/static/webseed/', |
755 | WEB_VIDEOS: '/static/web-videos/', | ||
756 | |||
757 | // Need to keep this legacy path for previously generated torrents | ||
758 | LEGACY_PRIVATE_WEB_VIDEOS: '/static/webseed/private/', | ||
759 | PRIVATE_WEB_VIDEOS: '/static/web-videos/private/', | ||
745 | 760 | ||
746 | REDUNDANCY: '/static/redundancy/', | 761 | REDUNDANCY: '/static/redundancy/', |
747 | 762 | ||
@@ -756,14 +771,18 @@ const STATIC_DOWNLOAD_PATHS = { | |||
756 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' | 771 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' |
757 | } | 772 | } |
758 | const LAZY_STATIC_PATHS = { | 773 | const LAZY_STATIC_PATHS = { |
774 | THUMBNAILS: '/lazy-static/thumbnails/', | ||
759 | BANNERS: '/lazy-static/banners/', | 775 | BANNERS: '/lazy-static/banners/', |
760 | AVATARS: '/lazy-static/avatars/', | 776 | AVATARS: '/lazy-static/avatars/', |
761 | PREVIEWS: '/lazy-static/previews/', | 777 | PREVIEWS: '/lazy-static/previews/', |
762 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', | 778 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', |
763 | TORRENTS: '/lazy-static/torrents/' | 779 | TORRENTS: '/lazy-static/torrents/', |
780 | STORYBOARDS: '/lazy-static/storyboards/' | ||
764 | } | 781 | } |
765 | const OBJECT_STORAGE_PROXY_PATHS = { | 782 | const OBJECT_STORAGE_PROXY_PATHS = { |
766 | PRIVATE_WEBSEED: '/object-storage-proxy/webseed/private/', | 783 | // Need to keep this legacy path for previously generated torrents |
784 | LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/', | ||
785 | PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/', | ||
767 | 786 | ||
768 | STREAMING_PLAYLISTS: { | 787 | STREAMING_PLAYLISTS: { |
769 | PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/' | 788 | PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/' |
@@ -807,6 +826,14 @@ const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: num | |||
807 | ] | 826 | ] |
808 | } | 827 | } |
809 | 828 | ||
829 | const STORYBOARD = { | ||
830 | SPRITE_SIZE: { | ||
831 | width: 192, | ||
832 | height: 108 | ||
833 | }, | ||
834 | SPRITES_MAX_EDGE_COUNT: 10 | ||
835 | } | ||
836 | |||
810 | const EMBED_SIZE = { | 837 | const EMBED_SIZE = { |
811 | width: 560, | 838 | width: 560, |
812 | height: 315 | 839 | height: 315 |
@@ -818,6 +845,10 @@ const FILES_CACHE = { | |||
818 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), | 845 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), |
819 | MAX_AGE: 1000 * 3600 * 3 // 3 hours | 846 | MAX_AGE: 1000 * 3600 * 3 // 3 hours |
820 | }, | 847 | }, |
848 | STORYBOARDS: { | ||
849 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'storyboards'), | ||
850 | MAX_AGE: 1000 * 3600 * 24 // 24 hours | ||
851 | }, | ||
821 | VIDEO_CAPTIONS: { | 852 | VIDEO_CAPTIONS: { |
822 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), | 853 | DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), |
823 | MAX_AGE: 1000 * 3600 * 3 // 3 hours | 854 | MAX_AGE: 1000 * 3600 * 3 // 3 hours |
@@ -832,8 +863,8 @@ const LRU_CACHE = { | |||
832 | USER_TOKENS: { | 863 | USER_TOKENS: { |
833 | MAX_SIZE: 1000 | 864 | MAX_SIZE: 1000 |
834 | }, | 865 | }, |
835 | ACTOR_IMAGE_STATIC: { | 866 | FILENAME_TO_PATH_PERMANENT_FILE_CACHE: { |
836 | MAX_SIZE: 500 | 867 | MAX_SIZE: 1000 |
837 | }, | 868 | }, |
838 | STATIC_VIDEO_FILES_RIGHTS_CHECK: { | 869 | STATIC_VIDEO_FILES_RIGHTS_CHECK: { |
839 | MAX_SIZE: 5000, | 870 | MAX_SIZE: 5000, |
@@ -857,8 +888,8 @@ const DIRECTORIES = { | |||
857 | }, | 888 | }, |
858 | 889 | ||
859 | VIDEOS: { | 890 | VIDEOS: { |
860 | PUBLIC: CONFIG.STORAGE.VIDEOS_DIR, | 891 | PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR, |
861 | PRIVATE: join(CONFIG.STORAGE.VIDEOS_DIR, 'private') | 892 | PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private') |
862 | }, | 893 | }, |
863 | 894 | ||
864 | HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | 895 | HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') |
@@ -1084,6 +1115,7 @@ export { | |||
1084 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 1115 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
1085 | RUNNER_JOB_STATES, | 1116 | RUNNER_JOB_STATES, |
1086 | P2P_MEDIA_LOADER_PEER_VERSION, | 1117 | P2P_MEDIA_LOADER_PEER_VERSION, |
1118 | STORYBOARD, | ||
1087 | ACTOR_IMAGES_SIZE, | 1119 | ACTOR_IMAGES_SIZE, |
1088 | ACCEPT_HEADERS, | 1120 | ACCEPT_HEADERS, |
1089 | BCRYPT_SALT_SIZE, | 1121 | BCRYPT_SALT_SIZE, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 14dd8c379..bc120e398 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -10,6 +10,7 @@ import { UserModel } from '@server/models/user/user' | |||
10 | import { UserNotificationModel } from '@server/models/user/user-notification' | 10 | import { UserNotificationModel } from '@server/models/user/user-notification' |
11 | import { UserRegistrationModel } from '@server/models/user/user-registration' | 11 | import { UserRegistrationModel } from '@server/models/user/user-registration' |
12 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 12 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
13 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
13 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | 14 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' |
14 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 15 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
15 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | 16 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' |
@@ -56,6 +57,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
56 | import { VideoTagModel } from '../models/video/video-tag' | 57 | import { VideoTagModel } from '../models/video/video-tag' |
57 | import { VideoViewModel } from '../models/view/video-view' | 58 | import { VideoViewModel } from '../models/view/video-view' |
58 | import { CONFIG } from './config' | 59 | import { CONFIG } from './config' |
60 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
59 | 61 | ||
60 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 62 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
61 | 63 | ||
@@ -163,9 +165,11 @@ async function initDatabaseModels (silent: boolean) { | |||
163 | VideoJobInfoModel, | 165 | VideoJobInfoModel, |
164 | VideoChannelSyncModel, | 166 | VideoChannelSyncModel, |
165 | UserRegistrationModel, | 167 | UserRegistrationModel, |
168 | VideoPasswordModel, | ||
166 | RunnerRegistrationTokenModel, | 169 | RunnerRegistrationTokenModel, |
167 | RunnerModel, | 170 | RunnerModel, |
168 | RunnerJobModel | 171 | RunnerJobModel, |
172 | StoryboardModel | ||
169 | ]) | 173 | ]) |
170 | 174 | ||
171 | // Check extensions exist in the database | 175 | // Check extensions exist in the database |
diff --git a/server/initializers/migrations/0785-video-password-protection.ts b/server/initializers/migrations/0785-video-password-protection.ts new file mode 100644 index 000000000..1d85f4489 --- /dev/null +++ b/server/initializers/migrations/0785-video-password-protection.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | { | ||
9 | const query = ` | ||
10 | CREATE TABLE IF NOT EXISTS "videoPassword" ( | ||
11 | "id" SERIAL, | ||
12 | "password" VARCHAR(255) NOT NULL, | ||
13 | "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
14 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
15 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
16 | PRIMARY KEY ("id") | ||
17 | ); | ||
18 | ` | ||
19 | |||
20 | await utils.sequelize.query(query, { transaction : utils.transaction }) | ||
21 | } | ||
22 | } | ||
23 | |||
24 | function down (options) { | ||
25 | throw new Error('Not implemented.') | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | up, | ||
30 | down | ||
31 | } | ||
diff --git a/server/initializers/migrations/0790-thumbnail-disk.ts b/server/initializers/migrations/0790-thumbnail-disk.ts new file mode 100644 index 000000000..0824c042e --- /dev/null +++ b/server/initializers/migrations/0790-thumbnail-disk.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | const { transaction } = utils | ||
9 | |||
10 | { | ||
11 | const data = { | ||
12 | type: Sequelize.BOOLEAN, | ||
13 | allowNull: true, | ||
14 | defaultValue: true | ||
15 | } | ||
16 | |||
17 | await utils.queryInterface.addColumn('thumbnail', 'onDisk', data, { transaction }) | ||
18 | } | ||
19 | |||
20 | { | ||
21 | // Remote previews are not on the disk | ||
22 | await utils.sequelize.query( | ||
23 | 'UPDATE "thumbnail" SET "onDisk" = FALSE ' + | ||
24 | 'WHERE "type" = 2 AND "videoId" NOT IN (SELECT "id" FROM "video" WHERE "remote" IS FALSE)', | ||
25 | { transaction } | ||
26 | ) | ||
27 | } | ||
28 | |||
29 | { | ||
30 | const data = { | ||
31 | type: Sequelize.BOOLEAN, | ||
32 | allowNull: false, | ||
33 | defaultValue: null | ||
34 | } | ||
35 | |||
36 | await utils.queryInterface.changeColumn('thumbnail', 'onDisk', data, { transaction }) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function down (options) { | ||
41 | throw new Error('Not implemented.') | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | up, | ||
46 | down | ||
47 | } | ||
diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts index 1f6ec221e..0fed3e8fd 100644 --- a/server/lib/activitypub/activity.ts +++ b/server/lib/activitypub/activity.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { ActivityType } from '@shared/models' | 1 | import { doJSONRequest } from '@server/helpers/requests' |
2 | import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models' | ||
2 | 3 | ||
3 | function getAPId (object: string | { id: string }) { | 4 | function getAPId (object: string | { id: string }) { |
4 | if (typeof object === 'string') return object | 5 | if (typeof object === 'string') return object |
@@ -32,8 +33,19 @@ function buildAvailableActivities (): ActivityType[] { | |||
32 | ] | 33 | ] |
33 | } | 34 | } |
34 | 35 | ||
36 | async function fetchAPObject <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) { | ||
37 | if (typeof object === 'string') { | ||
38 | const { body } = await doJSONRequest<Exclude<T, string>>(object, { activityPub: true }) | ||
39 | |||
40 | return body | ||
41 | } | ||
42 | |||
43 | return object as Exclude<T, string> | ||
44 | } | ||
45 | |||
35 | export { | 46 | export { |
36 | getAPId, | 47 | getAPId, |
48 | fetchAPObject, | ||
37 | getActivityStreamDuration, | 49 | getActivityStreamDuration, |
38 | buildAvailableActivities, | 50 | buildAvailableActivities, |
39 | getDurationFromActivityStream | 51 | getDurationFromActivityStream |
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts index e73b7d707..b2be3f5fb 100644 --- a/server/lib/activitypub/actors/get.ts +++ b/server/lib/activitypub/actors/get.ts | |||
@@ -3,8 +3,9 @@ import { logger } from '@server/helpers/logger' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders' | 4 | import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders' |
5 | import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' | 5 | import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' |
6 | import { ActivityPubActor } from '@shared/models' | 6 | import { arrayify } from '@shared/core-utils' |
7 | import { getAPId } from '../activity' | 7 | import { ActivityPubActor, APObjectId } from '@shared/models' |
8 | import { fetchAPObject, getAPId } from '../activity' | ||
8 | import { checkUrlsSameHost } from '../url' | 9 | import { checkUrlsSameHost } from '../url' |
9 | import { refreshActorIfNeeded } from './refresh' | 10 | import { refreshActorIfNeeded } from './refresh' |
10 | import { APActorCreator, fetchRemoteActor } from './shared' | 11 | import { APActorCreator, fetchRemoteActor } from './shared' |
@@ -40,7 +41,7 @@ async function getOrCreateAPActor ( | |||
40 | const { actorObject } = await fetchRemoteActor(actorUrl) | 41 | const { actorObject } = await fetchRemoteActor(actorUrl) |
41 | if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) | 42 | if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) |
42 | 43 | ||
43 | // actorUrl is just an alias/rediraction, so process object id instead | 44 | // actorUrl is just an alias/redirection, so process object id instead |
44 | if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections) | 45 | if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections) |
45 | 46 | ||
46 | // Create the attributed to actor | 47 | // Create the attributed to actor |
@@ -68,29 +69,48 @@ async function getOrCreateAPActor ( | |||
68 | return actorRefreshed | 69 | return actorRefreshed |
69 | } | 70 | } |
70 | 71 | ||
71 | function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) { | 72 | async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) { |
72 | const accountAttributedTo = actorObject.attributedTo.find(a => a.type === 'Person') | 73 | const accountAttributedTo = await findOwner(actorUrl, actorObject.attributedTo, 'Person') |
73 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actorUrl) | 74 | if (!accountAttributedTo) { |
74 | 75 | throw new Error(`Cannot find account attributed to video channel ${actorUrl}`) | |
75 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
76 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
77 | } | 76 | } |
78 | 77 | ||
79 | try { | 78 | try { |
80 | // Don't recurse another time | 79 | // Don't recurse another time |
81 | const recurseIfNeeded = false | 80 | const recurseIfNeeded = false |
82 | return getOrCreateAPActor(accountAttributedTo.id, 'all', recurseIfNeeded) | 81 | return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded) |
83 | } catch (err) { | 82 | } catch (err) { |
84 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) | 83 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) |
85 | throw new Error(err) | 84 | throw new Error(err) |
86 | } | 85 | } |
87 | } | 86 | } |
88 | 87 | ||
88 | async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') { | ||
89 | for (const actorToCheck of arrayify(attributedTo)) { | ||
90 | const actorObject = await fetchAPObject<ActivityPubActor>(getAPId(actorToCheck)) | ||
91 | |||
92 | if (!actorObject) { | ||
93 | logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl) | ||
94 | continue | ||
95 | } | ||
96 | |||
97 | if (checkUrlsSameHost(actorObject.id, rootUrl) !== true) { | ||
98 | logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootUrl}`) | ||
99 | continue | ||
100 | } | ||
101 | |||
102 | if (actorObject.type === type) return actorObject | ||
103 | } | ||
104 | |||
105 | return undefined | ||
106 | } | ||
107 | |||
89 | // --------------------------------------------------------------------------- | 108 | // --------------------------------------------------------------------------- |
90 | 109 | ||
91 | export { | 110 | export { |
92 | getOrCreateAPOwner, | 111 | getOrCreateAPOwner, |
93 | getOrCreateAPActor | 112 | getOrCreateAPActor, |
113 | findOwner | ||
94 | } | 114 | } |
95 | 115 | ||
96 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/actors/refresh.ts b/server/lib/activitypub/actors/refresh.ts index 6d8428d66..d15cb5e90 100644 --- a/server/lib/activitypub/actors/refresh.ts +++ b/server/lib/activitypub/actors/refresh.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { PromiseCache } from '@server/helpers/promise-cache' | 2 | import { CachePromiseFactory } from '@server/helpers/promise-cache' |
3 | import { PeerTubeRequestError } from '@server/helpers/requests' | 3 | import { PeerTubeRequestError } from '@server/helpers/requests' |
4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
@@ -16,7 +16,7 @@ type RefreshOptions <T> = { | |||
16 | fetchedType: ActorLoadByUrlType | 16 | fetchedType: ActorLoadByUrlType |
17 | } | 17 | } |
18 | 18 | ||
19 | const promiseCache = new PromiseCache(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url) | 19 | const promiseCache = new CachePromiseFactory(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url) |
20 | 20 | ||
21 | function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> { | 21 | function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> { |
22 | const actorArg = options.actor | 22 | const actorArg = options.actor |
diff --git a/server/lib/activitypub/context.ts b/server/lib/activitypub/context.ts index a3ca52a31..750276a11 100644 --- a/server/lib/activitypub/context.ts +++ b/server/lib/activitypub/context.ts | |||
@@ -46,6 +46,19 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string | |||
46 | 46 | ||
47 | Infohash: 'pt:Infohash', | 47 | Infohash: 'pt:Infohash', |
48 | 48 | ||
49 | tileWidth: { | ||
50 | '@type': 'sc:Number', | ||
51 | '@id': 'pt:tileWidth' | ||
52 | }, | ||
53 | tileHeight: { | ||
54 | '@type': 'sc:Number', | ||
55 | '@id': 'pt:tileHeight' | ||
56 | }, | ||
57 | tileDuration: { | ||
58 | '@type': 'sc:Number', | ||
59 | '@id': 'pt:tileDuration' | ||
60 | }, | ||
61 | |||
49 | originallyPublishedAt: 'sc:datePublished', | 62 | originallyPublishedAt: 'sc:datePublished', |
50 | views: { | 63 | views: { |
51 | '@type': 'sc:Number', | 64 | '@type': 'sc:Number', |
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts index 9339e8ea4..b24299f29 100644 --- a/server/lib/activitypub/playlists/create-update.ts +++ b/server/lib/activitypub/playlists/create-update.ts | |||
@@ -4,7 +4,7 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' | 5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' |
6 | import { sequelizeTypescript } from '@server/initializers/database' | 6 | import { sequelizeTypescript } from '@server/initializers/database' |
7 | import { updatePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' | 7 | import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' |
8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | 9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' |
10 | import { FilteredModelAttributes } from '@server/types' | 10 | import { FilteredModelAttributes } from '@server/types' |
@@ -77,7 +77,7 @@ async function setVideoChannel (playlistObject: PlaylistObject, playlistAttribut | |||
77 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) | 77 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) |
78 | } | 78 | } |
79 | 79 | ||
80 | const actor = await getOrCreateAPActor(playlistObject.attributedTo[0], 'all') | 80 | const actor = await getOrCreateAPActor(getAPId(playlistObject.attributedTo[0]), 'all') |
81 | 81 | ||
82 | if (!actor.VideoChannel) { | 82 | if (!actor.VideoChannel) { |
83 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) | 83 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) |
@@ -104,7 +104,7 @@ async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist | |||
104 | let thumbnailModel: MThumbnail | 104 | let thumbnailModel: MThumbnail |
105 | 105 | ||
106 | try { | 106 | try { |
107 | thumbnailModel = await updatePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) | 107 | thumbnailModel = await updateRemotePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) |
108 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) | 108 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) |
109 | } catch (err) { | 109 | } catch (err) { |
110 | logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) | 110 | logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) |
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts index bfaf52cc9..c34554d69 100644 --- a/server/lib/activitypub/playlists/get.ts +++ b/server/lib/activitypub/playlists/get.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
2 | import { MVideoPlaylistFullSummary } from '@server/types/models' | 2 | import { MVideoPlaylistFullSummary } from '@server/types/models' |
3 | import { APObject } from '@shared/models' | 3 | import { APObjectId } from '@shared/models' |
4 | import { getAPId } from '../activity' | 4 | import { getAPId } from '../activity' |
5 | import { createOrUpdateVideoPlaylist } from './create-update' | 5 | import { createOrUpdateVideoPlaylist } from './create-update' |
6 | import { scheduleRefreshIfNeeded } from './refresh' | 6 | import { scheduleRefreshIfNeeded } from './refresh' |
7 | import { fetchRemoteVideoPlaylist } from './shared' | 7 | import { fetchRemoteVideoPlaylist } from './shared' |
8 | 8 | ||
9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { | 9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise<MVideoPlaylistFullSummary> { |
10 | const playlistUrl = getAPId(playlistObjectArg) | 10 | const playlistUrl = getAPId(playlistObjectArg) |
11 | 11 | ||
12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) | 12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1e6e8956c..e89d1ab45 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,13 +1,24 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | 1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' |
2 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' | 4 | import { |
5 | AbuseObject, | ||
6 | ActivityCreate, | ||
7 | ActivityCreateObject, | ||
8 | ActivityObject, | ||
9 | CacheFileObject, | ||
10 | PlaylistObject, | ||
11 | VideoCommentObject, | ||
12 | VideoObject, | ||
13 | WatchActionObject | ||
14 | } from '@shared/models' | ||
5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 15 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
6 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 17 | import { sequelizeTypescript } from '../../../initializers/database' |
8 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 18 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 19 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
10 | import { Notifier } from '../../notifier' | 20 | import { Notifier } from '../../notifier' |
21 | import { fetchAPObject } from '../activity' | ||
11 | import { createOrUpdateCacheFile } from '../cache-file' | 22 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' | 23 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' |
13 | import { createOrUpdateVideoPlaylist } from '../playlists' | 24 | import { createOrUpdateVideoPlaylist } from '../playlists' |
@@ -15,35 +26,35 @@ import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | |||
15 | import { resolveThread } from '../video-comments' | 26 | import { resolveThread } from '../video-comments' |
16 | import { getOrCreateAPVideo } from '../videos' | 27 | import { getOrCreateAPVideo } from '../videos' |
17 | 28 | ||
18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 29 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) { |
19 | const { activity, byActor } = options | 30 | const { activity, byActor } = options |
20 | 31 | ||
21 | // Only notify if it is not from a fetcher job | 32 | // Only notify if it is not from a fetcher job |
22 | const notify = options.fromFetch !== true | 33 | const notify = options.fromFetch !== true |
23 | const activityObject = activity.object | 34 | const activityObject = await fetchAPObject<Exclude<ActivityObject, AbuseObject>>(activity.object) |
24 | const activityType = activityObject.type | 35 | const activityType = activityObject.type |
25 | 36 | ||
26 | if (activityType === 'Video') { | 37 | if (activityType === 'Video') { |
27 | return processCreateVideo(activity, notify) | 38 | return processCreateVideo(activityObject, notify) |
28 | } | 39 | } |
29 | 40 | ||
30 | if (activityType === 'Note') { | 41 | if (activityType === 'Note') { |
31 | // Comments will be fetched from videos | 42 | // Comments will be fetched from videos |
32 | if (options.fromFetch) return | 43 | if (options.fromFetch) return |
33 | 44 | ||
34 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) | 45 | return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify) |
35 | } | 46 | } |
36 | 47 | ||
37 | if (activityType === 'WatchAction') { | 48 | if (activityType === 'WatchAction') { |
38 | return retryTransactionWrapper(processCreateWatchAction, activity) | 49 | return retryTransactionWrapper(processCreateWatchAction, activityObject) |
39 | } | 50 | } |
40 | 51 | ||
41 | if (activityType === 'CacheFile') { | 52 | if (activityType === 'CacheFile') { |
42 | return retryTransactionWrapper(processCreateCacheFile, activity, byActor) | 53 | return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor) |
43 | } | 54 | } |
44 | 55 | ||
45 | if (activityType === 'Playlist') { | 56 | if (activityType === 'Playlist') { |
46 | return retryTransactionWrapper(processCreatePlaylist, activity, byActor) | 57 | return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor) |
47 | } | 58 | } |
48 | 59 | ||
49 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 60 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -58,9 +69,7 @@ export { | |||
58 | 69 | ||
59 | // --------------------------------------------------------------------------- | 70 | // --------------------------------------------------------------------------- |
60 | 71 | ||
61 | async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | 72 | async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) { |
62 | const videoToCreateData = activity.object as VideoObject | ||
63 | |||
64 | const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } | 73 | const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } |
65 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) | 74 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) |
66 | 75 | ||
@@ -69,11 +78,13 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | |||
69 | return video | 78 | return video |
70 | } | 79 | } |
71 | 80 | ||
72 | async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { | 81 | async function processCreateCacheFile ( |
82 | activity: ActivityCreate<CacheFileObject | string>, | ||
83 | cacheFile: CacheFileObject, | ||
84 | byActor: MActorSignature | ||
85 | ) { | ||
73 | if (await isRedundancyAccepted(activity, byActor) !== true) return | 86 | if (await isRedundancyAccepted(activity, byActor) !== true) return |
74 | 87 | ||
75 | const cacheFile = activity.object as CacheFileObject | ||
76 | |||
77 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) | 88 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) |
78 | 89 | ||
79 | await sequelizeTypescript.transaction(async t => { | 90 | await sequelizeTypescript.transaction(async t => { |
@@ -87,9 +98,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor | |||
87 | } | 98 | } |
88 | } | 99 | } |
89 | 100 | ||
90 | async function processCreateWatchAction (activity: ActivityCreate) { | 101 | async function processCreateWatchAction (watchAction: WatchActionObject) { |
91 | const watchAction = activity.object as WatchActionObject | ||
92 | |||
93 | if (watchAction.actionStatus !== 'CompletedActionStatus') return | 102 | if (watchAction.actionStatus !== 'CompletedActionStatus') return |
94 | 103 | ||
95 | const video = await VideoModel.loadByUrl(watchAction.object) | 104 | const video = await VideoModel.loadByUrl(watchAction.object) |
@@ -100,8 +109,12 @@ async function processCreateWatchAction (activity: ActivityCreate) { | |||
100 | }) | 109 | }) |
101 | } | 110 | } |
102 | 111 | ||
103 | async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { | 112 | async function processCreateVideoComment ( |
104 | const commentObject = activity.object as VideoCommentObject | 113 | activity: ActivityCreate<VideoCommentObject | string>, |
114 | commentObject: VideoCommentObject, | ||
115 | byActor: MActorSignature, | ||
116 | notify: boolean | ||
117 | ) { | ||
105 | const byAccount = byActor.Account | 118 | const byAccount = byActor.Account |
106 | 119 | ||
107 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) | 120 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) |
@@ -144,8 +157,11 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc | |||
144 | if (created && notify) Notifier.Instance.notifyOnNewComment(comment) | 157 | if (created && notify) Notifier.Instance.notifyOnNewComment(comment) |
145 | } | 158 | } |
146 | 159 | ||
147 | async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) { | 160 | async function processCreatePlaylist ( |
148 | const playlistObject = activity.object as PlaylistObject | 161 | activity: ActivityCreate<PlaylistObject | string>, |
162 | playlistObject: PlaylistObject, | ||
163 | byActor: MActorSignature | ||
164 | ) { | ||
149 | const byAccount = byActor.Account | 165 | const byAccount = byActor.Account |
150 | 166 | ||
151 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | 167 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) |
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts index 44e349b22..4e270f917 100644 --- a/server/lib/activitypub/process/process-dislike.ts +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' | 2 | import { ActivityDislike } from '@shared/models' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { sequelizeTypescript } from '../../../initializers/database' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
@@ -7,7 +7,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
7 | import { MActorSignature } from '../../../types/models' | 7 | import { MActorSignature } from '../../../types/models' |
8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
9 | 9 | ||
10 | async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { | 10 | async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) { |
11 | const { activity, byActor } = options | 11 | const { activity, byActor } = options |
12 | return retryTransactionWrapper(processDislike, activity, byActor) | 12 | return retryTransactionWrapper(processDislike, activity, byActor) |
13 | } | 13 | } |
@@ -20,11 +20,8 @@ export { | |||
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
23 | async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) { | 23 | async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { |
24 | const dislikeObject = activity.type === 'Dislike' | 24 | const dislikeObject = activity.object |
25 | ? activity.object | ||
26 | : (activity.object as DislikeObject).object | ||
27 | |||
28 | const byAccount = byActor.Account | 25 | const byAccount = byActor.Account |
29 | 26 | ||
30 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | 27 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) |
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index 10f58ef27..bea285670 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts | |||
@@ -3,7 +3,7 @@ import { AccountModel } from '@server/models/account/account' | |||
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | 4 | import { VideoCommentModel } from '@server/models/video/video-comment' |
5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' |
6 | import { AbuseObject, AbuseState, ActivityCreate, ActivityFlag } from '@shared/models' | 6 | import { AbuseState, ActivityFlag } from '@shared/models' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { sequelizeTypescript } from '../../../initializers/database' | 9 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity' | |||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' | 12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' |
13 | 13 | ||
14 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 14 | async function processFlagActivity (options: APProcessorOptions<ActivityFlag>) { |
15 | const { activity, byActor } = options | 15 | const { activity, byActor } = options |
16 | 16 | ||
17 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) | 17 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) |
@@ -25,9 +25,7 @@ export { | |||
25 | 25 | ||
26 | // --------------------------------------------------------------------------- | 26 | // --------------------------------------------------------------------------- |
27 | 27 | ||
28 | async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { | 28 | async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) { |
29 | const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject) | ||
30 | |||
31 | const account = byActor.Account | 29 | const account = byActor.Account |
32 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) | 30 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) |
33 | 31 | ||
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 99423a72b..25f68724d 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -1,6 +1,14 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' | 2 | import { |
3 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | 3 | ActivityAnnounce, |
4 | ActivityCreate, | ||
5 | ActivityDislike, | ||
6 | ActivityFollow, | ||
7 | ActivityLike, | ||
8 | ActivityUndo, | ||
9 | ActivityUndoObject, | ||
10 | CacheFileObject | ||
11 | } from '../../../../shared/models/activitypub' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 12 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers/database' | 14 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,10 +19,11 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc | |||
11 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
12 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 20 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
13 | import { MActorSignature } from '../../../types/models' | 21 | import { MActorSignature } from '../../../types/models' |
22 | import { fetchAPObject } from '../activity' | ||
14 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 23 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
15 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 24 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
16 | 25 | ||
17 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | 26 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo<ActivityUndoObject>>) { |
18 | const { activity, byActor } = options | 27 | const { activity, byActor } = options |
19 | const activityToUndo = activity.object | 28 | const activityToUndo = activity.object |
20 | 29 | ||
@@ -23,8 +32,10 @@ async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | |||
23 | } | 32 | } |
24 | 33 | ||
25 | if (activityToUndo.type === 'Create') { | 34 | if (activityToUndo.type === 'Create') { |
26 | if (activityToUndo.object.type === 'CacheFile') { | 35 | const objectToUndo = await fetchAPObject<CacheFileObject>(activityToUndo.object) |
27 | return retryTransactionWrapper(processUndoCacheFile, byActor, activity) | 36 | |
37 | if (objectToUndo.type === 'CacheFile') { | ||
38 | return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo) | ||
28 | } | 39 | } |
29 | } | 40 | } |
30 | 41 | ||
@@ -53,8 +64,8 @@ export { | |||
53 | 64 | ||
54 | // --------------------------------------------------------------------------- | 65 | // --------------------------------------------------------------------------- |
55 | 66 | ||
56 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { | 67 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo<ActivityLike>) { |
57 | const likeActivity = activity.object as ActivityLike | 68 | const likeActivity = activity.object |
58 | 69 | ||
59 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) | 70 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) |
60 | // We don't care about likes of remote videos | 71 | // We don't care about likes of remote videos |
@@ -78,12 +89,10 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo | |||
78 | }) | 89 | }) |
79 | } | 90 | } |
80 | 91 | ||
81 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { | 92 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo<ActivityDislike>) { |
82 | const dislike = activity.object.type === 'Dislike' | 93 | const dislikeActivity = activity.object |
83 | ? activity.object | ||
84 | : activity.object.object as DislikeObject | ||
85 | 94 | ||
86 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object }) | 95 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object }) |
87 | // We don't care about likes of remote videos | 96 | // We don't care about likes of remote videos |
88 | if (!onlyVideo.isOwned()) return | 97 | if (!onlyVideo.isOwned()) return |
89 | 98 | ||
@@ -91,7 +100,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
91 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 100 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
92 | 101 | ||
93 | const video = await VideoModel.loadFull(onlyVideo.id, t) | 102 | const video = await VideoModel.loadFull(onlyVideo.id, t) |
94 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t) | 103 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t) |
95 | if (!rate || rate.type !== 'dislike') { | 104 | if (!rate || rate.type !== 'dislike') { |
96 | logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) | 105 | logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) |
97 | return | 106 | return |
@@ -107,9 +116,11 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
107 | 116 | ||
108 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
109 | 118 | ||
110 | async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { | 119 | async function processUndoCacheFile ( |
111 | const cacheFileObject = activity.object.object as CacheFileObject | 120 | byActor: MActorSignature, |
112 | 121 | activity: ActivityUndo<ActivityCreate<CacheFileObject>>, | |
122 | cacheFileObject: CacheFileObject | ||
123 | ) { | ||
113 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) | 124 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) |
114 | 125 | ||
115 | return sequelizeTypescript.transaction(async t => { | 126 | return sequelizeTypescript.transaction(async t => { |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 4afdbd430..9caa74e04 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 1 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
2 | import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -10,16 +10,18 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
10 | import { ActorModel } from '../../../models/actor/actor' | 10 | import { ActorModel } from '../../../models/actor/actor' |
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MActorFull, MActorSignature } from '../../../types/models' | 12 | import { MActorFull, MActorSignature } from '../../../types/models' |
13 | import { fetchAPObject } from '../activity' | ||
13 | import { APActorUpdater } from '../actors/updater' | 14 | import { APActorUpdater } from '../actors/updater' |
14 | import { createOrUpdateCacheFile } from '../cache-file' | 15 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { createOrUpdateVideoPlaylist } from '../playlists' | 16 | import { createOrUpdateVideoPlaylist } from '../playlists' |
16 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 17 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
17 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' | 18 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' |
18 | 19 | ||
19 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 20 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) { |
20 | const { activity, byActor } = options | 21 | const { activity, byActor } = options |
21 | 22 | ||
22 | const objectType = activity.object.type | 23 | const object = await fetchAPObject(activity.object) |
24 | const objectType = object.type | ||
23 | 25 | ||
24 | if (objectType === 'Video') { | 26 | if (objectType === 'Video') { |
25 | return retryTransactionWrapper(processUpdateVideo, activity) | 27 | return retryTransactionWrapper(processUpdateVideo, activity) |
@@ -28,17 +30,17 @@ async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate | |||
28 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { | 30 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { |
29 | // We need more attributes | 31 | // We need more attributes |
30 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | 32 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) |
31 | return retryTransactionWrapper(processUpdateActor, byActorFull, activity) | 33 | return retryTransactionWrapper(processUpdateActor, byActorFull, object) |
32 | } | 34 | } |
33 | 35 | ||
34 | if (objectType === 'CacheFile') { | 36 | if (objectType === 'CacheFile') { |
35 | // We need more attributes | 37 | // We need more attributes |
36 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | 38 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) |
37 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) | 39 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity, object) |
38 | } | 40 | } |
39 | 41 | ||
40 | if (objectType === 'Playlist') { | 42 | if (objectType === 'Playlist') { |
41 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity) | 43 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object) |
42 | } | 44 | } |
43 | 45 | ||
44 | return undefined | 46 | return undefined |
@@ -52,7 +54,7 @@ export { | |||
52 | 54 | ||
53 | // --------------------------------------------------------------------------- | 55 | // --------------------------------------------------------------------------- |
54 | 56 | ||
55 | async function processUpdateVideo (activity: ActivityUpdate) { | 57 | async function processUpdateVideo (activity: ActivityUpdate<VideoObject | string>) { |
56 | const videoObject = activity.object as VideoObject | 58 | const videoObject = activity.object as VideoObject |
57 | 59 | ||
58 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { | 60 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { |
@@ -72,11 +74,13 @@ async function processUpdateVideo (activity: ActivityUpdate) { | |||
72 | return updater.update(activity.to) | 74 | return updater.update(activity.to) |
73 | } | 75 | } |
74 | 76 | ||
75 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { | 77 | async function processUpdateCacheFile ( |
78 | byActor: MActorSignature, | ||
79 | activity: ActivityUpdate<CacheFileObject | string>, | ||
80 | cacheFileObject: CacheFileObject | ||
81 | ) { | ||
76 | if (await isRedundancyAccepted(activity, byActor) !== true) return | 82 | if (await isRedundancyAccepted(activity, byActor) !== true) return |
77 | 83 | ||
78 | const cacheFileObject = activity.object as CacheFileObject | ||
79 | |||
80 | if (!isCacheFileObjectValid(cacheFileObject)) { | 84 | if (!isCacheFileObjectValid(cacheFileObject)) { |
81 | logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) | 85 | logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) |
82 | return undefined | 86 | return undefined |
@@ -96,19 +100,19 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ | |||
96 | } | 100 | } |
97 | } | 101 | } |
98 | 102 | ||
99 | async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { | 103 | async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) { |
100 | const actorObject = activity.object as ActivityPubActor | ||
101 | |||
102 | logger.debug('Updating remote account "%s".', actorObject.url) | 104 | logger.debug('Updating remote account "%s".', actorObject.url) |
103 | 105 | ||
104 | const updater = new APActorUpdater(actorObject, actor) | 106 | const updater = new APActorUpdater(actorObject, actor) |
105 | return updater.update() | 107 | return updater.update() |
106 | } | 108 | } |
107 | 109 | ||
108 | async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { | 110 | async function processUpdatePlaylist ( |
109 | const playlistObject = activity.object as PlaylistObject | 111 | byActor: MActorSignature, |
112 | activity: ActivityUpdate<PlaylistObject | string>, | ||
113 | playlistObject: PlaylistObject | ||
114 | ) { | ||
110 | const byAccount = byActor.Account | 115 | const byAccount = byActor.Account |
111 | |||
112 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | 116 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) |
113 | 117 | ||
114 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) | 118 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 0e996ab80..2cd4db14d 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -1,6 +1,14 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { |
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityCreateObject, | ||
7 | ContextType, | ||
8 | VideoCommentObject, | ||
9 | VideoPlaylistPrivacy, | ||
10 | VideoPrivacy | ||
11 | } from '@shared/models' | ||
4 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 12 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
5 | import { VideoCommentModel } from '../../../models/video/video-comment' | 13 | import { VideoCommentModel } from '../../../models/video/video-comment' |
6 | import { | 14 | import { |
@@ -107,7 +115,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: | |||
107 | 115 | ||
108 | const byActor = comment.Account.Actor | 116 | const byActor = comment.Account.Actor |
109 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) | 117 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) |
110 | const commentObject = comment.toActivityPubObject(threadParentComments) | 118 | const commentObject = comment.toActivityPubObject(threadParentComments) as VideoCommentObject |
111 | 119 | ||
112 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) | 120 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) |
113 | // Add the actor that commented too | 121 | // Add the actor that commented too |
@@ -168,7 +176,12 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: | |||
168 | }) | 176 | }) |
169 | } | 177 | } |
170 | 178 | ||
171 | function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { | 179 | function buildCreateActivity <T extends ActivityCreateObject> ( |
180 | url: string, | ||
181 | byActor: MActorLight, | ||
182 | object: T, | ||
183 | audience?: ActivityAudience | ||
184 | ): ActivityCreate<T> { | ||
172 | if (!audience) audience = getAudience(byActor) | 185 | if (!audience) audience = getAudience(byActor) |
173 | 186 | ||
174 | return audiencify( | 187 | return audiencify( |
@@ -176,7 +189,9 @@ function buildCreateActivity (url: string, byActor: MActorLight, object: any, au | |||
176 | type: 'Create' as 'Create', | 189 | type: 'Create' as 'Create', |
177 | id: url + '/activity', | 190 | id: url + '/activity', |
178 | actor: byActor.url, | 191 | actor: byActor.url, |
179 | object: audiencify(object, audience) | 192 | object: typeof object === 'string' |
193 | ? object | ||
194 | : audiencify(object, audience) | ||
180 | }, | 195 | }, |
181 | audience | 196 | audience |
182 | ) | 197 | ) |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index b8eb47ff6..b0b48c9c4 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -1,14 +1,5 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { | 2 | import { ActivityAudience, ActivityDislike, ActivityLike, ActivityUndo, ActivityUndoObject, ContextType } from '@shared/models' |
3 | ActivityAnnounce, | ||
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityDislike, | ||
7 | ActivityFollow, | ||
8 | ActivityLike, | ||
9 | ActivityUndo, | ||
10 | ContextType | ||
11 | } from '@shared/models' | ||
12 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
13 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
14 | import { | 5 | import { |
@@ -128,12 +119,12 @@ export { | |||
128 | 119 | ||
129 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
130 | 121 | ||
131 | function undoActivityData ( | 122 | function undoActivityData <T extends ActivityUndoObject> ( |
132 | url: string, | 123 | url: string, |
133 | byActor: MActorAudience, | 124 | byActor: MActorAudience, |
134 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, | 125 | object: T, |
135 | audience?: ActivityAudience | 126 | audience?: ActivityAudience |
136 | ): ActivityUndo { | 127 | ): ActivityUndo<T> { |
137 | if (!audience) audience = getAudience(byActor) | 128 | if (!audience) audience = getAudience(byActor) |
138 | 129 | ||
139 | return audiencify( | 130 | return audiencify( |
@@ -151,7 +142,7 @@ async function sendUndoVideoRelatedActivity (options: { | |||
151 | byActor: MActor | 142 | byActor: MActor |
152 | video: MVideoAccountLight | 143 | video: MVideoAccountLight |
153 | url: string | 144 | url: string |
154 | activity: ActivityFollow | ActivityCreate | ActivityAnnounce | 145 | activity: ActivityUndoObject |
155 | contextType: ContextType | 146 | contextType: ContextType |
156 | transaction: Transaction | 147 | transaction: Transaction |
157 | }) { | 148 | }) { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 379e2d9d8..f3fb741c6 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { AccountModel } from '../../../models/account/account' | 5 | import { AccountModel } from '../../../models/account/account' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
@@ -10,8 +10,7 @@ import { | |||
10 | MActor, | 10 | MActor, |
11 | MActorLight, | 11 | MActorLight, |
12 | MChannelDefault, | 12 | MChannelDefault, |
13 | MVideoAP, | 13 | MVideoAPLight, |
14 | MVideoAPWithoutCaption, | ||
15 | MVideoPlaylistFull, | 14 | MVideoPlaylistFull, |
16 | MVideoRedundancyVideo | 15 | MVideoRedundancyVideo |
17 | } from '../../../types/models' | 16 | } from '../../../types/models' |
@@ -20,10 +19,10 @@ import { getUpdateActivityPubUrl } from '../url' | |||
20 | import { getActorsInvolvedInVideo } from './shared' | 19 | import { getActorsInvolvedInVideo } from './shared' |
21 | import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils' | 20 | import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils' |
22 | 21 | ||
23 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: Transaction, overriddenByActor?: MActor) { | 22 | async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) { |
24 | const video = videoArg as MVideoAP | 23 | if (!videoArg.hasPrivacyForFederation()) return undefined |
25 | 24 | ||
26 | if (!video.hasPrivacyForFederation()) return undefined | 25 | const video = await videoArg.lightAPToFullAP(transaction) |
27 | 26 | ||
28 | logger.info('Creating job to update video %s.', video.url) | 27 | logger.info('Creating job to update video %s.', video.url) |
29 | 28 | ||
@@ -31,11 +30,6 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: T | |||
31 | 30 | ||
32 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) | 31 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) |
33 | 32 | ||
34 | // Needed to build the AP object | ||
35 | if (!video.VideoCaptions) { | ||
36 | video.VideoCaptions = await video.$get('VideoCaptions', { transaction }) | ||
37 | } | ||
38 | |||
39 | const videoObject = await video.toActivityPubObject() | 33 | const videoObject = await video.toActivityPubObject() |
40 | const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) | 34 | const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) |
41 | 35 | ||
@@ -143,7 +137,12 @@ export { | |||
143 | 137 | ||
144 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
145 | 139 | ||
146 | function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate { | 140 | function buildUpdateActivity ( |
141 | url: string, | ||
142 | byActor: MActorLight, | ||
143 | object: ActivityUpdateObject, | ||
144 | audience?: ActivityAudience | ||
145 | ): ActivityUpdate<ActivityUpdateObject> { | ||
147 | if (!audience) audience = getAudience(byActor) | 146 | if (!audience) audience = getAudience(byActor) |
148 | 147 | ||
149 | return audiencify( | 148 | return audiencify( |
diff --git a/server/lib/activitypub/videos/federate.ts b/server/lib/activitypub/videos/federate.ts index bd0c54b0c..d7e251153 100644 --- a/server/lib/activitypub/videos/federate.ts +++ b/server/lib/activitypub/videos/federate.ts | |||
@@ -1,10 +1,9 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { isArray } from '@server/helpers/custom-validators/misc' | 2 | import { MVideoAP, MVideoAPLight } from '@server/types/models' |
3 | import { MVideoAP, MVideoAPWithoutCaption } from '@server/types/models' | ||
4 | import { sendCreateVideo, sendUpdateVideo } from '../send' | 3 | import { sendCreateVideo, sendUpdateVideo } from '../send' |
5 | import { shareVideoByServerAndChannel } from '../share' | 4 | import { shareVideoByServerAndChannel } from '../share' |
6 | 5 | ||
7 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) { | 6 | async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) { |
8 | const video = videoArg as MVideoAP | 7 | const video = videoArg as MVideoAP |
9 | 8 | ||
10 | if ( | 9 | if ( |
@@ -13,13 +12,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
13 | // Check the video is public/unlisted and published | 12 | // Check the video is public/unlisted and published |
14 | video.hasPrivacyForFederation() && video.hasStateForFederation() | 13 | video.hasPrivacyForFederation() && video.hasStateForFederation() |
15 | ) { | 14 | ) { |
16 | // Fetch more attributes that we will need to serialize in AP object | 15 | const video = await videoArg.lightAPToFullAP(transaction) |
17 | if (isArray(video.VideoCaptions) === false) { | ||
18 | video.VideoCaptions = await video.$get('VideoCaptions', { | ||
19 | attributes: [ 'filename', 'language' ], | ||
20 | transaction | ||
21 | }) | ||
22 | } | ||
23 | 16 | ||
24 | if (isNewVideo) { | 17 | if (isNewVideo) { |
25 | // Now we'll add the video's meta data to our followers | 18 | // Now we'll add the video's meta data to our followers |
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts index 14ba55034..92387c5d4 100644 --- a/server/lib/activitypub/videos/get.ts +++ b/server/lib/activitypub/videos/get.ts | |||
@@ -3,7 +3,7 @@ import { logger } from '@server/helpers/logger' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' | 5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' |
6 | import { APObject } from '@shared/models' | 6 | import { APObjectId } from '@shared/models' |
7 | import { getAPId } from '../activity' | 7 | import { getAPId } from '../activity' |
8 | import { refreshVideoIfNeeded } from './refresh' | 8 | import { refreshVideoIfNeeded } from './refresh' |
9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | 9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' |
@@ -15,21 +15,21 @@ type GetVideoResult <T> = Promise<{ | |||
15 | }> | 15 | }> |
16 | 16 | ||
17 | type GetVideoParamAll = { | 17 | type GetVideoParamAll = { |
18 | videoObject: APObject | 18 | videoObject: APObjectId |
19 | syncParam?: SyncParam | 19 | syncParam?: SyncParam |
20 | fetchType?: 'all' | 20 | fetchType?: 'all' |
21 | allowRefresh?: boolean | 21 | allowRefresh?: boolean |
22 | } | 22 | } |
23 | 23 | ||
24 | type GetVideoParamImmutable = { | 24 | type GetVideoParamImmutable = { |
25 | videoObject: APObject | 25 | videoObject: APObjectId |
26 | syncParam?: SyncParam | 26 | syncParam?: SyncParam |
27 | fetchType: 'only-immutable-attributes' | 27 | fetchType: 'only-immutable-attributes' |
28 | allowRefresh: false | 28 | allowRefresh: false |
29 | } | 29 | } |
30 | 30 | ||
31 | type GetVideoParamOther = { | 31 | type GetVideoParamOther = { |
32 | videoObject: APObject | 32 | videoObject: APObjectId |
33 | syncParam?: SyncParam | 33 | syncParam?: SyncParam |
34 | fetchType?: 'all' | 'only-video' | 34 | fetchType?: 'all' | 'only-video' |
35 | allowRefresh?: boolean | 35 | allowRefresh?: boolean |
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts index c0b92c93d..98c2f58eb 100644 --- a/server/lib/activitypub/videos/shared/abstract-builder.ts +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { CreationAttributes, Transaction } from 'sequelize/types' | 1 | import { CreationAttributes, Transaction } from 'sequelize/types' |
2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' | 2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 4 | import { updateRemoteVideoThumbnail } from '@server/lib/thumbnail' |
5 | import { setVideoTags } from '@server/lib/video' | 5 | import { setVideoTags } from '@server/lib/video' |
6 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
6 | import { VideoCaptionModel } from '@server/models/video/video-caption' | 7 | import { VideoCaptionModel } from '@server/models/video/video-caption' |
7 | import { VideoFileModel } from '@server/models/video/video-file' | 8 | import { VideoFileModel } from '@server/models/video/video-file' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 9 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -10,20 +11,19 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
10 | import { | 11 | import { |
11 | MStreamingPlaylistFiles, | 12 | MStreamingPlaylistFiles, |
12 | MStreamingPlaylistFilesVideo, | 13 | MStreamingPlaylistFilesVideo, |
13 | MThumbnail, | ||
14 | MVideoCaption, | 14 | MVideoCaption, |
15 | MVideoFile, | 15 | MVideoFile, |
16 | MVideoFullLight, | 16 | MVideoFullLight, |
17 | MVideoThumbnail | 17 | MVideoThumbnail |
18 | } from '@server/types/models' | 18 | } from '@server/types/models' |
19 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' | 19 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' |
20 | import { getOrCreateAPActor } from '../../actors' | 20 | import { findOwner, getOrCreateAPActor } from '../../actors' |
21 | import { checkUrlsSameHost } from '../../url' | ||
22 | import { | 21 | import { |
23 | getCaptionAttributesFromObject, | 22 | getCaptionAttributesFromObject, |
24 | getFileAttributesFromUrl, | 23 | getFileAttributesFromUrl, |
25 | getLiveAttributesFromObject, | 24 | getLiveAttributesFromObject, |
26 | getPreviewFromIcons, | 25 | getPreviewFromIcons, |
26 | getStoryboardAttributeFromObject, | ||
27 | getStreamingPlaylistAttributesFromObject, | 27 | getStreamingPlaylistAttributesFromObject, |
28 | getTagsFromObject, | 28 | getTagsFromObject, |
29 | getThumbnailFromIcons | 29 | getThumbnailFromIcons |
@@ -35,38 +35,40 @@ export abstract class APVideoAbstractBuilder { | |||
35 | protected abstract lTags: LoggerTagsFn | 35 | protected abstract lTags: LoggerTagsFn |
36 | 36 | ||
37 | protected async getOrCreateVideoChannelFromVideoObject () { | 37 | protected async getOrCreateVideoChannelFromVideoObject () { |
38 | const channel = this.videoObject.attributedTo.find(a => a.type === 'Group') | 38 | const channel = await findOwner(this.videoObject.id, this.videoObject.attributedTo, 'Group') |
39 | if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url) | 39 | if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url) |
40 | 40 | ||
41 | if (checkUrlsSameHost(channel.id, this.videoObject.id) !== true) { | ||
42 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`) | ||
43 | } | ||
44 | |||
45 | return getOrCreateAPActor(channel.id, 'all') | 41 | return getOrCreateAPActor(channel.id, 'all') |
46 | } | 42 | } |
47 | 43 | ||
48 | protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> { | 44 | protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) { |
49 | return updateVideoMiniatureFromUrl({ | 45 | const miniatureIcon = getThumbnailFromIcons(this.videoObject) |
50 | downloadUrl: getThumbnailFromIcons(this.videoObject).url, | 46 | if (!miniatureIcon) { |
51 | video, | 47 | logger.warn('Cannot find thumbnail in video object', { object: this.videoObject }) |
52 | type: ThumbnailType.MINIATURE | ||
53 | }).catch(err => { | ||
54 | logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() }) | ||
55 | |||
56 | return undefined | 48 | return undefined |
49 | } | ||
50 | |||
51 | const miniatureModel = updateRemoteVideoThumbnail({ | ||
52 | fileUrl: miniatureIcon.url, | ||
53 | video, | ||
54 | type: ThumbnailType.MINIATURE, | ||
55 | size: miniatureIcon, | ||
56 | onDisk: false // Lazy download remote thumbnails | ||
57 | }) | 57 | }) |
58 | |||
59 | await video.addAndSaveThumbnail(miniatureModel, t) | ||
58 | } | 60 | } |
59 | 61 | ||
60 | protected async setPreview (video: MVideoFullLight, t?: Transaction) { | 62 | protected async setPreview (video: MVideoFullLight, t?: Transaction) { |
61 | // Don't fetch the preview that could be big, create a placeholder instead | ||
62 | const previewIcon = getPreviewFromIcons(this.videoObject) | 63 | const previewIcon = getPreviewFromIcons(this.videoObject) |
63 | if (!previewIcon) return | 64 | if (!previewIcon) return |
64 | 65 | ||
65 | const previewModel = updatePlaceholderThumbnail({ | 66 | const previewModel = updateRemoteVideoThumbnail({ |
66 | fileUrl: previewIcon.url, | 67 | fileUrl: previewIcon.url, |
67 | video, | 68 | video, |
68 | type: ThumbnailType.PREVIEW, | 69 | type: ThumbnailType.PREVIEW, |
69 | size: previewIcon | 70 | size: previewIcon, |
71 | onDisk: false // Lazy download remote previews | ||
70 | }) | 72 | }) |
71 | 73 | ||
72 | await video.addAndSaveThumbnail(previewModel, t) | 74 | await video.addAndSaveThumbnail(previewModel, t) |
@@ -107,6 +109,16 @@ export abstract class APVideoAbstractBuilder { | |||
107 | } | 109 | } |
108 | } | 110 | } |
109 | 111 | ||
112 | protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) { | ||
113 | const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t) | ||
114 | if (existingStoryboard) await existingStoryboard.destroy({ transaction: t }) | ||
115 | |||
116 | const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject) | ||
117 | if (!storyboardAttributes) return | ||
118 | |||
119 | return StoryboardModel.create(storyboardAttributes, { transaction: t }) | ||
120 | } | ||
121 | |||
110 | protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) { | 122 | protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) { |
111 | const attributes = getLiveAttributesFromObject(video, this.videoObject) | 123 | const attributes = getLiveAttributesFromObject(video, this.videoObject) |
112 | const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true }) | 124 | const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true }) |
@@ -114,7 +126,7 @@ export abstract class APVideoAbstractBuilder { | |||
114 | video.VideoLive = videoLive | 126 | video.VideoLive = videoLive |
115 | } | 127 | } |
116 | 128 | ||
117 | protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) { | 129 | protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) { |
118 | const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url) | 130 | const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url) |
119 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 131 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
120 | 132 | ||
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts index 77321d8a5..bc139e4fa 100644 --- a/server/lib/activitypub/videos/shared/creator.ts +++ b/server/lib/activitypub/videos/shared/creator.ts | |||
@@ -4,7 +4,7 @@ import { sequelizeTypescript } from '@server/initializers/database' | |||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | 5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | 7 | import { MVideoFullLight, MVideoThumbnail } from '@server/types/models' |
8 | import { VideoObject } from '@shared/models' | 8 | import { VideoObject } from '@shared/models' |
9 | import { APVideoAbstractBuilder } from './abstract-builder' | 9 | import { APVideoAbstractBuilder } from './abstract-builder' |
10 | import { getVideoAttributesFromObject } from './object-to-model-attributes' | 10 | import { getVideoAttributesFromObject } from './object-to-model-attributes' |
@@ -27,64 +27,38 @@ export class APVideoCreator extends APVideoAbstractBuilder { | |||
27 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) | 27 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) |
28 | const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail | 28 | const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail |
29 | 29 | ||
30 | const promiseThumbnail = this.tryToGenerateThumbnail(video) | ||
31 | |||
32 | let thumbnailModel: MThumbnail | ||
33 | if (waitThumbnail === true) { | ||
34 | thumbnailModel = await promiseThumbnail | ||
35 | } | ||
36 | |||
37 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { | 30 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { |
38 | try { | 31 | const videoCreated = await video.save({ transaction: t }) as MVideoFullLight |
39 | const videoCreated = await video.save({ transaction: t }) as MVideoFullLight | 32 | videoCreated.VideoChannel = channel |
40 | videoCreated.VideoChannel = channel | 33 | |
41 | 34 | await this.setThumbnail(videoCreated, t) | |
42 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 35 | await this.setPreview(videoCreated, t) |
43 | 36 | await this.setWebVideoFiles(videoCreated, t) | |
44 | await this.setPreview(videoCreated, t) | 37 | await this.setStreamingPlaylists(videoCreated, t) |
45 | await this.setWebTorrentFiles(videoCreated, t) | 38 | await this.setTags(videoCreated, t) |
46 | await this.setStreamingPlaylists(videoCreated, t) | 39 | await this.setTrackers(videoCreated, t) |
47 | await this.setTags(videoCreated, t) | 40 | await this.insertOrReplaceCaptions(videoCreated, t) |
48 | await this.setTrackers(videoCreated, t) | 41 | await this.insertOrReplaceLive(videoCreated, t) |
49 | await this.insertOrReplaceCaptions(videoCreated, t) | 42 | await this.insertOrReplaceStoryboard(videoCreated, t) |
50 | await this.insertOrReplaceLive(videoCreated, t) | 43 | |
51 | 44 | // We added a video in this channel, set it as updated | |
52 | // We added a video in this channel, set it as updated | 45 | await channel.setAsUpdated(t) |
53 | await channel.setAsUpdated(t) | 46 | |
54 | 47 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | |
55 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | 48 | video: videoCreated, |
56 | video: videoCreated, | 49 | user: undefined, |
57 | user: undefined, | 50 | isRemote: true, |
58 | isRemote: true, | 51 | isNew: true, |
59 | isNew: true, | 52 | transaction: t |
60 | transaction: t | 53 | }) |
61 | }) | ||
62 | |||
63 | logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) | ||
64 | 54 | ||
65 | Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) | 55 | logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) |
66 | 56 | ||
67 | return { autoBlacklisted, videoCreated } | 57 | Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) |
68 | } catch (err) { | ||
69 | // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | ||
70 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
71 | 58 | ||
72 | throw err | 59 | return { autoBlacklisted, videoCreated } |
73 | } | ||
74 | }) | 60 | }) |
75 | 61 | ||
76 | if (waitThumbnail === false) { | ||
77 | // Error is already caught above | ||
78 | // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
79 | promiseThumbnail.then(thumbnailModel => { | ||
80 | if (!thumbnailModel) return | ||
81 | |||
82 | thumbnailModel = videoCreated.id | ||
83 | |||
84 | return thumbnailModel.save() | ||
85 | }) | ||
86 | } | ||
87 | |||
88 | return { autoBlacklisted, videoCreated } | 62 | return { autoBlacklisted, videoCreated } |
89 | } | 63 | } |
90 | } | 64 | } |
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts index 8fd0a816c..a9e0bed97 100644 --- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { maxBy, minBy } from 'lodash' | 1 | import { maxBy, minBy } from 'lodash' |
2 | import { decode as magnetUriDecode } from 'magnet-uri' | 2 | import { decode as magnetUriDecode } from 'magnet-uri' |
3 | import { basename } from 'path' | 3 | import { basename, extname } from 'path' |
4 | import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos' | 4 | import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos' |
5 | import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos' | 5 | import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos' |
6 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
@@ -25,6 +25,9 @@ import { | |||
25 | VideoStreamingPlaylistType | 25 | VideoStreamingPlaylistType |
26 | } from '@shared/models' | 26 | } from '@shared/models' |
27 | import { getDurationFromActivityStream } from '../../activity' | 27 | import { getDurationFromActivityStream } from '../../activity' |
28 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
29 | import { generateImageFilename } from '@server/helpers/image-utils' | ||
30 | import { arrayify } from '@shared/core-utils' | ||
28 | 31 | ||
29 | function getThumbnailFromIcons (videoObject: VideoObject) { | 32 | function getThumbnailFromIcons (videoObject: VideoObject) { |
30 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | 33 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) |
@@ -166,6 +169,26 @@ function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObje | |||
166 | })) | 169 | })) |
167 | } | 170 | } |
168 | 171 | ||
172 | function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) { | ||
173 | if (!isArray(videoObject.preview)) return undefined | ||
174 | |||
175 | const storyboard = videoObject.preview.find(p => p.rel.includes('storyboard')) | ||
176 | if (!storyboard) return undefined | ||
177 | |||
178 | const url = arrayify(storyboard.url).find(u => u.mediaType === 'image/jpeg') | ||
179 | |||
180 | return { | ||
181 | filename: generateImageFilename(extname(url.href)), | ||
182 | totalHeight: url.height, | ||
183 | totalWidth: url.width, | ||
184 | spriteHeight: url.tileHeight, | ||
185 | spriteWidth: url.tileWidth, | ||
186 | spriteDuration: getDurationFromActivityStream(url.tileDuration), | ||
187 | fileUrl: url.href, | ||
188 | videoId: video.id | ||
189 | } | ||
190 | } | ||
191 | |||
169 | function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { | 192 | function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { |
170 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | 193 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
171 | ? VideoPrivacy.PUBLIC | 194 | ? VideoPrivacy.PUBLIC |
@@ -228,6 +251,7 @@ export { | |||
228 | 251 | ||
229 | getLiveAttributesFromObject, | 252 | getLiveAttributesFromObject, |
230 | getCaptionAttributesFromObject, | 253 | getCaptionAttributesFromObject, |
254 | getStoryboardAttributeFromObject, | ||
231 | 255 | ||
232 | getVideoAttributesFromObject | 256 | getVideoAttributesFromObject |
233 | } | 257 | } |
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts index 6ddd2301b..522d7b043 100644 --- a/server/lib/activitypub/videos/updater.ts +++ b/server/lib/activitypub/videos/updater.ts | |||
@@ -41,7 +41,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
41 | try { | 41 | try { |
42 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() | 42 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() |
43 | 43 | ||
44 | const thumbnailModel = await this.tryToGenerateThumbnail(this.video) | 44 | const thumbnailModel = await this.setThumbnail(this.video) |
45 | 45 | ||
46 | this.checkChannelUpdateOrThrow(channelActor) | 46 | this.checkChannelUpdateOrThrow(channelActor) |
47 | 47 | ||
@@ -50,15 +50,21 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
50 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel) | 50 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel) |
51 | 51 | ||
52 | await runInReadCommittedTransaction(async t => { | 52 | await runInReadCommittedTransaction(async t => { |
53 | await this.setWebTorrentFiles(videoUpdated, t) | 53 | await this.setWebVideoFiles(videoUpdated, t) |
54 | await this.setStreamingPlaylists(videoUpdated, t) | 54 | await this.setStreamingPlaylists(videoUpdated, t) |
55 | }) | 55 | }) |
56 | 56 | ||
57 | await Promise.all([ | 57 | await Promise.all([ |
58 | runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), | 58 | runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), |
59 | runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), | 59 | runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), |
60 | this.setOrDeleteLive(videoUpdated), | 60 | runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)), |
61 | this.setPreview(videoUpdated) | 61 | runInReadCommittedTransaction(t => { |
62 | return Promise.all([ | ||
63 | this.setPreview(videoUpdated, t), | ||
64 | this.setThumbnail(videoUpdated, t) | ||
65 | ]) | ||
66 | }), | ||
67 | this.setOrDeleteLive(videoUpdated) | ||
62 | ]) | 68 | ]) |
63 | 69 | ||
64 | await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) | 70 | await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) |
@@ -138,6 +144,10 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
138 | await this.insertOrReplaceCaptions(videoUpdated, t) | 144 | await this.insertOrReplaceCaptions(videoUpdated, t) |
139 | } | 145 | } |
140 | 146 | ||
147 | private async setStoryboard (videoUpdated: MVideoFullLight, t: Transaction) { | ||
148 | await this.insertOrReplaceStoryboard(videoUpdated, t) | ||
149 | } | ||
150 | |||
141 | private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) { | 151 | private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) { |
142 | if (!this.video.isLive) return | 152 | if (!this.video.isLive) return |
143 | 153 | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 18b16bee1..be6df1792 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -32,6 +32,7 @@ import { getActivityStreamDuration } from './activitypub/activity' | |||
32 | import { getBiggestActorImage } from './actor-image' | 32 | import { getBiggestActorImage } from './actor-image' |
33 | import { Hooks } from './plugins/hooks' | 33 | import { Hooks } from './plugins/hooks' |
34 | import { ServerConfigManager } from './server-config-manager' | 34 | import { ServerConfigManager } from './server-config-manager' |
35 | import { isVideoInPrivateDirectory } from './video-privacy' | ||
35 | 36 | ||
36 | type Tags = { | 37 | type Tags = { |
37 | ogType: string | 38 | ogType: string |
@@ -106,7 +107,7 @@ class ClientHtml { | |||
106 | ]) | 107 | ]) |
107 | 108 | ||
108 | // Let Angular application handle errors | 109 | // Let Angular application handle errors |
109 | if (!video || video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL || video.VideoBlacklist) { | 110 | if (!video || isVideoInPrivateDirectory(video.privacy) || video.VideoBlacklist) { |
110 | res.status(HttpStatusCode.NOT_FOUND_404) | 111 | res.status(HttpStatusCode.NOT_FOUND_404) |
111 | return html | 112 | return html |
112 | } | 113 | } |
diff --git a/server/lib/files-cache/avatar-permanent-file-cache.ts b/server/lib/files-cache/avatar-permanent-file-cache.ts new file mode 100644 index 000000000..0c508b063 --- /dev/null +++ b/server/lib/files-cache/avatar-permanent-file-cache.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
3 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
4 | import { MActorImage } from '@server/types/models' | ||
5 | import { AbstractPermanentFileCache } from './shared' | ||
6 | |||
7 | export class AvatarPermanentFileCache extends AbstractPermanentFileCache<MActorImage> { | ||
8 | |||
9 | constructor () { | ||
10 | super(CONFIG.STORAGE.ACTOR_IMAGES_DIR) | ||
11 | } | ||
12 | |||
13 | protected loadModel (filename: string) { | ||
14 | return ActorImageModel.loadByName(filename) | ||
15 | } | ||
16 | |||
17 | protected getImageSize (image: MActorImage): { width: number, height: number } { | ||
18 | if (image.width && image.height) { | ||
19 | return { | ||
20 | height: image.height, | ||
21 | width: image.width | ||
22 | } | ||
23 | } | ||
24 | |||
25 | return ACTOR_IMAGES_SIZE[image.type][0] | ||
26 | } | ||
27 | } | ||
diff --git a/server/lib/files-cache/index.ts b/server/lib/files-cache/index.ts index e5853f7d6..5630a9b80 100644 --- a/server/lib/files-cache/index.ts +++ b/server/lib/files-cache/index.ts | |||
@@ -1,3 +1,6 @@ | |||
1 | export * from './videos-preview-cache' | 1 | export * from './avatar-permanent-file-cache' |
2 | export * from './videos-caption-cache' | 2 | export * from './video-miniature-permanent-file-cache' |
3 | export * from './videos-torrent-cache' | 3 | export * from './video-captions-simple-file-cache' |
4 | export * from './video-previews-simple-file-cache' | ||
5 | export * from './video-storyboards-simple-file-cache' | ||
6 | export * from './video-torrents-simple-file-cache' | ||
diff --git a/server/lib/files-cache/shared/abstract-permanent-file-cache.ts b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts new file mode 100644 index 000000000..f990e9872 --- /dev/null +++ b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts | |||
@@ -0,0 +1,132 @@ | |||
1 | import express from 'express' | ||
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { Model } from 'sequelize' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { CachePromise } from '@server/helpers/promise-cache' | ||
6 | import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants' | ||
7 | import { downloadImageFromWorker } from '@server/lib/worker/parent-process' | ||
8 | import { HttpStatusCode } from '@shared/models' | ||
9 | |||
10 | type ImageModel = { | ||
11 | fileUrl: string | ||
12 | filename: string | ||
13 | onDisk: boolean | ||
14 | |||
15 | isOwned (): boolean | ||
16 | getPath (): string | ||
17 | |||
18 | save (): Promise<Model> | ||
19 | } | ||
20 | |||
21 | export abstract class AbstractPermanentFileCache <M extends ImageModel> { | ||
22 | // Unsafe because it can return paths that do not exist anymore | ||
23 | private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({ | ||
24 | max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE | ||
25 | }) | ||
26 | |||
27 | protected abstract getImageSize (image: M): { width: number, height: number } | ||
28 | protected abstract loadModel (filename: string): Promise<M> | ||
29 | |||
30 | constructor (private readonly directory: string) { | ||
31 | |||
32 | } | ||
33 | |||
34 | async lazyServe (options: { | ||
35 | filename: string | ||
36 | res: express.Response | ||
37 | next: express.NextFunction | ||
38 | }) { | ||
39 | const { filename, res, next } = options | ||
40 | |||
41 | if (this.filenameToPathUnsafeCache.has(filename)) { | ||
42 | return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | ||
43 | } | ||
44 | |||
45 | const image = await this.lazyLoadIfNeeded(filename) | ||
46 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
47 | |||
48 | const path = image.getPath() | ||
49 | this.filenameToPathUnsafeCache.set(filename, path) | ||
50 | |||
51 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
52 | if (!err) return | ||
53 | |||
54 | this.onServeError({ err, image, next, filename }) | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | @CachePromise({ | ||
59 | keyBuilder: filename => filename | ||
60 | }) | ||
61 | private async lazyLoadIfNeeded (filename: string) { | ||
62 | const image = await this.loadModel(filename) | ||
63 | if (!image) return undefined | ||
64 | |||
65 | if (image.onDisk === false) { | ||
66 | if (!image.fileUrl) return undefined | ||
67 | |||
68 | try { | ||
69 | await this.downloadRemoteFile(image) | ||
70 | } catch (err) { | ||
71 | logger.warn('Cannot process remote image %s.', image.fileUrl, { err }) | ||
72 | |||
73 | return undefined | ||
74 | } | ||
75 | } | ||
76 | |||
77 | return image | ||
78 | } | ||
79 | |||
80 | async downloadRemoteFile (image: M) { | ||
81 | logger.info('Download remote image %s lazily.', image.fileUrl) | ||
82 | |||
83 | const destination = await this.downloadImage({ | ||
84 | filename: image.filename, | ||
85 | fileUrl: image.fileUrl, | ||
86 | size: this.getImageSize(image) | ||
87 | }) | ||
88 | |||
89 | image.onDisk = true | ||
90 | image.save() | ||
91 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
92 | |||
93 | return destination | ||
94 | } | ||
95 | |||
96 | private onServeError (options: { | ||
97 | err: any | ||
98 | image: M | ||
99 | filename: string | ||
100 | next: express.NextFunction | ||
101 | }) { | ||
102 | const { err, image, filename, next } = options | ||
103 | |||
104 | // It seems this actor image is not on the disk anymore | ||
105 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
106 | logger.error('Cannot lazy serve image %s.', filename, { err }) | ||
107 | |||
108 | this.filenameToPathUnsafeCache.delete(filename) | ||
109 | |||
110 | image.onDisk = false | ||
111 | image.save() | ||
112 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
113 | } | ||
114 | |||
115 | return next(err) | ||
116 | } | ||
117 | |||
118 | private downloadImage (options: { | ||
119 | fileUrl: string | ||
120 | filename: string | ||
121 | size: { width: number, height: number } | ||
122 | }) { | ||
123 | const downloaderOptions = { | ||
124 | url: options.fileUrl, | ||
125 | destDir: this.directory, | ||
126 | destName: options.filename, | ||
127 | size: options.size | ||
128 | } | ||
129 | |||
130 | return downloadImageFromWorker(downloaderOptions) | ||
131 | } | ||
132 | } | ||
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/shared/abstract-simple-file-cache.ts index a7ac88525..6fab322cd 100644 --- a/server/lib/files-cache/abstract-video-static-file-cache.ts +++ b/server/lib/files-cache/shared/abstract-simple-file-cache.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import memoizee from 'memoizee' | 3 | import memoizee from 'memoizee' |
4 | 4 | ||
5 | type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined | 5 | type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined |
6 | 6 | ||
7 | export abstract class AbstractVideoStaticFileCache <T> { | 7 | export abstract class AbstractSimpleFileCache <T> { |
8 | 8 | ||
9 | getFilePath: (params: T) => Promise<GetFilePathResult> | 9 | getFilePath: (params: T) => Promise<GetFilePathResult> |
10 | 10 | ||
diff --git a/server/lib/files-cache/shared/index.ts b/server/lib/files-cache/shared/index.ts new file mode 100644 index 000000000..61c4aacc7 --- /dev/null +++ b/server/lib/files-cache/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './abstract-permanent-file-cache' | ||
2 | export * from './abstract-simple-file-cache' | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/video-captions-simple-file-cache.ts index d21acf4ef..cbeeff732 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/video-captions-simple-file-cache.ts | |||
@@ -5,11 +5,11 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { VideoCaptionModel } from '../../models/video/video-caption' | 7 | import { VideoCaptionModel } from '../../models/video/video-caption' |
8 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 8 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
9 | 9 | ||
10 | class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache <string> { |
11 | 11 | ||
12 | private static instance: VideosCaptionCache | 12 | private static instance: VideoCaptionsSimpleFileCache |
13 | 13 | ||
14 | private constructor () { | 14 | private constructor () { |
15 | super() | 15 | super() |
@@ -23,7 +23,9 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
23 | const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) | 23 | const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) |
24 | if (!videoCaption) return undefined | 24 | if (!videoCaption) return undefined |
25 | 25 | ||
26 | if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } | 26 | if (videoCaption.isOwned()) { |
27 | return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } | ||
28 | } | ||
27 | 29 | ||
28 | return this.loadRemoteFile(filename) | 30 | return this.loadRemoteFile(filename) |
29 | } | 31 | } |
@@ -55,5 +57,5 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
55 | } | 57 | } |
56 | 58 | ||
57 | export { | 59 | export { |
58 | VideosCaptionCache | 60 | VideoCaptionsSimpleFileCache |
59 | } | 61 | } |
diff --git a/server/lib/files-cache/video-miniature-permanent-file-cache.ts b/server/lib/files-cache/video-miniature-permanent-file-cache.ts new file mode 100644 index 000000000..35d9466f7 --- /dev/null +++ b/server/lib/files-cache/video-miniature-permanent-file-cache.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { THUMBNAILS_SIZE } from '@server/initializers/constants' | ||
3 | import { ThumbnailModel } from '@server/models/video/thumbnail' | ||
4 | import { MThumbnail } from '@server/types/models' | ||
5 | import { ThumbnailType } from '@shared/models' | ||
6 | import { AbstractPermanentFileCache } from './shared' | ||
7 | |||
8 | export class VideoMiniaturePermanentFileCache extends AbstractPermanentFileCache<MThumbnail> { | ||
9 | |||
10 | constructor () { | ||
11 | super(CONFIG.STORAGE.THUMBNAILS_DIR) | ||
12 | } | ||
13 | |||
14 | protected loadModel (filename: string) { | ||
15 | return ThumbnailModel.loadByFilename(filename, ThumbnailType.MINIATURE) | ||
16 | } | ||
17 | |||
18 | protected getImageSize (image: MThumbnail): { width: number, height: number } { | ||
19 | if (image.width && image.height) { | ||
20 | return { | ||
21 | height: image.height, | ||
22 | width: image.width | ||
23 | } | ||
24 | } | ||
25 | |||
26 | return THUMBNAILS_SIZE | ||
27 | } | ||
28 | } | ||
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/video-previews-simple-file-cache.ts index d19c3f4f4..a05e80e16 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/video-previews-simple-file-cache.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FILES_CACHE } from '../../initializers/constants' | 2 | import { FILES_CACHE } from '../../initializers/constants' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 4 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
5 | import { doRequestAndSaveToFile } from '@server/helpers/requests' | 5 | import { doRequestAndSaveToFile } from '@server/helpers/requests' |
6 | import { ThumbnailModel } from '@server/models/video/thumbnail' | 6 | import { ThumbnailModel } from '@server/models/video/thumbnail' |
7 | import { ThumbnailType } from '@shared/models' | 7 | import { ThumbnailType } from '@shared/models' |
8 | import { logger } from '@server/helpers/logger' | 8 | import { logger } from '@server/helpers/logger' |
9 | 9 | ||
10 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> { |
11 | 11 | ||
12 | private static instance: VideosPreviewCache | 12 | private static instance: VideoPreviewsSimpleFileCache |
13 | 13 | ||
14 | private constructor () { | 14 | private constructor () { |
15 | super() | 15 | super() |
@@ -54,5 +54,5 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
54 | } | 54 | } |
55 | 55 | ||
56 | export { | 56 | export { |
57 | VideosPreviewCache | 57 | VideoPreviewsSimpleFileCache |
58 | } | 58 | } |
diff --git a/server/lib/files-cache/video-storyboards-simple-file-cache.ts b/server/lib/files-cache/video-storyboards-simple-file-cache.ts new file mode 100644 index 000000000..4cd96e70c --- /dev/null +++ b/server/lib/files-cache/video-storyboards-simple-file-cache.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import { join } from 'path' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { doRequestAndSaveToFile } from '@server/helpers/requests' | ||
4 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
5 | import { FILES_CACHE } from '../../initializers/constants' | ||
6 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' | ||
7 | |||
8 | class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> { | ||
9 | |||
10 | private static instance: VideoStoryboardsSimpleFileCache | ||
11 | |||
12 | private constructor () { | ||
13 | super() | ||
14 | } | ||
15 | |||
16 | static get Instance () { | ||
17 | return this.instance || (this.instance = new this()) | ||
18 | } | ||
19 | |||
20 | async getFilePathImpl (filename: string) { | ||
21 | const storyboard = await StoryboardModel.loadWithVideoByFilename(filename) | ||
22 | if (!storyboard) return undefined | ||
23 | |||
24 | if (storyboard.Video.isOwned()) return { isOwned: true, path: storyboard.getPath() } | ||
25 | |||
26 | return this.loadRemoteFile(storyboard.filename) | ||
27 | } | ||
28 | |||
29 | // Key is the storyboard filename | ||
30 | protected async loadRemoteFile (key: string) { | ||
31 | const storyboard = await StoryboardModel.loadWithVideoByFilename(key) | ||
32 | if (!storyboard) return undefined | ||
33 | |||
34 | const destPath = join(FILES_CACHE.STORYBOARDS.DIRECTORY, storyboard.filename) | ||
35 | const remoteUrl = storyboard.getOriginFileUrl(storyboard.Video) | ||
36 | |||
37 | try { | ||
38 | await doRequestAndSaveToFile(remoteUrl, destPath) | ||
39 | |||
40 | logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath) | ||
41 | |||
42 | return { isOwned: false, path: destPath } | ||
43 | } catch (err) { | ||
44 | logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err }) | ||
45 | |||
46 | return undefined | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | export { | ||
52 | VideoStoryboardsSimpleFileCache | ||
53 | } | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/video-torrents-simple-file-cache.ts index a6bf98dd4..8bcd0b9bf 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/video-torrents-simple-file-cache.ts | |||
@@ -6,11 +6,11 @@ import { MVideo, MVideoFile } from '@server/types/models' | |||
6 | import { CONFIG } from '../../initializers/config' | 6 | import { CONFIG } from '../../initializers/config' |
7 | import { FILES_CACHE } from '../../initializers/constants' | 7 | import { FILES_CACHE } from '../../initializers/constants' |
8 | import { VideoModel } from '../../models/video/video' | 8 | import { VideoModel } from '../../models/video/video' |
9 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 9 | import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' |
10 | 10 | ||
11 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 11 | class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> { |
12 | 12 | ||
13 | private static instance: VideosTorrentCache | 13 | private static instance: VideoTorrentsSimpleFileCache |
14 | 14 | ||
15 | private constructor () { | 15 | private constructor () { |
16 | super() | 16 | super() |
@@ -66,5 +66,5 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
66 | } | 66 | } |
67 | 67 | ||
68 | export { | 68 | export { |
69 | VideosTorrentCache | 69 | VideoTorrentsSimpleFileCache |
70 | } | 70 | } |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index fc1d7e1b0..19044d7c2 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -8,7 +8,7 @@ import { sha256 } from '@shared/extra-utils' | |||
8 | import { getVideoStreamDimensionsInfo } from '@shared/ffmpeg' | 8 | import { getVideoStreamDimensionsInfo } from '@shared/ffmpeg' |
9 | import { VideoStorage } from '@shared/models' | 9 | import { VideoStorage } from '@shared/models' |
10 | import { getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg' | 10 | import { getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg' |
11 | import { logger } from '../helpers/logger' | 11 | import { logger, loggerTagsFactory } from '../helpers/logger' |
12 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | 12 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
13 | import { generateRandomString } from '../helpers/utils' | 13 | import { generateRandomString } from '../helpers/utils' |
14 | import { CONFIG } from '../initializers/config' | 14 | import { CONFIG } from '../initializers/config' |
@@ -20,6 +20,8 @@ import { storeHLSFileFromFilename } from './object-storage' | |||
20 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' | 20 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHlsResolutionPlaylistFilename } from './paths' |
21 | import { VideoPathManager } from './video-path-manager' | 21 | import { VideoPathManager } from './video-path-manager' |
22 | 22 | ||
23 | const lTags = loggerTagsFactory('hls') | ||
24 | |||
23 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 25 | async function updateStreamingPlaylistsInfohashesIfNeeded () { |
24 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() | 26 | const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() |
25 | 27 | ||
@@ -48,7 +50,7 @@ async function updatePlaylistAfterFileChange (video: MVideo, playlist: MStreamin | |||
48 | 50 | ||
49 | video.setHLSPlaylist(playlistWithFiles) | 51 | video.setHLSPlaylist(playlistWithFiles) |
50 | } catch (err) { | 52 | } catch (err) { |
51 | logger.info('Cannot update playlist after file change. Maybe due to concurrent transcoding', { err }) | 53 | logger.warn('Cannot update playlist after file change. Maybe due to concurrent transcoding', { err }) |
52 | } | 54 | } |
53 | } | 55 | } |
54 | 56 | ||
@@ -95,6 +97,8 @@ function updateMasterHLSPlaylist (video: MVideo, playlistArg: MStreamingPlaylist | |||
95 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) | 97 | const masterPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename) |
96 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | 98 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') |
97 | 99 | ||
100 | logger.info('Updating %s master playlist file of video %s', masterPlaylistPath, video.uuid, lTags(video.uuid)) | ||
101 | |||
98 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { | 102 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) { |
99 | playlist.playlistUrl = await storeHLSFileFromFilename(playlist, playlist.playlistFilename) | 103 | playlist.playlistUrl = await storeHLSFileFromFilename(playlist, playlist.playlistFilename) |
100 | await remove(masterPlaylistPath) | 104 | await remove(masterPlaylistPath) |
diff --git a/server/lib/job-queue/handlers/generate-storyboard.ts b/server/lib/job-queue/handlers/generate-storyboard.ts new file mode 100644 index 000000000..ec07c568c --- /dev/null +++ b/server/lib/job-queue/handlers/generate-storyboard.ts | |||
@@ -0,0 +1,149 @@ | |||
1 | import { Job } from 'bullmq' | ||
2 | import { join } from 'path' | ||
3 | import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg' | ||
4 | import { generateImageFilename, getImageSize } from '@server/helpers/image-utils' | ||
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
6 | import { CONFIG } from '@server/initializers/config' | ||
7 | import { STORYBOARD } from '@server/initializers/constants' | ||
8 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
9 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
10 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
11 | import { VideoModel } from '@server/models/video/video' | ||
12 | import { MVideo } from '@server/types/models' | ||
13 | import { FFmpegImage, isAudioFile } from '@shared/ffmpeg' | ||
14 | import { GenerateStoryboardPayload } from '@shared/models' | ||
15 | |||
16 | const lTagsBase = loggerTagsFactory('storyboard') | ||
17 | |||
18 | async function processGenerateStoryboard (job: Job): Promise<void> { | ||
19 | const payload = job.data as GenerateStoryboardPayload | ||
20 | const lTags = lTagsBase(payload.videoUUID) | ||
21 | |||
22 | logger.info('Processing generate storyboard of %s in job %s.', payload.videoUUID, job.id, lTags) | ||
23 | |||
24 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(payload.videoUUID) | ||
25 | |||
26 | try { | ||
27 | const video = await VideoModel.loadFull(payload.videoUUID) | ||
28 | if (!video) { | ||
29 | logger.info('Video %s does not exist anymore, skipping storyboard generation.', payload.videoUUID, lTags) | ||
30 | return | ||
31 | } | ||
32 | |||
33 | const inputFile = video.getMaxQualityFile() | ||
34 | |||
35 | await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async videoPath => { | ||
36 | const isAudio = await isAudioFile(videoPath) | ||
37 | |||
38 | if (isAudio) { | ||
39 | logger.info('Do not generate a storyboard of %s since the video does not have a video stream', payload.videoUUID, lTags) | ||
40 | return | ||
41 | } | ||
42 | |||
43 | const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail')) | ||
44 | |||
45 | const filename = generateImageFilename() | ||
46 | const destination = join(CONFIG.STORAGE.STORYBOARDS_DIR, filename) | ||
47 | |||
48 | const totalSprites = buildTotalSprites(video) | ||
49 | if (totalSprites === 0) { | ||
50 | logger.info('Do not generate a storyboard of %s because the video is not long enough', payload.videoUUID, lTags) | ||
51 | return | ||
52 | } | ||
53 | |||
54 | const spriteDuration = Math.round(video.duration / totalSprites) | ||
55 | |||
56 | const spritesCount = findGridSize({ | ||
57 | toFind: totalSprites, | ||
58 | maxEdgeCount: STORYBOARD.SPRITES_MAX_EDGE_COUNT | ||
59 | }) | ||
60 | |||
61 | logger.debug( | ||
62 | 'Generating storyboard from video of %s to %s', video.uuid, destination, | ||
63 | { ...lTags, spritesCount, spriteDuration, videoDuration: video.duration } | ||
64 | ) | ||
65 | |||
66 | await ffmpeg.generateStoryboardFromVideo({ | ||
67 | destination, | ||
68 | path: videoPath, | ||
69 | sprites: { | ||
70 | size: STORYBOARD.SPRITE_SIZE, | ||
71 | count: spritesCount, | ||
72 | duration: spriteDuration | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | const imageSize = await getImageSize(destination) | ||
77 | |||
78 | const existing = await StoryboardModel.loadByVideo(video.id) | ||
79 | if (existing) await existing.destroy() | ||
80 | |||
81 | await StoryboardModel.create({ | ||
82 | filename, | ||
83 | totalHeight: imageSize.height, | ||
84 | totalWidth: imageSize.width, | ||
85 | spriteHeight: STORYBOARD.SPRITE_SIZE.height, | ||
86 | spriteWidth: STORYBOARD.SPRITE_SIZE.width, | ||
87 | spriteDuration, | ||
88 | videoId: video.id | ||
89 | }) | ||
90 | |||
91 | logger.info('Storyboard generation %s ended for video %s.', destination, video.uuid, lTags) | ||
92 | }) | ||
93 | |||
94 | if (payload.federate) { | ||
95 | await federateVideoIfNeeded(video, false) | ||
96 | } | ||
97 | } finally { | ||
98 | inputFileMutexReleaser() | ||
99 | } | ||
100 | } | ||
101 | |||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
104 | export { | ||
105 | processGenerateStoryboard | ||
106 | } | ||
107 | |||
108 | function buildTotalSprites (video: MVideo) { | ||
109 | const maxSprites = STORYBOARD.SPRITE_SIZE.height * STORYBOARD.SPRITE_SIZE.width | ||
110 | const totalSprites = Math.min(Math.ceil(video.duration), maxSprites) | ||
111 | |||
112 | // We can generate a single line | ||
113 | if (totalSprites <= STORYBOARD.SPRITES_MAX_EDGE_COUNT) return totalSprites | ||
114 | |||
115 | return findGridFit(totalSprites, STORYBOARD.SPRITES_MAX_EDGE_COUNT) | ||
116 | } | ||
117 | |||
118 | function findGridSize (options: { | ||
119 | toFind: number | ||
120 | maxEdgeCount: number | ||
121 | }) { | ||
122 | const { toFind, maxEdgeCount } = options | ||
123 | |||
124 | for (let i = 1; i <= maxEdgeCount; i++) { | ||
125 | for (let j = i; j <= maxEdgeCount; j++) { | ||
126 | if (toFind === i * j) return { width: j, height: i } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | throw new Error(`Could not find grid size (to find: ${toFind}, max edge count: ${maxEdgeCount}`) | ||
131 | } | ||
132 | |||
133 | function findGridFit (value: number, maxMultiplier: number) { | ||
134 | for (let i = value; i--; i > 0) { | ||
135 | if (!isPrimeWithin(i, maxMultiplier)) return i | ||
136 | } | ||
137 | |||
138 | throw new Error('Could not find prime number below ' + value) | ||
139 | } | ||
140 | |||
141 | function isPrimeWithin (value: number, maxMultiplier: number) { | ||
142 | if (value < 2) return false | ||
143 | |||
144 | for (let i = 2, end = Math.min(Math.sqrt(value), maxMultiplier); i <= end; i++) { | ||
145 | if (value % i === 0 && value / i <= maxMultiplier) return false | ||
146 | } | ||
147 | |||
148 | return true | ||
149 | } | ||
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts index 26752ff37..9a99b6722 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts | |||
@@ -4,7 +4,7 @@ import { join } from 'path' | |||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
5 | import { updateTorrentMetadata } from '@server/helpers/webtorrent' | 5 | import { updateTorrentMetadata } from '@server/helpers/webtorrent' |
6 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' | 6 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' |
7 | import { storeHLSFileFromFilename, storeWebTorrentFile } from '@server/lib/object-storage' | 7 | import { storeHLSFileFromFilename, storeWebVideoFile } from '@server/lib/object-storage' |
8 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 8 | import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
9 | import { VideoPathManager } from '@server/lib/video-path-manager' | 9 | import { VideoPathManager } from '@server/lib/video-path-manager' |
10 | import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/lib/video-state' | 10 | import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/lib/video-state' |
@@ -33,9 +33,9 @@ export async function processMoveToObjectStorage (job: Job) { | |||
33 | 33 | ||
34 | try { | 34 | try { |
35 | if (video.VideoFiles) { | 35 | if (video.VideoFiles) { |
36 | logger.debug('Moving %d webtorrent files for video %s.', video.VideoFiles.length, video.uuid, lTags) | 36 | logger.debug('Moving %d web video files for video %s.', video.VideoFiles.length, video.uuid, lTags) |
37 | 37 | ||
38 | await moveWebTorrentFiles(video) | 38 | await moveWebVideoFiles(video) |
39 | } | 39 | } |
40 | 40 | ||
41 | if (video.VideoStreamingPlaylists) { | 41 | if (video.VideoStreamingPlaylists) { |
@@ -75,11 +75,11 @@ export async function onMoveToObjectStorageFailure (job: Job, err: any) { | |||
75 | 75 | ||
76 | // --------------------------------------------------------------------------- | 76 | // --------------------------------------------------------------------------- |
77 | 77 | ||
78 | async function moveWebTorrentFiles (video: MVideoWithAllFiles) { | 78 | async function moveWebVideoFiles (video: MVideoWithAllFiles) { |
79 | for (const file of video.VideoFiles) { | 79 | for (const file of video.VideoFiles) { |
80 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue | 80 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue |
81 | 81 | ||
82 | const fileUrl = await storeWebTorrentFile(video, file) | 82 | const fileUrl = await storeWebVideoFile(video, file) |
83 | 83 | ||
84 | const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) | 84 | const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) |
85 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) | 85 | await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 9a4550e4d..d221e8968 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -3,7 +3,7 @@ import { copy, stat } from 'fs-extra' | |||
3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
6 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 6 | import { generateWebVideoFilename } from '@server/lib/paths' |
7 | import { buildMoveToObjectStorageJob } from '@server/lib/video' | 7 | import { buildMoveToObjectStorageJob } from '@server/lib/video' |
8 | import { VideoPathManager } from '@server/lib/video-path-manager' | 8 | import { VideoPathManager } from '@server/lib/video-path-manager' |
9 | import { VideoModel } from '@server/models/video/video' | 9 | import { VideoModel } from '@server/models/video/video' |
@@ -56,7 +56,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
56 | 56 | ||
57 | if (currentVideoFile) { | 57 | if (currentVideoFile) { |
58 | // Remove old file and old torrent | 58 | // Remove old file and old torrent |
59 | await video.removeWebTorrentFile(currentVideoFile) | 59 | await video.removeWebVideoFile(currentVideoFile) |
60 | // Remove the old video file from the array | 60 | // Remove the old video file from the array |
61 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) | 61 | video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) |
62 | 62 | ||
@@ -66,7 +66,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
66 | const newVideoFile = new VideoFileModel({ | 66 | const newVideoFile = new VideoFileModel({ |
67 | resolution, | 67 | resolution, |
68 | extname: fileExt, | 68 | extname: fileExt, |
69 | filename: generateWebTorrentVideoFilename(resolution, fileExt), | 69 | filename: generateWebVideoFilename(resolution, fileExt), |
70 | storage: VideoStorage.FILE_SYSTEM, | 70 | storage: VideoStorage.FILE_SYSTEM, |
71 | size, | 71 | size, |
72 | fps, | 72 | fps, |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index cdd362f6e..e5cd258d6 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -4,7 +4,7 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
4 | import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' | 4 | import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' |
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { isPostImportVideoAccepted } from '@server/lib/moderation' | 6 | import { isPostImportVideoAccepted } from '@server/lib/moderation' |
7 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 7 | import { generateWebVideoFilename } from '@server/lib/paths' |
8 | import { Hooks } from '@server/lib/plugins/hooks' | 8 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { ServerConfigManager } from '@server/lib/server-config-manager' | 9 | import { ServerConfigManager } from '@server/lib/server-config-manager' |
10 | import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job' | 10 | import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job' |
@@ -39,7 +39,7 @@ import { VideoFileModel } from '../../../models/video/video-file' | |||
39 | import { VideoImportModel } from '../../../models/video/video-import' | 39 | import { VideoImportModel } from '../../../models/video/video-import' |
40 | import { federateVideoIfNeeded } from '../../activitypub/videos' | 40 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
41 | import { Notifier } from '../../notifier' | 41 | import { Notifier } from '../../notifier' |
42 | import { generateVideoMiniature } from '../../thumbnail' | 42 | import { generateLocalVideoMiniature } from '../../thumbnail' |
43 | import { JobQueue } from '../job-queue' | 43 | import { JobQueue } from '../job-queue' |
44 | 44 | ||
45 | async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> { | 45 | async function processVideoImport (job: Job): Promise<VideoImportPreventExceptionResult> { |
@@ -148,7 +148,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
148 | extname: fileExt, | 148 | extname: fileExt, |
149 | resolution, | 149 | resolution, |
150 | size: stats.size, | 150 | size: stats.size, |
151 | filename: generateWebTorrentVideoFilename(resolution, fileExt), | 151 | filename: generateWebVideoFilename(resolution, fileExt), |
152 | fps, | 152 | fps, |
153 | videoId: videoImport.videoId | 153 | videoId: videoImport.videoId |
154 | } | 154 | } |
@@ -274,7 +274,7 @@ async function generateMiniature (videoImportWithFiles: MVideoImportDefaultFiles | |||
274 | } | 274 | } |
275 | } | 275 | } |
276 | 276 | ||
277 | const miniatureModel = await generateVideoMiniature({ | 277 | const miniatureModel = await generateLocalVideoMiniature({ |
278 | video: videoImportWithFiles.Video, | 278 | video: videoImportWithFiles.Video, |
279 | videoFile, | 279 | videoFile, |
280 | type: thumbnailType | 280 | type: thumbnailType |
@@ -306,6 +306,15 @@ async function afterImportSuccess (options: { | |||
306 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) | 306 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) |
307 | } | 307 | } |
308 | 308 | ||
309 | // Generate the storyboard in the job queue, and don't forget to federate an update after | ||
310 | await JobQueue.Instance.createJob({ | ||
311 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
312 | payload: { | ||
313 | videoUUID: video.uuid, | ||
314 | federate: true | ||
315 | } | ||
316 | }) | ||
317 | |||
309 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | 318 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { |
310 | await JobQueue.Instance.createJob( | 319 | await JobQueue.Instance.createJob( |
311 | await buildMoveToObjectStorageJob({ video, previousVideoState: VideoState.TO_IMPORT }) | 320 | await buildMoveToObjectStorageJob({ video, previousVideoState: VideoState.TO_IMPORT }) |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 49feb53f2..ae886de35 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | import { Job } from 'bullmq' | 1 | import { Job } from 'bullmq' |
2 | import { readdir, remove } from 'fs-extra' | 2 | import { readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { peertubeTruncate } from '@server/helpers/core-utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
4 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
6 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' | 8 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' |
7 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' | 9 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' |
8 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 10 | import { generateLocalVideoMiniature } from '@server/lib/thumbnail' |
9 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' | 11 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' |
10 | import { VideoPathManager } from '@server/lib/video-path-manager' | 12 | import { VideoPathManager } from '@server/lib/video-path-manager' |
11 | import { moveToNextState } from '@server/lib/video-state' | 13 | import { moveToNextState } from '@server/lib/video-state' |
@@ -20,8 +22,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv | |||
20 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' | 22 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg' |
21 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 23 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
22 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 24 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
23 | import { peertubeTruncate } from '@server/helpers/core-utils' | 25 | import { JobQueue } from '../job-queue' |
24 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
25 | 26 | ||
26 | const lTags = loggerTagsFactory('live', 'job') | 27 | const lTags = loggerTagsFactory('live', 'job') |
27 | 28 | ||
@@ -142,11 +143,13 @@ async function saveReplayToExternalVideo (options: { | |||
142 | await remove(replayDirectory) | 143 | await remove(replayDirectory) |
143 | 144 | ||
144 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { | 145 | for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { |
145 | const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) | 146 | const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) |
146 | await replayVideo.addAndSaveThumbnail(image) | 147 | await replayVideo.addAndSaveThumbnail(image) |
147 | } | 148 | } |
148 | 149 | ||
149 | await moveToNextState({ video: replayVideo, isNewVideo: true }) | 150 | await moveToNextState({ video: replayVideo, isNewVideo: true }) |
151 | |||
152 | await createStoryboardJob(replayVideo) | ||
150 | } | 153 | } |
151 | 154 | ||
152 | async function replaceLiveByReplay (options: { | 155 | async function replaceLiveByReplay (options: { |
@@ -186,6 +189,7 @@ async function replaceLiveByReplay (options: { | |||
186 | 189 | ||
187 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) | 190 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) |
188 | 191 | ||
192 | // FIXME: should not happen in this function | ||
189 | if (permanentLive) { // Remove session replay | 193 | if (permanentLive) { // Remove session replay |
190 | await remove(replayDirectory) | 194 | await remove(replayDirectory) |
191 | } else { // We won't stream again in this live, we can delete the base replay directory | 195 | } else { // We won't stream again in this live, we can delete the base replay directory |
@@ -194,7 +198,7 @@ async function replaceLiveByReplay (options: { | |||
194 | 198 | ||
195 | // Regenerate the thumbnail & preview? | 199 | // Regenerate the thumbnail & preview? |
196 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | 200 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { |
197 | const miniature = await generateVideoMiniature({ | 201 | const miniature = await generateLocalVideoMiniature({ |
198 | video: videoWithFiles, | 202 | video: videoWithFiles, |
199 | videoFile: videoWithFiles.getMaxQualityFile(), | 203 | videoFile: videoWithFiles.getMaxQualityFile(), |
200 | type: ThumbnailType.MINIATURE | 204 | type: ThumbnailType.MINIATURE |
@@ -203,7 +207,7 @@ async function replaceLiveByReplay (options: { | |||
203 | } | 207 | } |
204 | 208 | ||
205 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { | 209 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { |
206 | const preview = await generateVideoMiniature({ | 210 | const preview = await generateLocalVideoMiniature({ |
207 | video: videoWithFiles, | 211 | video: videoWithFiles, |
208 | videoFile: videoWithFiles.getMaxQualityFile(), | 212 | videoFile: videoWithFiles.getMaxQualityFile(), |
209 | type: ThumbnailType.PREVIEW | 213 | type: ThumbnailType.PREVIEW |
@@ -213,6 +217,8 @@ async function replaceLiveByReplay (options: { | |||
213 | 217 | ||
214 | // We consider this is a new video | 218 | // We consider this is a new video |
215 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) | 219 | await moveToNextState({ video: videoWithFiles, isNewVideo: true }) |
220 | |||
221 | await createStoryboardJob(videoWithFiles) | ||
216 | } | 222 | } |
217 | 223 | ||
218 | async function assignReplayFilesToVideo (options: { | 224 | async function assignReplayFilesToVideo (options: { |
@@ -277,3 +283,13 @@ async function cleanupLiveAndFederate (options: { | |||
277 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) | 283 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) |
278 | } | 284 | } |
279 | } | 285 | } |
286 | |||
287 | function createStoryboardJob (video: MVideo) { | ||
288 | return JobQueue.Instance.createJob({ | ||
289 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
290 | payload: { | ||
291 | videoUUID: video.uuid, | ||
292 | federate: true | ||
293 | } | ||
294 | }) | ||
295 | } | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index f8758f170..1c8f4fd9f 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Job } from 'bullmq' | 1 | import { Job } from 'bullmq' |
2 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 2 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
3 | import { generateHlsPlaylistResolution } from '@server/lib/transcoding/hls-transcoding' | 3 | import { generateHlsPlaylistResolution } from '@server/lib/transcoding/hls-transcoding' |
4 | import { mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewWebTorrentResolution } from '@server/lib/transcoding/web-transcoding' | 4 | import { mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewWebVideoResolution } from '@server/lib/transcoding/web-transcoding' |
5 | import { removeAllWebTorrentFiles } from '@server/lib/video-file' | 5 | import { removeAllWebVideoFiles } from '@server/lib/video-file' |
6 | import { VideoPathManager } from '@server/lib/video-path-manager' | 6 | import { VideoPathManager } from '@server/lib/video-path-manager' |
7 | import { moveToFailedTranscodingState } from '@server/lib/video-state' | 7 | import { moveToFailedTranscodingState } from '@server/lib/video-state' |
8 | import { UserModel } from '@server/models/user/user' | 8 | import { UserModel } from '@server/models/user/user' |
@@ -11,7 +11,7 @@ import { MUser, MUserId, MVideoFullLight } from '@server/types/models' | |||
11 | import { | 11 | import { |
12 | HLSTranscodingPayload, | 12 | HLSTranscodingPayload, |
13 | MergeAudioTranscodingPayload, | 13 | MergeAudioTranscodingPayload, |
14 | NewWebTorrentResolutionTranscodingPayload, | 14 | NewWebVideoResolutionTranscodingPayload, |
15 | OptimizeTranscodingPayload, | 15 | OptimizeTranscodingPayload, |
16 | VideoTranscodingPayload | 16 | VideoTranscodingPayload |
17 | } from '@shared/models' | 17 | } from '@shared/models' |
@@ -22,9 +22,9 @@ type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVide | |||
22 | 22 | ||
23 | const handlers: { [ id in VideoTranscodingPayload['type'] ]: HandlerFunction } = { | 23 | const handlers: { [ id in VideoTranscodingPayload['type'] ]: HandlerFunction } = { |
24 | 'new-resolution-to-hls': handleHLSJob, | 24 | 'new-resolution-to-hls': handleHLSJob, |
25 | 'new-resolution-to-webtorrent': handleNewWebTorrentResolutionJob, | 25 | 'new-resolution-to-web-video': handleNewWebVideoResolutionJob, |
26 | 'merge-audio-to-webtorrent': handleWebTorrentMergeAudioJob, | 26 | 'merge-audio-to-web-video': handleWebVideoMergeAudioJob, |
27 | 'optimize-to-webtorrent': handleWebTorrentOptimizeJob | 27 | 'optimize-to-web-video': handleWebVideoOptimizeJob |
28 | } | 28 | } |
29 | 29 | ||
30 | const lTags = loggerTagsFactory('transcoding') | 30 | const lTags = loggerTagsFactory('transcoding') |
@@ -74,7 +74,7 @@ export { | |||
74 | // Job handlers | 74 | // Job handlers |
75 | // --------------------------------------------------------------------------- | 75 | // --------------------------------------------------------------------------- |
76 | 76 | ||
77 | async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 77 | async function handleWebVideoMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { |
78 | logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 78 | logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
79 | 79 | ||
80 | await mergeAudioVideofile({ video, resolution: payload.resolution, fps: payload.fps, job }) | 80 | await mergeAudioVideofile({ video, resolution: payload.resolution, fps: payload.fps, job }) |
@@ -84,7 +84,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans | |||
84 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: !payload.hasChildren, video }) | 84 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: !payload.hasChildren, video }) |
85 | } | 85 | } |
86 | 86 | ||
87 | async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 87 | async function handleWebVideoOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { |
88 | logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 88 | logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
89 | 89 | ||
90 | await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), quickTranscode: payload.quickTranscode, job }) | 90 | await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), quickTranscode: payload.quickTranscode, job }) |
@@ -96,12 +96,12 @@ async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodi | |||
96 | 96 | ||
97 | // --------------------------------------------------------------------------- | 97 | // --------------------------------------------------------------------------- |
98 | 98 | ||
99 | async function handleNewWebTorrentResolutionJob (job: Job, payload: NewWebTorrentResolutionTranscodingPayload, video: MVideoFullLight) { | 99 | async function handleNewWebVideoResolutionJob (job: Job, payload: NewWebVideoResolutionTranscodingPayload, video: MVideoFullLight) { |
100 | logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) | 100 | logger.info('Handling Web Video transcoding job for %s.', video.uuid, lTags(video.uuid), { payload }) |
101 | 101 | ||
102 | await transcodeNewWebTorrentResolution({ video, resolution: payload.resolution, fps: payload.fps, job }) | 102 | await transcodeNewWebVideoResolution({ video, resolution: payload.resolution, fps: payload.fps, job }) |
103 | 103 | ||
104 | logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) | 104 | logger.info('Web Video transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) |
105 | 105 | ||
106 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) | 106 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) |
107 | } | 107 | } |
@@ -118,7 +118,7 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, videoArg: | |||
118 | video = await VideoModel.loadFull(videoArg.uuid) | 118 | video = await VideoModel.loadFull(videoArg.uuid) |
119 | 119 | ||
120 | const videoFileInput = payload.copyCodecs | 120 | const videoFileInput = payload.copyCodecs |
121 | ? video.getWebTorrentFile(payload.resolution) | 121 | ? video.getWebVideoFile(payload.resolution) |
122 | : video.getMaxQualityFile() | 122 | : video.getMaxQualityFile() |
123 | 123 | ||
124 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() | 124 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() |
@@ -140,10 +140,10 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, videoArg: | |||
140 | 140 | ||
141 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) | 141 | logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid), { payload }) |
142 | 142 | ||
143 | if (payload.deleteWebTorrentFiles === true) { | 143 | if (payload.deleteWebVideoFiles === true) { |
144 | logger.info('Removing WebTorrent files of %s now we have a HLS version of it.', video.uuid, lTags(video.uuid)) | 144 | logger.info('Removing Web Video files of %s now we have a HLS version of it.', video.uuid, lTags(video.uuid)) |
145 | 145 | ||
146 | await removeAllWebTorrentFiles(video) | 146 | await removeAllWebVideoFiles(video) |
147 | } | 147 | } |
148 | 148 | ||
149 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) | 149 | await onTranscodingEnded({ isNewVideo: payload.isNewVideo, moveVideoToNextState: true, video }) |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 03f6fbea7..177bca285 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -25,6 +25,7 @@ import { | |||
25 | DeleteResumableUploadMetaFilePayload, | 25 | DeleteResumableUploadMetaFilePayload, |
26 | EmailPayload, | 26 | EmailPayload, |
27 | FederateVideoPayload, | 27 | FederateVideoPayload, |
28 | GenerateStoryboardPayload, | ||
28 | JobState, | 29 | JobState, |
29 | JobType, | 30 | JobType, |
30 | ManageVideoTorrentPayload, | 31 | ManageVideoTorrentPayload, |
@@ -65,6 +66,7 @@ import { processVideoLiveEnding } from './handlers/video-live-ending' | |||
65 | import { processVideoStudioEdition } from './handlers/video-studio-edition' | 66 | import { processVideoStudioEdition } from './handlers/video-studio-edition' |
66 | import { processVideoTranscoding } from './handlers/video-transcoding' | 67 | import { processVideoTranscoding } from './handlers/video-transcoding' |
67 | import { processVideosViewsStats } from './handlers/video-views-stats' | 68 | import { processVideosViewsStats } from './handlers/video-views-stats' |
69 | import { processGenerateStoryboard } from './handlers/generate-storyboard' | ||
68 | 70 | ||
69 | export type CreateJobArgument = | 71 | export type CreateJobArgument = |
70 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 72 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -91,7 +93,8 @@ export type CreateJobArgument = | |||
91 | { type: 'after-video-channel-import', payload: AfterVideoChannelImportPayload } | | 93 | { type: 'after-video-channel-import', payload: AfterVideoChannelImportPayload } | |
92 | { type: 'notify', payload: NotifyPayload } | | 94 | { type: 'notify', payload: NotifyPayload } | |
93 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } | | 95 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } | |
94 | { type: 'federate-video', payload: FederateVideoPayload } | 96 | { type: 'federate-video', payload: FederateVideoPayload } | |
97 | { type: 'generate-video-storyboard', payload: GenerateStoryboardPayload } | ||
95 | 98 | ||
96 | export type CreateJobOptions = { | 99 | export type CreateJobOptions = { |
97 | delay?: number | 100 | delay?: number |
@@ -122,7 +125,8 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = { | |||
122 | 'video-redundancy': processVideoRedundancy, | 125 | 'video-redundancy': processVideoRedundancy, |
123 | 'video-studio-edition': processVideoStudioEdition, | 126 | 'video-studio-edition': processVideoStudioEdition, |
124 | 'video-transcoding': processVideoTranscoding, | 127 | 'video-transcoding': processVideoTranscoding, |
125 | 'videos-views-stats': processVideosViewsStats | 128 | 'videos-views-stats': processVideosViewsStats, |
129 | 'generate-video-storyboard': processGenerateStoryboard | ||
126 | } | 130 | } |
127 | 131 | ||
128 | const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = { | 132 | const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = { |
@@ -141,10 +145,11 @@ const jobTypes: JobType[] = [ | |||
141 | 'after-video-channel-import', | 145 | 'after-video-channel-import', |
142 | 'email', | 146 | 'email', |
143 | 'federate-video', | 147 | 'federate-video', |
144 | 'transcoding-job-builder', | 148 | 'generate-video-storyboard', |
145 | 'manage-video-torrent', | 149 | 'manage-video-torrent', |
146 | 'move-to-object-storage', | 150 | 'move-to-object-storage', |
147 | 'notify', | 151 | 'notify', |
152 | 'transcoding-job-builder', | ||
148 | 'video-channel-import', | 153 | 'video-channel-import', |
149 | 'video-file-import', | 154 | 'video-file-import', |
150 | 'video-import', | 155 | 'video-import', |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index 16dc265a3..611e6d0af 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { Transaction } from 'sequelize/types' | 3 | import { Transaction } from 'sequelize/types' |
5 | import { ActorModel } from '@server/models/actor/actor' | 4 | import { ActorModel } from '@server/models/actor/actor' |
@@ -8,14 +7,14 @@ import { buildUUID } from '@shared/extra-utils' | |||
8 | import { ActivityPubActorType, ActorImageType } from '@shared/models' | 7 | import { ActivityPubActorType, ActorImageType } from '@shared/models' |
9 | import { retryTransactionWrapper } from '../helpers/database-utils' | 8 | import { retryTransactionWrapper } from '../helpers/database-utils' |
10 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' | 10 | import { ACTOR_IMAGES_SIZE, WEBSERVER } from '../initializers/constants' |
12 | import { sequelizeTypescript } from '../initializers/database' | 11 | import { sequelizeTypescript } from '../initializers/database' |
13 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 12 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
14 | import { deleteActorImages, updateActorImages } from './activitypub/actors' | 13 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
15 | import { sendUpdateActor } from './activitypub/send' | 14 | import { sendUpdateActor } from './activitypub/send' |
16 | import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' | 15 | import { processImageFromWorker } from './worker/parent-process' |
17 | 16 | ||
18 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 17 | export function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
19 | return new ActorModel({ | 18 | return new ActorModel({ |
20 | type, | 19 | type, |
21 | url, | 20 | url, |
@@ -32,7 +31,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
32 | }) as MActor | 31 | }) as MActor |
33 | } | 32 | } |
34 | 33 | ||
35 | async function updateLocalActorImageFiles ( | 34 | export async function updateLocalActorImageFiles ( |
36 | accountOrChannel: MAccountDefault | MChannelDefault, | 35 | accountOrChannel: MAccountDefault | MChannelDefault, |
37 | imagePhysicalFile: Express.Multer.File, | 36 | imagePhysicalFile: Express.Multer.File, |
38 | type: ActorImageType | 37 | type: ActorImageType |
@@ -41,7 +40,7 @@ async function updateLocalActorImageFiles ( | |||
41 | const extension = getLowercaseExtension(imagePhysicalFile.filename) | 40 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
42 | 41 | ||
43 | const imageName = buildUUID() + extension | 42 | const imageName = buildUUID() + extension |
44 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 43 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, imageName) |
45 | await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true }) | 44 | await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true }) |
46 | 45 | ||
47 | return { | 46 | return { |
@@ -73,7 +72,7 @@ async function updateLocalActorImageFiles ( | |||
73 | })) | 72 | })) |
74 | } | 73 | } |
75 | 74 | ||
76 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | 75 | export async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { |
77 | return retryTransactionWrapper(() => { | 76 | return retryTransactionWrapper(() => { |
78 | return sequelizeTypescript.transaction(async t => { | 77 | return sequelizeTypescript.transaction(async t => { |
79 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) | 78 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) |
@@ -88,7 +87,7 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC | |||
88 | 87 | ||
89 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
90 | 89 | ||
91 | async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { | 90 | export async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { |
92 | let actor = await ActorModel.loadLocalByName(baseActorName, transaction) | 91 | let actor = await ActorModel.loadLocalByName(baseActorName, transaction) |
93 | if (!actor) return baseActorName | 92 | if (!actor) return baseActorName |
94 | 93 | ||
@@ -101,34 +100,3 @@ async function findAvailableLocalActorName (baseActorName: string, transaction?: | |||
101 | 100 | ||
102 | throw new Error('Cannot find available actor local name (too much iterations).') | 101 | throw new Error('Cannot find available actor local name (too much iterations).') |
103 | } | 102 | } |
104 | |||
105 | // --------------------------------------------------------------------------- | ||
106 | |||
107 | function downloadActorImageFromWorker (options: { | ||
108 | fileUrl: string | ||
109 | filename: string | ||
110 | type: ActorImageType | ||
111 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | ||
112 | }) { | ||
113 | const downloaderOptions = { | ||
114 | url: options.fileUrl, | ||
115 | destDir: CONFIG.STORAGE.ACTOR_IMAGES, | ||
116 | destName: options.filename, | ||
117 | size: options.size | ||
118 | } | ||
119 | |||
120 | return downloadImageFromWorker(downloaderOptions) | ||
121 | } | ||
122 | |||
123 | // Unsafe so could returns paths that does not exist anymore | ||
124 | const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) | ||
125 | |||
126 | export { | ||
127 | actorImagePathUnsafeCache, | ||
128 | updateLocalActorImageFiles, | ||
129 | findAvailableLocalActorName, | ||
130 | downloadActorImageFromWorker, | ||
131 | deleteLocalActorImageFile, | ||
132 | downloadImageFromWorker, | ||
133 | buildActorInstance | ||
134 | } | ||
diff --git a/server/lib/object-storage/index.ts b/server/lib/object-storage/index.ts index 6525f8dfb..3ad6cab63 100644 --- a/server/lib/object-storage/index.ts +++ b/server/lib/object-storage/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './keys' | 1 | export * from './keys' |
2 | export * from './proxy' | 2 | export * from './proxy' |
3 | export * from './pre-signed-urls' | ||
3 | export * from './urls' | 4 | export * from './urls' |
4 | export * from './videos' | 5 | export * from './videos' |
diff --git a/server/lib/object-storage/keys.ts b/server/lib/object-storage/keys.ts index 4f17073f4..6d2098298 100644 --- a/server/lib/object-storage/keys.ts +++ b/server/lib/object-storage/keys.ts | |||
@@ -9,12 +9,12 @@ function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylistVideo) { | |||
9 | return join(playlist.getStringType(), playlist.Video.uuid) | 9 | return join(playlist.getStringType(), playlist.Video.uuid) |
10 | } | 10 | } |
11 | 11 | ||
12 | function generateWebTorrentObjectStorageKey (filename: string) { | 12 | function generateWebVideoObjectStorageKey (filename: string) { |
13 | return filename | 13 | return filename |
14 | } | 14 | } |
15 | 15 | ||
16 | export { | 16 | export { |
17 | generateHLSObjectStorageKey, | 17 | generateHLSObjectStorageKey, |
18 | generateHLSObjectBaseStorageKey, | 18 | generateHLSObjectBaseStorageKey, |
19 | generateWebTorrentObjectStorageKey | 19 | generateWebVideoObjectStorageKey |
20 | } | 20 | } |
diff --git a/server/lib/object-storage/pre-signed-urls.ts b/server/lib/object-storage/pre-signed-urls.ts new file mode 100644 index 000000000..caf149bb8 --- /dev/null +++ b/server/lib/object-storage/pre-signed-urls.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { GetObjectCommand } from '@aws-sdk/client-s3' | ||
2 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner' | ||
3 | import { CONFIG } from '@server/initializers/config' | ||
4 | import { MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' | ||
5 | import { generateHLSObjectStorageKey, generateWebVideoObjectStorageKey } from './keys' | ||
6 | import { buildKey, getClient } from './shared' | ||
7 | import { getHLSPublicFileUrl, getWebVideoPublicFileUrl } from './urls' | ||
8 | |||
9 | export async function generateWebVideoPresignedUrl (options: { | ||
10 | file: MVideoFile | ||
11 | downloadFilename: string | ||
12 | }) { | ||
13 | const { file, downloadFilename } = options | ||
14 | |||
15 | const key = generateWebVideoObjectStorageKey(file.filename) | ||
16 | |||
17 | const command = new GetObjectCommand({ | ||
18 | Bucket: CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME, | ||
19 | Key: buildKey(key, CONFIG.OBJECT_STORAGE.WEB_VIDEOS), | ||
20 | ResponseContentDisposition: `attachment; filename=${downloadFilename}` | ||
21 | }) | ||
22 | |||
23 | const url = await getSignedUrl(getClient(), command, { expiresIn: 3600 * 24 }) | ||
24 | |||
25 | return getWebVideoPublicFileUrl(url) | ||
26 | } | ||
27 | |||
28 | export async function generateHLSFilePresignedUrl (options: { | ||
29 | streamingPlaylist: MStreamingPlaylistVideo | ||
30 | file: MVideoFile | ||
31 | downloadFilename: string | ||
32 | }) { | ||
33 | const { streamingPlaylist, file, downloadFilename } = options | ||
34 | |||
35 | const key = generateHLSObjectStorageKey(streamingPlaylist, file.filename) | ||
36 | |||
37 | const command = new GetObjectCommand({ | ||
38 | Bucket: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME, | ||
39 | Key: buildKey(key, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS), | ||
40 | ResponseContentDisposition: `attachment; filename=${downloadFilename}` | ||
41 | }) | ||
42 | |||
43 | const url = await getSignedUrl(getClient(), command, { expiresIn: 3600 * 24 }) | ||
44 | |||
45 | return getHLSPublicFileUrl(url) | ||
46 | } | ||
diff --git a/server/lib/object-storage/proxy.ts b/server/lib/object-storage/proxy.ts index c782a8a25..c09a0d1b0 100644 --- a/server/lib/object-storage/proxy.ts +++ b/server/lib/object-storage/proxy.ts | |||
@@ -7,19 +7,19 @@ import { StreamReplacer } from '@server/helpers/stream-replacer' | |||
7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { HttpStatusCode } from '@shared/models' | 8 | import { HttpStatusCode } from '@shared/models' |
9 | import { injectQueryToPlaylistUrls } from '../hls' | 9 | import { injectQueryToPlaylistUrls } from '../hls' |
10 | import { getHLSFileReadStream, getWebTorrentFileReadStream } from './videos' | 10 | import { getHLSFileReadStream, getWebVideoFileReadStream } from './videos' |
11 | 11 | ||
12 | export async function proxifyWebTorrentFile (options: { | 12 | export async function proxifyWebVideoFile (options: { |
13 | req: express.Request | 13 | req: express.Request |
14 | res: express.Response | 14 | res: express.Response |
15 | filename: string | 15 | filename: string |
16 | }) { | 16 | }) { |
17 | const { req, res, filename } = options | 17 | const { req, res, filename } = options |
18 | 18 | ||
19 | logger.debug('Proxifying WebTorrent file %s from object storage.', filename) | 19 | logger.debug('Proxifying Web Video file %s from object storage.', filename) |
20 | 20 | ||
21 | try { | 21 | try { |
22 | const { response: s3Response, stream } = await getWebTorrentFileReadStream({ | 22 | const { response: s3Response, stream } = await getWebVideoFileReadStream({ |
23 | filename, | 23 | filename, |
24 | rangeHeader: req.header('range') | 24 | rangeHeader: req.header('range') |
25 | }) | 25 | }) |
diff --git a/server/lib/object-storage/urls.ts b/server/lib/object-storage/urls.ts index b8ef94559..40619cd5a 100644 --- a/server/lib/object-storage/urls.ts +++ b/server/lib/object-storage/urls.ts | |||
@@ -9,8 +9,8 @@ function getInternalUrl (config: BucketInfo, keyWithoutPrefix: string) { | |||
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
11 | 11 | ||
12 | function getWebTorrentPublicFileUrl (fileUrl: string) { | 12 | function getWebVideoPublicFileUrl (fileUrl: string) { |
13 | const baseUrl = CONFIG.OBJECT_STORAGE.VIDEOS.BASE_URL | 13 | const baseUrl = CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BASE_URL |
14 | if (!baseUrl) return fileUrl | 14 | if (!baseUrl) return fileUrl |
15 | 15 | ||
16 | return replaceByBaseUrl(fileUrl, baseUrl) | 16 | return replaceByBaseUrl(fileUrl, baseUrl) |
@@ -29,8 +29,8 @@ function getHLSPrivateFileUrl (video: MVideoUUID, filename: string) { | |||
29 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}` | 29 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}` |
30 | } | 30 | } |
31 | 31 | ||
32 | function getWebTorrentPrivateFileUrl (filename: string) { | 32 | function getWebVideoPrivateFileUrl (filename: string) { |
33 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + filename | 33 | return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + filename |
34 | } | 34 | } |
35 | 35 | ||
36 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
@@ -38,11 +38,11 @@ function getWebTorrentPrivateFileUrl (filename: string) { | |||
38 | export { | 38 | export { |
39 | getInternalUrl, | 39 | getInternalUrl, |
40 | 40 | ||
41 | getWebTorrentPublicFileUrl, | 41 | getWebVideoPublicFileUrl, |
42 | getHLSPublicFileUrl, | 42 | getHLSPublicFileUrl, |
43 | 43 | ||
44 | getHLSPrivateFileUrl, | 44 | getHLSPrivateFileUrl, |
45 | getWebTorrentPrivateFileUrl, | 45 | getWebVideoPrivateFileUrl, |
46 | 46 | ||
47 | replaceByBaseUrl | 47 | replaceByBaseUrl |
48 | } | 48 | } |
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts index 9152c5352..891e9ff76 100644 --- a/server/lib/object-storage/videos.ts +++ b/server/lib/object-storage/videos.ts | |||
@@ -4,7 +4,7 @@ import { CONFIG } from '@server/initializers/config' | |||
4 | import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models' | 4 | import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models' |
5 | import { getHLSDirectory } from '../paths' | 5 | import { getHLSDirectory } from '../paths' |
6 | import { VideoPathManager } from '../video-path-manager' | 6 | import { VideoPathManager } from '../video-path-manager' |
7 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' | 7 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebVideoObjectStorageKey } from './keys' |
8 | import { | 8 | import { |
9 | createObjectReadStream, | 9 | createObjectReadStream, |
10 | listKeysOfPrefix, | 10 | listKeysOfPrefix, |
@@ -55,21 +55,21 @@ function storeHLSFileFromContent (playlist: MStreamingPlaylistVideo, path: strin | |||
55 | 55 | ||
56 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
57 | 57 | ||
58 | function storeWebTorrentFile (video: MVideo, file: MVideoFile) { | 58 | function storeWebVideoFile (video: MVideo, file: MVideoFile) { |
59 | return storeObject({ | 59 | return storeObject({ |
60 | inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), | 60 | inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), |
61 | objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), | 61 | objectStorageKey: generateWebVideoObjectStorageKey(file.filename), |
62 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 62 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
63 | isPrivate: video.hasPrivateStaticPath() | 63 | isPrivate: video.hasPrivateStaticPath() |
64 | }) | 64 | }) |
65 | } | 65 | } |
66 | 66 | ||
67 | // --------------------------------------------------------------------------- | 67 | // --------------------------------------------------------------------------- |
68 | 68 | ||
69 | async function updateWebTorrentFileACL (video: MVideo, file: MVideoFile) { | 69 | async function updateWebVideoFileACL (video: MVideo, file: MVideoFile) { |
70 | await updateObjectACL({ | 70 | await updateObjectACL({ |
71 | objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), | 71 | objectStorageKey: generateWebVideoObjectStorageKey(file.filename), |
72 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 72 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
73 | isPrivate: video.hasPrivateStaticPath() | 73 | isPrivate: video.hasPrivateStaticPath() |
74 | }) | 74 | }) |
75 | } | 75 | } |
@@ -102,8 +102,8 @@ function removeHLSFileObjectStorageByFullKey (key: string) { | |||
102 | 102 | ||
103 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
104 | 104 | ||
105 | function removeWebTorrentObjectStorage (videoFile: MVideoFile) { | 105 | function removeWebVideoObjectStorage (videoFile: MVideoFile) { |
106 | return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS) | 106 | return removeObject(generateWebVideoObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.WEB_VIDEOS) |
107 | } | 107 | } |
108 | 108 | ||
109 | // --------------------------------------------------------------------------- | 109 | // --------------------------------------------------------------------------- |
@@ -122,15 +122,15 @@ async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename | |||
122 | return destination | 122 | return destination |
123 | } | 123 | } |
124 | 124 | ||
125 | async function makeWebTorrentFileAvailable (filename: string, destination: string) { | 125 | async function makeWebVideoFileAvailable (filename: string, destination: string) { |
126 | const key = generateWebTorrentObjectStorageKey(filename) | 126 | const key = generateWebVideoObjectStorageKey(filename) |
127 | 127 | ||
128 | logger.info('Fetching WebTorrent file %s from object storage to %s.', key, destination, lTags()) | 128 | logger.info('Fetching Web Video file %s from object storage to %s.', key, destination, lTags()) |
129 | 129 | ||
130 | await makeAvailable({ | 130 | await makeAvailable({ |
131 | key, | 131 | key, |
132 | destination, | 132 | destination, |
133 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS | 133 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS |
134 | }) | 134 | }) |
135 | 135 | ||
136 | return destination | 136 | return destination |
@@ -138,17 +138,17 @@ async function makeWebTorrentFileAvailable (filename: string, destination: strin | |||
138 | 138 | ||
139 | // --------------------------------------------------------------------------- | 139 | // --------------------------------------------------------------------------- |
140 | 140 | ||
141 | function getWebTorrentFileReadStream (options: { | 141 | function getWebVideoFileReadStream (options: { |
142 | filename: string | 142 | filename: string |
143 | rangeHeader: string | 143 | rangeHeader: string |
144 | }) { | 144 | }) { |
145 | const { filename, rangeHeader } = options | 145 | const { filename, rangeHeader } = options |
146 | 146 | ||
147 | const key = generateWebTorrentObjectStorageKey(filename) | 147 | const key = generateWebVideoObjectStorageKey(filename) |
148 | 148 | ||
149 | return createObjectReadStream({ | 149 | return createObjectReadStream({ |
150 | key, | 150 | key, |
151 | bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS, | 151 | bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS, |
152 | rangeHeader | 152 | rangeHeader |
153 | }) | 153 | }) |
154 | } | 154 | } |
@@ -174,12 +174,12 @@ function getHLSFileReadStream (options: { | |||
174 | export { | 174 | export { |
175 | listHLSFileKeysOf, | 175 | listHLSFileKeysOf, |
176 | 176 | ||
177 | storeWebTorrentFile, | 177 | storeWebVideoFile, |
178 | storeHLSFileFromFilename, | 178 | storeHLSFileFromFilename, |
179 | storeHLSFileFromPath, | 179 | storeHLSFileFromPath, |
180 | storeHLSFileFromContent, | 180 | storeHLSFileFromContent, |
181 | 181 | ||
182 | updateWebTorrentFileACL, | 182 | updateWebVideoFileACL, |
183 | updateHLSFilesACL, | 183 | updateHLSFilesACL, |
184 | 184 | ||
185 | removeHLSObjectStorage, | 185 | removeHLSObjectStorage, |
@@ -187,11 +187,11 @@ export { | |||
187 | removeHLSFileObjectStorageByPath, | 187 | removeHLSFileObjectStorageByPath, |
188 | removeHLSFileObjectStorageByFullKey, | 188 | removeHLSFileObjectStorageByFullKey, |
189 | 189 | ||
190 | removeWebTorrentObjectStorage, | 190 | removeWebVideoObjectStorage, |
191 | 191 | ||
192 | makeWebTorrentFileAvailable, | 192 | makeWebVideoFileAvailable, |
193 | makeHLSFileAvailable, | 193 | makeHLSFileAvailable, |
194 | 194 | ||
195 | getWebTorrentFileReadStream, | 195 | getWebVideoFileReadStream, |
196 | getHLSFileReadStream | 196 | getHLSFileReadStream |
197 | } | 197 | } |
diff --git a/server/lib/paths.ts b/server/lib/paths.ts index 470970f55..db1cdede2 100644 --- a/server/lib/paths.ts +++ b/server/lib/paths.ts | |||
@@ -8,7 +8,7 @@ import { isVideoInPrivateDirectory } from './video-privacy' | |||
8 | 8 | ||
9 | // ################## Video file name ################## | 9 | // ################## Video file name ################## |
10 | 10 | ||
11 | function generateWebTorrentVideoFilename (resolution: number, extname: string) { | 11 | function generateWebVideoFilename (resolution: number, extname: string) { |
12 | return buildUUID() + '-' + resolution + extname | 12 | return buildUUID() + '-' + resolution + extname |
13 | } | 13 | } |
14 | 14 | ||
@@ -76,7 +76,7 @@ function getFSTorrentFilePath (videoFile: MVideoFile) { | |||
76 | 76 | ||
77 | export { | 77 | export { |
78 | generateHLSVideoFilename, | 78 | generateHLSVideoFilename, |
79 | generateWebTorrentVideoFilename, | 79 | generateWebVideoFilename, |
80 | 80 | ||
81 | generateTorrentFileName, | 81 | generateTorrentFileName, |
82 | getFSTorrentFilePath, | 82 | getFSTorrentFilePath, |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index d235f52c0..b4e3eece4 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -104,7 +104,7 @@ function buildVideosHelpers () { | |||
104 | const video = await VideoModel.loadFull(id) | 104 | const video = await VideoModel.loadFull(id) |
105 | if (!video) return undefined | 105 | if (!video) return undefined |
106 | 106 | ||
107 | const webtorrentVideoFiles = (video.VideoFiles || []).map(f => ({ | 107 | const webVideoFiles = (video.VideoFiles || []).map(f => ({ |
108 | path: f.storage === VideoStorage.FILE_SYSTEM | 108 | path: f.storage === VideoStorage.FILE_SYSTEM |
109 | ? VideoPathManager.Instance.getFSVideoFileOutputPath(video, f) | 109 | ? VideoPathManager.Instance.getFSVideoFileOutputPath(video, f) |
110 | : null, | 110 | : null, |
@@ -138,8 +138,12 @@ function buildVideosHelpers () { | |||
138 | })) | 138 | })) |
139 | 139 | ||
140 | return { | 140 | return { |
141 | webtorrent: { | 141 | webtorrent: { // TODO: remove in v7 |
142 | videoFiles: webtorrentVideoFiles | 142 | videoFiles: webVideoFiles |
143 | }, | ||
144 | |||
145 | webVideo: { | ||
146 | videoFiles: webVideoFiles | ||
143 | }, | 147 | }, |
144 | 148 | ||
145 | hls: { | 149 | hls: { |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 8430b2227..48d9986b5 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -325,8 +325,8 @@ class Redis { | |||
325 | const value = await this.getValue('resumable-upload-' + uploadId) | 325 | const value = await this.getValue('resumable-upload-' + uploadId) |
326 | 326 | ||
327 | return value | 327 | return value |
328 | ? JSON.parse(value) | 328 | ? JSON.parse(value) as { video: { id: number, shortUUID: string, uuid: string } } |
329 | : '' | 329 | : undefined |
330 | } | 330 | } |
331 | 331 | ||
332 | deleteUploadSession (uploadId: string) { | 332 | deleteUploadSession (uploadId: string) { |
diff --git a/server/lib/runners/job-handlers/shared/vod-helpers.ts b/server/lib/runners/job-handlers/shared/vod-helpers.ts index 93ae89ff8..1a2ad02ca 100644 --- a/server/lib/runners/job-handlers/shared/vod-helpers.ts +++ b/server/lib/runners/job-handlers/shared/vod-helpers.ts | |||
@@ -2,7 +2,7 @@ import { move } from 'fs-extra' | |||
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 4 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
5 | import { onWebTorrentVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding' | 5 | import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding' |
6 | import { buildNewFile } from '@server/lib/video-file' | 6 | import { buildNewFile } from '@server/lib/video-file' |
7 | import { VideoModel } from '@server/models/video/video' | 7 | import { VideoModel } from '@server/models/video/video' |
8 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
@@ -22,7 +22,7 @@ export async function onVODWebVideoOrAudioMergeTranscodingJob (options: { | |||
22 | const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename) | 22 | const newVideoFilePath = join(dirname(videoFilePath), videoFile.filename) |
23 | await move(videoFilePath, newVideoFilePath) | 23 | await move(videoFilePath, newVideoFilePath) |
24 | 24 | ||
25 | await onWebTorrentVideoFileTranscoding({ | 25 | await onWebVideoFileTranscoding({ |
26 | video, | 26 | video, |
27 | videoFile, | 27 | videoFile, |
28 | videoOutputPath: newVideoFilePath | 28 | videoOutputPath: newVideoFilePath |
diff --git a/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts b/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts index 5f247d792..905007db9 100644 --- a/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts +++ b/server/lib/runners/job-handlers/vod-audio-merge-transcoding-job-handler.ts | |||
@@ -83,7 +83,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo | |||
83 | 83 | ||
84 | // We can remove the old audio file | 84 | // We can remove the old audio file |
85 | const oldAudioFile = video.VideoFiles[0] | 85 | const oldAudioFile = video.VideoFiles[0] |
86 | await video.removeWebTorrentFile(oldAudioFile) | 86 | await video.removeWebVideoFile(oldAudioFile) |
87 | await oldAudioFile.destroy() | 87 | await oldAudioFile.destroy() |
88 | video.VideoFiles = [] | 88 | video.VideoFiles = [] |
89 | 89 | ||
diff --git a/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts b/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts index cc94bcbda..02845952c 100644 --- a/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts +++ b/server/lib/runners/job-handlers/vod-hls-transcoding-job-handler.ts | |||
@@ -5,7 +5,7 @@ import { renameVideoFileInPlaylist } from '@server/lib/hls' | |||
5 | import { getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 5 | import { getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
6 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' | 6 | import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding' |
7 | import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding' | 7 | import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding' |
8 | import { buildNewFile, removeAllWebTorrentFiles } from '@server/lib/video-file' | 8 | import { buildNewFile, removeAllWebVideoFiles } from '@server/lib/video-file' |
9 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 9 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
10 | import { MVideo } from '@server/types/models' | 10 | import { MVideo } from '@server/types/models' |
11 | import { MRunnerJob } from '@server/types/models/runners' | 11 | import { MRunnerJob } from '@server/types/models/runners' |
@@ -106,7 +106,7 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle | |||
106 | if (privatePayload.deleteWebVideoFiles === true) { | 106 | if (privatePayload.deleteWebVideoFiles === true) { |
107 | logger.info('Removing web video files of %s now we have a HLS version of it.', video.uuid, this.lTags(video.uuid)) | 107 | logger.info('Removing web video files of %s now we have a HLS version of it.', video.uuid, this.lTags(video.uuid)) |
108 | 108 | ||
109 | await removeAllWebTorrentFiles(video) | 109 | await removeAllWebVideoFiles(video) |
110 | } | 110 | } |
111 | 111 | ||
112 | logger.info('Runner VOD HLS job %s for %s ended.', runnerJob.uuid, video.uuid, this.lTags(runnerJob.uuid, video.uuid)) | 112 | logger.info('Runner VOD HLS job %s for %s ended.', runnerJob.uuid, video.uuid, this.lTags(runnerJob.uuid, video.uuid)) |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index dc450c338..24d340a73 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -23,7 +23,7 @@ import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlayli | |||
23 | import { getOrCreateAPVideo } from '../activitypub/videos' | 23 | import { getOrCreateAPVideo } from '../activitypub/videos' |
24 | import { downloadPlaylistSegments } from '../hls' | 24 | import { downloadPlaylistSegments } from '../hls' |
25 | import { removeVideoRedundancy } from '../redundancy' | 25 | import { removeVideoRedundancy } from '../redundancy' |
26 | import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-urls' | 26 | import { generateHLSRedundancyUrl, generateWebVideoRedundancyUrl } from '../video-urls' |
27 | import { AbstractScheduler } from './abstract-scheduler' | 27 | import { AbstractScheduler } from './abstract-scheduler' |
28 | 28 | ||
29 | const lTags = loggerTagsFactory('redundancy') | 29 | const lTags = loggerTagsFactory('redundancy') |
@@ -244,7 +244,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
244 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ | 244 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ |
245 | expiresOn, | 245 | expiresOn, |
246 | url: getLocalVideoCacheFileActivityPubUrl(file), | 246 | url: getLocalVideoCacheFileActivityPubUrl(file), |
247 | fileUrl: generateWebTorrentRedundancyUrl(file), | 247 | fileUrl: generateWebVideoRedundancyUrl(file), |
248 | strategy, | 248 | strategy, |
249 | videoFileId: file.id, | 249 | videoFileId: file.id, |
250 | actorId: serverActor.id | 250 | actorId: serverActor.id |
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index 924adb337..5ce89b16d 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts | |||
@@ -132,8 +132,8 @@ class ServerConfigManager { | |||
132 | hls: { | 132 | hls: { |
133 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.HLS.ENABLED | 133 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.HLS.ENABLED |
134 | }, | 134 | }, |
135 | webtorrent: { | 135 | web_videos: { |
136 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 136 | enabled: CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED |
137 | }, | 137 | }, |
138 | enabledResolutions: this.getEnabledResolutions('vod'), | 138 | enabledResolutions: this.getEnabledResolutions('vod'), |
139 | profile: CONFIG.TRANSCODING.PROFILE, | 139 | profile: CONFIG.TRANSCODING.PROFILE, |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 02b867a91..d95442795 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -7,13 +7,12 @@ import { ThumbnailModel } from '../models/video/thumbnail' | |||
7 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' | 7 | import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' |
8 | import { MThumbnail } from '../types/models/video/thumbnail' | 8 | import { MThumbnail } from '../types/models/video/thumbnail' |
9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
10 | import { downloadImageFromWorker } from './local-actor' | ||
11 | import { VideoPathManager } from './video-path-manager' | 10 | import { VideoPathManager } from './video-path-manager' |
12 | import { processImageFromWorker } from './worker/parent-process' | 11 | import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' |
13 | 12 | ||
14 | type ImageSize = { height?: number, width?: number } | 13 | type ImageSize = { height?: number, width?: number } |
15 | 14 | ||
16 | function updatePlaylistMiniatureFromExisting (options: { | 15 | function updateLocalPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string | 16 | inputPath: string |
18 | playlist: MVideoPlaylistThumbnail | 17 | playlist: MVideoPlaylistThumbnail |
19 | automaticallyGenerated: boolean | 18 | automaticallyGenerated: boolean |
@@ -35,11 +34,12 @@ function updatePlaylistMiniatureFromExisting (options: { | |||
35 | width, | 34 | width, |
36 | type, | 35 | type, |
37 | automaticallyGenerated, | 36 | automaticallyGenerated, |
37 | onDisk: true, | ||
38 | existingThumbnail | 38 | existingThumbnail |
39 | }) | 39 | }) |
40 | } | 40 | } |
41 | 41 | ||
42 | function updatePlaylistMiniatureFromUrl (options: { | 42 | function updateRemotePlaylistMiniatureFromUrl (options: { |
43 | downloadUrl: string | 43 | downloadUrl: string |
44 | playlist: MVideoPlaylistThumbnail | 44 | playlist: MVideoPlaylistThumbnail |
45 | size?: ImageSize | 45 | size?: ImageSize |
@@ -57,42 +57,10 @@ function updatePlaylistMiniatureFromUrl (options: { | |||
57 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | 57 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) |
58 | } | 58 | } |
59 | 59 | ||
60 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 60 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) |
61 | } | 61 | } |
62 | 62 | ||
63 | function updateVideoMiniatureFromUrl (options: { | 63 | function updateLocalVideoMiniatureFromExisting (options: { |
64 | downloadUrl: string | ||
65 | video: MVideoThumbnail | ||
66 | type: ThumbnailType | ||
67 | size?: ImageSize | ||
68 | }) { | ||
69 | const { downloadUrl, video, type, size } = options | ||
70 | const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
71 | |||
72 | // Only save the file URL if it is a remote video | ||
73 | const fileUrl = video.isOwned() | ||
74 | ? null | ||
75 | : downloadUrl | ||
76 | |||
77 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video) | ||
78 | |||
79 | // Do not change the thumbnail filename if the file did not change | ||
80 | const filename = thumbnailUrlChanged | ||
81 | ? updatedFilename | ||
82 | : existingThumbnail.filename | ||
83 | |||
84 | const thumbnailCreator = () => { | ||
85 | if (thumbnailUrlChanged) { | ||
86 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
87 | } | ||
88 | |||
89 | return Promise.resolve() | ||
90 | } | ||
91 | |||
92 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | ||
93 | } | ||
94 | |||
95 | function updateVideoMiniatureFromExisting (options: { | ||
96 | inputPath: string | 64 | inputPath: string |
97 | video: MVideoThumbnail | 65 | video: MVideoThumbnail |
98 | type: ThumbnailType | 66 | type: ThumbnailType |
@@ -115,11 +83,12 @@ function updateVideoMiniatureFromExisting (options: { | |||
115 | width, | 83 | width, |
116 | type, | 84 | type, |
117 | automaticallyGenerated, | 85 | automaticallyGenerated, |
118 | existingThumbnail | 86 | existingThumbnail, |
87 | onDisk: true | ||
119 | }) | 88 | }) |
120 | } | 89 | } |
121 | 90 | ||
122 | function generateVideoMiniature (options: { | 91 | function generateLocalVideoMiniature (options: { |
123 | video: MVideoThumbnail | 92 | video: MVideoThumbnail |
124 | videoFile: MVideoFile | 93 | videoFile: MVideoFile |
125 | type: ThumbnailType | 94 | type: ThumbnailType |
@@ -150,34 +119,68 @@ function generateVideoMiniature (options: { | |||
150 | width, | 119 | width, |
151 | type, | 120 | type, |
152 | automaticallyGenerated: true, | 121 | automaticallyGenerated: true, |
122 | onDisk: true, | ||
153 | existingThumbnail | 123 | existingThumbnail |
154 | }) | 124 | }) |
155 | }) | 125 | }) |
156 | } | 126 | } |
157 | 127 | ||
158 | function updatePlaceholderThumbnail (options: { | 128 | // --------------------------------------------------------------------------- |
159 | fileUrl: string | 129 | |
130 | function updateLocalVideoMiniatureFromUrl (options: { | ||
131 | downloadUrl: string | ||
160 | video: MVideoThumbnail | 132 | video: MVideoThumbnail |
161 | type: ThumbnailType | 133 | type: ThumbnailType |
162 | size: ImageSize | 134 | size?: ImageSize |
163 | }) { | 135 | }) { |
164 | const { fileUrl, video, type, size } = options | 136 | const { downloadUrl, video, type, size } = options |
165 | const { filename: updatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 137 | const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
166 | 138 | ||
167 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video) | 139 | // Only save the file URL if it is a remote video |
140 | const fileUrl = video.isOwned() | ||
141 | ? null | ||
142 | : downloadUrl | ||
168 | 143 | ||
169 | const thumbnail = existingThumbnail || new ThumbnailModel() | 144 | const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video) |
170 | 145 | ||
171 | // Do not change the thumbnail filename if the file did not change | 146 | // Do not change the thumbnail filename if the file did not change |
172 | const filename = thumbnailUrlChanged | 147 | const filename = thumbnailUrlChanged |
173 | ? updatedFilename | 148 | ? updatedFilename |
174 | : existingThumbnail.filename | 149 | : existingThumbnail.filename |
175 | 150 | ||
176 | thumbnail.filename = filename | 151 | const thumbnailCreator = () => { |
152 | if (thumbnailUrlChanged) { | ||
153 | return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) | ||
154 | } | ||
155 | |||
156 | return Promise.resolve() | ||
157 | } | ||
158 | |||
159 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) | ||
160 | } | ||
161 | |||
162 | function updateRemoteVideoThumbnail (options: { | ||
163 | fileUrl: string | ||
164 | video: MVideoThumbnail | ||
165 | type: ThumbnailType | ||
166 | size: ImageSize | ||
167 | onDisk: boolean | ||
168 | }) { | ||
169 | const { fileUrl, video, type, size, onDisk } = options | ||
170 | const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
171 | |||
172 | const thumbnail = existingThumbnail || new ThumbnailModel() | ||
173 | |||
174 | // Do not change the thumbnail filename if the file did not change | ||
175 | if (hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)) { | ||
176 | thumbnail.filename = generatedFilename | ||
177 | } | ||
178 | |||
177 | thumbnail.height = height | 179 | thumbnail.height = height |
178 | thumbnail.width = width | 180 | thumbnail.width = width |
179 | thumbnail.type = type | 181 | thumbnail.type = type |
180 | thumbnail.fileUrl = fileUrl | 182 | thumbnail.fileUrl = fileUrl |
183 | thumbnail.onDisk = onDisk | ||
181 | 184 | ||
182 | return thumbnail | 185 | return thumbnail |
183 | } | 186 | } |
@@ -185,14 +188,18 @@ function updatePlaceholderThumbnail (options: { | |||
185 | // --------------------------------------------------------------------------- | 188 | // --------------------------------------------------------------------------- |
186 | 189 | ||
187 | export { | 190 | export { |
188 | generateVideoMiniature, | 191 | generateLocalVideoMiniature, |
189 | updateVideoMiniatureFromUrl, | 192 | updateLocalVideoMiniatureFromUrl, |
190 | updateVideoMiniatureFromExisting, | 193 | updateLocalVideoMiniatureFromExisting, |
191 | updatePlaceholderThumbnail, | 194 | updateRemoteVideoThumbnail, |
192 | updatePlaylistMiniatureFromUrl, | 195 | updateRemotePlaylistMiniatureFromUrl, |
193 | updatePlaylistMiniatureFromExisting | 196 | updateLocalPlaylistMiniatureFromExisting |
194 | } | 197 | } |
195 | 198 | ||
199 | // --------------------------------------------------------------------------- | ||
200 | // Private | ||
201 | // --------------------------------------------------------------------------- | ||
202 | |||
196 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { | 203 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { |
197 | const existingUrl = existingThumbnail | 204 | const existingUrl = existingThumbnail |
198 | ? existingThumbnail.fileUrl | 205 | ? existingThumbnail.fileUrl |
@@ -258,6 +265,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
258 | height: number | 265 | height: number |
259 | width: number | 266 | width: number |
260 | type: ThumbnailType | 267 | type: ThumbnailType |
268 | onDisk: boolean | ||
261 | automaticallyGenerated?: boolean | 269 | automaticallyGenerated?: boolean |
262 | fileUrl?: string | 270 | fileUrl?: string |
263 | existingThumbnail?: MThumbnail | 271 | existingThumbnail?: MThumbnail |
@@ -269,6 +277,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
269 | height, | 277 | height, |
270 | type, | 278 | type, |
271 | existingThumbnail, | 279 | existingThumbnail, |
280 | onDisk, | ||
272 | automaticallyGenerated = null, | 281 | automaticallyGenerated = null, |
273 | fileUrl = null | 282 | fileUrl = null |
274 | } = parameters | 283 | } = parameters |
@@ -285,6 +294,7 @@ async function updateThumbnailFromFunction (parameters: { | |||
285 | thumbnail.type = type | 294 | thumbnail.type = type |
286 | thumbnail.fileUrl = fileUrl | 295 | thumbnail.fileUrl = fileUrl |
287 | thumbnail.automaticallyGenerated = automaticallyGenerated | 296 | thumbnail.automaticallyGenerated = automaticallyGenerated |
297 | thumbnail.onDisk = onDisk | ||
288 | 298 | ||
289 | if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename | 299 | if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename |
290 | 300 | ||
diff --git a/server/lib/transcoding/create-transcoding-job.ts b/server/lib/transcoding/create-transcoding-job.ts index abe32684d..d78e68b87 100644 --- a/server/lib/transcoding/create-transcoding-job.ts +++ b/server/lib/transcoding/create-transcoding-job.ts | |||
@@ -15,7 +15,7 @@ export function createOptimizeOrMergeAudioJobs (options: { | |||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | export function createTranscodingJobs (options: { | 17 | export function createTranscodingJobs (options: { |
18 | transcodingType: 'hls' | 'webtorrent' | 18 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
19 | video: MVideoFullLight | 19 | video: MVideoFullLight |
20 | resolutions: number[] | 20 | resolutions: number[] |
21 | isNewVideo: boolean | 21 | isNewVideo: boolean |
diff --git a/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts index 80dc05bfb..15fc814ae 100644 --- a/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts +++ b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts | |||
@@ -12,7 +12,7 @@ export abstract class AbstractJobBuilder { | |||
12 | }): Promise<any> | 12 | }): Promise<any> |
13 | 13 | ||
14 | abstract createTranscodingJobs (options: { | 14 | abstract createTranscodingJobs (options: { |
15 | transcodingType: 'hls' | 'webtorrent' | 15 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
16 | video: MVideoFullLight | 16 | video: MVideoFullLight |
17 | resolutions: number[] | 17 | resolutions: number[] |
18 | isNewVideo: boolean | 18 | isNewVideo: boolean |
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts index 4f802e2a6..0505c2b2f 100644 --- a/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts +++ b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts | |||
@@ -12,7 +12,7 @@ import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAud | |||
12 | import { | 12 | import { |
13 | HLSTranscodingPayload, | 13 | HLSTranscodingPayload, |
14 | MergeAudioTranscodingPayload, | 14 | MergeAudioTranscodingPayload, |
15 | NewWebTorrentResolutionTranscodingPayload, | 15 | NewWebVideoResolutionTranscodingPayload, |
16 | OptimizeTranscodingPayload, | 16 | OptimizeTranscodingPayload, |
17 | VideoTranscodingPayload | 17 | VideoTranscodingPayload |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
@@ -33,7 +33,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
33 | const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options | 33 | const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options |
34 | 34 | ||
35 | let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload | 35 | let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload |
36 | let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] | 36 | let nextTranscodingSequentialJobPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] |
37 | 37 | ||
38 | const mutexReleaser = videoFileAlreadyLocked | 38 | const mutexReleaser = videoFileAlreadyLocked |
39 | ? () => {} | 39 | ? () => {} |
@@ -60,7 +60,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
60 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { | 60 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { |
61 | nextTranscodingSequentialJobPayloads.push([ | 61 | nextTranscodingSequentialJobPayloads.push([ |
62 | this.buildHLSJobPayload({ | 62 | this.buildHLSJobPayload({ |
63 | deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false, | 63 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false, |
64 | 64 | ||
65 | // We had some issues with a web video quick transcoded while producing a HLS version of it | 65 | // We had some issues with a web video quick transcoded while producing a HLS version of it |
66 | copyCodecs: !quickTranscode, | 66 | copyCodecs: !quickTranscode, |
@@ -116,7 +116,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
116 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
117 | 117 | ||
118 | async createTranscodingJobs (options: { | 118 | async createTranscodingJobs (options: { |
119 | transcodingType: 'hls' | 'webtorrent' | 119 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
120 | video: MVideoFullLight | 120 | video: MVideoFullLight |
121 | resolutions: number[] | 121 | resolutions: number[] |
122 | isNewVideo: boolean | 122 | isNewVideo: boolean |
@@ -138,8 +138,8 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
138 | return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) | 138 | return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) |
139 | } | 139 | } |
140 | 140 | ||
141 | if (transcodingType === 'webtorrent') { | 141 | if (transcodingType === 'webtorrent' || transcodingType === 'web-video') { |
142 | return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) | 142 | return this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo }) |
143 | } | 143 | } |
144 | 144 | ||
145 | throw new Error('Unknown transcoding type') | 145 | throw new Error('Unknown transcoding type') |
@@ -149,7 +149,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
149 | 149 | ||
150 | const parent = transcodingType === 'hls' | 150 | const parent = transcodingType === 'hls' |
151 | ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) | 151 | ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) |
152 | : this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) | 152 | : this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo }) |
153 | 153 | ||
154 | // Process the last resolution after the other ones to prevent concurrency issue | 154 | // Process the last resolution after the other ones to prevent concurrency issue |
155 | // Because low resolutions use the biggest one as ffmpeg input | 155 | // Because low resolutions use the biggest one as ffmpeg input |
@@ -160,8 +160,8 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
160 | 160 | ||
161 | private async createTranscodingJobsWithChildren (options: { | 161 | private async createTranscodingJobsWithChildren (options: { |
162 | videoUUID: string | 162 | videoUUID: string |
163 | parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload) | 163 | parent: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload) |
164 | children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[] | 164 | children: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)[] |
165 | user: MUserId | null | 165 | user: MUserId | null |
166 | }) { | 166 | }) { |
167 | const { videoUUID, parent, children, user } = options | 167 | const { videoUUID, parent, children, user } = options |
@@ -203,14 +203,14 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
203 | options | 203 | options |
204 | ) | 204 | ) |
205 | 205 | ||
206 | const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] | 206 | const sequentialPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = [] |
207 | 207 | ||
208 | for (const resolution of resolutionsEnabled) { | 208 | for (const resolution of resolutionsEnabled) { |
209 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) | 209 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) |
210 | 210 | ||
211 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { | 211 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) { |
212 | const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [ | 212 | const payloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[] = [ |
213 | this.buildWebTorrentJobPayload({ | 213 | this.buildWebVideoJobPayload({ |
214 | videoUUID: video.uuid, | 214 | videoUUID: video.uuid, |
215 | resolution, | 215 | resolution, |
216 | fps, | 216 | fps, |
@@ -253,10 +253,10 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
253 | resolution: number | 253 | resolution: number |
254 | fps: number | 254 | fps: number |
255 | isNewVideo: boolean | 255 | isNewVideo: boolean |
256 | deleteWebTorrentFiles?: boolean // default false | 256 | deleteWebVideoFiles?: boolean // default false |
257 | copyCodecs?: boolean // default false | 257 | copyCodecs?: boolean // default false |
258 | }): HLSTranscodingPayload { | 258 | }): HLSTranscodingPayload { |
259 | const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options | 259 | const { videoUUID, resolution, fps, isNewVideo, deleteWebVideoFiles = false, copyCodecs = false } = options |
260 | 260 | ||
261 | return { | 261 | return { |
262 | type: 'new-resolution-to-hls', | 262 | type: 'new-resolution-to-hls', |
@@ -265,20 +265,20 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
265 | fps, | 265 | fps, |
266 | copyCodecs, | 266 | copyCodecs, |
267 | isNewVideo, | 267 | isNewVideo, |
268 | deleteWebTorrentFiles | 268 | deleteWebVideoFiles |
269 | } | 269 | } |
270 | } | 270 | } |
271 | 271 | ||
272 | private buildWebTorrentJobPayload (options: { | 272 | private buildWebVideoJobPayload (options: { |
273 | videoUUID: string | 273 | videoUUID: string |
274 | resolution: number | 274 | resolution: number |
275 | fps: number | 275 | fps: number |
276 | isNewVideo: boolean | 276 | isNewVideo: boolean |
277 | }): NewWebTorrentResolutionTranscodingPayload { | 277 | }): NewWebVideoResolutionTranscodingPayload { |
278 | const { videoUUID, resolution, fps, isNewVideo } = options | 278 | const { videoUUID, resolution, fps, isNewVideo } = options |
279 | 279 | ||
280 | return { | 280 | return { |
281 | type: 'new-resolution-to-webtorrent', | 281 | type: 'new-resolution-to-web-video', |
282 | videoUUID, | 282 | videoUUID, |
283 | isNewVideo, | 283 | isNewVideo, |
284 | resolution, | 284 | resolution, |
@@ -294,7 +294,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
294 | const { videoUUID, isNewVideo, hasChildren } = options | 294 | const { videoUUID, isNewVideo, hasChildren } = options |
295 | 295 | ||
296 | return { | 296 | return { |
297 | type: 'merge-audio-to-webtorrent', | 297 | type: 'merge-audio-to-web-video', |
298 | resolution: DEFAULT_AUDIO_RESOLUTION, | 298 | resolution: DEFAULT_AUDIO_RESOLUTION, |
299 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, | 299 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, |
300 | videoUUID, | 300 | videoUUID, |
@@ -312,7 +312,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder { | |||
312 | const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options | 312 | const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options |
313 | 313 | ||
314 | return { | 314 | return { |
315 | type: 'optimize-to-webtorrent', | 315 | type: 'optimize-to-web-video', |
316 | videoUUID, | 316 | videoUUID, |
317 | isNewVideo, | 317 | isNewVideo, |
318 | hasChildren, | 318 | hasChildren, |
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts index ba2a46f44..f0671bd7a 100644 --- a/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts +++ b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts | |||
@@ -62,7 +62,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
62 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { | 62 | if (CONFIG.TRANSCODING.HLS.ENABLED === true) { |
63 | await new VODHLSTranscodingJobHandler().create({ | 63 | await new VODHLSTranscodingJobHandler().create({ |
64 | video, | 64 | video, |
65 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false, | 65 | deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false, |
66 | resolution: maxResolution, | 66 | resolution: maxResolution, |
67 | fps, | 67 | fps, |
68 | isNewVideo, | 68 | isNewVideo, |
@@ -89,7 +89,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
89 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
90 | 90 | ||
91 | async createTranscodingJobs (options: { | 91 | async createTranscodingJobs (options: { |
92 | transcodingType: 'hls' | 'webtorrent' | 92 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
93 | video: MVideoFullLight | 93 | video: MVideoFullLight |
94 | resolutions: number[] | 94 | resolutions: number[] |
95 | isNewVideo: boolean | 95 | isNewVideo: boolean |
@@ -130,7 +130,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
130 | continue | 130 | continue |
131 | } | 131 | } |
132 | 132 | ||
133 | if (transcodingType === 'webtorrent') { | 133 | if (transcodingType === 'webtorrent' || transcodingType === 'web-video') { |
134 | await new VODWebVideoTranscodingJobHandler().create({ | 134 | await new VODWebVideoTranscodingJobHandler().create({ |
135 | video, | 135 | video, |
136 | resolution, | 136 | resolution, |
@@ -169,7 +169,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder { | |||
169 | for (const resolution of resolutionsEnabled) { | 169 | for (const resolution of resolutionsEnabled) { |
170 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) | 170 | const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution }) |
171 | 171 | ||
172 | if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { | 172 | if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) { |
173 | await new VODWebVideoTranscodingJobHandler().create({ | 173 | await new VODWebVideoTranscodingJobHandler().create({ |
174 | video, | 174 | video, |
175 | resolution, | 175 | resolution, |
diff --git a/server/lib/transcoding/web-transcoding.ts b/server/lib/transcoding/web-transcoding.ts index 7cc8f20bc..f92d457a0 100644 --- a/server/lib/transcoding/web-transcoding.ts +++ b/server/lib/transcoding/web-transcoding.ts | |||
@@ -9,7 +9,8 @@ import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVOD | |||
9 | import { VideoResolution, VideoStorage } from '@shared/models' | 9 | import { VideoResolution, VideoStorage } from '@shared/models' |
10 | import { CONFIG } from '../../initializers/config' | 10 | import { CONFIG } from '../../initializers/config' |
11 | import { VideoFileModel } from '../../models/video/video-file' | 11 | import { VideoFileModel } from '../../models/video/video-file' |
12 | import { generateWebTorrentVideoFilename } from '../paths' | 12 | import { JobQueue } from '../job-queue' |
13 | import { generateWebVideoFilename } from '../paths' | ||
13 | import { buildFileMetadata } from '../video-file' | 14 | import { buildFileMetadata } from '../video-file' |
14 | import { VideoPathManager } from '../video-path-manager' | 15 | import { VideoPathManager } from '../video-path-manager' |
15 | import { buildFFmpegVOD } from './shared' | 16 | import { buildFFmpegVOD } from './shared' |
@@ -62,10 +63,10 @@ export async function optimizeOriginalVideofile (options: { | |||
62 | // Important to do this before getVideoFilename() to take in account the new filename | 63 | // Important to do this before getVideoFilename() to take in account the new filename |
63 | inputVideoFile.resolution = resolution | 64 | inputVideoFile.resolution = resolution |
64 | inputVideoFile.extname = newExtname | 65 | inputVideoFile.extname = newExtname |
65 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | 66 | inputVideoFile.filename = generateWebVideoFilename(resolution, newExtname) |
66 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | 67 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM |
67 | 68 | ||
68 | const { videoFile } = await onWebTorrentVideoFileTranscoding({ | 69 | const { videoFile } = await onWebVideoFileTranscoding({ |
69 | video, | 70 | video, |
70 | videoFile: inputVideoFile, | 71 | videoFile: inputVideoFile, |
71 | videoOutputPath | 72 | videoOutputPath |
@@ -82,8 +83,8 @@ export async function optimizeOriginalVideofile (options: { | |||
82 | } | 83 | } |
83 | } | 84 | } |
84 | 85 | ||
85 | // Transcode the original video file to a lower resolution compatible with WebTorrent | 86 | // Transcode the original video file to a lower resolution compatible with web browsers |
86 | export async function transcodeNewWebTorrentResolution (options: { | 87 | export async function transcodeNewWebVideoResolution (options: { |
87 | video: MVideoFullLight | 88 | video: MVideoFullLight |
88 | resolution: VideoResolution | 89 | resolution: VideoResolution |
89 | fps: number | 90 | fps: number |
@@ -104,7 +105,7 @@ export async function transcodeNewWebTorrentResolution (options: { | |||
104 | const newVideoFile = new VideoFileModel({ | 105 | const newVideoFile = new VideoFileModel({ |
105 | resolution, | 106 | resolution, |
106 | extname: newExtname, | 107 | extname: newExtname, |
107 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | 108 | filename: generateWebVideoFilename(resolution, newExtname), |
108 | size: 0, | 109 | size: 0, |
109 | videoId: video.id | 110 | videoId: video.id |
110 | }) | 111 | }) |
@@ -125,7 +126,7 @@ export async function transcodeNewWebTorrentResolution (options: { | |||
125 | 126 | ||
126 | await buildFFmpegVOD(job).transcode(transcodeOptions) | 127 | await buildFFmpegVOD(job).transcode(transcodeOptions) |
127 | 128 | ||
128 | return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) | 129 | return onWebVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) |
129 | }) | 130 | }) |
130 | 131 | ||
131 | return result | 132 | return result |
@@ -188,17 +189,18 @@ export async function mergeAudioVideofile (options: { | |||
188 | // Important to do this before getVideoFilename() to take in account the new file extension | 189 | // Important to do this before getVideoFilename() to take in account the new file extension |
189 | inputVideoFile.extname = newExtname | 190 | inputVideoFile.extname = newExtname |
190 | inputVideoFile.resolution = resolution | 191 | inputVideoFile.resolution = resolution |
191 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | 192 | inputVideoFile.filename = generateWebVideoFilename(inputVideoFile.resolution, newExtname) |
192 | 193 | ||
193 | // ffmpeg generated a new video file, so update the video duration | 194 | // ffmpeg generated a new video file, so update the video duration |
194 | // See https://trac.ffmpeg.org/ticket/5456 | 195 | // See https://trac.ffmpeg.org/ticket/5456 |
195 | video.duration = await getVideoStreamDuration(videoOutputPath) | 196 | video.duration = await getVideoStreamDuration(videoOutputPath) |
196 | await video.save() | 197 | await video.save() |
197 | 198 | ||
198 | return onWebTorrentVideoFileTranscoding({ | 199 | return onWebVideoFileTranscoding({ |
199 | video, | 200 | video, |
200 | videoFile: inputVideoFile, | 201 | videoFile: inputVideoFile, |
201 | videoOutputPath | 202 | videoOutputPath, |
203 | wasAudioFile: true | ||
202 | }) | 204 | }) |
203 | }) | 205 | }) |
204 | 206 | ||
@@ -208,12 +210,13 @@ export async function mergeAudioVideofile (options: { | |||
208 | } | 210 | } |
209 | } | 211 | } |
210 | 212 | ||
211 | export async function onWebTorrentVideoFileTranscoding (options: { | 213 | export async function onWebVideoFileTranscoding (options: { |
212 | video: MVideoFullLight | 214 | video: MVideoFullLight |
213 | videoFile: MVideoFile | 215 | videoFile: MVideoFile |
214 | videoOutputPath: string | 216 | videoOutputPath: string |
217 | wasAudioFile?: boolean // default false | ||
215 | }) { | 218 | }) { |
216 | const { video, videoFile, videoOutputPath } = options | 219 | const { video, videoFile, videoOutputPath, wasAudioFile } = options |
217 | 220 | ||
218 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 221 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
219 | 222 | ||
@@ -236,12 +239,23 @@ export async function onWebTorrentVideoFileTranscoding (options: { | |||
236 | 239 | ||
237 | await createTorrentAndSetInfoHash(video, videoFile) | 240 | await createTorrentAndSetInfoHash(video, videoFile) |
238 | 241 | ||
239 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | 242 | const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) |
240 | if (oldFile) await video.removeWebTorrentFile(oldFile) | 243 | if (oldFile) await video.removeWebVideoFile(oldFile) |
241 | 244 | ||
242 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 245 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) |
243 | video.VideoFiles = await video.$get('VideoFiles') | 246 | video.VideoFiles = await video.$get('VideoFiles') |
244 | 247 | ||
248 | if (wasAudioFile) { | ||
249 | await JobQueue.Instance.createJob({ | ||
250 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
251 | payload: { | ||
252 | videoUUID: video.uuid, | ||
253 | // No need to federate, we process these jobs sequentially | ||
254 | federate: false | ||
255 | } | ||
256 | }) | ||
257 | } | ||
258 | |||
245 | return { video, videoFile } | 259 | return { video, videoFile } |
246 | } finally { | 260 | } finally { |
247 | mutexReleaser() | 261 | mutexReleaser() |
diff --git a/server/lib/video-file.ts b/server/lib/video-file.ts index 88d48c945..46af67ccd 100644 --- a/server/lib/video-file.ts +++ b/server/lib/video-file.ts | |||
@@ -7,7 +7,7 @@ import { getFileSize } from '@shared/extra-utils' | |||
7 | import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@shared/ffmpeg' | 7 | import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@shared/ffmpeg' |
8 | import { VideoFileMetadata, VideoResolution } from '@shared/models' | 8 | import { VideoFileMetadata, VideoResolution } from '@shared/models' |
9 | import { lTags } from './object-storage/shared' | 9 | import { lTags } from './object-storage/shared' |
10 | import { generateHLSVideoFilename, generateWebTorrentVideoFilename } from './paths' | 10 | import { generateHLSVideoFilename, generateWebVideoFilename } from './paths' |
11 | import { VideoPathManager } from './video-path-manager' | 11 | import { VideoPathManager } from './video-path-manager' |
12 | 12 | ||
13 | async function buildNewFile (options: { | 13 | async function buildNewFile (options: { |
@@ -33,7 +33,7 @@ async function buildNewFile (options: { | |||
33 | } | 33 | } |
34 | 34 | ||
35 | videoFile.filename = mode === 'web-video' | 35 | videoFile.filename = mode === 'web-video' |
36 | ? generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) | 36 | ? generateWebVideoFilename(videoFile.resolution, videoFile.extname) |
37 | : generateHLSVideoFilename(videoFile.resolution) | 37 | : generateHLSVideoFilename(videoFile.resolution) |
38 | 38 | ||
39 | return videoFile | 39 | return videoFile |
@@ -85,12 +85,12 @@ async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) | |||
85 | 85 | ||
86 | // --------------------------------------------------------------------------- | 86 | // --------------------------------------------------------------------------- |
87 | 87 | ||
88 | async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) { | 88 | async function removeAllWebVideoFiles (video: MVideoWithAllFiles) { |
89 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 89 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
90 | 90 | ||
91 | try { | 91 | try { |
92 | for (const file of video.VideoFiles) { | 92 | for (const file of video.VideoFiles) { |
93 | await video.removeWebTorrentFile(file) | 93 | await video.removeWebVideoFile(file) |
94 | await file.destroy() | 94 | await file.destroy() |
95 | } | 95 | } |
96 | 96 | ||
@@ -102,17 +102,17 @@ async function removeAllWebTorrentFiles (video: MVideoWithAllFiles) { | |||
102 | return video | 102 | return video |
103 | } | 103 | } |
104 | 104 | ||
105 | async function removeWebTorrentFile (video: MVideoWithAllFiles, fileToDeleteId: number) { | 105 | async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: number) { |
106 | const files = video.VideoFiles | 106 | const files = video.VideoFiles |
107 | 107 | ||
108 | if (files.length === 1) { | 108 | if (files.length === 1) { |
109 | return removeAllWebTorrentFiles(video) | 109 | return removeAllWebVideoFiles(video) |
110 | } | 110 | } |
111 | 111 | ||
112 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | 112 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
113 | try { | 113 | try { |
114 | const toDelete = files.find(f => f.id === fileToDeleteId) | 114 | const toDelete = files.find(f => f.id === fileToDeleteId) |
115 | await video.removeWebTorrentFile(toDelete) | 115 | await video.removeWebVideoFile(toDelete) |
116 | await toDelete.destroy() | 116 | await toDelete.destroy() |
117 | 117 | ||
118 | video.VideoFiles = files.filter(f => f.id !== toDelete.id) | 118 | video.VideoFiles = files.filter(f => f.id !== toDelete.id) |
@@ -138,8 +138,8 @@ export { | |||
138 | 138 | ||
139 | removeHLSPlaylist, | 139 | removeHLSPlaylist, |
140 | removeHLSFile, | 140 | removeHLSFile, |
141 | removeAllWebTorrentFiles, | 141 | removeAllWebVideoFiles, |
142 | removeWebTorrentFile, | 142 | removeWebVideoFile, |
143 | 143 | ||
144 | buildFileMetadata | 144 | buildFileMetadata |
145 | } | 145 | } |
diff --git a/server/lib/video-path-manager.ts b/server/lib/video-path-manager.ts index 9953cae5d..133544bb2 100644 --- a/server/lib/video-path-manager.ts +++ b/server/lib/video-path-manager.ts | |||
@@ -8,7 +8,7 @@ import { DIRECTORIES } from '@server/initializers/constants' | |||
8 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' | 8 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' |
9 | import { buildUUID } from '@shared/extra-utils' | 9 | import { buildUUID } from '@shared/extra-utils' |
10 | import { VideoStorage } from '@shared/models' | 10 | import { VideoStorage } from '@shared/models' |
11 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' | 11 | import { makeHLSFileAvailable, makeWebVideoFileAvailable } from './object-storage' |
12 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' | 12 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' |
13 | import { isVideoInPrivateDirectory } from './video-privacy' | 13 | import { isVideoInPrivateDirectory } from './video-privacy' |
14 | 14 | ||
@@ -78,7 +78,7 @@ class VideoPathManager { | |||
78 | } | 78 | } |
79 | 79 | ||
80 | return this.makeAvailableFactory( | 80 | return this.makeAvailableFactory( |
81 | () => makeWebTorrentFileAvailable(videoFile.filename, destination), | 81 | () => makeWebVideoFileAvailable(videoFile.filename, destination), |
82 | true, | 82 | true, |
83 | cb | 83 | cb |
84 | ) | 84 | ) |
diff --git a/server/lib/video-pre-import.ts b/server/lib/video-pre-import.ts index df67dc953..381f1f535 100644 --- a/server/lib/video-pre-import.ts +++ b/server/lib/video-pre-import.ts | |||
@@ -29,7 +29,8 @@ import { | |||
29 | } from '@server/types/models' | 29 | } from '@server/types/models' |
30 | import { ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' | 30 | import { ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' |
31 | import { getLocalVideoActivityPubUrl } from './activitypub/url' | 31 | import { getLocalVideoActivityPubUrl } from './activitypub/url' |
32 | import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from './thumbnail' | 32 | import { updateLocalVideoMiniatureFromExisting, updateLocalVideoMiniatureFromUrl } from './thumbnail' |
33 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
33 | 34 | ||
34 | class YoutubeDlImportError extends Error { | 35 | class YoutubeDlImportError extends Error { |
35 | code: YoutubeDlImportError.CODE | 36 | code: YoutubeDlImportError.CODE |
@@ -64,8 +65,9 @@ async function insertFromImportIntoDB (parameters: { | |||
64 | tags: string[] | 65 | tags: string[] |
65 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> | 66 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> |
66 | user: MUser | 67 | user: MUser |
68 | videoPasswords?: string[] | ||
67 | }): Promise<MVideoImportFormattable> { | 69 | }): Promise<MVideoImportFormattable> { |
68 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters | 70 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user, videoPasswords } = parameters |
69 | 71 | ||
70 | const videoImport = await sequelizeTypescript.transaction(async t => { | 72 | const videoImport = await sequelizeTypescript.transaction(async t => { |
71 | const sequelizeOptions = { transaction: t } | 73 | const sequelizeOptions = { transaction: t } |
@@ -77,6 +79,10 @@ async function insertFromImportIntoDB (parameters: { | |||
77 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 79 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
78 | if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 80 | if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) |
79 | 81 | ||
82 | if (videoCreated.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
83 | await VideoPasswordModel.addPasswords(videoPasswords, video.id, t) | ||
84 | } | ||
85 | |||
80 | await autoBlacklistVideoIfNeeded({ | 86 | await autoBlacklistVideoIfNeeded({ |
81 | video: videoCreated, | 87 | video: videoCreated, |
82 | user, | 88 | user, |
@@ -208,7 +214,8 @@ async function buildYoutubeDLImport (options: { | |||
208 | state: VideoImportState.PENDING, | 214 | state: VideoImportState.PENDING, |
209 | userId: user.id, | 215 | userId: user.id, |
210 | videoChannelSyncId: channelSync?.id | 216 | videoChannelSyncId: channelSync?.id |
211 | } | 217 | }, |
218 | videoPasswords: importDataOverride.videoPasswords | ||
212 | }) | 219 | }) |
213 | 220 | ||
214 | // Get video subtitles | 221 | // Get video subtitles |
@@ -249,19 +256,22 @@ async function forgeThumbnail ({ inputPath, video, downloadUrl, type }: { | |||
249 | type: ThumbnailType | 256 | type: ThumbnailType |
250 | }): Promise<MThumbnail> { | 257 | }): Promise<MThumbnail> { |
251 | if (inputPath) { | 258 | if (inputPath) { |
252 | return updateVideoMiniatureFromExisting({ | 259 | return updateLocalVideoMiniatureFromExisting({ |
253 | inputPath, | 260 | inputPath, |
254 | video, | 261 | video, |
255 | type, | 262 | type, |
256 | automaticallyGenerated: false | 263 | automaticallyGenerated: false |
257 | }) | 264 | }) |
258 | } else if (downloadUrl) { | 265 | } |
266 | |||
267 | if (downloadUrl) { | ||
259 | try { | 268 | try { |
260 | return await updateVideoMiniatureFromUrl({ downloadUrl, video, type }) | 269 | return await updateLocalVideoMiniatureFromUrl({ downloadUrl, video, type }) |
261 | } catch (err) { | 270 | } catch (err) { |
262 | logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err }) | 271 | logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err }) |
263 | } | 272 | } |
264 | } | 273 | } |
274 | |||
265 | return null | 275 | return null |
266 | } | 276 | } |
267 | 277 | ||
diff --git a/server/lib/video-privacy.ts b/server/lib/video-privacy.ts index 41f9d62b3..5dd4d9781 100644 --- a/server/lib/video-privacy.ts +++ b/server/lib/video-privacy.ts | |||
@@ -4,7 +4,13 @@ import { logger } from '@server/helpers/logger' | |||
4 | import { DIRECTORIES } from '@server/initializers/constants' | 4 | import { DIRECTORIES } from '@server/initializers/constants' |
5 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 5 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { VideoPrivacy, VideoStorage } from '@shared/models' | 6 | import { VideoPrivacy, VideoStorage } from '@shared/models' |
7 | import { updateHLSFilesACL, updateWebTorrentFileACL } from './object-storage' | 7 | import { updateHLSFilesACL, updateWebVideoFileACL } from './object-storage' |
8 | |||
9 | const validPrivacySet = new Set([ | ||
10 | VideoPrivacy.PRIVATE, | ||
11 | VideoPrivacy.INTERNAL, | ||
12 | VideoPrivacy.PASSWORD_PROTECTED | ||
13 | ]) | ||
8 | 14 | ||
9 | function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { | 15 | function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { |
10 | if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { | 16 | if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { |
@@ -14,8 +20,8 @@ function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { | |||
14 | video.privacy = newPrivacy | 20 | video.privacy = newPrivacy |
15 | } | 21 | } |
16 | 22 | ||
17 | function isVideoInPrivateDirectory (privacy: VideoPrivacy) { | 23 | function isVideoInPrivateDirectory (privacy) { |
18 | return privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.INTERNAL | 24 | return validPrivacySet.has(privacy) |
19 | } | 25 | } |
20 | 26 | ||
21 | function isVideoInPublicDirectory (privacy: VideoPrivacy) { | 27 | function isVideoInPublicDirectory (privacy: VideoPrivacy) { |
@@ -61,9 +67,9 @@ async function moveFiles (options: { | |||
61 | 67 | ||
62 | for (const file of video.VideoFiles) { | 68 | for (const file of video.VideoFiles) { |
63 | if (file.storage === VideoStorage.FILE_SYSTEM) { | 69 | if (file.storage === VideoStorage.FILE_SYSTEM) { |
64 | await moveWebTorrentFileOnFS(type, video, file) | 70 | await moveWebVideoFileOnFS(type, video, file) |
65 | } else { | 71 | } else { |
66 | await updateWebTorrentFileACL(video, file) | 72 | await updateWebVideoFileACL(video, file) |
67 | } | 73 | } |
68 | } | 74 | } |
69 | 75 | ||
@@ -78,22 +84,22 @@ async function moveFiles (options: { | |||
78 | } | 84 | } |
79 | } | 85 | } |
80 | 86 | ||
81 | async function moveWebTorrentFileOnFS (type: MoveType, video: MVideo, file: MVideoFile) { | 87 | async function moveWebVideoFileOnFS (type: MoveType, video: MVideo, file: MVideoFile) { |
82 | const directories = getWebTorrentDirectories(type) | 88 | const directories = getWebVideoDirectories(type) |
83 | 89 | ||
84 | const source = join(directories.old, file.filename) | 90 | const source = join(directories.old, file.filename) |
85 | const destination = join(directories.new, file.filename) | 91 | const destination = join(directories.new, file.filename) |
86 | 92 | ||
87 | try { | 93 | try { |
88 | logger.info('Moving WebTorrent files of %s after privacy change (%s -> %s).', video.uuid, source, destination) | 94 | logger.info('Moving web video files of %s after privacy change (%s -> %s).', video.uuid, source, destination) |
89 | 95 | ||
90 | await move(source, destination) | 96 | await move(source, destination) |
91 | } catch (err) { | 97 | } catch (err) { |
92 | logger.error('Cannot move webtorrent file %s to %s after privacy change', source, destination, { err }) | 98 | logger.error('Cannot move web video file %s to %s after privacy change', source, destination, { err }) |
93 | } | 99 | } |
94 | } | 100 | } |
95 | 101 | ||
96 | function getWebTorrentDirectories (moveType: MoveType) { | 102 | function getWebVideoDirectories (moveType: MoveType) { |
97 | if (moveType === 'private-to-public') { | 103 | if (moveType === 'private-to-public') { |
98 | return { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC } | 104 | return { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC } |
99 | } | 105 | } |
diff --git a/server/lib/video-studio.ts b/server/lib/video-studio.ts index 0d3db8f60..f549a7084 100644 --- a/server/lib/video-studio.ts +++ b/server/lib/video-studio.ts | |||
@@ -12,7 +12,7 @@ import { JobQueue } from './job-queue' | |||
12 | import { VideoStudioTranscodingJobHandler } from './runners' | 12 | import { VideoStudioTranscodingJobHandler } from './runners' |
13 | import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job' | 13 | import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job' |
14 | import { getTranscodingJobPriority } from './transcoding/transcoding-priority' | 14 | import { getTranscodingJobPriority } from './transcoding/transcoding-priority' |
15 | import { buildNewFile, removeHLSPlaylist, removeWebTorrentFile } from './video-file' | 15 | import { buildNewFile, removeHLSPlaylist, removeWebVideoFile } from './video-file' |
16 | import { VideoPathManager } from './video-path-manager' | 16 | import { VideoPathManager } from './video-path-manager' |
17 | 17 | ||
18 | const lTags = loggerTagsFactory('video-studio') | 18 | const lTags = loggerTagsFactory('video-studio') |
@@ -119,12 +119,12 @@ export async function onVideoStudioEnded (options: { | |||
119 | // Private | 119 | // Private |
120 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
121 | 121 | ||
122 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { | 122 | async function removeAllFiles (video: MVideoWithAllFiles, webVideoFileException: MVideoFile) { |
123 | await removeHLSPlaylist(video) | 123 | await removeHLSPlaylist(video) |
124 | 124 | ||
125 | for (const file of video.VideoFiles) { | 125 | for (const file of video.VideoFiles) { |
126 | if (file.id === webTorrentFileException.id) continue | 126 | if (file.id === webVideoFileException.id) continue |
127 | 127 | ||
128 | await removeWebTorrentFile(video, file.id) | 128 | await removeWebVideoFile(video, file.id) |
129 | } | 129 | } |
130 | } | 130 | } |
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts index 660533528..e28e55cf7 100644 --- a/server/lib/video-tokens-manager.ts +++ b/server/lib/video-tokens-manager.ts | |||
@@ -12,26 +12,34 @@ class VideoTokensManager { | |||
12 | 12 | ||
13 | private static instance: VideoTokensManager | 13 | private static instance: VideoTokensManager |
14 | 14 | ||
15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user: MUserAccountUrl }>({ | 15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user?: MUserAccountUrl }>({ |
16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | 16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, |
17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | 17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL |
18 | }) | 18 | }) |
19 | 19 | ||
20 | private constructor () {} | 20 | private constructor () {} |
21 | 21 | ||
22 | create (options: { | 22 | createForAuthUser (options: { |
23 | user: MUserAccountUrl | 23 | user: MUserAccountUrl |
24 | videoUUID: string | 24 | videoUUID: string |
25 | }) { | 25 | }) { |
26 | const token = buildUUID() | 26 | const { token, expires } = this.generateVideoToken() |
27 | |||
28 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | ||
29 | 27 | ||
30 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) | 28 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) |
31 | 29 | ||
32 | return { token, expires } | 30 | return { token, expires } |
33 | } | 31 | } |
34 | 32 | ||
33 | createForPasswordProtectedVideo (options: { | ||
34 | videoUUID: string | ||
35 | }) { | ||
36 | const { token, expires } = this.generateVideoToken() | ||
37 | |||
38 | this.lruCache.set(token, pick(options, [ 'videoUUID' ])) | ||
39 | |||
40 | return { token, expires } | ||
41 | } | ||
42 | |||
35 | hasToken (options: { | 43 | hasToken (options: { |
36 | token: string | 44 | token: string |
37 | videoUUID: string | 45 | videoUUID: string |
@@ -54,6 +62,13 @@ class VideoTokensManager { | |||
54 | static get Instance () { | 62 | static get Instance () { |
55 | return this.instance || (this.instance = new this()) | 63 | return this.instance || (this.instance = new this()) |
56 | } | 64 | } |
65 | |||
66 | private generateVideoToken () { | ||
67 | const token = buildUUID() | ||
68 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | ||
69 | |||
70 | return { token, expires } | ||
71 | } | ||
57 | } | 72 | } |
58 | 73 | ||
59 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/video-urls.ts b/server/lib/video-urls.ts index 64c2c9bf9..0597488ad 100644 --- a/server/lib/video-urls.ts +++ b/server/lib/video-urls.ts | |||
@@ -9,7 +9,7 @@ function generateHLSRedundancyUrl (video: MVideo, playlist: MStreamingPlaylist) | |||
9 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid | 9 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + playlist.getStringType() + '/' + video.uuid |
10 | } | 10 | } |
11 | 11 | ||
12 | function generateWebTorrentRedundancyUrl (file: MVideoFile) { | 12 | function generateWebVideoRedundancyUrl (file: MVideoFile) { |
13 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename | 13 | return WEBSERVER.URL + STATIC_PATHS.REDUNDANCY + file.filename |
14 | } | 14 | } |
15 | 15 | ||
@@ -26,6 +26,6 @@ function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile) | |||
26 | export { | 26 | export { |
27 | getLocalVideoFileMetadataUrl, | 27 | getLocalVideoFileMetadataUrl, |
28 | 28 | ||
29 | generateWebTorrentRedundancyUrl, | 29 | generateWebVideoRedundancyUrl, |
30 | generateHLSRedundancyUrl | 30 | generateHLSRedundancyUrl |
31 | } | 31 | } |
diff --git a/server/lib/video.ts b/server/lib/video.ts index 588dc553f..362c861a5 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -10,7 +10,7 @@ import { FilteredModelAttributes } from '@server/types' | |||
10 | import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 10 | import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
11 | import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' | 11 | import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
12 | import { CreateJobArgument, JobQueue } from './job-queue/job-queue' | 12 | import { CreateJobArgument, JobQueue } from './job-queue/job-queue' |
13 | import { updateVideoMiniatureFromExisting } from './thumbnail' | 13 | import { updateLocalVideoMiniatureFromExisting } from './thumbnail' |
14 | import { moveFilesIfPrivacyChanged } from './video-privacy' | 14 | import { moveFilesIfPrivacyChanged } from './video-privacy' |
15 | 15 | ||
16 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 16 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
@@ -55,7 +55,7 @@ async function buildVideoThumbnailsFromReq (options: { | |||
55 | const fields = files?.[p.fieldName] | 55 | const fields = files?.[p.fieldName] |
56 | 56 | ||
57 | if (fields) { | 57 | if (fields) { |
58 | return updateVideoMiniatureFromExisting({ | 58 | return updateLocalVideoMiniatureFromExisting({ |
59 | inputPath: fields[0].path, | 59 | inputPath: fields[0].path, |
60 | video, | 60 | video, |
61 | type: p.type, | 61 | type: p.type, |
diff --git a/server/lib/worker/workers/image-downloader.ts b/server/lib/worker/workers/image-downloader.ts index 4b32f723e..209594589 100644 --- a/server/lib/worker/workers/image-downloader.ts +++ b/server/lib/worker/workers/image-downloader.ts | |||
@@ -24,6 +24,8 @@ async function downloadImage (options: { | |||
24 | 24 | ||
25 | throw err | 25 | throw err |
26 | } | 26 | } |
27 | |||
28 | return destPath | ||
27 | } | 29 | } |
28 | 30 | ||
29 | module.exports = downloadImage | 31 | module.exports = downloadImage |
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index 0eefa2a8e..39a7b2998 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -5,6 +5,7 @@ import { RunnerModel } from '@server/models/runner/runner' | |||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
6 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | 7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' |
8 | import { ServerErrorCode } from '@shared/models' | ||
8 | 9 | ||
9 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 10 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
10 | handleOAuthAuthenticate(req, res) | 11 | handleOAuthAuthenticate(req, res) |
@@ -48,15 +49,23 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
48 | .catch(err => logger.error('Cannot get access token.', { err })) | 49 | .catch(err => logger.error('Cannot get access token.', { err })) |
49 | } | 50 | } |
50 | 51 | ||
51 | function authenticatePromise (req: express.Request, res: express.Response) { | 52 | function authenticatePromise (options: { |
53 | req: express.Request | ||
54 | res: express.Response | ||
55 | errorMessage?: string | ||
56 | errorStatus?: HttpStatusCode | ||
57 | errorType?: ServerErrorCode | ||
58 | }) { | ||
59 | const { req, res, errorMessage = 'Not authenticated', errorStatus = HttpStatusCode.UNAUTHORIZED_401, errorType } = options | ||
52 | return new Promise<void>(resolve => { | 60 | return new Promise<void>(resolve => { |
53 | // Already authenticated? (or tried to) | 61 | // Already authenticated? (or tried to) |
54 | if (res.locals.oauth?.token.User) return resolve() | 62 | if (res.locals.oauth?.token.User) return resolve() |
55 | 63 | ||
56 | if (res.locals.authenticated === false) { | 64 | if (res.locals.authenticated === false) { |
57 | return res.fail({ | 65 | return res.fail({ |
58 | status: HttpStatusCode.UNAUTHORIZED_401, | 66 | status: errorStatus, |
59 | message: 'Not authenticated' | 67 | type: errorType, |
68 | message: errorMessage | ||
60 | }) | 69 | }) |
61 | } | 70 | } |
62 | 71 | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index a0074cb24..a6dbba524 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -25,6 +25,7 @@ const customConfigUpdateValidator = [ | |||
25 | body('cache.previews.size').isInt(), | 25 | body('cache.previews.size').isInt(), |
26 | body('cache.captions.size').isInt(), | 26 | body('cache.captions.size').isInt(), |
27 | body('cache.torrents.size').isInt(), | 27 | body('cache.torrents.size').isInt(), |
28 | body('cache.storyboards.size').isInt(), | ||
28 | 29 | ||
29 | body('signup.enabled').isBoolean(), | 30 | body('signup.enabled').isBoolean(), |
30 | body('signup.limit').isInt(), | 31 | body('signup.limit').isInt(), |
@@ -58,7 +59,7 @@ const customConfigUpdateValidator = [ | |||
58 | 59 | ||
59 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), | 60 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), |
60 | 61 | ||
61 | body('transcoding.webtorrent.enabled').isBoolean(), | 62 | body('transcoding.webVideos.enabled').isBoolean(), |
62 | body('transcoding.hls.enabled').isBoolean(), | 63 | body('transcoding.hls.enabled').isBoolean(), |
63 | 64 | ||
64 | body('videoStudio.enabled').isBoolean(), | 65 | body('videoStudio.enabled').isBoolean(), |
@@ -152,8 +153,8 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp | |||
152 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { | 153 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { |
153 | if (customConfig.transcoding.enabled === false) return true | 154 | if (customConfig.transcoding.enabled === false) return true |
154 | 155 | ||
155 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { | 156 | if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) { |
156 | res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' }) | 157 | res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' }) |
157 | return false | 158 | return false |
158 | } | 159 | } |
159 | 160 | ||
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts index de98cd442..e5cff2dda 100644 --- a/server/middlewares/validators/shared/index.ts +++ b/server/middlewares/validators/shared/index.ts | |||
@@ -10,4 +10,5 @@ export * from './video-comments' | |||
10 | export * from './video-imports' | 10 | export * from './video-imports' |
11 | export * from './video-ownerships' | 11 | export * from './video-ownerships' |
12 | export * from './video-playlists' | 12 | export * from './video-playlists' |
13 | export * from './video-passwords' | ||
13 | export * from './videos' | 14 | export * from './videos' |
diff --git a/server/middlewares/validators/shared/video-passwords.ts b/server/middlewares/validators/shared/video-passwords.ts new file mode 100644 index 000000000..efcc95dc4 --- /dev/null +++ b/server/middlewares/validators/shared/video-passwords.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
5 | import { header } from 'express-validator' | ||
6 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
7 | |||
8 | function isValidVideoPasswordHeader () { | ||
9 | return header('x-peertube-video-password') | ||
10 | .optional() | ||
11 | .isString() | ||
12 | } | ||
13 | |||
14 | function checkVideoIsPasswordProtected (res: express.Response) { | ||
15 | const video = getVideoWithAttributes(res) | ||
16 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
17 | res.fail({ | ||
18 | status: HttpStatusCode.BAD_REQUEST_400, | ||
19 | message: 'Video is not password protected' | ||
20 | }) | ||
21 | return false | ||
22 | } | ||
23 | |||
24 | return true | ||
25 | } | ||
26 | |||
27 | async function doesVideoPasswordExist (idArg: number | string, res: express.Response) { | ||
28 | const video = getVideoWithAttributes(res) | ||
29 | const id = forceNumber(idArg) | ||
30 | const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id }) | ||
31 | |||
32 | if (!videoPassword) { | ||
33 | res.fail({ | ||
34 | status: HttpStatusCode.NOT_FOUND_404, | ||
35 | message: 'Video password not found' | ||
36 | }) | ||
37 | return false | ||
38 | } | ||
39 | |||
40 | res.locals.videoPassword = videoPassword | ||
41 | |||
42 | return true | ||
43 | } | ||
44 | |||
45 | async function isVideoPasswordDeletable (res: express.Response) { | ||
46 | const user = res.locals.oauth.token.User | ||
47 | const userAccount = user.Account | ||
48 | const video = res.locals.videoAll | ||
49 | |||
50 | // Check if the user who did the request is able to delete the video passwords | ||
51 | if ( | ||
52 | user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator | ||
53 | video.VideoChannel.accountId !== userAccount.id // Not the video owner | ||
54 | ) { | ||
55 | res.fail({ | ||
56 | status: HttpStatusCode.FORBIDDEN_403, | ||
57 | message: 'Cannot remove passwords of another user\'s video' | ||
58 | }) | ||
59 | return false | ||
60 | } | ||
61 | |||
62 | const passwordCount = await VideoPasswordModel.countByVideoId(video.id) | ||
63 | |||
64 | if (passwordCount <= 1) { | ||
65 | res.fail({ | ||
66 | status: HttpStatusCode.BAD_REQUEST_400, | ||
67 | message: 'Cannot delete the last password of the protected video' | ||
68 | }) | ||
69 | return false | ||
70 | } | ||
71 | |||
72 | return true | ||
73 | } | ||
74 | |||
75 | export { | ||
76 | isValidVideoPasswordHeader, | ||
77 | checkVideoIsPasswordProtected as isVideoPasswordProtected, | ||
78 | doesVideoPasswordExist, | ||
79 | isVideoPasswordDeletable | ||
80 | } | ||
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index 0033a32ff..9a7497007 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -20,6 +20,8 @@ import { | |||
20 | MVideoWithRights | 20 | MVideoWithRights |
21 | } from '@server/types/models' | 21 | } from '@server/types/models' |
22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' | 22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' |
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
23 | 25 | ||
24 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { | 26 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { |
25 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 27 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
@@ -111,8 +113,12 @@ async function checkCanSeeVideo (options: { | |||
111 | }) { | 113 | }) { |
112 | const { req, res, video, paramId } = options | 114 | const { req, res, video, paramId } = options |
113 | 115 | ||
114 | if (video.requiresAuth({ urlParamId: paramId, checkBlacklist: true })) { | 116 | if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) { |
115 | return checkCanSeeAuthVideo(req, res, video) | 117 | return checkCanSeeUserAuthVideo({ req, res, video }) |
118 | } | ||
119 | |||
120 | if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
121 | return checkCanSeePasswordProtectedVideo({ req, res, video }) | ||
116 | } | 122 | } |
117 | 123 | ||
118 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { | 124 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { |
@@ -122,7 +128,13 @@ async function checkCanSeeVideo (options: { | |||
122 | throw new Error('Unknown video privacy when checking video right ' + video.url) | 128 | throw new Error('Unknown video privacy when checking video right ' + video.url) |
123 | } | 129 | } |
124 | 130 | ||
125 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { | 131 | async function checkCanSeeUserAuthVideo (options: { |
132 | req: Request | ||
133 | res: Response | ||
134 | video: MVideoId | MVideoWithRights | ||
135 | }) { | ||
136 | const { req, res, video } = options | ||
137 | |||
126 | const fail = () => { | 138 | const fail = () => { |
127 | res.fail({ | 139 | res.fail({ |
128 | status: HttpStatusCode.FORBIDDEN_403, | 140 | status: HttpStatusCode.FORBIDDEN_403, |
@@ -132,14 +144,12 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
132 | return false | 144 | return false |
133 | } | 145 | } |
134 | 146 | ||
135 | await authenticatePromise(req, res) | 147 | await authenticatePromise({ req, res }) |
136 | 148 | ||
137 | const user = res.locals.oauth?.token.User | 149 | const user = res.locals.oauth?.token.User |
138 | if (!user) return fail() | 150 | if (!user) return fail() |
139 | 151 | ||
140 | const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId | 152 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) |
141 | ? video as MVideoWithRights | ||
142 | : await VideoModel.loadFull(video.id) | ||
143 | 153 | ||
144 | const privacy = videoWithRights.privacy | 154 | const privacy = videoWithRights.privacy |
145 | 155 | ||
@@ -148,16 +158,14 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
148 | return true | 158 | return true |
149 | } | 159 | } |
150 | 160 | ||
151 | const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id | ||
152 | |||
153 | if (videoWithRights.isBlacklisted()) { | 161 | if (videoWithRights.isBlacklisted()) { |
154 | if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true | 162 | if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true |
155 | 163 | ||
156 | return fail() | 164 | return fail() |
157 | } | 165 | } |
158 | 166 | ||
159 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { | 167 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { |
160 | if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true | 168 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true |
161 | 169 | ||
162 | return fail() | 170 | return fail() |
163 | } | 171 | } |
@@ -166,6 +174,59 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
166 | return fail() | 174 | return fail() |
167 | } | 175 | } |
168 | 176 | ||
177 | async function checkCanSeePasswordProtectedVideo (options: { | ||
178 | req: Request | ||
179 | res: Response | ||
180 | video: MVideo | ||
181 | }) { | ||
182 | const { req, res, video } = options | ||
183 | |||
184 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) | ||
185 | |||
186 | const videoPassword = req.header('x-peertube-video-password') | ||
187 | |||
188 | if (!exists(videoPassword)) { | ||
189 | const errorMessage = 'Please provide a password to access this password protected video' | ||
190 | const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
191 | |||
192 | if (req.header('authorization')) { | ||
193 | await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType }) | ||
194 | const user = res.locals.oauth?.token.User | ||
195 | |||
196 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true | ||
197 | } | ||
198 | |||
199 | res.fail({ | ||
200 | status: HttpStatusCode.FORBIDDEN_403, | ||
201 | type: errorType, | ||
202 | message: errorMessage | ||
203 | }) | ||
204 | return false | ||
205 | } | ||
206 | |||
207 | if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true | ||
208 | |||
209 | res.fail({ | ||
210 | status: HttpStatusCode.FORBIDDEN_403, | ||
211 | type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD, | ||
212 | message: 'Incorrect video password. Access to the video is denied.' | ||
213 | }) | ||
214 | |||
215 | return false | ||
216 | } | ||
217 | |||
218 | function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRight) { | ||
219 | const isOwnedByUser = video.VideoChannel.Account.userId === user.id | ||
220 | |||
221 | return isOwnedByUser || user.hasRight(right) | ||
222 | } | ||
223 | |||
224 | async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> { | ||
225 | return video.VideoChannel?.Account?.userId | ||
226 | ? video | ||
227 | : VideoModel.loadFull(video.id) | ||
228 | } | ||
229 | |||
169 | // --------------------------------------------------------------------------- | 230 | // --------------------------------------------------------------------------- |
170 | 231 | ||
171 | async function checkCanAccessVideoStaticFiles (options: { | 232 | async function checkCanAccessVideoStaticFiles (options: { |
@@ -176,7 +237,7 @@ async function checkCanAccessVideoStaticFiles (options: { | |||
176 | }) { | 237 | }) { |
177 | const { video, req, res } = options | 238 | const { video, req, res } = options |
178 | 239 | ||
179 | if (res.locals.oauth?.token.User) { | 240 | if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) { |
180 | return checkCanSeeVideo(options) | 241 | return checkCanSeeVideo(options) |
181 | } | 242 | } |
182 | 243 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 959f663ac..07d6cba82 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -28,6 +28,7 @@ export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | |||
28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | 28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) |
29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | 29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) |
30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | 30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) |
31 | export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS) | ||
31 | 32 | ||
32 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | 33 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) |
33 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | 34 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts index 9c2d890ba..86cc0a8d7 100644 --- a/server/middlewares/validators/static.ts +++ b/server/middlewares/validators/static.ts | |||
@@ -9,7 +9,7 @@ import { VideoModel } from '@server/models/video/video' | |||
9 | import { VideoFileModel } from '@server/models/video/video-file' | 9 | import { VideoFileModel } from '@server/models/video/video-file' |
10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' | 10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' |
11 | import { HttpStatusCode } from '@shared/models' | 11 | import { HttpStatusCode } from '@shared/models' |
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | 12 | import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared' |
13 | 13 | ||
14 | type LRUValue = { | 14 | type LRUValue = { |
15 | allowed: boolean | 15 | allowed: boolean |
@@ -22,9 +22,11 @@ const staticFileTokenBypass = new LRUCache<string, LRUValue>({ | |||
22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | 22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL |
23 | }) | 23 | }) |
24 | 24 | ||
25 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | 25 | const ensureCanAccessVideoPrivateWebVideoFiles = [ |
26 | query('videoFileToken').optional().custom(exists), | 26 | query('videoFileToken').optional().custom(exists), |
27 | 27 | ||
28 | isValidVideoPasswordHeader(), | ||
29 | |||
28 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 30 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
29 | if (areValidationErrors(req, res)) return | 31 | if (areValidationErrors(req, res)) return |
30 | 32 | ||
@@ -46,7 +48,7 @@ const ensureCanAccessVideoPrivateWebTorrentFiles = [ | |||
46 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 48 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) |
47 | } | 49 | } |
48 | 50 | ||
49 | const result = await isWebTorrentAllowed(req, res) | 51 | const result = await isWebVideoAllowed(req, res) |
50 | 52 | ||
51 | staticFileTokenBypass.set(cacheKey, result) | 53 | staticFileTokenBypass.set(cacheKey, result) |
52 | 54 | ||
@@ -73,6 +75,8 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
73 | .optional() | 75 | .optional() |
74 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), | 76 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), |
75 | 77 | ||
78 | isValidVideoPasswordHeader(), | ||
79 | |||
76 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
77 | if (areValidationErrors(req, res)) return | 81 | if (areValidationErrors(req, res)) return |
78 | 82 | ||
@@ -118,13 +122,13 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
118 | ] | 122 | ] |
119 | 123 | ||
120 | export { | 124 | export { |
121 | ensureCanAccessVideoPrivateWebTorrentFiles, | 125 | ensureCanAccessVideoPrivateWebVideoFiles, |
122 | ensureCanAccessPrivateVideoHLSFiles | 126 | ensureCanAccessPrivateVideoHLSFiles |
123 | } | 127 | } |
124 | 128 | ||
125 | // --------------------------------------------------------------------------- | 129 | // --------------------------------------------------------------------------- |
126 | 130 | ||
127 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | 131 | async function isWebVideoAllowed (req: express.Request, res: express.Response) { |
128 | const filename = basename(req.path) | 132 | const filename = basename(req.path) |
129 | 133 | ||
130 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | 134 | const file = await VideoFileModel.loadWithVideoByFilename(filename) |
@@ -167,11 +171,11 @@ async function isHLSAllowed (req: express.Request, res: express.Response, videoU | |||
167 | } | 171 | } |
168 | 172 | ||
169 | function extractTokenOrDie (req: express.Request, res: express.Response) { | 173 | function extractTokenOrDie (req: express.Request, res: express.Response) { |
170 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | 174 | const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken |
171 | 175 | ||
172 | if (!token) { | 176 | if (!token) { |
173 | return res.fail({ | 177 | return res.fail({ |
174 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | 178 | message: 'Video password header, video file token query parameter and bearer token are all missing', // |
175 | status: HttpStatusCode.FORBIDDEN_403 | 179 | status: HttpStatusCode.FORBIDDEN_403 |
176 | }) | 180 | }) |
177 | } | 181 | } |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index d225dfe45..0c824c314 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -12,6 +12,8 @@ export * from './video-shares' | |||
12 | export * from './video-source' | 12 | export * from './video-source' |
13 | export * from './video-stats' | 13 | export * from './video-stats' |
14 | export * from './video-studio' | 14 | export * from './video-studio' |
15 | export * from './video-token' | ||
15 | export * from './video-transcoding' | 16 | export * from './video-transcoding' |
16 | export * from './videos' | 17 | export * from './videos' |
17 | export * from './video-channel-sync' | 18 | export * from './video-channel-sync' |
19 | export * from './video-passwords' | ||
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 72b2febc3..077a58d2e 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -10,7 +10,8 @@ import { | |||
10 | checkUserCanManageVideo, | 10 | checkUserCanManageVideo, |
11 | doesVideoCaptionExist, | 11 | doesVideoCaptionExist, |
12 | doesVideoExist, | 12 | doesVideoExist, |
13 | isValidVideoIdParam | 13 | isValidVideoIdParam, |
14 | isValidVideoPasswordHeader | ||
14 | } from '../shared' | 15 | } from '../shared' |
15 | 16 | ||
16 | const addVideoCaptionValidator = [ | 17 | const addVideoCaptionValidator = [ |
@@ -62,6 +63,8 @@ const deleteVideoCaptionValidator = [ | |||
62 | const listVideoCaptionsValidator = [ | 63 | const listVideoCaptionsValidator = [ |
63 | isValidVideoIdParam('videoId'), | 64 | isValidVideoIdParam('videoId'), |
64 | 65 | ||
66 | isValidVideoPasswordHeader(), | ||
67 | |||
65 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 68 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
66 | if (areValidationErrors(req, res)) return | 69 | if (areValidationErrors(req, res)) return |
67 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 70 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 133feb7bd..70689b02e 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -14,7 +14,8 @@ import { | |||
14 | doesVideoCommentExist, | 14 | doesVideoCommentExist, |
15 | doesVideoCommentThreadExist, | 15 | doesVideoCommentThreadExist, |
16 | doesVideoExist, | 16 | doesVideoExist, |
17 | isValidVideoIdParam | 17 | isValidVideoIdParam, |
18 | isValidVideoPasswordHeader | ||
18 | } from '../shared' | 19 | } from '../shared' |
19 | 20 | ||
20 | const listVideoCommentsValidator = [ | 21 | const listVideoCommentsValidator = [ |
@@ -51,6 +52,7 @@ const listVideoCommentsValidator = [ | |||
51 | 52 | ||
52 | const listVideoCommentThreadsValidator = [ | 53 | const listVideoCommentThreadsValidator = [ |
53 | isValidVideoIdParam('videoId'), | 54 | isValidVideoIdParam('videoId'), |
55 | isValidVideoPasswordHeader(), | ||
54 | 56 | ||
55 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 57 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
56 | if (areValidationErrors(req, res)) return | 58 | if (areValidationErrors(req, res)) return |
@@ -67,6 +69,7 @@ const listVideoThreadCommentsValidator = [ | |||
67 | 69 | ||
68 | param('threadId') | 70 | param('threadId') |
69 | .custom(isIdValid), | 71 | .custom(isIdValid), |
72 | isValidVideoPasswordHeader(), | ||
70 | 73 | ||
71 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
72 | if (areValidationErrors(req, res)) return | 75 | if (areValidationErrors(req, res)) return |
@@ -84,6 +87,7 @@ const addVideoCommentThreadValidator = [ | |||
84 | 87 | ||
85 | body('text') | 88 | body('text') |
86 | .custom(isValidVideoCommentText), | 89 | .custom(isValidVideoCommentText), |
90 | isValidVideoPasswordHeader(), | ||
87 | 91 | ||
88 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 92 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
89 | if (areValidationErrors(req, res)) return | 93 | if (areValidationErrors(req, res)) return |
@@ -102,6 +106,7 @@ const addVideoCommentReplyValidator = [ | |||
102 | isValidVideoIdParam('videoId'), | 106 | isValidVideoIdParam('videoId'), |
103 | 107 | ||
104 | param('commentId').custom(isIdValid), | 108 | param('commentId').custom(isIdValid), |
109 | isValidVideoPasswordHeader(), | ||
105 | 110 | ||
106 | body('text').custom(isValidVideoCommentText), | 111 | body('text').custom(isValidVideoCommentText), |
107 | 112 | ||
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts index 92c5b9483..6c0ecda42 100644 --- a/server/middlewares/validators/videos/video-files.ts +++ b/server/middlewares/validators/videos/video-files.ts | |||
@@ -5,7 +5,7 @@ import { MVideo } from '@server/types/models' | |||
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
7 | 7 | ||
8 | const videoFilesDeleteWebTorrentValidator = [ | 8 | const videoFilesDeleteWebVideoValidator = [ |
9 | isValidVideoIdParam('id'), | 9 | isValidVideoIdParam('id'), |
10 | 10 | ||
11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -16,17 +16,17 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
16 | 16 | ||
17 | if (!checkLocalVideo(video, res)) return | 17 | if (!checkLocalVideo(video, res)) return |
18 | 18 | ||
19 | if (!video.hasWebTorrentFiles()) { | 19 | if (!video.hasWebVideoFiles()) { |
20 | return res.fail({ | 20 | return res.fail({ |
21 | status: HttpStatusCode.BAD_REQUEST_400, | 21 | status: HttpStatusCode.BAD_REQUEST_400, |
22 | message: 'This video does not have WebTorrent files' | 22 | message: 'This video does not have Web Video files' |
23 | }) | 23 | }) |
24 | } | 24 | } |
25 | 25 | ||
26 | if (!video.getHLSPlaylist()) { | 26 | if (!video.getHLSPlaylist()) { |
27 | return res.fail({ | 27 | return res.fail({ |
28 | status: HttpStatusCode.BAD_REQUEST_400, | 28 | status: HttpStatusCode.BAD_REQUEST_400, |
29 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 29 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
30 | }) | 30 | }) |
31 | } | 31 | } |
32 | 32 | ||
@@ -34,7 +34,7 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
34 | } | 34 | } |
35 | ] | 35 | ] |
36 | 36 | ||
37 | const videoFilesDeleteWebTorrentFileValidator = [ | 37 | const videoFilesDeleteWebVideoFileValidator = [ |
38 | isValidVideoIdParam('id'), | 38 | isValidVideoIdParam('id'), |
39 | 39 | ||
40 | param('videoFileId') | 40 | param('videoFileId') |
@@ -52,14 +52,14 @@ const videoFilesDeleteWebTorrentFileValidator = [ | |||
52 | if (!files.find(f => f.id === +req.params.videoFileId)) { | 52 | if (!files.find(f => f.id === +req.params.videoFileId)) { |
53 | return res.fail({ | 53 | return res.fail({ |
54 | status: HttpStatusCode.NOT_FOUND_404, | 54 | status: HttpStatusCode.NOT_FOUND_404, |
55 | message: 'This video does not have this WebTorrent file id' | 55 | message: 'This video does not have this Web Video file id' |
56 | }) | 56 | }) |
57 | } | 57 | } |
58 | 58 | ||
59 | if (files.length === 1 && !video.getHLSPlaylist()) { | 59 | if (files.length === 1 && !video.getHLSPlaylist()) { |
60 | return res.fail({ | 60 | return res.fail({ |
61 | status: HttpStatusCode.BAD_REQUEST_400, | 61 | status: HttpStatusCode.BAD_REQUEST_400, |
62 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 62 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
@@ -87,10 +87,10 @@ const videoFilesDeleteHLSValidator = [ | |||
87 | }) | 87 | }) |
88 | } | 88 | } |
89 | 89 | ||
90 | if (!video.hasWebTorrentFiles()) { | 90 | if (!video.hasWebVideoFiles()) { |
91 | return res.fail({ | 91 | return res.fail({ |
92 | status: HttpStatusCode.BAD_REQUEST_400, | 92 | status: HttpStatusCode.BAD_REQUEST_400, |
93 | message: 'Cannot delete HLS playlist since this video does not have WebTorrent files' | 93 | message: 'Cannot delete HLS playlist since this video does not have Web Video files' |
94 | }) | 94 | }) |
95 | } | 95 | } |
96 | 96 | ||
@@ -128,10 +128,10 @@ const videoFilesDeleteHLSFileValidator = [ | |||
128 | } | 128 | } |
129 | 129 | ||
130 | // Last file to delete | 130 | // Last file to delete |
131 | if (hlsFiles.length === 1 && !video.hasWebTorrentFiles()) { | 131 | if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) { |
132 | return res.fail({ | 132 | return res.fail({ |
133 | status: HttpStatusCode.BAD_REQUEST_400, | 133 | status: HttpStatusCode.BAD_REQUEST_400, |
134 | message: 'Cannot delete last HLS playlist file since this video does not have WebTorrent files' | 134 | message: 'Cannot delete last HLS playlist file since this video does not have Web Video files' |
135 | }) | 135 | }) |
136 | } | 136 | } |
137 | 137 | ||
@@ -140,8 +140,8 @@ const videoFilesDeleteHLSFileValidator = [ | |||
140 | ] | 140 | ] |
141 | 141 | ||
142 | export { | 142 | export { |
143 | videoFilesDeleteWebTorrentValidator, | 143 | videoFilesDeleteWebVideoValidator, |
144 | videoFilesDeleteWebTorrentFileValidator, | 144 | videoFilesDeleteWebVideoFileValidator, |
145 | 145 | ||
146 | videoFilesDeleteHLSValidator, | 146 | videoFilesDeleteHLSValidator, |
147 | videoFilesDeleteHLSFileValidator | 147 | videoFilesDeleteHLSFileValidator |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index 72442aeb6..a1cb65b70 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -9,7 +9,11 @@ import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | |||
9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | 11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
12 | import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' | 12 | import { |
13 | isValidPasswordProtectedPrivacy, | ||
14 | isVideoMagnetUriValid, | ||
15 | isVideoNameValid | ||
16 | } from '../../../helpers/custom-validators/videos' | ||
13 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 17 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
14 | import { logger } from '../../../helpers/logger' | 18 | import { logger } from '../../../helpers/logger' |
15 | import { CONFIG } from '../../../initializers/config' | 19 | import { CONFIG } from '../../../initializers/config' |
@@ -38,6 +42,10 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
38 | .custom(isVideoNameValid).withMessage( | 42 | .custom(isVideoNameValid).withMessage( |
39 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | 43 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` |
40 | ), | 44 | ), |
45 | body('videoPasswords') | ||
46 | .optional() | ||
47 | .isArray() | ||
48 | .withMessage('Video passwords should be an array.'), | ||
41 | 49 | ||
42 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 50 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
43 | const user = res.locals.oauth.token.User | 51 | const user = res.locals.oauth.token.User |
@@ -45,6 +53,8 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
45 | 53 | ||
46 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 54 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
47 | 55 | ||
56 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
57 | |||
48 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { | 58 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { |
49 | cleanUpReqFiles(req) | 59 | cleanUpReqFiles(req) |
50 | 60 | ||
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 2aff831a8..ec69a3011 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | VideoState | 17 | VideoState |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
20 | import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos' | 20 | import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos' |
21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { CONFIG } from '../../../initializers/config' | 23 | import { CONFIG } from '../../../initializers/config' |
@@ -69,7 +69,7 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
69 | body('replaySettings.privacy') | 69 | body('replaySettings.privacy') |
70 | .optional() | 70 | .optional() |
71 | .customSanitizer(toIntOrNull) | 71 | .customSanitizer(toIntOrNull) |
72 | .custom(isVideoPrivacyValid), | 72 | .custom(isVideoReplayPrivacyValid), |
73 | 73 | ||
74 | body('permanentLive') | 74 | body('permanentLive') |
75 | .optional() | 75 | .optional() |
@@ -81,9 +81,16 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
81 | .customSanitizer(toIntOrNull) | 81 | .customSanitizer(toIntOrNull) |
82 | .custom(isLiveLatencyModeValid), | 82 | .custom(isLiveLatencyModeValid), |
83 | 83 | ||
84 | body('videoPasswords') | ||
85 | .optional() | ||
86 | .isArray() | ||
87 | .withMessage('Video passwords should be an array.'), | ||
88 | |||
84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 89 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
85 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 90 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
86 | 91 | ||
92 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
93 | |||
87 | if (CONFIG.LIVE.ENABLED !== true) { | 94 | if (CONFIG.LIVE.ENABLED !== true) { |
88 | cleanUpReqFiles(req) | 95 | cleanUpReqFiles(req) |
89 | 96 | ||
@@ -170,7 +177,7 @@ const videoLiveUpdateValidator = [ | |||
170 | body('replaySettings.privacy') | 177 | body('replaySettings.privacy') |
171 | .optional() | 178 | .optional() |
172 | .customSanitizer(toIntOrNull) | 179 | .customSanitizer(toIntOrNull) |
173 | .custom(isVideoPrivacyValid), | 180 | .custom(isVideoReplayPrivacyValid), |
174 | 181 | ||
175 | body('latencyMode') | 182 | body('latencyMode') |
176 | .optional() | 183 | .optional() |
diff --git a/server/middlewares/validators/videos/video-passwords.ts b/server/middlewares/validators/videos/video-passwords.ts new file mode 100644 index 000000000..200e496f6 --- /dev/null +++ b/server/middlewares/validators/videos/video-passwords.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import express from 'express' | ||
2 | import { | ||
3 | areValidationErrors, | ||
4 | doesVideoExist, | ||
5 | isVideoPasswordProtected, | ||
6 | isValidVideoIdParam, | ||
7 | doesVideoPasswordExist, | ||
8 | isVideoPasswordDeletable, | ||
9 | checkUserCanManageVideo | ||
10 | } from '../shared' | ||
11 | import { body, param } from 'express-validator' | ||
12 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
13 | import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos' | ||
14 | import { UserRight } from '@shared/models' | ||
15 | |||
16 | const listVideoPasswordValidator = [ | ||
17 | isValidVideoIdParam('videoId'), | ||
18 | |||
19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
20 | if (areValidationErrors(req, res)) return | ||
21 | |||
22 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
23 | if (!isVideoPasswordProtected(res)) return | ||
24 | |||
25 | // Check if the user who did the request is able to access video password list | ||
26 | const user = res.locals.oauth.token.User | ||
27 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | ] | ||
32 | |||
33 | const updateVideoPasswordListValidator = [ | ||
34 | body('passwords') | ||
35 | .optional() | ||
36 | .isArray() | ||
37 | .withMessage('Video passwords should be an array.'), | ||
38 | |||
39 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
40 | if (areValidationErrors(req, res)) return | ||
41 | |||
42 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
43 | if (!isValidPasswordProtectedPrivacy(req, res)) return | ||
44 | |||
45 | // Check if the user who did the request is able to update video passwords | ||
46 | const user = res.locals.oauth.token.User | ||
47 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return | ||
48 | |||
49 | return next() | ||
50 | } | ||
51 | ] | ||
52 | |||
53 | const removeVideoPasswordValidator = [ | ||
54 | isValidVideoIdParam('videoId'), | ||
55 | |||
56 | param('passwordId') | ||
57 | .custom(isIdValid), | ||
58 | |||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
60 | if (areValidationErrors(req, res)) return | ||
61 | |||
62 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
63 | if (!isVideoPasswordProtected(res)) return | ||
64 | if (!await doesVideoPasswordExist(req.params.passwordId, res)) return | ||
65 | if (!await isVideoPasswordDeletable(res)) return | ||
66 | |||
67 | return next() | ||
68 | } | ||
69 | ] | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | listVideoPasswordValidator, | ||
75 | updateVideoPasswordListValidator, | ||
76 | removeVideoPasswordValidator | ||
77 | } | ||
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index c631a16f8..95a5ba63a 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -153,7 +153,7 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
153 | } | 153 | } |
154 | 154 | ||
155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
156 | await authenticatePromise(req, res) | 156 | await authenticatePromise({ req, res }) |
157 | 157 | ||
158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
159 | 159 | ||
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 275634d5b..c837b047b 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -7,13 +7,14 @@ import { isIdValid } from '../../../helpers/custom-validators/misc' | |||
7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | 7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' |
8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | 8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' |
9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | 10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared' |
11 | 11 | ||
12 | const videoUpdateRateValidator = [ | 12 | const videoUpdateRateValidator = [ |
13 | isValidVideoIdParam('id'), | 13 | isValidVideoIdParam('id'), |
14 | 14 | ||
15 | body('rating') | 15 | body('rating') |
16 | .custom(isVideoRatingTypeValid), | 16 | .custom(isVideoRatingTypeValid), |
17 | isValidVideoPasswordHeader(), | ||
17 | 18 | ||
18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
19 | if (areValidationErrors(req, res)) return | 20 | if (areValidationErrors(req, res)) return |
diff --git a/server/middlewares/validators/videos/video-token.ts b/server/middlewares/validators/videos/video-token.ts new file mode 100644 index 000000000..d4253e21d --- /dev/null +++ b/server/middlewares/validators/videos/video-token.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { exists } from '@server/helpers/custom-validators/misc' | ||
5 | |||
6 | const videoFileTokenValidator = [ | ||
7 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
8 | const video = res.locals.onlyVideo | ||
9 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) { | ||
10 | return res.fail({ | ||
11 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
12 | message: 'Not authenticated' | ||
13 | }) | ||
14 | } | ||
15 | |||
16 | return next() | ||
17 | } | ||
18 | ] | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | videoFileTokenValidator | ||
24 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 794e1d4f1..b39d13a23 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { isTestInstance } from '@server/helpers/core-utils' | 3 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | 4 | import { getResumableUploadPath } from '@server/helpers/upload' |
5 | import { uploadx } from '@server/lib/uploadx' | ||
5 | import { Redis } from '@server/lib/redis' | 6 | import { Redis } from '@server/lib/redis' |
6 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 8 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
@@ -23,6 +24,7 @@ import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../ | |||
23 | import { | 24 | import { |
24 | areVideoTagsValid, | 25 | areVideoTagsValid, |
25 | isScheduleVideoUpdatePrivacyValid, | 26 | isScheduleVideoUpdatePrivacyValid, |
27 | isValidPasswordProtectedPrivacy, | ||
26 | isVideoCategoryValid, | 28 | isVideoCategoryValid, |
27 | isVideoDescriptionValid, | 29 | isVideoDescriptionValid, |
28 | isVideoFileMimeTypeValid, | 30 | isVideoFileMimeTypeValid, |
@@ -39,7 +41,6 @@ import { | |||
39 | } from '../../../helpers/custom-validators/videos' | 41 | } from '../../../helpers/custom-validators/videos' |
40 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 42 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
41 | import { logger } from '../../../helpers/logger' | 43 | import { logger } from '../../../helpers/logger' |
42 | import { deleteFileAndCatch } from '../../../helpers/utils' | ||
43 | import { getVideoWithAttributes } from '../../../helpers/video' | 44 | import { getVideoWithAttributes } from '../../../helpers/video' |
44 | import { CONFIG } from '../../../initializers/config' | 45 | import { CONFIG } from '../../../initializers/config' |
45 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 46 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
@@ -55,7 +56,8 @@ import { | |||
55 | doesVideoChannelOfAccountExist, | 56 | doesVideoChannelOfAccountExist, |
56 | doesVideoExist, | 57 | doesVideoExist, |
57 | doesVideoFileOfVideoExist, | 58 | doesVideoFileOfVideoExist, |
58 | isValidVideoIdParam | 59 | isValidVideoIdParam, |
60 | isValidVideoPasswordHeader | ||
59 | } from '../shared' | 61 | } from '../shared' |
60 | 62 | ||
61 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | 63 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
@@ -70,6 +72,10 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
70 | body('channelId') | 72 | body('channelId') |
71 | .customSanitizer(toIntOrNull) | 73 | .customSanitizer(toIntOrNull) |
72 | .custom(isIdValid), | 74 | .custom(isIdValid), |
75 | body('videoPasswords') | ||
76 | .optional() | ||
77 | .isArray() | ||
78 | .withMessage('Video passwords should be an array.'), | ||
73 | 79 | ||
74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 81 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
@@ -81,6 +87,8 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
81 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
82 | } | 88 | } |
83 | 89 | ||
90 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
91 | |||
84 | try { | 92 | try { |
85 | if (!videoFile.duration) await addDurationToVideo(videoFile) | 93 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
86 | } catch (err) { | 94 | } catch (err) { |
@@ -107,7 +115,7 @@ const videosAddResumableValidator = [ | |||
107 | const user = res.locals.oauth.token.User | 115 | const user = res.locals.oauth.token.User |
108 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | 116 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body |
109 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | 117 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } |
110 | const cleanup = () => deleteFileAndCatch(file.path) | 118 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) |
111 | 119 | ||
112 | const uploadId = req.query.upload_id | 120 | const uploadId = req.query.upload_id |
113 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) | 121 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) |
@@ -124,11 +132,15 @@ const videosAddResumableValidator = [ | |||
124 | }) | 132 | }) |
125 | } | 133 | } |
126 | 134 | ||
127 | if (isTestInstance()) { | 135 | const videoStillExists = await VideoModel.load(sessionResponse.video.id) |
128 | res.setHeader('x-resumable-upload-cached', 'true') | 136 | |
129 | } | 137 | if (videoStillExists) { |
138 | if (isTestInstance()) { | ||
139 | res.setHeader('x-resumable-upload-cached', 'true') | ||
140 | } | ||
130 | 141 | ||
131 | return res.json(sessionResponse) | 142 | return res.json(sessionResponse) |
143 | } | ||
132 | } | 144 | } |
133 | 145 | ||
134 | await Redis.Instance.setUploadSession(uploadId) | 146 | await Redis.Instance.setUploadSession(uploadId) |
@@ -174,6 +186,10 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
174 | body('channelId') | 186 | body('channelId') |
175 | .customSanitizer(toIntOrNull) | 187 | .customSanitizer(toIntOrNull) |
176 | .custom(isIdValid), | 188 | .custom(isIdValid), |
189 | body('videoPasswords') | ||
190 | .optional() | ||
191 | .isArray() | ||
192 | .withMessage('Video passwords should be an array.'), | ||
177 | 193 | ||
178 | header('x-upload-content-length') | 194 | header('x-upload-content-length') |
179 | .isNumeric() | 195 | .isNumeric() |
@@ -205,10 +221,14 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
205 | const files = { videofile: [ videoFileMetadata ] } | 221 | const files = { videofile: [ videoFileMetadata ] } |
206 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | 222 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() |
207 | 223 | ||
208 | // multer required unsetting the Content-Type, now we can set it for node-uploadx | 224 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup() |
225 | |||
226 | // Multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
209 | req.headers['content-type'] = 'application/json; charset=utf-8' | 227 | req.headers['content-type'] = 'application/json; charset=utf-8' |
210 | // place previewfile in metadata so that uploadx saves it in .META | 228 | |
229 | // Place thumbnail/previewfile in metadata so that uploadx saves it in .META | ||
211 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] | 230 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] |
231 | if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile'] | ||
212 | 232 | ||
213 | return next() | 233 | return next() |
214 | } | 234 | } |
@@ -227,12 +247,18 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | |||
227 | .optional() | 247 | .optional() |
228 | .customSanitizer(toIntOrNull) | 248 | .customSanitizer(toIntOrNull) |
229 | .custom(isIdValid), | 249 | .custom(isIdValid), |
250 | body('videoPasswords') | ||
251 | .optional() | ||
252 | .isArray() | ||
253 | .withMessage('Video passwords should be an array.'), | ||
230 | 254 | ||
231 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 255 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
232 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 256 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
233 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | 257 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) |
234 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | 258 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) |
235 | 259 | ||
260 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
261 | |||
236 | const video = getVideoWithAttributes(res) | 262 | const video = getVideoWithAttributes(res) |
237 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { | 263 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { |
238 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | 264 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) |
@@ -281,6 +307,8 @@ const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | | |||
281 | return [ | 307 | return [ |
282 | isValidVideoIdParam('id'), | 308 | isValidVideoIdParam('id'), |
283 | 309 | ||
310 | isValidVideoPasswordHeader(), | ||
311 | |||
284 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 312 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
285 | if (areValidationErrors(req, res)) return | 313 | if (areValidationErrors(req, res)) return |
286 | if (!await doesVideoExist(req.params.id, res, fetchType)) return | 314 | if (!await doesVideoExist(req.params.id, res, fetchType)) return |
@@ -478,10 +506,14 @@ const commonVideosFiltersValidator = [ | |||
478 | .optional() | 506 | .optional() |
479 | .customSanitizer(toBooleanOrNull) | 507 | .customSanitizer(toBooleanOrNull) |
480 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), | 508 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), |
481 | query('hasWebtorrentFiles') | 509 | query('hasWebtorrentFiles') // TODO: remove in v7 |
482 | .optional() | 510 | .optional() |
483 | .customSanitizer(toBooleanOrNull) | 511 | .customSanitizer(toBooleanOrNull) |
484 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), | 512 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), |
513 | query('hasWebVideoFiles') | ||
514 | .optional() | ||
515 | .customSanitizer(toBooleanOrNull) | ||
516 | .custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'), | ||
485 | query('skipCount') | 517 | query('skipCount') |
486 | .optional() | 518 | .optional() |
487 | .customSanitizer(toBooleanOrNull) | 519 | .customSanitizer(toBooleanOrNull) |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 9c34a0101..51085a16d 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -157,11 +157,11 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
157 | } | 157 | } |
158 | 158 | ||
159 | getPath () { | 159 | getPath () { |
160 | return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 160 | return join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, this.filename) |
161 | } | 161 | } |
162 | 162 | ||
163 | removeImage () { | 163 | removeImage () { |
164 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 164 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES_DIR, this.filename) |
165 | return remove(imagePath) | 165 | return remove(imagePath) |
166 | } | 166 | } |
167 | 167 | ||
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index c2a72b71f..cebf47dfd 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -162,7 +162,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 162 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
163 | logger.info('Removing duplicated video file %s.', logIdentifier) | 163 | logger.info('Removing duplicated video file %s.', logIdentifier) |
164 | 164 | ||
165 | videoFile.Video.removeWebTorrentFile(videoFile, true) | 165 | videoFile.Video.removeWebVideoFile(videoFile, true) |
166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 166 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
167 | } | 167 | } |
168 | 168 | ||
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 4f6a8fce4..ff6328d48 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -786,7 +786,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
786 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | 786 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + |
787 | `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` | 787 | `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` |
788 | 788 | ||
789 | const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + | 789 | const webVideoFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + |
790 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' + | 790 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' + |
791 | videoChannelJoin | 791 | videoChannelJoin |
792 | 792 | ||
@@ -797,7 +797,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
797 | 797 | ||
798 | return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + | 798 | return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + |
799 | 'FROM (' + | 799 | 'FROM (' + |
800 | `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` + | 800 | `SELECT MAX("t1"."size") AS "size" FROM (${webVideoFiles} UNION ${hlsFiles}) t1 ` + |
801 | 'GROUP BY "t1"."videoId"' + | 801 | 'GROUP BY "t1"."videoId"' + |
802 | ') t2' | 802 | ') t2' |
803 | } | 803 | } |
@@ -890,8 +890,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
890 | 890 | ||
891 | nsfwPolicy: this.nsfwPolicy, | 891 | nsfwPolicy: this.nsfwPolicy, |
892 | 892 | ||
893 | // FIXME: deprecated in 4.1 | ||
894 | webTorrentEnabled: this.p2pEnabled, | ||
895 | p2pEnabled: this.p2pEnabled, | 893 | p2pEnabled: this.p2pEnabled, |
896 | 894 | ||
897 | videosHistoryEnabled: this.videosHistoryEnabled, | 895 | videosHistoryEnabled: this.videosHistoryEnabled, |
diff --git a/server/models/video/formatter/index.ts b/server/models/video/formatter/index.ts new file mode 100644 index 000000000..77b406559 --- /dev/null +++ b/server/models/video/formatter/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-activity-pub-format' | ||
2 | export * from './video-api-format' | ||
diff --git a/server/models/video/formatter/shared/index.ts b/server/models/video/formatter/shared/index.ts new file mode 100644 index 000000000..d558fa7d6 --- /dev/null +++ b/server/models/video/formatter/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-format-utils' | |||
diff --git a/server/models/video/formatter/shared/video-format-utils.ts b/server/models/video/formatter/shared/video-format-utils.ts new file mode 100644 index 000000000..df3bbdf1c --- /dev/null +++ b/server/models/video/formatter/shared/video-format-utils.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { MVideoFile } from '@server/types/models' | ||
2 | |||
3 | export function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
4 | if (fileA.resolution < fileB.resolution) return 1 | ||
5 | if (fileA.resolution === fileB.resolution) return 0 | ||
6 | return -1 | ||
7 | } | ||
diff --git a/server/models/video/formatter/video-activity-pub-format.ts b/server/models/video/formatter/video-activity-pub-format.ts new file mode 100644 index 000000000..c0d3d5f3e --- /dev/null +++ b/server/models/video/formatter/video-activity-pub-format.ts | |||
@@ -0,0 +1,295 @@ | |||
1 | |||
2 | import { isArray } from 'lodash' | ||
3 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
4 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
5 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
6 | import { | ||
7 | ActivityIconObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityPubStoryboard, | ||
10 | ActivityTagObject, | ||
11 | ActivityTrackerUrlObject, | ||
12 | ActivityUrlObject, | ||
13 | VideoObject | ||
14 | } from '@shared/models' | ||
15 | import { MIMETYPES, WEBSERVER } from '../../../initializers/constants' | ||
16 | import { | ||
17 | getLocalVideoCommentsActivityPubUrl, | ||
18 | getLocalVideoDislikesActivityPubUrl, | ||
19 | getLocalVideoLikesActivityPubUrl, | ||
20 | getLocalVideoSharesActivityPubUrl | ||
21 | } from '../../../lib/activitypub/url' | ||
22 | import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '../../../types/models' | ||
23 | import { VideoCaptionModel } from '../video-caption' | ||
24 | import { sortByResolutionDesc } from './shared' | ||
25 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format' | ||
26 | |||
27 | export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
28 | const language = video.language | ||
29 | ? { identifier: video.language, name: getLanguageLabel(video.language) } | ||
30 | : undefined | ||
31 | |||
32 | const category = video.category | ||
33 | ? { identifier: video.category + '', name: getCategoryLabel(video.category) } | ||
34 | : undefined | ||
35 | |||
36 | const licence = video.licence | ||
37 | ? { identifier: video.licence + '', name: getLicenceLabel(video.licence) } | ||
38 | : undefined | ||
39 | |||
40 | const url: ActivityUrlObject[] = [ | ||
41 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
42 | { | ||
43 | type: 'Link', | ||
44 | mediaType: 'text/html', | ||
45 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
46 | } as ActivityUrlObject, | ||
47 | |||
48 | ...buildVideoFileUrls({ video, files: video.VideoFiles }), | ||
49 | |||
50 | ...buildStreamingPlaylistUrls(video), | ||
51 | |||
52 | ...buildTrackerUrls(video) | ||
53 | ] | ||
54 | |||
55 | return { | ||
56 | type: 'Video' as 'Video', | ||
57 | id: video.url, | ||
58 | name: video.name, | ||
59 | duration: getActivityStreamDuration(video.duration), | ||
60 | uuid: video.uuid, | ||
61 | category, | ||
62 | licence, | ||
63 | language, | ||
64 | views: video.views, | ||
65 | sensitive: video.nsfw, | ||
66 | waitTranscoding: video.waitTranscoding, | ||
67 | |||
68 | state: video.state, | ||
69 | commentsEnabled: video.commentsEnabled, | ||
70 | downloadEnabled: video.downloadEnabled, | ||
71 | published: video.publishedAt.toISOString(), | ||
72 | |||
73 | originallyPublishedAt: video.originallyPublishedAt | ||
74 | ? video.originallyPublishedAt.toISOString() | ||
75 | : null, | ||
76 | |||
77 | updated: video.updatedAt.toISOString(), | ||
78 | |||
79 | tag: buildTags(video), | ||
80 | |||
81 | mediaType: 'text/markdown', | ||
82 | content: video.description, | ||
83 | support: video.support, | ||
84 | |||
85 | subtitleLanguage: buildSubtitleLanguage(video), | ||
86 | |||
87 | icon: buildIcon(video), | ||
88 | |||
89 | preview: buildPreviewAPAttribute(video), | ||
90 | |||
91 | url, | ||
92 | |||
93 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
94 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
95 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
96 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
97 | |||
98 | attributedTo: [ | ||
99 | { | ||
100 | type: 'Person', | ||
101 | id: video.VideoChannel.Account.Actor.url | ||
102 | }, | ||
103 | { | ||
104 | type: 'Group', | ||
105 | id: video.VideoChannel.Actor.url | ||
106 | } | ||
107 | ], | ||
108 | |||
109 | ...buildLiveAPAttributes(video) | ||
110 | } | ||
111 | } | ||
112 | |||
113 | // --------------------------------------------------------------------------- | ||
114 | // Private | ||
115 | // --------------------------------------------------------------------------- | ||
116 | |||
117 | function buildLiveAPAttributes (video: MVideoAP) { | ||
118 | if (!video.isLive) { | ||
119 | return { | ||
120 | isLiveBroadcast: false, | ||
121 | liveSaveReplay: null, | ||
122 | permanentLive: null, | ||
123 | latencyMode: null | ||
124 | } | ||
125 | } | ||
126 | |||
127 | return { | ||
128 | isLiveBroadcast: true, | ||
129 | liveSaveReplay: video.VideoLive.saveReplay, | ||
130 | permanentLive: video.VideoLive.permanentLive, | ||
131 | latencyMode: video.VideoLive.latencyMode | ||
132 | } | ||
133 | } | ||
134 | |||
135 | function buildPreviewAPAttribute (video: MVideoAP): ActivityPubStoryboard[] { | ||
136 | if (!video.Storyboard) return undefined | ||
137 | |||
138 | const storyboard = video.Storyboard | ||
139 | |||
140 | return [ | ||
141 | { | ||
142 | type: 'Image', | ||
143 | rel: [ 'storyboard' ], | ||
144 | url: [ | ||
145 | { | ||
146 | mediaType: 'image/jpeg', | ||
147 | |||
148 | href: storyboard.getOriginFileUrl(video), | ||
149 | |||
150 | width: storyboard.totalWidth, | ||
151 | height: storyboard.totalHeight, | ||
152 | |||
153 | tileWidth: storyboard.spriteWidth, | ||
154 | tileHeight: storyboard.spriteHeight, | ||
155 | tileDuration: getActivityStreamDuration(storyboard.spriteDuration) | ||
156 | } | ||
157 | ] | ||
158 | } | ||
159 | ] | ||
160 | } | ||
161 | |||
162 | function buildVideoFileUrls (options: { | ||
163 | video: MVideo | ||
164 | files: MVideoFile[] | ||
165 | user?: MUserId | ||
166 | }): ActivityUrlObject[] { | ||
167 | const { video, files } = options | ||
168 | |||
169 | if (!isArray(files)) return [] | ||
170 | |||
171 | const urls: ActivityUrlObject[] = [] | ||
172 | |||
173 | const trackerUrls = video.getTrackerUrls() | ||
174 | const sortedFiles = files | ||
175 | .filter(f => !f.isLive()) | ||
176 | .sort(sortByResolutionDesc) | ||
177 | |||
178 | for (const file of sortedFiles) { | ||
179 | urls.push({ | ||
180 | type: 'Link', | ||
181 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
182 | href: file.getFileUrl(video), | ||
183 | height: file.resolution, | ||
184 | size: file.size, | ||
185 | fps: file.fps | ||
186 | }) | ||
187 | |||
188 | urls.push({ | ||
189 | type: 'Link', | ||
190 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
191 | mediaType: 'application/json' as 'application/json', | ||
192 | href: getLocalVideoFileMetadataUrl(video, file), | ||
193 | height: file.resolution, | ||
194 | fps: file.fps | ||
195 | }) | ||
196 | |||
197 | if (file.hasTorrent()) { | ||
198 | urls.push({ | ||
199 | type: 'Link', | ||
200 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
201 | href: file.getTorrentUrl(), | ||
202 | height: file.resolution | ||
203 | }) | ||
204 | |||
205 | urls.push({ | ||
206 | type: 'Link', | ||
207 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
208 | href: generateMagnetUri(video, file, trackerUrls), | ||
209 | height: file.resolution | ||
210 | }) | ||
211 | } | ||
212 | } | ||
213 | |||
214 | return urls | ||
215 | } | ||
216 | |||
217 | // --------------------------------------------------------------------------- | ||
218 | |||
219 | function buildStreamingPlaylistUrls (video: MVideoAP): ActivityPlaylistUrlObject[] { | ||
220 | if (!isArray(video.VideoStreamingPlaylists)) return [] | ||
221 | |||
222 | return video.VideoStreamingPlaylists | ||
223 | .map(playlist => ({ | ||
224 | type: 'Link', | ||
225 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
226 | href: playlist.getMasterPlaylistUrl(video), | ||
227 | tag: buildStreamingPlaylistTags(video, playlist) | ||
228 | })) | ||
229 | } | ||
230 | |||
231 | function buildStreamingPlaylistTags (video: MVideoAP, playlist: MStreamingPlaylistFiles) { | ||
232 | return [ | ||
233 | ...playlist.p2pMediaLoaderInfohashes.map(i => ({ type: 'Infohash' as 'Infohash', name: i })), | ||
234 | |||
235 | { | ||
236 | type: 'Link', | ||
237 | name: 'sha256', | ||
238 | mediaType: 'application/json' as 'application/json', | ||
239 | href: playlist.getSha256SegmentsUrl(video) | ||
240 | }, | ||
241 | |||
242 | ...buildVideoFileUrls({ video, files: playlist.VideoFiles }) | ||
243 | ] as ActivityTagObject[] | ||
244 | } | ||
245 | |||
246 | // --------------------------------------------------------------------------- | ||
247 | |||
248 | function buildTrackerUrls (video: MVideoAP): ActivityTrackerUrlObject[] { | ||
249 | return video.getTrackerUrls() | ||
250 | .map(trackerUrl => { | ||
251 | const rel2 = trackerUrl.startsWith('http') | ||
252 | ? 'http' | ||
253 | : 'websocket' | ||
254 | |||
255 | return { | ||
256 | type: 'Link', | ||
257 | name: `tracker-${rel2}`, | ||
258 | rel: [ 'tracker', rel2 ], | ||
259 | href: trackerUrl | ||
260 | } | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
266 | function buildTags (video: MVideoAP) { | ||
267 | if (!isArray(video.Tags)) return [] | ||
268 | |||
269 | return video.Tags.map(t => ({ | ||
270 | type: 'Hashtag' as 'Hashtag', | ||
271 | name: t.name | ||
272 | })) | ||
273 | } | ||
274 | |||
275 | function buildIcon (video: MVideoAP): ActivityIconObject[] { | ||
276 | return [ video.getMiniature(), video.getPreview() ] | ||
277 | .map(i => ({ | ||
278 | type: 'Image', | ||
279 | url: i.getOriginFileUrl(video), | ||
280 | mediaType: 'image/jpeg', | ||
281 | width: i.width, | ||
282 | height: i.height | ||
283 | })) | ||
284 | } | ||
285 | |||
286 | function buildSubtitleLanguage (video: MVideoAP) { | ||
287 | if (!isArray(video.VideoCaptions)) return [] | ||
288 | |||
289 | return video.VideoCaptions | ||
290 | .map(caption => ({ | ||
291 | identifier: caption.language, | ||
292 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
293 | url: caption.getFileUrl(video) | ||
294 | })) | ||
295 | } | ||
diff --git a/server/models/video/formatter/video-api-format.ts b/server/models/video/formatter/video-api-format.ts new file mode 100644 index 000000000..1af51d132 --- /dev/null +++ b/server/models/video/formatter/video-api-format.ts | |||
@@ -0,0 +1,304 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
4 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
5 | import { uuidToShort } from '@shared/extra-utils' | ||
6 | import { | ||
7 | Video, | ||
8 | VideoAdditionalAttributes, | ||
9 | VideoDetails, | ||
10 | VideoFile, | ||
11 | VideoInclude, | ||
12 | VideosCommonQueryAfterSanitize, | ||
13 | VideoStreamingPlaylist | ||
14 | } from '@shared/models' | ||
15 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
16 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants' | ||
17 | import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models' | ||
18 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
19 | import { sortByResolutionDesc } from './shared' | ||
20 | |||
21 | export type VideoFormattingJSONOptions = { | ||
22 | completeDescription?: boolean | ||
23 | |||
24 | additionalAttributes?: { | ||
25 | state?: boolean | ||
26 | waitTranscoding?: boolean | ||
27 | scheduledUpdate?: boolean | ||
28 | blacklistInfo?: boolean | ||
29 | files?: boolean | ||
30 | blockedOwner?: boolean | ||
31 | } | ||
32 | } | ||
33 | |||
34 | export function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
35 | if (!query?.include) return {} | ||
36 | |||
37 | return { | ||
38 | additionalAttributes: { | ||
39 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
40 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
41 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
42 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
43 | files: !!(query.include & VideoInclude.FILES), | ||
44 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
52 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
53 | |||
54 | const userHistory = isArray(video.UserVideoHistories) | ||
55 | ? video.UserVideoHistories[0] | ||
56 | : undefined | ||
57 | |||
58 | const videoObject: Video = { | ||
59 | id: video.id, | ||
60 | uuid: video.uuid, | ||
61 | shortUUID: uuidToShort(video.uuid), | ||
62 | |||
63 | url: video.url, | ||
64 | |||
65 | name: video.name, | ||
66 | category: { | ||
67 | id: video.category, | ||
68 | label: getCategoryLabel(video.category) | ||
69 | }, | ||
70 | licence: { | ||
71 | id: video.licence, | ||
72 | label: getLicenceLabel(video.licence) | ||
73 | }, | ||
74 | language: { | ||
75 | id: video.language, | ||
76 | label: getLanguageLabel(video.language) | ||
77 | }, | ||
78 | privacy: { | ||
79 | id: video.privacy, | ||
80 | label: getPrivacyLabel(video.privacy) | ||
81 | }, | ||
82 | nsfw: video.nsfw, | ||
83 | |||
84 | truncatedDescription: video.getTruncatedDescription(), | ||
85 | description: options && options.completeDescription === true | ||
86 | ? video.description | ||
87 | : video.getTruncatedDescription(), | ||
88 | |||
89 | isLocal: video.isOwned(), | ||
90 | duration: video.duration, | ||
91 | |||
92 | views: video.views, | ||
93 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
94 | |||
95 | likes: video.likes, | ||
96 | dislikes: video.dislikes, | ||
97 | thumbnailPath: video.getMiniatureStaticPath(), | ||
98 | previewPath: video.getPreviewStaticPath(), | ||
99 | embedPath: video.getEmbedStaticPath(), | ||
100 | createdAt: video.createdAt, | ||
101 | updatedAt: video.updatedAt, | ||
102 | publishedAt: video.publishedAt, | ||
103 | originallyPublishedAt: video.originallyPublishedAt, | ||
104 | |||
105 | isLive: video.isLive, | ||
106 | |||
107 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
108 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
109 | |||
110 | userHistory: userHistory | ||
111 | ? { currentTime: userHistory.currentTime } | ||
112 | : undefined, | ||
113 | |||
114 | // Can be added by external plugins | ||
115 | pluginData: (video as any).pluginData, | ||
116 | |||
117 | ...buildAdditionalAttributes(video, options) | ||
118 | } | ||
119 | |||
120 | span.end() | ||
121 | |||
122 | return videoObject | ||
123 | } | ||
124 | |||
125 | export function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
126 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
127 | |||
128 | const videoJSON = video.toFormattedJSON({ | ||
129 | completeDescription: true, | ||
130 | additionalAttributes: { | ||
131 | scheduledUpdate: true, | ||
132 | blacklistInfo: true, | ||
133 | files: true | ||
134 | } | ||
135 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists' | 'scheduledUpdate' | 'blacklisted' | 'blacklistedReason'>> | ||
136 | |||
137 | const tags = video.Tags | ||
138 | ? video.Tags.map(t => t.name) | ||
139 | : [] | ||
140 | |||
141 | const detailsJSON = { | ||
142 | ...videoJSON, | ||
143 | |||
144 | support: video.support, | ||
145 | descriptionPath: video.getDescriptionAPIPath(), | ||
146 | channel: video.VideoChannel.toFormattedJSON(), | ||
147 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
148 | tags, | ||
149 | commentsEnabled: video.commentsEnabled, | ||
150 | downloadEnabled: video.downloadEnabled, | ||
151 | waitTranscoding: video.waitTranscoding, | ||
152 | state: { | ||
153 | id: video.state, | ||
154 | label: getStateLabel(video.state) | ||
155 | }, | ||
156 | |||
157 | trackerUrls: video.getTrackerUrls() | ||
158 | } | ||
159 | |||
160 | span.end() | ||
161 | |||
162 | return detailsJSON | ||
163 | } | ||
164 | |||
165 | export function streamingPlaylistsModelToFormattedJSON ( | ||
166 | video: MVideoFormattable, | ||
167 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
168 | ): VideoStreamingPlaylist[] { | ||
169 | if (isArray(playlists) === false) return [] | ||
170 | |||
171 | return playlists | ||
172 | .map(playlist => ({ | ||
173 | id: playlist.id, | ||
174 | type: playlist.type, | ||
175 | |||
176 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
177 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
178 | |||
179 | redundancies: isArray(playlist.RedundancyVideos) | ||
180 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
181 | : [], | ||
182 | |||
183 | files: videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
184 | })) | ||
185 | } | ||
186 | |||
187 | export function videoFilesModelToFormattedJSON ( | ||
188 | video: MVideoFormattable, | ||
189 | videoFiles: MVideoFileRedundanciesOpt[], | ||
190 | options: { | ||
191 | includeMagnet?: boolean // default true | ||
192 | } = {} | ||
193 | ): VideoFile[] { | ||
194 | const { includeMagnet = true } = options | ||
195 | |||
196 | if (isArray(videoFiles) === false) return [] | ||
197 | |||
198 | const trackerUrls = includeMagnet | ||
199 | ? video.getTrackerUrls() | ||
200 | : [] | ||
201 | |||
202 | return videoFiles | ||
203 | .filter(f => !f.isLive()) | ||
204 | .sort(sortByResolutionDesc) | ||
205 | .map(videoFile => { | ||
206 | return { | ||
207 | id: videoFile.id, | ||
208 | |||
209 | resolution: { | ||
210 | id: videoFile.resolution, | ||
211 | label: videoFile.resolution === 0 | ||
212 | ? 'Audio' | ||
213 | : `${videoFile.resolution}p` | ||
214 | }, | ||
215 | |||
216 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
217 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
218 | : undefined, | ||
219 | |||
220 | size: videoFile.size, | ||
221 | fps: videoFile.fps, | ||
222 | |||
223 | torrentUrl: videoFile.getTorrentUrl(), | ||
224 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
225 | |||
226 | fileUrl: videoFile.getFileUrl(video), | ||
227 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
228 | |||
229 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
230 | } | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | // --------------------------------------------------------------------------- | ||
235 | |||
236 | export function getCategoryLabel (id: number) { | ||
237 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
238 | } | ||
239 | |||
240 | export function getLicenceLabel (id: number) { | ||
241 | return VIDEO_LICENCES[id] || 'Unknown' | ||
242 | } | ||
243 | |||
244 | export function getLanguageLabel (id: string) { | ||
245 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
246 | } | ||
247 | |||
248 | export function getPrivacyLabel (id: number) { | ||
249 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
250 | } | ||
251 | |||
252 | export function getStateLabel (id: number) { | ||
253 | return VIDEO_STATES[id] || 'Unknown' | ||
254 | } | ||
255 | |||
256 | // --------------------------------------------------------------------------- | ||
257 | // Private | ||
258 | // --------------------------------------------------------------------------- | ||
259 | |||
260 | function buildAdditionalAttributes (video: MVideoFormattable, options: VideoFormattingJSONOptions) { | ||
261 | const add = options.additionalAttributes | ||
262 | |||
263 | const result: Partial<VideoAdditionalAttributes> = {} | ||
264 | |||
265 | if (add?.state === true) { | ||
266 | result.state = { | ||
267 | id: video.state, | ||
268 | label: getStateLabel(video.state) | ||
269 | } | ||
270 | } | ||
271 | |||
272 | if (add?.waitTranscoding === true) { | ||
273 | result.waitTranscoding = video.waitTranscoding | ||
274 | } | ||
275 | |||
276 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
277 | result.scheduledUpdate = { | ||
278 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
279 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
280 | } | ||
281 | } | ||
282 | |||
283 | if (add?.blacklistInfo === true) { | ||
284 | result.blacklisted = !!video.VideoBlacklist | ||
285 | result.blacklistedReason = | ||
286 | video.VideoBlacklist | ||
287 | ? video.VideoBlacklist.reason | ||
288 | : null | ||
289 | } | ||
290 | |||
291 | if (add?.blockedOwner === true) { | ||
292 | result.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
293 | |||
294 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
295 | result.blockedServer = !!(server?.isBlocked()) | ||
296 | } | ||
297 | |||
298 | if (add?.files === true) { | ||
299 | result.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
300 | result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
301 | } | ||
302 | |||
303 | return result | ||
304 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts deleted file mode 100644 index f2001e432..000000000 --- a/server/models/video/formatter/video-format-utils.ts +++ /dev/null | |||
@@ -1,543 +0,0 @@ | |||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
2 | import { getActivityStreamDuration } from '@server/lib/activitypub/activity' | ||
3 | import { tracer } from '@server/lib/opentelemetry/tracing' | ||
4 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | ||
5 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
6 | import { uuidToShort } from '@shared/extra-utils' | ||
7 | import { | ||
8 | ActivityTagObject, | ||
9 | ActivityUrlObject, | ||
10 | Video, | ||
11 | VideoDetails, | ||
12 | VideoFile, | ||
13 | VideoInclude, | ||
14 | VideoObject, | ||
15 | VideosCommonQueryAfterSanitize, | ||
16 | VideoStreamingPlaylist | ||
17 | } from '@shared/models' | ||
18 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
19 | import { | ||
20 | MIMETYPES, | ||
21 | VIDEO_CATEGORIES, | ||
22 | VIDEO_LANGUAGES, | ||
23 | VIDEO_LICENCES, | ||
24 | VIDEO_PRIVACIES, | ||
25 | VIDEO_STATES, | ||
26 | WEBSERVER | ||
27 | } from '../../../initializers/constants' | ||
28 | import { | ||
29 | getLocalVideoCommentsActivityPubUrl, | ||
30 | getLocalVideoDislikesActivityPubUrl, | ||
31 | getLocalVideoLikesActivityPubUrl, | ||
32 | getLocalVideoSharesActivityPubUrl | ||
33 | } from '../../../lib/activitypub/url' | ||
34 | import { | ||
35 | MServer, | ||
36 | MStreamingPlaylistRedundanciesOpt, | ||
37 | MUserId, | ||
38 | MVideo, | ||
39 | MVideoAP, | ||
40 | MVideoFile, | ||
41 | MVideoFormattable, | ||
42 | MVideoFormattableDetails | ||
43 | } from '../../../types/models' | ||
44 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' | ||
45 | import { VideoCaptionModel } from '../video-caption' | ||
46 | |||
47 | export type VideoFormattingJSONOptions = { | ||
48 | completeDescription?: boolean | ||
49 | |||
50 | additionalAttributes?: { | ||
51 | state?: boolean | ||
52 | waitTranscoding?: boolean | ||
53 | scheduledUpdate?: boolean | ||
54 | blacklistInfo?: boolean | ||
55 | files?: boolean | ||
56 | blockedOwner?: boolean | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { | ||
61 | if (!query?.include) return {} | ||
62 | |||
63 | return { | ||
64 | additionalAttributes: { | ||
65 | state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
66 | waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
67 | scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), | ||
68 | blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), | ||
69 | files: !!(query.include & VideoInclude.FILES), | ||
70 | blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video { | ||
76 | const span = tracer.startSpan('peertube.VideoModel.toFormattedJSON') | ||
77 | |||
78 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | ||
79 | |||
80 | const videoObject: Video = { | ||
81 | id: video.id, | ||
82 | uuid: video.uuid, | ||
83 | shortUUID: uuidToShort(video.uuid), | ||
84 | |||
85 | url: video.url, | ||
86 | |||
87 | name: video.name, | ||
88 | category: { | ||
89 | id: video.category, | ||
90 | label: getCategoryLabel(video.category) | ||
91 | }, | ||
92 | licence: { | ||
93 | id: video.licence, | ||
94 | label: getLicenceLabel(video.licence) | ||
95 | }, | ||
96 | language: { | ||
97 | id: video.language, | ||
98 | label: getLanguageLabel(video.language) | ||
99 | }, | ||
100 | privacy: { | ||
101 | id: video.privacy, | ||
102 | label: getPrivacyLabel(video.privacy) | ||
103 | }, | ||
104 | nsfw: video.nsfw, | ||
105 | |||
106 | truncatedDescription: video.getTruncatedDescription(), | ||
107 | description: options && options.completeDescription === true | ||
108 | ? video.description | ||
109 | : video.getTruncatedDescription(), | ||
110 | |||
111 | isLocal: video.isOwned(), | ||
112 | duration: video.duration, | ||
113 | |||
114 | views: video.views, | ||
115 | viewers: VideoViewsManager.Instance.getViewers(video), | ||
116 | |||
117 | likes: video.likes, | ||
118 | dislikes: video.dislikes, | ||
119 | thumbnailPath: video.getMiniatureStaticPath(), | ||
120 | previewPath: video.getPreviewStaticPath(), | ||
121 | embedPath: video.getEmbedStaticPath(), | ||
122 | createdAt: video.createdAt, | ||
123 | updatedAt: video.updatedAt, | ||
124 | publishedAt: video.publishedAt, | ||
125 | originallyPublishedAt: video.originallyPublishedAt, | ||
126 | |||
127 | isLive: video.isLive, | ||
128 | |||
129 | account: video.VideoChannel.Account.toFormattedSummaryJSON(), | ||
130 | channel: video.VideoChannel.toFormattedSummaryJSON(), | ||
131 | |||
132 | userHistory: userHistory | ||
133 | ? { currentTime: userHistory.currentTime } | ||
134 | : undefined, | ||
135 | |||
136 | // Can be added by external plugins | ||
137 | pluginData: (video as any).pluginData | ||
138 | } | ||
139 | |||
140 | const add = options.additionalAttributes | ||
141 | if (add?.state === true) { | ||
142 | videoObject.state = { | ||
143 | id: video.state, | ||
144 | label: getStateLabel(video.state) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (add?.waitTranscoding === true) { | ||
149 | videoObject.waitTranscoding = video.waitTranscoding | ||
150 | } | ||
151 | |||
152 | if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
153 | videoObject.scheduledUpdate = { | ||
154 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
155 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
156 | } | ||
157 | } | ||
158 | |||
159 | if (add?.blacklistInfo === true) { | ||
160 | videoObject.blacklisted = !!video.VideoBlacklist | ||
161 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
162 | } | ||
163 | |||
164 | if (add?.blockedOwner === true) { | ||
165 | videoObject.blockedOwner = video.VideoChannel.Account.isBlocked() | ||
166 | |||
167 | const server = video.VideoChannel.Account.Actor.Server as MServer | ||
168 | videoObject.blockedServer = !!(server?.isBlocked()) | ||
169 | } | ||
170 | |||
171 | if (add?.files === true) { | ||
172 | videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
173 | videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
174 | } | ||
175 | |||
176 | span.end() | ||
177 | |||
178 | return videoObject | ||
179 | } | ||
180 | |||
181 | function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { | ||
182 | const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') | ||
183 | |||
184 | const videoJSON = video.toFormattedJSON({ | ||
185 | completeDescription: true, | ||
186 | additionalAttributes: { | ||
187 | scheduledUpdate: true, | ||
188 | blacklistInfo: true, | ||
189 | files: true | ||
190 | } | ||
191 | }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>> | ||
192 | |||
193 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | ||
194 | |||
195 | const detailsJSON = { | ||
196 | support: video.support, | ||
197 | descriptionPath: video.getDescriptionAPIPath(), | ||
198 | channel: video.VideoChannel.toFormattedJSON(), | ||
199 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
200 | tags, | ||
201 | commentsEnabled: video.commentsEnabled, | ||
202 | downloadEnabled: video.downloadEnabled, | ||
203 | waitTranscoding: video.waitTranscoding, | ||
204 | state: { | ||
205 | id: video.state, | ||
206 | label: getStateLabel(video.state) | ||
207 | }, | ||
208 | |||
209 | trackerUrls: video.getTrackerUrls() | ||
210 | } | ||
211 | |||
212 | span.end() | ||
213 | |||
214 | return Object.assign(videoJSON, detailsJSON) | ||
215 | } | ||
216 | |||
217 | function streamingPlaylistsModelToFormattedJSON ( | ||
218 | video: MVideoFormattable, | ||
219 | playlists: MStreamingPlaylistRedundanciesOpt[] | ||
220 | ): VideoStreamingPlaylist[] { | ||
221 | if (isArray(playlists) === false) return [] | ||
222 | |||
223 | return playlists | ||
224 | .map(playlist => { | ||
225 | const redundancies = isArray(playlist.RedundancyVideos) | ||
226 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
227 | : [] | ||
228 | |||
229 | const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles) | ||
230 | |||
231 | return { | ||
232 | id: playlist.id, | ||
233 | type: playlist.type, | ||
234 | playlistUrl: playlist.getMasterPlaylistUrl(video), | ||
235 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), | ||
236 | redundancies, | ||
237 | files | ||
238 | } | ||
239 | }) | ||
240 | } | ||
241 | |||
242 | function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) { | ||
243 | if (fileA.resolution < fileB.resolution) return 1 | ||
244 | if (fileA.resolution === fileB.resolution) return 0 | ||
245 | return -1 | ||
246 | } | ||
247 | |||
248 | function videoFilesModelToFormattedJSON ( | ||
249 | video: MVideoFormattable, | ||
250 | videoFiles: MVideoFileRedundanciesOpt[], | ||
251 | options: { | ||
252 | includeMagnet?: boolean // default true | ||
253 | } = {} | ||
254 | ): VideoFile[] { | ||
255 | const { includeMagnet = true } = options | ||
256 | |||
257 | const trackerUrls = includeMagnet | ||
258 | ? video.getTrackerUrls() | ||
259 | : [] | ||
260 | |||
261 | return (videoFiles || []) | ||
262 | .filter(f => !f.isLive()) | ||
263 | .sort(sortByResolutionDesc) | ||
264 | .map(videoFile => { | ||
265 | return { | ||
266 | id: videoFile.id, | ||
267 | |||
268 | resolution: { | ||
269 | id: videoFile.resolution, | ||
270 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` | ||
271 | }, | ||
272 | |||
273 | magnetUri: includeMagnet && videoFile.hasTorrent() | ||
274 | ? generateMagnetUri(video, videoFile, trackerUrls) | ||
275 | : undefined, | ||
276 | |||
277 | size: videoFile.size, | ||
278 | fps: videoFile.fps, | ||
279 | |||
280 | torrentUrl: videoFile.getTorrentUrl(), | ||
281 | torrentDownloadUrl: videoFile.getTorrentDownloadUrl(), | ||
282 | |||
283 | fileUrl: videoFile.getFileUrl(video), | ||
284 | fileDownloadUrl: videoFile.getFileDownloadUrl(video), | ||
285 | |||
286 | metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile) | ||
287 | } as VideoFile | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | function addVideoFilesInAPAcc (options: { | ||
292 | acc: ActivityUrlObject[] | ActivityTagObject[] | ||
293 | video: MVideo | ||
294 | files: MVideoFile[] | ||
295 | user?: MUserId | ||
296 | }) { | ||
297 | const { acc, video, files } = options | ||
298 | |||
299 | const trackerUrls = video.getTrackerUrls() | ||
300 | |||
301 | const sortedFiles = (files || []) | ||
302 | .filter(f => !f.isLive()) | ||
303 | .sort(sortByResolutionDesc) | ||
304 | |||
305 | for (const file of sortedFiles) { | ||
306 | acc.push({ | ||
307 | type: 'Link', | ||
308 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, | ||
309 | href: file.getFileUrl(video), | ||
310 | height: file.resolution, | ||
311 | size: file.size, | ||
312 | fps: file.fps | ||
313 | }) | ||
314 | |||
315 | acc.push({ | ||
316 | type: 'Link', | ||
317 | rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], | ||
318 | mediaType: 'application/json' as 'application/json', | ||
319 | href: getLocalVideoFileMetadataUrl(video, file), | ||
320 | height: file.resolution, | ||
321 | fps: file.fps | ||
322 | }) | ||
323 | |||
324 | if (file.hasTorrent()) { | ||
325 | acc.push({ | ||
326 | type: 'Link', | ||
327 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
328 | href: file.getTorrentUrl(), | ||
329 | height: file.resolution | ||
330 | }) | ||
331 | |||
332 | acc.push({ | ||
333 | type: 'Link', | ||
334 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
335 | href: generateMagnetUri(video, file, trackerUrls), | ||
336 | height: file.resolution | ||
337 | }) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | |||
342 | function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | ||
343 | if (!video.Tags) video.Tags = [] | ||
344 | |||
345 | const tag = video.Tags.map(t => ({ | ||
346 | type: 'Hashtag' as 'Hashtag', | ||
347 | name: t.name | ||
348 | })) | ||
349 | |||
350 | let language | ||
351 | if (video.language) { | ||
352 | language = { | ||
353 | identifier: video.language, | ||
354 | name: getLanguageLabel(video.language) | ||
355 | } | ||
356 | } | ||
357 | |||
358 | let category | ||
359 | if (video.category) { | ||
360 | category = { | ||
361 | identifier: video.category + '', | ||
362 | name: getCategoryLabel(video.category) | ||
363 | } | ||
364 | } | ||
365 | |||
366 | let licence | ||
367 | if (video.licence) { | ||
368 | licence = { | ||
369 | identifier: video.licence + '', | ||
370 | name: getLicenceLabel(video.licence) | ||
371 | } | ||
372 | } | ||
373 | |||
374 | const url: ActivityUrlObject[] = [ | ||
375 | // HTML url should be the first element in the array so Mastodon correctly displays the embed | ||
376 | { | ||
377 | type: 'Link', | ||
378 | mediaType: 'text/html', | ||
379 | href: WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
380 | } | ||
381 | ] | ||
382 | |||
383 | addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] }) | ||
384 | |||
385 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
386 | const tag = playlist.p2pMediaLoaderInfohashes | ||
387 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[] | ||
388 | tag.push({ | ||
389 | type: 'Link', | ||
390 | name: 'sha256', | ||
391 | mediaType: 'application/json' as 'application/json', | ||
392 | href: playlist.getSha256SegmentsUrl(video) | ||
393 | }) | ||
394 | |||
395 | addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] }) | ||
396 | |||
397 | url.push({ | ||
398 | type: 'Link', | ||
399 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
400 | href: playlist.getMasterPlaylistUrl(video), | ||
401 | tag | ||
402 | }) | ||
403 | } | ||
404 | |||
405 | for (const trackerUrl of video.getTrackerUrls()) { | ||
406 | const rel2 = trackerUrl.startsWith('http') | ||
407 | ? 'http' | ||
408 | : 'websocket' | ||
409 | |||
410 | url.push({ | ||
411 | type: 'Link', | ||
412 | name: `tracker-${rel2}`, | ||
413 | rel: [ 'tracker', rel2 ], | ||
414 | href: trackerUrl | ||
415 | }) | ||
416 | } | ||
417 | |||
418 | const subtitleLanguage = [] | ||
419 | for (const caption of video.VideoCaptions) { | ||
420 | subtitleLanguage.push({ | ||
421 | identifier: caption.language, | ||
422 | name: VideoCaptionModel.getLanguageLabel(caption.language), | ||
423 | url: caption.getFileUrl(video) | ||
424 | }) | ||
425 | } | ||
426 | |||
427 | const icons = [ video.getMiniature(), video.getPreview() ] | ||
428 | |||
429 | return { | ||
430 | type: 'Video' as 'Video', | ||
431 | id: video.url, | ||
432 | name: video.name, | ||
433 | duration: getActivityStreamDuration(video.duration), | ||
434 | uuid: video.uuid, | ||
435 | tag, | ||
436 | category, | ||
437 | licence, | ||
438 | language, | ||
439 | views: video.views, | ||
440 | sensitive: video.nsfw, | ||
441 | waitTranscoding: video.waitTranscoding, | ||
442 | |||
443 | state: video.state, | ||
444 | commentsEnabled: video.commentsEnabled, | ||
445 | downloadEnabled: video.downloadEnabled, | ||
446 | published: video.publishedAt.toISOString(), | ||
447 | |||
448 | originallyPublishedAt: video.originallyPublishedAt | ||
449 | ? video.originallyPublishedAt.toISOString() | ||
450 | : null, | ||
451 | |||
452 | updated: video.updatedAt.toISOString(), | ||
453 | |||
454 | mediaType: 'text/markdown', | ||
455 | content: video.description, | ||
456 | support: video.support, | ||
457 | |||
458 | subtitleLanguage, | ||
459 | |||
460 | icon: icons.map(i => ({ | ||
461 | type: 'Image', | ||
462 | url: i.getOriginFileUrl(video), | ||
463 | mediaType: 'image/jpeg', | ||
464 | width: i.width, | ||
465 | height: i.height | ||
466 | })), | ||
467 | |||
468 | url, | ||
469 | |||
470 | likes: getLocalVideoLikesActivityPubUrl(video), | ||
471 | dislikes: getLocalVideoDislikesActivityPubUrl(video), | ||
472 | shares: getLocalVideoSharesActivityPubUrl(video), | ||
473 | comments: getLocalVideoCommentsActivityPubUrl(video), | ||
474 | |||
475 | attributedTo: [ | ||
476 | { | ||
477 | type: 'Person', | ||
478 | id: video.VideoChannel.Account.Actor.url | ||
479 | }, | ||
480 | { | ||
481 | type: 'Group', | ||
482 | id: video.VideoChannel.Actor.url | ||
483 | } | ||
484 | ], | ||
485 | |||
486 | ...buildLiveAPAttributes(video) | ||
487 | } | ||
488 | } | ||
489 | |||
490 | function getCategoryLabel (id: number) { | ||
491 | return VIDEO_CATEGORIES[id] || 'Unknown' | ||
492 | } | ||
493 | |||
494 | function getLicenceLabel (id: number) { | ||
495 | return VIDEO_LICENCES[id] || 'Unknown' | ||
496 | } | ||
497 | |||
498 | function getLanguageLabel (id: string) { | ||
499 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
500 | } | ||
501 | |||
502 | function getPrivacyLabel (id: number) { | ||
503 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
504 | } | ||
505 | |||
506 | function getStateLabel (id: number) { | ||
507 | return VIDEO_STATES[id] || 'Unknown' | ||
508 | } | ||
509 | |||
510 | export { | ||
511 | videoModelToFormattedJSON, | ||
512 | videoModelToFormattedDetailsJSON, | ||
513 | videoFilesModelToFormattedJSON, | ||
514 | videoModelToActivityPubObject, | ||
515 | |||
516 | guessAdditionalAttributesFromQuery, | ||
517 | |||
518 | getCategoryLabel, | ||
519 | getLicenceLabel, | ||
520 | getLanguageLabel, | ||
521 | getPrivacyLabel, | ||
522 | getStateLabel | ||
523 | } | ||
524 | |||
525 | // --------------------------------------------------------------------------- | ||
526 | |||
527 | function buildLiveAPAttributes (video: MVideoAP) { | ||
528 | if (!video.isLive) { | ||
529 | return { | ||
530 | isLiveBroadcast: false, | ||
531 | liveSaveReplay: null, | ||
532 | permanentLive: null, | ||
533 | latencyMode: null | ||
534 | } | ||
535 | } | ||
536 | |||
537 | return { | ||
538 | isLiveBroadcast: true, | ||
539 | liveSaveReplay: video.VideoLive.saveReplay, | ||
540 | permanentLive: video.VideoLive.permanentLive, | ||
541 | latencyMode: video.VideoLive.latencyMode | ||
542 | } | ||
543 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index cbd57ad8c..56a00aa0c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -111,7 +111,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
111 | } | 111 | } |
112 | } | 112 | } |
113 | 113 | ||
114 | protected includeWebtorrentFiles () { | 114 | protected includeWebVideoFiles () { |
115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | 115 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') |
116 | 116 | ||
117 | this.attributes = { | 117 | this.attributes = { |
@@ -263,7 +263,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
263 | } | 263 | } |
264 | } | 264 | } |
265 | 265 | ||
266 | protected includeWebTorrentRedundancies () { | 266 | protected includeWebVideoRedundancies () { |
267 | this.addJoin( | 267 | this.addJoin( |
268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + | 268 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + |
269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' | 269 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' |
diff --git a/server/models/video/sql/video/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index cc53a4860..196b72b43 100644 --- a/server/models/video/sql/video/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
@@ -14,7 +14,7 @@ export type FileQueryOptions = { | |||
14 | 14 | ||
15 | /** | 15 | /** |
16 | * | 16 | * |
17 | * Fetch files (webtorrent and streaming playlist) according to a video | 17 | * Fetch files (web videos and streaming playlist) according to a video |
18 | * | 18 | * |
19 | */ | 19 | */ |
20 | 20 | ||
@@ -25,8 +25,8 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
25 | super(sequelize, 'get') | 25 | super(sequelize, 'get') |
26 | } | 26 | } |
27 | 27 | ||
28 | queryWebTorrentVideos (options: FileQueryOptions) { | 28 | queryWebVideos (options: FileQueryOptions) { |
29 | this.buildWebtorrentFilesQuery(options) | 29 | this.buildWebVideoFilesQuery(options) |
30 | 30 | ||
31 | return this.runQuery(options) | 31 | return this.runQuery(options) |
32 | } | 32 | } |
@@ -37,15 +37,15 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
37 | return this.runQuery(options) | 37 | return this.runQuery(options) |
38 | } | 38 | } |
39 | 39 | ||
40 | private buildWebtorrentFilesQuery (options: FileQueryOptions) { | 40 | private buildWebVideoFilesQuery (options: FileQueryOptions) { |
41 | this.attributes = { | 41 | this.attributes = { |
42 | '"video"."id"': '' | 42 | '"video"."id"': '' |
43 | } | 43 | } |
44 | 44 | ||
45 | this.includeWebtorrentFiles() | 45 | this.includeWebVideoFiles() |
46 | 46 | ||
47 | if (options.includeRedundancy) { | 47 | if (options.includeRedundancy) { |
48 | this.includeWebTorrentRedundancies() | 48 | this.includeWebVideoRedundancies() |
49 | } | 49 | } |
50 | 50 | ||
51 | this.whereId(options) | 51 | this.whereId(options) |
diff --git a/server/models/video/sql/video/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 0a2beb7db..740aa842f 100644 --- a/server/models/video/sql/video/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -60,10 +60,10 @@ export class VideoModelBuilder { | |||
60 | buildVideosFromRows (options: { | 60 | buildVideosFromRows (options: { |
61 | rows: SQLRow[] | 61 | rows: SQLRow[] |
62 | include?: VideoInclude | 62 | include?: VideoInclude |
63 | rowsWebTorrentFiles?: SQLRow[] | 63 | rowsWebVideoFiles?: SQLRow[] |
64 | rowsStreamingPlaylist?: SQLRow[] | 64 | rowsStreamingPlaylist?: SQLRow[] |
65 | }) { | 65 | }) { |
66 | const { rows, rowsWebTorrentFiles, rowsStreamingPlaylist, include } = options | 66 | const { rows, rowsWebVideoFiles, rowsStreamingPlaylist, include } = options |
67 | 67 | ||
68 | this.reinit() | 68 | this.reinit() |
69 | 69 | ||
@@ -85,8 +85,8 @@ export class VideoModelBuilder { | |||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | 85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) |
86 | } | 86 | } |
87 | 87 | ||
88 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebVideoFiles) { |
89 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebVideoFile(row, videoModel) |
90 | } | 90 | } |
91 | 91 | ||
92 | if (!rowsStreamingPlaylist) { | 92 | if (!rowsStreamingPlaylist) { |
@@ -112,7 +112,7 @@ export class VideoModelBuilder { | |||
112 | } | 112 | } |
113 | } | 113 | } |
114 | 114 | ||
115 | this.grabSeparateWebTorrentFiles(rowsWebTorrentFiles) | 115 | this.grabSeparateWebVideoFiles(rowsWebVideoFiles) |
116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) | 116 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) |
117 | 117 | ||
118 | return this.videos | 118 | return this.videos |
@@ -140,15 +140,15 @@ export class VideoModelBuilder { | |||
140 | this.videos = [] | 140 | this.videos = [] |
141 | } | 141 | } |
142 | 142 | ||
143 | private grabSeparateWebTorrentFiles (rowsWebTorrentFiles?: SQLRow[]) { | 143 | private grabSeparateWebVideoFiles (rowsWebVideoFiles?: SQLRow[]) { |
144 | if (!rowsWebTorrentFiles) return | 144 | if (!rowsWebVideoFiles) return |
145 | 145 | ||
146 | for (const row of rowsWebTorrentFiles) { | 146 | for (const row of rowsWebVideoFiles) { |
147 | const id = row['VideoFiles.id'] | 147 | const id = row['VideoFiles.id'] |
148 | if (!id) continue | 148 | if (!id) continue |
149 | 149 | ||
150 | const videoModel = this.videosMemo[row.id] | 150 | const videoModel = this.videosMemo[row.id] |
151 | this.addWebTorrentFile(row, videoModel) | 151 | this.addWebVideoFile(row, videoModel) |
152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) | 152 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) |
153 | } | 153 | } |
154 | } | 154 | } |
@@ -258,7 +258,7 @@ export class VideoModelBuilder { | |||
258 | this.thumbnailsDone.add(id) | 258 | this.thumbnailsDone.add(id) |
259 | } | 259 | } |
260 | 260 | ||
261 | private addWebTorrentFile (row: SQLRow, videoModel: VideoModel) { | 261 | private addWebVideoFile (row: SQLRow, videoModel: VideoModel) { |
262 | const id = row['VideoFiles.id'] | 262 | const id = row['VideoFiles.id'] |
263 | if (!id || this.videoFileMemo[id]) return | 263 | if (!id || this.videoFileMemo[id]) return |
264 | 264 | ||
diff --git a/server/models/video/sql/video/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 34967cd20..e0fa9d7c1 100644 --- a/server/models/video/sql/video/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -60,6 +60,7 @@ export class VideoTableAttributes { | |||
60 | 'height', | 60 | 'height', |
61 | 'width', | 61 | 'width', |
62 | 'fileUrl', | 62 | 'fileUrl', |
63 | 'onDisk', | ||
63 | 'automaticallyGenerated', | 64 | 'automaticallyGenerated', |
64 | 'videoId', | 65 | 'videoId', |
65 | 'videoPlaylistId', | 66 | 'videoPlaylistId', |
diff --git a/server/models/video/sql/video/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index 8e90ff641..3f43d4d92 100644 --- a/server/models/video/sql/video/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
@@ -35,7 +35,7 @@ export type BuildVideoGetQueryOptions = { | |||
35 | 35 | ||
36 | export class VideoModelGetQueryBuilder { | 36 | export class VideoModelGetQueryBuilder { |
37 | videoQueryBuilder: VideosModelGetQuerySubBuilder | 37 | videoQueryBuilder: VideosModelGetQuerySubBuilder |
38 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 38 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 39 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
40 | 40 | ||
41 | private readonly videoModelBuilder: VideoModelBuilder | 41 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -44,7 +44,7 @@ export class VideoModelGetQueryBuilder { | |||
44 | 44 | ||
45 | constructor (protected readonly sequelize: Sequelize) { | 45 | constructor (protected readonly sequelize: Sequelize) { |
46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) | 46 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) |
47 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 47 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 48 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
49 | 49 | ||
50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) | 50 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTableAttributes('get')) |
@@ -57,11 +57,11 @@ export class VideoModelGetQueryBuilder { | |||
57 | includeRedundancy: this.shouldIncludeRedundancies(options) | 57 | includeRedundancy: this.shouldIncludeRedundancies(options) |
58 | } | 58 | } |
59 | 59 | ||
60 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | 60 | const [ videoRows, webVideoFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ |
61 | this.videoQueryBuilder.queryVideos(options), | 61 | this.videoQueryBuilder.queryVideos(options), |
62 | 62 | ||
63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
64 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions) | 64 | ? this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions) |
65 | : Promise.resolve(undefined), | 65 | : Promise.resolve(undefined), |
66 | 66 | ||
67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
@@ -71,7 +71,7 @@ export class VideoModelGetQueryBuilder { | |||
71 | 71 | ||
72 | const videos = this.videoModelBuilder.buildVideosFromRows({ | 72 | const videos = this.videoModelBuilder.buildVideosFromRows({ |
73 | rows: videoRows, | 73 | rows: videoRows, |
74 | rowsWebTorrentFiles: webtorrentFilesRows, | 74 | rowsWebVideoFiles: webVideoFilesRows, |
75 | rowsStreamingPlaylist: streamingPlaylistFilesRows | 75 | rowsStreamingPlaylist: streamingPlaylistFilesRows |
76 | }) | 76 | }) |
77 | 77 | ||
@@ -92,7 +92,7 @@ export class VideoModelGetQueryBuilder { | |||
92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { | 92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { |
93 | protected attributes: { [key: string]: string } | 93 | protected attributes: { [key: string]: string } |
94 | 94 | ||
95 | protected webtorrentFilesQuery: string | 95 | protected webVideoFilesQuery: string |
96 | protected streamingPlaylistFilesQuery: string | 96 | protected streamingPlaylistFilesQuery: string |
97 | 97 | ||
98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) | 98 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index cba77c1d1..7f2376102 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -48,7 +48,9 @@ export type BuildVideosListQueryOptions = { | |||
48 | 48 | ||
49 | hasFiles?: boolean | 49 | hasFiles?: boolean |
50 | hasHLSFiles?: boolean | 50 | hasHLSFiles?: boolean |
51 | hasWebtorrentFiles?: boolean | 51 | |
52 | hasWebVideoFiles?: boolean | ||
53 | hasWebtorrentFiles?: boolean // TODO: Remove in v7 | ||
52 | 54 | ||
53 | accountId?: number | 55 | accountId?: number |
54 | videoChannelId?: number | 56 | videoChannelId?: number |
@@ -175,7 +177,9 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
175 | } | 177 | } |
176 | 178 | ||
177 | if (exists(options.hasWebtorrentFiles)) { | 179 | if (exists(options.hasWebtorrentFiles)) { |
178 | this.whereWebTorrentFileExists(options.hasWebtorrentFiles) | 180 | this.whereWebVideoFileExists(options.hasWebtorrentFiles) |
181 | } else if (exists(options.hasWebVideoFiles)) { | ||
182 | this.whereWebVideoFileExists(options.hasWebVideoFiles) | ||
179 | } | 183 | } |
180 | 184 | ||
181 | if (exists(options.hasHLSFiles)) { | 185 | if (exists(options.hasHLSFiles)) { |
@@ -400,18 +404,18 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
400 | } | 404 | } |
401 | 405 | ||
402 | private whereFileExists () { | 406 | private whereFileExists () { |
403 | this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) | 407 | this.and.push(`(${this.buildWebVideoFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`) |
404 | } | 408 | } |
405 | 409 | ||
406 | private whereWebTorrentFileExists (exists: boolean) { | 410 | private whereWebVideoFileExists (exists: boolean) { |
407 | this.and.push(this.buildWebTorrentFileExistsQuery(exists)) | 411 | this.and.push(this.buildWebVideoFileExistsQuery(exists)) |
408 | } | 412 | } |
409 | 413 | ||
410 | private whereHLSFileExists (exists: boolean) { | 414 | private whereHLSFileExists (exists: boolean) { |
411 | this.and.push(this.buildHLSFileExistsQuery(exists)) | 415 | this.and.push(this.buildHLSFileExistsQuery(exists)) |
412 | } | 416 | } |
413 | 417 | ||
414 | private buildWebTorrentFileExistsQuery (exists: boolean) { | 418 | private buildWebVideoFileExistsQuery (exists: boolean) { |
415 | const prefix = exists ? '' : 'NOT ' | 419 | const prefix = exists ? '' : 'NOT ' |
416 | 420 | ||
417 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' | 421 | return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' |
diff --git a/server/models/video/sql/video/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index 3fdac4ed3..b73dc28cd 100644 --- a/server/models/video/sql/video/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
@@ -18,7 +18,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
18 | private innerQuery: string | 18 | private innerQuery: string |
19 | private innerSort: string | 19 | private innerSort: string |
20 | 20 | ||
21 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | 21 | webVideoFilesQueryBuilder: VideoFileQueryBuilder |
22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | 22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder |
23 | 23 | ||
24 | private readonly videoModelBuilder: VideoModelBuilder | 24 | private readonly videoModelBuilder: VideoModelBuilder |
@@ -27,7 +27,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
27 | super(sequelize, 'list') | 27 | super(sequelize, 'list') |
28 | 28 | ||
29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | 29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) |
30 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 30 | this.webVideoFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | 31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) |
32 | } | 32 | } |
33 | 33 | ||
@@ -48,12 +48,12 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
48 | includeRedundancy: false | 48 | includeRedundancy: false |
49 | } | 49 | } |
50 | 50 | ||
51 | const [ rowsWebTorrentFiles, rowsStreamingPlaylist ] = await Promise.all([ | 51 | const [ rowsWebVideoFiles, rowsStreamingPlaylist ] = await Promise.all([ |
52 | this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions), | 52 | this.webVideoFilesQueryBuilder.queryWebVideos(fileQueryOptions), |
53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) | 53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) |
54 | ]) | 54 | ]) |
55 | 55 | ||
56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebTorrentFiles }) | 56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebVideoFiles }) |
57 | } | 57 | } |
58 | } | 58 | } |
59 | 59 | ||
diff --git a/server/models/video/storyboard.ts b/server/models/video/storyboard.ts new file mode 100644 index 000000000..65a044c98 --- /dev/null +++ b/server/models/video/storyboard.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { MStoryboard, MStoryboardVideo, MVideo } from '@server/types/models' | ||
6 | import { Storyboard } from '@shared/models' | ||
7 | import { AttributesOnly } from '@shared/typescript-utils' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | ||
10 | import { VideoModel } from './video' | ||
11 | import { Transaction } from 'sequelize' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'storyboard', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'videoId' ], | ||
18 | unique: true | ||
19 | }, | ||
20 | { | ||
21 | fields: [ 'filename' ], | ||
22 | unique: true | ||
23 | } | ||
24 | ] | ||
25 | }) | ||
26 | export class StoryboardModel extends Model<Partial<AttributesOnly<StoryboardModel>>> { | ||
27 | |||
28 | @AllowNull(false) | ||
29 | @Column | ||
30 | filename: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | totalHeight: number | ||
35 | |||
36 | @AllowNull(false) | ||
37 | @Column | ||
38 | totalWidth: number | ||
39 | |||
40 | @AllowNull(false) | ||
41 | @Column | ||
42 | spriteHeight: number | ||
43 | |||
44 | @AllowNull(false) | ||
45 | @Column | ||
46 | spriteWidth: number | ||
47 | |||
48 | @AllowNull(false) | ||
49 | @Column | ||
50 | spriteDuration: number | ||
51 | |||
52 | @AllowNull(true) | ||
53 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) | ||
54 | fileUrl: string | ||
55 | |||
56 | @ForeignKey(() => VideoModel) | ||
57 | @Column | ||
58 | videoId: number | ||
59 | |||
60 | @BelongsTo(() => VideoModel, { | ||
61 | foreignKey: { | ||
62 | allowNull: true | ||
63 | }, | ||
64 | onDelete: 'CASCADE' | ||
65 | }) | ||
66 | Video: VideoModel | ||
67 | |||
68 | @CreatedAt | ||
69 | createdAt: Date | ||
70 | |||
71 | @UpdatedAt | ||
72 | updatedAt: Date | ||
73 | |||
74 | @AfterDestroy | ||
75 | static removeInstanceFile (instance: StoryboardModel) { | ||
76 | logger.info('Removing storyboard file %s.', instance.filename) | ||
77 | |||
78 | // Don't block the transaction | ||
79 | instance.removeFile() | ||
80 | .catch(err => logger.error('Cannot remove storyboard file %s.', instance.filename, { err })) | ||
81 | } | ||
82 | |||
83 | static loadByVideo (videoId: number, transaction?: Transaction): Promise<MStoryboard> { | ||
84 | const query = { | ||
85 | where: { | ||
86 | videoId | ||
87 | }, | ||
88 | transaction | ||
89 | } | ||
90 | |||
91 | return StoryboardModel.findOne(query) | ||
92 | } | ||
93 | |||
94 | static loadByFilename (filename: string): Promise<MStoryboard> { | ||
95 | const query = { | ||
96 | where: { | ||
97 | filename | ||
98 | } | ||
99 | } | ||
100 | |||
101 | return StoryboardModel.findOne(query) | ||
102 | } | ||
103 | |||
104 | static loadWithVideoByFilename (filename: string): Promise<MStoryboardVideo> { | ||
105 | const query = { | ||
106 | where: { | ||
107 | filename | ||
108 | }, | ||
109 | include: [ | ||
110 | { | ||
111 | model: VideoModel.unscoped(), | ||
112 | required: true | ||
113 | } | ||
114 | ] | ||
115 | } | ||
116 | |||
117 | return StoryboardModel.findOne(query) | ||
118 | } | ||
119 | |||
120 | // --------------------------------------------------------------------------- | ||
121 | |||
122 | static async listStoryboardsOf (video: MVideo): Promise<MStoryboardVideo[]> { | ||
123 | const query = { | ||
124 | where: { | ||
125 | videoId: video.id | ||
126 | } | ||
127 | } | ||
128 | |||
129 | const storyboards = await StoryboardModel.findAll<MStoryboard>(query) | ||
130 | |||
131 | return storyboards.map(s => Object.assign(s, { Video: video })) | ||
132 | } | ||
133 | |||
134 | // --------------------------------------------------------------------------- | ||
135 | |||
136 | getOriginFileUrl (video: MVideo) { | ||
137 | if (video.isOwned()) { | ||
138 | return WEBSERVER.URL + this.getLocalStaticPath() | ||
139 | } | ||
140 | |||
141 | return this.fileUrl | ||
142 | } | ||
143 | |||
144 | getLocalStaticPath () { | ||
145 | return LAZY_STATIC_PATHS.STORYBOARDS + this.filename | ||
146 | } | ||
147 | |||
148 | getPath () { | ||
149 | return join(CONFIG.STORAGE.STORYBOARDS_DIR, this.filename) | ||
150 | } | ||
151 | |||
152 | removeFile () { | ||
153 | return remove(this.getPath()) | ||
154 | } | ||
155 | |||
156 | toFormattedJSON (this: MStoryboardVideo): Storyboard { | ||
157 | return { | ||
158 | storyboardPath: this.getLocalStaticPath(), | ||
159 | |||
160 | totalHeight: this.totalHeight, | ||
161 | totalWidth: this.totalWidth, | ||
162 | |||
163 | spriteWidth: this.spriteWidth, | ||
164 | spriteHeight: this.spriteHeight, | ||
165 | |||
166 | spriteDuration: this.spriteDuration | ||
167 | } | ||
168 | } | ||
169 | } | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index a4ac581e5..1722acdb4 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -21,7 +21,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | 24 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
25 | import { VideoModel } from './video' | 25 | import { VideoModel } from './video' |
26 | import { VideoPlaylistModel } from './video-playlist' | 26 | import { VideoPlaylistModel } from './video-playlist' |
27 | 27 | ||
@@ -69,6 +69,10 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
69 | @Column | 69 | @Column |
70 | automaticallyGenerated: boolean | 70 | automaticallyGenerated: boolean |
71 | 71 | ||
72 | @AllowNull(false) | ||
73 | @Column | ||
74 | onDisk: boolean | ||
75 | |||
72 | @ForeignKey(() => VideoModel) | 76 | @ForeignKey(() => VideoModel) |
73 | @Column | 77 | @Column |
74 | videoId: number | 78 | videoId: number |
@@ -106,7 +110,7 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
106 | [ThumbnailType.MINIATURE]: { | 110 | [ThumbnailType.MINIATURE]: { |
107 | label: 'miniature', | 111 | label: 'miniature', |
108 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | 112 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, |
109 | staticPath: STATIC_PATHS.THUMBNAILS | 113 | staticPath: LAZY_STATIC_PATHS.THUMBNAILS |
110 | }, | 114 | }, |
111 | [ThumbnailType.PREVIEW]: { | 115 | [ThumbnailType.PREVIEW]: { |
112 | label: 'preview', | 116 | label: 'preview', |
@@ -197,4 +201,8 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel> | |||
197 | 201 | ||
198 | this.previousThumbnailFilename = undefined | 202 | this.previousThumbnailFilename = undefined |
199 | } | 203 | } |
204 | |||
205 | isOwned () { | ||
206 | return !this.fileUrl | ||
207 | } | ||
200 | } | 208 | } |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 1fb1cae82..dd4cefd65 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | Table, | 15 | Table, |
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' | 18 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionLanguageUrl, MVideoCaptionVideo } from '@server/types/models' |
19 | import { buildUUID } from '@shared/extra-utils' | 19 | import { buildUUID } from '@shared/extra-utils' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
@@ -225,7 +225,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
225 | } | 225 | } |
226 | } | 226 | } |
227 | 227 | ||
228 | getCaptionStaticPath (this: MVideoCaption) { | 228 | getCaptionStaticPath (this: MVideoCaptionLanguageUrl) { |
229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) | 229 | return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename) |
230 | } | 230 | } |
231 | 231 | ||
@@ -233,9 +233,7 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption | |||
233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) | 233 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) |
234 | } | 234 | } |
235 | 235 | ||
236 | getFileUrl (video: MVideo) { | 236 | getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideo) { |
237 | if (!this.Video) this.Video = video as VideoModel | ||
238 | |||
239 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() | 237 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() |
240 | 238 | ||
241 | return this.fileUrl | 239 | return this.fileUrl |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 2db4b523a..26f072f4f 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -45,7 +45,7 @@ enum ScopeNames { | |||
45 | { | 45 | { |
46 | model: VideoModel.scope([ | 46 | model: VideoModel.scope([ |
47 | VideoScopeNames.WITH_THUMBNAILS, | 47 | VideoScopeNames.WITH_THUMBNAILS, |
48 | VideoScopeNames.WITH_WEBTORRENT_FILES, | 48 | VideoScopeNames.WITH_WEB_VIDEO_FILES, |
49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | 49 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, |
50 | VideoScopeNames.WITH_ACCOUNT_DETAILS | 50 | VideoScopeNames.WITH_ACCOUNT_DETAILS |
51 | ]), | 51 | ]), |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 07bc13de1..ee34ad2ff 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -26,8 +26,8 @@ import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | |||
26 | import { | 26 | import { |
27 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
28 | getHLSPublicFileUrl, | 28 | getHLSPublicFileUrl, |
29 | getWebTorrentPrivateFileUrl, | 29 | getWebVideoPrivateFileUrl, |
30 | getWebTorrentPublicFileUrl | 30 | getWebVideoPublicFileUrl |
31 | } from '@server/lib/object-storage' | 31 | } from '@server/lib/object-storage' |
32 | import { getFSTorrentFilePath } from '@server/lib/paths' | 32 | import { getFSTorrentFilePath } from '@server/lib/paths' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
@@ -276,15 +276,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
276 | 276 | ||
277 | static async doesOwnedTorrentFileExist (filename: string) { | 277 | static async doesOwnedTorrentFileExist (filename: string) { |
278 | const query = 'SELECT 1 FROM "videoFile" ' + | 278 | const query = 'SELECT 1 FROM "videoFile" ' + |
279 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | 279 | 'LEFT JOIN "video" "webvideo" ON "webvideo"."id" = "videoFile"."videoId" AND "webvideo"."remote" IS FALSE ' + |
280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | 280 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + |
281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webvideo"."id" IS NOT NULL) LIMIT 1' |
283 | 283 | ||
284 | return doesExist(this.sequelize, query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
285 | } | 285 | } |
286 | 286 | ||
287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebVideoFileExist (filename: string) { |
288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
290 | 290 | ||
@@ -378,7 +378,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
378 | } | 378 | } |
379 | 379 | ||
380 | static getStats () { | 380 | static getStats () { |
381 | const webtorrentFilesQuery: FindOptions = { | 381 | const webVideoFilesQuery: FindOptions = { |
382 | include: [ | 382 | include: [ |
383 | { | 383 | { |
384 | attributes: [], | 384 | attributes: [], |
@@ -412,10 +412,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
412 | } | 412 | } |
413 | 413 | ||
414 | return Promise.all([ | 414 | return Promise.all([ |
415 | VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery), | 415 | VideoFileModel.aggregate('size', 'SUM', webVideoFilesQuery), |
416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) | 416 | VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery) |
417 | ]).then(([ webtorrentResult, hlsResult ]) => ({ | 417 | ]).then(([ webVideoResult, hlsResult ]) => ({ |
418 | totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult) | 418 | totalLocalVideoFilesSize: parseAggregateResult(webVideoResult) + parseAggregateResult(hlsResult) |
419 | })) | 419 | })) |
420 | } | 420 | } |
421 | 421 | ||
@@ -433,7 +433,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
433 | 433 | ||
434 | const element = mode === 'streaming-playlist' | 434 | const element = mode === 'streaming-playlist' |
435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) | 435 | ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId }) |
436 | : await VideoFileModel.loadWebTorrentFile({ ...baseFind, videoId: videoFile.videoId }) | 436 | : await VideoFileModel.loadWebVideoFile({ ...baseFind, videoId: videoFile.videoId }) |
437 | 437 | ||
438 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
439 | 439 | ||
@@ -444,7 +444,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
444 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
445 | } | 445 | } |
446 | 446 | ||
447 | static async loadWebTorrentFile (options: { | 447 | static async loadWebVideoFile (options: { |
448 | videoId: number | 448 | videoId: number |
449 | fps: number | 449 | fps: number |
450 | resolution: number | 450 | resolution: number |
@@ -523,7 +523,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
523 | return getHLSPrivateFileUrl(video, this.filename) | 523 | return getHLSPrivateFileUrl(video, this.filename) |
524 | } | 524 | } |
525 | 525 | ||
526 | return getWebTorrentPrivateFileUrl(this.filename) | 526 | return getWebVideoPrivateFileUrl(this.filename) |
527 | } | 527 | } |
528 | 528 | ||
529 | private getPublicObjectStorageUrl () { | 529 | private getPublicObjectStorageUrl () { |
@@ -531,7 +531,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
531 | return getHLSPublicFileUrl(this.fileUrl) | 531 | return getHLSPublicFileUrl(this.fileUrl) |
532 | } | 532 | } |
533 | 533 | ||
534 | return getWebTorrentPublicFileUrl(this.fileUrl) | 534 | return getWebVideoPublicFileUrl(this.fileUrl) |
535 | } | 535 | } |
536 | 536 | ||
537 | // --------------------------------------------------------------------------- | 537 | // --------------------------------------------------------------------------- |
@@ -553,15 +553,15 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
553 | getFileStaticPath (video: MVideo) { | 553 | getFileStaticPath (video: MVideo) { |
554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) | 554 | if (this.isHLS()) return this.getHLSFileStaticPath(video) |
555 | 555 | ||
556 | return this.getWebTorrentFileStaticPath(video) | 556 | return this.getWebVideoFileStaticPath(video) |
557 | } | 557 | } |
558 | 558 | ||
559 | private getWebTorrentFileStaticPath (video: MVideo) { | 559 | private getWebVideoFileStaticPath (video: MVideo) { |
560 | if (isVideoInPrivateDirectory(video.privacy)) { | 560 | if (isVideoInPrivateDirectory(video.privacy)) { |
561 | return join(STATIC_PATHS.PRIVATE_WEBSEED, this.filename) | 561 | return join(STATIC_PATHS.PRIVATE_WEB_VIDEOS, this.filename) |
562 | } | 562 | } |
563 | 563 | ||
564 | return join(STATIC_PATHS.WEBSEED, this.filename) | 564 | return join(STATIC_PATHS.WEB_VIDEOS, this.filename) |
565 | } | 565 | } |
566 | 566 | ||
567 | private getHLSFileStaticPath (video: MVideo) { | 567 | private getHLSFileStaticPath (video: MVideo) { |
diff --git a/server/models/video/video-password.ts b/server/models/video/video-password.ts new file mode 100644 index 000000000..648366c3b --- /dev/null +++ b/server/models/video/video-password.ts | |||
@@ -0,0 +1,137 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from './video' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | import { ResultList, VideoPassword } from '@shared/models' | ||
5 | import { getSort, throwIfNotValid } from '../shared' | ||
6 | import { FindOptions, Transaction } from 'sequelize' | ||
7 | import { MVideoPassword } from '@server/types/models' | ||
8 | import { isPasswordValid } from '@server/helpers/custom-validators/videos' | ||
9 | import { pick } from '@shared/core-utils' | ||
10 | |||
11 | @DefaultScope(() => ({ | ||
12 | include: [ | ||
13 | { | ||
14 | model: VideoModel.unscoped(), | ||
15 | required: true | ||
16 | } | ||
17 | ] | ||
18 | })) | ||
19 | @Table({ | ||
20 | tableName: 'videoPassword', | ||
21 | indexes: [ | ||
22 | { | ||
23 | fields: [ 'videoId', 'password' ], | ||
24 | unique: true | ||
25 | } | ||
26 | ] | ||
27 | }) | ||
28 | export class VideoPasswordModel extends Model<Partial<AttributesOnly<VideoPasswordModel>>> { | ||
29 | |||
30 | @AllowNull(false) | ||
31 | @Is('VideoPassword', value => throwIfNotValid(value, isPasswordValid, 'videoPassword')) | ||
32 | @Column | ||
33 | password: string | ||
34 | |||
35 | @CreatedAt | ||
36 | createdAt: Date | ||
37 | |||
38 | @UpdatedAt | ||
39 | updatedAt: Date | ||
40 | |||
41 | @ForeignKey(() => VideoModel) | ||
42 | @Column | ||
43 | videoId: number | ||
44 | |||
45 | @BelongsTo(() => VideoModel, { | ||
46 | foreignKey: { | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'cascade' | ||
50 | }) | ||
51 | Video: VideoModel | ||
52 | |||
53 | static async countByVideoId (videoId: number, t?: Transaction) { | ||
54 | const query: FindOptions = { | ||
55 | where: { | ||
56 | videoId | ||
57 | }, | ||
58 | transaction: t | ||
59 | } | ||
60 | |||
61 | return VideoPasswordModel.count(query) | ||
62 | } | ||
63 | |||
64 | static async loadByIdAndVideo (options: { id: number, videoId: number, t?: Transaction }): Promise<MVideoPassword> { | ||
65 | const { id, videoId, t } = options | ||
66 | const query: FindOptions = { | ||
67 | where: { | ||
68 | id, | ||
69 | videoId | ||
70 | }, | ||
71 | transaction: t | ||
72 | } | ||
73 | |||
74 | return VideoPasswordModel.findOne(query) | ||
75 | } | ||
76 | |||
77 | static async listPasswords (options: { | ||
78 | start: number | ||
79 | count: number | ||
80 | sort: string | ||
81 | videoId: number | ||
82 | }): Promise<ResultList<MVideoPassword>> { | ||
83 | const { start, count, sort, videoId } = options | ||
84 | |||
85 | const { count: total, rows: data } = await VideoPasswordModel.findAndCountAll({ | ||
86 | where: { videoId }, | ||
87 | order: getSort(sort), | ||
88 | offset: start, | ||
89 | limit: count | ||
90 | }) | ||
91 | |||
92 | return { total, data } | ||
93 | } | ||
94 | |||
95 | static async addPasswords (passwords: string[], videoId: number, transaction?: Transaction): Promise<void> { | ||
96 | for (const password of passwords) { | ||
97 | await VideoPasswordModel.create({ | ||
98 | password, | ||
99 | videoId | ||
100 | }, { transaction }) | ||
101 | } | ||
102 | } | ||
103 | |||
104 | static async deleteAllPasswords (videoId: number, transaction?: Transaction) { | ||
105 | await VideoPasswordModel.destroy({ | ||
106 | where: { videoId }, | ||
107 | transaction | ||
108 | }) | ||
109 | } | ||
110 | |||
111 | static async deletePassword (passwordId: number, transaction?: Transaction) { | ||
112 | await VideoPasswordModel.destroy({ | ||
113 | where: { id: passwordId }, | ||
114 | transaction | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | static async isACorrectPassword (options: { | ||
119 | videoId: number | ||
120 | password: string | ||
121 | }) { | ||
122 | const query = { | ||
123 | where: pick(options, [ 'videoId', 'password' ]) | ||
124 | } | ||
125 | return VideoPasswordModel.findOne(query) | ||
126 | } | ||
127 | |||
128 | toFormattedJSON (): VideoPassword { | ||
129 | return { | ||
130 | id: this.id, | ||
131 | password: this.password, | ||
132 | videoId: this.videoId, | ||
133 | createdAt: this.createdAt, | ||
134 | updatedAt: this.updatedAt | ||
135 | } | ||
136 | } | ||
137 | } | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index b832f9768..61ae6b9fe 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -336,7 +336,10 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
336 | // Internal video? | 336 | // Internal video? |
337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR | 337 | if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR |
338 | 338 | ||
339 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE | 339 | // Private, internal and password protected videos cannot be read without appropriate access (ownership, internal) |
340 | if (new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy)) { | ||
341 | return VideoPlaylistElementType.PRIVATE | ||
342 | } | ||
340 | 343 | ||
341 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | 344 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE |
342 | 345 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index faf4bea78..15999d409 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -32,7 +32,7 @@ import { | |||
32 | import { | 32 | import { |
33 | ACTIVITY_PUB, | 33 | ACTIVITY_PUB, |
34 | CONSTRAINTS_FIELDS, | 34 | CONSTRAINTS_FIELDS, |
35 | STATIC_PATHS, | 35 | LAZY_STATIC_PATHS, |
36 | THUMBNAILS_SIZE, | 36 | THUMBNAILS_SIZE, |
37 | VIDEO_PLAYLIST_PRIVACIES, | 37 | VIDEO_PLAYLIST_PRIVACIES, |
38 | VIDEO_PLAYLIST_TYPES, | 38 | VIDEO_PLAYLIST_TYPES, |
@@ -592,13 +592,13 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
592 | getThumbnailUrl () { | 592 | getThumbnailUrl () { |
593 | if (!this.hasThumbnail()) return null | 593 | if (!this.hasThumbnail()) return null |
594 | 594 | ||
595 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename | 595 | return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename |
596 | } | 596 | } |
597 | 597 | ||
598 | getThumbnailStaticPath () { | 598 | getThumbnailStaticPath () { |
599 | if (!this.hasThumbnail()) return null | 599 | if (!this.hasThumbnail()) return null |
600 | 600 | ||
601 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) | 601 | return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
602 | } | 602 | } |
603 | 603 | ||
604 | getWatchStaticPath () { | 604 | getWatchStaticPath () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8e3af62a4..4c6297243 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -29,7 +29,7 @@ import { | |||
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' | 30 | import { InternalEventEmitter } from '@server/lib/internal-event-emitter' |
31 | import { LiveManager } from '@server/lib/live/live-manager' | 31 | import { LiveManager } from '@server/lib/live/live-manager' |
32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 32 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebVideoObjectStorage } from '@server/lib/object-storage' |
33 | import { tracer } from '@server/lib/opentelemetry/tracing' | 33 | import { tracer } from '@server/lib/opentelemetry/tracing' |
34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 34 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
35 | import { Hooks } from '@server/lib/plugins/hooks' | 35 | import { Hooks } from '@server/lib/plugins/hooks' |
@@ -58,7 +58,7 @@ import { | |||
58 | import { AttributesOnly } from '@shared/typescript-utils' | 58 | import { AttributesOnly } from '@shared/typescript-utils' |
59 | import { peertubeTruncate } from '../../helpers/core-utils' | 59 | import { peertubeTruncate } from '../../helpers/core-utils' |
60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 60 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
61 | import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' | 61 | import { exists, isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' |
62 | import { | 62 | import { |
63 | isVideoDescriptionValid, | 63 | isVideoDescriptionValid, |
64 | isVideoDurationValid, | 64 | isVideoDurationValid, |
@@ -75,6 +75,7 @@ import { | |||
75 | MChannel, | 75 | MChannel, |
76 | MChannelAccountDefault, | 76 | MChannelAccountDefault, |
77 | MChannelId, | 77 | MChannelId, |
78 | MStoryboard, | ||
78 | MStreamingPlaylist, | 79 | MStreamingPlaylist, |
79 | MStreamingPlaylistFilesVideo, | 80 | MStreamingPlaylistFilesVideo, |
80 | MUserAccountId, | 81 | MUserAccountId, |
@@ -83,6 +84,8 @@ import { | |||
83 | MVideoAccountLight, | 84 | MVideoAccountLight, |
84 | MVideoAccountLightBlacklistAllFiles, | 85 | MVideoAccountLightBlacklistAllFiles, |
85 | MVideoAP, | 86 | MVideoAP, |
87 | MVideoAPLight, | ||
88 | MVideoCaptionLanguageUrl, | ||
86 | MVideoDetails, | 89 | MVideoDetails, |
87 | MVideoFileVideo, | 90 | MVideoFileVideo, |
88 | MVideoFormattable, | 91 | MVideoFormattable, |
@@ -111,13 +114,13 @@ import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, | |||
111 | import { UserModel } from '../user/user' | 114 | import { UserModel } from '../user/user' |
112 | import { UserVideoHistoryModel } from '../user/user-video-history' | 115 | import { UserVideoHistoryModel } from '../user/user-video-history' |
113 | import { VideoViewModel } from '../view/video-view' | 116 | import { VideoViewModel } from '../view/video-view' |
117 | import { videoModelToActivityPubObject } from './formatter/video-activity-pub-format' | ||
114 | import { | 118 | import { |
115 | videoFilesModelToFormattedJSON, | 119 | videoFilesModelToFormattedJSON, |
116 | VideoFormattingJSONOptions, | 120 | VideoFormattingJSONOptions, |
117 | videoModelToActivityPubObject, | ||
118 | videoModelToFormattedDetailsJSON, | 121 | videoModelToFormattedDetailsJSON, |
119 | videoModelToFormattedJSON | 122 | videoModelToFormattedJSON |
120 | } from './formatter/video-format-utils' | 123 | } from './formatter/video-api-format' |
121 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 124 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
122 | import { | 125 | import { |
123 | BuildVideosListQueryOptions, | 126 | BuildVideosListQueryOptions, |
@@ -126,6 +129,7 @@ import { | |||
126 | VideosIdListQueryBuilder, | 129 | VideosIdListQueryBuilder, |
127 | VideosModelListQueryBuilder | 130 | VideosModelListQueryBuilder |
128 | } from './sql/video' | 131 | } from './sql/video' |
132 | import { StoryboardModel } from './storyboard' | ||
129 | import { TagModel } from './tag' | 133 | import { TagModel } from './tag' |
130 | import { ThumbnailModel } from './thumbnail' | 134 | import { ThumbnailModel } from './thumbnail' |
131 | import { VideoBlacklistModel } from './video-blacklist' | 135 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -136,6 +140,7 @@ import { VideoFileModel } from './video-file' | |||
136 | import { VideoImportModel } from './video-import' | 140 | import { VideoImportModel } from './video-import' |
137 | import { VideoJobInfoModel } from './video-job-info' | 141 | import { VideoJobInfoModel } from './video-job-info' |
138 | import { VideoLiveModel } from './video-live' | 142 | import { VideoLiveModel } from './video-live' |
143 | import { VideoPasswordModel } from './video-password' | ||
139 | import { VideoPlaylistElementModel } from './video-playlist-element' | 144 | import { VideoPlaylistElementModel } from './video-playlist-element' |
140 | import { VideoShareModel } from './video-share' | 145 | import { VideoShareModel } from './video-share' |
141 | import { VideoSourceModel } from './video-source' | 146 | import { VideoSourceModel } from './video-source' |
@@ -146,7 +151,7 @@ export enum ScopeNames { | |||
146 | FOR_API = 'FOR_API', | 151 | FOR_API = 'FOR_API', |
147 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 152 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
148 | WITH_TAGS = 'WITH_TAGS', | 153 | WITH_TAGS = 'WITH_TAGS', |
149 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', | 154 | WITH_WEB_VIDEO_FILES = 'WITH_WEB_VIDEO_FILES', |
150 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 155 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
151 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 156 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
152 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 157 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
@@ -285,7 +290,7 @@ export type ForAPIOptions = { | |||
285 | } | 290 | } |
286 | ] | 291 | ] |
287 | }, | 292 | }, |
288 | [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => { | 293 | [ScopeNames.WITH_WEB_VIDEO_FILES]: (withRedundancies = false) => { |
289 | let subInclude: any[] = [] | 294 | let subInclude: any[] = [] |
290 | 295 | ||
291 | if (withRedundancies === true) { | 296 | if (withRedundancies === true) { |
@@ -734,6 +739,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
734 | }) | 739 | }) |
735 | VideoCaptions: VideoCaptionModel[] | 740 | VideoCaptions: VideoCaptionModel[] |
736 | 741 | ||
742 | @HasMany(() => VideoPasswordModel, { | ||
743 | foreignKey: { | ||
744 | name: 'videoId', | ||
745 | allowNull: false | ||
746 | }, | ||
747 | onDelete: 'cascade' | ||
748 | }) | ||
749 | VideoPasswords: VideoPasswordModel[] | ||
750 | |||
737 | @HasOne(() => VideoJobInfoModel, { | 751 | @HasOne(() => VideoJobInfoModel, { |
738 | foreignKey: { | 752 | foreignKey: { |
739 | name: 'videoId', | 753 | name: 'videoId', |
@@ -743,6 +757,16 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
743 | }) | 757 | }) |
744 | VideoJobInfo: VideoJobInfoModel | 758 | VideoJobInfo: VideoJobInfoModel |
745 | 759 | ||
760 | @HasOne(() => StoryboardModel, { | ||
761 | foreignKey: { | ||
762 | name: 'videoId', | ||
763 | allowNull: false | ||
764 | }, | ||
765 | onDelete: 'cascade', | ||
766 | hooks: true | ||
767 | }) | ||
768 | Storyboard: StoryboardModel | ||
769 | |||
746 | @AfterCreate | 770 | @AfterCreate |
747 | static notifyCreate (video: MVideo) { | 771 | static notifyCreate (video: MVideo) { |
748 | InternalEventEmitter.Instance.emit('video-created', { video }) | 772 | InternalEventEmitter.Instance.emit('video-created', { video }) |
@@ -789,7 +813,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
789 | 813 | ||
790 | // Remove physical files and torrents | 814 | // Remove physical files and torrents |
791 | instance.VideoFiles.forEach(file => { | 815 | instance.VideoFiles.forEach(file => { |
792 | tasks.push(instance.removeWebTorrentFile(file)) | 816 | tasks.push(instance.removeWebVideoFile(file)) |
793 | }) | 817 | }) |
794 | 818 | ||
795 | // Remove playlists file | 819 | // Remove playlists file |
@@ -894,6 +918,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
894 | required: false | 918 | required: false |
895 | }, | 919 | }, |
896 | { | 920 | { |
921 | model: StoryboardModel.unscoped(), | ||
922 | required: false | ||
923 | }, | ||
924 | { | ||
897 | attributes: [ 'id', 'url' ], | 925 | attributes: [ 'id', 'url' ], |
898 | model: VideoShareModel.unscoped(), | 926 | model: VideoShareModel.unscoped(), |
899 | required: false, | 927 | required: false, |
@@ -1079,7 +1107,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1079 | include?: VideoInclude | 1107 | include?: VideoInclude |
1080 | 1108 | ||
1081 | hasFiles?: boolean // default false | 1109 | hasFiles?: boolean // default false |
1082 | hasWebtorrentFiles?: boolean | 1110 | |
1111 | hasWebtorrentFiles?: boolean // TODO: remove in v7 | ||
1112 | hasWebVideoFiles?: boolean | ||
1113 | |||
1083 | hasHLSFiles?: boolean | 1114 | hasHLSFiles?: boolean |
1084 | 1115 | ||
1085 | categoryOneOf?: number[] | 1116 | categoryOneOf?: number[] |
@@ -1144,6 +1175,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1144 | 'historyOfUser', | 1175 | 'historyOfUser', |
1145 | 'hasHLSFiles', | 1176 | 'hasHLSFiles', |
1146 | 'hasWebtorrentFiles', | 1177 | 'hasWebtorrentFiles', |
1178 | 'hasWebVideoFiles', | ||
1147 | 'search', | 1179 | 'search', |
1148 | 'excludeAlreadyWatched' | 1180 | 'excludeAlreadyWatched' |
1149 | ]), | 1181 | ]), |
@@ -1177,7 +1209,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1177 | 1209 | ||
1178 | user?: MUserAccountId | 1210 | user?: MUserAccountId |
1179 | 1211 | ||
1180 | hasWebtorrentFiles?: boolean | 1212 | hasWebtorrentFiles?: boolean // TODO: remove in v7 |
1213 | hasWebVideoFiles?: boolean | ||
1214 | |||
1181 | hasHLSFiles?: boolean | 1215 | hasHLSFiles?: boolean |
1182 | 1216 | ||
1183 | search?: string | 1217 | search?: string |
@@ -1224,6 +1258,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1224 | 'durationMax', | 1258 | 'durationMax', |
1225 | 'hasHLSFiles', | 1259 | 'hasHLSFiles', |
1226 | 'hasWebtorrentFiles', | 1260 | 'hasWebtorrentFiles', |
1261 | 'hasWebVideoFiles', | ||
1227 | 'uuids', | 1262 | 'uuids', |
1228 | 'search', | 1263 | 'search', |
1229 | 'displayOnlyForFollower', | 1264 | 'displayOnlyForFollower', |
@@ -1648,7 +1683,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1648 | return this.getQualityFileBy(minBy) | 1683 | return this.getQualityFileBy(minBy) |
1649 | } | 1684 | } |
1650 | 1685 | ||
1651 | getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { | 1686 | getWebVideoFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { |
1652 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1687 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1653 | 1688 | ||
1654 | const file = this.VideoFiles.find(f => f.resolution === resolution) | 1689 | const file = this.VideoFiles.find(f => f.resolution === resolution) |
@@ -1657,7 +1692,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1657 | return Object.assign(file, { Video: this }) | 1692 | return Object.assign(file, { Video: this }) |
1658 | } | 1693 | } |
1659 | 1694 | ||
1660 | hasWebTorrentFiles () { | 1695 | hasWebVideoFiles () { |
1661 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 | 1696 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 |
1662 | } | 1697 | } |
1663 | 1698 | ||
@@ -1758,6 +1793,32 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1758 | ) | 1793 | ) |
1759 | } | 1794 | } |
1760 | 1795 | ||
1796 | async lightAPToFullAP (this: MVideoAPLight, transaction: Transaction): Promise<MVideoAP> { | ||
1797 | const videoAP = this as MVideoAP | ||
1798 | |||
1799 | const getCaptions = () => { | ||
1800 | if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions | ||
1801 | |||
1802 | return this.$get('VideoCaptions', { | ||
1803 | attributes: [ 'filename', 'language', 'fileUrl' ], | ||
1804 | transaction | ||
1805 | }) as Promise<MVideoCaptionLanguageUrl[]> | ||
1806 | } | ||
1807 | |||
1808 | const getStoryboard = () => { | ||
1809 | if (videoAP.Storyboard) return videoAP.Storyboard | ||
1810 | |||
1811 | return this.$get('Storyboard', { transaction }) as Promise<MStoryboard> | ||
1812 | } | ||
1813 | |||
1814 | const [ captions, storyboard ] = await Promise.all([ getCaptions(), getStoryboard() ]) | ||
1815 | |||
1816 | return Object.assign(this, { | ||
1817 | VideoCaptions: captions, | ||
1818 | Storyboard: storyboard | ||
1819 | }) | ||
1820 | } | ||
1821 | |||
1761 | getTruncatedDescription () { | 1822 | getTruncatedDescription () { |
1762 | if (!this.description) return null | 1823 | if (!this.description) return null |
1763 | 1824 | ||
@@ -1830,7 +1891,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1830 | .concat(toAdd) | 1891 | .concat(toAdd) |
1831 | } | 1892 | } |
1832 | 1893 | ||
1833 | removeWebTorrentFile (videoFile: MVideoFile, isRedundancy = false) { | 1894 | removeWebVideoFile (videoFile: MVideoFile, isRedundancy = false) { |
1834 | const filePath = isRedundancy | 1895 | const filePath = isRedundancy |
1835 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) | 1896 | ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) |
1836 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) | 1897 | : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) |
@@ -1839,7 +1900,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1839 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | 1900 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) |
1840 | 1901 | ||
1841 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { | 1902 | if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { |
1842 | promises.push(removeWebTorrentObjectStorage(videoFile)) | 1903 | promises.push(removeWebVideoObjectStorage(videoFile)) |
1843 | } | 1904 | } |
1844 | 1905 | ||
1845 | return Promise.all(promises) | 1906 | return Promise.all(promises) |
@@ -1918,7 +1979,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1918 | 1979 | ||
1919 | // --------------------------------------------------------------------------- | 1980 | // --------------------------------------------------------------------------- |
1920 | 1981 | ||
1921 | requiresAuth (options: { | 1982 | requiresUserAuth (options: { |
1922 | urlParamId: string | 1983 | urlParamId: string |
1923 | checkBlacklist: boolean | 1984 | checkBlacklist: boolean |
1924 | }) { | 1985 | }) { |
@@ -1936,11 +1997,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1936 | 1997 | ||
1937 | if (checkBlacklist && this.VideoBlacklist) return true | 1998 | if (checkBlacklist && this.VideoBlacklist) return true |
1938 | 1999 | ||
1939 | if (this.privacy !== VideoPrivacy.PUBLIC) { | 2000 | if (this.privacy === VideoPrivacy.PUBLIC || this.privacy === VideoPrivacy.PASSWORD_PROTECTED) { |
1940 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) | 2001 | return false |
1941 | } | 2002 | } |
1942 | 2003 | ||
1943 | return false | 2004 | throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) |
1944 | } | 2005 | } |
1945 | 2006 | ||
1946 | hasPrivateStaticPath () { | 2007 | hasPrivateStaticPath () { |
@@ -1962,7 +2023,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1962 | } | 2023 | } |
1963 | 2024 | ||
1964 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { | 2025 | getBandwidthBits (this: MVideo, videoFile: MVideoFile) { |
1965 | if (!this.duration) throw new Error(`Cannot get bandwidth bits because video ${this.url} has duration of 0`) | 2026 | if (!this.duration) return videoFile.size |
1966 | 2027 | ||
1967 | return Math.ceil((videoFile.size * 8) / this.duration) | 2028 | return Math.ceil((videoFile.size * 8) / this.duration) |
1968 | } | 2029 | } |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 472cad182..80b616ccf 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -74,6 +74,9 @@ describe('Test config API validators', function () { | |||
74 | }, | 74 | }, |
75 | torrents: { | 75 | torrents: { |
76 | size: 4 | 76 | size: 4 |
77 | }, | ||
78 | storyboards: { | ||
79 | size: 5 | ||
77 | } | 80 | } |
78 | }, | 81 | }, |
79 | signup: { | 82 | signup: { |
@@ -123,7 +126,7 @@ describe('Test config API validators', function () { | |||
123 | '2160p': false | 126 | '2160p': false |
124 | }, | 127 | }, |
125 | alwaysTranscodeOriginalResolution: false, | 128 | alwaysTranscodeOriginalResolution: false, |
126 | webtorrent: { | 129 | webVideos: { |
127 | enabled: true | 130 | enabled: true |
128 | }, | 131 | }, |
129 | hls: { | 132 | hls: { |
@@ -342,7 +345,7 @@ describe('Test config API validators', function () { | |||
342 | }) | 345 | }) |
343 | }) | 346 | }) |
344 | 347 | ||
345 | it('Should fail with a disabled webtorrent & hls transcoding', async function () { | 348 | it('Should fail with a disabled web videos & hls transcoding', async function () { |
346 | const newUpdateParams = { | 349 | const newUpdateParams = { |
347 | ...updateParams, | 350 | ...updateParams, |
348 | 351 | ||
@@ -350,7 +353,7 @@ describe('Test config API validators', function () { | |||
350 | hls: { | 353 | hls: { |
351 | enabled: false | 354 | enabled: false |
352 | }, | 355 | }, |
353 | webtorrent: { | 356 | web_videos: { |
354 | enabled: false | 357 | enabled: false |
355 | } | 358 | } |
356 | } | 359 | } |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 400d312d3..c2a7ccd78 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -34,6 +34,7 @@ import './video-comments' | |||
34 | import './video-files' | 34 | import './video-files' |
35 | import './video-imports' | 35 | import './video-imports' |
36 | import './video-playlists' | 36 | import './video-playlists' |
37 | import './video-storyboards' | ||
37 | import './video-source' | 38 | import './video-source' |
38 | import './video-studio' | 39 | import './video-studio' |
39 | import './video-token' | 40 | import './video-token' |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 2dc735c23..5021db516 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -143,7 +143,7 @@ describe('Test video lives API validator', function () { | |||
143 | }) | 143 | }) |
144 | 144 | ||
145 | it('Should fail with a bad privacy for replay settings', async function () { | 145 | it('Should fail with a bad privacy for replay settings', async function () { |
146 | const fields = { ...baseCorrectParams, replaySettings: { privacy: 5 } } | 146 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } } |
147 | 147 | ||
148 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 148 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
149 | }) | 149 | }) |
@@ -194,7 +194,7 @@ describe('Test video lives API validator', function () { | |||
194 | it('Should fail with a big thumbnail file', async function () { | 194 | it('Should fail with a big thumbnail file', async function () { |
195 | const fields = baseCorrectParams | 195 | const fields = baseCorrectParams |
196 | const attaches = { | 196 | const attaches = { |
197 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') | 197 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') |
198 | } | 198 | } |
199 | 199 | ||
200 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 200 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -212,7 +212,7 @@ describe('Test video lives API validator', function () { | |||
212 | it('Should fail with a big preview file', async function () { | 212 | it('Should fail with a big preview file', async function () { |
213 | const fields = baseCorrectParams | 213 | const fields = baseCorrectParams |
214 | const attaches = { | 214 | const attaches = { |
215 | previewfile: buildAbsoluteFixturePath('preview-big.png') | 215 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') |
216 | } | 216 | } |
217 | 217 | ||
218 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 218 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -472,7 +472,7 @@ describe('Test video lives API validator', function () { | |||
472 | }) | 472 | }) |
473 | 473 | ||
474 | it('Should fail with a bad privacy for replay settings', async function () { | 474 | it('Should fail with a bad privacy for replay settings', async function () { |
475 | const fields = { saveReplay: true, replaySettings: { privacy: 5 } } | 475 | const fields = { saveReplay: true, replaySettings: { privacy: 999 } } |
476 | 476 | ||
477 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 477 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
478 | }) | 478 | }) |
diff --git a/server/tests/api/check-params/runners.ts b/server/tests/api/check-params/runners.ts index 48821b678..4ba90802f 100644 --- a/server/tests/api/check-params/runners.ts +++ b/server/tests/api/check-params/runners.ts | |||
@@ -752,7 +752,7 @@ describe('Test managing runners', function () { | |||
752 | }) | 752 | }) |
753 | 753 | ||
754 | it('Should fail with an invalid vod audio merge payload', async function () { | 754 | it('Should fail with an invalid vod audio merge payload', async function () { |
755 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 755 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
756 | await server.videos.upload({ attributes, mode: 'legacy' }) | 756 | await server.videos.upload({ attributes, mode: 'legacy' }) |
757 | 757 | ||
758 | await waitJobs([ server ]) | 758 | await waitJobs([ server ]) |
diff --git a/server/tests/api/check-params/transcoding.ts b/server/tests/api/check-params/transcoding.ts index 9846ac182..4bebcb528 100644 --- a/server/tests/api/check-params/transcoding.ts +++ b/server/tests/api/check-params/transcoding.ts | |||
@@ -49,21 +49,21 @@ describe('Test transcoding API validators', function () { | |||
49 | 49 | ||
50 | it('Should not run transcoding of a unknown video', async function () { | 50 | it('Should not run transcoding of a unknown video', async function () { |
51 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 51 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
52 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'webtorrent', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 52 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
53 | }) | 53 | }) |
54 | 54 | ||
55 | it('Should not run transcoding of a remote video', async function () { | 55 | it('Should not run transcoding of a remote video', async function () { |
56 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 56 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
57 | 57 | ||
58 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) | 58 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) |
59 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'webtorrent', expectedStatus }) | 59 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus }) |
60 | }) | 60 | }) |
61 | 61 | ||
62 | it('Should not run transcoding by a non admin user', async function () { | 62 | it('Should not run transcoding by a non admin user', async function () { |
63 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | 63 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 |
64 | 64 | ||
65 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) | 65 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) |
66 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', token: moderatorToken, expectedStatus }) | 66 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus }) |
67 | }) | 67 | }) |
68 | 68 | ||
69 | it('Should not run transcoding without transcoding type', async function () { | 69 | it('Should not run transcoding without transcoding type', async function () { |
@@ -82,7 +82,7 @@ describe('Test transcoding API validators', function () { | |||
82 | await servers[0].config.disableTranscoding() | 82 | await servers[0].config.disableTranscoding() |
83 | 83 | ||
84 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) | 84 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) |
85 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | 85 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) |
86 | }) | 86 | }) |
87 | 87 | ||
88 | it('Should run transcoding', async function () { | 88 | it('Should run transcoding', async function () { |
@@ -93,15 +93,15 @@ describe('Test transcoding API validators', function () { | |||
93 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) | 93 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) |
94 | await waitJobs(servers) | 94 | await waitJobs(servers) |
95 | 95 | ||
96 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | 96 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) |
97 | await waitJobs(servers) | 97 | await waitJobs(servers) |
98 | }) | 98 | }) |
99 | 99 | ||
100 | it('Should not run transcoding on a video that is already being transcoded', async function () { | 100 | it('Should not run transcoding on a video that is already being transcoded', async function () { |
101 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | 101 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) |
102 | 102 | ||
103 | const expectedStatus = HttpStatusCode.CONFLICT_409 | 103 | const expectedStatus = HttpStatusCode.CONFLICT_409 |
104 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | 104 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) |
105 | }) | 105 | }) |
106 | 106 | ||
107 | after(async function () { | 107 | after(async function () { |
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts index 9dc59a1b5..4d43ab6f8 100644 --- a/server/tests/api/check-params/video-files.ts +++ b/server/tests/api/check-params/video-files.ts | |||
@@ -60,7 +60,7 @@ describe('Test videos files', function () { | |||
60 | }) | 60 | }) |
61 | 61 | ||
62 | describe('Deleting files', function () { | 62 | describe('Deleting files', function () { |
63 | let webtorrentId: string | 63 | let webVideoId: string |
64 | let hlsId: string | 64 | let hlsId: string |
65 | let remoteId: string | 65 | let remoteId: string |
66 | 66 | ||
@@ -68,10 +68,10 @@ describe('Test videos files', function () { | |||
68 | let validId2: string | 68 | let validId2: string |
69 | 69 | ||
70 | let hlsFileId: number | 70 | let hlsFileId: number |
71 | let webtorrentFileId: number | 71 | let webVideoFileId: number |
72 | 72 | ||
73 | let remoteHLSFileId: number | 73 | let remoteHLSFileId: number |
74 | let remoteWebtorrentFileId: number | 74 | let remoteWebVideoFileId: number |
75 | 75 | ||
76 | before(async function () { | 76 | before(async function () { |
77 | this.timeout(300_000) | 77 | this.timeout(300_000) |
@@ -83,7 +83,7 @@ describe('Test videos files', function () { | |||
83 | const video = await servers[1].videos.get({ id: uuid }) | 83 | const video = await servers[1].videos.get({ id: uuid }) |
84 | remoteId = video.uuid | 84 | remoteId = video.uuid |
85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id | 85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id |
86 | remoteWebtorrentFileId = video.files[0].id | 86 | remoteWebVideoFileId = video.files[0].id |
87 | } | 87 | } |
88 | 88 | ||
89 | { | 89 | { |
@@ -96,7 +96,7 @@ describe('Test videos files', function () { | |||
96 | const video = await servers[0].videos.get({ id: uuid }) | 96 | const video = await servers[0].videos.get({ id: uuid }) |
97 | validId1 = video.uuid | 97 | validId1 = video.uuid |
98 | hlsFileId = video.streamingPlaylists[0].files[0].id | 98 | hlsFileId = video.streamingPlaylists[0].files[0].id |
99 | webtorrentFileId = video.files[0].id | 99 | webVideoFileId = video.files[0].id |
100 | } | 100 | } |
101 | 101 | ||
102 | { | 102 | { |
@@ -117,8 +117,8 @@ describe('Test videos files', function () { | |||
117 | 117 | ||
118 | { | 118 | { |
119 | await servers[0].config.enableTranscoding(false, true) | 119 | await servers[0].config.enableTranscoding(false, true) |
120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | 120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) |
121 | webtorrentId = uuid | 121 | webVideoId = uuid |
122 | } | 122 | } |
123 | 123 | ||
124 | await waitJobs(servers) | 124 | await waitJobs(servers) |
@@ -128,27 +128,27 @@ describe('Test videos files', function () { | |||
128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
129 | 129 | ||
130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) | 130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) |
131 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: 404, expectedStatus }) | 131 | await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus }) |
132 | 132 | ||
133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) | 133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) |
134 | await servers[0].videos.removeWebTorrentFile({ videoId: 404, fileId: webtorrentFileId, expectedStatus }) | 134 | await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus }) |
135 | }) | 135 | }) |
136 | 136 | ||
137 | it('Should not delete unknown files', async function () { | 137 | it('Should not delete unknown files', async function () { |
138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | 138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 |
139 | 139 | ||
140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webtorrentFileId, expectedStatus }) | 140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus }) |
141 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) | 141 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) |
142 | }) | 142 | }) |
143 | 143 | ||
144 | it('Should not delete files of a remote video', async function () { | 144 | it('Should not delete files of a remote video', async function () { |
145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
146 | 146 | ||
147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) | 147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) |
148 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: remoteId, expectedStatus }) | 148 | await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus }) |
149 | 149 | ||
150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) | 150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) |
151 | await servers[0].videos.removeWebTorrentFile({ videoId: remoteId, fileId: remoteWebtorrentFileId, expectedStatus }) | 151 | await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus }) |
152 | }) | 152 | }) |
153 | 153 | ||
154 | it('Should not delete files by a non admin user', async function () { | 154 | it('Should not delete files by a non admin user', async function () { |
@@ -157,35 +157,35 @@ describe('Test videos files', function () { | |||
157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) | 157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) |
158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) | 158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) |
159 | 159 | ||
160 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus }) | 160 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus }) |
161 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | 161 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) |
162 | 162 | ||
163 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) | 163 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) |
164 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) | 164 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) |
165 | 165 | ||
166 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: userToken, expectedStatus }) | 166 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus }) |
167 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId, token: moderatorToken, expectedStatus }) | 167 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus }) |
168 | }) | 168 | }) |
169 | 169 | ||
170 | it('Should not delete files if the files are not available', async function () { | 170 | it('Should not delete files if the files are not available', async function () { |
171 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 171 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
172 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 172 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
173 | 173 | ||
174 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 174 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
175 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 175 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
176 | }) | 176 | }) |
177 | 177 | ||
178 | it('Should not delete files if no both versions are available', async function () { | 178 | it('Should not delete files if no both versions are available', async function () { |
179 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 179 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
180 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 180 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
181 | }) | 181 | }) |
182 | 182 | ||
183 | it('Should delete files if both versions are available', async function () { | 183 | it('Should delete files if both versions are available', async function () { |
184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) | 184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) |
185 | await servers[0].videos.removeWebTorrentFile({ videoId: validId1, fileId: webtorrentFileId }) | 185 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId }) |
186 | 186 | ||
187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) | 187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) |
188 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId2 }) | 188 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 }) |
189 | }) | 189 | }) |
190 | }) | 190 | }) |
191 | 191 | ||
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 7f19b9ee9..8c6f43c12 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -244,7 +244,7 @@ describe('Test video imports API validator', function () { | |||
244 | it('Should fail with a big thumbnail file', async function () { | 244 | it('Should fail with a big thumbnail file', async function () { |
245 | const fields = baseCorrectParams | 245 | const fields = baseCorrectParams |
246 | const attaches = { | 246 | const attaches = { |
247 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') | 247 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') |
248 | } | 248 | } |
249 | 249 | ||
250 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 250 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -262,7 +262,7 @@ describe('Test video imports API validator', function () { | |||
262 | it('Should fail with a big preview file', async function () { | 262 | it('Should fail with a big preview file', async function () { |
263 | const fields = baseCorrectParams | 263 | const fields = baseCorrectParams |
264 | const attaches = { | 264 | const attaches = { |
265 | previewfile: buildAbsoluteFixturePath('preview-big.png') | 265 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') |
266 | } | 266 | } |
267 | 267 | ||
268 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 268 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
diff --git a/server/tests/api/check-params/video-passwords.ts b/server/tests/api/check-params/video-passwords.ts new file mode 100644 index 000000000..4e936b5d2 --- /dev/null +++ b/server/tests/api/check-params/video-passwords.ts | |||
@@ -0,0 +1,609 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { | ||
3 | FIXTURE_URLS, | ||
4 | checkBadCountPagination, | ||
5 | checkBadSortPagination, | ||
6 | checkBadStartPagination, | ||
7 | checkUploadVideoParam | ||
8 | } from '@server/tests/shared' | ||
9 | import { root } from '@shared/core-utils' | ||
10 | import { | ||
11 | HttpStatusCode, | ||
12 | PeerTubeProblemDocument, | ||
13 | ServerErrorCode, | ||
14 | VideoCreateResult, | ||
15 | VideoPrivacy | ||
16 | } from '@shared/models' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | createSingleServer, | ||
20 | makePostBodyRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers | ||
23 | } from '@shared/server-commands' | ||
24 | import { expect } from 'chai' | ||
25 | import { join } from 'path' | ||
26 | |||
27 | describe('Test video passwords validator', function () { | ||
28 | let path: string | ||
29 | let server: PeerTubeServer | ||
30 | let userAccessToken = '' | ||
31 | let video: VideoCreateResult | ||
32 | let channelId: number | ||
33 | let publicVideo: VideoCreateResult | ||
34 | let commentId: number | ||
35 | // --------------------------------------------------------------- | ||
36 | |||
37 | before(async function () { | ||
38 | this.timeout(50000) | ||
39 | |||
40 | server = await createSingleServer(1) | ||
41 | |||
42 | await setAccessTokensToServers([ server ]) | ||
43 | |||
44 | await server.config.updateCustomSubConfig({ | ||
45 | newConfig: { | ||
46 | live: { | ||
47 | enabled: true, | ||
48 | latencySetting: { | ||
49 | enabled: false | ||
50 | }, | ||
51 | allowReplay: false | ||
52 | }, | ||
53 | import: { | ||
54 | videos: { | ||
55 | http:{ | ||
56 | enabled: true | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | }) | ||
62 | |||
63 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
64 | |||
65 | { | ||
66 | const body = await server.users.getMyInfo() | ||
67 | channelId = body.videoChannels[0].id | ||
68 | } | ||
69 | |||
70 | { | ||
71 | video = await server.videos.quickUpload({ | ||
72 | name: 'password protected video', | ||
73 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
74 | videoPasswords: [ 'password1', 'password2' ] | ||
75 | }) | ||
76 | } | ||
77 | path = '/api/v1/videos/' | ||
78 | }) | ||
79 | |||
80 | async function checkVideoPasswordOptions (options: { | ||
81 | server: PeerTubeServer | ||
82 | token: string | ||
83 | videoPasswords: string[] | ||
84 | expectedStatus: HttpStatusCode | ||
85 | mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live' | ||
86 | }) { | ||
87 | const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options | ||
88 | const attaches = { | ||
89 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') | ||
90 | } | ||
91 | const baseCorrectParams = { | ||
92 | name: 'my super name', | ||
93 | category: 5, | ||
94 | licence: 1, | ||
95 | language: 'pt', | ||
96 | nsfw: false, | ||
97 | commentsEnabled: true, | ||
98 | downloadEnabled: true, | ||
99 | waitTranscoding: true, | ||
100 | description: 'my super description', | ||
101 | support: 'my super support text', | ||
102 | tags: [ 'tag1', 'tag2' ], | ||
103 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
104 | channelId, | ||
105 | originallyPublishedAt: new Date().toISOString() | ||
106 | } | ||
107 | if (mode === 'uploadLegacy') { | ||
108 | const fields = { ...baseCorrectParams, videoPasswords } | ||
109 | return checkUploadVideoParam(server, token, { ...fields, ...attaches }, expectedStatus, 'legacy') | ||
110 | } | ||
111 | |||
112 | if (mode === 'uploadResumable') { | ||
113 | const fields = { ...baseCorrectParams, videoPasswords } | ||
114 | return checkUploadVideoParam(server, token, { ...fields, ...attaches }, expectedStatus, 'resumable') | ||
115 | } | ||
116 | |||
117 | if (mode === 'import') { | ||
118 | const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords } | ||
119 | return server.imports.importVideo({ attributes, expectedStatus }) | ||
120 | } | ||
121 | |||
122 | if (mode === 'updateVideo') { | ||
123 | const attributes = { ...baseCorrectParams, videoPasswords } | ||
124 | return server.videos.update({ token, expectedStatus, id: video.id, attributes }) | ||
125 | } | ||
126 | |||
127 | if (mode === 'updatePasswords') { | ||
128 | return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords }) | ||
129 | } | ||
130 | |||
131 | if (mode === 'live') { | ||
132 | const fields = { ...baseCorrectParams, videoPasswords } | ||
133 | |||
134 | return server.live.create({ fields, expectedStatus }) | ||
135 | } | ||
136 | } | ||
137 | |||
138 | function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') { | ||
139 | |||
140 | it('Should fail with a password protected privacy without providing a password', async function () { | ||
141 | await checkVideoPasswordOptions({ | ||
142 | server, | ||
143 | token: server.accessToken, | ||
144 | videoPasswords: undefined, | ||
145 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
146 | mode | ||
147 | }) | ||
148 | }) | ||
149 | |||
150 | it('Should fail with a password protected privacy and an empty password list', async function () { | ||
151 | const videoPasswords = [] | ||
152 | |||
153 | await checkVideoPasswordOptions({ | ||
154 | server, | ||
155 | token: server.accessToken, | ||
156 | videoPasswords, | ||
157 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
158 | mode | ||
159 | }) | ||
160 | }) | ||
161 | |||
162 | it('Should fail with a password protected privacy and a too short password', async function () { | ||
163 | const videoPasswords = [ 'p' ] | ||
164 | |||
165 | await checkVideoPasswordOptions({ | ||
166 | server, | ||
167 | token: server.accessToken, | ||
168 | videoPasswords, | ||
169 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
170 | mode | ||
171 | }) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with a password protected privacy and a too long password', async function () { | ||
175 | const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ] | ||
176 | |||
177 | await checkVideoPasswordOptions({ | ||
178 | server, | ||
179 | token: server.accessToken, | ||
180 | videoPasswords, | ||
181 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
182 | mode | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with a password protected privacy and an empty password', async function () { | ||
187 | const videoPasswords = [ '' ] | ||
188 | |||
189 | await checkVideoPasswordOptions({ | ||
190 | server, | ||
191 | token: server.accessToken, | ||
192 | videoPasswords, | ||
193 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
194 | mode | ||
195 | }) | ||
196 | }) | ||
197 | |||
198 | it('Should fail with a password protected privacy and duplicated passwords', async function () { | ||
199 | const videoPasswords = [ 'password', 'password' ] | ||
200 | |||
201 | await checkVideoPasswordOptions({ | ||
202 | server, | ||
203 | token: server.accessToken, | ||
204 | videoPasswords, | ||
205 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
206 | mode | ||
207 | }) | ||
208 | }) | ||
209 | |||
210 | if (mode === 'updatePasswords') { | ||
211 | it('Should fail for an unauthenticated user', async function () { | ||
212 | const videoPasswords = [ 'password' ] | ||
213 | await checkVideoPasswordOptions({ | ||
214 | server, | ||
215 | token: null, | ||
216 | videoPasswords, | ||
217 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
218 | mode | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | it('Should fail for an unauthorized user', async function () { | ||
223 | const videoPasswords = [ 'password' ] | ||
224 | await checkVideoPasswordOptions({ | ||
225 | server, | ||
226 | token: userAccessToken, | ||
227 | videoPasswords, | ||
228 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
229 | mode | ||
230 | }) | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | it('Should succeed with a password protected privacy and correct passwords', async function () { | ||
235 | const videoPasswords = [ 'password1', 'password2' ] | ||
236 | const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo' | ||
237 | ? HttpStatusCode.NO_CONTENT_204 | ||
238 | : HttpStatusCode.OK_200 | ||
239 | |||
240 | await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode }) | ||
241 | }) | ||
242 | } | ||
243 | |||
244 | describe('When adding or updating a video', function () { | ||
245 | describe('Resumable upload', function () { | ||
246 | validateVideoPasswordList('uploadResumable') | ||
247 | }) | ||
248 | |||
249 | describe('Legacy upload', function () { | ||
250 | validateVideoPasswordList('uploadLegacy') | ||
251 | }) | ||
252 | |||
253 | describe('When importing a video', function () { | ||
254 | validateVideoPasswordList('import') | ||
255 | }) | ||
256 | |||
257 | describe('When updating a video', function () { | ||
258 | validateVideoPasswordList('updateVideo') | ||
259 | }) | ||
260 | |||
261 | describe('When updating the password list of a video', function () { | ||
262 | validateVideoPasswordList('updatePasswords') | ||
263 | }) | ||
264 | |||
265 | describe('When creating a live', function () { | ||
266 | validateVideoPasswordList('live') | ||
267 | }) | ||
268 | }) | ||
269 | |||
270 | async function checkVideoAccessOptions (options: { | ||
271 | server: PeerTubeServer | ||
272 | token?: string | ||
273 | videoPassword?: string | ||
274 | expectedStatus: HttpStatusCode | ||
275 | mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token' | ||
276 | }) { | ||
277 | const { server, token = null, videoPassword, expectedStatus, mode } = options | ||
278 | |||
279 | if (mode === 'get') { | ||
280 | return server.videos.get({ id: video.id, expectedStatus }) | ||
281 | } | ||
282 | |||
283 | if (mode === 'getWithToken') { | ||
284 | return server.videos.getWithToken({ | ||
285 | id: video.id, | ||
286 | token, | ||
287 | expectedStatus | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | if (mode === 'getWithPassword') { | ||
292 | return server.videos.getWithPassword({ | ||
293 | id: video.id, | ||
294 | token, | ||
295 | expectedStatus, | ||
296 | password: videoPassword | ||
297 | }) | ||
298 | } | ||
299 | |||
300 | if (mode === 'rate') { | ||
301 | return server.videos.rate({ | ||
302 | id: video.id, | ||
303 | token, | ||
304 | expectedStatus, | ||
305 | rating: 'like', | ||
306 | videoPassword | ||
307 | }) | ||
308 | } | ||
309 | |||
310 | if (mode === 'createThread') { | ||
311 | const fields = { text: 'super comment' } | ||
312 | const headers = videoPassword !== undefined && videoPassword !== null | ||
313 | ? { 'x-peertube-video-password': videoPassword } | ||
314 | : undefined | ||
315 | const body = await makePostBodyRequest({ | ||
316 | url: server.url, | ||
317 | path: path + video.uuid + '/comment-threads', | ||
318 | token, | ||
319 | fields, | ||
320 | headers, | ||
321 | expectedStatus | ||
322 | }) | ||
323 | return JSON.parse(body.text) | ||
324 | } | ||
325 | |||
326 | if (mode === 'replyThread') { | ||
327 | const fields = { text: 'super reply' } | ||
328 | const headers = videoPassword !== undefined && videoPassword !== null | ||
329 | ? { 'x-peertube-video-password': videoPassword } | ||
330 | : undefined | ||
331 | return makePostBodyRequest({ | ||
332 | url: server.url, | ||
333 | path: path + video.uuid + '/comments/' + commentId, | ||
334 | token, | ||
335 | fields, | ||
336 | headers, | ||
337 | expectedStatus | ||
338 | }) | ||
339 | } | ||
340 | if (mode === 'listThreads') { | ||
341 | return server.comments.listThreads({ | ||
342 | videoId: video.id, | ||
343 | token, | ||
344 | expectedStatus, | ||
345 | videoPassword | ||
346 | }) | ||
347 | } | ||
348 | |||
349 | if (mode === 'listCaptions') { | ||
350 | return server.captions.list({ | ||
351 | videoId: video.id, | ||
352 | token, | ||
353 | expectedStatus, | ||
354 | videoPassword | ||
355 | }) | ||
356 | } | ||
357 | |||
358 | if (mode === 'token') { | ||
359 | return server.videoToken.create({ | ||
360 | videoId: video.id, | ||
361 | token, | ||
362 | expectedStatus, | ||
363 | videoPassword | ||
364 | }) | ||
365 | } | ||
366 | } | ||
367 | |||
368 | function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') { | ||
369 | const serverCode = mode === 'providePassword' | ||
370 | ? ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
371 | : ServerErrorCode.INCORRECT_VIDEO_PASSWORD | ||
372 | |||
373 | const message = mode === 'providePassword' | ||
374 | ? 'Please provide a password to access this password protected video' | ||
375 | : 'Incorrect video password. Access to the video is denied.' | ||
376 | |||
377 | if (!error.code) { | ||
378 | error = JSON.parse(error.text) | ||
379 | } | ||
380 | |||
381 | expect(error.code).to.equal(serverCode) | ||
382 | expect(error.detail).to.equal(message) | ||
383 | expect(error.error).to.equal(message) | ||
384 | |||
385 | expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
386 | } | ||
387 | |||
388 | function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') { | ||
389 | const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode) | ||
390 | let tokens: string[] | ||
391 | if (!requiresUserAuth) { | ||
392 | it('Should fail without providing a password for an unlogged user', async function () { | ||
393 | const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode }) | ||
394 | const error = body as unknown as PeerTubeProblemDocument | ||
395 | |||
396 | checkVideoError(error, 'providePassword') | ||
397 | }) | ||
398 | } | ||
399 | |||
400 | it('Should fail without providing a password for an unauthorised user', async function () { | ||
401 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
402 | |||
403 | const body = await checkVideoAccessOptions({ | ||
404 | server, | ||
405 | token: userAccessToken, | ||
406 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
407 | mode: tmp | ||
408 | }) | ||
409 | |||
410 | const error = body as unknown as PeerTubeProblemDocument | ||
411 | |||
412 | checkVideoError(error, 'providePassword') | ||
413 | }) | ||
414 | |||
415 | it('Should fail if a wrong password is entered', async function () { | ||
416 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
417 | tokens = [ userAccessToken, server.accessToken ] | ||
418 | |||
419 | if (!requiresUserAuth) tokens.push(null) | ||
420 | |||
421 | for (const token of tokens) { | ||
422 | const body = await checkVideoAccessOptions({ | ||
423 | server, | ||
424 | token, | ||
425 | videoPassword: 'toto', | ||
426 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
427 | mode: tmp | ||
428 | }) | ||
429 | const error = body as unknown as PeerTubeProblemDocument | ||
430 | |||
431 | checkVideoError(error, 'incorrectPassword') | ||
432 | } | ||
433 | }) | ||
434 | |||
435 | it('Should fail if an empty password is entered', async function () { | ||
436 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
437 | |||
438 | for (const token of tokens) { | ||
439 | const body = await checkVideoAccessOptions({ | ||
440 | server, | ||
441 | token, | ||
442 | videoPassword: '', | ||
443 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
444 | mode: tmp | ||
445 | }) | ||
446 | const error = body as unknown as PeerTubeProblemDocument | ||
447 | |||
448 | checkVideoError(error, 'incorrectPassword') | ||
449 | } | ||
450 | }) | ||
451 | |||
452 | it('Should fail if an inccorect password containing the correct password is entered', async function () { | ||
453 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
454 | |||
455 | for (const token of tokens) { | ||
456 | const body = await checkVideoAccessOptions({ | ||
457 | server, | ||
458 | token, | ||
459 | videoPassword: 'password11', | ||
460 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
461 | mode: tmp | ||
462 | }) | ||
463 | const error = body as unknown as PeerTubeProblemDocument | ||
464 | |||
465 | checkVideoError(error, 'incorrectPassword') | ||
466 | } | ||
467 | }) | ||
468 | |||
469 | it('Should succeed without providing a password for an authorised user', async function () { | ||
470 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
471 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
472 | |||
473 | const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp }) | ||
474 | |||
475 | if (mode === 'createThread') commentId = body.comment.id | ||
476 | }) | ||
477 | |||
478 | it('Should succeed using correct passwords', async function () { | ||
479 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
480 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
481 | |||
482 | for (const token of tokens) { | ||
483 | await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp }) | ||
484 | await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp }) | ||
485 | } | ||
486 | }) | ||
487 | } | ||
488 | |||
489 | describe('When accessing password protected video', function () { | ||
490 | |||
491 | describe('For getting a password protected video', function () { | ||
492 | validateVideoAccess('get') | ||
493 | }) | ||
494 | |||
495 | describe('For rating a video', function () { | ||
496 | validateVideoAccess('rate') | ||
497 | }) | ||
498 | |||
499 | describe('For creating a thread', function () { | ||
500 | validateVideoAccess('createThread') | ||
501 | }) | ||
502 | |||
503 | describe('For replying to a thread', function () { | ||
504 | validateVideoAccess('replyThread') | ||
505 | }) | ||
506 | |||
507 | describe('For listing threads', function () { | ||
508 | validateVideoAccess('listThreads') | ||
509 | }) | ||
510 | |||
511 | describe('For getting captions', function () { | ||
512 | validateVideoAccess('listCaptions') | ||
513 | }) | ||
514 | |||
515 | describe('For creating video file token', function () { | ||
516 | validateVideoAccess('token') | ||
517 | }) | ||
518 | }) | ||
519 | |||
520 | describe('When listing passwords', function () { | ||
521 | it('Should fail with a bad start pagination', async function () { | ||
522 | await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
523 | }) | ||
524 | |||
525 | it('Should fail with a bad count pagination', async function () { | ||
526 | await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
527 | }) | ||
528 | |||
529 | it('Should fail with an incorrect sort', async function () { | ||
530 | await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
531 | }) | ||
532 | |||
533 | it('Should fail for unauthenticated user', async function () { | ||
534 | await server.videoPasswords.list({ | ||
535 | token: null, | ||
536 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
537 | videoId: video.id | ||
538 | }) | ||
539 | }) | ||
540 | |||
541 | it('Should fail for unauthorized user', async function () { | ||
542 | await server.videoPasswords.list({ | ||
543 | token: userAccessToken, | ||
544 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
545 | videoId: video.id | ||
546 | }) | ||
547 | }) | ||
548 | |||
549 | it('Should succeed with the correct parameters', async function () { | ||
550 | await server.videoPasswords.list({ | ||
551 | token: server.accessToken, | ||
552 | expectedStatus: HttpStatusCode.OK_200, | ||
553 | videoId: video.id | ||
554 | }) | ||
555 | }) | ||
556 | }) | ||
557 | |||
558 | describe('When deleting a password', async function () { | ||
559 | const passwords = (await server.videoPasswords.list({ videoId: video.id })).data | ||
560 | |||
561 | it('Should fail with wrong password id', async function () { | ||
562 | await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
563 | }) | ||
564 | |||
565 | it('Should fail for unauthenticated user', async function () { | ||
566 | await server.videoPasswords.remove({ | ||
567 | id: passwords[0].id, | ||
568 | token: null, | ||
569 | videoId: video.id, | ||
570 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
571 | }) | ||
572 | }) | ||
573 | |||
574 | it('Should fail for unauthorized user', async function () { | ||
575 | await server.videoPasswords.remove({ | ||
576 | id: passwords[0].id, | ||
577 | token: userAccessToken, | ||
578 | videoId: video.id, | ||
579 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
580 | }) | ||
581 | }) | ||
582 | |||
583 | it('Should fail for non password protected video', async function () { | ||
584 | publicVideo = await server.videos.quickUpload({ name: 'public video' }) | ||
585 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
586 | }) | ||
587 | |||
588 | it('Should fail for password not linked to correct video', async function () { | ||
589 | const video2 = await server.videos.quickUpload({ | ||
590 | name: 'password protected video', | ||
591 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
592 | videoPasswords: [ 'password1', 'password2' ] | ||
593 | }) | ||
594 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
595 | }) | ||
596 | |||
597 | it('Should succeed with correct parameter', async function () { | ||
598 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
599 | }) | ||
600 | |||
601 | it('Should fail for last password of a video', async function () { | ||
602 | await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
603 | }) | ||
604 | }) | ||
605 | |||
606 | after(async function () { | ||
607 | await cleanupTests([ server ]) | ||
608 | }) | ||
609 | }) | ||
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index 8090897c1..8c3233e0b 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/server/tests/api/check-params/video-playlists.ts | |||
@@ -196,7 +196,7 @@ describe('Test video playlists API validator', function () { | |||
196 | attributes: { | 196 | attributes: { |
197 | displayName: 'display name', | 197 | displayName: 'display name', |
198 | privacy: VideoPlaylistPrivacy.UNLISTED, | 198 | privacy: VideoPlaylistPrivacy.UNLISTED, |
199 | thumbnailfile: 'thumbnail.jpg', | 199 | thumbnailfile: 'custom-thumbnail.jpg', |
200 | videoChannelId: server.store.channel.id, | 200 | videoChannelId: server.store.channel.id, |
201 | 201 | ||
202 | ...attributes | 202 | ...attributes |
@@ -260,7 +260,7 @@ describe('Test video playlists API validator', function () { | |||
260 | }) | 260 | }) |
261 | 261 | ||
262 | it('Should fail with a thumbnail file too big', async function () { | 262 | it('Should fail with a thumbnail file too big', async function () { |
263 | const params = getBase({ thumbnailfile: 'preview-big.png' }) | 263 | const params = getBase({ thumbnailfile: 'custom-preview-big.png' }) |
264 | 264 | ||
265 | await command.create(params) | 265 | await command.create(params) |
266 | await command.update(getUpdate(params, playlist.shortUUID)) | 266 | await command.update(getUpdate(params, playlist.shortUUID)) |
diff --git a/server/tests/api/check-params/video-storyboards.ts b/server/tests/api/check-params/video-storyboards.ts new file mode 100644 index 000000000..c038e7370 --- /dev/null +++ b/server/tests/api/check-params/video-storyboards.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
5 | |||
6 | describe('Test video storyboards API validator', function () { | ||
7 | let server: PeerTubeServer | ||
8 | |||
9 | let publicVideo: { uuid: string } | ||
10 | let privateVideo: { uuid: string } | ||
11 | |||
12 | // --------------------------------------------------------------- | ||
13 | |||
14 | before(async function () { | ||
15 | this.timeout(120000) | ||
16 | |||
17 | server = await createSingleServer(1) | ||
18 | await setAccessTokensToServers([ server ]) | ||
19 | |||
20 | publicVideo = await server.videos.quickUpload({ name: 'public' }) | ||
21 | privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) | ||
22 | }) | ||
23 | |||
24 | it('Should fail without a valid uuid', async function () { | ||
25 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
26 | }) | ||
27 | |||
28 | it('Should receive 404 when passing a non existing video id', async function () { | ||
29 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
30 | }) | ||
31 | |||
32 | it('Should not get the private storyboard without the appropriate token', async function () { | ||
33 | await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) | ||
34 | await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null }) | ||
35 | }) | ||
36 | |||
37 | it('Should succeed with the correct parameters', async function () { | ||
38 | await server.storyboard.list({ id: privateVideo.uuid }) | ||
39 | await server.storyboard.list({ id: publicVideo.uuid }) | ||
40 | }) | ||
41 | |||
42 | after(async function () { | ||
43 | await cleanupTests([ server ]) | ||
44 | }) | ||
45 | }) | ||
diff --git a/server/tests/api/check-params/video-studio.ts b/server/tests/api/check-params/video-studio.ts index add8d9164..4ac0d93ed 100644 --- a/server/tests/api/check-params/video-studio.ts +++ b/server/tests/api/check-params/video-studio.ts | |||
@@ -293,7 +293,7 @@ describe('Test video studio API validator', function () { | |||
293 | it('Should succeed with the correct params', async function () { | 293 | it('Should succeed with the correct params', async function () { |
294 | this.timeout(120000) | 294 | this.timeout(120000) |
295 | 295 | ||
296 | await addWatermark('thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) | 296 | await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) |
297 | 297 | ||
298 | await waitJobs([ server ]) | 298 | await waitJobs([ server ]) |
299 | }) | 299 | }) |
@@ -322,8 +322,8 @@ describe('Test video studio API validator', function () { | |||
322 | }) | 322 | }) |
323 | 323 | ||
324 | it('Should fail with an invalid file', async function () { | 324 | it('Should fail with an invalid file', async function () { |
325 | await addIntroOutro('add-intro', 'thumbnail.jpg') | 325 | await addIntroOutro('add-intro', 'custom-thumbnail.jpg') |
326 | await addIntroOutro('add-outro', 'thumbnail.jpg') | 326 | await addIntroOutro('add-outro', 'custom-thumbnail.jpg') |
327 | }) | 327 | }) |
328 | 328 | ||
329 | it('Should fail with a file that does not contain video stream', async function () { | 329 | it('Should fail with a file that does not contain video stream', async function () { |
diff --git a/server/tests/api/check-params/video-token.ts b/server/tests/api/check-params/video-token.ts index 7acb9d580..7cb3e84a2 100644 --- a/server/tests/api/check-params/video-token.ts +++ b/server/tests/api/check-params/video-token.ts | |||
@@ -5,9 +5,12 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ | |||
5 | 5 | ||
6 | describe('Test video tokens', function () { | 6 | describe('Test video tokens', function () { |
7 | let server: PeerTubeServer | 7 | let server: PeerTubeServer |
8 | let videoId: string | 8 | let privateVideoId: string |
9 | let passwordProtectedVideoId: string | ||
9 | let userToken: string | 10 | let userToken: string |
10 | 11 | ||
12 | const videoPassword = 'password' | ||
13 | |||
11 | // --------------------------------------------------------------- | 14 | // --------------------------------------------------------------- |
12 | 15 | ||
13 | before(async function () { | 16 | before(async function () { |
@@ -15,27 +18,50 @@ describe('Test video tokens', function () { | |||
15 | 18 | ||
16 | server = await createSingleServer(1) | 19 | server = await createSingleServer(1) |
17 | await setAccessTokensToServers([ server ]) | 20 | await setAccessTokensToServers([ server ]) |
18 | 21 | { | |
19 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 22 | const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) |
20 | videoId = uuid | 23 | privateVideoId = uuid |
21 | 24 | } | |
25 | { | ||
26 | const { uuid } = await server.videos.quickUpload({ | ||
27 | name: 'password protected video', | ||
28 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
29 | videoPasswords: [ videoPassword ] | ||
30 | }) | ||
31 | passwordProtectedVideoId = uuid | ||
32 | } | ||
22 | userToken = await server.users.generateUserAndToken('user1') | 33 | userToken = await server.users.generateUserAndToken('user1') |
23 | }) | 34 | }) |
24 | 35 | ||
25 | it('Should not generate tokens for unauthenticated user', async function () { | 36 | it('Should not generate tokens on private video for unauthenticated user', async function () { |
26 | await server.videoToken.create({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | 37 | await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) |
27 | }) | 38 | }) |
28 | 39 | ||
29 | it('Should not generate tokens of unknown video', async function () { | 40 | it('Should not generate tokens of unknown video', async function () { |
30 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 41 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
31 | }) | 42 | }) |
32 | 43 | ||
44 | it('Should not generate tokens with incorrect password', async function () { | ||
45 | await server.videoToken.create({ | ||
46 | videoId: passwordProtectedVideoId, | ||
47 | token: null, | ||
48 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
49 | videoPassword: 'incorrectPassword' | ||
50 | }) | ||
51 | }) | ||
52 | |||
33 | it('Should not generate tokens of a non owned video', async function () { | 53 | it('Should not generate tokens of a non owned video', async function () { |
34 | await server.videoToken.create({ videoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 54 | await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
35 | }) | 55 | }) |
36 | 56 | ||
37 | it('Should generate token', async function () { | 57 | it('Should generate token', async function () { |
38 | await server.videoToken.create({ videoId }) | 58 | await server.videoToken.create({ videoId: privateVideoId }) |
59 | }) | ||
60 | |||
61 | it('Should generate token on password protected video', async function () { | ||
62 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null }) | ||
63 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken }) | ||
64 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword }) | ||
39 | }) | 65 | }) |
40 | 66 | ||
41 | after(async function () { | 67 | after(async function () { |
diff --git a/server/tests/api/check-params/videos-overviews.ts b/server/tests/api/check-params/videos-overviews.ts index f9cdb7ab3..ae7de24dd 100644 --- a/server/tests/api/check-params/videos-overviews.ts +++ b/server/tests/api/check-params/videos-overviews.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/server-commands' | 3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@shared/server-commands' |
4 | 4 | ||
5 | describe('Test videos overview', function () { | 5 | describe('Test videos overview API validator', function () { |
6 | let server: PeerTubeServer | 6 | let server: PeerTubeServer |
7 | 7 | ||
8 | // --------------------------------------------------------------- | 8 | // --------------------------------------------------------------- |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 094ab6891..6ee1955a7 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -384,7 +384,7 @@ describe('Test videos API validator', function () { | |||
384 | it('Should fail with a big thumbnail file', async function () { | 384 | it('Should fail with a big thumbnail file', async function () { |
385 | const fields = baseCorrectParams | 385 | const fields = baseCorrectParams |
386 | const attaches = { | 386 | const attaches = { |
387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png'), |
388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
389 | } | 389 | } |
390 | 390 | ||
@@ -404,7 +404,7 @@ describe('Test videos API validator', function () { | |||
404 | it('Should fail with a big preview file', async function () { | 404 | it('Should fail with a big preview file', async function () { |
405 | const fields = baseCorrectParams | 405 | const fields = baseCorrectParams |
406 | const attaches = { | 406 | const attaches = { |
407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png'), |
408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
409 | } | 409 | } |
410 | 410 | ||
@@ -615,7 +615,7 @@ describe('Test videos API validator', function () { | |||
615 | it('Should fail with a big thumbnail file', async function () { | 615 | it('Should fail with a big thumbnail file', async function () { |
616 | const fields = baseCorrectParams | 616 | const fields = baseCorrectParams |
617 | const attaches = { | 617 | const attaches = { |
618 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png') | 618 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png') |
619 | } | 619 | } |
620 | 620 | ||
621 | await makeUploadRequest({ | 621 | await makeUploadRequest({ |
@@ -647,7 +647,7 @@ describe('Test videos API validator', function () { | |||
647 | it('Should fail with a big preview file', async function () { | 647 | it('Should fail with a big preview file', async function () { |
648 | const fields = baseCorrectParams | 648 | const fields = baseCorrectParams |
649 | const attaches = { | 649 | const attaches = { |
650 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png') | 650 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'custom-preview-big.png') |
651 | } | 651 | } |
652 | 652 | ||
653 | await makeUploadRequest({ | 653 | await makeUploadRequest({ |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 7ab67b126..2b302a8a2 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { basename, join } from 'path' | 4 | import { basename, join } from 'path' |
5 | import { SQLCommand, testImage, testLiveVideoResolutions } from '@server/tests/shared' | 5 | import { SQLCommand, testImageGeneratedByFFmpeg, testLiveVideoResolutions } from '@server/tests/shared' |
6 | import { getAllFiles, wait } from '@shared/core-utils' | 6 | import { getAllFiles, wait } from '@shared/core-utils' |
7 | import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' | 7 | import { ffprobePromise, getVideoStream } from '@shared/ffmpeg' |
8 | import { | 8 | import { |
@@ -121,8 +121,8 @@ describe('Test live', function () { | |||
121 | expect(video.downloadEnabled).to.be.false | 121 | expect(video.downloadEnabled).to.be.false |
122 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | 122 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
123 | 123 | ||
124 | await testImage(server.url, 'video_short1-preview.webm', video.previewPath) | 124 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) |
125 | await testImage(server.url, 'video_short1.webm', video.thumbnailPath) | 125 | await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath) |
126 | 126 | ||
127 | const live = await server.live.get({ videoId: liveVideoUUID }) | 127 | const live = await server.live.get({ videoId: liveVideoUUID }) |
128 | 128 | ||
diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index af9d681b2..64ab542a5 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts | |||
@@ -39,7 +39,7 @@ describe('Object storage for video static file privacy', function () { | |||
39 | const video = await server.videos.getWithToken({ id: uuid }) | 39 | const video = await server.videos.getWithToken({ id: uuid }) |
40 | 40 | ||
41 | for (const file of video.files) { | 41 | for (const file of video.files) { |
42 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/webseed/private/') | 42 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/web-videos/private/') |
43 | 43 | ||
44 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 44 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
45 | } | 45 | } |
@@ -107,15 +107,20 @@ describe('Object storage for video static file privacy', function () { | |||
107 | describe('VOD', function () { | 107 | describe('VOD', function () { |
108 | let privateVideoUUID: string | 108 | let privateVideoUUID: string |
109 | let publicVideoUUID: string | 109 | let publicVideoUUID: string |
110 | let passwordProtectedVideoUUID: string | ||
110 | let userPrivateVideoUUID: string | 111 | let userPrivateVideoUUID: string |
111 | 112 | ||
113 | const correctPassword = 'my super password' | ||
114 | const correctPasswordHeader = { 'x-peertube-video-password': correctPassword } | ||
115 | const incorrectPasswordHeader = { 'x-peertube-video-password': correctPassword + 'toto' } | ||
116 | |||
112 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
113 | 118 | ||
114 | async function getSampleFileUrls (videoId: string) { | 119 | async function getSampleFileUrls (videoId: string) { |
115 | const video = await server.videos.getWithToken({ id: videoId }) | 120 | const video = await server.videos.getWithToken({ id: videoId }) |
116 | 121 | ||
117 | return { | 122 | return { |
118 | webTorrentFile: video.files[0].fileUrl, | 123 | webVideoFile: video.files[0].fileUrl, |
119 | hlsFile: getHLS(video).files[0].fileUrl | 124 | hlsFile: getHLS(video).files[0].fileUrl |
120 | } | 125 | } |
121 | } | 126 | } |
@@ -140,6 +145,22 @@ describe('Object storage for video static file privacy', function () { | |||
140 | await checkPrivateVODFiles(privateVideoUUID) | 145 | await checkPrivateVODFiles(privateVideoUUID) |
141 | }) | 146 | }) |
142 | 147 | ||
148 | it('Should upload a password protected video and have appropriate object storage ACL', async function () { | ||
149 | this.timeout(120000) | ||
150 | |||
151 | { | ||
152 | const { uuid } = await server.videos.quickUpload({ | ||
153 | name: 'video', | ||
154 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
155 | videoPasswords: [ correctPassword ] | ||
156 | }) | ||
157 | passwordProtectedVideoUUID = uuid | ||
158 | } | ||
159 | await waitJobs([ server ]) | ||
160 | |||
161 | await checkPrivateVODFiles(passwordProtectedVideoUUID) | ||
162 | }) | ||
163 | |||
143 | it('Should upload a public video and have appropriate object storage ACL', async function () { | 164 | it('Should upload a public video and have appropriate object storage ACL', async function () { |
144 | this.timeout(120000) | 165 | this.timeout(120000) |
145 | 166 | ||
@@ -154,13 +175,49 @@ describe('Object storage for video static file privacy', function () { | |||
154 | it('Should not get files without appropriate OAuth token', async function () { | 175 | it('Should not get files without appropriate OAuth token', async function () { |
155 | this.timeout(60000) | 176 | this.timeout(60000) |
156 | 177 | ||
157 | const { webTorrentFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | 178 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) |
179 | |||
180 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
181 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
182 | |||
183 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
184 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
185 | }) | ||
186 | |||
187 | it('Should not get files without appropriate password or appropriate OAuth token', async function () { | ||
188 | this.timeout(60000) | ||
189 | |||
190 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
158 | 191 | ||
159 | await makeRawRequest({ url: webTorrentFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 192 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
160 | await makeRawRequest({ url: webTorrentFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 193 | await makeRawRequest({ |
194 | url: webVideoFile, | ||
195 | token: null, | ||
196 | headers: incorrectPasswordHeader, | ||
197 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
198 | }) | ||
199 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
200 | await makeRawRequest({ | ||
201 | url: webVideoFile, | ||
202 | token: null, | ||
203 | headers: correctPasswordHeader, | ||
204 | expectedStatus: HttpStatusCode.OK_200 | ||
205 | }) | ||
161 | 206 | ||
162 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 207 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
208 | await makeRawRequest({ | ||
209 | url: hlsFile, | ||
210 | token: null, | ||
211 | headers: incorrectPasswordHeader, | ||
212 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
213 | }) | ||
163 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 214 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
215 | await makeRawRequest({ | ||
216 | url: hlsFile, | ||
217 | token: null, | ||
218 | headers: correctPasswordHeader, | ||
219 | expectedStatus: HttpStatusCode.OK_200 | ||
220 | }) | ||
164 | }) | 221 | }) |
165 | 222 | ||
166 | it('Should not get HLS file of another video', async function () { | 223 | it('Should not get HLS file of another video', async function () { |
@@ -176,21 +233,50 @@ describe('Object storage for video static file privacy', function () { | |||
176 | await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 233 | await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
177 | }) | 234 | }) |
178 | 235 | ||
179 | it('Should correctly check OAuth or video file token', async function () { | 236 | it('Should correctly check OAuth, video file token of private video', async function () { |
180 | this.timeout(60000) | 237 | this.timeout(60000) |
181 | 238 | ||
182 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) | 239 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) |
183 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) | 240 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) |
184 | 241 | ||
185 | const { webTorrentFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | 242 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) |
243 | |||
244 | for (const url of [ webVideoFile, hlsFile ]) { | ||
245 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
246 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
247 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
248 | |||
249 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
250 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
251 | |||
252 | } | ||
253 | }) | ||
254 | |||
255 | it('Should correctly check OAuth, video file token or video password of password protected video', async function () { | ||
256 | this.timeout(60000) | ||
186 | 257 | ||
187 | for (const url of [ webTorrentFile, hlsFile ]) { | 258 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) |
259 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ | ||
260 | videoId: passwordProtectedVideoUUID, | ||
261 | videoPassword: correctPassword | ||
262 | }) | ||
263 | |||
264 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
265 | |||
266 | for (const url of [ hlsFile, webVideoFile ]) { | ||
188 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 267 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
189 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 268 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
190 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 269 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
191 | 270 | ||
192 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 271 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
193 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | 272 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) |
273 | |||
274 | await makeRawRequest({ | ||
275 | url, | ||
276 | headers: incorrectPasswordHeader, | ||
277 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
278 | }) | ||
279 | await makeRawRequest({ url, headers: correctPasswordHeader, expectedStatus: HttpStatusCode.OK_200 }) | ||
194 | } | 280 | } |
195 | }) | 281 | }) |
196 | 282 | ||
@@ -232,16 +318,26 @@ describe('Object storage for video static file privacy', function () { | |||
232 | let permanentLiveId: string | 318 | let permanentLiveId: string |
233 | let permanentLive: LiveVideo | 319 | let permanentLive: LiveVideo |
234 | 320 | ||
321 | let passwordProtectedLiveId: string | ||
322 | let passwordProtectedLive: LiveVideo | ||
323 | |||
324 | const correctPassword = 'my super password' | ||
325 | |||
235 | let unrelatedFileToken: string | 326 | let unrelatedFileToken: string |
236 | 327 | ||
237 | // --------------------------------------------------------------------------- | 328 | // --------------------------------------------------------------------------- |
238 | 329 | ||
239 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | 330 | async function checkLiveFiles (live: LiveVideo, liveId: string, videoPassword?: string) { |
240 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | 331 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
241 | await server.live.waitUntilPublished({ videoId: liveId }) | 332 | await server.live.waitUntilPublished({ videoId: liveId }) |
242 | 333 | ||
243 | const video = await server.videos.getWithToken({ id: liveId }) | 334 | const video = videoPassword |
244 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | 335 | ? await server.videos.getWithPassword({ id: liveId, password: videoPassword }) |
336 | : await server.videos.getWithToken({ id: liveId }) | ||
337 | |||
338 | const fileToken = videoPassword | ||
339 | ? await server.videoToken.getVideoFileToken({ token: null, videoId: video.uuid, videoPassword }) | ||
340 | : await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
245 | 341 | ||
246 | const hls = video.streamingPlaylists[0] | 342 | const hls = video.streamingPlaylists[0] |
247 | 343 | ||
@@ -253,10 +349,19 @@ describe('Object storage for video static file privacy', function () { | |||
253 | 349 | ||
254 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | 350 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
255 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | 351 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) |
256 | 352 | if (videoPassword) { | |
353 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
354 | } | ||
257 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 355 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
258 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 356 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
259 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 357 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
358 | if (videoPassword) { | ||
359 | await makeRawRequest({ | ||
360 | url, | ||
361 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
362 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
363 | }) | ||
364 | } | ||
260 | } | 365 | } |
261 | 366 | ||
262 | await stopFfmpeg(ffmpegCommand) | 367 | await stopFfmpeg(ffmpegCommand) |
@@ -326,6 +431,17 @@ describe('Object storage for video static file privacy', function () { | |||
326 | permanentLiveId = video.uuid | 431 | permanentLiveId = video.uuid |
327 | permanentLive = live | 432 | permanentLive = live |
328 | } | 433 | } |
434 | |||
435 | { | ||
436 | const { video, live } = await server.live.quickCreate({ | ||
437 | saveReplay: false, | ||
438 | permanentLive: false, | ||
439 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
440 | videoPasswords: [ correctPassword ] | ||
441 | }) | ||
442 | passwordProtectedLiveId = video.uuid | ||
443 | passwordProtectedLive = live | ||
444 | } | ||
329 | }) | 445 | }) |
330 | 446 | ||
331 | it('Should create a private normal live and have a private static path', async function () { | 447 | it('Should create a private normal live and have a private static path', async function () { |
@@ -340,6 +456,12 @@ describe('Object storage for video static file privacy', function () { | |||
340 | await checkLiveFiles(permanentLive, permanentLiveId) | 456 | await checkLiveFiles(permanentLive, permanentLiveId) |
341 | }) | 457 | }) |
342 | 458 | ||
459 | it('Should create a password protected live and have a private static path', async function () { | ||
460 | this.timeout(240000) | ||
461 | |||
462 | await checkLiveFiles(passwordProtectedLive, passwordProtectedLiveId, correctPassword) | ||
463 | }) | ||
464 | |||
343 | it('Should reinject video file token in permanent live', async function () { | 465 | it('Should reinject video file token in permanent live', async function () { |
344 | this.timeout(240000) | 466 | this.timeout(240000) |
345 | 467 | ||
@@ -412,11 +534,11 @@ describe('Object storage for video static file privacy', function () { | |||
412 | 534 | ||
413 | it('Should not be able to access object storage proxy', async function () { | 535 | it('Should not be able to access object storage proxy', async function () { |
414 | const privateVideo = await server.videos.getWithToken({ id: videoUUID }) | 536 | const privateVideo = await server.videos.getWithToken({ id: videoUUID }) |
415 | const webtorrentFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl) | 537 | const webVideoFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl) |
416 | const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl) | 538 | const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl) |
417 | 539 | ||
418 | await makeRawRequest({ | 540 | await makeRawRequest({ |
419 | url: server.url + '/object-storage-proxy/webseed/private/' + webtorrentFilename, | 541 | url: server.url + '/object-storage-proxy/web-videos/private/' + webVideoFilename, |
420 | token: server.accessToken, | 542 | token: server.accessToken, |
421 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | 543 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 |
422 | }) | 544 | }) |
diff --git a/server/tests/api/object-storage/videos.ts b/server/tests/api/object-storage/videos.ts index f837d9966..dcc52ef06 100644 --- a/server/tests/api/object-storage/videos.ts +++ b/server/tests/api/object-storage/videos.ts | |||
@@ -41,8 +41,8 @@ async function checkFiles (options: { | |||
41 | playlistBucket: string | 41 | playlistBucket: string |
42 | playlistPrefix?: string | 42 | playlistPrefix?: string |
43 | 43 | ||
44 | webtorrentBucket: string | 44 | webVideoBucket: string |
45 | webtorrentPrefix?: string | 45 | webVideoPrefix?: string |
46 | }) { | 46 | }) { |
47 | const { | 47 | const { |
48 | server, | 48 | server, |
@@ -50,20 +50,20 @@ async function checkFiles (options: { | |||
50 | originSQLCommand, | 50 | originSQLCommand, |
51 | video, | 51 | video, |
52 | playlistBucket, | 52 | playlistBucket, |
53 | webtorrentBucket, | 53 | webVideoBucket, |
54 | baseMockUrl, | 54 | baseMockUrl, |
55 | playlistPrefix, | 55 | playlistPrefix, |
56 | webtorrentPrefix | 56 | webVideoPrefix |
57 | } = options | 57 | } = options |
58 | 58 | ||
59 | let allFiles = video.files | 59 | let allFiles = video.files |
60 | 60 | ||
61 | for (const file of video.files) { | 61 | for (const file of video.files) { |
62 | const baseUrl = baseMockUrl | 62 | const baseUrl = baseMockUrl |
63 | ? `${baseMockUrl}/${webtorrentBucket}/` | 63 | ? `${baseMockUrl}/${webVideoBucket}/` |
64 | : `http://${webtorrentBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` | 64 | : `http://${webVideoBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` |
65 | 65 | ||
66 | const prefix = webtorrentPrefix || '' | 66 | const prefix = webVideoPrefix || '' |
67 | const start = baseUrl + prefix | 67 | const start = baseUrl + prefix |
68 | 68 | ||
69 | expectStartWith(file.fileUrl, start) | 69 | expectStartWith(file.fileUrl, start) |
@@ -134,8 +134,8 @@ function runTestSuite (options: { | |||
134 | playlistBucket: string | 134 | playlistBucket: string |
135 | playlistPrefix?: string | 135 | playlistPrefix?: string |
136 | 136 | ||
137 | webtorrentBucket: string | 137 | webVideoBucket: string |
138 | webtorrentPrefix?: string | 138 | webVideoPrefix?: string |
139 | 139 | ||
140 | useMockBaseUrl?: boolean | 140 | useMockBaseUrl?: boolean |
141 | }) { | 141 | }) { |
@@ -161,7 +161,7 @@ function runTestSuite (options: { | |||
161 | : undefined | 161 | : undefined |
162 | 162 | ||
163 | await objectStorage.createMockBucket(options.playlistBucket) | 163 | await objectStorage.createMockBucket(options.playlistBucket) |
164 | await objectStorage.createMockBucket(options.webtorrentBucket) | 164 | await objectStorage.createMockBucket(options.webVideoBucket) |
165 | 165 | ||
166 | const config = { | 166 | const config = { |
167 | object_storage: { | 167 | object_storage: { |
@@ -181,11 +181,11 @@ function runTestSuite (options: { | |||
181 | : undefined | 181 | : undefined |
182 | }, | 182 | }, |
183 | 183 | ||
184 | videos: { | 184 | web_videos: { |
185 | bucket_name: options.webtorrentBucket, | 185 | bucket_name: options.webVideoBucket, |
186 | prefix: options.webtorrentPrefix, | 186 | prefix: options.webVideoPrefix, |
187 | base_url: baseMockUrl | 187 | base_url: baseMockUrl |
188 | ? `${baseMockUrl}/${options.webtorrentBucket}` | 188 | ? `${baseMockUrl}/${options.webVideoBucket}` |
189 | : undefined | 189 | : undefined |
190 | } | 190 | } |
191 | } | 191 | } |
@@ -308,7 +308,7 @@ describe('Object storage for videos', function () { | |||
308 | bucket_name: 'aaa' | 308 | bucket_name: 'aaa' |
309 | }, | 309 | }, |
310 | 310 | ||
311 | videos: { | 311 | web_videos: { |
312 | bucket_name: 'aaa' | 312 | bucket_name: 'aaa' |
313 | } | 313 | } |
314 | } | 314 | } |
@@ -386,27 +386,27 @@ describe('Object storage for videos', function () { | |||
386 | describe('Test simple object storage', function () { | 386 | describe('Test simple object storage', function () { |
387 | runTestSuite({ | 387 | runTestSuite({ |
388 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | 388 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), |
389 | webtorrentBucket: objectStorage.getMockBucketName('videos') | 389 | webVideoBucket: objectStorage.getMockBucketName('web-videos') |
390 | }) | 390 | }) |
391 | }) | 391 | }) |
392 | 392 | ||
393 | describe('Test object storage with prefix', function () { | 393 | describe('Test object storage with prefix', function () { |
394 | runTestSuite({ | 394 | runTestSuite({ |
395 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | 395 | playlistBucket: objectStorage.getMockBucketName('mybucket'), |
396 | webtorrentBucket: objectStorage.getMockBucketName('mybucket'), | 396 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), |
397 | 397 | ||
398 | playlistPrefix: 'streaming-playlists_', | 398 | playlistPrefix: 'streaming-playlists_', |
399 | webtorrentPrefix: 'webtorrent_' | 399 | webVideoPrefix: 'webvideo_' |
400 | }) | 400 | }) |
401 | }) | 401 | }) |
402 | 402 | ||
403 | describe('Test object storage with prefix and base URL', function () { | 403 | describe('Test object storage with prefix and base URL', function () { |
404 | runTestSuite({ | 404 | runTestSuite({ |
405 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | 405 | playlistBucket: objectStorage.getMockBucketName('mybucket'), |
406 | webtorrentBucket: objectStorage.getMockBucketName('mybucket'), | 406 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), |
407 | 407 | ||
408 | playlistPrefix: 'streaming-playlists/', | 408 | playlistPrefix: 'streaming-playlists/', |
409 | webtorrentPrefix: 'webtorrent/', | 409 | webVideoPrefix: 'webvideo/', |
410 | 410 | ||
411 | useMockBaseUrl: true | 411 | useMockBaseUrl: true |
412 | }) | 412 | }) |
@@ -431,7 +431,7 @@ describe('Object storage for videos', function () { | |||
431 | runTestSuite({ | 431 | runTestSuite({ |
432 | maxUploadPart, | 432 | maxUploadPart, |
433 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | 433 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), |
434 | webtorrentBucket: objectStorage.getMockBucketName('videos'), | 434 | webVideoBucket: objectStorage.getMockBucketName('web-videos'), |
435 | fixture | 435 | fixture |
436 | }) | 436 | }) |
437 | }) | 437 | }) |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 5262c503f..0c5c27225 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -43,7 +43,7 @@ async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], ser | |||
43 | } | 43 | } |
44 | } | 44 | } |
45 | 45 | ||
46 | async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) { | 46 | async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebVideo = true) { |
47 | const strategies: any[] = [] | 47 | const strategies: any[] = [] |
48 | 48 | ||
49 | if (strategy !== null) { | 49 | if (strategy !== null) { |
@@ -60,8 +60,8 @@ async function createServers (strategy: VideoRedundancyStrategy | null, addition | |||
60 | 60 | ||
61 | const config = { | 61 | const config = { |
62 | transcoding: { | 62 | transcoding: { |
63 | webtorrent: { | 63 | web_videos: { |
64 | enabled: withWebtorrent | 64 | enabled: withWebVideo |
65 | }, | 65 | }, |
66 | hls: { | 66 | hls: { |
67 | enabled: true | 67 | enabled: true |
@@ -100,7 +100,7 @@ async function createServers (strategy: VideoRedundancyStrategy | null, addition | |||
100 | } | 100 | } |
101 | 101 | ||
102 | async function ensureSameFilenames (videoUUID: string) { | 102 | async function ensureSameFilenames (videoUUID: string) { |
103 | let webtorrentFilenames: string[] | 103 | let webVideoFilenames: string[] |
104 | let hlsFilenames: string[] | 104 | let hlsFilenames: string[] |
105 | 105 | ||
106 | for (const server of servers) { | 106 | for (const server of servers) { |
@@ -108,24 +108,24 @@ async function ensureSameFilenames (videoUUID: string) { | |||
108 | 108 | ||
109 | // Ensure we use the same filenames that the origin | 109 | // Ensure we use the same filenames that the origin |
110 | 110 | ||
111 | const localWebtorrentFilenames = video.files.map(f => basename(f.fileUrl)).sort() | 111 | const localWebVideoFilenames = video.files.map(f => basename(f.fileUrl)).sort() |
112 | const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort() | 112 | const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort() |
113 | 113 | ||
114 | if (webtorrentFilenames) expect(webtorrentFilenames).to.deep.equal(localWebtorrentFilenames) | 114 | if (webVideoFilenames) expect(webVideoFilenames).to.deep.equal(localWebVideoFilenames) |
115 | else webtorrentFilenames = localWebtorrentFilenames | 115 | else webVideoFilenames = localWebVideoFilenames |
116 | 116 | ||
117 | if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames) | 117 | if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames) |
118 | else hlsFilenames = localHLSFilenames | 118 | else hlsFilenames = localHLSFilenames |
119 | } | 119 | } |
120 | 120 | ||
121 | return { webtorrentFilenames, hlsFilenames } | 121 | return { webVideoFilenames, hlsFilenames } |
122 | } | 122 | } |
123 | 123 | ||
124 | async function check1WebSeed (videoUUID?: string) { | 124 | async function check1WebSeed (videoUUID?: string) { |
125 | if (!videoUUID) videoUUID = video1Server2.uuid | 125 | if (!videoUUID) videoUUID = video1Server2.uuid |
126 | 126 | ||
127 | const webseeds = [ | 127 | const webseeds = [ |
128 | `${servers[1].url}/static/webseed/` | 128 | `${servers[1].url}/static/web-videos/` |
129 | ] | 129 | ] |
130 | 130 | ||
131 | for (const server of servers) { | 131 | for (const server of servers) { |
@@ -145,7 +145,7 @@ async function check2Webseeds (videoUUID?: string) { | |||
145 | 145 | ||
146 | const webseeds = [ | 146 | const webseeds = [ |
147 | `${servers[0].url}/static/redundancy/`, | 147 | `${servers[0].url}/static/redundancy/`, |
148 | `${servers[1].url}/static/webseed/` | 148 | `${servers[1].url}/static/web-videos/` |
149 | ] | 149 | ] |
150 | 150 | ||
151 | for (const server of servers) { | 151 | for (const server of servers) { |
@@ -156,11 +156,11 @@ async function check2Webseeds (videoUUID?: string) { | |||
156 | } | 156 | } |
157 | } | 157 | } |
158 | 158 | ||
159 | const { webtorrentFilenames } = await ensureSameFilenames(videoUUID) | 159 | const { webVideoFilenames } = await ensureSameFilenames(videoUUID) |
160 | 160 | ||
161 | const directories = [ | 161 | const directories = [ |
162 | servers[0].getDirectoryPath('redundancy'), | 162 | servers[0].getDirectoryPath('redundancy'), |
163 | servers[1].getDirectoryPath('videos') | 163 | servers[1].getDirectoryPath('web-videos') |
164 | ] | 164 | ] |
165 | 165 | ||
166 | for (const directory of directories) { | 166 | for (const directory of directories) { |
@@ -168,7 +168,7 @@ async function check2Webseeds (videoUUID?: string) { | |||
168 | expect(files).to.have.length.at.least(4) | 168 | expect(files).to.have.length.at.least(4) |
169 | 169 | ||
170 | // Ensure we files exist on disk | 170 | // Ensure we files exist on disk |
171 | expect(files.find(f => webtorrentFilenames.includes(f))).to.exist | 171 | expect(files.find(f => webVideoFilenames.includes(f))).to.exist |
172 | } | 172 | } |
173 | } | 173 | } |
174 | 174 | ||
diff --git a/server/tests/api/runners/runner-studio-transcoding.ts b/server/tests/api/runners/runner-studio-transcoding.ts index 41c556775..443a9d02a 100644 --- a/server/tests/api/runners/runner-studio-transcoding.ts +++ b/server/tests/api/runners/runner-studio-transcoding.ts | |||
@@ -104,7 +104,7 @@ describe('Test runner video studio transcoding', function () { | |||
104 | { | 104 | { |
105 | name: 'add-watermark' as 'add-watermark', | 105 | name: 'add-watermark' as 'add-watermark', |
106 | options: { | 106 | options: { |
107 | file: 'thumbnail.png' | 107 | file: 'custom-thumbnail.png' |
108 | } | 108 | } |
109 | }, | 109 | }, |
110 | { | 110 | { |
diff --git a/server/tests/api/runners/runner-vod-transcoding.ts b/server/tests/api/runners/runner-vod-transcoding.ts index d9da0f40d..ca16d9c10 100644 --- a/server/tests/api/runners/runner-vod-transcoding.ts +++ b/server/tests/api/runners/runner-vod-transcoding.ts | |||
@@ -424,7 +424,7 @@ describe('Test runner VOD transcoding', function () { | |||
424 | 424 | ||
425 | await servers[0].config.enableTranscoding(true, true) | 425 | await servers[0].config.enableTranscoding(true, true) |
426 | 426 | ||
427 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 427 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
428 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | 428 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) |
429 | videoUUID = uuid | 429 | videoUUID = uuid |
430 | 430 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 011ba268c..0e700eddb 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -46,6 +46,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
46 | expect(data.cache.previews.size).to.equal(1) | 46 | expect(data.cache.previews.size).to.equal(1) |
47 | expect(data.cache.captions.size).to.equal(1) | 47 | expect(data.cache.captions.size).to.equal(1) |
48 | expect(data.cache.torrents.size).to.equal(1) | 48 | expect(data.cache.torrents.size).to.equal(1) |
49 | expect(data.cache.storyboards.size).to.equal(1) | ||
49 | 50 | ||
50 | expect(data.signup.enabled).to.be.true | 51 | expect(data.signup.enabled).to.be.true |
51 | expect(data.signup.limit).to.equal(4) | 52 | expect(data.signup.limit).to.equal(4) |
@@ -78,7 +79,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
78 | expect(data.transcoding.resolutions['1440p']).to.be.true | 79 | expect(data.transcoding.resolutions['1440p']).to.be.true |
79 | expect(data.transcoding.resolutions['2160p']).to.be.true | 80 | expect(data.transcoding.resolutions['2160p']).to.be.true |
80 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true | 81 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true |
81 | expect(data.transcoding.webtorrent.enabled).to.be.true | 82 | expect(data.transcoding.webVideos.enabled).to.be.true |
82 | expect(data.transcoding.hls.enabled).to.be.true | 83 | expect(data.transcoding.hls.enabled).to.be.true |
83 | 84 | ||
84 | expect(data.live.enabled).to.be.false | 85 | expect(data.live.enabled).to.be.false |
@@ -154,6 +155,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
154 | expect(data.cache.previews.size).to.equal(2) | 155 | expect(data.cache.previews.size).to.equal(2) |
155 | expect(data.cache.captions.size).to.equal(3) | 156 | expect(data.cache.captions.size).to.equal(3) |
156 | expect(data.cache.torrents.size).to.equal(4) | 157 | expect(data.cache.torrents.size).to.equal(4) |
158 | expect(data.cache.storyboards.size).to.equal(5) | ||
157 | 159 | ||
158 | expect(data.signup.enabled).to.be.false | 160 | expect(data.signup.enabled).to.be.false |
159 | expect(data.signup.limit).to.equal(5) | 161 | expect(data.signup.limit).to.equal(5) |
@@ -190,7 +192,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
190 | expect(data.transcoding.resolutions['2160p']).to.be.false | 192 | expect(data.transcoding.resolutions['2160p']).to.be.false |
191 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false | 193 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false |
192 | expect(data.transcoding.hls.enabled).to.be.false | 194 | expect(data.transcoding.hls.enabled).to.be.false |
193 | expect(data.transcoding.webtorrent.enabled).to.be.true | 195 | expect(data.transcoding.webVideos.enabled).to.be.true |
194 | 196 | ||
195 | expect(data.live.enabled).to.be.true | 197 | expect(data.live.enabled).to.be.true |
196 | expect(data.live.allowReplay).to.be.true | 198 | expect(data.live.allowReplay).to.be.true |
@@ -290,6 +292,9 @@ const newCustomConfig: CustomConfig = { | |||
290 | }, | 292 | }, |
291 | torrents: { | 293 | torrents: { |
292 | size: 4 | 294 | size: 4 |
295 | }, | ||
296 | storyboards: { | ||
297 | size: 5 | ||
293 | } | 298 | } |
294 | }, | 299 | }, |
295 | signup: { | 300 | signup: { |
@@ -339,7 +344,7 @@ const newCustomConfig: CustomConfig = { | |||
339 | '2160p': false | 344 | '2160p': false |
340 | }, | 345 | }, |
341 | alwaysTranscodeOriginalResolution: false, | 346 | alwaysTranscodeOriginalResolution: false, |
342 | webtorrent: { | 347 | webVideos: { |
343 | enabled: true | 348 | enabled: true |
344 | }, | 349 | }, |
345 | hls: { | 350 | hls: { |
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 2a5fff82b..e3e4605ee 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -6,611 +6,636 @@ import { Video, VideoPrivacy } from '@shared/models' | |||
6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' | 6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' |
7 | 7 | ||
8 | describe('Test follows', function () { | 8 | describe('Test follows', function () { |
9 | let servers: PeerTubeServer[] = [] | ||
10 | 9 | ||
11 | before(async function () { | 10 | describe('Complex follow', function () { |
12 | this.timeout(120000) | 11 | let servers: PeerTubeServer[] = [] |
13 | 12 | ||
14 | servers = await createMultipleServers(3) | 13 | before(async function () { |
14 | this.timeout(120000) | ||
15 | 15 | ||
16 | // Get the access tokens | 16 | servers = await createMultipleServers(3) |
17 | await setAccessTokensToServers(servers) | ||
18 | }) | ||
19 | 17 | ||
20 | describe('Data propagation after follow', function () { | 18 | // Get the access tokens |
19 | await setAccessTokensToServers(servers) | ||
20 | }) | ||
21 | 21 | ||
22 | it('Should not have followers/followings', async function () { | 22 | describe('Data propagation after follow', function () { |
23 | for (const server of servers) { | ||
24 | const bodies = await Promise.all([ | ||
25 | server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), | ||
26 | server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
27 | ]) | ||
28 | 23 | ||
29 | for (const body of bodies) { | 24 | it('Should not have followers/followings', async function () { |
30 | expect(body.total).to.equal(0) | 25 | for (const server of servers) { |
26 | const bodies = await Promise.all([ | ||
27 | server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), | ||
28 | server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
29 | ]) | ||
31 | 30 | ||
32 | const follows = body.data | 31 | for (const body of bodies) { |
33 | expect(follows).to.be.an('array') | 32 | expect(body.total).to.equal(0) |
34 | expect(follows).to.have.lengthOf(0) | 33 | |
34 | const follows = body.data | ||
35 | expect(follows).to.be.an('array') | ||
36 | expect(follows).to.have.lengthOf(0) | ||
37 | } | ||
35 | } | 38 | } |
36 | } | 39 | }) |
37 | }) | 40 | |
41 | it('Should have server 1 following root account of server 2 and server 3', async function () { | ||
42 | this.timeout(30000) | ||
38 | 43 | ||
39 | it('Should have server 1 following root account of server 2 and server 3', async function () { | 44 | await servers[0].follows.follow({ |
40 | this.timeout(30000) | 45 | hosts: [ servers[2].url ], |
46 | handles: [ 'root@' + servers[1].host ] | ||
47 | }) | ||
41 | 48 | ||
42 | await servers[0].follows.follow({ | 49 | await waitJobs(servers) |
43 | hosts: [ servers[2].url ], | ||
44 | handles: [ 'root@' + servers[1].host ] | ||
45 | }) | 50 | }) |
46 | 51 | ||
47 | await waitJobs(servers) | 52 | it('Should have 2 followings on server 1', async function () { |
48 | }) | 53 | const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) |
54 | expect(body.total).to.equal(2) | ||
49 | 55 | ||
50 | it('Should have 2 followings on server 1', async function () { | 56 | let follows = body.data |
51 | const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) | 57 | expect(follows).to.be.an('array') |
52 | expect(body.total).to.equal(2) | 58 | expect(follows).to.have.lengthOf(1) |
53 | 59 | ||
54 | let follows = body.data | 60 | const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) |
55 | expect(follows).to.be.an('array') | 61 | follows = follows.concat(body2.data) |
56 | expect(follows).to.have.lengthOf(1) | ||
57 | 62 | ||
58 | const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) | 63 | const server2Follow = follows.find(f => f.following.host === servers[1].host) |
59 | follows = follows.concat(body2.data) | 64 | const server3Follow = follows.find(f => f.following.host === servers[2].host) |
60 | 65 | ||
61 | const server2Follow = follows.find(f => f.following.host === servers[1].host) | 66 | expect(server2Follow).to.not.be.undefined |
62 | const server3Follow = follows.find(f => f.following.host === servers[2].host) | 67 | expect(server2Follow.following.name).to.equal('root') |
68 | expect(server2Follow.state).to.equal('accepted') | ||
63 | 69 | ||
64 | expect(server2Follow).to.not.be.undefined | 70 | expect(server3Follow).to.not.be.undefined |
65 | expect(server2Follow.following.name).to.equal('root') | 71 | expect(server3Follow.following.name).to.equal('peertube') |
66 | expect(server2Follow.state).to.equal('accepted') | 72 | expect(server3Follow.state).to.equal('accepted') |
73 | }) | ||
67 | 74 | ||
68 | expect(server3Follow).to.not.be.undefined | 75 | it('Should have 0 followings on server 2 and 3', async function () { |
69 | expect(server3Follow.following.name).to.equal('peertube') | 76 | for (const server of [ servers[1], servers[2] ]) { |
70 | expect(server3Follow.state).to.equal('accepted') | 77 | const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) |
71 | }) | 78 | expect(body.total).to.equal(0) |
72 | 79 | ||
73 | it('Should have 0 followings on server 2 and 3', async function () { | 80 | const follows = body.data |
74 | for (const server of [ servers[1], servers[2] ]) { | 81 | expect(follows).to.be.an('array') |
75 | const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | 82 | expect(follows).to.have.lengthOf(0) |
76 | expect(body.total).to.equal(0) | 83 | } |
84 | }) | ||
85 | |||
86 | it('Should have 1 followers on server 3', async function () { | ||
87 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
88 | expect(body.total).to.equal(1) | ||
77 | 89 | ||
78 | const follows = body.data | 90 | const follows = body.data |
79 | expect(follows).to.be.an('array') | 91 | expect(follows).to.be.an('array') |
80 | expect(follows).to.have.lengthOf(0) | 92 | expect(follows).to.have.lengthOf(1) |
81 | } | 93 | expect(follows[0].follower.host).to.equal(servers[0].host) |
82 | }) | 94 | }) |
83 | 95 | ||
84 | it('Should have 1 followers on server 3', async function () { | 96 | it('Should have 0 followers on server 1 and 2', async function () { |
85 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | 97 | for (const server of [ servers[0], servers[1] ]) { |
86 | expect(body.total).to.equal(1) | 98 | const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) |
99 | expect(body.total).to.equal(0) | ||
87 | 100 | ||
88 | const follows = body.data | 101 | const follows = body.data |
89 | expect(follows).to.be.an('array') | 102 | expect(follows).to.be.an('array') |
90 | expect(follows).to.have.lengthOf(1) | 103 | expect(follows).to.have.lengthOf(0) |
91 | expect(follows[0].follower.host).to.equal(servers[0].host) | 104 | } |
92 | }) | 105 | }) |
93 | 106 | ||
94 | it('Should have 0 followers on server 1 and 2', async function () { | 107 | it('Should search/filter followings on server 1', async function () { |
95 | for (const server of [ servers[0], servers[1] ]) { | 108 | const sort = 'createdAt' |
96 | const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) | 109 | const start = 0 |
97 | expect(body.total).to.equal(0) | 110 | const count = 1 |
98 | 111 | ||
99 | const follows = body.data | 112 | { |
100 | expect(follows).to.be.an('array') | 113 | const search = ':' + servers[1].port |
101 | expect(follows).to.have.lengthOf(0) | ||
102 | } | ||
103 | }) | ||
104 | 114 | ||
105 | it('Should search/filter followings on server 1', async function () { | 115 | { |
106 | const sort = 'createdAt' | 116 | const body = await servers[0].follows.getFollowings({ start, count, sort, search }) |
107 | const start = 0 | 117 | expect(body.total).to.equal(1) |
108 | const count = 1 | ||
109 | 118 | ||
110 | { | 119 | const follows = body.data |
111 | const search = ':' + servers[1].port | 120 | expect(follows).to.have.lengthOf(1) |
121 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
122 | } | ||
112 | 123 | ||
113 | { | 124 | { |
114 | const body = await servers[0].follows.getFollowings({ start, count, sort, search }) | 125 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) |
115 | expect(body.total).to.equal(1) | 126 | expect(body.total).to.equal(1) |
127 | expect(body.data).to.have.lengthOf(1) | ||
128 | } | ||
116 | 129 | ||
117 | const follows = body.data | 130 | { |
118 | expect(follows).to.have.lengthOf(1) | 131 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) |
119 | expect(follows[0].following.host).to.equal(servers[1].host) | 132 | expect(body.total).to.equal(1) |
120 | } | 133 | expect(body.data).to.have.lengthOf(1) |
134 | } | ||
121 | 135 | ||
122 | { | 136 | { |
123 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) | 137 | const body = await servers[0].follows.getFollowings({ |
124 | expect(body.total).to.equal(1) | 138 | start, |
125 | expect(body.data).to.have.lengthOf(1) | 139 | count, |
140 | sort, | ||
141 | search, | ||
142 | state: 'accepted', | ||
143 | actorType: 'Application' | ||
144 | }) | ||
145 | expect(body.total).to.equal(0) | ||
146 | expect(body.data).to.have.lengthOf(0) | ||
147 | } | ||
148 | |||
149 | { | ||
150 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) | ||
151 | expect(body.total).to.equal(0) | ||
152 | expect(body.data).to.have.lengthOf(0) | ||
153 | } | ||
126 | } | 154 | } |
127 | 155 | ||
128 | { | 156 | { |
129 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | 157 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) |
130 | expect(body.total).to.equal(1) | 158 | expect(body.total).to.equal(1) |
131 | expect(body.data).to.have.lengthOf(1) | 159 | expect(body.data).to.have.lengthOf(1) |
132 | } | 160 | } |
133 | 161 | ||
134 | { | 162 | { |
135 | const body = await servers[0].follows.getFollowings({ | 163 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) |
136 | start, | ||
137 | count, | ||
138 | sort, | ||
139 | search, | ||
140 | state: 'accepted', | ||
141 | actorType: 'Application' | ||
142 | }) | ||
143 | expect(body.total).to.equal(0) | 164 | expect(body.total).to.equal(0) |
144 | expect(body.data).to.have.lengthOf(0) | ||
145 | } | ||
146 | 165 | ||
147 | { | ||
148 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) | ||
149 | expect(body.total).to.equal(0) | ||
150 | expect(body.data).to.have.lengthOf(0) | 166 | expect(body.data).to.have.lengthOf(0) |
151 | } | 167 | } |
152 | } | 168 | }) |
153 | 169 | ||
154 | { | 170 | it('Should search/filter followers on server 2', async function () { |
155 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) | 171 | const start = 0 |
156 | expect(body.total).to.equal(1) | 172 | const count = 5 |
157 | expect(body.data).to.have.lengthOf(1) | 173 | const sort = 'createdAt' |
158 | } | ||
159 | 174 | ||
160 | { | 175 | { |
161 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) | 176 | const search = servers[0].port + '' |
162 | expect(body.total).to.equal(0) | ||
163 | 177 | ||
164 | expect(body.data).to.have.lengthOf(0) | 178 | { |
165 | } | 179 | const body = await servers[2].follows.getFollowers({ start, count, sort, search }) |
166 | }) | 180 | expect(body.total).to.equal(1) |
167 | 181 | ||
168 | it('Should search/filter followers on server 2', async function () { | 182 | const follows = body.data |
169 | const start = 0 | 183 | expect(follows).to.have.lengthOf(1) |
170 | const count = 5 | 184 | expect(follows[0].following.host).to.equal(servers[2].host) |
171 | const sort = 'createdAt' | 185 | } |
172 | 186 | ||
173 | { | 187 | { |
174 | const search = servers[0].port + '' | 188 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) |
189 | expect(body.total).to.equal(1) | ||
190 | expect(body.data).to.have.lengthOf(1) | ||
191 | } | ||
175 | 192 | ||
176 | { | 193 | { |
177 | const body = await servers[2].follows.getFollowers({ start, count, sort, search }) | 194 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) |
178 | expect(body.total).to.equal(1) | 195 | expect(body.total).to.equal(0) |
196 | expect(body.data).to.have.lengthOf(0) | ||
197 | } | ||
179 | 198 | ||
180 | const follows = body.data | 199 | { |
181 | expect(follows).to.have.lengthOf(1) | 200 | const body = await servers[2].follows.getFollowers({ |
182 | expect(follows[0].following.host).to.equal(servers[2].host) | 201 | start, |
183 | } | 202 | count, |
203 | sort, | ||
204 | search, | ||
205 | state: 'accepted', | ||
206 | actorType: 'Application' | ||
207 | }) | ||
208 | expect(body.total).to.equal(1) | ||
209 | expect(body.data).to.have.lengthOf(1) | ||
210 | } | ||
184 | 211 | ||
185 | { | 212 | { |
186 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) | 213 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) |
187 | expect(body.total).to.equal(1) | 214 | expect(body.total).to.equal(0) |
188 | expect(body.data).to.have.lengthOf(1) | 215 | expect(body.data).to.have.lengthOf(0) |
216 | } | ||
189 | } | 217 | } |
190 | 218 | ||
191 | { | 219 | { |
192 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | 220 | const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) |
193 | expect(body.total).to.equal(0) | 221 | expect(body.total).to.equal(0) |
194 | expect(body.data).to.have.lengthOf(0) | ||
195 | } | ||
196 | 222 | ||
197 | { | 223 | const follows = body.data |
198 | const body = await servers[2].follows.getFollowers({ | 224 | expect(follows).to.have.lengthOf(0) |
199 | start, | ||
200 | count, | ||
201 | sort, | ||
202 | search, | ||
203 | state: 'accepted', | ||
204 | actorType: 'Application' | ||
205 | }) | ||
206 | expect(body.total).to.equal(1) | ||
207 | expect(body.data).to.have.lengthOf(1) | ||
208 | } | 225 | } |
226 | }) | ||
209 | 227 | ||
210 | { | 228 | it('Should have the correct follows counts', async function () { |
211 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) | 229 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) |
212 | expect(body.total).to.equal(0) | 230 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
213 | expect(body.data).to.have.lengthOf(0) | 231 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) |
214 | } | 232 | |
215 | } | 233 | // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) |
234 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
235 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
236 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
237 | |||
238 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
239 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
240 | }) | ||
241 | |||
242 | it('Should unfollow server 3 on server 1', async function () { | ||
243 | this.timeout(15000) | ||
244 | |||
245 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
216 | 246 | ||
217 | { | 247 | await waitJobs(servers) |
218 | const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) | 248 | }) |
249 | |||
250 | it('Should not follow server 3 on server 1 anymore', async function () { | ||
251 | const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) | ||
252 | expect(body.total).to.equal(1) | ||
253 | |||
254 | const follows = body.data | ||
255 | expect(follows).to.be.an('array') | ||
256 | expect(follows).to.have.lengthOf(1) | ||
257 | |||
258 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
259 | }) | ||
260 | |||
261 | it('Should not have server 1 as follower on server 3 anymore', async function () { | ||
262 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
219 | expect(body.total).to.equal(0) | 263 | expect(body.total).to.equal(0) |
220 | 264 | ||
221 | const follows = body.data | 265 | const follows = body.data |
266 | expect(follows).to.be.an('array') | ||
222 | expect(follows).to.have.lengthOf(0) | 267 | expect(follows).to.have.lengthOf(0) |
223 | } | 268 | }) |
224 | }) | ||
225 | 269 | ||
226 | it('Should have the correct follows counts', async function () { | 270 | it('Should have the correct follows counts after the unfollow', async function () { |
227 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | 271 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
228 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 272 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
229 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | 273 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) |
230 | 274 | ||
231 | // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) | 275 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
232 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 276 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) |
233 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 277 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) |
234 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
235 | 278 | ||
236 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 279 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) |
237 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | 280 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) |
238 | }) | 281 | }) |
239 | 282 | ||
240 | it('Should unfollow server 3 on server 1', async function () { | 283 | it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { |
241 | this.timeout(15000) | 284 | this.timeout(160000) |
242 | 285 | ||
243 | await servers[0].follows.unfollow({ target: servers[2] }) | 286 | await servers[1].videos.upload({ attributes: { name: 'server2' } }) |
287 | await servers[2].videos.upload({ attributes: { name: 'server3' } }) | ||
244 | 288 | ||
245 | await waitJobs(servers) | 289 | await waitJobs(servers) |
246 | }) | ||
247 | 290 | ||
248 | it('Should not follow server 3 on server 1 anymore', async function () { | 291 | { |
249 | const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) | 292 | const { total, data } = await servers[0].videos.list() |
250 | expect(body.total).to.equal(1) | 293 | expect(total).to.equal(1) |
294 | expect(data[0].name).to.equal('server2') | ||
295 | } | ||
251 | 296 | ||
252 | const follows = body.data | 297 | { |
253 | expect(follows).to.be.an('array') | 298 | const { total, data } = await servers[1].videos.list() |
254 | expect(follows).to.have.lengthOf(1) | 299 | expect(total).to.equal(1) |
300 | expect(data[0].name).to.equal('server2') | ||
301 | } | ||
255 | 302 | ||
256 | expect(follows[0].following.host).to.equal(servers[1].host) | 303 | { |
257 | }) | 304 | const { total, data } = await servers[2].videos.list() |
305 | expect(total).to.equal(1) | ||
306 | expect(data[0].name).to.equal('server3') | ||
307 | } | ||
308 | }) | ||
258 | 309 | ||
259 | it('Should not have server 1 as follower on server 3 anymore', async function () { | 310 | it('Should remove account follow', async function () { |
260 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | 311 | this.timeout(15000) |
261 | expect(body.total).to.equal(0) | ||
262 | 312 | ||
263 | const follows = body.data | 313 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) |
264 | expect(follows).to.be.an('array') | ||
265 | expect(follows).to.have.lengthOf(0) | ||
266 | }) | ||
267 | 314 | ||
268 | it('Should have the correct follows counts after the unfollow', async function () { | 315 | await waitJobs(servers) |
269 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 316 | }) |
270 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
271 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | ||
272 | 317 | ||
273 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 318 | it('Should have removed the account follow', async function () { |
274 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | 319 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
275 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | 320 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
276 | 321 | ||
277 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) | 322 | { |
278 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | 323 | const { total, data } = await servers[0].follows.getFollowings() |
279 | }) | 324 | expect(total).to.equal(0) |
325 | expect(data).to.have.lengthOf(0) | ||
326 | } | ||
280 | 327 | ||
281 | it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { | 328 | { |
282 | this.timeout(160000) | 329 | const { total, data } = await servers[0].videos.list() |
330 | expect(total).to.equal(0) | ||
331 | expect(data).to.have.lengthOf(0) | ||
332 | } | ||
333 | }) | ||
283 | 334 | ||
284 | await servers[1].videos.upload({ attributes: { name: 'server2' } }) | 335 | it('Should follow a channel', async function () { |
285 | await servers[2].videos.upload({ attributes: { name: 'server3' } }) | 336 | this.timeout(15000) |
286 | 337 | ||
287 | await waitJobs(servers) | 338 | await servers[0].follows.follow({ |
339 | handles: [ 'root_channel@' + servers[1].host ] | ||
340 | }) | ||
288 | 341 | ||
289 | { | 342 | await waitJobs(servers) |
290 | const { total, data } = await servers[0].videos.list() | ||
291 | expect(total).to.equal(1) | ||
292 | expect(data[0].name).to.equal('server2') | ||
293 | } | ||
294 | 343 | ||
295 | { | 344 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
296 | const { total, data } = await servers[1].videos.list() | 345 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
297 | expect(total).to.equal(1) | ||
298 | expect(data[0].name).to.equal('server2') | ||
299 | } | ||
300 | 346 | ||
301 | { | 347 | { |
302 | const { total, data } = await servers[2].videos.list() | 348 | const { total, data } = await servers[0].follows.getFollowings() |
303 | expect(total).to.equal(1) | 349 | expect(total).to.equal(1) |
304 | expect(data[0].name).to.equal('server3') | 350 | expect(data).to.have.lengthOf(1) |
305 | } | 351 | } |
352 | |||
353 | { | ||
354 | const { total, data } = await servers[0].videos.list() | ||
355 | expect(total).to.equal(1) | ||
356 | expect(data).to.have.lengthOf(1) | ||
357 | } | ||
358 | }) | ||
306 | }) | 359 | }) |
307 | 360 | ||
308 | it('Should remove account follow', async function () { | 361 | describe('Should propagate data on a new server follow', function () { |
309 | this.timeout(15000) | 362 | let video4: Video |
310 | 363 | ||
311 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) | 364 | before(async function () { |
365 | this.timeout(240000) | ||
312 | 366 | ||
313 | await waitJobs(servers) | 367 | const video4Attributes = { |
314 | }) | 368 | name: 'server3-4', |
369 | category: 2, | ||
370 | nsfw: true, | ||
371 | licence: 6, | ||
372 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
373 | } | ||
315 | 374 | ||
316 | it('Should have removed the account follow', async function () { | 375 | await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) |
317 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | 376 | await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) |
318 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
319 | 377 | ||
320 | { | 378 | const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) |
321 | const { total, data } = await servers[0].follows.getFollowings() | ||
322 | expect(total).to.equal(0) | ||
323 | expect(data).to.have.lengthOf(0) | ||
324 | } | ||
325 | 379 | ||
326 | { | 380 | await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) |
327 | const { total, data } = await servers[0].videos.list() | 381 | await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) |
328 | expect(total).to.equal(0) | ||
329 | expect(data).to.have.lengthOf(0) | ||
330 | } | ||
331 | }) | ||
332 | 382 | ||
333 | it('Should follow a channel', async function () { | 383 | { |
334 | this.timeout(15000) | 384 | const userAccessToken = await servers[2].users.generateUserAndToken('captain') |
335 | 385 | ||
336 | await servers[0].follows.follow({ | 386 | await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) |
337 | handles: [ 'root_channel@' + servers[1].host ] | 387 | await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) |
338 | }) | 388 | } |
339 | 389 | ||
340 | await waitJobs(servers) | 390 | { |
391 | await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) | ||
341 | 392 | ||
342 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | 393 | await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) |
343 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | 394 | await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) |
395 | await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) | ||
396 | } | ||
344 | 397 | ||
345 | { | 398 | { |
346 | const { total, data } = await servers[0].follows.getFollowings() | 399 | const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) |
347 | expect(total).to.equal(1) | 400 | await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) |
348 | expect(data).to.have.lengthOf(1) | ||
349 | } | ||
350 | 401 | ||
351 | { | 402 | const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) |
352 | const { total, data } = await servers[0].videos.list() | ||
353 | expect(total).to.equal(1) | ||
354 | expect(data).to.have.lengthOf(1) | ||
355 | } | ||
356 | }) | ||
357 | }) | ||
358 | 403 | ||
359 | describe('Should propagate data on a new server follow', function () { | 404 | await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) |
360 | let video4: Video | ||
361 | 405 | ||
362 | before(async function () { | 406 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) |
363 | this.timeout(120000) | 407 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) |
408 | } | ||
364 | 409 | ||
365 | const video4Attributes = { | 410 | await servers[2].captions.add({ |
366 | name: 'server3-4', | 411 | language: 'ar', |
367 | category: 2, | 412 | videoId: video4CreateResult.id, |
368 | nsfw: true, | 413 | fixture: 'subtitle-good2.vtt' |
369 | licence: 6, | 414 | }) |
370 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
371 | } | ||
372 | 415 | ||
373 | await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) | 416 | await waitJobs(servers) |
374 | await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) | ||
375 | const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) | ||
376 | await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) | ||
377 | await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) | ||
378 | 417 | ||
379 | { | 418 | // Server 1 follows server 3 |
380 | const userAccessToken = await servers[2].users.generateUserAndToken('captain') | 419 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) |
381 | 420 | ||
382 | await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) | 421 | await waitJobs(servers) |
383 | await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) | 422 | }) |
384 | } | ||
385 | 423 | ||
386 | { | 424 | it('Should have the correct follows counts', async function () { |
387 | await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) | 425 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) |
426 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
427 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
428 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
388 | 429 | ||
389 | await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) | 430 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
390 | await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) | 431 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) |
391 | await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) | 432 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) |
392 | } | 433 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) |
393 | 434 | ||
394 | { | 435 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) |
395 | const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) | 436 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) |
396 | await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) | 437 | }) |
397 | 438 | ||
398 | const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) | 439 | it('Should have propagated videos', async function () { |
440 | const { total, data } = await servers[0].videos.list() | ||
441 | expect(total).to.equal(7) | ||
442 | |||
443 | const video2 = data.find(v => v.name === 'server3-2') | ||
444 | video4 = data.find(v => v.name === 'server3-4') | ||
445 | const video6 = data.find(v => v.name === 'server3-6') | ||
446 | |||
447 | expect(video2).to.not.be.undefined | ||
448 | expect(video4).to.not.be.undefined | ||
449 | expect(video6).to.not.be.undefined | ||
450 | |||
451 | const isLocal = false | ||
452 | const checkAttributes = { | ||
453 | name: 'server3-4', | ||
454 | category: 2, | ||
455 | licence: 6, | ||
456 | language: 'zh', | ||
457 | nsfw: true, | ||
458 | description: 'my super description', | ||
459 | support: 'my super support text', | ||
460 | account: { | ||
461 | name: 'root', | ||
462 | host: servers[2].host | ||
463 | }, | ||
464 | isLocal, | ||
465 | commentsEnabled: true, | ||
466 | downloadEnabled: true, | ||
467 | duration: 5, | ||
468 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
469 | privacy: VideoPrivacy.PUBLIC, | ||
470 | likes: 1, | ||
471 | dislikes: 1, | ||
472 | channel: { | ||
473 | displayName: 'Main root channel', | ||
474 | name: 'root_channel', | ||
475 | description: '', | ||
476 | isLocal | ||
477 | }, | ||
478 | fixture: 'video_short.webm', | ||
479 | files: [ | ||
480 | { | ||
481 | resolution: 720, | ||
482 | size: 218910 | ||
483 | } | ||
484 | ] | ||
485 | } | ||
486 | await completeVideoCheck({ | ||
487 | server: servers[0], | ||
488 | originServer: servers[2], | ||
489 | videoUUID: video4.uuid, | ||
490 | attributes: checkAttributes | ||
491 | }) | ||
492 | }) | ||
399 | 493 | ||
400 | await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) | 494 | it('Should have propagated comments', async function () { |
495 | const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) | ||
401 | 496 | ||
402 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) | 497 | expect(total).to.equal(2) |
403 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) | 498 | expect(data).to.be.an('array') |
404 | } | 499 | expect(data).to.have.lengthOf(2) |
405 | 500 | ||
406 | await servers[2].captions.add({ | 501 | { |
407 | language: 'ar', | 502 | const comment = data[0] |
408 | videoId: video4CreateResult.id, | 503 | expect(comment.inReplyToCommentId).to.be.null |
409 | fixture: 'subtitle-good2.vtt' | 504 | expect(comment.text).equal('my super first comment') |
410 | }) | 505 | expect(comment.videoId).to.equal(video4.id) |
506 | expect(comment.id).to.equal(comment.threadId) | ||
507 | expect(comment.account.name).to.equal('root') | ||
508 | expect(comment.account.host).to.equal(servers[2].host) | ||
509 | expect(comment.totalReplies).to.equal(3) | ||
510 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
511 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
512 | |||
513 | const threadId = comment.threadId | ||
514 | |||
515 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) | ||
516 | expect(tree.comment.text).equal('my super first comment') | ||
517 | expect(tree.children).to.have.lengthOf(2) | ||
518 | |||
519 | const firstChild = tree.children[0] | ||
520 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
521 | expect(firstChild.children).to.have.lengthOf(1) | ||
522 | |||
523 | const childOfFirstChild = firstChild.children[0] | ||
524 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
525 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
526 | |||
527 | const secondChild = tree.children[1] | ||
528 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
529 | expect(secondChild.children).to.have.lengthOf(0) | ||
530 | } | ||
411 | 531 | ||
412 | await waitJobs(servers) | 532 | { |
533 | const deletedComment = data[1] | ||
534 | expect(deletedComment).to.not.be.undefined | ||
535 | expect(deletedComment.isDeleted).to.be.true | ||
536 | expect(deletedComment.deletedAt).to.not.be.null | ||
537 | expect(deletedComment.text).to.equal('') | ||
538 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
539 | expect(deletedComment.account).to.be.null | ||
540 | expect(deletedComment.totalReplies).to.equal(2) | ||
541 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
542 | |||
543 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) | ||
544 | const [ commentRoot, deletedChildRoot ] = tree.children | ||
545 | |||
546 | expect(deletedChildRoot).to.not.be.undefined | ||
547 | expect(deletedChildRoot.comment.isDeleted).to.be.true | ||
548 | expect(deletedChildRoot.comment.deletedAt).to.not.be.null | ||
549 | expect(deletedChildRoot.comment.text).to.equal('') | ||
550 | expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
551 | expect(deletedChildRoot.comment.account).to.be.null | ||
552 | expect(deletedChildRoot.children).to.have.lengthOf(1) | ||
553 | |||
554 | const answerToDeletedChild = deletedChildRoot.children[0] | ||
555 | expect(answerToDeletedChild.comment).to.not.be.undefined | ||
556 | expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) | ||
557 | expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') | ||
558 | expect(answerToDeletedChild.comment.account.name).to.equal('root') | ||
559 | |||
560 | expect(commentRoot.comment).to.not.be.undefined | ||
561 | expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
562 | expect(commentRoot.comment.text).to.equal('answer to deleted') | ||
563 | expect(commentRoot.comment.account.name).to.equal('root') | ||
564 | } | ||
565 | }) | ||
413 | 566 | ||
414 | // Server 1 follows server 3 | 567 | it('Should have propagated captions', async function () { |
415 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) | 568 | const body = await servers[0].captions.list({ videoId: video4.id }) |
569 | expect(body.total).to.equal(1) | ||
570 | expect(body.data).to.have.lengthOf(1) | ||
416 | 571 | ||
417 | await waitJobs(servers) | 572 | const caption1 = body.data[0] |
418 | }) | 573 | expect(caption1.language.id).to.equal('ar') |
574 | expect(caption1.language.label).to.equal('Arabic') | ||
575 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) | ||
576 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | ||
577 | }) | ||
419 | 578 | ||
420 | it('Should have the correct follows counts', async function () { | 579 | it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { |
421 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | 580 | this.timeout(5000) |
422 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
423 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
424 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
425 | 581 | ||
426 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 582 | await servers[0].follows.unfollow({ target: servers[2] }) |
427 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
428 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
429 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
430 | 583 | ||
431 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | 584 | await waitJobs(servers) |
432 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
433 | }) | ||
434 | 585 | ||
435 | it('Should have propagated videos', async function () { | 586 | const { total } = await servers[0].videos.list() |
436 | const { total, data } = await servers[0].videos.list() | 587 | expect(total).to.equal(1) |
437 | expect(total).to.equal(7) | ||
438 | |||
439 | const video2 = data.find(v => v.name === 'server3-2') | ||
440 | video4 = data.find(v => v.name === 'server3-4') | ||
441 | const video6 = data.find(v => v.name === 'server3-6') | ||
442 | |||
443 | expect(video2).to.not.be.undefined | ||
444 | expect(video4).to.not.be.undefined | ||
445 | expect(video6).to.not.be.undefined | ||
446 | |||
447 | const isLocal = false | ||
448 | const checkAttributes = { | ||
449 | name: 'server3-4', | ||
450 | category: 2, | ||
451 | licence: 6, | ||
452 | language: 'zh', | ||
453 | nsfw: true, | ||
454 | description: 'my super description', | ||
455 | support: 'my super support text', | ||
456 | account: { | ||
457 | name: 'root', | ||
458 | host: servers[2].host | ||
459 | }, | ||
460 | isLocal, | ||
461 | commentsEnabled: true, | ||
462 | downloadEnabled: true, | ||
463 | duration: 5, | ||
464 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
465 | privacy: VideoPrivacy.PUBLIC, | ||
466 | likes: 1, | ||
467 | dislikes: 1, | ||
468 | channel: { | ||
469 | displayName: 'Main root channel', | ||
470 | name: 'root_channel', | ||
471 | description: '', | ||
472 | isLocal | ||
473 | }, | ||
474 | fixture: 'video_short.webm', | ||
475 | files: [ | ||
476 | { | ||
477 | resolution: 720, | ||
478 | size: 218910 | ||
479 | } | ||
480 | ] | ||
481 | } | ||
482 | await completeVideoCheck({ | ||
483 | server: servers[0], | ||
484 | originServer: servers[2], | ||
485 | videoUUID: video4.uuid, | ||
486 | attributes: checkAttributes | ||
487 | }) | 588 | }) |
488 | }) | 589 | }) |
489 | 590 | ||
490 | it('Should have propagated comments', async function () { | 591 | after(async function () { |
491 | const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) | 592 | await cleanupTests(servers) |
492 | |||
493 | expect(total).to.equal(2) | ||
494 | expect(data).to.be.an('array') | ||
495 | expect(data).to.have.lengthOf(2) | ||
496 | |||
497 | { | ||
498 | const comment = data[0] | ||
499 | expect(comment.inReplyToCommentId).to.be.null | ||
500 | expect(comment.text).equal('my super first comment') | ||
501 | expect(comment.videoId).to.equal(video4.id) | ||
502 | expect(comment.id).to.equal(comment.threadId) | ||
503 | expect(comment.account.name).to.equal('root') | ||
504 | expect(comment.account.host).to.equal(servers[2].host) | ||
505 | expect(comment.totalReplies).to.equal(3) | ||
506 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
507 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
508 | |||
509 | const threadId = comment.threadId | ||
510 | |||
511 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) | ||
512 | expect(tree.comment.text).equal('my super first comment') | ||
513 | expect(tree.children).to.have.lengthOf(2) | ||
514 | |||
515 | const firstChild = tree.children[0] | ||
516 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
517 | expect(firstChild.children).to.have.lengthOf(1) | ||
518 | |||
519 | const childOfFirstChild = firstChild.children[0] | ||
520 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
521 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
522 | |||
523 | const secondChild = tree.children[1] | ||
524 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
525 | expect(secondChild.children).to.have.lengthOf(0) | ||
526 | } | ||
527 | |||
528 | { | ||
529 | const deletedComment = data[1] | ||
530 | expect(deletedComment).to.not.be.undefined | ||
531 | expect(deletedComment.isDeleted).to.be.true | ||
532 | expect(deletedComment.deletedAt).to.not.be.null | ||
533 | expect(deletedComment.text).to.equal('') | ||
534 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
535 | expect(deletedComment.account).to.be.null | ||
536 | expect(deletedComment.totalReplies).to.equal(2) | ||
537 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
538 | |||
539 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) | ||
540 | const [ commentRoot, deletedChildRoot ] = tree.children | ||
541 | |||
542 | expect(deletedChildRoot).to.not.be.undefined | ||
543 | expect(deletedChildRoot.comment.isDeleted).to.be.true | ||
544 | expect(deletedChildRoot.comment.deletedAt).to.not.be.null | ||
545 | expect(deletedChildRoot.comment.text).to.equal('') | ||
546 | expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
547 | expect(deletedChildRoot.comment.account).to.be.null | ||
548 | expect(deletedChildRoot.children).to.have.lengthOf(1) | ||
549 | |||
550 | const answerToDeletedChild = deletedChildRoot.children[0] | ||
551 | expect(answerToDeletedChild.comment).to.not.be.undefined | ||
552 | expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) | ||
553 | expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') | ||
554 | expect(answerToDeletedChild.comment.account.name).to.equal('root') | ||
555 | |||
556 | expect(commentRoot.comment).to.not.be.undefined | ||
557 | expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
558 | expect(commentRoot.comment.text).to.equal('answer to deleted') | ||
559 | expect(commentRoot.comment.account.name).to.equal('root') | ||
560 | } | ||
561 | }) | 593 | }) |
594 | }) | ||
562 | 595 | ||
563 | it('Should have propagated captions', async function () { | 596 | describe('Simple data propagation propagate data on a new channel follow', function () { |
564 | const body = await servers[0].captions.list({ videoId: video4.id }) | 597 | let servers: PeerTubeServer[] = [] |
565 | expect(body.total).to.equal(1) | ||
566 | expect(body.data).to.have.lengthOf(1) | ||
567 | 598 | ||
568 | const caption1 = body.data[0] | 599 | before(async function () { |
569 | expect(caption1.language.id).to.equal('ar') | 600 | this.timeout(120000) |
570 | expect(caption1.language.label).to.equal('Arabic') | ||
571 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) | ||
572 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | ||
573 | }) | ||
574 | 601 | ||
575 | it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { | 602 | servers = await createMultipleServers(3) |
576 | this.timeout(5000) | 603 | await setAccessTokensToServers(servers) |
577 | 604 | ||
578 | await servers[0].follows.unfollow({ target: servers[2] }) | 605 | await servers[0].videos.upload({ attributes: { name: 'video to add' } }) |
579 | 606 | ||
580 | await waitJobs(servers) | 607 | await waitJobs(servers) |
581 | 608 | ||
582 | const { total } = await servers[0].videos.list() | 609 | for (const server of [ servers[1], servers[2] ]) { |
583 | expect(total).to.equal(1) | 610 | const video = await server.videos.find({ name: 'video to add' }) |
611 | expect(video).to.not.exist | ||
612 | } | ||
584 | }) | 613 | }) |
585 | }) | ||
586 | |||
587 | describe('Should propagate data on a new channel follow', function () { | ||
588 | 614 | ||
589 | before(async function () { | 615 | it('Should have propagated video after new channel follow', async function () { |
590 | this.timeout(60000) | 616 | this.timeout(60000) |
591 | 617 | ||
592 | await servers[2].videos.upload({ attributes: { name: 'server3-7' } }) | 618 | await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] }) |
593 | 619 | ||
594 | await waitJobs(servers) | 620 | await waitJobs(servers) |
595 | 621 | ||
596 | const video = await servers[0].videos.find({ name: 'server3-7' }) | 622 | const video = await servers[1].videos.find({ name: 'video to add' }) |
597 | expect(video).to.not.exist | 623 | expect(video).to.exist |
598 | }) | 624 | }) |
599 | 625 | ||
600 | it('Should have propagated channel video', async function () { | 626 | it('Should have propagated video after new account follow', async function () { |
601 | this.timeout(60000) | 627 | this.timeout(60000) |
602 | 628 | ||
603 | await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] }) | 629 | await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] }) |
604 | 630 | ||
605 | await waitJobs(servers) | 631 | await waitJobs(servers) |
606 | 632 | ||
607 | const video = await servers[0].videos.find({ name: 'server3-7' }) | 633 | const video = await servers[2].videos.find({ name: 'video to add' }) |
608 | |||
609 | expect(video).to.exist | 634 | expect(video).to.exist |
610 | }) | 635 | }) |
611 | }) | ||
612 | 636 | ||
613 | after(async function () { | 637 | after(async function () { |
614 | await cleanupTests(servers) | 638 | await cleanupTests(servers) |
639 | }) | ||
615 | }) | 640 | }) |
616 | }) | 641 | }) |
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index aad0d231a..a1bf189fa 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -194,7 +194,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
194 | newConfig: { | 194 | newConfig: { |
195 | transcoding: { | 195 | transcoding: { |
196 | enabled: true, | 196 | enabled: true, |
197 | webtorrent: { | 197 | webVideos: { |
198 | enabled: true | 198 | enabled: true |
199 | }, | 199 | }, |
200 | hls: { | 200 | hls: { |
diff --git a/server/tests/api/transcoding/audio-only.ts b/server/tests/api/transcoding/audio-only.ts index 1e31418e7..f4cc012ef 100644 --- a/server/tests/api/transcoding/audio-only.ts +++ b/server/tests/api/transcoding/audio-only.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | describe('Test audio only video transcoding', function () { | 14 | describe('Test audio only video transcoding', function () { |
15 | let servers: PeerTubeServer[] = [] | 15 | let servers: PeerTubeServer[] = [] |
16 | let videoUUID: string | 16 | let videoUUID: string |
17 | let webtorrentAudioFileUrl: string | 17 | let webVideoAudioFileUrl: string |
18 | let fragmentedAudioFileUrl: string | 18 | let fragmentedAudioFileUrl: string |
19 | 19 | ||
20 | before(async function () { | 20 | before(async function () { |
@@ -37,7 +37,7 @@ describe('Test audio only video transcoding', function () { | |||
37 | hls: { | 37 | hls: { |
38 | enabled: true | 38 | enabled: true |
39 | }, | 39 | }, |
40 | webtorrent: { | 40 | web_videos: { |
41 | enabled: true | 41 | enabled: true |
42 | } | 42 | } |
43 | } | 43 | } |
@@ -71,7 +71,7 @@ describe('Test audio only video transcoding', function () { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | if (server.serverNumber === 1) { | 73 | if (server.serverNumber === 1) { |
74 | webtorrentAudioFileUrl = video.files[2].fileUrl | 74 | webVideoAudioFileUrl = video.files[2].fileUrl |
75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl | 75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl |
76 | } | 76 | } |
77 | } | 77 | } |
@@ -79,7 +79,7 @@ describe('Test audio only video transcoding', function () { | |||
79 | 79 | ||
80 | it('0p transcoded video should not have video', async function () { | 80 | it('0p transcoded video should not have video', async function () { |
81 | const paths = [ | 81 | const paths = [ |
82 | servers[0].servers.buildWebTorrentFilePath(webtorrentAudioFileUrl), | 82 | servers[0].servers.buildWebVideoFilePath(webVideoAudioFileUrl), |
83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) | 83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) |
84 | ] | 84 | ] |
85 | 85 | ||
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts index d6f5b01dc..9a891043c 100644 --- a/server/tests/api/transcoding/create-transcoding.ts +++ b/server/tests/api/transcoding/create-transcoding.ts | |||
@@ -96,12 +96,12 @@ function runTests (enableObjectStorage: boolean) { | |||
96 | } | 96 | } |
97 | }) | 97 | }) |
98 | 98 | ||
99 | it('Should generate WebTorrent', async function () { | 99 | it('Should generate Web Video', async function () { |
100 | this.timeout(60000) | 100 | this.timeout(60000) |
101 | 101 | ||
102 | await servers[0].videos.runTranscoding({ | 102 | await servers[0].videos.runTranscoding({ |
103 | videoId: videoUUID, | 103 | videoId: videoUUID, |
104 | transcodingType: 'webtorrent' | 104 | transcodingType: 'web-video' |
105 | }) | 105 | }) |
106 | 106 | ||
107 | await waitJobs(servers) | 107 | await waitJobs(servers) |
@@ -117,13 +117,13 @@ function runTests (enableObjectStorage: boolean) { | |||
117 | } | 117 | } |
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should generate WebTorrent from HLS only video', async function () { | 120 | it('Should generate Web Video from HLS only video', async function () { |
121 | this.timeout(60000) | 121 | this.timeout(60000) |
122 | 122 | ||
123 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: videoUUID }) | 123 | await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID }) |
124 | await waitJobs(servers) | 124 | await waitJobs(servers) |
125 | 125 | ||
126 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 126 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) |
127 | await waitJobs(servers) | 127 | await waitJobs(servers) |
128 | 128 | ||
129 | for (const server of servers) { | 129 | for (const server of servers) { |
@@ -137,13 +137,13 @@ function runTests (enableObjectStorage: boolean) { | |||
137 | } | 137 | } |
138 | }) | 138 | }) |
139 | 139 | ||
140 | it('Should only generate WebTorrent', async function () { | 140 | it('Should only generate Web Video', async function () { |
141 | this.timeout(60000) | 141 | this.timeout(60000) |
142 | 142 | ||
143 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) | 143 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) |
144 | await waitJobs(servers) | 144 | await waitJobs(servers) |
145 | 145 | ||
146 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | 146 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) |
147 | await waitJobs(servers) | 147 | await waitJobs(servers) |
148 | 148 | ||
149 | for (const server of servers) { | 149 | for (const server of servers) { |
@@ -165,7 +165,7 @@ function runTests (enableObjectStorage: boolean) { | |||
165 | enabled: true, | 165 | enabled: true, |
166 | resolutions: ConfigCommand.getCustomConfigResolutions(false), | 166 | resolutions: ConfigCommand.getCustomConfigResolutions(false), |
167 | 167 | ||
168 | webtorrent: { | 168 | webVideos: { |
169 | enabled: true | 169 | enabled: true |
170 | }, | 170 | }, |
171 | hls: { | 171 | hls: { |
@@ -201,7 +201,7 @@ function runTests (enableObjectStorage: boolean) { | |||
201 | enabled: true, | 201 | enabled: true, |
202 | resolutions: ConfigCommand.getCustomConfigResolutions(true), | 202 | resolutions: ConfigCommand.getCustomConfigResolutions(true), |
203 | 203 | ||
204 | webtorrent: { | 204 | webVideos: { |
205 | enabled: true | 205 | enabled: true |
206 | }, | 206 | }, |
207 | hls: { | 207 | hls: { |
diff --git a/server/tests/api/transcoding/hls.ts b/server/tests/api/transcoding/hls.ts index c668d7e0b..d67043c2a 100644 --- a/server/tests/api/transcoding/hls.ts +++ b/server/tests/api/transcoding/hls.ts | |||
@@ -75,8 +75,8 @@ describe('Test HLS videos', function () { | |||
75 | 75 | ||
76 | it('Should have the playlists/segment deleted from the disk', async function () { | 76 | it('Should have the playlists/segment deleted from the disk', async function () { |
77 | for (const server of servers) { | 77 | for (const server of servers) { |
78 | await checkDirectoryIsEmpty(server, 'videos', [ 'private' ]) | 78 | await checkDirectoryIsEmpty(server, 'web-videos', [ 'private' ]) |
79 | await checkDirectoryIsEmpty(server, join('videos', 'private')) | 79 | await checkDirectoryIsEmpty(server, join('web-videos', 'private')) |
80 | 80 | ||
81 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) | 81 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) |
82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) | 82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) |
@@ -111,7 +111,7 @@ describe('Test HLS videos', function () { | |||
111 | await doubleFollow(servers[0], servers[1]) | 111 | await doubleFollow(servers[0], servers[1]) |
112 | }) | 112 | }) |
113 | 113 | ||
114 | describe('With WebTorrent & HLS enabled', function () { | 114 | describe('With Web Video & HLS enabled', function () { |
115 | runTestSuite(false) | 115 | runTestSuite(false) |
116 | }) | 116 | }) |
117 | 117 | ||
@@ -136,7 +136,7 @@ describe('Test HLS videos', function () { | |||
136 | hls: { | 136 | hls: { |
137 | enabled: true | 137 | enabled: true |
138 | }, | 138 | }, |
139 | webtorrent: { | 139 | webVideos: { |
140 | enabled: false | 140 | enabled: false |
141 | } | 141 | } |
142 | } | 142 | } |
diff --git a/server/tests/api/transcoding/transcoder.ts b/server/tests/api/transcoding/transcoder.ts index 8a0a7f6d2..5386d236f 100644 --- a/server/tests/api/transcoding/transcoder.ts +++ b/server/tests/api/transcoding/transcoder.ts | |||
@@ -31,7 +31,7 @@ function updateConfigForTranscoding (server: PeerTubeServer) { | |||
31 | allowAdditionalExtensions: true, | 31 | allowAdditionalExtensions: true, |
32 | allowAudioFiles: true, | 32 | allowAudioFiles: true, |
33 | hls: { enabled: true }, | 33 | hls: { enabled: true }, |
34 | webtorrent: { enabled: true }, | 34 | webVideos: { enabled: true }, |
35 | resolutions: { | 35 | resolutions: { |
36 | '0p': false, | 36 | '0p': false, |
37 | '144p': true, | 37 | '144p': true, |
@@ -251,7 +251,7 @@ describe('Test video transcoding', function () { | |||
251 | expect(videoDetails.files).to.have.lengthOf(5) | 251 | expect(videoDetails.files).to.have.lengthOf(5) |
252 | 252 | ||
253 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 253 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
254 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 254 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
255 | const probe = await getAudioStream(path) | 255 | const probe = await getAudioStream(path) |
256 | 256 | ||
257 | if (probe.audioStream) { | 257 | if (probe.audioStream) { |
@@ -281,7 +281,7 @@ describe('Test video transcoding', function () { | |||
281 | const videoDetails = await server.videos.get({ id: video.id }) | 281 | const videoDetails = await server.videos.get({ id: video.id }) |
282 | 282 | ||
283 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 283 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
284 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 284 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
285 | 285 | ||
286 | expect(await hasAudioStream(path)).to.be.false | 286 | expect(await hasAudioStream(path)).to.be.false |
287 | } | 287 | } |
@@ -310,7 +310,7 @@ describe('Test video transcoding', function () { | |||
310 | const fixtureVideoProbe = await getAudioStream(fixturePath) | 310 | const fixtureVideoProbe = await getAudioStream(fixturePath) |
311 | 311 | ||
312 | const file = videoDetails.files.find(f => f.resolution.id === 240) | 312 | const file = videoDetails.files.find(f => f.resolution.id === 240) |
313 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 313 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
314 | 314 | ||
315 | const videoProbe = await getAudioStream(path) | 315 | const videoProbe = await getAudioStream(path) |
316 | 316 | ||
@@ -333,7 +333,7 @@ describe('Test video transcoding', function () { | |||
333 | newConfig: { | 333 | newConfig: { |
334 | transcoding: { | 334 | transcoding: { |
335 | hls: { enabled: true }, | 335 | hls: { enabled: true }, |
336 | webtorrent: { enabled: true }, | 336 | webVideos: { enabled: true }, |
337 | resolutions: { | 337 | resolutions: { |
338 | '0p': false, | 338 | '0p': false, |
339 | '144p': false, | 339 | '144p': false, |
@@ -353,7 +353,7 @@ describe('Test video transcoding', function () { | |||
353 | it('Should merge an audio file with the preview file', async function () { | 353 | it('Should merge an audio file with the preview file', async function () { |
354 | this.timeout(60_000) | 354 | this.timeout(60_000) |
355 | 355 | ||
356 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 356 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
357 | await servers[1].videos.upload({ attributes, mode }) | 357 | await servers[1].videos.upload({ attributes, mode }) |
358 | 358 | ||
359 | await waitJobs(servers) | 359 | await waitJobs(servers) |
@@ -405,7 +405,7 @@ describe('Test video transcoding', function () { | |||
405 | newConfig: { | 405 | newConfig: { |
406 | transcoding: { | 406 | transcoding: { |
407 | hls: { enabled: true }, | 407 | hls: { enabled: true }, |
408 | webtorrent: { enabled: true }, | 408 | webVideos: { enabled: true }, |
409 | resolutions: { | 409 | resolutions: { |
410 | '0p': true, | 410 | '0p': true, |
411 | '144p': false, | 411 | '144p': false, |
@@ -416,7 +416,7 @@ describe('Test video transcoding', function () { | |||
416 | } | 416 | } |
417 | }) | 417 | }) |
418 | 418 | ||
419 | const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 419 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } |
420 | const { id } = await servers[1].videos.upload({ attributes, mode }) | 420 | const { id } = await servers[1].videos.upload({ attributes, mode }) |
421 | 421 | ||
422 | await waitJobs(servers) | 422 | await waitJobs(servers) |
@@ -472,14 +472,14 @@ describe('Test video transcoding', function () { | |||
472 | 472 | ||
473 | for (const resolution of [ 144, 240, 360, 480 ]) { | 473 | for (const resolution of [ 144, 240, 360, 480 ]) { |
474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) | 474 | const file = videoDetails.files.find(f => f.resolution.id === resolution) |
475 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 475 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
476 | const fps = await getVideoStreamFPS(path) | 476 | const fps = await getVideoStreamFPS(path) |
477 | 477 | ||
478 | expect(fps).to.be.below(31) | 478 | expect(fps).to.be.below(31) |
479 | } | 479 | } |
480 | 480 | ||
481 | const file = videoDetails.files.find(f => f.resolution.id === 720) | 481 | const file = videoDetails.files.find(f => f.resolution.id === 720) |
482 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 482 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
483 | const fps = await getVideoStreamFPS(path) | 483 | const fps = await getVideoStreamFPS(path) |
484 | 484 | ||
485 | expect(fps).to.be.above(58).and.below(62) | 485 | expect(fps).to.be.above(58).and.below(62) |
@@ -516,14 +516,14 @@ describe('Test video transcoding', function () { | |||
516 | 516 | ||
517 | { | 517 | { |
518 | const file = video.files.find(f => f.resolution.id === 240) | 518 | const file = video.files.find(f => f.resolution.id === 240) |
519 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 519 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
520 | const fps = await getVideoStreamFPS(path) | 520 | const fps = await getVideoStreamFPS(path) |
521 | expect(fps).to.be.equal(25) | 521 | expect(fps).to.be.equal(25) |
522 | } | 522 | } |
523 | 523 | ||
524 | { | 524 | { |
525 | const file = video.files.find(f => f.resolution.id === 720) | 525 | const file = video.files.find(f => f.resolution.id === 720) |
526 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 526 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
527 | const fps = await getVideoStreamFPS(path) | 527 | const fps = await getVideoStreamFPS(path) |
528 | expect(fps).to.be.equal(59) | 528 | expect(fps).to.be.equal(59) |
529 | } | 529 | } |
@@ -556,7 +556,7 @@ describe('Test video transcoding', function () { | |||
556 | 556 | ||
557 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { | 557 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { |
558 | const file = video.files.find(f => f.resolution.id === resolution) | 558 | const file = video.files.find(f => f.resolution.id === resolution) |
559 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 559 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
560 | 560 | ||
561 | const bitrate = await getVideoStreamBitrate(path) | 561 | const bitrate = await getVideoStreamBitrate(path) |
562 | const fps = await getVideoStreamFPS(path) | 562 | const fps = await getVideoStreamFPS(path) |
@@ -586,7 +586,7 @@ describe('Test video transcoding', function () { | |||
586 | '1440p': true, | 586 | '1440p': true, |
587 | '2160p': true | 587 | '2160p': true |
588 | }, | 588 | }, |
589 | webtorrent: { enabled: true }, | 589 | webVideos: { enabled: true }, |
590 | hls: { enabled: true } | 590 | hls: { enabled: true } |
591 | } | 591 | } |
592 | } | 592 | } |
@@ -607,7 +607,7 @@ describe('Test video transcoding', function () { | |||
607 | for (const r of resolutions) { | 607 | for (const r of resolutions) { |
608 | const file = video.files.find(f => f.resolution.id === r) | 608 | const file = video.files.find(f => f.resolution.id === r) |
609 | 609 | ||
610 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 610 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
611 | const bitrate = await getVideoStreamBitrate(path) | 611 | const bitrate = await getVideoStreamBitrate(path) |
612 | 612 | ||
613 | const inputBitrate = 60_000 | 613 | const inputBitrate = 60_000 |
@@ -631,7 +631,7 @@ describe('Test video transcoding', function () { | |||
631 | { | 631 | { |
632 | const video = await servers[1].videos.get({ id: videoUUID }) | 632 | const video = await servers[1].videos.get({ id: videoUUID }) |
633 | const file = video.files.find(f => f.resolution.id === 240) | 633 | const file = video.files.find(f => f.resolution.id === 240) |
634 | const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl) | 634 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) |
635 | 635 | ||
636 | const probe = await ffprobePromise(path) | 636 | const probe = await ffprobePromise(path) |
637 | const metadata = new VideoFileMetadata(probe) | 637 | const metadata = new VideoFileMetadata(probe) |
@@ -704,14 +704,14 @@ describe('Test video transcoding', function () { | |||
704 | expect(transcodingJobs).to.have.lengthOf(16) | 704 | expect(transcodingJobs).to.have.lengthOf(16) |
705 | 705 | ||
706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') | 706 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') |
707 | const webtorrentJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-webtorrent') | 707 | const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video') |
708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-webtorrent') | 708 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video') |
709 | 709 | ||
710 | expect(hlsJobs).to.have.lengthOf(8) | 710 | expect(hlsJobs).to.have.lengthOf(8) |
711 | expect(webtorrentJobs).to.have.lengthOf(7) | 711 | expect(webVideoJobs).to.have.lengthOf(7) |
712 | expect(optimizeJobs).to.have.lengthOf(1) | 712 | expect(optimizeJobs).to.have.lengthOf(1) |
713 | 713 | ||
714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webtorrentJobs))) { | 714 | for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) { |
715 | expect(j.priority).to.be.greaterThan(100) | 715 | expect(j.priority).to.be.greaterThan(100) |
716 | expect(j.priority).to.be.lessThan(150) | 716 | expect(j.priority).to.be.lessThan(150) |
717 | } | 717 | } |
@@ -728,7 +728,7 @@ describe('Test video transcoding', function () { | |||
728 | transcoding: { | 728 | transcoding: { |
729 | enabled: true, | 729 | enabled: true, |
730 | hls: { enabled: true }, | 730 | hls: { enabled: true }, |
731 | webtorrent: { enabled: true }, | 731 | webVideos: { enabled: true }, |
732 | resolutions: { | 732 | resolutions: { |
733 | '0p': false, | 733 | '0p': false, |
734 | '144p': false, | 734 | '144p': false, |
diff --git a/server/tests/api/transcoding/update-while-transcoding.ts b/server/tests/api/transcoding/update-while-transcoding.ts index 61655f102..cfb4fa0cc 100644 --- a/server/tests/api/transcoding/update-while-transcoding.ts +++ b/server/tests/api/transcoding/update-while-transcoding.ts | |||
@@ -96,7 +96,7 @@ describe('Test update video privacy while transcoding', function () { | |||
96 | await doubleFollow(servers[0], servers[1]) | 96 | await doubleFollow(servers[0], servers[1]) |
97 | }) | 97 | }) |
98 | 98 | ||
99 | describe('With WebTorrent & HLS enabled', function () { | 99 | describe('With Web Video & HLS enabled', function () { |
100 | runTestSuite(false) | 100 | runTestSuite(false) |
101 | }) | 101 | }) |
102 | 102 | ||
@@ -121,7 +121,7 @@ describe('Test update video privacy while transcoding', function () { | |||
121 | hls: { | 121 | hls: { |
122 | enabled: true | 122 | enabled: true |
123 | }, | 123 | }, |
124 | webtorrent: { | 124 | webVideos: { |
125 | enabled: false | 125 | enabled: false |
126 | } | 126 | } |
127 | } | 127 | } |
diff --git a/server/tests/api/transcoding/video-studio.ts b/server/tests/api/transcoding/video-studio.ts index d1298caf7..ba68f8e24 100644 --- a/server/tests/api/transcoding/video-studio.ts +++ b/server/tests/api/transcoding/video-studio.ts | |||
@@ -241,7 +241,7 @@ describe('Test video studio', function () { | |||
241 | { | 241 | { |
242 | name: 'add-watermark', | 242 | name: 'add-watermark', |
243 | options: { | 243 | options: { |
244 | file: 'thumbnail.png' | 244 | file: 'custom-thumbnail.png' |
245 | } | 245 | } |
246 | } | 246 | } |
247 | ]) | 247 | ]) |
@@ -273,11 +273,11 @@ describe('Test video studio', function () { | |||
273 | describe('HLS only studio edition', function () { | 273 | describe('HLS only studio edition', function () { |
274 | 274 | ||
275 | before(async function () { | 275 | before(async function () { |
276 | // Disable webtorrent | 276 | // Disable Web Videos |
277 | await servers[0].config.updateExistingSubConfig({ | 277 | await servers[0].config.updateExistingSubConfig({ |
278 | newConfig: { | 278 | newConfig: { |
279 | transcoding: { | 279 | transcoding: { |
280 | webtorrent: { | 280 | webVideos: { |
281 | enabled: false | 281 | enabled: false |
282 | } | 282 | } |
283 | } | 283 | } |
@@ -354,8 +354,8 @@ describe('Test video studio', function () { | |||
354 | expect(oldFileUrls).to.not.include(f.fileUrl) | 354 | expect(oldFileUrls).to.not.include(f.fileUrl) |
355 | } | 355 | } |
356 | 356 | ||
357 | for (const webtorrentFile of video.files) { | 357 | for (const webVideoFile of video.files) { |
358 | expectStartWith(webtorrentFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | 358 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) |
359 | } | 359 | } |
360 | 360 | ||
361 | for (const hlsFile of video.streamingPlaylists[0].files) { | 361 | for (const hlsFile of video.streamingPlaylists[0].files) { |
diff --git a/server/tests/api/users/user-videos.ts b/server/tests/api/users/user-videos.ts index 696949504..77226e48e 100644 --- a/server/tests/api/users/user-videos.ts +++ b/server/tests/api/users/user-videos.ts | |||
@@ -184,12 +184,12 @@ describe('Test user videos', function () { | |||
184 | } | 184 | } |
185 | }) | 185 | }) |
186 | 186 | ||
187 | it('Should disable webtorrent, enable HLS, and update my quota', async function () { | 187 | it('Should disable web videos, enable HLS, and update my quota', async function () { |
188 | this.timeout(160000) | 188 | this.timeout(160000) |
189 | 189 | ||
190 | { | 190 | { |
191 | const config = await server.config.getCustomConfig() | 191 | const config = await server.config.getCustomConfig() |
192 | config.transcoding.webtorrent.enabled = false | 192 | config.transcoding.webVideos.enabled = false |
193 | config.transcoding.hls.enabled = true | 193 | config.transcoding.hls.enabled = true |
194 | config.transcoding.enabled = true | 194 | config.transcoding.enabled = true |
195 | await server.config.updateCustomSubConfig({ newConfig: config }) | 195 | await server.config.updateCustomSubConfig({ newConfig: config }) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 1c00f9a93..67ade1d0d 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -229,25 +229,13 @@ describe('Test users', function () { | |||
229 | }) | 229 | }) |
230 | 230 | ||
231 | it('Should be able to change the p2p attribute', async function () { | 231 | it('Should be able to change the p2p attribute', async function () { |
232 | { | 232 | await server.users.updateMe({ |
233 | await server.users.updateMe({ | 233 | token: userToken, |
234 | token: userToken, | 234 | p2pEnabled: true |
235 | webTorrentEnabled: false | 235 | }) |
236 | }) | ||
237 | |||
238 | const user = await server.users.getMyInfo({ token: userToken }) | ||
239 | expect(user.p2pEnabled).to.be.false | ||
240 | } | ||
241 | |||
242 | { | ||
243 | await server.users.updateMe({ | ||
244 | token: userToken, | ||
245 | p2pEnabled: true | ||
246 | }) | ||
247 | 236 | ||
248 | const user = await server.users.getMyInfo({ token: userToken }) | 237 | const user = await server.users.getMyInfo({ token: userToken }) |
249 | expect(user.p2pEnabled).to.be.true | 238 | expect(user.p2pEnabled).to.be.true |
250 | } | ||
251 | }) | 239 | }) |
252 | 240 | ||
253 | it('Should be able to change the email attribute', async function () { | 241 | it('Should be able to change the email attribute', async function () { |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 357c08199..9c79b3aa6 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -20,3 +20,4 @@ import './videos-history' | |||
20 | import './videos-overview' | 20 | import './videos-overview' |
21 | import './video-source' | 21 | import './video-source' |
22 | import './video-static-file-privacy' | 22 | import './video-static-file-privacy' |
23 | import './video-storyboard' | ||
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 27ba00d3d..e9aa0e3a1 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | completeVideoCheck, | 9 | completeVideoCheck, |
10 | dateIsValid, | 10 | dateIsValid, |
11 | saveVideoInServers, | 11 | saveVideoInServers, |
12 | testImage | 12 | testImageGeneratedByFFmpeg |
13 | } from '@server/tests/shared' | 13 | } from '@server/tests/shared' |
14 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | 14 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' |
15 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@shared/models' | 15 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@shared/models' |
@@ -70,8 +70,9 @@ describe('Test multiple servers', function () { | |||
70 | }) | 70 | }) |
71 | 71 | ||
72 | describe('Should upload the video and propagate on each server', function () { | 72 | describe('Should upload the video and propagate on each server', function () { |
73 | |||
73 | it('Should upload the video on server 1 and propagate on each server', async function () { | 74 | it('Should upload the video on server 1 and propagate on each server', async function () { |
74 | this.timeout(25000) | 75 | this.timeout(60000) |
75 | 76 | ||
76 | const attributes = { | 77 | const attributes = { |
77 | name: 'my super name for server 1', | 78 | name: 'my super name for server 1', |
@@ -175,8 +176,8 @@ describe('Test multiple servers', function () { | |||
175 | support: 'my super support text for server 2', | 176 | support: 'my super support text for server 2', |
176 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], | 177 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], |
177 | fixture: 'video_short2.webm', | 178 | fixture: 'video_short2.webm', |
178 | thumbnailfile: 'thumbnail.jpg', | 179 | thumbnailfile: 'custom-thumbnail.jpg', |
179 | previewfile: 'preview.jpg' | 180 | previewfile: 'custom-preview.jpg' |
180 | } | 181 | } |
181 | await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' }) | 182 | await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' }) |
182 | 183 | ||
@@ -229,8 +230,8 @@ describe('Test multiple servers', function () { | |||
229 | size: 750000 | 230 | size: 750000 |
230 | } | 231 | } |
231 | ], | 232 | ], |
232 | thumbnailfile: 'thumbnail', | 233 | thumbnailfile: 'custom-thumbnail', |
233 | previewfile: 'preview' | 234 | previewfile: 'custom-preview' |
234 | } | 235 | } |
235 | 236 | ||
236 | const { data } = await server.videos.list() | 237 | const { data } = await server.videos.list() |
@@ -619,9 +620,9 @@ describe('Test multiple servers', function () { | |||
619 | description: 'my super description updated', | 620 | description: 'my super description updated', |
620 | support: 'my super support text updated', | 621 | support: 'my super support text updated', |
621 | tags: [ 'tag_up_1', 'tag_up_2' ], | 622 | tags: [ 'tag_up_1', 'tag_up_2' ], |
622 | thumbnailfile: 'thumbnail.jpg', | 623 | thumbnailfile: 'custom-thumbnail.jpg', |
623 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', | 624 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', |
624 | previewfile: 'preview.jpg' | 625 | previewfile: 'custom-preview.jpg' |
625 | } | 626 | } |
626 | 627 | ||
627 | updatedAtMin = new Date() | 628 | updatedAtMin = new Date() |
@@ -674,8 +675,8 @@ describe('Test multiple servers', function () { | |||
674 | size: 292677 | 675 | size: 292677 |
675 | } | 676 | } |
676 | ], | 677 | ], |
677 | thumbnailfile: 'thumbnail', | 678 | thumbnailfile: 'custom-thumbnail', |
678 | previewfile: 'preview' | 679 | previewfile: 'custom-preview' |
679 | } | 680 | } |
680 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes }) | 681 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes }) |
681 | } | 682 | } |
@@ -685,7 +686,7 @@ describe('Test multiple servers', function () { | |||
685 | this.timeout(30000) | 686 | this.timeout(30000) |
686 | 687 | ||
687 | const attributes = { | 688 | const attributes = { |
688 | thumbnailfile: 'thumbnail.jpg' | 689 | thumbnailfile: 'custom-thumbnail.jpg' |
689 | } | 690 | } |
690 | 691 | ||
691 | updatedAtMin = new Date() | 692 | updatedAtMin = new Date() |
@@ -761,7 +762,7 @@ describe('Test multiple servers', function () { | |||
761 | for (const server of servers) { | 762 | for (const server of servers) { |
762 | const video = await server.videos.get({ id: videoUUID }) | 763 | const video = await server.videos.get({ id: videoUUID }) |
763 | 764 | ||
764 | await testImage(server.url, 'video_short1-preview.webm', video.previewPath) | 765 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) |
765 | } | 766 | } |
766 | }) | 767 | }) |
767 | }) | 768 | }) |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts index 2fbefb392..91eb61833 100644 --- a/server/tests/api/videos/resumable-upload.ts +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -93,10 +93,10 @@ describe('Test resumable upload', function () { | |||
93 | expect((await stat(filePath)).size).to.equal(expectedSize) | 93 | expect((await stat(filePath)).size).to.equal(expectedSize) |
94 | } | 94 | } |
95 | 95 | ||
96 | async function countResumableUploads () { | 96 | async function countResumableUploads (wait?: number) { |
97 | const subPath = join('tmp', 'resumable-uploads') | 97 | const subPath = join('tmp', 'resumable-uploads') |
98 | const filePath = server.servers.buildDirectory(subPath) | 98 | const filePath = server.servers.buildDirectory(subPath) |
99 | 99 | await new Promise(resolve => setTimeout(resolve, wait)) | |
100 | const files = await readdir(filePath) | 100 | const files = await readdir(filePath) |
101 | return files.length | 101 | return files.length |
102 | } | 102 | } |
@@ -122,14 +122,20 @@ describe('Test resumable upload', function () { | |||
122 | 122 | ||
123 | describe('Directory cleaning', function () { | 123 | describe('Directory cleaning', function () { |
124 | 124 | ||
125 | // FIXME: https://github.com/kukhariev/node-uploadx/pull/524/files#r852989382 | 125 | it('Should correctly delete files after an upload', async function () { |
126 | // it('Should correctly delete files after an upload', async function () { | 126 | const uploadId = await prepareUpload() |
127 | // const uploadId = await prepareUpload() | 127 | await sendChunks({ pathUploadId: uploadId }) |
128 | // await sendChunks({ pathUploadId: uploadId }) | 128 | await server.videos.endResumableUpload({ pathUploadId: uploadId }) |
129 | // await server.videos.endResumableUpload({ pathUploadId: uploadId }) | 129 | |
130 | expect(await countResumableUploads()).to.equal(0) | ||
131 | }) | ||
132 | |||
133 | it('Should correctly delete corrupt files', async function () { | ||
134 | const uploadId = await prepareUpload({ size: 8 * 1024 }) | ||
135 | await sendChunks({ pathUploadId: uploadId, size: 8 * 1024, expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 }) | ||
130 | 136 | ||
131 | // expect(await countResumableUploads()).to.equal(0) | 137 | expect(await countResumableUploads(2000)).to.equal(0) |
132 | // }) | 138 | }) |
133 | 139 | ||
134 | it('Should not delete files after an unfinished upload', async function () { | 140 | it('Should not delete files after an unfinished upload', async function () { |
135 | await prepareUpload() | 141 | await prepareUpload() |
@@ -254,6 +260,24 @@ describe('Test resumable upload', function () { | |||
254 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | 260 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist |
255 | }) | 261 | }) |
256 | 262 | ||
263 | it('Should not cache after video deletion', async function () { | ||
264 | const originalName = 'toto.mp4' | ||
265 | const lastModified = new Date().getTime() | ||
266 | |||
267 | const uploadId1 = await prepareUpload({ originalName, lastModified }) | ||
268 | const result1 = await sendChunks({ pathUploadId: uploadId1 }) | ||
269 | await server.videos.remove({ id: result1.body.video.uuid }) | ||
270 | |||
271 | const uploadId2 = await prepareUpload({ originalName, lastModified }) | ||
272 | const result2 = await sendChunks({ pathUploadId: uploadId2 }) | ||
273 | expect(result1.body.video.uuid).to.not.equal(result2.body.video.uuid) | ||
274 | |||
275 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | ||
276 | |||
277 | await checkFileSize(uploadId1, null) | ||
278 | await checkFileSize(uploadId2, null) | ||
279 | }) | ||
280 | |||
257 | it('Should refuse an invalid digest', async function () { | 281 | it('Should refuse an invalid digest', async function () { |
258 | const uploadId = await prepareUpload({ token: server.accessToken }) | 282 | const uploadId = await prepareUpload({ token: server.accessToken }) |
259 | 283 | ||
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 0cb64d5a5..66414aa5b 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' | 4 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { Video, VideoPrivacy } from '@shared/models' | 6 | import { Video, VideoPrivacy } from '@shared/models' |
7 | import { | 7 | import { |
@@ -260,7 +260,7 @@ describe('Test a single server', function () { | |||
260 | 260 | ||
261 | for (const video of data) { | 261 | for (const video of data) { |
262 | const videoName = video.name.replace(' name', '') | 262 | const videoName = video.name.replace(' name', '') |
263 | await testImage(server.url, videoName, video.thumbnailPath) | 263 | await testImageGeneratedByFFmpeg(server.url, videoName, video.thumbnailPath) |
264 | } | 264 | } |
265 | }) | 265 | }) |
266 | 266 | ||
diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts index 8c913bf31..0a183c44d 100644 --- a/server/tests/api/videos/video-files.ts +++ b/server/tests/api/videos/video-files.ts | |||
@@ -48,10 +48,10 @@ describe('Test videos files', function () { | |||
48 | await waitJobs(servers) | 48 | await waitJobs(servers) |
49 | }) | 49 | }) |
50 | 50 | ||
51 | it('Should delete webtorrent files', async function () { | 51 | it('Should delete web video files', async function () { |
52 | this.timeout(30_000) | 52 | this.timeout(30_000) |
53 | 53 | ||
54 | await servers[0].videos.removeAllWebTorrentFiles({ videoId: validId1 }) | 54 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1 }) |
55 | 55 | ||
56 | await waitJobs(servers) | 56 | await waitJobs(servers) |
57 | 57 | ||
@@ -80,15 +80,15 @@ describe('Test videos files', function () { | |||
80 | }) | 80 | }) |
81 | 81 | ||
82 | describe('When deleting a specific file', function () { | 82 | describe('When deleting a specific file', function () { |
83 | let webtorrentId: string | 83 | let webVideoId: string |
84 | let hlsId: string | 84 | let hlsId: string |
85 | 85 | ||
86 | before(async function () { | 86 | before(async function () { |
87 | this.timeout(120_000) | 87 | this.timeout(120_000) |
88 | 88 | ||
89 | { | 89 | { |
90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' }) | 90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) |
91 | webtorrentId = uuid | 91 | webVideoId = uuid |
92 | } | 92 | } |
93 | 93 | ||
94 | { | 94 | { |
@@ -99,38 +99,38 @@ describe('Test videos files', function () { | |||
99 | await waitJobs(servers) | 99 | await waitJobs(servers) |
100 | }) | 100 | }) |
101 | 101 | ||
102 | it('Shoulde delete a webtorrent file', async function () { | 102 | it('Shoulde delete a web video file', async function () { |
103 | this.timeout(30_000) | 103 | this.timeout(30_000) |
104 | 104 | ||
105 | const video = await servers[0].videos.get({ id: webtorrentId }) | 105 | const video = await servers[0].videos.get({ id: webVideoId }) |
106 | const files = video.files | 106 | const files = video.files |
107 | 107 | ||
108 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: files[0].id }) | 108 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id }) |
109 | 109 | ||
110 | await waitJobs(servers) | 110 | await waitJobs(servers) |
111 | 111 | ||
112 | for (const server of servers) { | 112 | for (const server of servers) { |
113 | const video = await server.videos.get({ id: webtorrentId }) | 113 | const video = await server.videos.get({ id: webVideoId }) |
114 | 114 | ||
115 | expect(video.files).to.have.lengthOf(files.length - 1) | 115 | expect(video.files).to.have.lengthOf(files.length - 1) |
116 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist | 116 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist |
117 | } | 117 | } |
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should delete all webtorrent files', async function () { | 120 | it('Should delete all web video files', async function () { |
121 | this.timeout(30_000) | 121 | this.timeout(30_000) |
122 | 122 | ||
123 | const video = await servers[0].videos.get({ id: webtorrentId }) | 123 | const video = await servers[0].videos.get({ id: webVideoId }) |
124 | const files = video.files | 124 | const files = video.files |
125 | 125 | ||
126 | for (const file of files) { | 126 | for (const file of files) { |
127 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentId, fileId: file.id }) | 127 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: file.id }) |
128 | } | 128 | } |
129 | 129 | ||
130 | await waitJobs(servers) | 130 | await waitJobs(servers) |
131 | 131 | ||
132 | for (const server of servers) { | 132 | for (const server of servers) { |
133 | const video = await server.videos.get({ id: webtorrentId }) | 133 | const video = await server.videos.get({ id: webVideoId }) |
134 | 134 | ||
135 | expect(video.files).to.have.lengthOf(0) | 135 | expect(video.files).to.have.lengthOf(0) |
136 | } | 136 | } |
@@ -182,16 +182,16 @@ describe('Test videos files', function () { | |||
182 | it('Should not delete last file of a video', async function () { | 182 | it('Should not delete last file of a video', async function () { |
183 | this.timeout(60_000) | 183 | this.timeout(60_000) |
184 | 184 | ||
185 | const webtorrentOnly = await servers[0].videos.get({ id: hlsId }) | 185 | const webVideoOnly = await servers[0].videos.get({ id: hlsId }) |
186 | const hlsOnly = await servers[0].videos.get({ id: webtorrentId }) | 186 | const hlsOnly = await servers[0].videos.get({ id: webVideoId }) |
187 | 187 | ||
188 | for (let i = 0; i < 4; i++) { | 188 | for (let i = 0; i < 4; i++) { |
189 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[i].id }) | 189 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[i].id }) |
190 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) | 190 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) |
191 | } | 191 | } |
192 | 192 | ||
193 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | 193 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 |
194 | await servers[0].videos.removeWebTorrentFile({ videoId: webtorrentOnly.id, fileId: webtorrentOnly.files[4].id, expectedStatus }) | 194 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[4].id, expectedStatus }) |
195 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) | 195 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) |
196 | }) | 196 | }) |
197 | }) | 197 | }) |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index 192b2aeb9..b78b4f344 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { pathExists, readdir, remove } from 'fs-extra' | 4 | import { pathExists, readdir, remove } from 'fs-extra' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared' | 6 | import { FIXTURE_URLS, testCaptionFile, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' | 7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' |
8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models' | 8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models' |
9 | import { | 9 | import { |
@@ -67,7 +67,7 @@ async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { | |||
67 | expect(video.description).to.equal('my super description') | 67 | expect(video.description).to.equal('my super description') |
68 | expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) | 68 | expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) |
69 | 69 | ||
70 | await testImage(server.url, 'thumbnail', video.thumbnailPath) | 70 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', video.thumbnailPath) |
71 | 71 | ||
72 | expect(video.files).to.have.lengthOf(1) | 72 | expect(video.files).to.have.lengthOf(1) |
73 | 73 | ||
@@ -119,15 +119,15 @@ describe('Test video imports', function () { | |||
119 | expect(video.name).to.equal('small video - youtube') | 119 | expect(video.name).to.equal('small video - youtube') |
120 | 120 | ||
121 | { | 121 | { |
122 | expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) | 122 | expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`)) |
123 | expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) | 123 | expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) |
124 | 124 | ||
125 | const suffix = mode === 'yt-dlp' | 125 | const suffix = mode === 'yt-dlp' |
126 | ? '_yt_dlp' | 126 | ? '_yt_dlp' |
127 | : '' | 127 | : '' |
128 | 128 | ||
129 | await testImage(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) | 129 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) |
130 | await testImage(servers[0].url, 'video_import_preview' + suffix, video.previewPath) | 130 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_preview' + suffix, video.previewPath) |
131 | } | 131 | } |
132 | 132 | ||
133 | const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) | 133 | const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) |
@@ -266,7 +266,7 @@ describe('Test video imports', function () { | |||
266 | name: 'my super name', | 266 | name: 'my super name', |
267 | description: 'my super description', | 267 | description: 'my super description', |
268 | tags: [ 'supertag1', 'supertag2' ], | 268 | tags: [ 'supertag1', 'supertag2' ], |
269 | thumbnailfile: 'thumbnail.jpg' | 269 | thumbnailfile: 'custom-thumbnail.jpg' |
270 | } | 270 | } |
271 | }) | 271 | }) |
272 | expect(video.name).to.equal('my super name') | 272 | expect(video.name).to.equal('my super name') |
@@ -328,7 +328,7 @@ describe('Test video imports', function () { | |||
328 | '1440p': false, | 328 | '1440p': false, |
329 | '2160p': false | 329 | '2160p': false |
330 | }, | 330 | }, |
331 | webtorrent: { enabled: true }, | 331 | webVideos: { enabled: true }, |
332 | hls: { enabled: false } | 332 | hls: { enabled: false } |
333 | } | 333 | } |
334 | } | 334 | } |
diff --git a/server/tests/api/videos/video-passwords.ts b/server/tests/api/videos/video-passwords.ts new file mode 100644 index 000000000..e01a93a4d --- /dev/null +++ b/server/tests/api/videos/video-passwords.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | VideoPasswordsCommand, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | ||
13 | import { VideoPrivacy } from '@shared/models' | ||
14 | |||
15 | describe('Test video passwords', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoUUID: string | ||
18 | |||
19 | let userAccessTokenServer1: string | ||
20 | |||
21 | let videoPasswords: string[] = [] | ||
22 | let command: VideoPasswordsCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | |||
31 | for (let i = 0; i < 10; i++) { | ||
32 | videoPasswords.push(`password ${i + 1}`) | ||
33 | } | ||
34 | const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } }) | ||
35 | videoUUID = uuid | ||
36 | |||
37 | await setDefaultChannelAvatar(server) | ||
38 | await setDefaultAccountAvatar(server) | ||
39 | |||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | ||
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
43 | |||
44 | command = server.videoPasswords | ||
45 | }) | ||
46 | |||
47 | it('Should list video passwords', async function () { | ||
48 | const body = await command.list({ videoId: videoUUID }) | ||
49 | |||
50 | expect(body.total).to.equal(10) | ||
51 | expect(body.data).to.be.an('array') | ||
52 | expect(body.data).to.have.lengthOf(10) | ||
53 | }) | ||
54 | |||
55 | it('Should filter passwords on this video', async function () { | ||
56 | const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' }) | ||
57 | |||
58 | expect(body.total).to.equal(10) | ||
59 | expect(body.data).to.be.an('array') | ||
60 | expect(body.data).to.have.lengthOf(2) | ||
61 | expect(body.data[0].password).to.equal('password 4') | ||
62 | expect(body.data[1].password).to.equal('password 5') | ||
63 | }) | ||
64 | |||
65 | it('Should update password for this video', async function () { | ||
66 | videoPasswords = [ 'my super new password 1', 'my super new password 2' ] | ||
67 | |||
68 | await command.updateAll({ videoId: videoUUID, passwords: videoPasswords }) | ||
69 | const body = await command.list({ videoId: videoUUID }) | ||
70 | expect(body.total).to.equal(2) | ||
71 | expect(body.data).to.be.an('array') | ||
72 | expect(body.data).to.have.lengthOf(2) | ||
73 | expect(body.data[0].password).to.equal('my super new password 2') | ||
74 | expect(body.data[1].password).to.equal('my super new password 1') | ||
75 | }) | ||
76 | |||
77 | it('Should delete one password', async function () { | ||
78 | { | ||
79 | const body = await command.list({ videoId: videoUUID }) | ||
80 | expect(body.total).to.equal(2) | ||
81 | expect(body.data).to.be.an('array') | ||
82 | expect(body.data).to.have.lengthOf(2) | ||
83 | await command.remove({ id: body.data[0].id, videoId: videoUUID }) | ||
84 | } | ||
85 | { | ||
86 | const body = await command.list({ videoId: videoUUID }) | ||
87 | |||
88 | expect(body.total).to.equal(1) | ||
89 | expect(body.data).to.be.an('array') | ||
90 | expect(body.data).to.have.lengthOf(1) | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | after(async function () { | ||
95 | await cleanupTests([ server ]) | ||
96 | }) | ||
97 | }) | ||
diff --git a/server/tests/api/videos/video-playlist-thumbnails.ts b/server/tests/api/videos/video-playlist-thumbnails.ts index 356939b93..c274c20bf 100644 --- a/server/tests/api/videos/video-playlist-thumbnails.ts +++ b/server/tests/api/videos/video-playlist-thumbnails.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { testImage } from '@server/tests/shared' | 4 | import { testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { VideoPlaylistPrivacy } from '@shared/models' | 5 | import { VideoPlaylistPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
@@ -83,7 +83,7 @@ describe('Playlist thumbnail', function () { | |||
83 | 83 | ||
84 | for (const server of servers) { | 84 | for (const server of servers) { |
85 | const p = await getPlaylistWithoutThumbnail(server) | 85 | const p = await getPlaylistWithoutThumbnail(server) |
86 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 86 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
87 | } | 87 | } |
88 | }) | 88 | }) |
89 | 89 | ||
@@ -95,7 +95,7 @@ describe('Playlist thumbnail', function () { | |||
95 | displayName: 'playlist with thumbnail', | 95 | displayName: 'playlist with thumbnail', |
96 | privacy: VideoPlaylistPrivacy.PUBLIC, | 96 | privacy: VideoPlaylistPrivacy.PUBLIC, |
97 | videoChannelId: servers[1].store.channel.id, | 97 | videoChannelId: servers[1].store.channel.id, |
98 | thumbnailfile: 'thumbnail.jpg' | 98 | thumbnailfile: 'custom-thumbnail.jpg' |
99 | } | 99 | } |
100 | }) | 100 | }) |
101 | playlistWithThumbnailId = created.id | 101 | playlistWithThumbnailId = created.id |
@@ -110,7 +110,7 @@ describe('Playlist thumbnail', function () { | |||
110 | 110 | ||
111 | for (const server of servers) { | 111 | for (const server of servers) { |
112 | const p = await getPlaylistWithThumbnail(server) | 112 | const p = await getPlaylistWithThumbnail(server) |
113 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 113 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
114 | } | 114 | } |
115 | }) | 115 | }) |
116 | 116 | ||
@@ -135,7 +135,7 @@ describe('Playlist thumbnail', function () { | |||
135 | 135 | ||
136 | for (const server of servers) { | 136 | for (const server of servers) { |
137 | const p = await getPlaylistWithoutThumbnail(server) | 137 | const p = await getPlaylistWithoutThumbnail(server) |
138 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 138 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
139 | } | 139 | } |
140 | }) | 140 | }) |
141 | 141 | ||
@@ -160,7 +160,7 @@ describe('Playlist thumbnail', function () { | |||
160 | 160 | ||
161 | for (const server of servers) { | 161 | for (const server of servers) { |
162 | const p = await getPlaylistWithThumbnail(server) | 162 | const p = await getPlaylistWithThumbnail(server) |
163 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 163 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
164 | } | 164 | } |
165 | }) | 165 | }) |
166 | 166 | ||
@@ -176,7 +176,7 @@ describe('Playlist thumbnail', function () { | |||
176 | 176 | ||
177 | for (const server of servers) { | 177 | for (const server of servers) { |
178 | const p = await getPlaylistWithoutThumbnail(server) | 178 | const p = await getPlaylistWithoutThumbnail(server) |
179 | await testImage(server.url, 'thumbnail-playlist', p.thumbnailPath) | 179 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) |
180 | } | 180 | } |
181 | }) | 181 | }) |
182 | 182 | ||
@@ -192,7 +192,7 @@ describe('Playlist thumbnail', function () { | |||
192 | 192 | ||
193 | for (const server of servers) { | 193 | for (const server of servers) { |
194 | const p = await getPlaylistWithThumbnail(server) | 194 | const p = await getPlaylistWithThumbnail(server) |
195 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 195 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
196 | } | 196 | } |
197 | }) | 197 | }) |
198 | 198 | ||
@@ -224,7 +224,7 @@ describe('Playlist thumbnail', function () { | |||
224 | 224 | ||
225 | for (const server of servers) { | 225 | for (const server of servers) { |
226 | const p = await getPlaylistWithThumbnail(server) | 226 | const p = await getPlaylistWithThumbnail(server) |
227 | await testImage(server.url, 'thumbnail', p.thumbnailPath) | 227 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) |
228 | } | 228 | } |
229 | }) | 229 | }) |
230 | 230 | ||
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index d9c5bdf16..3bfa874cb 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { checkPlaylistFilesWereRemoved, testImage } from '@server/tests/shared' | 4 | import { checkPlaylistFilesWereRemoved, testImageGeneratedByFFmpeg } from '@server/tests/shared' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { uuidToShort } from '@shared/extra-utils' | 6 | import { uuidToShort } from '@shared/extra-utils' |
7 | import { | 7 | import { |
@@ -133,7 +133,7 @@ describe('Test video playlists', function () { | |||
133 | displayName: 'my super playlist', | 133 | displayName: 'my super playlist', |
134 | privacy: VideoPlaylistPrivacy.PUBLIC, | 134 | privacy: VideoPlaylistPrivacy.PUBLIC, |
135 | description: 'my super description', | 135 | description: 'my super description', |
136 | thumbnailfile: 'thumbnail.jpg', | 136 | thumbnailfile: 'custom-thumbnail.jpg', |
137 | videoChannelId: servers[0].store.channel.id | 137 | videoChannelId: servers[0].store.channel.id |
138 | } | 138 | } |
139 | }) | 139 | }) |
@@ -225,7 +225,7 @@ describe('Test video playlists', function () { | |||
225 | displayName: 'my super playlist', | 225 | displayName: 'my super playlist', |
226 | privacy: VideoPlaylistPrivacy.PUBLIC, | 226 | privacy: VideoPlaylistPrivacy.PUBLIC, |
227 | description: 'my super description', | 227 | description: 'my super description', |
228 | thumbnailfile: 'thumbnail.jpg', | 228 | thumbnailfile: 'custom-thumbnail.jpg', |
229 | videoChannelId: servers[0].store.channel.id | 229 | videoChannelId: servers[0].store.channel.id |
230 | } | 230 | } |
231 | }) | 231 | }) |
@@ -286,7 +286,7 @@ describe('Test video playlists', function () { | |||
286 | attributes: { | 286 | attributes: { |
287 | displayName: 'playlist 3', | 287 | displayName: 'playlist 3', |
288 | privacy: VideoPlaylistPrivacy.PUBLIC, | 288 | privacy: VideoPlaylistPrivacy.PUBLIC, |
289 | thumbnailfile: 'thumbnail.jpg', | 289 | thumbnailfile: 'custom-thumbnail.jpg', |
290 | videoChannelId: servers[1].store.channel.id | 290 | videoChannelId: servers[1].store.channel.id |
291 | } | 291 | } |
292 | }) | 292 | }) |
@@ -314,11 +314,11 @@ describe('Test video playlists', function () { | |||
314 | 314 | ||
315 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | 315 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') |
316 | expect(playlist2).to.not.be.undefined | 316 | expect(playlist2).to.not.be.undefined |
317 | await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) | 317 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) |
318 | 318 | ||
319 | const playlist3 = body.data.find(p => p.displayName === 'playlist 3') | 319 | const playlist3 = body.data.find(p => p.displayName === 'playlist 3') |
320 | expect(playlist3).to.not.be.undefined | 320 | expect(playlist3).to.not.be.undefined |
321 | await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) | 321 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', playlist3.thumbnailPath) |
322 | } | 322 | } |
323 | 323 | ||
324 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | 324 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) |
@@ -336,7 +336,7 @@ describe('Test video playlists', function () { | |||
336 | 336 | ||
337 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | 337 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') |
338 | expect(playlist2).to.not.be.undefined | 338 | expect(playlist2).to.not.be.undefined |
339 | await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) | 339 | await testImageGeneratedByFFmpeg(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) |
340 | 340 | ||
341 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined | 341 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined |
342 | }) | 342 | }) |
@@ -474,7 +474,7 @@ describe('Test video playlists', function () { | |||
474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) | 474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) |
475 | }) | 475 | }) |
476 | 476 | ||
477 | it('Should get unlisted plyaylist using uuid or shortUUID', async function () { | 477 | it('Should get unlisted playlist using uuid or shortUUID', async function () { |
478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) | 478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) |
479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) | 479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) |
480 | }) | 480 | }) |
@@ -502,7 +502,7 @@ describe('Test video playlists', function () { | |||
502 | displayName: 'playlist 3 updated', | 502 | displayName: 'playlist 3 updated', |
503 | description: 'description updated', | 503 | description: 'description updated', |
504 | privacy: VideoPlaylistPrivacy.UNLISTED, | 504 | privacy: VideoPlaylistPrivacy.UNLISTED, |
505 | thumbnailfile: 'thumbnail.jpg', | 505 | thumbnailfile: 'custom-thumbnail.jpg', |
506 | videoChannelId: servers[1].store.channel.id | 506 | videoChannelId: servers[1].store.channel.id |
507 | }, | 507 | }, |
508 | playlistId: playlistServer2Id2 | 508 | playlistId: playlistServer2Id2 |
@@ -686,7 +686,7 @@ describe('Test video playlists', function () { | |||
686 | await waitJobs(servers) | 686 | await waitJobs(servers) |
687 | }) | 687 | }) |
688 | 688 | ||
689 | it('Should update the element type if the video is private', async function () { | 689 | it('Should update the element type if the video is private/password protected', async function () { |
690 | this.timeout(20000) | 690 | this.timeout(20000) |
691 | 691 | ||
692 | const name = 'video 89' | 692 | const name = 'video 89' |
@@ -703,6 +703,19 @@ describe('Test video playlists', function () { | |||
703 | } | 703 | } |
704 | 704 | ||
705 | { | 705 | { |
706 | await servers[0].videos.update({ | ||
707 | id: video1, | ||
708 | attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
709 | }) | ||
710 | await waitJobs(servers) | ||
711 | |||
712 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
713 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
714 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
715 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
716 | } | ||
717 | |||
718 | { | ||
706 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) | 719 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) |
707 | await waitJobs(servers) | 720 | await waitJobs(servers) |
708 | 721 | ||
diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts index 542848533..0a9864134 100644 --- a/server/tests/api/videos/video-static-file-privacy.ts +++ b/server/tests/api/videos/video-static-file-privacy.ts | |||
@@ -41,7 +41,7 @@ describe('Test video static file privacy', function () { | |||
41 | 41 | ||
42 | for (const file of video.files) { | 42 | for (const file of video.files) { |
43 | expect(file.fileDownloadUrl).to.not.include('/private/') | 43 | expect(file.fileDownloadUrl).to.not.include('/private/') |
44 | expectStartWith(file.fileUrl, server.url + '/static/webseed/private/') | 44 | expectStartWith(file.fileUrl, server.url + '/static/web-videos/private/') |
45 | 45 | ||
46 | const torrent = await parseTorrentVideo(server, file) | 46 | const torrent = await parseTorrentVideo(server, file) |
47 | expect(torrent.urlList).to.have.lengthOf(0) | 47 | expect(torrent.urlList).to.have.lengthOf(0) |
@@ -90,7 +90,7 @@ describe('Test video static file privacy', function () { | |||
90 | } | 90 | } |
91 | } | 91 | } |
92 | 92 | ||
93 | it('Should upload a private/internal video and have a private static path', async function () { | 93 | it('Should upload a private/internal/password protected video and have a private static path', async function () { |
94 | this.timeout(120000) | 94 | this.timeout(120000) |
95 | 95 | ||
96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | 96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { |
@@ -99,6 +99,15 @@ describe('Test video static file privacy', function () { | |||
99 | 99 | ||
100 | await checkPrivateFiles(uuid) | 100 | await checkPrivateFiles(uuid) |
101 | } | 101 | } |
102 | |||
103 | const { uuid } = await server.videos.quickUpload({ | ||
104 | name: 'video', | ||
105 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
106 | videoPasswords: [ 'my super password' ] | ||
107 | }) | ||
108 | await waitJobs([ server ]) | ||
109 | |||
110 | await checkPrivateFiles(uuid) | ||
102 | }) | 111 | }) |
103 | 112 | ||
104 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { | 113 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { |
@@ -185,8 +194,9 @@ describe('Test video static file privacy', function () { | |||
185 | expectedStatus: HttpStatusCode | 194 | expectedStatus: HttpStatusCode |
186 | token: string | 195 | token: string |
187 | videoFileToken: string | 196 | videoFileToken: string |
197 | videoPassword?: string | ||
188 | }) { | 198 | }) { |
189 | const { id, expectedStatus, token, videoFileToken } = options | 199 | const { id, expectedStatus, token, videoFileToken, videoPassword } = options |
190 | 200 | ||
191 | const video = await server.videos.getWithToken({ id }) | 201 | const video = await server.videos.getWithToken({ id }) |
192 | 202 | ||
@@ -196,6 +206,12 @@ describe('Test video static file privacy', function () { | |||
196 | 206 | ||
197 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) | 207 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) |
198 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) | 208 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) |
209 | |||
210 | if (videoPassword) { | ||
211 | const headers = { 'x-peertube-video-password': videoPassword } | ||
212 | await makeRawRequest({ url: file.fileUrl, headers, expectedStatus }) | ||
213 | await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus }) | ||
214 | } | ||
199 | } | 215 | } |
200 | 216 | ||
201 | const hls = video.streamingPlaylists[0] | 217 | const hls = video.streamingPlaylists[0] |
@@ -204,6 +220,12 @@ describe('Test video static file privacy', function () { | |||
204 | 220 | ||
205 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) | 221 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) |
206 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) | 222 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) |
223 | |||
224 | if (videoPassword) { | ||
225 | const headers = { 'x-peertube-video-password': videoPassword } | ||
226 | await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus }) | ||
227 | await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus }) | ||
228 | } | ||
207 | } | 229 | } |
208 | 230 | ||
209 | before(async function () { | 231 | before(async function () { |
@@ -216,13 +238,53 @@ describe('Test video static file privacy', function () { | |||
216 | it('Should not be able to access a private video files without OAuth token and file token', async function () { | 238 | it('Should not be able to access a private video files without OAuth token and file token', async function () { |
217 | this.timeout(120000) | 239 | this.timeout(120000) |
218 | 240 | ||
219 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | 241 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
220 | await waitJobs([ server ]) | 242 | await waitJobs([ server ]) |
221 | 243 | ||
222 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) | 244 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) |
223 | }) | 245 | }) |
224 | 246 | ||
225 | it('Should not be able to access an internal video files without appropriate OAuth token and file token', async function () { | 247 | it('Should not be able to access password protected video files without OAuth token, file token and password', async function () { |
248 | this.timeout(120000) | ||
249 | const videoPassword = 'my super password' | ||
250 | |||
251 | const { uuid } = await server.videos.quickUpload({ | ||
252 | name: 'password protected video', | ||
253 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
254 | videoPasswords: [ videoPassword ] | ||
255 | }) | ||
256 | await waitJobs([ server ]) | ||
257 | |||
258 | await checkVideoFiles({ | ||
259 | id: uuid, | ||
260 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
261 | token: null, | ||
262 | videoFileToken: null, | ||
263 | videoPassword: null | ||
264 | }) | ||
265 | }) | ||
266 | |||
267 | it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () { | ||
268 | this.timeout(120000) | ||
269 | const videoPassword = 'my super password' | ||
270 | |||
271 | const { uuid } = await server.videos.quickUpload({ | ||
272 | name: 'password protected video', | ||
273 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
274 | videoPasswords: [ videoPassword ] | ||
275 | }) | ||
276 | await waitJobs([ server ]) | ||
277 | |||
278 | await checkVideoFiles({ | ||
279 | id: uuid, | ||
280 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
281 | token: userToken, | ||
282 | videoFileToken: unrelatedFileToken, | ||
283 | videoPassword: 'incorrectPassword' | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () { | ||
226 | this.timeout(120000) | 288 | this.timeout(120000) |
227 | 289 | ||
228 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 290 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
@@ -247,6 +309,23 @@ describe('Test video static file privacy', function () { | |||
247 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | 309 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) |
248 | }) | 310 | }) |
249 | 311 | ||
312 | it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () { | ||
313 | this.timeout(120000) | ||
314 | const videoPassword = 'my super password' | ||
315 | |||
316 | const { uuid } = await server.videos.quickUpload({ | ||
317 | name: 'video', | ||
318 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
319 | videoPasswords: [ videoPassword ] | ||
320 | }) | ||
321 | |||
322 | const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword }) | ||
323 | |||
324 | await waitJobs([ server ]) | ||
325 | |||
326 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword }) | ||
327 | }) | ||
328 | |||
250 | it('Should reinject video file token', async function () { | 329 | it('Should reinject video file token', async function () { |
251 | this.timeout(120000) | 330 | this.timeout(120000) |
252 | 331 | ||
@@ -294,13 +373,20 @@ describe('Test video static file privacy', function () { | |||
294 | let permanentLiveId: string | 373 | let permanentLiveId: string |
295 | let permanentLive: LiveVideo | 374 | let permanentLive: LiveVideo |
296 | 375 | ||
376 | let passwordProtectedLiveId: string | ||
377 | let passwordProtectedLive: LiveVideo | ||
378 | |||
379 | const correctPassword = 'my super password' | ||
380 | |||
297 | let unrelatedFileToken: string | 381 | let unrelatedFileToken: string |
298 | 382 | ||
299 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | 383 | async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) { |
384 | const { live, liveId, videoPassword } = options | ||
300 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | 385 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
301 | await server.live.waitUntilPublished({ videoId: liveId }) | 386 | await server.live.waitUntilPublished({ videoId: liveId }) |
302 | 387 | ||
303 | const video = await server.videos.getWithToken({ id: liveId }) | 388 | const video = await server.videos.getWithToken({ id: liveId }) |
389 | |||
304 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | 390 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) |
305 | 391 | ||
306 | const hls = video.streamingPlaylists[0] | 392 | const hls = video.streamingPlaylists[0] |
@@ -314,6 +400,16 @@ describe('Test video static file privacy', function () { | |||
314 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 400 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
315 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 401 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
316 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 402 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
403 | |||
404 | if (videoPassword) { | ||
405 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
406 | await makeRawRequest({ | ||
407 | url, | ||
408 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
409 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
410 | }) | ||
411 | } | ||
412 | |||
317 | } | 413 | } |
318 | 414 | ||
319 | await stopFfmpeg(ffmpegCommand) | 415 | await stopFfmpeg(ffmpegCommand) |
@@ -381,18 +477,35 @@ describe('Test video static file privacy', function () { | |||
381 | permanentLiveId = video.uuid | 477 | permanentLiveId = video.uuid |
382 | permanentLive = live | 478 | permanentLive = live |
383 | } | 479 | } |
480 | |||
481 | { | ||
482 | const { video, live } = await server.live.quickCreate({ | ||
483 | saveReplay: false, | ||
484 | permanentLive: false, | ||
485 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
486 | videoPasswords: [ correctPassword ] | ||
487 | }) | ||
488 | passwordProtectedLiveId = video.uuid | ||
489 | passwordProtectedLive = live | ||
490 | } | ||
384 | }) | 491 | }) |
385 | 492 | ||
386 | it('Should create a private normal live and have a private static path', async function () { | 493 | it('Should create a private normal live and have a private static path', async function () { |
387 | this.timeout(240000) | 494 | this.timeout(240000) |
388 | 495 | ||
389 | await checkLiveFiles(normalLive, normalLiveId) | 496 | await checkLiveFiles({ live: normalLive, liveId: normalLiveId }) |
390 | }) | 497 | }) |
391 | 498 | ||
392 | it('Should create a private permanent live and have a private static path', async function () { | 499 | it('Should create a private permanent live and have a private static path', async function () { |
393 | this.timeout(240000) | 500 | this.timeout(240000) |
394 | 501 | ||
395 | await checkLiveFiles(permanentLive, permanentLiveId) | 502 | await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId }) |
503 | }) | ||
504 | |||
505 | it('Should create a password protected live and have a private static path', async function () { | ||
506 | this.timeout(240000) | ||
507 | |||
508 | await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword }) | ||
396 | }) | 509 | }) |
397 | 510 | ||
398 | it('Should reinject video file token on permanent live', async function () { | 511 | it('Should reinject video file token on permanent live', async function () { |
diff --git a/server/tests/api/videos/video-storyboard.ts b/server/tests/api/videos/video-storyboard.ts new file mode 100644 index 000000000..fc4b4450f --- /dev/null +++ b/server/tests/api/videos/video-storyboard.ts | |||
@@ -0,0 +1,213 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs-extra' | ||
5 | import { basename } from 'path' | ||
6 | import { FIXTURE_URLS } from '@server/tests/shared' | ||
7 | import { areHttpImportTestsDisabled } from '@shared/core-utils' | ||
8 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | doubleFollow, | ||
13 | makeGetRequest, | ||
14 | PeerTubeServer, | ||
15 | sendRTMPStream, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | stopFfmpeg, | ||
19 | waitJobs | ||
20 | } from '@shared/server-commands' | ||
21 | |||
22 | async function checkStoryboard (options: { | ||
23 | server: PeerTubeServer | ||
24 | uuid: string | ||
25 | tilesCount?: number | ||
26 | minSize?: number | ||
27 | }) { | ||
28 | const { server, uuid, tilesCount, minSize = 1000 } = options | ||
29 | |||
30 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
31 | |||
32 | expect(storyboards).to.have.lengthOf(1) | ||
33 | |||
34 | const storyboard = storyboards[0] | ||
35 | |||
36 | expect(storyboard.spriteDuration).to.equal(1) | ||
37 | expect(storyboard.spriteHeight).to.equal(108) | ||
38 | expect(storyboard.spriteWidth).to.equal(192) | ||
39 | expect(storyboard.storyboardPath).to.exist | ||
40 | |||
41 | if (tilesCount) { | ||
42 | expect(storyboard.totalWidth).to.equal(192 * Math.min(tilesCount, 10)) | ||
43 | expect(storyboard.totalHeight).to.equal(108 * Math.max((tilesCount / 10), 1)) | ||
44 | } | ||
45 | |||
46 | const { body } = await makeGetRequest({ url: server.url, path: storyboard.storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
47 | expect(body.length).to.be.above(minSize) | ||
48 | } | ||
49 | |||
50 | describe('Test video storyboard', function () { | ||
51 | let servers: PeerTubeServer[] | ||
52 | |||
53 | let baseUUID: string | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(120000) | ||
57 | |||
58 | servers = await createMultipleServers(2) | ||
59 | await setAccessTokensToServers(servers) | ||
60 | await setDefaultVideoChannel(servers) | ||
61 | |||
62 | await doubleFollow(servers[0], servers[1]) | ||
63 | }) | ||
64 | |||
65 | it('Should generate a storyboard after upload without transcoding', async function () { | ||
66 | this.timeout(60000) | ||
67 | |||
68 | // 5s video | ||
69 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
70 | baseUUID = uuid | ||
71 | await waitJobs(servers) | ||
72 | |||
73 | for (const server of servers) { | ||
74 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should generate a storyboard after upload without transcoding with a long video', async function () { | ||
79 | this.timeout(60000) | ||
80 | |||
81 | // 124s video | ||
82 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_very_long_10p.mp4' }) | ||
83 | await waitJobs(servers) | ||
84 | |||
85 | for (const server of servers) { | ||
86 | await checkStoryboard({ server, uuid, tilesCount: 100 }) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should generate a storyboard after upload with transcoding', async function () { | ||
91 | this.timeout(60000) | ||
92 | |||
93 | await servers[0].config.enableMinimumTranscoding() | ||
94 | |||
95 | // 5s video | ||
96 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
97 | await waitJobs(servers) | ||
98 | |||
99 | for (const server of servers) { | ||
100 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should generate a storyboard after an audio upload', async function () { | ||
105 | this.timeout(60000) | ||
106 | |||
107 | // 6s audio | ||
108 | const attributes = { name: 'audio', fixture: 'sample.ogg' } | ||
109 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | ||
110 | await waitJobs(servers) | ||
111 | |||
112 | for (const server of servers) { | ||
113 | try { | ||
114 | await checkStoryboard({ server, uuid, tilesCount: 6, minSize: 250 }) | ||
115 | } catch { // FIXME: to remove after ffmpeg CI upgrade, ffmpeg CI version (4.3) generates a 7.6s length video | ||
116 | await checkStoryboard({ server, uuid, tilesCount: 8, minSize: 250 }) | ||
117 | } | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | it('Should generate a storyboard after HTTP import', async function () { | ||
122 | this.timeout(60000) | ||
123 | |||
124 | if (areHttpImportTestsDisabled()) return | ||
125 | |||
126 | // 3s video | ||
127 | const { video } = await servers[0].imports.importVideo({ | ||
128 | attributes: { | ||
129 | targetUrl: FIXTURE_URLS.goodVideo, | ||
130 | channelId: servers[0].store.channel.id, | ||
131 | privacy: VideoPrivacy.PUBLIC | ||
132 | } | ||
133 | }) | ||
134 | await waitJobs(servers) | ||
135 | |||
136 | for (const server of servers) { | ||
137 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 3 }) | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should generate a storyboard after torrent import', async function () { | ||
142 | this.timeout(60000) | ||
143 | |||
144 | if (areHttpImportTestsDisabled()) return | ||
145 | |||
146 | // 10s video | ||
147 | const { video } = await servers[0].imports.importVideo({ | ||
148 | attributes: { | ||
149 | magnetUri: FIXTURE_URLS.magnet, | ||
150 | channelId: servers[0].store.channel.id, | ||
151 | privacy: VideoPrivacy.PUBLIC | ||
152 | } | ||
153 | }) | ||
154 | await waitJobs(servers) | ||
155 | |||
156 | for (const server of servers) { | ||
157 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 10 }) | ||
158 | } | ||
159 | }) | ||
160 | |||
161 | it('Should generate a storyboard after a live', async function () { | ||
162 | this.timeout(240000) | ||
163 | |||
164 | await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | ||
165 | |||
166 | const { live, video } = await servers[0].live.quickCreate({ | ||
167 | saveReplay: true, | ||
168 | permanentLive: false, | ||
169 | privacy: VideoPrivacy.PUBLIC | ||
170 | }) | ||
171 | |||
172 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
173 | await servers[0].live.waitUntilPublished({ videoId: video.id }) | ||
174 | |||
175 | await stopFfmpeg(ffmpegCommand) | ||
176 | |||
177 | await servers[0].live.waitUntilReplacedByReplay({ videoId: video.id }) | ||
178 | await waitJobs(servers) | ||
179 | |||
180 | for (const server of servers) { | ||
181 | await checkStoryboard({ server, uuid: video.uuid }) | ||
182 | } | ||
183 | }) | ||
184 | |||
185 | it('Should cleanup storyboards on video deletion', async function () { | ||
186 | this.timeout(60000) | ||
187 | |||
188 | const { storyboards } = await servers[0].storyboard.list({ id: baseUUID }) | ||
189 | const storyboardName = basename(storyboards[0].storyboardPath) | ||
190 | |||
191 | const listFiles = () => { | ||
192 | const storyboardPath = servers[0].getDirectoryPath('storyboards') | ||
193 | return readdir(storyboardPath) | ||
194 | } | ||
195 | |||
196 | { | ||
197 | const storyboads = await listFiles() | ||
198 | expect(storyboads).to.include(storyboardName) | ||
199 | } | ||
200 | |||
201 | await servers[0].videos.remove({ id: baseUUID }) | ||
202 | await waitJobs(servers) | ||
203 | |||
204 | { | ||
205 | const storyboads = await listFiles() | ||
206 | expect(storyboads).to.not.include(storyboardName) | ||
207 | } | ||
208 | }) | ||
209 | |||
210 | after(async function () { | ||
211 | await cleanupTests(servers) | ||
212 | }) | ||
213 | }) | ||
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 30251706b..73c066bfb 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -154,7 +154,7 @@ describe('Test videos filter', function () { | |||
154 | server: PeerTubeServer | 154 | server: PeerTubeServer |
155 | path: string | 155 | path: string |
156 | isLocal?: boolean | 156 | isLocal?: boolean |
157 | hasWebtorrentFiles?: boolean | 157 | hasWebVideoFiles?: boolean |
158 | hasHLSFiles?: boolean | 158 | hasHLSFiles?: boolean |
159 | include?: VideoInclude | 159 | include?: VideoInclude |
160 | privacyOneOf?: VideoPrivacy[] | 160 | privacyOneOf?: VideoPrivacy[] |
@@ -174,7 +174,7 @@ describe('Test videos filter', function () { | |||
174 | 'include', | 174 | 'include', |
175 | 'category', | 175 | 'category', |
176 | 'tagsAllOf', | 176 | 'tagsAllOf', |
177 | 'hasWebtorrentFiles', | 177 | 'hasWebVideoFiles', |
178 | 'hasHLSFiles', | 178 | 'hasHLSFiles', |
179 | 'privacyOneOf', | 179 | 'privacyOneOf', |
180 | 'excludeAlreadyWatched' | 180 | 'excludeAlreadyWatched' |
@@ -463,14 +463,14 @@ describe('Test videos filter', function () { | |||
463 | } | 463 | } |
464 | }) | 464 | }) |
465 | 465 | ||
466 | it('Should filter by HLS or WebTorrent files', async function () { | 466 | it('Should filter by HLS or Web Video files', async function () { |
467 | this.timeout(360000) | 467 | this.timeout(360000) |
468 | 468 | ||
469 | const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name) | 469 | const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name) |
470 | 470 | ||
471 | await servers[0].config.enableTranscoding(true, false) | 471 | await servers[0].config.enableTranscoding(true, false) |
472 | await servers[0].videos.upload({ attributes: { name: 'webtorrent video' } }) | 472 | await servers[0].videos.upload({ attributes: { name: 'web video video' } }) |
473 | const hasWebtorrent = finderFactory('webtorrent video') | 473 | const hasWebVideo = finderFactory('web video video') |
474 | 474 | ||
475 | await waitJobs(servers) | 475 | await waitJobs(servers) |
476 | 476 | ||
@@ -481,24 +481,24 @@ describe('Test videos filter', function () { | |||
481 | await waitJobs(servers) | 481 | await waitJobs(servers) |
482 | 482 | ||
483 | await servers[0].config.enableTranscoding(true, true) | 483 | await servers[0].config.enableTranscoding(true, true) |
484 | await servers[0].videos.upload({ attributes: { name: 'hls and webtorrent video' } }) | 484 | await servers[0].videos.upload({ attributes: { name: 'hls and web video video' } }) |
485 | const hasBoth = finderFactory('hls and webtorrent video') | 485 | const hasBoth = finderFactory('hls and web video video') |
486 | 486 | ||
487 | await waitJobs(servers) | 487 | await waitJobs(servers) |
488 | 488 | ||
489 | for (const path of paths) { | 489 | for (const path of paths) { |
490 | { | 490 | { |
491 | const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: true }) | 491 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: true }) |
492 | 492 | ||
493 | expect(hasWebtorrent(videos)).to.be.true | 493 | expect(hasWebVideo(videos)).to.be.true |
494 | expect(hasHLS(videos)).to.be.false | 494 | expect(hasHLS(videos)).to.be.false |
495 | expect(hasBoth(videos)).to.be.true | 495 | expect(hasBoth(videos)).to.be.true |
496 | } | 496 | } |
497 | 497 | ||
498 | { | 498 | { |
499 | const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: false }) | 499 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: false }) |
500 | 500 | ||
501 | expect(hasWebtorrent(videos)).to.be.false | 501 | expect(hasWebVideo(videos)).to.be.false |
502 | expect(hasHLS(videos)).to.be.true | 502 | expect(hasHLS(videos)).to.be.true |
503 | expect(hasBoth(videos)).to.be.false | 503 | expect(hasBoth(videos)).to.be.false |
504 | } | 504 | } |
@@ -506,7 +506,7 @@ describe('Test videos filter', function () { | |||
506 | { | 506 | { |
507 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true }) | 507 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true }) |
508 | 508 | ||
509 | expect(hasWebtorrent(videos)).to.be.false | 509 | expect(hasWebVideo(videos)).to.be.false |
510 | expect(hasHLS(videos)).to.be.true | 510 | expect(hasHLS(videos)).to.be.true |
511 | expect(hasBoth(videos)).to.be.true | 511 | expect(hasBoth(videos)).to.be.true |
512 | } | 512 | } |
@@ -514,23 +514,23 @@ describe('Test videos filter', function () { | |||
514 | { | 514 | { |
515 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false }) | 515 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false }) |
516 | 516 | ||
517 | expect(hasWebtorrent(videos)).to.be.true | 517 | expect(hasWebVideo(videos)).to.be.true |
518 | expect(hasHLS(videos)).to.be.false | 518 | expect(hasHLS(videos)).to.be.false |
519 | expect(hasBoth(videos)).to.be.false | 519 | expect(hasBoth(videos)).to.be.false |
520 | } | 520 | } |
521 | 521 | ||
522 | { | 522 | { |
523 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebtorrentFiles: false }) | 523 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebVideoFiles: false }) |
524 | 524 | ||
525 | expect(hasWebtorrent(videos)).to.be.false | 525 | expect(hasWebVideo(videos)).to.be.false |
526 | expect(hasHLS(videos)).to.be.false | 526 | expect(hasHLS(videos)).to.be.false |
527 | expect(hasBoth(videos)).to.be.false | 527 | expect(hasBoth(videos)).to.be.false |
528 | } | 528 | } |
529 | 529 | ||
530 | { | 530 | { |
531 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebtorrentFiles: true }) | 531 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebVideoFiles: true }) |
532 | 532 | ||
533 | expect(hasWebtorrent(videos)).to.be.false | 533 | expect(hasWebVideo(videos)).to.be.false |
534 | expect(hasHLS(videos)).to.be.false | 534 | expect(hasHLS(videos)).to.be.false |
535 | expect(hasBoth(videos)).to.be.true | 535 | expect(hasBoth(videos)).to.be.true |
536 | } | 536 | } |
diff --git a/server/tests/cli/create-generate-storyboard-job.ts b/server/tests/cli/create-generate-storyboard-job.ts new file mode 100644 index 000000000..02a4be8ae --- /dev/null +++ b/server/tests/cli/create-generate-storyboard-job.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir, remove } from 'fs-extra' | ||
5 | import { join } from 'path' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | makeGetRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@shared/server-commands' | ||
16 | import { SQLCommand } from '../shared' | ||
17 | |||
18 | function listStoryboardFiles (server: PeerTubeServer) { | ||
19 | const storage = server.getDirectoryPath('storyboards') | ||
20 | |||
21 | return readdir(storage) | ||
22 | } | ||
23 | |||
24 | describe('Test create generate storyboard job', function () { | ||
25 | let servers: PeerTubeServer[] = [] | ||
26 | const uuids: string[] = [] | ||
27 | let sql: SQLCommand | ||
28 | let existingStoryboardName: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | // Run server 2 to have transcoding enabled | ||
34 | servers = await createMultipleServers(2) | ||
35 | await setAccessTokensToServers(servers) | ||
36 | |||
37 | await doubleFollow(servers[0], servers[1]) | ||
38 | |||
39 | for (let i = 0; i < 3; i++) { | ||
40 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video ' + i }) | ||
41 | uuids.push(uuid) | ||
42 | } | ||
43 | |||
44 | await waitJobs(servers) | ||
45 | |||
46 | const storage = servers[0].getDirectoryPath('storyboards') | ||
47 | for (const storyboard of await listStoryboardFiles(servers[0])) { | ||
48 | await remove(join(storage, storyboard)) | ||
49 | } | ||
50 | |||
51 | sql = new SQLCommand(servers[0]) | ||
52 | await sql.deleteAll('storyboard') | ||
53 | |||
54 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 4' }) | ||
55 | uuids.push(uuid) | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | |||
59 | const storyboards = await listStoryboardFiles(servers[0]) | ||
60 | existingStoryboardName = storyboards[0] | ||
61 | }) | ||
62 | |||
63 | it('Should create a storyboard of a video', async function () { | ||
64 | this.timeout(120000) | ||
65 | |||
66 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
67 | const command = `npm run create-generate-storyboard-job -- -v ${uuid}` | ||
68 | await servers[0].cli.execWithEnv(command) | ||
69 | } | ||
70 | |||
71 | await waitJobs(servers) | ||
72 | |||
73 | { | ||
74 | const storyboards = await listStoryboardFiles(servers[0]) | ||
75 | expect(storyboards).to.have.lengthOf(2) | ||
76 | expect(storyboards).to.not.include(existingStoryboardName) | ||
77 | |||
78 | existingStoryboardName = storyboards[0] | ||
79 | } | ||
80 | |||
81 | for (const server of servers) { | ||
82 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
83 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
84 | expect(storyboards).to.have.lengthOf(1) | ||
85 | |||
86 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
87 | } | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | it('Should create missing storyboards', async function () { | ||
92 | this.timeout(120000) | ||
93 | |||
94 | const command = `npm run create-generate-storyboard-job -- -a` | ||
95 | await servers[0].cli.execWithEnv(command) | ||
96 | |||
97 | await waitJobs(servers) | ||
98 | |||
99 | { | ||
100 | const storyboards = await listStoryboardFiles(servers[0]) | ||
101 | expect(storyboards).to.have.lengthOf(4) | ||
102 | expect(storyboards).to.include(existingStoryboardName) | ||
103 | } | ||
104 | |||
105 | for (const server of servers) { | ||
106 | for (const uuid of uuids) { | ||
107 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
108 | expect(storyboards).to.have.lengthOf(1) | ||
109 | |||
110 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
111 | } | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | after(async function () { | ||
116 | await sql.cleanup() | ||
117 | |||
118 | await cleanupTests(servers) | ||
119 | }) | ||
120 | }) | ||
diff --git a/server/tests/cli/create-move-video-storage-job.ts b/server/tests/cli/create-move-video-storage-job.ts index 253fc983e..fc6a8e648 100644 --- a/server/tests/cli/create-move-video-storage-job.ts +++ b/server/tests/cli/create-move-video-storage-job.ts | |||
@@ -109,8 +109,8 @@ describe('Test create move video storage job', function () { | |||
109 | }) | 109 | }) |
110 | 110 | ||
111 | it('Should not have files on disk anymore', async function () { | 111 | it('Should not have files on disk anymore', async function () { |
112 | await checkDirectoryIsEmpty(servers[0], 'videos', [ 'private' ]) | 112 | await checkDirectoryIsEmpty(servers[0], 'web-videos', [ 'private' ]) |
113 | await checkDirectoryIsEmpty(servers[0], join('videos', 'private')) | 113 | await checkDirectoryIsEmpty(servers[0], join('web-videos', 'private')) |
114 | 114 | ||
115 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls'), [ 'private' ]) | 115 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls'), [ 'private' ]) |
116 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls', 'private')) | 116 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls', 'private')) |
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index 8579be39c..94444ace3 100644 --- a/server/tests/cli/index.ts +++ b/server/tests/cli/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './create-import-video-file-job' | 2 | import './create-import-video-file-job' |
3 | import './create-generate-storyboard-job' | ||
3 | import './create-move-video-storage-job' | 4 | import './create-move-video-storage-job' |
4 | import './peertube' | 5 | import './peertube' |
5 | import './plugins' | 6 | import './plugins' |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index 8bdf2136d..561ed6a68 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -35,10 +35,10 @@ async function assertNotExists (server: PeerTubeServer, directory: string, subst | |||
35 | 35 | ||
36 | async function assertCountAreOkay (servers: PeerTubeServer[]) { | 36 | async function assertCountAreOkay (servers: PeerTubeServer[]) { |
37 | for (const server of servers) { | 37 | for (const server of servers) { |
38 | const videosCount = await countFiles(server, 'videos') | 38 | const videosCount = await countFiles(server, 'web-videos') |
39 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory | 39 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory |
40 | 40 | ||
41 | const privateVideosCount = await countFiles(server, 'videos/private') | 41 | const privateVideosCount = await countFiles(server, 'web-videos/private') |
42 | expect(privateVideosCount).to.equal(4) | 42 | expect(privateVideosCount).to.equal(4) |
43 | 43 | ||
44 | const torrentsCount = await countFiles(server, 'torrents') | 44 | const torrentsCount = await countFiles(server, 'torrents') |
@@ -48,7 +48,7 @@ async function assertCountAreOkay (servers: PeerTubeServer[]) { | |||
48 | expect(previewsCount).to.equal(3) | 48 | expect(previewsCount).to.equal(3) |
49 | 49 | ||
50 | const thumbnailsCount = await countFiles(server, 'thumbnails') | 50 | const thumbnailsCount = await countFiles(server, 'thumbnails') |
51 | expect(thumbnailsCount).to.equal(7) // 3 local videos, 1 local playlist, 2 remotes videos and 1 remote playlist | 51 | expect(thumbnailsCount).to.equal(5) // 3 local videos, 1 local playlist, 2 remotes videos (lazy downloaded) and 1 remote playlist |
52 | 52 | ||
53 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
54 | expect(avatarsCount).to.equal(4) | 54 | expect(avatarsCount).to.equal(4) |
@@ -85,7 +85,7 @@ describe('Test prune storage scripts', function () { | |||
85 | displayName: 'playlist', | 85 | displayName: 'playlist', |
86 | privacy: VideoPlaylistPrivacy.PUBLIC, | 86 | privacy: VideoPlaylistPrivacy.PUBLIC, |
87 | videoChannelId: server.store.channel.id, | 87 | videoChannelId: server.store.channel.id, |
88 | thumbnailfile: 'thumbnail.jpg' | 88 | thumbnailfile: 'custom-thumbnail.jpg' |
89 | } | 89 | } |
90 | }) | 90 | }) |
91 | } | 91 | } |
@@ -131,8 +131,8 @@ describe('Test prune storage scripts', function () { | |||
131 | it('Should create some dirty files', async function () { | 131 | it('Should create some dirty files', async function () { |
132 | for (let i = 0; i < 2; i++) { | 132 | for (let i = 0; i < 2; i++) { |
133 | { | 133 | { |
134 | const basePublic = servers[0].servers.buildDirectory('videos') | 134 | const basePublic = servers[0].servers.buildDirectory('web-videos') |
135 | const basePrivate = servers[0].servers.buildDirectory(join('videos', 'private')) | 135 | const basePrivate = servers[0].servers.buildDirectory(join('web-videos', 'private')) |
136 | 136 | ||
137 | const n1 = buildUUID() + '.mp4' | 137 | const n1 = buildUUID() + '.mp4' |
138 | const n2 = buildUUID() + '.webm' | 138 | const n2 = buildUUID() + '.webm' |
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts index 16a8adcda..66de7f79c 100644 --- a/server/tests/cli/regenerate-thumbnails.ts +++ b/server/tests/cli/regenerate-thumbnails.ts | |||
@@ -60,6 +60,9 @@ describe('Test regenerate thumbnails script', function () { | |||
60 | 60 | ||
61 | remoteVideo = await servers[0].videos.get({ id: videoUUID }) | 61 | remoteVideo = await servers[0].videos.get({ id: videoUUID }) |
62 | 62 | ||
63 | // Load remote thumbnail on disk | ||
64 | await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
65 | |||
63 | thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath)) | 66 | thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath)) |
64 | } | 67 | } |
65 | 68 | ||
diff --git a/server/tests/client.ts b/server/tests/client.ts index e84251561..68f3a1d14 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts | |||
@@ -56,6 +56,7 @@ describe('Test a client controllers', function () { | |||
56 | let privateVideoId: string | 56 | let privateVideoId: string |
57 | let internalVideoId: string | 57 | let internalVideoId: string |
58 | let unlistedVideoId: string | 58 | let unlistedVideoId: string |
59 | let passwordProtectedVideoId: string | ||
59 | 60 | ||
60 | let playlistIds: (string | number)[] = [] | 61 | let playlistIds: (string | number)[] = [] |
61 | 62 | ||
@@ -92,7 +93,12 @@ describe('Test a client controllers', function () { | |||
92 | { | 93 | { |
93 | ({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })); | 94 | ({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })); |
94 | ({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })); | 95 | ({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })); |
95 | ({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL })) | 96 | ({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL })); |
97 | ({ uuid: passwordProtectedVideoId } = await servers[0].videos.quickUpload({ | ||
98 | name: 'password protected', | ||
99 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
100 | videoPasswords: [ 'password' ] | ||
101 | })) | ||
96 | } | 102 | } |
97 | 103 | ||
98 | // Playlist | 104 | // Playlist |
@@ -502,9 +508,9 @@ describe('Test a client controllers', function () { | |||
502 | } | 508 | } |
503 | }) | 509 | }) |
504 | 510 | ||
505 | it('Should not display internal/private video', async function () { | 511 | it('Should not display internal/private/password protected video', async function () { |
506 | for (const basePath of watchVideoBasePaths) { | 512 | for (const basePath of watchVideoBasePaths) { |
507 | for (const id of [ privateVideoId, internalVideoId ]) { | 513 | for (const id of [ privateVideoId, internalVideoId, passwordProtectedVideoId ]) { |
508 | const res = await makeGetRequest({ | 514 | const res = await makeGetRequest({ |
509 | url: servers[0].url, | 515 | url: servers[0].url, |
510 | path: basePath + id, | 516 | path: basePath + id, |
@@ -514,6 +520,7 @@ describe('Test a client controllers', function () { | |||
514 | 520 | ||
515 | expect(res.text).to.not.contain('internal') | 521 | expect(res.text).to.not.contain('internal') |
516 | expect(res.text).to.not.contain('private') | 522 | expect(res.text).to.not.contain('private') |
523 | expect(res.text).to.not.contain('password protected') | ||
517 | } | 524 | } |
518 | } | 525 | } |
519 | }) | 526 | }) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 8433c873e..1754ac466 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -47,7 +47,7 @@ describe('Test syndication feeds', () => { | |||
47 | serverHLSOnly = await createSingleServer(3, { | 47 | serverHLSOnly = await createSingleServer(3, { |
48 | transcoding: { | 48 | transcoding: { |
49 | enabled: true, | 49 | enabled: true, |
50 | webtorrent: { enabled: false }, | 50 | web_videos: { enabled: false }, |
51 | hls: { enabled: true } | 51 | hls: { enabled: true } |
52 | } | 52 | } |
53 | }) | 53 | }) |
@@ -99,6 +99,13 @@ describe('Test syndication feeds', () => { | |||
99 | await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' }) | 99 | await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' }) |
100 | } | 100 | } |
101 | 101 | ||
102 | { | ||
103 | const attributes = { name: 'password protected video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
104 | const { id } = await servers[0].videos.upload({ attributes }) | ||
105 | |||
106 | await servers[0].comments.createThread({ videoId: id, text: 'comment on password protected video' }) | ||
107 | } | ||
108 | |||
102 | await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } }) | 109 | await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } }) |
103 | 110 | ||
104 | await waitJobs([ ...servers, serverHLSOnly ]) | 111 | await waitJobs([ ...servers, serverHLSOnly ]) |
@@ -445,7 +452,7 @@ describe('Test syndication feeds', () => { | |||
445 | 452 | ||
446 | describe('Video comments feed', function () { | 453 | describe('Video comments feed', function () { |
447 | 454 | ||
448 | it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted videos', async function () { | 455 | it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted/password protected videos', async function () { |
449 | for (const server of servers) { | 456 | for (const server of servers) { |
450 | const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | 457 | const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) |
451 | 458 | ||
diff --git a/server/tests/fixtures/custom-preview-big.png b/server/tests/fixtures/custom-preview-big.png new file mode 100644 index 000000000..03d171af3 --- /dev/null +++ b/server/tests/fixtures/custom-preview-big.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-preview.jpg b/server/tests/fixtures/custom-preview.jpg new file mode 100644 index 000000000..5a039d830 --- /dev/null +++ b/server/tests/fixtures/custom-preview.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail-big.jpg b/server/tests/fixtures/custom-thumbnail-big.jpg new file mode 100644 index 000000000..08375e425 --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail-big.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail.jpg b/server/tests/fixtures/custom-thumbnail.jpg new file mode 100644 index 000000000..ef818442d --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/custom-thumbnail.png b/server/tests/fixtures/custom-thumbnail.png new file mode 100644 index 000000000..9f34daec1 --- /dev/null +++ b/server/tests/fixtures/custom-thumbnail.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/preview-big.png b/server/tests/fixtures/preview-big.png deleted file mode 100644 index 612e297f1..000000000 --- a/server/tests/fixtures/preview-big.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/preview.jpg b/server/tests/fixtures/preview.jpg deleted file mode 100644 index 1421da738..000000000 --- a/server/tests/fixtures/preview.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail-big.jpg b/server/tests/fixtures/thumbnail-big.jpg deleted file mode 100644 index 537720d24..000000000 --- a/server/tests/fixtures/thumbnail-big.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail.jpg b/server/tests/fixtures/thumbnail.jpg deleted file mode 100644 index 1e2897fb8..000000000 --- a/server/tests/fixtures/thumbnail.jpg +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/thumbnail.png b/server/tests/fixtures/thumbnail.png deleted file mode 100644 index b331aba3b..000000000 --- a/server/tests/fixtures/thumbnail.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1-preview.webm.jpg b/server/tests/fixtures/video_short1-preview.webm.jpg index d65af1f21..15454942d 100644 --- a/server/tests/fixtures/video_short1-preview.webm.jpg +++ b/server/tests/fixtures/video_short1-preview.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1.webm.jpg b/server/tests/fixtures/video_short1.webm.jpg index 0ab7c58ad..b2740d73d 100644 --- a/server/tests/fixtures/video_short1.webm.jpg +++ b/server/tests/fixtures/video_short1.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short2.webm.jpg b/server/tests/fixtures/video_short2.webm.jpg index 1e2897fb8..afe476c7f 100644 --- a/server/tests/fixtures/video_short2.webm.jpg +++ b/server/tests/fixtures/video_short2.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_very_long_10p.mp4 b/server/tests/fixtures/video_very_long_10p.mp4 new file mode 100644 index 000000000..852297933 --- /dev/null +++ b/server/tests/fixtures/video_very_long_10p.mp4 | |||
Binary files differ | |||
diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts index 530c9bacd..6021ffc48 100644 --- a/server/tests/helpers/image.ts +++ b/server/tests/helpers/image.ts | |||
@@ -35,28 +35,28 @@ describe('Image helpers', function () { | |||
35 | const thumbnailSize = { width: 280, height: 157 } | 35 | const thumbnailSize = { width: 280, height: 157 } |
36 | 36 | ||
37 | it('Should skip processing if the source image is okay', async function () { | 37 | it('Should skip processing if the source image is okay', async function () { |
38 | const input = buildAbsoluteFixturePath('thumbnail.jpg') | 38 | const input = buildAbsoluteFixturePath('custom-thumbnail.jpg') |
39 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 39 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
40 | 40 | ||
41 | await checkBuffers(input, imageDestJPG, true) | 41 | await checkBuffers(input, imageDestJPG, true) |
42 | }) | 42 | }) |
43 | 43 | ||
44 | it('Should not skip processing if the source image does not have the appropriate extension', async function () { | 44 | it('Should not skip processing if the source image does not have the appropriate extension', async function () { |
45 | const input = buildAbsoluteFixturePath('thumbnail.png') | 45 | const input = buildAbsoluteFixturePath('custom-thumbnail.png') |
46 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 46 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
47 | 47 | ||
48 | await checkBuffers(input, imageDestJPG, false) | 48 | await checkBuffers(input, imageDestJPG, false) |
49 | }) | 49 | }) |
50 | 50 | ||
51 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | 51 | it('Should not skip processing if the source image does not have the appropriate size', async function () { |
52 | const input = buildAbsoluteFixturePath('preview.jpg') | 52 | const input = buildAbsoluteFixturePath('custom-preview.jpg') |
53 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 53 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
54 | 54 | ||
55 | await checkBuffers(input, imageDestJPG, false) | 55 | await checkBuffers(input, imageDestJPG, false) |
56 | }) | 56 | }) |
57 | 57 | ||
58 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | 58 | it('Should not skip processing if the source image does not have the appropriate size', async function () { |
59 | const input = buildAbsoluteFixturePath('thumbnail-big.jpg') | 59 | const input = buildAbsoluteFixturePath('custom-thumbnail-big.jpg') |
60 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | 60 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) |
61 | 61 | ||
62 | await checkBuffers(input, imageDestJPG, false) | 62 | await checkBuffers(input, imageDestJPG, false) |
diff --git a/server/tests/peertube-runner/studio-transcoding.ts b/server/tests/peertube-runner/studio-transcoding.ts index 988201947..c265d7934 100644 --- a/server/tests/peertube-runner/studio-transcoding.ts +++ b/server/tests/peertube-runner/studio-transcoding.ts | |||
@@ -44,8 +44,8 @@ describe('Test studio transcoding in peertube-runner program', function () { | |||
44 | } | 44 | } |
45 | 45 | ||
46 | if (objectStorage) { | 46 | if (objectStorage) { |
47 | for (const webtorrentFile of video.files) { | 47 | for (const webVideoFile of video.files) { |
48 | expectStartWith(webtorrentFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | 48 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) |
49 | } | 49 | } |
50 | 50 | ||
51 | for (const hlsFile of video.streamingPlaylists[0].files) { | 51 | for (const hlsFile of video.streamingPlaylists[0].files) { |
diff --git a/server/tests/peertube-runner/vod-transcoding.ts b/server/tests/peertube-runner/vod-transcoding.ts index c3f41c097..eef6faf4e 100644 --- a/server/tests/peertube-runner/vod-transcoding.ts +++ b/server/tests/peertube-runner/vod-transcoding.ts | |||
@@ -24,13 +24,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
24 | let peertubeRunner: PeerTubeRunnerProcess | 24 | let peertubeRunner: PeerTubeRunnerProcess |
25 | 25 | ||
26 | function runSuite (options: { | 26 | function runSuite (options: { |
27 | webtorrentEnabled: boolean | 27 | webVideoEnabled: boolean |
28 | hlsEnabled: boolean | 28 | hlsEnabled: boolean |
29 | objectStorage?: ObjectStorageCommand | 29 | objectStorage?: ObjectStorageCommand |
30 | }) { | 30 | }) { |
31 | const { webtorrentEnabled, hlsEnabled, objectStorage } = options | 31 | const { webVideoEnabled, hlsEnabled, objectStorage } = options |
32 | 32 | ||
33 | const objectStorageBaseUrlWebTorrent = objectStorage | 33 | const objectStorageBaseUrlWebVideo = objectStorage |
34 | ? objectStorage.getMockWebVideosBaseUrl() | 34 | ? objectStorage.getMockWebVideosBaseUrl() |
35 | : undefined | 35 | : undefined |
36 | 36 | ||
@@ -46,13 +46,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
46 | await waitJobs(servers, { runnerJobs: true }) | 46 | await waitJobs(servers, { runnerJobs: true }) |
47 | 47 | ||
48 | for (const server of servers) { | 48 | for (const server of servers) { |
49 | if (webtorrentEnabled) { | 49 | if (webVideoEnabled) { |
50 | await completeWebVideoFilesCheck({ | 50 | await completeWebVideoFilesCheck({ |
51 | server, | 51 | server, |
52 | originServer: servers[0], | 52 | originServer: servers[0], |
53 | fixture: 'video_short.mp4', | 53 | fixture: 'video_short.mp4', |
54 | videoUUID: uuid, | 54 | videoUUID: uuid, |
55 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 55 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
56 | files: [ | 56 | files: [ |
57 | { resolution: 0 }, | 57 | { resolution: 0 }, |
58 | { resolution: 144 }, | 58 | { resolution: 144 }, |
@@ -66,7 +66,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
66 | 66 | ||
67 | if (hlsEnabled) { | 67 | if (hlsEnabled) { |
68 | await completeCheckHlsPlaylist({ | 68 | await completeCheckHlsPlaylist({ |
69 | hlsOnly: !webtorrentEnabled, | 69 | hlsOnly: !webVideoEnabled, |
70 | servers, | 70 | servers, |
71 | videoUUID: uuid, | 71 | videoUUID: uuid, |
72 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 72 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -84,13 +84,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
84 | await waitJobs(servers, { runnerJobs: true }) | 84 | await waitJobs(servers, { runnerJobs: true }) |
85 | 85 | ||
86 | for (const server of servers) { | 86 | for (const server of servers) { |
87 | if (webtorrentEnabled) { | 87 | if (webVideoEnabled) { |
88 | await completeWebVideoFilesCheck({ | 88 | await completeWebVideoFilesCheck({ |
89 | server, | 89 | server, |
90 | originServer: servers[0], | 90 | originServer: servers[0], |
91 | fixture: 'video_short.webm', | 91 | fixture: 'video_short.webm', |
92 | videoUUID: uuid, | 92 | videoUUID: uuid, |
93 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 93 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
94 | files: [ | 94 | files: [ |
95 | { resolution: 0 }, | 95 | { resolution: 0 }, |
96 | { resolution: 144 }, | 96 | { resolution: 144 }, |
@@ -104,7 +104,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
104 | 104 | ||
105 | if (hlsEnabled) { | 105 | if (hlsEnabled) { |
106 | await completeCheckHlsPlaylist({ | 106 | await completeCheckHlsPlaylist({ |
107 | hlsOnly: !webtorrentEnabled, | 107 | hlsOnly: !webVideoEnabled, |
108 | servers, | 108 | servers, |
109 | videoUUID: uuid, | 109 | videoUUID: uuid, |
110 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 110 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -123,13 +123,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
123 | await waitJobs(servers, { runnerJobs: true }) | 123 | await waitJobs(servers, { runnerJobs: true }) |
124 | 124 | ||
125 | for (const server of servers) { | 125 | for (const server of servers) { |
126 | if (webtorrentEnabled) { | 126 | if (webVideoEnabled) { |
127 | await completeWebVideoFilesCheck({ | 127 | await completeWebVideoFilesCheck({ |
128 | server, | 128 | server, |
129 | originServer: servers[0], | 129 | originServer: servers[0], |
130 | fixture: 'sample.ogg', | 130 | fixture: 'sample.ogg', |
131 | videoUUID: uuid, | 131 | videoUUID: uuid, |
132 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 132 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
133 | files: [ | 133 | files: [ |
134 | { resolution: 0 }, | 134 | { resolution: 0 }, |
135 | { resolution: 144 }, | 135 | { resolution: 144 }, |
@@ -142,7 +142,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
142 | 142 | ||
143 | if (hlsEnabled) { | 143 | if (hlsEnabled) { |
144 | await completeCheckHlsPlaylist({ | 144 | await completeCheckHlsPlaylist({ |
145 | hlsOnly: !webtorrentEnabled, | 145 | hlsOnly: !webVideoEnabled, |
146 | servers, | 146 | servers, |
147 | videoUUID: uuid, | 147 | videoUUID: uuid, |
148 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 148 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -159,13 +159,13 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
159 | 159 | ||
160 | await waitJobs(servers, { runnerJobs: true }) | 160 | await waitJobs(servers, { runnerJobs: true }) |
161 | 161 | ||
162 | if (webtorrentEnabled) { | 162 | if (webVideoEnabled) { |
163 | await completeWebVideoFilesCheck({ | 163 | await completeWebVideoFilesCheck({ |
164 | server: servers[0], | 164 | server: servers[0], |
165 | originServer: servers[0], | 165 | originServer: servers[0], |
166 | fixture: 'video_short.mp4', | 166 | fixture: 'video_short.mp4', |
167 | videoUUID: uuid, | 167 | videoUUID: uuid, |
168 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 168 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
169 | files: [ | 169 | files: [ |
170 | { resolution: 0 }, | 170 | { resolution: 0 }, |
171 | { resolution: 144 }, | 171 | { resolution: 144 }, |
@@ -179,7 +179,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
179 | 179 | ||
180 | if (hlsEnabled) { | 180 | if (hlsEnabled) { |
181 | await completeCheckHlsPlaylist({ | 181 | await completeCheckHlsPlaylist({ |
182 | hlsOnly: !webtorrentEnabled, | 182 | hlsOnly: !webVideoEnabled, |
183 | servers: [ servers[0] ], | 183 | servers: [ servers[0] ], |
184 | videoUUID: uuid, | 184 | videoUUID: uuid, |
185 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | 185 | objectStorageBaseUrl: objectStorageBaseUrlHLS, |
@@ -203,7 +203,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
203 | 203 | ||
204 | await servers[0].config.enableTranscoding(true, true, true) | 204 | await servers[0].config.enableTranscoding(true, true, true) |
205 | 205 | ||
206 | await servers[0].videos.runTranscoding({ transcodingType: 'webtorrent', videoId: uuid }) | 206 | await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuid }) |
207 | await waitJobs(servers, { runnerJobs: true }) | 207 | await waitJobs(servers, { runnerJobs: true }) |
208 | 208 | ||
209 | await completeWebVideoFilesCheck({ | 209 | await completeWebVideoFilesCheck({ |
@@ -211,7 +211,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
211 | originServer: servers[0], | 211 | originServer: servers[0], |
212 | fixture: 'video_short.mp4', | 212 | fixture: 'video_short.mp4', |
213 | videoUUID: uuid, | 213 | videoUUID: uuid, |
214 | objectStorageBaseUrl: objectStorageBaseUrlWebTorrent, | 214 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, |
215 | files: [ | 215 | files: [ |
216 | { resolution: 0 }, | 216 | { resolution: 0 }, |
217 | { resolution: 144 }, | 217 | { resolution: 144 }, |
@@ -262,7 +262,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
262 | await servers[0].config.enableTranscoding(true, false, true) | 262 | await servers[0].config.enableTranscoding(true, false, true) |
263 | }) | 263 | }) |
264 | 264 | ||
265 | runSuite({ webtorrentEnabled: true, hlsEnabled: false }) | 265 | runSuite({ webVideoEnabled: true, hlsEnabled: false }) |
266 | }) | 266 | }) |
267 | 267 | ||
268 | describe('HLS videos only enabled', function () { | 268 | describe('HLS videos only enabled', function () { |
@@ -271,7 +271,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
271 | await servers[0].config.enableTranscoding(false, true, true) | 271 | await servers[0].config.enableTranscoding(false, true, true) |
272 | }) | 272 | }) |
273 | 273 | ||
274 | runSuite({ webtorrentEnabled: false, hlsEnabled: true }) | 274 | runSuite({ webVideoEnabled: false, hlsEnabled: true }) |
275 | }) | 275 | }) |
276 | 276 | ||
277 | describe('Web video & HLS enabled', function () { | 277 | describe('Web video & HLS enabled', function () { |
@@ -280,7 +280,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
280 | await servers[0].config.enableTranscoding(true, true, true) | 280 | await servers[0].config.enableTranscoding(true, true, true) |
281 | }) | 281 | }) |
282 | 282 | ||
283 | runSuite({ webtorrentEnabled: true, hlsEnabled: true }) | 283 | runSuite({ webVideoEnabled: true, hlsEnabled: true }) |
284 | }) | 284 | }) |
285 | }) | 285 | }) |
286 | 286 | ||
@@ -306,7 +306,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
306 | await servers[0].config.enableTranscoding(true, false, true) | 306 | await servers[0].config.enableTranscoding(true, false, true) |
307 | }) | 307 | }) |
308 | 308 | ||
309 | runSuite({ webtorrentEnabled: true, hlsEnabled: false, objectStorage }) | 309 | runSuite({ webVideoEnabled: true, hlsEnabled: false, objectStorage }) |
310 | }) | 310 | }) |
311 | 311 | ||
312 | describe('HLS videos only enabled', function () { | 312 | describe('HLS videos only enabled', function () { |
@@ -315,7 +315,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
315 | await servers[0].config.enableTranscoding(false, true, true) | 315 | await servers[0].config.enableTranscoding(false, true, true) |
316 | }) | 316 | }) |
317 | 317 | ||
318 | runSuite({ webtorrentEnabled: false, hlsEnabled: true, objectStorage }) | 318 | runSuite({ webVideoEnabled: false, hlsEnabled: true, objectStorage }) |
319 | }) | 319 | }) |
320 | 320 | ||
321 | describe('Web video & HLS enabled', function () { | 321 | describe('Web video & HLS enabled', function () { |
@@ -324,7 +324,7 @@ describe('Test VOD transcoding in peertube-runner program', function () { | |||
324 | await servers[0].config.enableTranscoding(true, true, true) | 324 | await servers[0].config.enableTranscoding(true, true, true) |
325 | }) | 325 | }) |
326 | 326 | ||
327 | runSuite({ webtorrentEnabled: true, hlsEnabled: true, objectStorage }) | 327 | runSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage }) |
328 | }) | 328 | }) |
329 | 329 | ||
330 | after(async function () { | 330 | after(async function () { |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index a02a53c50..a75a8c8fa 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -493,7 +493,7 @@ describe('Test plugin filter hooks', function () { | |||
493 | await servers[0].config.updateCustomSubConfig({ | 493 | await servers[0].config.updateCustomSubConfig({ |
494 | newConfig: { | 494 | newConfig: { |
495 | transcoding: { | 495 | transcoding: { |
496 | webtorrent: { | 496 | webVideos: { |
497 | enabled: true | 497 | enabled: true |
498 | }, | 498 | }, |
499 | hls: { | 499 | hls: { |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index e951a1299..f5a0cbe85 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -302,11 +302,11 @@ describe('Test plugin helpers', function () { | |||
302 | 302 | ||
303 | // Video files check | 303 | // Video files check |
304 | { | 304 | { |
305 | expect(body.webtorrent.videoFiles).to.be.an('array') | 305 | expect(body.webVideo.videoFiles).to.be.an('array') |
306 | expect(body.hls.videoFiles).to.be.an('array') | 306 | expect(body.hls.videoFiles).to.be.an('array') |
307 | 307 | ||
308 | for (const resolution of [ 144, 240, 360, 480, 720 ]) { | 308 | for (const resolution of [ 144, 240, 360, 480, 720 ]) { |
309 | for (const files of [ body.webtorrent.videoFiles, body.hls.videoFiles ]) { | 309 | for (const files of [ body.webVideo.videoFiles, body.hls.videoFiles ]) { |
310 | const file = files.find(f => f.resolution === resolution) | 310 | const file = files.find(f => f.resolution === resolution) |
311 | expect(file).to.exist | 311 | expect(file).to.exist |
312 | 312 | ||
@@ -318,7 +318,7 @@ describe('Test plugin helpers', function () { | |||
318 | } | 318 | } |
319 | } | 319 | } |
320 | 320 | ||
321 | videoPath = body.webtorrent.videoFiles[0].path | 321 | videoPath = body.webVideo.videoFiles[0].path |
322 | } | 322 | } |
323 | 323 | ||
324 | // Thumbnails check | 324 | // Thumbnails check |
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts index 689eec5ac..21f82fbac 100644 --- a/server/tests/plugins/plugin-transcoding.ts +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -35,7 +35,7 @@ function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: st | |||
35 | hls: { | 35 | hls: { |
36 | enabled: true | 36 | enabled: true |
37 | }, | 37 | }, |
38 | webtorrent: { | 38 | webVideos: { |
39 | enabled: true | 39 | enabled: true |
40 | }, | 40 | }, |
41 | resolutions: { | 41 | resolutions: { |
@@ -247,7 +247,7 @@ describe('Test transcoding plugins', function () { | |||
247 | 247 | ||
248 | const video = await server.videos.get({ id: videoUUID }) | 248 | const video = await server.videos.get({ id: videoUUID }) |
249 | 249 | ||
250 | const path = server.servers.buildWebTorrentFilePath(video.files[0].fileUrl) | 250 | const path = server.servers.buildWebVideoFilePath(video.files[0].fileUrl) |
251 | const audioProbe = await getAudioStream(path) | 251 | const audioProbe = await getAudioStream(path) |
252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | 252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') |
253 | 253 | ||
diff --git a/server/tests/shared/checks.ts b/server/tests/shared/checks.ts index feaef37c6..90179c6ac 100644 --- a/server/tests/shared/checks.ts +++ b/server/tests/shared/checks.ts | |||
@@ -61,6 +61,16 @@ async function testImageSize (url: string, imageName: string, imageHTTPPath: str | |||
61 | expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') | 61 | expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') |
62 | } | 62 | } |
63 | 63 | ||
64 | async function testImageGeneratedByFFmpeg (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | ||
65 | if (process.env.ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS !== 'true') { | ||
66 | console.log( | ||
67 | 'Pixel comparison of image generated by ffmpeg is disabled. ' + | ||
68 | 'You can enable it using `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true env variable') | ||
69 | } | ||
70 | |||
71 | return testImage(url, imageName, imageHTTPPath, extension) | ||
72 | } | ||
73 | |||
64 | async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | 74 | async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { |
65 | const res = await makeGetRequest({ | 75 | const res = await makeGetRequest({ |
66 | url, | 76 | url, |
@@ -148,6 +158,7 @@ async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, du | |||
148 | 158 | ||
149 | export { | 159 | export { |
150 | dateIsValid, | 160 | dateIsValid, |
161 | testImageGeneratedByFFmpeg, | ||
151 | testImageSize, | 162 | testImageSize, |
152 | testImage, | 163 | testImage, |
153 | expectLogDoesNotContain, | 164 | expectLogDoesNotContain, |
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts index 856fabd11..e09bd60b5 100644 --- a/server/tests/shared/videos.ts +++ b/server/tests/shared/videos.ts | |||
@@ -7,7 +7,7 @@ import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO | |||
7 | import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils' | 7 | import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils' |
8 | import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models' | 8 | import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models' |
9 | import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands' | 9 | import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands' |
10 | import { dateIsValid, expectStartWith, testImage } from './checks' | 10 | import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks' |
11 | import { checkWebTorrentWorks } from './webtorrent' | 11 | import { checkWebTorrentWorks } from './webtorrent' |
12 | 12 | ||
13 | loadLanguages() | 13 | loadLanguages() |
@@ -28,7 +28,7 @@ async function completeWebVideoFilesCheck (options: { | |||
28 | const serverConfig = await originServer.config.getConfig() | 28 | const serverConfig = await originServer.config.getConfig() |
29 | const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL | 29 | const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL |
30 | 30 | ||
31 | const transcodingEnabled = serverConfig.transcoding.webtorrent.enabled | 31 | const transcodingEnabled = serverConfig.transcoding.web_videos.enabled |
32 | 32 | ||
33 | for (const attributeFile of files) { | 33 | for (const attributeFile of files) { |
34 | const file = video.files.find(f => f.resolution.id === attributeFile.resolution) | 34 | const file = video.files.find(f => f.resolution.id === attributeFile.resolution) |
@@ -51,11 +51,12 @@ async function completeWebVideoFilesCheck (options: { | |||
51 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`)) | 51 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`)) |
52 | 52 | ||
53 | if (objectStorageBaseUrl && requiresAuth) { | 53 | if (objectStorageBaseUrl && requiresAuth) { |
54 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/object-storage-proxy/webseed/${privatePath}${nameReg}${extension}`)) | 54 | const regexp = new RegExp(`${originServer.url}/object-storage-proxy/web-videos/${privatePath}${nameReg}${extension}`) |
55 | expect(file.fileUrl).to.match(regexp) | ||
55 | } else if (objectStorageBaseUrl) { | 56 | } else if (objectStorageBaseUrl) { |
56 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | 57 | expectStartWith(file.fileUrl, objectStorageBaseUrl) |
57 | } else { | 58 | } else { |
58 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/webseed/${privatePath}${nameReg}${extension}`)) | 59 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/web-videos/${privatePath}${nameReg}${extension}`)) |
59 | } | 60 | } |
60 | 61 | ||
61 | expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`)) | 62 | expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`)) |
@@ -197,11 +198,11 @@ async function completeVideoCheck (options: { | |||
197 | expect(video.downloadEnabled).to.equal(attributes.downloadEnabled) | 198 | expect(video.downloadEnabled).to.equal(attributes.downloadEnabled) |
198 | 199 | ||
199 | expect(video.thumbnailPath).to.exist | 200 | expect(video.thumbnailPath).to.exist |
200 | await testImage(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath) | 201 | await testImageGeneratedByFFmpeg(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath) |
201 | 202 | ||
202 | if (attributes.previewfile) { | 203 | if (attributes.previewfile) { |
203 | expect(video.previewPath).to.exist | 204 | expect(video.previewPath).to.exist |
204 | await testImage(server.url, attributes.previewfile, video.previewPath) | 205 | await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath) |
205 | } | 206 | } |
206 | 207 | ||
207 | await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) | 208 | await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) |
@@ -215,22 +216,22 @@ async function checkVideoFilesWereRemoved (options: { | |||
215 | }) { | 216 | }) { |
216 | const { video, server, captions = [], onlyVideoFiles = false } = options | 217 | const { video, server, captions = [], onlyVideoFiles = false } = options |
217 | 218 | ||
218 | const webtorrentFiles = video.files || [] | 219 | const webVideoFiles = video.files || [] |
219 | const hlsFiles = video.streamingPlaylists[0]?.files || [] | 220 | const hlsFiles = video.streamingPlaylists[0]?.files || [] |
220 | 221 | ||
221 | const thumbnailName = basename(video.thumbnailPath) | 222 | const thumbnailName = basename(video.thumbnailPath) |
222 | const previewName = basename(video.previewPath) | 223 | const previewName = basename(video.previewPath) |
223 | 224 | ||
224 | const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) | 225 | const torrentNames = webVideoFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) |
225 | 226 | ||
226 | const captionNames = captions.map(c => basename(c.captionPath)) | 227 | const captionNames = captions.map(c => basename(c.captionPath)) |
227 | 228 | ||
228 | const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl)) | 229 | const webVideoFilenames = webVideoFiles.map(f => basename(f.fileUrl)) |
229 | const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) | 230 | const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) |
230 | 231 | ||
231 | let directories: { [ directory: string ]: string[] } = { | 232 | let directories: { [ directory: string ]: string[] } = { |
232 | videos: webtorrentFilenames, | 233 | videos: webVideoFilenames, |
233 | redundancy: webtorrentFilenames, | 234 | redundancy: webVideoFilenames, |
234 | [join('playlists', 'hls')]: hlsFilenames, | 235 | [join('playlists', 'hls')]: hlsFilenames, |
235 | [join('redundancy', 'hls')]: hlsFilenames | 236 | [join('redundancy', 'hls')]: hlsFilenames |
236 | } | 237 | } |
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts index fd6c760b2..c24eb5233 100644 --- a/server/tools/peertube-redundancy.ts +++ b/server/tools/peertube-redundancy.ts | |||
@@ -65,19 +65,19 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | |||
65 | }) as any | 65 | }) as any |
66 | 66 | ||
67 | for (const redundancy of data) { | 67 | for (const redundancy of data) { |
68 | const webtorrentFiles = redundancy.redundancies.files | 68 | const webVideoFiles = redundancy.redundancies.files |
69 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists | 69 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists |
70 | 70 | ||
71 | let totalSize = '' | 71 | let totalSize = '' |
72 | if (target === 'remote-videos') { | 72 | if (target === 'remote-videos') { |
73 | const tmp = webtorrentFiles.concat(streamingPlaylists) | 73 | const tmp = webVideoFiles.concat(streamingPlaylists) |
74 | .reduce((a, b) => a + b.size, 0) | 74 | .reduce((a, b) => a + b.size, 0) |
75 | 75 | ||
76 | totalSize = bytes(tmp) | 76 | totalSize = bytes(tmp) |
77 | } | 77 | } |
78 | 78 | ||
79 | const instances = uniqify( | 79 | const instances = uniqify( |
80 | webtorrentFiles.concat(streamingPlaylists) | 80 | webVideoFiles.concat(streamingPlaylists) |
81 | .map(r => r.fileUrl) | 81 | .map(r => r.fileUrl) |
82 | .map(u => new URL(u).host) | 82 | .map(u => new URL(u).host) |
83 | ) | 83 | ) |
@@ -86,7 +86,7 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | |||
86 | redundancy.id.toString(), | 86 | redundancy.id.toString(), |
87 | redundancy.name, | 87 | redundancy.name, |
88 | redundancy.url, | 88 | redundancy.url, |
89 | webtorrentFiles.length, | 89 | webVideoFiles.length, |
90 | streamingPlaylists.length, | 90 | streamingPlaylists.length, |
91 | instances.join('\n'), | 91 | instances.join('\n'), |
92 | totalSize | 92 | totalSize |
diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 510b9f94e..9c1be9bd1 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | MVideoId, | 18 | MVideoId, |
19 | MVideoImmutable, | 19 | MVideoImmutable, |
20 | MVideoLiveFormattable, | 20 | MVideoLiveFormattable, |
21 | MVideoPassword, | ||
21 | MVideoPlaylistFull, | 22 | MVideoPlaylistFull, |
22 | MVideoPlaylistFullSummary | 23 | MVideoPlaylistFullSummary |
23 | } from '@server/types/models' | 24 | } from '@server/types/models' |
@@ -165,6 +166,8 @@ declare module 'express' { | |||
165 | videoCommentFull?: MCommentOwnerVideoReply | 166 | videoCommentFull?: MCommentOwnerVideoReply |
166 | videoCommentThread?: MComment | 167 | videoCommentThread?: MComment |
167 | 168 | ||
169 | videoPassword?: MVideoPassword | ||
170 | |||
168 | follow?: MActorFollowActorsDefault | 171 | follow?: MActorFollowActorsDefault |
169 | subscription?: MActorFollowActorsDefaultSubscription | 172 | subscription?: MActorFollowActorsDefaultSubscription |
170 | 173 | ||
diff --git a/server/types/models/video/index.ts b/server/types/models/video/index.ts index 6e45fcc79..7f05db666 100644 --- a/server/types/models/video/index.ts +++ b/server/types/models/video/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './local-video-viewer-watch-section' | 1 | export * from './local-video-viewer-watch-section' |
2 | export * from './local-video-viewer-watch-section' | 2 | export * from './local-video-viewer-watch-section' |
3 | export * from './local-video-viewer' | 3 | export * from './local-video-viewer' |
4 | export * from './storyboard' | ||
4 | export * from './schedule-video-update' | 5 | export * from './schedule-video-update' |
5 | export * from './tag' | 6 | export * from './tag' |
6 | export * from './thumbnail' | 7 | export * from './thumbnail' |
@@ -16,6 +17,7 @@ export * from './video-import' | |||
16 | export * from './video-live-replay-setting' | 17 | export * from './video-live-replay-setting' |
17 | export * from './video-live-session' | 18 | export * from './video-live-session' |
18 | export * from './video-live' | 19 | export * from './video-live' |
20 | export * from './video-password' | ||
19 | export * from './video-playlist' | 21 | export * from './video-playlist' |
20 | export * from './video-playlist-element' | 22 | export * from './video-playlist-element' |
21 | export * from './video-rate' | 23 | export * from './video-rate' |
diff --git a/server/types/models/video/storyboard.ts b/server/types/models/video/storyboard.ts new file mode 100644 index 000000000..a0403d4f0 --- /dev/null +++ b/server/types/models/video/storyboard.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
2 | import { PickWith } from '@shared/typescript-utils' | ||
3 | import { MVideo } from './video' | ||
4 | |||
5 | type Use<K extends keyof StoryboardModel, M> = PickWith<StoryboardModel, K, M> | ||
6 | |||
7 | // ############################################################################ | ||
8 | |||
9 | export type MStoryboard = Omit<StoryboardModel, 'Video'> | ||
10 | |||
11 | // ############################################################################ | ||
12 | |||
13 | export type MStoryboardVideo = | ||
14 | MStoryboard & | ||
15 | Use<'Video', MVideo> | ||
diff --git a/server/types/models/video/video-caption.ts b/server/types/models/video/video-caption.ts index 8cd801064..d3adec362 100644 --- a/server/types/models/video/video-caption.ts +++ b/server/types/models/video/video-caption.ts | |||
@@ -11,7 +11,7 @@ export type MVideoCaption = Omit<VideoCaptionModel, 'Video'> | |||
11 | // ############################################################################ | 11 | // ############################################################################ |
12 | 12 | ||
13 | export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'> | 13 | export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'> |
14 | export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'getFileUrl'> | 14 | export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'filename' | 'getFileUrl' | 'getCaptionStaticPath'> |
15 | 15 | ||
16 | export type MVideoCaptionVideo = | 16 | export type MVideoCaptionVideo = |
17 | MVideoCaption & | 17 | MVideoCaption & |
diff --git a/server/types/models/video/video-file.ts b/server/types/models/video/video-file.ts index 55603e59c..68106788d 100644 --- a/server/types/models/video/video-file.ts +++ b/server/types/models/video/video-file.ts | |||
@@ -38,6 +38,6 @@ export function isStreamingPlaylistFile (file: any): file is MVideoFileStreaming | |||
38 | return !!file.videoStreamingPlaylistId | 38 | return !!file.videoStreamingPlaylistId |
39 | } | 39 | } |
40 | 40 | ||
41 | export function isWebtorrentFile (file: any): file is MVideoFileVideo { | 41 | export function isWebVideoFile (file: any): file is MVideoFileVideo { |
42 | return !!file.videoId | 42 | return !!file.videoId |
43 | } | 43 | } |
diff --git a/server/types/models/video/video-password.ts b/server/types/models/video/video-password.ts new file mode 100644 index 000000000..313cc3e0c --- /dev/null +++ b/server/types/models/video/video-password.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
2 | |||
3 | export type MVideoPassword = Omit<VideoPasswordModel, 'Video'> | ||
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts index 58ae7baad..53ee94269 100644 --- a/server/types/models/video/video.ts +++ b/server/types/models/video/video.ts | |||
@@ -3,6 +3,7 @@ import { VideoModel } from '../../../models/video/video' | |||
3 | import { MTrackerUrl } from '../server/tracker' | 3 | import { MTrackerUrl } from '../server/tracker' |
4 | import { MUserVideoHistoryTime } from '../user/user-video-history' | 4 | import { MUserVideoHistoryTime } from '../user/user-video-history' |
5 | import { MScheduleVideoUpdate } from './schedule-video-update' | 5 | import { MScheduleVideoUpdate } from './schedule-video-update' |
6 | import { MStoryboard } from './storyboard' | ||
6 | import { MTag } from './tag' | 7 | import { MTag } from './tag' |
7 | import { MThumbnail } from './thumbnail' | 8 | import { MThumbnail } from './thumbnail' |
8 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' | 9 | import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' |
@@ -32,7 +33,7 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M> | |||
32 | export type MVideo = | 33 | export type MVideo = |
33 | Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | | 34 | Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | |
34 | 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | | 35 | 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | |
35 | 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive' | 'Trackers'> | 36 | 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions' | 'VideoLive' | 'Trackers' | 'VideoPasswords' | 'Storyboard'> |
36 | 37 | ||
37 | // ############################################################################ | 38 | // ############################################################################ |
38 | 39 | ||
@@ -46,7 +47,7 @@ export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> | |||
46 | 47 | ||
47 | // ############################################################################ | 48 | // ############################################################################ |
48 | 49 | ||
49 | // Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists | 50 | // Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists, passwords |
50 | 51 | ||
51 | // "With" to not confuse with the VideoFile model | 52 | // "With" to not confuse with the VideoFile model |
52 | export type MVideoWithFile = | 53 | export type MVideoWithFile = |
@@ -173,9 +174,10 @@ export type MVideoAP = | |||
173 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & | 174 | Use<'VideoBlacklist', MVideoBlacklistUnfederated> & |
174 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & | 175 | Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & |
175 | Use<'Thumbnails', MThumbnail[]> & | 176 | Use<'Thumbnails', MThumbnail[]> & |
176 | Use<'VideoLive', MVideoLive> | 177 | Use<'VideoLive', MVideoLive> & |
178 | Use<'Storyboard', MStoryboard> | ||
177 | 179 | ||
178 | export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> | 180 | export type MVideoAPLight = Omit<MVideoAP, 'VideoCaptions' | 'Storyboard'> |
179 | 181 | ||
180 | export type MVideoDetails = | 182 | export type MVideoDetails = |
181 | MVideo & | 183 | MVideo & |
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index df419fff4..103ef234b 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -41,7 +41,17 @@ export type PeerTubeHelpers = { | |||
41 | ffprobe: (path: string) => Promise<any> | 41 | ffprobe: (path: string) => Promise<any> |
42 | 42 | ||
43 | getFiles: (id: number | string) => Promise<{ | 43 | getFiles: (id: number | string) => Promise<{ |
44 | webtorrent: { | 44 | webtorrent: { // TODO: remove in v7 |
45 | videoFiles: { | ||
46 | path: string // Could be null if using remote storage | ||
47 | url: string | ||
48 | resolution: number | ||
49 | size: number | ||
50 | fps: number | ||
51 | }[] | ||
52 | } | ||
53 | |||
54 | webVideo: { | ||
45 | videoFiles: { | 55 | videoFiles: { |
46 | path: string // Could be null if using remote storage | 56 | path: string // Could be null if using remote storage |
47 | url: string | 57 | url: string |
diff --git a/shared/core-utils/common/array.ts b/shared/core-utils/common/array.ts index e1b422165..878ed1ffe 100644 --- a/shared/core-utils/common/array.ts +++ b/shared/core-utils/common/array.ts | |||
@@ -20,8 +20,22 @@ function uniqify <T> (elements: T[]) { | |||
20 | return Array.from(new Set(elements)) | 20 | return Array.from(new Set(elements)) |
21 | } | 21 | } |
22 | 22 | ||
23 | // Thanks: https://stackoverflow.com/a/12646864 | ||
24 | function shuffle <T> (elements: T[]) { | ||
25 | const shuffled = [ ...elements ] | ||
26 | |||
27 | for (let i = shuffled.length - 1; i > 0; i--) { | ||
28 | const j = Math.floor(Math.random() * (i + 1)); | ||
29 | |||
30 | [ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ] | ||
31 | } | ||
32 | |||
33 | return shuffled | ||
34 | } | ||
35 | |||
23 | export { | 36 | export { |
24 | uniqify, | 37 | uniqify, |
25 | findCommonElement, | 38 | findCommonElement, |
39 | shuffle, | ||
26 | arrayify | 40 | arrayify |
27 | } | 41 | } |
diff --git a/shared/core-utils/videos/common.ts b/shared/core-utils/videos/common.ts index 2c6efdb7f..0431edaaf 100644 --- a/shared/core-utils/videos/common.ts +++ b/shared/core-utils/videos/common.ts | |||
@@ -3,7 +3,7 @@ import { VideoPrivacy } from '../../models/videos/video-privacy.enum' | |||
3 | import { VideoDetails } from '../../models/videos/video.model' | 3 | import { VideoDetails } from '../../models/videos/video.model' |
4 | 4 | ||
5 | function getAllPrivacies () { | 5 | function getAllPrivacies () { |
6 | return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED ] | 6 | return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ] |
7 | } | 7 | } |
8 | 8 | ||
9 | function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) { | 9 | function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) { |
diff --git a/shared/ffmpeg/ffmpeg-images.ts b/shared/ffmpeg/ffmpeg-images.ts index 2db63bd8b..618fac7d1 100644 --- a/shared/ffmpeg/ffmpeg-images.ts +++ b/shared/ffmpeg/ffmpeg-images.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper' | 1 | import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper' |
2 | import { getVideoStreamDuration } from './ffprobe' | ||
2 | 3 | ||
3 | export class FFmpegImage { | 4 | export class FFmpegImage { |
4 | private readonly commandWrapper: FFmpegCommandWrapper | 5 | private readonly commandWrapper: FFmpegCommandWrapper |
@@ -36,24 +37,56 @@ export class FFmpegImage { | |||
36 | 37 | ||
37 | async generateThumbnailFromVideo (options: { | 38 | async generateThumbnailFromVideo (options: { |
38 | fromPath: string | 39 | fromPath: string |
39 | folder: string | 40 | output: string |
40 | imageName: string | ||
41 | }) { | 41 | }) { |
42 | const { fromPath, folder, imageName } = options | 42 | const { fromPath, output } = options |
43 | 43 | ||
44 | const pendingImageName = 'pending-' + imageName | 44 | let duration = await getVideoStreamDuration(fromPath) |
45 | if (isNaN(duration)) duration = 0 | ||
45 | 46 | ||
46 | const thumbnailOptions = { | 47 | this.commandWrapper.buildCommand(fromPath) |
47 | filename: pendingImageName, | 48 | .seekInput(duration / 2) |
48 | count: 1, | 49 | .videoFilter('thumbnail=500') |
49 | folder | 50 | .outputOption('-frames:v 1') |
51 | .output(output) | ||
52 | |||
53 | return this.commandWrapper.runCommand() | ||
54 | } | ||
55 | |||
56 | async generateStoryboardFromVideo (options: { | ||
57 | path: string | ||
58 | destination: string | ||
59 | |||
60 | sprites: { | ||
61 | size: { | ||
62 | width: number | ||
63 | height: number | ||
64 | } | ||
65 | |||
66 | count: { | ||
67 | width: number | ||
68 | height: number | ||
69 | } | ||
70 | |||
71 | duration: number | ||
50 | } | 72 | } |
73 | }) { | ||
74 | const { path, destination, sprites } = options | ||
75 | |||
76 | const command = this.commandWrapper.buildCommand(path) | ||
51 | 77 | ||
52 | return new Promise<string>((res, rej) => { | 78 | const filter = [ |
53 | this.commandWrapper.buildCommand(fromPath) | 79 | `setpts=N/round(FRAME_RATE)/TB`, |
54 | .on('error', rej) | 80 | `select='not(mod(t,${options.sprites.duration}))'`, |
55 | .on('end', () => res(imageName)) | 81 | `scale=${sprites.size.width}:${sprites.size.height}`, |
56 | .thumbnail(thumbnailOptions) | 82 | `tile=layout=${sprites.count.width}x${sprites.count.height}` |
57 | }) | 83 | ].join(',') |
84 | |||
85 | command.outputOption('-filter_complex', filter) | ||
86 | command.outputOption('-frames:v', '1') | ||
87 | command.outputOption('-q:v', '2') | ||
88 | command.output(destination) | ||
89 | |||
90 | return this.commandWrapper.runCommand() | ||
58 | } | 91 | } |
59 | } | 92 | } |
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index fd5d38316..10cf53ead 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts | |||
@@ -1,20 +1,34 @@ | |||
1 | import { ActivityPubActor } from './activitypub-actor' | 1 | import { ActivityPubActor } from './activitypub-actor' |
2 | import { ActivityPubSignature } from './activitypub-signature' | 2 | import { ActivityPubSignature } from './activitypub-signature' |
3 | import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects' | 3 | import { |
4 | import { AbuseObject } from './objects/abuse-object' | 4 | ActivityFlagReasonObject, |
5 | import { DislikeObject } from './objects/dislike-object' | 5 | ActivityObject, |
6 | import { APObject } from './objects/object.model' | 6 | APObjectId, |
7 | import { PlaylistObject } from './objects/playlist-object' | 7 | CacheFileObject, |
8 | import { VideoCommentObject } from './objects/video-comment-object' | 8 | PlaylistObject, |
9 | VideoCommentObject, | ||
10 | VideoObject, | ||
11 | WatchActionObject | ||
12 | } from './objects' | ||
13 | |||
14 | export type ActivityUpdateObject = | ||
15 | Extract<ActivityObject, VideoObject | CacheFileObject | PlaylistObject | ActivityPubActor | string> | ActivityPubActor | ||
16 | |||
17 | // Cannot Extract from Activity because of circular reference | ||
18 | export type ActivityUndoObject = | ||
19 | ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate<CacheFileObject | string> | ActivityAnnounce | ||
20 | |||
21 | export type ActivityCreateObject = | ||
22 | Extract<ActivityObject, VideoObject | CacheFileObject | WatchActionObject | VideoCommentObject | PlaylistObject | string> | ||
9 | 23 | ||
10 | export type Activity = | 24 | export type Activity = |
11 | ActivityCreate | | 25 | ActivityCreate<ActivityCreateObject> | |
12 | ActivityUpdate | | 26 | ActivityUpdate<ActivityUpdateObject> | |
13 | ActivityDelete | | 27 | ActivityDelete | |
14 | ActivityFollow | | 28 | ActivityFollow | |
15 | ActivityAccept | | 29 | ActivityAccept | |
16 | ActivityAnnounce | | 30 | ActivityAnnounce | |
17 | ActivityUndo | | 31 | ActivityUndo<ActivityUndoObject> | |
18 | ActivityLike | | 32 | ActivityLike | |
19 | ActivityReject | | 33 | ActivityReject | |
20 | ActivityView | | 34 | ActivityView | |
@@ -50,19 +64,19 @@ export interface BaseActivity { | |||
50 | signature?: ActivityPubSignature | 64 | signature?: ActivityPubSignature |
51 | } | 65 | } |
52 | 66 | ||
53 | export interface ActivityCreate extends BaseActivity { | 67 | export interface ActivityCreate <T extends ActivityCreateObject> extends BaseActivity { |
54 | type: 'Create' | 68 | type: 'Create' |
55 | object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | WatchActionObject | 69 | object: T |
56 | } | 70 | } |
57 | 71 | ||
58 | export interface ActivityUpdate extends BaseActivity { | 72 | export interface ActivityUpdate <T extends ActivityUpdateObject> extends BaseActivity { |
59 | type: 'Update' | 73 | type: 'Update' |
60 | object: VideoObject | ActivityPubActor | CacheFileObject | PlaylistObject | 74 | object: T |
61 | } | 75 | } |
62 | 76 | ||
63 | export interface ActivityDelete extends BaseActivity { | 77 | export interface ActivityDelete extends BaseActivity { |
64 | type: 'Delete' | 78 | type: 'Delete' |
65 | object: string | { id: string } | 79 | object: APObjectId |
66 | } | 80 | } |
67 | 81 | ||
68 | export interface ActivityFollow extends BaseActivity { | 82 | export interface ActivityFollow extends BaseActivity { |
@@ -82,23 +96,23 @@ export interface ActivityReject extends BaseActivity { | |||
82 | 96 | ||
83 | export interface ActivityAnnounce extends BaseActivity { | 97 | export interface ActivityAnnounce extends BaseActivity { |
84 | type: 'Announce' | 98 | type: 'Announce' |
85 | object: APObject | 99 | object: APObjectId |
86 | } | 100 | } |
87 | 101 | ||
88 | export interface ActivityUndo extends BaseActivity { | 102 | export interface ActivityUndo <T extends ActivityUndoObject> extends BaseActivity { |
89 | type: 'Undo' | 103 | type: 'Undo' |
90 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce | 104 | object: T |
91 | } | 105 | } |
92 | 106 | ||
93 | export interface ActivityLike extends BaseActivity { | 107 | export interface ActivityLike extends BaseActivity { |
94 | type: 'Like' | 108 | type: 'Like' |
95 | object: APObject | 109 | object: APObjectId |
96 | } | 110 | } |
97 | 111 | ||
98 | export interface ActivityView extends BaseActivity { | 112 | export interface ActivityView extends BaseActivity { |
99 | type: 'View' | 113 | type: 'View' |
100 | actor: string | 114 | actor: string |
101 | object: APObject | 115 | object: APObjectId |
102 | 116 | ||
103 | // If sending a "viewer" event | 117 | // If sending a "viewer" event |
104 | expires?: string | 118 | expires?: string |
@@ -108,13 +122,13 @@ export interface ActivityDislike extends BaseActivity { | |||
108 | id: string | 122 | id: string |
109 | type: 'Dislike' | 123 | type: 'Dislike' |
110 | actor: string | 124 | actor: string |
111 | object: APObject | 125 | object: APObjectId |
112 | } | 126 | } |
113 | 127 | ||
114 | export interface ActivityFlag extends BaseActivity { | 128 | export interface ActivityFlag extends BaseActivity { |
115 | type: 'Flag' | 129 | type: 'Flag' |
116 | content: string | 130 | content: string |
117 | object: APObject | APObject[] | 131 | object: APObjectId | APObjectId[] |
118 | tag?: ActivityFlagReasonObject[] | 132 | tag?: ActivityFlagReasonObject[] |
119 | startAt?: number | 133 | startAt?: number |
120 | endAt?: number | 134 | endAt?: number |
diff --git a/shared/models/activitypub/objects/activitypub-object.ts b/shared/models/activitypub/objects/activitypub-object.ts new file mode 100644 index 000000000..faeac2618 --- /dev/null +++ b/shared/models/activitypub/objects/activitypub-object.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import { AbuseObject } from './abuse-object' | ||
2 | import { CacheFileObject } from './cache-file-object' | ||
3 | import { PlaylistObject } from './playlist-object' | ||
4 | import { VideoCommentObject } from './video-comment-object' | ||
5 | import { VideoObject } from './video-object' | ||
6 | import { WatchActionObject } from './watch-action-object' | ||
7 | |||
8 | export type ActivityObject = | ||
9 | VideoObject | | ||
10 | AbuseObject | | ||
11 | VideoCommentObject | | ||
12 | CacheFileObject | | ||
13 | PlaylistObject | | ||
14 | WatchActionObject | | ||
15 | string | ||
16 | |||
17 | export type APObjectId = string | { id: string } | ||
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 9bf994379..db9c73658 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts | |||
@@ -114,10 +114,7 @@ export type ActivityUrlObject = | |||
114 | | ActivityVideoFileMetadataUrlObject | 114 | | ActivityVideoFileMetadataUrlObject |
115 | | ActivityTrackerUrlObject | 115 | | ActivityTrackerUrlObject |
116 | 116 | ||
117 | export interface ActivityPubAttributedTo { | 117 | export type ActivityPubAttributedTo = { type: 'Group' | 'Person', id: string } | string |
118 | type: 'Group' | 'Person' | ||
119 | id: string | ||
120 | } | ||
121 | 118 | ||
122 | export interface ActivityTombstoneObject { | 119 | export interface ActivityTombstoneObject { |
123 | '@context'?: any | 120 | '@context'?: any |
diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts deleted file mode 100644 index 7218fb784..000000000 --- a/shared/models/activitypub/objects/dislike-object.ts +++ /dev/null | |||
@@ -1,6 +0,0 @@ | |||
1 | export interface DislikeObject { | ||
2 | id: string | ||
3 | type: 'Dislike' | ||
4 | actor: string | ||
5 | object: string | ||
6 | } | ||
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index 9aa3c462c..753e02003 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts | |||
@@ -1,10 +1,9 @@ | |||
1 | export * from './abuse-object' | 1 | export * from './abuse-object' |
2 | export * from './activitypub-object' | ||
2 | export * from './cache-file-object' | 3 | export * from './cache-file-object' |
3 | export * from './common-objects' | 4 | export * from './common-objects' |
4 | export * from './dislike-object' | ||
5 | export * from './object.model' | ||
6 | export * from './playlist-element-object' | 5 | export * from './playlist-element-object' |
7 | export * from './playlist-object' | 6 | export * from './playlist-object' |
8 | export * from './video-comment-object' | 7 | export * from './video-comment-object' |
9 | export * from './video-torrent-object' | 8 | export * from './video-object' |
10 | export * from './watch-action-object' | 9 | export * from './watch-action-object' |
diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts deleted file mode 100644 index 3fd33800a..000000000 --- a/shared/models/activitypub/objects/object.model.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export type APObject = string | { id: string } | ||
diff --git a/shared/models/activitypub/objects/playlist-object.ts b/shared/models/activitypub/objects/playlist-object.ts index 842c03790..0ccb71828 100644 --- a/shared/models/activitypub/objects/playlist-object.ts +++ b/shared/models/activitypub/objects/playlist-object.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { ActivityIconObject } from './common-objects' | 1 | import { ActivityIconObject, ActivityPubAttributedTo } from './common-objects' |
2 | 2 | ||
3 | export interface PlaylistObject { | 3 | export interface PlaylistObject { |
4 | id: string | 4 | id: string |
@@ -12,7 +12,7 @@ export interface PlaylistObject { | |||
12 | uuid: string | 12 | uuid: string |
13 | 13 | ||
14 | totalItems: number | 14 | totalItems: number |
15 | attributedTo: string[] | 15 | attributedTo: ActivityPubAttributedTo[] |
16 | 16 | ||
17 | icon?: ActivityIconObject | 17 | icon?: ActivityIconObject |
18 | 18 | ||
diff --git a/shared/models/activitypub/objects/video-comment-object.ts b/shared/models/activitypub/objects/video-comment-object.ts index ba9001730..fb1e6f8db 100644 --- a/shared/models/activitypub/objects/video-comment-object.ts +++ b/shared/models/activitypub/objects/video-comment-object.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { ActivityTagObject } from './common-objects' | 1 | import { ActivityPubAttributedTo, ActivityTagObject } from './common-objects' |
2 | 2 | ||
3 | export interface VideoCommentObject { | 3 | export interface VideoCommentObject { |
4 | type: 'Note' | 4 | type: 'Note' |
@@ -11,6 +11,6 @@ export interface VideoCommentObject { | |||
11 | published: string | 11 | published: string |
12 | updated: string | 12 | updated: string |
13 | url: string | 13 | url: string |
14 | attributedTo: string | 14 | attributedTo: ActivityPubAttributedTo |
15 | tag: ActivityTagObject[] | 15 | tag: ActivityTagObject[] |
16 | } | 16 | } |
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-object.ts index 23d54bdbd..a252a2df0 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-object.ts | |||
@@ -51,6 +51,22 @@ export interface VideoObject { | |||
51 | 51 | ||
52 | attributedTo: ActivityPubAttributedTo[] | 52 | attributedTo: ActivityPubAttributedTo[] |
53 | 53 | ||
54 | preview?: ActivityPubStoryboard[] | ||
55 | |||
54 | to?: string[] | 56 | to?: string[] |
55 | cc?: string[] | 57 | cc?: string[] |
56 | } | 58 | } |
59 | |||
60 | export interface ActivityPubStoryboard { | ||
61 | type: 'Image' | ||
62 | rel: [ 'storyboard' ] | ||
63 | url: { | ||
64 | href: string | ||
65 | mediaType: string | ||
66 | width: number | ||
67 | height: number | ||
68 | tileWidth: number | ||
69 | tileHeight: number | ||
70 | tileDuration: string | ||
71 | }[] | ||
72 | } | ||
diff --git a/shared/models/metrics/playback-metric-create.model.ts b/shared/models/metrics/playback-metric-create.model.ts index d669ab690..3a8f328c8 100644 --- a/shared/models/metrics/playback-metric-create.model.ts +++ b/shared/models/metrics/playback-metric-create.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { VideoResolution } from '../videos' | 1 | import { VideoResolution } from '../videos' |
2 | 2 | ||
3 | export interface PlaybackMetricCreate { | 3 | export interface PlaybackMetricCreate { |
4 | playerMode: 'p2p-media-loader' | 'webtorrent' | 4 | playerMode: 'p2p-media-loader' | 'webtorrent' | 'web-video' // FIXME: remove webtorrent player mode not used anymore in PeerTube v6 |
5 | 5 | ||
6 | resolution?: VideoResolution | 6 | resolution?: VideoResolution |
7 | fps?: number | 7 | fps?: number |
diff --git a/shared/models/plugins/client/client-hook.model.ts b/shared/models/plugins/client/client-hook.model.ts index bc3f5dd9f..4a0818c99 100644 --- a/shared/models/plugins/client/client-hook.model.ts +++ b/shared/models/plugins/client/client-hook.model.ts | |||
@@ -59,6 +59,10 @@ export const clientFilterHookObject = { | |||
59 | 'filter:internal.video-watch.player.build-options.params': true, | 59 | 'filter:internal.video-watch.player.build-options.params': true, |
60 | 'filter:internal.video-watch.player.build-options.result': true, | 60 | 'filter:internal.video-watch.player.build-options.result': true, |
61 | 61 | ||
62 | // Filter the options to load a new video in our player | ||
63 | 'filter:internal.video-watch.player.load-options.params': true, | ||
64 | 'filter:internal.video-watch.player.load-options.result': true, | ||
65 | |||
62 | // Filter our SVG icons content | 66 | // Filter our SVG icons content |
63 | 'filter:internal.common.svg-icons.get-content.params': true, | 67 | 'filter:internal.common.svg-icons.get-content.params': true, |
64 | 'filter:internal.common.svg-icons.get-content.result': true, | 68 | 'filter:internal.common.svg-icons.get-content.result': true, |
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts index da479c928..2c52ca8cf 100644 --- a/shared/models/search/videos-common-query.model.ts +++ b/shared/models/search/videos-common-query.model.ts | |||
@@ -30,7 +30,9 @@ export interface VideosCommonQuery { | |||
30 | tagsAllOf?: string[] | 30 | tagsAllOf?: string[] |
31 | 31 | ||
32 | hasHLSFiles?: boolean | 32 | hasHLSFiles?: boolean |
33 | hasWebtorrentFiles?: boolean | 33 | |
34 | hasWebtorrentFiles?: boolean // TODO: remove in v7 | ||
35 | hasWebVideoFiles?: boolean | ||
34 | 36 | ||
35 | skipCount?: boolean | 37 | skipCount?: boolean |
36 | 38 | ||
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 4202589f3..9aa66f2b8 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -78,6 +78,10 @@ export interface CustomConfig { | |||
78 | torrents: { | 78 | torrents: { |
79 | size: number | 79 | size: number |
80 | } | 80 | } |
81 | |||
82 | storyboards: { | ||
83 | size: number | ||
84 | } | ||
81 | } | 85 | } |
82 | 86 | ||
83 | signup: { | 87 | signup: { |
@@ -129,7 +133,7 @@ export interface CustomConfig { | |||
129 | 133 | ||
130 | alwaysTranscodeOriginalResolution: boolean | 134 | alwaysTranscodeOriginalResolution: boolean |
131 | 135 | ||
132 | webtorrent: { | 136 | webVideos: { |
133 | enabled: boolean | 137 | enabled: boolean |
134 | } | 138 | } |
135 | 139 | ||
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 22ecee324..c14806dab 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -30,6 +30,7 @@ export type JobType = | |||
30 | | 'video-studio-edition' | 30 | | 'video-studio-edition' |
31 | | 'video-transcoding' | 31 | | 'video-transcoding' |
32 | | 'videos-views-stats' | 32 | | 'videos-views-stats' |
33 | | 'generate-video-storyboard' | ||
33 | 34 | ||
34 | export interface Job { | 35 | export interface Job { |
35 | id: number | string | 36 | id: number | string |
@@ -147,17 +148,17 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload { | |||
147 | fps: number | 148 | fps: number |
148 | copyCodecs: boolean | 149 | copyCodecs: boolean |
149 | 150 | ||
150 | deleteWebTorrentFiles: boolean | 151 | deleteWebVideoFiles: boolean |
151 | } | 152 | } |
152 | 153 | ||
153 | export interface NewWebTorrentResolutionTranscodingPayload extends BaseTranscodingPayload { | 154 | export interface NewWebVideoResolutionTranscodingPayload extends BaseTranscodingPayload { |
154 | type: 'new-resolution-to-webtorrent' | 155 | type: 'new-resolution-to-web-video' |
155 | resolution: VideoResolution | 156 | resolution: VideoResolution |
156 | fps: number | 157 | fps: number |
157 | } | 158 | } |
158 | 159 | ||
159 | export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { | 160 | export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { |
160 | type: 'merge-audio-to-webtorrent' | 161 | type: 'merge-audio-to-web-video' |
161 | 162 | ||
162 | resolution: VideoResolution | 163 | resolution: VideoResolution |
163 | fps: number | 164 | fps: number |
@@ -166,7 +167,7 @@ export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { | |||
166 | } | 167 | } |
167 | 168 | ||
168 | export interface OptimizeTranscodingPayload extends BaseTranscodingPayload { | 169 | export interface OptimizeTranscodingPayload extends BaseTranscodingPayload { |
169 | type: 'optimize-to-webtorrent' | 170 | type: 'optimize-to-web-video' |
170 | 171 | ||
171 | quickTranscode: boolean | 172 | quickTranscode: boolean |
172 | 173 | ||
@@ -175,7 +176,7 @@ export interface OptimizeTranscodingPayload extends BaseTranscodingPayload { | |||
175 | 176 | ||
176 | export type VideoTranscodingPayload = | 177 | export type VideoTranscodingPayload = |
177 | HLSTranscodingPayload | 178 | HLSTranscodingPayload |
178 | | NewWebTorrentResolutionTranscodingPayload | 179 | | NewWebVideoResolutionTranscodingPayload |
179 | | OptimizeTranscodingPayload | 180 | | OptimizeTranscodingPayload |
180 | | MergeAudioTranscodingPayload | 181 | | MergeAudioTranscodingPayload |
181 | 182 | ||
@@ -294,3 +295,10 @@ export interface TranscodingJobBuilderPayload { | |||
294 | priority?: number | 295 | priority?: number |
295 | }[][] | 296 | }[][] |
296 | } | 297 | } |
298 | |||
299 | // --------------------------------------------------------------------------- | ||
300 | |||
301 | export interface GenerateStoryboardPayload { | ||
302 | videoUUID: string | ||
303 | federate: boolean | ||
304 | } | ||
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 024ed35bf..288cf84cd 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -140,7 +140,7 @@ export interface ServerConfig { | |||
140 | enabled: boolean | 140 | enabled: boolean |
141 | } | 141 | } |
142 | 142 | ||
143 | webtorrent: { | 143 | web_videos: { |
144 | enabled: boolean | 144 | enabled: boolean |
145 | } | 145 | } |
146 | 146 | ||
diff --git a/shared/models/server/server-error-code.enum.ts b/shared/models/server/server-error-code.enum.ts index 2b093380c..77d1e1d3f 100644 --- a/shared/models/server/server-error-code.enum.ts +++ b/shared/models/server/server-error-code.enum.ts | |||
@@ -49,7 +49,10 @@ export const enum ServerErrorCode { | |||
49 | 49 | ||
50 | RUNNER_JOB_NOT_IN_PROCESSING_STATE = 'runner_job_not_in_processing_state', | 50 | RUNNER_JOB_NOT_IN_PROCESSING_STATE = 'runner_job_not_in_processing_state', |
51 | RUNNER_JOB_NOT_IN_PENDING_STATE = 'runner_job_not_in_pending_state', | 51 | RUNNER_JOB_NOT_IN_PENDING_STATE = 'runner_job_not_in_pending_state', |
52 | UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token' | 52 | UNKNOWN_RUNNER_TOKEN = 'unknown_runner_token', |
53 | |||
54 | VIDEO_REQUIRES_PASSWORD = 'video_requires_password', | ||
55 | INCORRECT_VIDEO_PASSWORD = 'incorrect_video_password' | ||
53 | } | 56 | } |
54 | 57 | ||
55 | /** | 58 | /** |
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts index f3cceb5f2..c1d5ffba4 100644 --- a/shared/models/users/user-update-me.model.ts +++ b/shared/models/users/user-update-me.model.ts | |||
@@ -5,8 +5,6 @@ export interface UserUpdateMe { | |||
5 | description?: string | 5 | description?: string |
6 | nsfwPolicy?: NSFWPolicyType | 6 | nsfwPolicy?: NSFWPolicyType |
7 | 7 | ||
8 | // FIXME: deprecated in favour of p2pEnabled in 4.1 | ||
9 | webTorrentEnabled?: boolean | ||
10 | p2pEnabled?: boolean | 8 | p2pEnabled?: boolean |
11 | 9 | ||
12 | autoPlayVideo?: boolean | 10 | autoPlayVideo?: boolean |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 0761c1e32..9de4118b4 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -22,8 +22,6 @@ export interface User { | |||
22 | autoPlayNextVideo: boolean | 22 | autoPlayNextVideo: boolean |
23 | autoPlayNextVideoPlaylist: boolean | 23 | autoPlayNextVideoPlaylist: boolean |
24 | 24 | ||
25 | // @deprecated in favour of p2pEnabled | ||
26 | webTorrentEnabled: boolean | ||
27 | p2pEnabled: boolean | 25 | p2pEnabled: boolean |
28 | 26 | ||
29 | videosHistoryEnabled: boolean | 27 | videosHistoryEnabled: boolean |
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 4c1790228..b3ce6ad3f 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -15,6 +15,7 @@ export * from './channel-sync' | |||
15 | 15 | ||
16 | export * from './nsfw-policy.type' | 16 | export * from './nsfw-policy.type' |
17 | 17 | ||
18 | export * from './storyboard.model' | ||
18 | export * from './thumbnail.type' | 19 | export * from './thumbnail.type' |
19 | 20 | ||
20 | export * from './video-constant.model' | 21 | export * from './video-constant.model' |
@@ -39,3 +40,4 @@ export * from './video-update.model' | |||
39 | export * from './video-view.model' | 40 | export * from './video-view.model' |
40 | export * from './video.model' | 41 | export * from './video.model' |
41 | export * from './video-create-result.model' | 42 | export * from './video-create-result.model' |
43 | export * from './video-password.model' | ||
diff --git a/shared/models/videos/storyboard.model.ts b/shared/models/videos/storyboard.model.ts new file mode 100644 index 000000000..c92c81f09 --- /dev/null +++ b/shared/models/videos/storyboard.model.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | export interface Storyboard { | ||
2 | storyboardPath: string | ||
3 | |||
4 | totalHeight: number | ||
5 | totalWidth: number | ||
6 | |||
7 | spriteHeight: number | ||
8 | spriteWidth: number | ||
9 | |||
10 | spriteDuration: number | ||
11 | } | ||
diff --git a/shared/models/videos/transcoding/video-transcoding-create.model.ts b/shared/models/videos/transcoding/video-transcoding-create.model.ts index aeb393e57..c6e756a0a 100644 --- a/shared/models/videos/transcoding/video-transcoding-create.model.ts +++ b/shared/models/videos/transcoding/video-transcoding-create.model.ts | |||
@@ -1,3 +1,3 @@ | |||
1 | export interface VideoTranscodingCreate { | 1 | export interface VideoTranscodingCreate { |
2 | transcodingType: 'hls' | 'webtorrent' | 2 | transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7 |
3 | } | 3 | } |
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 732d508d1..7a34b5afe 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -18,6 +18,7 @@ export interface VideoCreate { | |||
18 | privacy: VideoPrivacy | 18 | privacy: VideoPrivacy |
19 | scheduleUpdate?: VideoScheduleUpdate | 19 | scheduleUpdate?: VideoScheduleUpdate |
20 | originallyPublishedAt?: Date | string | 20 | originallyPublishedAt?: Date | string |
21 | videoPasswords?: string[] | ||
21 | 22 | ||
22 | thumbnailfile?: Blob | string | 23 | thumbnailfile?: Blob | string |
23 | previewfile?: Blob | string | 24 | previewfile?: Blob | string |
diff --git a/shared/models/videos/video-password.model.ts b/shared/models/videos/video-password.model.ts new file mode 100644 index 000000000..c0280b9b9 --- /dev/null +++ b/shared/models/videos/video-password.model.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export interface VideoPassword { | ||
2 | id: number | ||
3 | password: string | ||
4 | videoId: number | ||
5 | createdAt: Date | string | ||
6 | updatedAt: Date | string | ||
7 | } | ||
diff --git a/shared/models/videos/video-privacy.enum.ts b/shared/models/videos/video-privacy.enum.ts index 39fd0529f..12e1d196f 100644 --- a/shared/models/videos/video-privacy.enum.ts +++ b/shared/models/videos/video-privacy.enum.ts | |||
@@ -2,5 +2,6 @@ export const enum VideoPrivacy { | |||
2 | PUBLIC = 1, | 2 | PUBLIC = 1, |
3 | UNLISTED = 2, | 3 | UNLISTED = 2, |
4 | PRIVATE = 3, | 4 | PRIVATE = 3, |
5 | INTERNAL = 4 | 5 | INTERNAL = 4, |
6 | PASSWORD_PROTECTED = 5 | ||
6 | } | 7 | } |
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts index 86653b959..43537b5af 100644 --- a/shared/models/videos/video-update.model.ts +++ b/shared/models/videos/video-update.model.ts | |||
@@ -19,6 +19,7 @@ export interface VideoUpdate { | |||
19 | previewfile?: Blob | 19 | previewfile?: Blob |
20 | scheduleUpdate?: VideoScheduleUpdate | 20 | scheduleUpdate?: VideoScheduleUpdate |
21 | originallyPublishedAt?: Date | string | 21 | originallyPublishedAt?: Date | string |
22 | videoPasswords?: string[] | ||
22 | 23 | ||
23 | pluginData?: any | 24 | pluginData?: any |
24 | } | 25 | } |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 06ffb327c..9004efb35 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -7,7 +7,7 @@ import { VideoScheduleUpdate } from './video-schedule-update.model' | |||
7 | import { VideoState } from './video-state.enum' | 7 | import { VideoState } from './video-state.enum' |
8 | import { VideoStreamingPlaylist } from './video-streaming-playlist.model' | 8 | import { VideoStreamingPlaylist } from './video-streaming-playlist.model' |
9 | 9 | ||
10 | export interface Video { | 10 | export interface Video extends Partial<VideoAdditionalAttributes> { |
11 | id: number | 11 | id: number |
12 | uuid: string | 12 | uuid: string |
13 | shortUUID: string | 13 | shortUUID: string |
@@ -57,20 +57,22 @@ export interface Video { | |||
57 | } | 57 | } |
58 | 58 | ||
59 | pluginData?: any | 59 | pluginData?: any |
60 | } | ||
60 | 61 | ||
61 | // Additional attributes dependending on the query | 62 | // Not included by default, needs query params |
62 | waitTranscoding?: boolean | 63 | export interface VideoAdditionalAttributes { |
63 | state?: VideoConstant<VideoState> | 64 | waitTranscoding: boolean |
64 | scheduledUpdate?: VideoScheduleUpdate | 65 | state: VideoConstant<VideoState> |
66 | scheduledUpdate: VideoScheduleUpdate | ||
65 | 67 | ||
66 | blacklisted?: boolean | 68 | blacklisted: boolean |
67 | blacklistedReason?: string | 69 | blacklistedReason: string |
68 | 70 | ||
69 | blockedOwner?: boolean | 71 | blockedOwner: boolean |
70 | blockedServer?: boolean | 72 | blockedServer: boolean |
71 | 73 | ||
72 | files?: VideoFile[] | 74 | files: VideoFile[] |
73 | streamingPlaylists?: VideoStreamingPlaylist[] | 75 | streamingPlaylists: VideoStreamingPlaylist[] |
74 | } | 76 | } |
75 | 77 | ||
76 | export interface VideoDetails extends Video { | 78 | export interface VideoDetails extends Video { |
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts index e3f1817f1..8227017eb 100644 --- a/shared/server-commands/requests/requests.ts +++ b/shared/server-commands/requests/requests.ts | |||
@@ -29,6 +29,7 @@ function makeRawRequest (options: { | |||
29 | range?: string | 29 | range?: string |
30 | query?: { [ id: string ]: string } | 30 | query?: { [ id: string ]: string } |
31 | method?: 'GET' | 'POST' | 31 | method?: 'GET' | 'POST' |
32 | headers?: { [ name: string ]: string } | ||
32 | }) { | 33 | }) { |
33 | const { host, protocol, pathname } = new URL(options.url) | 34 | const { host, protocol, pathname } = new URL(options.url) |
34 | 35 | ||
@@ -37,7 +38,7 @@ function makeRawRequest (options: { | |||
37 | path: pathname, | 38 | path: pathname, |
38 | contentType: undefined, | 39 | contentType: undefined, |
39 | 40 | ||
40 | ...pick(options, [ 'expectedStatus', 'range', 'token', 'query' ]) | 41 | ...pick(options, [ 'expectedStatus', 'range', 'token', 'query', 'headers' ]) |
41 | } | 42 | } |
42 | 43 | ||
43 | if (options.method === 'POST') { | 44 | if (options.method === 'POST') { |
@@ -132,6 +133,7 @@ function makePutBodyRequest (options: { | |||
132 | token?: string | 133 | token?: string |
133 | fields: { [ fieldName: string ]: any } | 134 | fields: { [ fieldName: string ]: any } |
134 | expectedStatus?: HttpStatusCode | 135 | expectedStatus?: HttpStatusCode |
136 | headers?: { [name: string]: string } | ||
135 | }) { | 137 | }) { |
136 | const req = request(options.url).put(options.path) | 138 | const req = request(options.url).put(options.path) |
137 | .send(options.fields) | 139 | .send(options.fields) |
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts index b94bd2625..7f1e9d977 100644 --- a/shared/server-commands/server/config-command.ts +++ b/shared/server-commands/server/config-command.ts | |||
@@ -131,7 +131,7 @@ export class ConfigCommand extends AbstractCommand { | |||
131 | } | 131 | } |
132 | 132 | ||
133 | // TODO: convert args to object | 133 | // TODO: convert args to object |
134 | enableTranscoding (webtorrent = true, hls = true, with0p = false) { | 134 | enableTranscoding (webVideo = true, hls = true, with0p = false) { |
135 | return this.updateExistingSubConfig({ | 135 | return this.updateExistingSubConfig({ |
136 | newConfig: { | 136 | newConfig: { |
137 | transcoding: { | 137 | transcoding: { |
@@ -142,8 +142,8 @@ export class ConfigCommand extends AbstractCommand { | |||
142 | 142 | ||
143 | resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p), | 143 | resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p), |
144 | 144 | ||
145 | webtorrent: { | 145 | webVideos: { |
146 | enabled: webtorrent | 146 | enabled: webVideo |
147 | }, | 147 | }, |
148 | hls: { | 148 | hls: { |
149 | enabled: hls | 149 | enabled: hls |
@@ -154,19 +154,23 @@ export class ConfigCommand extends AbstractCommand { | |||
154 | } | 154 | } |
155 | 155 | ||
156 | // TODO: convert args to object | 156 | // TODO: convert args to object |
157 | enableMinimumTranscoding (webtorrent = true, hls = true) { | 157 | enableMinimumTranscoding (webVideo = true, hls = true) { |
158 | return this.updateExistingSubConfig({ | 158 | return this.updateExistingSubConfig({ |
159 | newConfig: { | 159 | newConfig: { |
160 | transcoding: { | 160 | transcoding: { |
161 | enabled: true, | 161 | enabled: true, |
162 | |||
163 | allowAudioFiles: true, | ||
164 | allowAdditionalExtensions: true, | ||
165 | |||
162 | resolutions: { | 166 | resolutions: { |
163 | ...ConfigCommand.getCustomConfigResolutions(false), | 167 | ...ConfigCommand.getCustomConfigResolutions(false), |
164 | 168 | ||
165 | '240p': true | 169 | '240p': true |
166 | }, | 170 | }, |
167 | 171 | ||
168 | webtorrent: { | 172 | webVideos: { |
169 | enabled: webtorrent | 173 | enabled: webVideo |
170 | }, | 174 | }, |
171 | hls: { | 175 | hls: { |
172 | enabled: hls | 176 | enabled: hls |
@@ -368,6 +372,9 @@ export class ConfigCommand extends AbstractCommand { | |||
368 | }, | 372 | }, |
369 | torrents: { | 373 | torrents: { |
370 | size: 4 | 374 | size: 4 |
375 | }, | ||
376 | storyboards: { | ||
377 | size: 5 | ||
371 | } | 378 | } |
372 | }, | 379 | }, |
373 | signup: { | 380 | signup: { |
@@ -417,7 +424,7 @@ export class ConfigCommand extends AbstractCommand { | |||
417 | '2160p': false | 424 | '2160p': false |
418 | }, | 425 | }, |
419 | alwaysTranscodeOriginalResolution: true, | 426 | alwaysTranscodeOriginalResolution: true, |
420 | webtorrent: { | 427 | webVideos: { |
421 | enabled: true | 428 | enabled: true |
422 | }, | 429 | }, |
423 | hls: { | 430 | hls: { |
diff --git a/shared/server-commands/server/jobs.ts b/shared/server-commands/server/jobs.ts index ff3098063..8f131fba4 100644 --- a/shared/server-commands/server/jobs.ts +++ b/shared/server-commands/server/jobs.ts | |||
@@ -33,6 +33,8 @@ async function waitJobs ( | |||
33 | 33 | ||
34 | // Check if each server has pending request | 34 | // Check if each server has pending request |
35 | for (const server of servers) { | 35 | for (const server of servers) { |
36 | if (process.env.DEBUG) console.log('Checking ' + server.url) | ||
37 | |||
36 | for (const state of states) { | 38 | for (const state of states) { |
37 | 39 | ||
38 | const jobPromise = server.jobs.list({ | 40 | const jobPromise = server.jobs.list({ |
@@ -45,6 +47,10 @@ async function waitJobs ( | |||
45 | .then(jobs => { | 47 | .then(jobs => { |
46 | if (jobs.length !== 0) { | 48 | if (jobs.length !== 0) { |
47 | pendingRequests = true | 49 | pendingRequests = true |
50 | |||
51 | if (process.env.DEBUG) { | ||
52 | console.log(jobs) | ||
53 | } | ||
48 | } | 54 | } |
49 | }) | 55 | }) |
50 | 56 | ||
@@ -55,6 +61,10 @@ async function waitJobs ( | |||
55 | .then(obj => { | 61 | .then(obj => { |
56 | if (obj.activityPubMessagesWaiting !== 0) { | 62 | if (obj.activityPubMessagesWaiting !== 0) { |
57 | pendingRequests = true | 63 | pendingRequests = true |
64 | |||
65 | if (process.env.DEBUG) { | ||
66 | console.log('AP messages waiting: ' + obj.activityPubMessagesWaiting) | ||
67 | } | ||
58 | } | 68 | } |
59 | }) | 69 | }) |
60 | tasks.push(debugPromise) | 70 | tasks.push(debugPromise) |
@@ -65,12 +75,15 @@ async function waitJobs ( | |||
65 | for (const job of data) { | 75 | for (const job of data) { |
66 | if (job.state.id !== RunnerJobState.COMPLETED) { | 76 | if (job.state.id !== RunnerJobState.COMPLETED) { |
67 | pendingRequests = true | 77 | pendingRequests = true |
78 | |||
79 | if (process.env.DEBUG) { | ||
80 | console.log(job) | ||
81 | } | ||
68 | } | 82 | } |
69 | } | 83 | } |
70 | }) | 84 | }) |
71 | tasks.push(runnerJobsPromise) | 85 | tasks.push(runnerJobsPromise) |
72 | } | 86 | } |
73 | |||
74 | } | 87 | } |
75 | 88 | ||
76 | return tasks | 89 | return tasks |
diff --git a/shared/server-commands/server/object-storage-command.ts b/shared/server-commands/server/object-storage-command.ts index 7d8ec93cd..6bb232c36 100644 --- a/shared/server-commands/server/object-storage-command.ts +++ b/shared/server-commands/server/object-storage-command.ts | |||
@@ -42,7 +42,7 @@ export class ObjectStorageCommand { | |||
42 | bucket_name: this.getMockStreamingPlaylistsBucketName() | 42 | bucket_name: this.getMockStreamingPlaylistsBucketName() |
43 | }, | 43 | }, |
44 | 44 | ||
45 | videos: { | 45 | web_videos: { |
46 | bucket_name: this.getMockWebVideosBucketName() | 46 | bucket_name: this.getMockWebVideosBucketName() |
47 | } | 47 | } |
48 | } | 48 | } |
@@ -136,9 +136,9 @@ export class ObjectStorageCommand { | |||
136 | prefix: `test:server-${serverNumber}-streaming-playlists:` | 136 | prefix: `test:server-${serverNumber}-streaming-playlists:` |
137 | }, | 137 | }, |
138 | 138 | ||
139 | videos: { | 139 | web_videos: { |
140 | bucket_name: this.DEFAULT_SCALEWAY_BUCKET, | 140 | bucket_name: this.DEFAULT_SCALEWAY_BUCKET, |
141 | prefix: `test:server-${serverNumber}-videos:` | 141 | prefix: `test:server-${serverNumber}-web-videos:` |
142 | } | 142 | } |
143 | } | 143 | } |
144 | } | 144 | } |
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts index 70f7a3ee2..38568a890 100644 --- a/shared/server-commands/server/server.ts +++ b/shared/server-commands/server/server.ts | |||
@@ -32,8 +32,10 @@ import { | |||
32 | HistoryCommand, | 32 | HistoryCommand, |
33 | ImportsCommand, | 33 | ImportsCommand, |
34 | LiveCommand, | 34 | LiveCommand, |
35 | VideoPasswordsCommand, | ||
35 | PlaylistsCommand, | 36 | PlaylistsCommand, |
36 | ServicesCommand, | 37 | ServicesCommand, |
38 | StoryboardCommand, | ||
37 | StreamingPlaylistsCommand, | 39 | StreamingPlaylistsCommand, |
38 | VideosCommand, | 40 | VideosCommand, |
39 | VideoStudioCommand, | 41 | VideoStudioCommand, |
@@ -146,6 +148,9 @@ export class PeerTubeServer { | |||
146 | twoFactor?: TwoFactorCommand | 148 | twoFactor?: TwoFactorCommand |
147 | videoToken?: VideoTokenCommand | 149 | videoToken?: VideoTokenCommand |
148 | registrations?: RegistrationsCommand | 150 | registrations?: RegistrationsCommand |
151 | videoPasswords?: VideoPasswordsCommand | ||
152 | |||
153 | storyboard?: StoryboardCommand | ||
149 | 154 | ||
150 | runners?: RunnersCommand | 155 | runners?: RunnersCommand |
151 | runnerRegistrationTokens?: RunnerRegistrationTokensCommand | 156 | runnerRegistrationTokens?: RunnerRegistrationTokensCommand |
@@ -232,7 +237,7 @@ export class PeerTubeServer { | |||
232 | } | 237 | } |
233 | 238 | ||
234 | // Share the environment | 239 | // Share the environment |
235 | const env = Object.create(process.env) | 240 | const env = { ...process.env } |
236 | env['NODE_ENV'] = 'test' | 241 | env['NODE_ENV'] = 'test' |
237 | env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString() | 242 | env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString() |
238 | env['NODE_CONFIG'] = JSON.stringify(configOverride) | 243 | env['NODE_CONFIG'] = JSON.stringify(configOverride) |
@@ -365,12 +370,13 @@ export class PeerTubeServer { | |||
365 | tmp_persistent: this.getDirectoryPath('tmp-persistent') + '/', | 370 | tmp_persistent: this.getDirectoryPath('tmp-persistent') + '/', |
366 | bin: this.getDirectoryPath('bin') + '/', | 371 | bin: this.getDirectoryPath('bin') + '/', |
367 | avatars: this.getDirectoryPath('avatars') + '/', | 372 | avatars: this.getDirectoryPath('avatars') + '/', |
368 | videos: this.getDirectoryPath('videos') + '/', | 373 | web_videos: this.getDirectoryPath('web-videos') + '/', |
369 | streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/', | 374 | streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/', |
370 | redundancy: this.getDirectoryPath('redundancy') + '/', | 375 | redundancy: this.getDirectoryPath('redundancy') + '/', |
371 | logs: this.getDirectoryPath('logs') + '/', | 376 | logs: this.getDirectoryPath('logs') + '/', |
372 | previews: this.getDirectoryPath('previews') + '/', | 377 | previews: this.getDirectoryPath('previews') + '/', |
373 | thumbnails: this.getDirectoryPath('thumbnails') + '/', | 378 | thumbnails: this.getDirectoryPath('thumbnails') + '/', |
379 | storyboards: this.getDirectoryPath('storyboards') + '/', | ||
374 | torrents: this.getDirectoryPath('torrents') + '/', | 380 | torrents: this.getDirectoryPath('torrents') + '/', |
375 | captions: this.getDirectoryPath('captions') + '/', | 381 | captions: this.getDirectoryPath('captions') + '/', |
376 | cache: this.getDirectoryPath('cache') + '/', | 382 | cache: this.getDirectoryPath('cache') + '/', |
@@ -434,8 +440,11 @@ export class PeerTubeServer { | |||
434 | this.videoToken = new VideoTokenCommand(this) | 440 | this.videoToken = new VideoTokenCommand(this) |
435 | this.registrations = new RegistrationsCommand(this) | 441 | this.registrations = new RegistrationsCommand(this) |
436 | 442 | ||
443 | this.storyboard = new StoryboardCommand(this) | ||
444 | |||
437 | this.runners = new RunnersCommand(this) | 445 | this.runners = new RunnersCommand(this) |
438 | this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this) | 446 | this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this) |
439 | this.runnerJobs = new RunnerJobsCommand(this) | 447 | this.runnerJobs = new RunnerJobsCommand(this) |
448 | this.videoPasswords = new VideoPasswordsCommand(this) | ||
440 | } | 449 | } |
441 | } | 450 | } |
diff --git a/shared/server-commands/server/servers-command.ts b/shared/server-commands/server/servers-command.ts index 19645cb93..c91c2b008 100644 --- a/shared/server-commands/server/servers-command.ts +++ b/shared/server-commands/server/servers-command.ts | |||
@@ -77,8 +77,8 @@ export class ServersCommand extends AbstractCommand { | |||
77 | return join(root(), 'test' + this.server.internalServerNumber, directory) | 77 | return join(root(), 'test' + this.server.internalServerNumber, directory) |
78 | } | 78 | } |
79 | 79 | ||
80 | buildWebTorrentFilePath (fileUrl: string) { | 80 | buildWebVideoFilePath (fileUrl: string) { |
81 | return this.buildDirectory(join('videos', basename(fileUrl))) | 81 | return this.buildDirectory(join('web-videos', basename(fileUrl))) |
82 | } | 82 | } |
83 | 83 | ||
84 | buildFragmentedFilePath (videoUUID: string, fileUrl: string) { | 84 | buildFragmentedFilePath (videoUUID: string, fileUrl: string) { |
diff --git a/shared/server-commands/shared/abstract-command.ts b/shared/server-commands/shared/abstract-command.ts index ca4ffada9..463acc26b 100644 --- a/shared/server-commands/shared/abstract-command.ts +++ b/shared/server-commands/shared/abstract-command.ts | |||
@@ -101,25 +101,29 @@ abstract class AbstractCommand { | |||
101 | 101 | ||
102 | protected putBodyRequest (options: InternalCommonCommandOptions & { | 102 | protected putBodyRequest (options: InternalCommonCommandOptions & { |
103 | fields?: { [ fieldName: string ]: any } | 103 | fields?: { [ fieldName: string ]: any } |
104 | headers?: { [name: string]: string } | ||
104 | }) { | 105 | }) { |
105 | const { fields } = options | 106 | const { fields, headers } = options |
106 | 107 | ||
107 | return makePutBodyRequest({ | 108 | return makePutBodyRequest({ |
108 | ...this.buildCommonRequestOptions(options), | 109 | ...this.buildCommonRequestOptions(options), |
109 | 110 | ||
110 | fields | 111 | fields, |
112 | headers | ||
111 | }) | 113 | }) |
112 | } | 114 | } |
113 | 115 | ||
114 | protected postBodyRequest (options: InternalCommonCommandOptions & { | 116 | protected postBodyRequest (options: InternalCommonCommandOptions & { |
115 | fields?: { [ fieldName: string ]: any } | 117 | fields?: { [ fieldName: string ]: any } |
118 | headers?: { [name: string]: string } | ||
116 | }) { | 119 | }) { |
117 | const { fields } = options | 120 | const { fields, headers } = options |
118 | 121 | ||
119 | return makePostBodyRequest({ | 122 | return makePostBodyRequest({ |
120 | ...this.buildCommonRequestOptions(options), | 123 | ...this.buildCommonRequestOptions(options), |
121 | 124 | ||
122 | fields | 125 | fields, |
126 | headers | ||
123 | }) | 127 | }) |
124 | } | 128 | } |
125 | 129 | ||
@@ -206,6 +210,12 @@ abstract class AbstractCommand { | |||
206 | 210 | ||
207 | return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus | 211 | return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus |
208 | } | 212 | } |
213 | |||
214 | protected buildVideoPasswordHeader (videoPassword: string) { | ||
215 | return videoPassword !== undefined && videoPassword !== null | ||
216 | ? { 'x-peertube-video-password': videoPassword } | ||
217 | : undefined | ||
218 | } | ||
209 | } | 219 | } |
210 | 220 | ||
211 | export { | 221 | export { |
diff --git a/shared/server-commands/videos/captions-command.ts b/shared/server-commands/videos/captions-command.ts index 62bf9c5e6..a26fcb57d 100644 --- a/shared/server-commands/videos/captions-command.ts +++ b/shared/server-commands/videos/captions-command.ts | |||
@@ -34,14 +34,16 @@ export class CaptionsCommand extends AbstractCommand { | |||
34 | 34 | ||
35 | list (options: OverrideCommandOptions & { | 35 | list (options: OverrideCommandOptions & { |
36 | videoId: string | number | 36 | videoId: string | number |
37 | videoPassword?: string | ||
37 | }) { | 38 | }) { |
38 | const { videoId } = options | 39 | const { videoId, videoPassword } = options |
39 | const path = '/api/v1/videos/' + videoId + '/captions' | 40 | const path = '/api/v1/videos/' + videoId + '/captions' |
40 | 41 | ||
41 | return this.getRequestBody<ResultList<VideoCaption>>({ | 42 | return this.getRequestBody<ResultList<VideoCaption>>({ |
42 | ...options, | 43 | ...options, |
43 | 44 | ||
44 | path, | 45 | path, |
46 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
45 | implicitToken: false, | 47 | implicitToken: false, |
46 | defaultExpectedStatus: HttpStatusCode.OK_200 | 48 | defaultExpectedStatus: HttpStatusCode.OK_200 |
47 | }) | 49 | }) |
diff --git a/shared/server-commands/videos/comments-command.ts b/shared/server-commands/videos/comments-command.ts index 154ec0c24..0dab1b66a 100644 --- a/shared/server-commands/videos/comments-command.ts +++ b/shared/server-commands/videos/comments-command.ts | |||
@@ -36,11 +36,12 @@ export class CommentsCommand extends AbstractCommand { | |||
36 | 36 | ||
37 | listThreads (options: OverrideCommandOptions & { | 37 | listThreads (options: OverrideCommandOptions & { |
38 | videoId: number | string | 38 | videoId: number | string |
39 | videoPassword?: string | ||
39 | start?: number | 40 | start?: number |
40 | count?: number | 41 | count?: number |
41 | sort?: string | 42 | sort?: string |
42 | }) { | 43 | }) { |
43 | const { start, count, sort, videoId } = options | 44 | const { start, count, sort, videoId, videoPassword } = options |
44 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | 45 | const path = '/api/v1/videos/' + videoId + '/comment-threads' |
45 | 46 | ||
46 | return this.getRequestBody<VideoCommentThreads>({ | 47 | return this.getRequestBody<VideoCommentThreads>({ |
@@ -48,6 +49,7 @@ export class CommentsCommand extends AbstractCommand { | |||
48 | 49 | ||
49 | path, | 50 | path, |
50 | query: { start, count, sort }, | 51 | query: { start, count, sort }, |
52 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
51 | implicitToken: false, | 53 | implicitToken: false, |
52 | defaultExpectedStatus: HttpStatusCode.OK_200 | 54 | defaultExpectedStatus: HttpStatusCode.OK_200 |
53 | }) | 55 | }) |
@@ -72,8 +74,9 @@ export class CommentsCommand extends AbstractCommand { | |||
72 | async createThread (options: OverrideCommandOptions & { | 74 | async createThread (options: OverrideCommandOptions & { |
73 | videoId: number | string | 75 | videoId: number | string |
74 | text: string | 76 | text: string |
77 | videoPassword?: string | ||
75 | }) { | 78 | }) { |
76 | const { videoId, text } = options | 79 | const { videoId, text, videoPassword } = options |
77 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | 80 | const path = '/api/v1/videos/' + videoId + '/comment-threads' |
78 | 81 | ||
79 | const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ | 82 | const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ |
@@ -81,6 +84,7 @@ export class CommentsCommand extends AbstractCommand { | |||
81 | 84 | ||
82 | path, | 85 | path, |
83 | fields: { text }, | 86 | fields: { text }, |
87 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
84 | implicitToken: true, | 88 | implicitToken: true, |
85 | defaultExpectedStatus: HttpStatusCode.OK_200 | 89 | defaultExpectedStatus: HttpStatusCode.OK_200 |
86 | })) | 90 | })) |
@@ -95,8 +99,9 @@ export class CommentsCommand extends AbstractCommand { | |||
95 | videoId: number | string | 99 | videoId: number | string |
96 | toCommentId: number | 100 | toCommentId: number |
97 | text: string | 101 | text: string |
102 | videoPassword?: string | ||
98 | }) { | 103 | }) { |
99 | const { videoId, toCommentId, text } = options | 104 | const { videoId, toCommentId, text, videoPassword } = options |
100 | const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId | 105 | const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId |
101 | 106 | ||
102 | const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ | 107 | const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ |
@@ -104,6 +109,7 @@ export class CommentsCommand extends AbstractCommand { | |||
104 | 109 | ||
105 | path, | 110 | path, |
106 | fields: { text }, | 111 | fields: { text }, |
112 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
107 | implicitToken: true, | 113 | implicitToken: true, |
108 | defaultExpectedStatus: HttpStatusCode.OK_200 | 114 | defaultExpectedStatus: HttpStatusCode.OK_200 |
109 | })) | 115 | })) |
diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts index c17f6ef20..106d80af0 100644 --- a/shared/server-commands/videos/index.ts +++ b/shared/server-commands/videos/index.ts | |||
@@ -11,9 +11,11 @@ export * from './live-command' | |||
11 | export * from './live' | 11 | export * from './live' |
12 | export * from './playlists-command' | 12 | export * from './playlists-command' |
13 | export * from './services-command' | 13 | export * from './services-command' |
14 | export * from './storyboard-command' | ||
14 | export * from './streaming-playlists-command' | 15 | export * from './streaming-playlists-command' |
15 | export * from './comments-command' | 16 | export * from './comments-command' |
16 | export * from './video-studio-command' | 17 | export * from './video-studio-command' |
17 | export * from './video-token-command' | 18 | export * from './video-token-command' |
18 | export * from './views-command' | 19 | export * from './views-command' |
19 | export * from './videos-command' | 20 | export * from './videos-command' |
21 | export * from './video-passwords-command' | ||
diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts index 44d625970..6006d9fe9 100644 --- a/shared/server-commands/videos/live-command.ts +++ b/shared/server-commands/videos/live-command.ts | |||
@@ -120,8 +120,13 @@ export class LiveCommand extends AbstractCommand { | |||
120 | saveReplay: boolean | 120 | saveReplay: boolean |
121 | permanentLive: boolean | 121 | permanentLive: boolean |
122 | privacy?: VideoPrivacy | 122 | privacy?: VideoPrivacy |
123 | videoPasswords?: string[] | ||
123 | }) { | 124 | }) { |
124 | const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options | 125 | const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC, videoPasswords } = options |
126 | |||
127 | const replaySettings = privacy === VideoPrivacy.PASSWORD_PROTECTED | ||
128 | ? { privacy: VideoPrivacy.PRIVATE } | ||
129 | : { privacy } | ||
125 | 130 | ||
126 | const { uuid } = await this.create({ | 131 | const { uuid } = await this.create({ |
127 | ...options, | 132 | ...options, |
@@ -130,9 +135,10 @@ export class LiveCommand extends AbstractCommand { | |||
130 | name: 'live', | 135 | name: 'live', |
131 | permanentLive, | 136 | permanentLive, |
132 | saveReplay, | 137 | saveReplay, |
133 | replaySettings: { privacy }, | 138 | replaySettings, |
134 | channelId: this.server.store.channel.id, | 139 | channelId: this.server.store.channel.id, |
135 | privacy | 140 | privacy, |
141 | videoPasswords | ||
136 | } | 142 | } |
137 | }) | 143 | }) |
138 | 144 | ||
diff --git a/shared/server-commands/videos/storyboard-command.ts b/shared/server-commands/videos/storyboard-command.ts new file mode 100644 index 000000000..06d90fc12 --- /dev/null +++ b/shared/server-commands/videos/storyboard-command.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { HttpStatusCode, Storyboard } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
3 | |||
4 | export class StoryboardCommand extends AbstractCommand { | ||
5 | |||
6 | list (options: OverrideCommandOptions & { | ||
7 | id: number | string | ||
8 | }) { | ||
9 | const path = '/api/v1/videos/' + options.id + '/storyboards' | ||
10 | |||
11 | return this.getRequestBody<{ storyboards: Storyboard[] }>({ | ||
12 | ...options, | ||
13 | |||
14 | path, | ||
15 | implicitToken: true, | ||
16 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
17 | }) | ||
18 | } | ||
19 | } | ||
diff --git a/shared/server-commands/videos/video-passwords-command.ts b/shared/server-commands/videos/video-passwords-command.ts new file mode 100644 index 000000000..bf10335b4 --- /dev/null +++ b/shared/server-commands/videos/video-passwords-command.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import { HttpStatusCode, ResultList, VideoPassword } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
3 | export class VideoPasswordsCommand extends AbstractCommand { | ||
4 | |||
5 | list (options: OverrideCommandOptions & { | ||
6 | videoId: number | string | ||
7 | start?: number | ||
8 | count?: number | ||
9 | sort?: string | ||
10 | }) { | ||
11 | const { start, count, sort, videoId } = options | ||
12 | const path = '/api/v1/videos/' + videoId + '/passwords' | ||
13 | |||
14 | return this.getRequestBody<ResultList<VideoPassword>>({ | ||
15 | ...options, | ||
16 | |||
17 | path, | ||
18 | query: { start, count, sort }, | ||
19 | implicitToken: true, | ||
20 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | updateAll (options: OverrideCommandOptions & { | ||
25 | videoId: number | string | ||
26 | passwords: string[] | ||
27 | }) { | ||
28 | const { videoId, passwords } = options | ||
29 | const path = `/api/v1/videos/${videoId}/passwords` | ||
30 | |||
31 | return this.putBodyRequest({ | ||
32 | ...options, | ||
33 | path, | ||
34 | fields: { passwords }, | ||
35 | implicitToken: true, | ||
36 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | remove (options: OverrideCommandOptions & { | ||
41 | id: number | ||
42 | videoId: number | string | ||
43 | }) { | ||
44 | const { id, videoId } = options | ||
45 | const path = `/api/v1/videos/${videoId}/passwords/${id}` | ||
46 | |||
47 | return this.deleteRequest({ | ||
48 | ...options, | ||
49 | |||
50 | path, | ||
51 | implicitToken: true, | ||
52 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
53 | }) | ||
54 | } | ||
55 | } | ||
diff --git a/shared/server-commands/videos/video-studio-command.ts b/shared/server-commands/videos/video-studio-command.ts index 9fe467cc2..675cd84b7 100644 --- a/shared/server-commands/videos/video-studio-command.ts +++ b/shared/server-commands/videos/video-studio-command.ts | |||
@@ -25,7 +25,7 @@ export class VideoStudioCommand extends AbstractCommand { | |||
25 | { | 25 | { |
26 | name: 'add-watermark', | 26 | name: 'add-watermark', |
27 | options: { | 27 | options: { |
28 | file: 'thumbnail.png' | 28 | file: 'custom-thumbnail.png' |
29 | } | 29 | } |
30 | }, | 30 | }, |
31 | 31 | ||
diff --git a/shared/server-commands/videos/video-token-command.ts b/shared/server-commands/videos/video-token-command.ts index 0531bee65..c4ed29a8c 100644 --- a/shared/server-commands/videos/video-token-command.ts +++ b/shared/server-commands/videos/video-token-command.ts | |||
@@ -8,12 +8,14 @@ export class VideoTokenCommand extends AbstractCommand { | |||
8 | 8 | ||
9 | create (options: OverrideCommandOptions & { | 9 | create (options: OverrideCommandOptions & { |
10 | videoId: number | string | 10 | videoId: number | string |
11 | videoPassword?: string | ||
11 | }) { | 12 | }) { |
12 | const { videoId } = options | 13 | const { videoId, videoPassword } = options |
13 | const path = '/api/v1/videos/' + videoId + '/token' | 14 | const path = '/api/v1/videos/' + videoId + '/token' |
14 | 15 | ||
15 | return unwrapBody<VideoToken>(this.postBodyRequest({ | 16 | return unwrapBody<VideoToken>(this.postBodyRequest({ |
16 | ...options, | 17 | ...options, |
18 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
17 | 19 | ||
18 | path, | 20 | path, |
19 | implicitToken: true, | 21 | implicitToken: true, |
@@ -23,6 +25,7 @@ export class VideoTokenCommand extends AbstractCommand { | |||
23 | 25 | ||
24 | async getVideoFileToken (options: OverrideCommandOptions & { | 26 | async getVideoFileToken (options: OverrideCommandOptions & { |
25 | videoId: number | string | 27 | videoId: number | string |
28 | videoPassword?: string | ||
26 | }) { | 29 | }) { |
27 | const { files } = await this.create(options) | 30 | const { files } = await this.create(options) |
28 | 31 | ||
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts index b5df9c325..9602fa7da 100644 --- a/shared/server-commands/videos/videos-command.ts +++ b/shared/server-commands/videos/videos-command.ts | |||
@@ -111,8 +111,9 @@ export class VideosCommand extends AbstractCommand { | |||
111 | rate (options: OverrideCommandOptions & { | 111 | rate (options: OverrideCommandOptions & { |
112 | id: number | string | 112 | id: number | string |
113 | rating: UserVideoRateType | 113 | rating: UserVideoRateType |
114 | videoPassword?: string | ||
114 | }) { | 115 | }) { |
115 | const { id, rating } = options | 116 | const { id, rating, videoPassword } = options |
116 | const path = '/api/v1/videos/' + id + '/rate' | 117 | const path = '/api/v1/videos/' + id + '/rate' |
117 | 118 | ||
118 | return this.putBodyRequest({ | 119 | return this.putBodyRequest({ |
@@ -120,6 +121,7 @@ export class VideosCommand extends AbstractCommand { | |||
120 | 121 | ||
121 | path, | 122 | path, |
122 | fields: { rating }, | 123 | fields: { rating }, |
124 | headers: this.buildVideoPasswordHeader(videoPassword), | ||
123 | implicitToken: true, | 125 | implicitToken: true, |
124 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | 126 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 |
125 | }) | 127 | }) |
@@ -151,6 +153,23 @@ export class VideosCommand extends AbstractCommand { | |||
151 | }) | 153 | }) |
152 | } | 154 | } |
153 | 155 | ||
156 | getWithPassword (options: OverrideCommandOptions & { | ||
157 | id: number | string | ||
158 | password?: string | ||
159 | }) { | ||
160 | const path = '/api/v1/videos/' + options.id | ||
161 | |||
162 | return this.getRequestBody<VideoDetails>({ | ||
163 | ...options, | ||
164 | headers:{ | ||
165 | 'x-peertube-video-password': options.password | ||
166 | }, | ||
167 | path, | ||
168 | implicitToken: false, | ||
169 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
170 | }) | ||
171 | } | ||
172 | |||
154 | getSource (options: OverrideCommandOptions & { | 173 | getSource (options: OverrideCommandOptions & { |
155 | id: number | string | 174 | id: number | string |
156 | }) { | 175 | }) { |
@@ -608,11 +627,13 @@ export class VideosCommand extends AbstractCommand { | |||
608 | nsfw?: boolean | 627 | nsfw?: boolean |
609 | privacy?: VideoPrivacy | 628 | privacy?: VideoPrivacy |
610 | fixture?: string | 629 | fixture?: string |
630 | videoPasswords?: string[] | ||
611 | }) { | 631 | }) { |
612 | const attributes: VideoEdit = { name: options.name } | 632 | const attributes: VideoEdit = { name: options.name } |
613 | if (options.nsfw) attributes.nsfw = options.nsfw | 633 | if (options.nsfw) attributes.nsfw = options.nsfw |
614 | if (options.privacy) attributes.privacy = options.privacy | 634 | if (options.privacy) attributes.privacy = options.privacy |
615 | if (options.fixture) attributes.fixture = options.fixture | 635 | if (options.fixture) attributes.fixture = options.fixture |
636 | if (options.videoPasswords) attributes.videoPasswords = options.videoPasswords | ||
616 | 637 | ||
617 | return this.upload({ ...options, attributes }) | 638 | return this.upload({ ...options, attributes }) |
618 | } | 639 | } |
@@ -665,10 +686,10 @@ export class VideosCommand extends AbstractCommand { | |||
665 | }) | 686 | }) |
666 | } | 687 | } |
667 | 688 | ||
668 | removeAllWebTorrentFiles (options: OverrideCommandOptions & { | 689 | removeAllWebVideoFiles (options: OverrideCommandOptions & { |
669 | videoId: number | string | 690 | videoId: number | string |
670 | }) { | 691 | }) { |
671 | const path = '/api/v1/videos/' + options.videoId + '/webtorrent' | 692 | const path = '/api/v1/videos/' + options.videoId + '/web-videos' |
672 | 693 | ||
673 | return this.deleteRequest({ | 694 | return this.deleteRequest({ |
674 | ...options, | 695 | ...options, |
@@ -679,11 +700,11 @@ export class VideosCommand extends AbstractCommand { | |||
679 | }) | 700 | }) |
680 | } | 701 | } |
681 | 702 | ||
682 | removeWebTorrentFile (options: OverrideCommandOptions & { | 703 | removeWebVideoFile (options: OverrideCommandOptions & { |
683 | videoId: number | string | 704 | videoId: number | string |
684 | fileId: number | 705 | fileId: number |
685 | }) { | 706 | }) { |
686 | const path = '/api/v1/videos/' + options.videoId + '/webtorrent/' + options.fileId | 707 | const path = '/api/v1/videos/' + options.videoId + '/web-videos/' + options.fileId |
687 | 708 | ||
688 | return this.deleteRequest({ | 709 | return this.deleteRequest({ |
689 | ...options, | 710 | ...options, |
@@ -696,7 +717,7 @@ export class VideosCommand extends AbstractCommand { | |||
696 | 717 | ||
697 | runTranscoding (options: OverrideCommandOptions & { | 718 | runTranscoding (options: OverrideCommandOptions & { |
698 | videoId: number | string | 719 | videoId: number | string |
699 | transcodingType: 'hls' | 'webtorrent' | 720 | transcodingType: 'hls' | 'webtorrent' | 'web-video' |
700 | }) { | 721 | }) { |
701 | const path = '/api/v1/videos/' + options.videoId + '/transcoding' | 722 | const path = '/api/v1/videos/' + options.videoId + '/transcoding' |
702 | 723 | ||
diff --git a/support/doc/api/embeds.md b/support/doc/api/embeds.md index 1dd1443e7..fd5507e38 100644 --- a/support/doc/api/embeds.md +++ b/support/doc/api/embeds.md | |||
@@ -52,7 +52,7 @@ player.pause() | |||
52 | ## Embed URL parameters | 52 | ## Embed URL parameters |
53 | 53 | ||
54 | You can customize PeerTube player by specifying URL query parameters. | 54 | You can customize PeerTube player by specifying URL query parameters. |
55 | For example `https://my-instance.example.com/videos/embed/52a10666-3a18-4e73-93da-e8d3c12c305a??start=1s&stop=18s&loop=1&autoplay=1&muted=1&warningTitle=0&controlBar=0&peertubeLink=0&p2p=0` | 55 | For example `https://my-instance.example.com/videos/embed/52a10666-3a18-4e73-93da-e8d3c12c305a?start=1s&stop=18s&loop=1&autoplay=1&muted=1&warningTitle=0&controlBar=0&peertubeLink=0&p2p=0` |
56 | 56 | ||
57 | ### start | 57 | ### start |
58 | 58 | ||
@@ -108,6 +108,10 @@ Most web browsers disable video autoplay if the user did not interact with the v | |||
108 | 108 | ||
109 | Value must be `0` or `1`. | 109 | Value must be `0` or `1`. |
110 | 110 | ||
111 | ### playbackRate | ||
112 | |||
113 | Force the default playback rate (`0.75`, `1.5` etc). | ||
114 | |||
111 | ### title | 115 | ### title |
112 | 116 | ||
113 | Hide embed title. | 117 | Hide embed title. |
@@ -142,7 +146,7 @@ Value must be a valid color (`red` or `rgba(100, 100, 100, 0.5)`). | |||
142 | 146 | ||
143 | Force a specific player engine. | 147 | Force a specific player engine. |
144 | 148 | ||
145 | Value must be a valid mode (`webtorrent` or `p2p-media-loader`). | 149 | Value must be a valid mode (`web-video` or `p2p-media-loader`). |
146 | 150 | ||
147 | ### api | 151 | ### api |
148 | 152 | ||
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index cd50e86a6..2dfad9987 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -300,6 +300,8 @@ tags: | |||
300 | - name: Runner Registration Token | 300 | - name: Runner Registration Token |
301 | description: | | 301 | description: | |
302 | Manage runner registration token | 302 | Manage runner registration token |
303 | - name: Video Passwords | ||
304 | description: Operation on video passwords | ||
303 | 305 | ||
304 | x-tagGroups: | 306 | x-tagGroups: |
305 | - name: Static endpoints | 307 | - name: Static endpoints |
@@ -337,6 +339,7 @@ x-tagGroups: | |||
337 | - Video Transcoding | 339 | - Video Transcoding |
338 | - Live Videos | 340 | - Live Videos |
339 | - Channels Sync | 341 | - Channels Sync |
342 | - Video Passwords | ||
340 | - name: Search | 343 | - name: Search |
341 | tags: | 344 | tags: |
342 | - Search | 345 | - Search |
@@ -363,11 +366,11 @@ x-tagGroups: | |||
363 | - Runners | 366 | - Runners |
364 | 367 | ||
365 | paths: | 368 | paths: |
366 | '/static/webseed/{filename}': | 369 | '/static/web-videos/{filename}': |
367 | get: | 370 | get: |
368 | tags: | 371 | tags: |
369 | - Static Video Files | 372 | - Static Video Files |
370 | summary: Get public WebTorrent video file | 373 | summary: Get public Web Video file |
371 | parameters: | 374 | parameters: |
372 | - $ref: '#/components/parameters/staticFilename' | 375 | - $ref: '#/components/parameters/staticFilename' |
373 | responses: | 376 | responses: |
@@ -375,11 +378,11 @@ paths: | |||
375 | description: successful operation | 378 | description: successful operation |
376 | '404': | 379 | '404': |
377 | description: not found | 380 | description: not found |
378 | '/static/webseed/private/{filename}': | 381 | '/static/web-videos/private/{filename}': |
379 | get: | 382 | get: |
380 | tags: | 383 | tags: |
381 | - Static Video Files | 384 | - Static Video Files |
382 | summary: Get private WebTorrent video file | 385 | summary: Get private Web Video file |
383 | parameters: | 386 | parameters: |
384 | - $ref: '#/components/parameters/staticFilename' | 387 | - $ref: '#/components/parameters/staticFilename' |
385 | - $ref: '#/components/parameters/videoFileToken' | 388 | - $ref: '#/components/parameters/videoFileToken' |
@@ -571,7 +574,7 @@ paths: | |||
571 | - $ref: '#/components/parameters/include' | 574 | - $ref: '#/components/parameters/include' |
572 | - $ref: '#/components/parameters/privacyOneOf' | 575 | - $ref: '#/components/parameters/privacyOneOf' |
573 | - $ref: '#/components/parameters/hasHLSFiles' | 576 | - $ref: '#/components/parameters/hasHLSFiles' |
574 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 577 | - $ref: '#/components/parameters/hasWebVideoFiles' |
575 | responses: | 578 | responses: |
576 | '200': | 579 | '200': |
577 | description: successful operation | 580 | description: successful operation |
@@ -655,7 +658,7 @@ paths: | |||
655 | - $ref: '#/components/parameters/include' | 658 | - $ref: '#/components/parameters/include' |
656 | - $ref: '#/components/parameters/privacyOneOf' | 659 | - $ref: '#/components/parameters/privacyOneOf' |
657 | - $ref: '#/components/parameters/hasHLSFiles' | 660 | - $ref: '#/components/parameters/hasHLSFiles' |
658 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 661 | - $ref: '#/components/parameters/hasWebVideoFiles' |
659 | responses: | 662 | responses: |
660 | '200': | 663 | '200': |
661 | description: successful operation | 664 | description: successful operation |
@@ -745,7 +748,7 @@ paths: | |||
745 | - $ref: '#/components/parameters/include' | 748 | - $ref: '#/components/parameters/include' |
746 | - $ref: '#/components/parameters/privacyOneOf' | 749 | - $ref: '#/components/parameters/privacyOneOf' |
747 | - $ref: '#/components/parameters/hasHLSFiles' | 750 | - $ref: '#/components/parameters/hasHLSFiles' |
748 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 751 | - $ref: '#/components/parameters/hasWebVideoFiles' |
749 | - $ref: '#/components/parameters/skipCount' | 752 | - $ref: '#/components/parameters/skipCount' |
750 | - $ref: '#/components/parameters/start' | 753 | - $ref: '#/components/parameters/start' |
751 | - $ref: '#/components/parameters/count' | 754 | - $ref: '#/components/parameters/count' |
@@ -915,7 +918,7 @@ paths: | |||
915 | description: > | 918 | description: > |
916 | Arises when: | 919 | Arises when: |
917 | - the emailer is disabled and the instance is open to registrations | 920 | - the emailer is disabled and the instance is open to registrations |
918 | - webtorrent and hls are disabled with transcoding enabled - you need at least one enabled | 921 | - web videos and hls are disabled with transcoding enabled - you need at least one enabled |
919 | delete: | 922 | delete: |
920 | summary: Delete instance runtime configuration | 923 | summary: Delete instance runtime configuration |
921 | operationId: delCustomConfig | 924 | operationId: delCustomConfig |
@@ -1870,7 +1873,7 @@ paths: | |||
1870 | - $ref: '#/components/parameters/include' | 1873 | - $ref: '#/components/parameters/include' |
1871 | - $ref: '#/components/parameters/privacyOneOf' | 1874 | - $ref: '#/components/parameters/privacyOneOf' |
1872 | - $ref: '#/components/parameters/hasHLSFiles' | 1875 | - $ref: '#/components/parameters/hasHLSFiles' |
1873 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 1876 | - $ref: '#/components/parameters/hasWebVideoFiles' |
1874 | - $ref: '#/components/parameters/skipCount' | 1877 | - $ref: '#/components/parameters/skipCount' |
1875 | - $ref: '#/components/parameters/start' | 1878 | - $ref: '#/components/parameters/start' |
1876 | - $ref: '#/components/parameters/count' | 1879 | - $ref: '#/components/parameters/count' |
@@ -2359,6 +2362,7 @@ paths: | |||
2359 | - OAuth2: [] | 2362 | - OAuth2: [] |
2360 | parameters: | 2363 | parameters: |
2361 | - $ref: '#/components/parameters/idOrUUID' | 2364 | - $ref: '#/components/parameters/idOrUUID' |
2365 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
2362 | responses: | 2366 | responses: |
2363 | '200': | 2367 | '200': |
2364 | description: successful operation | 2368 | description: successful operation |
@@ -2414,7 +2418,7 @@ paths: | |||
2414 | - $ref: '#/components/parameters/include' | 2418 | - $ref: '#/components/parameters/include' |
2415 | - $ref: '#/components/parameters/privacyOneOf' | 2419 | - $ref: '#/components/parameters/privacyOneOf' |
2416 | - $ref: '#/components/parameters/hasHLSFiles' | 2420 | - $ref: '#/components/parameters/hasHLSFiles' |
2417 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 2421 | - $ref: '#/components/parameters/hasWebVideoFiles' |
2418 | - $ref: '#/components/parameters/skipCount' | 2422 | - $ref: '#/components/parameters/skipCount' |
2419 | - $ref: '#/components/parameters/start' | 2423 | - $ref: '#/components/parameters/start' |
2420 | - $ref: '#/components/parameters/count' | 2424 | - $ref: '#/components/parameters/count' |
@@ -2578,6 +2582,8 @@ paths: | |||
2578 | format: date-time | 2582 | format: date-time |
2579 | scheduleUpdate: | 2583 | scheduleUpdate: |
2580 | $ref: '#/components/schemas/VideoScheduledUpdate' | 2584 | $ref: '#/components/schemas/VideoScheduledUpdate' |
2585 | videoPasswords: | ||
2586 | $ref: '#/components/schemas/AddVideoPasswords' | ||
2581 | encoding: | 2587 | encoding: |
2582 | thumbnailfile: | 2588 | thumbnailfile: |
2583 | contentType: image/jpeg | 2589 | contentType: image/jpeg |
@@ -2590,6 +2596,7 @@ paths: | |||
2590 | - Video | 2596 | - Video |
2591 | parameters: | 2597 | parameters: |
2592 | - $ref: '#/components/parameters/idOrUUID' | 2598 | - $ref: '#/components/parameters/idOrUUID' |
2599 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
2593 | responses: | 2600 | responses: |
2594 | '200': | 2601 | '200': |
2595 | description: successful operation | 2602 | description: successful operation |
@@ -2597,6 +2604,8 @@ paths: | |||
2597 | application/json: | 2604 | application/json: |
2598 | schema: | 2605 | schema: |
2599 | $ref: '#/components/schemas/VideoDetails' | 2606 | $ref: '#/components/schemas/VideoDetails' |
2607 | '403': | ||
2608 | description: provide a correct password to access this password protected video | ||
2600 | delete: | 2609 | delete: |
2601 | summary: Delete a video | 2610 | summary: Delete a video |
2602 | operationId: delVideo | 2611 | operationId: delVideo |
@@ -2618,6 +2627,7 @@ paths: | |||
2618 | - Video | 2627 | - Video |
2619 | parameters: | 2628 | parameters: |
2620 | - $ref: '#/components/parameters/idOrUUID' | 2629 | - $ref: '#/components/parameters/idOrUUID' |
2630 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
2621 | responses: | 2631 | responses: |
2622 | '200': | 2632 | '200': |
2623 | description: successful operation | 2633 | description: successful operation |
@@ -3267,6 +3277,7 @@ paths: | |||
3267 | - Live Videos | 3277 | - Live Videos |
3268 | parameters: | 3278 | parameters: |
3269 | - $ref: '#/components/parameters/idOrUUID' | 3279 | - $ref: '#/components/parameters/idOrUUID' |
3280 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
3270 | responses: | 3281 | responses: |
3271 | '200': | 3282 | '200': |
3272 | description: successful operation | 3283 | description: successful operation |
@@ -3657,6 +3668,27 @@ paths: | |||
3657 | items: | 3668 | items: |
3658 | $ref: '#/components/schemas/VideoBlacklist' | 3669 | $ref: '#/components/schemas/VideoBlacklist' |
3659 | 3670 | ||
3671 | /api/v1/videos/{id}/storyboards: | ||
3672 | get: | ||
3673 | summary: List storyboards of a video | ||
3674 | operationId: listVideoStoryboards | ||
3675 | tags: | ||
3676 | - Video | ||
3677 | parameters: | ||
3678 | - $ref: '#/components/parameters/idOrUUID' | ||
3679 | responses: | ||
3680 | '200': | ||
3681 | description: successful operation | ||
3682 | content: | ||
3683 | application/json: | ||
3684 | schema: | ||
3685 | type: object | ||
3686 | properties: | ||
3687 | storyboards: | ||
3688 | type: array | ||
3689 | items: | ||
3690 | $ref: '#/components/schemas/Storyboard' | ||
3691 | |||
3660 | /api/v1/videos/{id}/captions: | 3692 | /api/v1/videos/{id}/captions: |
3661 | get: | 3693 | get: |
3662 | summary: List captions of a video | 3694 | summary: List captions of a video |
@@ -3665,6 +3697,7 @@ paths: | |||
3665 | - Video Captions | 3697 | - Video Captions |
3666 | parameters: | 3698 | parameters: |
3667 | - $ref: '#/components/parameters/idOrUUID' | 3699 | - $ref: '#/components/parameters/idOrUUID' |
3700 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
3668 | responses: | 3701 | responses: |
3669 | '200': | 3702 | '200': |
3670 | description: successful operation | 3703 | description: successful operation |
@@ -3728,6 +3761,70 @@ paths: | |||
3728 | '404': | 3761 | '404': |
3729 | description: video or language or caption for that language not found | 3762 | description: video or language or caption for that language not found |
3730 | 3763 | ||
3764 | /api/v1/videos/{id}/passwords: | ||
3765 | get: | ||
3766 | summary: List video passwords | ||
3767 | security: | ||
3768 | - OAuth2: | ||
3769 | - user | ||
3770 | tags: | ||
3771 | - Video Passwords | ||
3772 | parameters: | ||
3773 | - $ref: '#/components/parameters/idOrUUID' | ||
3774 | - $ref: '#/components/parameters/start' | ||
3775 | - $ref: '#/components/parameters/count' | ||
3776 | - $ref: '#/components/parameters/sort' | ||
3777 | responses: | ||
3778 | '204': | ||
3779 | description: successful operation | ||
3780 | content: | ||
3781 | application/json: | ||
3782 | schema: | ||
3783 | $ref: '#/components/schemas/VideoPasswordList' | ||
3784 | '400': | ||
3785 | description: video is not password protected | ||
3786 | put: | ||
3787 | summary: Update video passwords | ||
3788 | security: | ||
3789 | - OAuth2: | ||
3790 | - user | ||
3791 | tags: | ||
3792 | - Video Passwords | ||
3793 | parameters: | ||
3794 | - $ref: '#/components/parameters/idOrUUID' | ||
3795 | requestBody: | ||
3796 | content: | ||
3797 | application/json: | ||
3798 | schema: | ||
3799 | type: object | ||
3800 | properties: | ||
3801 | passwords: | ||
3802 | $ref: '#/components/schemas/AddVideoPasswords' | ||
3803 | responses: | ||
3804 | '204': | ||
3805 | description: successful operation | ||
3806 | '400': | ||
3807 | description: video is not password protected | ||
3808 | |||
3809 | /api/v1/videos/{id}/passwords/{videoPasswordId}: | ||
3810 | delete: | ||
3811 | summary: Delete a video password | ||
3812 | security: | ||
3813 | - OAuth2: | ||
3814 | - user | ||
3815 | tags: | ||
3816 | - Video Passwords | ||
3817 | parameters: | ||
3818 | - $ref: '#/components/parameters/idOrUUID' | ||
3819 | - $ref: '#/components/parameters/videoPasswordId' | ||
3820 | responses: | ||
3821 | '204': | ||
3822 | description: successful operation | ||
3823 | '403': | ||
3824 | description: cannot delete the last password of the protected video | ||
3825 | '400': | ||
3826 | description: video is not password protected | ||
3827 | |||
3731 | /api/v1/video-channels: | 3828 | /api/v1/video-channels: |
3732 | get: | 3829 | get: |
3733 | summary: List video channels | 3830 | summary: List video channels |
@@ -3836,7 +3933,7 @@ paths: | |||
3836 | - $ref: '#/components/parameters/include' | 3933 | - $ref: '#/components/parameters/include' |
3837 | - $ref: '#/components/parameters/privacyOneOf' | 3934 | - $ref: '#/components/parameters/privacyOneOf' |
3838 | - $ref: '#/components/parameters/hasHLSFiles' | 3935 | - $ref: '#/components/parameters/hasHLSFiles' |
3839 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 3936 | - $ref: '#/components/parameters/hasWebVideoFiles' |
3840 | - $ref: '#/components/parameters/skipCount' | 3937 | - $ref: '#/components/parameters/skipCount' |
3841 | - $ref: '#/components/parameters/start' | 3938 | - $ref: '#/components/parameters/start' |
3842 | - $ref: '#/components/parameters/count' | 3939 | - $ref: '#/components/parameters/count' |
@@ -4554,6 +4651,7 @@ paths: | |||
4554 | - $ref: '#/components/parameters/start' | 4651 | - $ref: '#/components/parameters/start' |
4555 | - $ref: '#/components/parameters/count' | 4652 | - $ref: '#/components/parameters/count' |
4556 | - $ref: '#/components/parameters/commentsSort' | 4653 | - $ref: '#/components/parameters/commentsSort' |
4654 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
4557 | responses: | 4655 | responses: |
4558 | '200': | 4656 | '200': |
4559 | description: successful operation | 4657 | description: successful operation |
@@ -4600,6 +4698,7 @@ paths: | |||
4600 | parameters: | 4698 | parameters: |
4601 | - $ref: '#/components/parameters/idOrUUID' | 4699 | - $ref: '#/components/parameters/idOrUUID' |
4602 | - $ref: '#/components/parameters/threadId' | 4700 | - $ref: '#/components/parameters/threadId' |
4701 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
4603 | responses: | 4702 | responses: |
4604 | '200': | 4703 | '200': |
4605 | description: successful operation | 4704 | description: successful operation |
@@ -4618,6 +4717,7 @@ paths: | |||
4618 | parameters: | 4717 | parameters: |
4619 | - $ref: '#/components/parameters/idOrUUID' | 4718 | - $ref: '#/components/parameters/idOrUUID' |
4620 | - $ref: '#/components/parameters/commentId' | 4719 | - $ref: '#/components/parameters/commentId' |
4720 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
4621 | responses: | 4721 | responses: |
4622 | '200': | 4722 | '200': |
4623 | description: successful operation | 4723 | description: successful operation |
@@ -4668,6 +4768,7 @@ paths: | |||
4668 | - Video Rates | 4768 | - Video Rates |
4669 | parameters: | 4769 | parameters: |
4670 | - $ref: '#/components/parameters/idOrUUID' | 4770 | - $ref: '#/components/parameters/idOrUUID' |
4771 | - $ref: '#/components/parameters/videoPasswordHeader' | ||
4671 | requestBody: | 4772 | requestBody: |
4672 | content: | 4773 | content: |
4673 | application/json: | 4774 | application/json: |
@@ -4703,15 +4804,15 @@ paths: | |||
4703 | description: successful operation | 4804 | description: successful operation |
4704 | '404': | 4805 | '404': |
4705 | description: video does not exist | 4806 | description: video does not exist |
4706 | '/api/v1/videos/{id}/webtorrent': | 4807 | '/api/v1/videos/{id}/web-videos': |
4707 | delete: | 4808 | delete: |
4708 | summary: Delete video WebTorrent files | 4809 | summary: Delete video Web Video files |
4709 | security: | 4810 | security: |
4710 | - OAuth2: | 4811 | - OAuth2: |
4711 | - admin | 4812 | - admin |
4712 | tags: | 4813 | tags: |
4713 | - Video Files | 4814 | - Video Files |
4714 | operationId: delVideoWebTorrent | 4815 | operationId: delVideoWebVideos |
4715 | parameters: | 4816 | parameters: |
4716 | - $ref: '#/components/parameters/idOrUUID' | 4817 | - $ref: '#/components/parameters/idOrUUID' |
4717 | responses: | 4818 | responses: |
@@ -4741,7 +4842,7 @@ paths: | |||
4741 | type: string | 4842 | type: string |
4742 | enum: | 4843 | enum: |
4743 | - hls | 4844 | - hls |
4744 | - webtorrent | 4845 | - web-video |
4745 | required: | 4846 | required: |
4746 | - transcodingType | 4847 | - transcodingType |
4747 | responses: | 4848 | responses: |
@@ -4779,7 +4880,7 @@ paths: | |||
4779 | - $ref: '#/components/parameters/privacyOneOf' | 4880 | - $ref: '#/components/parameters/privacyOneOf' |
4780 | - $ref: '#/components/parameters/uuids' | 4881 | - $ref: '#/components/parameters/uuids' |
4781 | - $ref: '#/components/parameters/hasHLSFiles' | 4882 | - $ref: '#/components/parameters/hasHLSFiles' |
4782 | - $ref: '#/components/parameters/hasWebtorrentFiles' | 4883 | - $ref: '#/components/parameters/hasWebVideoFiles' |
4783 | - $ref: '#/components/parameters/skipCount' | 4884 | - $ref: '#/components/parameters/skipCount' |
4784 | - $ref: '#/components/parameters/start' | 4885 | - $ref: '#/components/parameters/start' |
4785 | - $ref: '#/components/parameters/count' | 4886 | - $ref: '#/components/parameters/count' |
@@ -6370,13 +6471,13 @@ components: | |||
6370 | schema: | 6471 | schema: |
6371 | type: boolean | 6472 | type: boolean |
6372 | description: '**PeerTube >= 4.0** Display only videos that have HLS files' | 6473 | description: '**PeerTube >= 4.0** Display only videos that have HLS files' |
6373 | hasWebtorrentFiles: | 6474 | hasWebVideoFiles: |
6374 | name: hasWebtorrentFiles | 6475 | name: hasWebVideoFiles |
6375 | in: query | 6476 | in: query |
6376 | required: false | 6477 | required: false |
6377 | schema: | 6478 | schema: |
6378 | type: boolean | 6479 | type: boolean |
6379 | description: '**PeerTube >= 4.0** Display only videos that have WebTorrent files' | 6480 | description: '**PeerTube >= 4.0** Display only videos that have Web Video files' |
6380 | privacyOneOf: | 6481 | privacyOneOf: |
6381 | name: privacyOneOf | 6482 | name: privacyOneOf |
6382 | in: query | 6483 | in: query |
@@ -6525,7 +6626,20 @@ components: | |||
6525 | required: true | 6626 | required: true |
6526 | schema: | 6627 | schema: |
6527 | $ref: '#/components/schemas/UUIDv4' | 6628 | $ref: '#/components/schemas/UUIDv4' |
6528 | 6629 | videoPasswordId: | |
6630 | name: videoPasswordId | ||
6631 | in: path | ||
6632 | required: true | ||
6633 | description: The video password id | ||
6634 | schema: | ||
6635 | $ref: '#/components/schemas/id' | ||
6636 | videoPasswordHeader: | ||
6637 | name: x-peertube-video-password | ||
6638 | description: Required on password protected video | ||
6639 | in: header | ||
6640 | required: false | ||
6641 | schema: | ||
6642 | type: string | ||
6529 | securitySchemes: | 6643 | securitySchemes: |
6530 | OAuth2: | 6644 | OAuth2: |
6531 | description: | | 6645 | description: | |
@@ -7015,7 +7129,7 @@ components: | |||
7015 | maxLength: 120 | 7129 | maxLength: 120 |
7016 | thumbnailPath: | 7130 | thumbnailPath: |
7017 | type: string | 7131 | type: string |
7018 | example: /static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg | 7132 | example: /lazy-static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg |
7019 | previewPath: | 7133 | previewPath: |
7020 | type: string | 7134 | type: string |
7021 | example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg | 7135 | example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg |
@@ -7108,7 +7222,7 @@ components: | |||
7108 | items: | 7222 | items: |
7109 | $ref: '#/components/schemas/VideoFile' | 7223 | $ref: '#/components/schemas/VideoFile' |
7110 | description: | | 7224 | description: | |
7111 | WebTorrent/raw video files. If WebTorrent is disabled on the server: | 7225 | Web compatible video files. If Web Video is disabled on the server: |
7112 | 7226 | ||
7113 | - field will be empty | 7227 | - field will be empty |
7114 | - video files will be found in `streamingPlaylists[].files` field | 7228 | - video files will be found in `streamingPlaylists[].files` field |
@@ -7416,6 +7530,20 @@ components: | |||
7416 | type: array | 7530 | type: array |
7417 | items: | 7531 | items: |
7418 | $ref: '#/components/schemas/VideoCommentThreadTree' | 7532 | $ref: '#/components/schemas/VideoCommentThreadTree' |
7533 | Storyboard: | ||
7534 | properties: | ||
7535 | storyboardPath: | ||
7536 | type: string | ||
7537 | totalHeight: | ||
7538 | type: integer | ||
7539 | totalWidth: | ||
7540 | type: integer | ||
7541 | spriteHeight: | ||
7542 | type: integer | ||
7543 | spriteWidth: | ||
7544 | type: integer | ||
7545 | spriteDuration: | ||
7546 | type: integer | ||
7419 | VideoCaption: | 7547 | VideoCaption: |
7420 | properties: | 7548 | properties: |
7421 | language: | 7549 | language: |
@@ -7640,7 +7768,7 @@ components: | |||
7640 | properties: | 7768 | properties: |
7641 | enabled: | 7769 | enabled: |
7642 | type: boolean | 7770 | type: boolean |
7643 | webtorrent: | 7771 | web_videos: |
7644 | type: object | 7772 | type: object |
7645 | properties: | 7773 | properties: |
7646 | enabled: | 7774 | enabled: |
@@ -8006,15 +8134,15 @@ components: | |||
8006 | type: boolean | 8134 | type: boolean |
8007 | 2160p: | 8135 | 2160p: |
8008 | type: boolean | 8136 | type: boolean |
8009 | webtorrent: | 8137 | web_videos: |
8010 | type: object | 8138 | type: object |
8011 | description: WebTorrent-specific settings | 8139 | description: Web Video specific settings |
8012 | properties: | 8140 | properties: |
8013 | enabled: | 8141 | enabled: |
8014 | type: boolean | 8142 | type: boolean |
8015 | hls: | 8143 | hls: |
8016 | type: object | 8144 | type: object |
8017 | description: HLS-specific settings | 8145 | description: HLS specific settings |
8018 | properties: | 8146 | properties: |
8019 | enabled: | 8147 | enabled: |
8020 | type: boolean | 8148 | type: boolean |
@@ -8228,6 +8356,8 @@ components: | |||
8228 | description: Video preview file | 8356 | description: Video preview file |
8229 | type: string | 8357 | type: string |
8230 | format: binary | 8358 | format: binary |
8359 | videoPasswords: | ||
8360 | $ref: '#/components/schemas/AddVideoPasswords' | ||
8231 | required: | 8361 | required: |
8232 | - channelId | 8362 | - channelId |
8233 | - name | 8363 | - name |
@@ -9391,7 +9521,7 @@ components: | |||
9391 | type: string | 9521 | type: string |
9392 | enum: | 9522 | enum: |
9393 | - 'p2p-media-loader' | 9523 | - 'p2p-media-loader' |
9394 | - 'webtorrent' | 9524 | - 'web-video' |
9395 | resolution: | 9525 | resolution: |
9396 | type: number | 9526 | type: number |
9397 | description: Current player video resolution | 9527 | description: Current player video resolution |
@@ -9616,6 +9746,29 @@ components: | |||
9616 | privatePayload: | 9746 | privatePayload: |
9617 | type: object | 9747 | type: object |
9618 | 9748 | ||
9749 | VideoPassword: | ||
9750 | properties: | ||
9751 | id: | ||
9752 | $ref: '#/components/schemas/id' | ||
9753 | password: | ||
9754 | type: string | ||
9755 | minLength: 2 | ||
9756 | videoId: | ||
9757 | $ref: '#/components/schemas/id' | ||
9758 | VideoPasswordList: | ||
9759 | properties: | ||
9760 | total: | ||
9761 | type: integer | ||
9762 | example: 1 | ||
9763 | data: | ||
9764 | type: array | ||
9765 | items: | ||
9766 | $ref: '#/components/schemas/VideoPassword' | ||
9767 | AddVideoPasswords: | ||
9768 | type: array | ||
9769 | items: | ||
9770 | $ref: "#/components/schemas/VideoPassword/properties/password" | ||
9771 | uniqueItems: true | ||
9619 | callbacks: | 9772 | callbacks: |
9620 | searchIndex: | 9773 | searchIndex: |
9621 | 'https://search.example.org/api/v1/search/videos': | 9774 | 'https://search.example.org/api/v1/search/videos': |
diff --git a/support/doc/development/lib.md b/support/doc/development/lib.md index 3cccaf3d0..25fe3068e 100644 --- a/support/doc/development/lib.md +++ b/support/doc/development/lib.md | |||
@@ -5,7 +5,7 @@ | |||
5 | ### Build & Publish | 5 | ### Build & Publish |
6 | 6 | ||
7 | ``` | 7 | ``` |
8 | cd client/src/standalone/player/ | 8 | cd client/src/standalone/embed-player-api/ |
9 | npm run build | 9 | npm run build |
10 | npm publish --access=public | 10 | npm publish --access=public |
11 | ``` | 11 | ``` |
diff --git a/support/doc/development/release.md b/support/doc/development/release.md index c4935524c..81e35c58d 100644 --- a/support/doc/development/release.md +++ b/support/doc/development/release.md | |||
@@ -28,9 +28,11 @@ NODE_APP_INSTANCE=6 NODE_ENV=test node dist/server --benchmark-startup | |||
28 | 28 | ||
29 | ## @peertube/embed-api | 29 | ## @peertube/embed-api |
30 | 30 | ||
31 | At the root of PeerTube: | ||
32 | |||
31 | ``` | 33 | ``` |
32 | cd client/src/standalone/player | 34 | cd client/src/standalone/embed-player-api |
33 | npm version patch | 35 | npm version patch |
34 | npm run build | 36 | cd ../../../../ |
35 | npm publish --access=public | 37 | npm run release-embed-api |
36 | ``` | 38 | ``` |
diff --git a/support/doc/development/tests.md b/support/doc/development/tests.md index e3a65c35f..1c2589c8a 100644 --- a/support/doc/development/tests.md +++ b/support/doc/development/tests.md | |||
@@ -71,6 +71,7 @@ Some env variables can be defined to disable/enable some tests: | |||
71 | * `ENABLE_OBJECT_STORAGE_TESTS=true`: enable object storage tests (needs `chocobozzz/s3-ninja` container first) | 71 | * `ENABLE_OBJECT_STORAGE_TESTS=true`: enable object storage tests (needs `chocobozzz/s3-ninja` container first) |
72 | * `AKISMET_KEY`: specify an Akismet key to test akismet external PeerTube plugin | 72 | * `AKISMET_KEY`: specify an Akismet key to test akismet external PeerTube plugin |
73 | * `OBJECT_STORAGE_SCALEWAY_KEY_ID` and `OBJECT_STORAGE_SCALEWAY_ACCESS_KEY`: specify Scaleway API keys to test object storage ACL (not supported by our `chocobozzz/s3-ninja` container) | 73 | * `OBJECT_STORAGE_SCALEWAY_KEY_ID` and `OBJECT_STORAGE_SCALEWAY_ACCESS_KEY`: specify Scaleway API keys to test object storage ACL (not supported by our `chocobozzz/s3-ninja` container) |
74 | * `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true`: enable pixel comparison on images generated by ffmpeg. Disabled by default because a custom ffmpeg version may fails the tests | ||
74 | 75 | ||
75 | 76 | ||
76 | ### Debug server logs | 77 | ### Debug server logs |
diff --git a/support/doc/tools.md b/support/doc/tools.md index 39f5ab787..2b3ebf159 100644 --- a/support/doc/tools.md +++ b/support/doc/tools.md | |||
@@ -227,7 +227,7 @@ docker-compose exec -u peertube peertube npm run regenerate-thumbnails | |||
227 | 227 | ||
228 | ### create-import-video-file-job.js | 228 | ### create-import-video-file-job.js |
229 | 229 | ||
230 | You can use this script to import a video file to replace an already uploaded file or to add a new webtorrent resolution to a video. PeerTube needs to be running. | 230 | You can use this script to import a video file to replace an already uploaded file or to add a new web compatible resolution to a video. PeerTube needs to be running. |
231 | You can then create a transcoding job using the web interface if you need to optimize your file or create an HLS version of it. | 231 | You can then create a transcoding job using the web interface if you need to optimize your file or create an HLS version of it. |
232 | 232 | ||
233 | ```bash | 233 | ```bash |
@@ -268,6 +268,35 @@ cd /var/www/peertube-docker | |||
268 | docker-compose exec -u peertube peertube npm run create-move-video-storage-job -- --to-object-storage --all-videos | 268 | docker-compose exec -u peertube peertube npm run create-move-video-storage-job -- --to-object-storage --all-videos |
269 | ``` | 269 | ``` |
270 | 270 | ||
271 | <!-- TODO: uncomment when PeerTube 6 is released | ||
272 | ### create-generate-storyboard-job | ||
273 | |||
274 | **PeerTube >= 6.0** | ||
275 | |||
276 | Use this script to generate storyboard of a specific video: | ||
277 | |||
278 | ```bash | ||
279 | # Basic installation | ||
280 | cd /var/www/peertube/peertube-latest | ||
281 | sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-generate-storyboard-job -- -v [videoUUID] | ||
282 | |||
283 | # Docker installation | ||
284 | cd /var/www/peertube-docker | ||
285 | docker-compose exec -u peertube peertube npm run create-generate-storyboard-job -- -v [videoUUID] | ||
286 | ``` | ||
287 | |||
288 | The script can also generate all missing storyboards of local videos: | ||
289 | |||
290 | ```bash | ||
291 | # Basic installation | ||
292 | cd /var/www/peertube/peertube-latest | ||
293 | sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-generate-storyboard-job -- --all-videos | ||
294 | |||
295 | # Docker installation | ||
296 | cd /var/www/peertube-docker | ||
297 | docker-compose exec -u peertube peertube npm run create-generate-storyboard-job -- --all-videos | ||
298 | ``` | ||
299 | --> | ||
271 | 300 | ||
272 | ### prune-storage.js | 301 | ### prune-storage.js |
273 | 302 | ||
@@ -357,6 +386,15 @@ PeerTube >= 5.2 supports VOD or Live transcoding by a remote PeerTube runner. | |||
357 | 386 | ||
358 | ### Installation | 387 | ### Installation |
359 | 388 | ||
389 | Ensure you have `ffmpeg` and `ffprobe` installed on your system: | ||
390 | |||
391 | ```bash | ||
392 | ffprobe -version # Should be >= 4.3 | ||
393 | ffmpeg -version # Should be >= 4.3 | ||
394 | ``` | ||
395 | |||
396 | Then install the CLI: | ||
397 | |||
360 | ```bash | 398 | ```bash |
361 | sudo npm install -g @peertube/peertube-runner | 399 | sudo npm install -g @peertube/peertube-runner |
362 | ``` | 400 | ``` |
@@ -385,7 +423,7 @@ peertube-runner server | |||
385 | 423 | ||
386 | ### Register | 424 | ### Register |
387 | 425 | ||
388 | Then, you can register the runner on a new PeerTube instance so the runner can process its transcoding job: | 426 | Then, you can register the runner to process transcoding job of a remote PeerTube instance: |
389 | 427 | ||
390 | ```bash | 428 | ```bash |
391 | peertube-runner register --url http://peertube.example.com --registration-token ptrrt-... --runner-name my-runner-name | 429 | peertube-runner register --url http://peertube.example.com --registration-token ptrrt-... --runner-name my-runner-name |
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml index 8fff54229..0058cbd64 100644 --- a/support/docker/production/config/custom-environment-variables.yaml +++ b/support/docker/production/config/custom-environment-variables.yaml | |||
@@ -1,7 +1,7 @@ | |||
1 | # | 1 | # |
2 | # This file will be read by node-config | 2 | # This file will be read by node-config |
3 | # See https://github.com/node-config/node-config/wiki/Environment-Variables#custom-environment-variables | 3 | # See https://github.com/node-config/node-config/wiki/Environment-Variables#custom-environment-variables |
4 | # | 4 | # |
5 | 5 | ||
6 | webserver: | 6 | webserver: |
7 | hostname: "PEERTUBE_WEBSERVER_HOSTNAME" | 7 | hostname: "PEERTUBE_WEBSERVER_HOSTNAME" |
@@ -86,10 +86,10 @@ object_storage: | |||
86 | base_url: "PEERTUBE_OBJECT_STORAGE_STREAMING_PLAYLISTS_BASE_URL" | 86 | base_url: "PEERTUBE_OBJECT_STORAGE_STREAMING_PLAYLISTS_BASE_URL" |
87 | upload_acl: "PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL" | 87 | upload_acl: "PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL" |
88 | 88 | ||
89 | videos: | 89 | web_videos: |
90 | bucket_name: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BUCKET_NAME" | 90 | bucket_name: "PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BUCKET_NAME" |
91 | prefix: "PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX" | 91 | prefix: "PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_PREFIX" |
92 | base_url: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL" | 92 | base_url: "PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BASE_URL" |
93 | 93 | ||
94 | webadmin: | 94 | webadmin: |
95 | configuration: | 95 | configuration: |
@@ -177,9 +177,9 @@ transcoding: | |||
177 | 2160p: | 177 | 2160p: |
178 | __name: "PEERTUBE_TRANSCODING_2160P" | 178 | __name: "PEERTUBE_TRANSCODING_2160P" |
179 | __format: "json" | 179 | __format: "json" |
180 | webtorrent: | 180 | web_videos: |
181 | enabled: | 181 | enabled: |
182 | __name: "PEERTUBE_TRANSCODING_WEBTORRENT_ENABLED" | 182 | __name: "PEERTUBE_TRANSCODING_WEB_VIDEOS_ENABLED" |
183 | __format: "json" | 183 | __format: "json" |
184 | hls: | 184 | hls: |
185 | enabled: | 185 | enabled: |
diff --git a/support/docker/production/config/production.yaml b/support/docker/production/config/production.yaml index e3f6247d8..9c9581e40 100644 --- a/support/docker/production/config/production.yaml +++ b/support/docker/production/config/production.yaml | |||
@@ -47,7 +47,7 @@ storage: | |||
47 | tmp_persistent: '../data/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts | 47 | tmp_persistent: '../data/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts |
48 | bin: '../data/bin/' | 48 | bin: '../data/bin/' |
49 | avatars: '../data/avatars/' | 49 | avatars: '../data/avatars/' |
50 | videos: '../data/videos/' | 50 | web_videos: '../data/web-videos/' |
51 | streaming_playlists: '../data/streaming-playlists' | 51 | streaming_playlists: '../data/streaming-playlists' |
52 | redundancy: '../data/redundancy/' | 52 | redundancy: '../data/redundancy/' |
53 | logs: '../data/logs/' | 53 | logs: '../data/logs/' |
diff --git a/support/nginx/peertube b/support/nginx/peertube index 05a59c072..822f6f9ac 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube | |||
@@ -199,29 +199,7 @@ server { | |||
199 | alias /var/www/peertube/peertube-latest/client/dist/$1; | 199 | alias /var/www/peertube/peertube-latest/client/dist/$1; |
200 | } | 200 | } |
201 | 201 | ||
202 | # Bypass PeerTube for performance reasons. Optional. | 202 | location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download { |
203 | location ~ ^/static/(thumbnails|avatars)/ { | ||
204 | if ($request_method = 'OPTIONS') { | ||
205 | add_header Access-Control-Allow-Origin '*'; | ||
206 | add_header Access-Control-Allow-Methods 'GET, OPTIONS'; | ||
207 | add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; | ||
208 | add_header Access-Control-Max-Age 1728000; # Preflight request can be cached 20 days | ||
209 | add_header Content-Type 'text/plain charset=UTF-8'; | ||
210 | add_header Content-Length 0; | ||
211 | return 204; | ||
212 | } | ||
213 | |||
214 | add_header Access-Control-Allow-Origin '*'; | ||
215 | add_header Access-Control-Allow-Methods 'GET, OPTIONS'; | ||
216 | add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; | ||
217 | add_header Cache-Control "public, max-age=7200"; # Cache response 2 hours | ||
218 | |||
219 | rewrite ^/static/(.*)$ /$1 break; | ||
220 | |||
221 | try_files $uri @api; | ||
222 | } | ||
223 | |||
224 | location ~ ^(/static/(webseed|streaming-playlists)/private/)|^/download { | ||
225 | # We can't rate limit a try_files directive, so we need to duplicate @api | 203 | # We can't rate limit a try_files directive, so we need to duplicate @api |
226 | 204 | ||
227 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | 205 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
@@ -234,16 +212,10 @@ server { | |||
234 | } | 212 | } |
235 | 213 | ||
236 | # Bypass PeerTube for performance reasons. Optional. | 214 | # Bypass PeerTube for performance reasons. Optional. |
237 | location ~ ^/static/(webseed|redundancy|streaming-playlists)/ { | 215 | location ~ ^/static/(webseed|web-videos|redundancy|streaming-playlists)/ { |
238 | limit_rate_after 5M; | 216 | limit_rate_after 5M; |
239 | 217 | ||
240 | # Clients usually have 4 simultaneous webseed connections, so the real limit is 3MB/s per client | 218 | set $peertube_limit_rate 5M; |
241 | set $peertube_limit_rate 800k; | ||
242 | |||
243 | # Increase rate limit in HLS mode, because we don't have multiple simultaneous connections | ||
244 | if ($request_uri ~ -fragmented.mp4$) { | ||
245 | set $peertube_limit_rate 5M; | ||
246 | } | ||
247 | 219 | ||
248 | # Use this line with nginx >= 1.17.0 | 220 | # Use this line with nginx >= 1.17.0 |
249 | limit_rate $peertube_limit_rate; | 221 | limit_rate $peertube_limit_rate; |
@@ -430,6 +430,16 @@ | |||
430 | "@aws-sdk/util-hex-encoding" "3.310.0" | 430 | "@aws-sdk/util-hex-encoding" "3.310.0" |
431 | tslib "^2.5.0" | 431 | tslib "^2.5.0" |
432 | 432 | ||
433 | "@aws-sdk/eventstream-codec@3.342.0": | ||
434 | version "3.342.0" | ||
435 | resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.342.0.tgz#aef9ab3c5fdaa02c6da9836194eada9d35515fa1" | ||
436 | integrity sha512-IwtvSuplioMyiu/pQgpazKkGWDM5M5BOx85zmsB0uNxt6rmje8+WqPmGmuPdmJv4bLC5dJPLovcCp/fuH8XWhA== | ||
437 | dependencies: | ||
438 | "@aws-crypto/crc32" "3.0.0" | ||
439 | "@aws-sdk/types" "3.342.0" | ||
440 | "@aws-sdk/util-hex-encoding" "3.310.0" | ||
441 | tslib "^2.5.0" | ||
442 | |||
433 | "@aws-sdk/eventstream-serde-browser@3.329.0": | 443 | "@aws-sdk/eventstream-serde-browser@3.329.0": |
434 | version "3.329.0" | 444 | version "3.329.0" |
435 | resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.329.0.tgz#3ba7866a691905e2af8a89c1f562f91fb3779ef9" | 445 | resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.329.0.tgz#3ba7866a691905e2af8a89c1f562f91fb3779ef9" |
@@ -571,6 +581,17 @@ | |||
571 | "@aws-sdk/util-middleware" "3.329.0" | 581 | "@aws-sdk/util-middleware" "3.329.0" |
572 | tslib "^2.5.0" | 582 | tslib "^2.5.0" |
573 | 583 | ||
584 | "@aws-sdk/middleware-endpoint@3.344.0": | ||
585 | version "3.344.0" | ||
586 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.344.0.tgz#3acd2815fcbd07b005fb8ffea09a0a109b5acb93" | ||
587 | integrity sha512-rg4ysfusGw5tm8XTqNpdWo0wP0K79hZs3z1xkkskeSsMrbYiDn78Bkkt4s3JELUJY64VanQktPaKo08dNFYNZw== | ||
588 | dependencies: | ||
589 | "@aws-sdk/middleware-serde" "3.342.0" | ||
590 | "@aws-sdk/types" "3.342.0" | ||
591 | "@aws-sdk/url-parser" "3.342.0" | ||
592 | "@aws-sdk/util-middleware" "3.342.0" | ||
593 | tslib "^2.5.0" | ||
594 | |||
574 | "@aws-sdk/middleware-expect-continue@3.329.0": | 595 | "@aws-sdk/middleware-expect-continue@3.329.0": |
575 | version "3.329.0" | 596 | version "3.329.0" |
576 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.329.0.tgz#2a69584020b9c93926b83735fbd9741de117a586" | 597 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.329.0.tgz#2a69584020b9c93926b83735fbd9741de117a586" |
@@ -667,6 +688,14 @@ | |||
667 | "@aws-sdk/types" "3.329.0" | 688 | "@aws-sdk/types" "3.329.0" |
668 | tslib "^2.5.0" | 689 | tslib "^2.5.0" |
669 | 690 | ||
691 | "@aws-sdk/middleware-serde@3.342.0": | ||
692 | version "3.342.0" | ||
693 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.342.0.tgz#ed051e4e7dfc33e431aa27f260e065b9fbb5ee0f" | ||
694 | integrity sha512-WRD+Cyu6+h1ymfPnAw4fI2q3zXjihJ55HFe1uRF8VPN4uBbJNfN3IqL38y/SMEdZ0gH9zNlRNxZLhR0q6SNZEQ== | ||
695 | dependencies: | ||
696 | "@aws-sdk/types" "3.342.0" | ||
697 | tslib "^2.5.0" | ||
698 | |||
670 | "@aws-sdk/middleware-signing@3.329.0": | 699 | "@aws-sdk/middleware-signing@3.329.0": |
671 | version "3.329.0" | 700 | version "3.329.0" |
672 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.329.0.tgz#25011abb0911c1a23840d8d228676758f5b55926" | 701 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.329.0.tgz#25011abb0911c1a23840d8d228676758f5b55926" |
@@ -694,6 +723,13 @@ | |||
694 | dependencies: | 723 | dependencies: |
695 | tslib "^2.5.0" | 724 | tslib "^2.5.0" |
696 | 725 | ||
726 | "@aws-sdk/middleware-stack@3.342.0": | ||
727 | version "3.342.0" | ||
728 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.342.0.tgz#e755815cb22a66f15a964db12e998211f736eda0" | ||
729 | integrity sha512-nDYtLAv9IZq8YFxtbyAiK/U1mtvtJS0DG6HiIPT5jpHcRpuWRHQ170EAW51zYts+21Ffj1VA6ZPkbup83+T6/w== | ||
730 | dependencies: | ||
731 | tslib "^2.5.0" | ||
732 | |||
697 | "@aws-sdk/middleware-user-agent@3.332.0": | 733 | "@aws-sdk/middleware-user-agent@3.332.0": |
698 | version "3.332.0" | 734 | version "3.332.0" |
699 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.332.0.tgz#6f2de9579b09dd7feeab27ef8a18c236694ad903" | 735 | resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.332.0.tgz#6f2de9579b09dd7feeab27ef8a18c236694ad903" |
@@ -741,6 +777,14 @@ | |||
741 | "@aws-sdk/types" "3.329.0" | 777 | "@aws-sdk/types" "3.329.0" |
742 | tslib "^2.5.0" | 778 | tslib "^2.5.0" |
743 | 779 | ||
780 | "@aws-sdk/protocol-http@3.342.0": | ||
781 | version "3.342.0" | ||
782 | resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.342.0.tgz#2f4852a1ff14491f8785ca094684e7fcd80db4e5" | ||
783 | integrity sha512-zuF2urcTJBZ1tltPdTBQzRasuGB7+4Yfs9i5l0F7lE0luK5Azy6G+2r3WWENUNxFTYuP94GrrqaOhVyj8XXLPQ== | ||
784 | dependencies: | ||
785 | "@aws-sdk/types" "3.342.0" | ||
786 | tslib "^2.5.0" | ||
787 | |||
744 | "@aws-sdk/querystring-builder@3.329.0": | 788 | "@aws-sdk/querystring-builder@3.329.0": |
745 | version "3.329.0" | 789 | version "3.329.0" |
746 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.329.0.tgz#c6e6dd03dcd4378d1fbee576ce2a81dd94ac46a6" | 790 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.329.0.tgz#c6e6dd03dcd4378d1fbee576ce2a81dd94ac46a6" |
@@ -750,6 +794,15 @@ | |||
750 | "@aws-sdk/util-uri-escape" "3.310.0" | 794 | "@aws-sdk/util-uri-escape" "3.310.0" |
751 | tslib "^2.5.0" | 795 | tslib "^2.5.0" |
752 | 796 | ||
797 | "@aws-sdk/querystring-builder@3.342.0": | ||
798 | version "3.342.0" | ||
799 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.342.0.tgz#1163c1b9ec901b1264911be504a42638113f1002" | ||
800 | integrity sha512-tb3FbtC36a7XBYeupdKm60LeM0etp73I6/7pDAkzAlw7zJdvY0aQIvj1c0U6nZlwZF8sSSxC7vlamR+wCspdMw== | ||
801 | dependencies: | ||
802 | "@aws-sdk/types" "3.342.0" | ||
803 | "@aws-sdk/util-uri-escape" "3.310.0" | ||
804 | tslib "^2.5.0" | ||
805 | |||
753 | "@aws-sdk/querystring-parser@3.329.0": | 806 | "@aws-sdk/querystring-parser@3.329.0": |
754 | version "3.329.0" | 807 | version "3.329.0" |
755 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.329.0.tgz#dbbf2fd23ff0dfa2e4663fa414de1d5e60814896" | 808 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.329.0.tgz#dbbf2fd23ff0dfa2e4663fa414de1d5e60814896" |
@@ -758,6 +811,27 @@ | |||
758 | "@aws-sdk/types" "3.329.0" | 811 | "@aws-sdk/types" "3.329.0" |
759 | tslib "^2.5.0" | 812 | tslib "^2.5.0" |
760 | 813 | ||
814 | "@aws-sdk/querystring-parser@3.342.0": | ||
815 | version "3.342.0" | ||
816 | resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.342.0.tgz#20b3e13cb727171045625c1fbb87e351f300bb20" | ||
817 | integrity sha512-6svvr/LZW1EPJaARnOpjf92FIiK25wuO7fRq05gLTcTRAfUMDvub+oDg3Ro9EjJERumrYQrYCem5Qi4X9w8K2g== | ||
818 | dependencies: | ||
819 | "@aws-sdk/types" "3.342.0" | ||
820 | tslib "^2.5.0" | ||
821 | |||
822 | "@aws-sdk/s3-request-presigner@^3.345.0": | ||
823 | version "3.345.0" | ||
824 | resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.345.0.tgz#3e1e82123b57eae816bc3132c23244b4272d327d" | ||
825 | integrity sha512-xtmYp0d5OzYoiXo2Vw4JtIyW40OvFU68keC4p4Ik9ttQVVQIQ9kgphxBGAYezgcXNBbxeZ/VJUZuP7SkbVlyWA== | ||
826 | dependencies: | ||
827 | "@aws-sdk/middleware-endpoint" "3.344.0" | ||
828 | "@aws-sdk/protocol-http" "3.342.0" | ||
829 | "@aws-sdk/signature-v4-multi-region" "3.344.0" | ||
830 | "@aws-sdk/smithy-client" "3.342.0" | ||
831 | "@aws-sdk/types" "3.342.0" | ||
832 | "@aws-sdk/util-format-url" "3.342.0" | ||
833 | tslib "^2.5.0" | ||
834 | |||
761 | "@aws-sdk/service-error-classification@3.329.0": | 835 | "@aws-sdk/service-error-classification@3.329.0": |
762 | version "3.329.0" | 836 | version "3.329.0" |
763 | resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.329.0.tgz#32db59091ff28f14e526cee738bc14e32a6850f6" | 837 | resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.329.0.tgz#32db59091ff28f14e526cee738bc14e32a6850f6" |
@@ -781,6 +855,16 @@ | |||
781 | "@aws-sdk/types" "3.329.0" | 855 | "@aws-sdk/types" "3.329.0" |
782 | tslib "^2.5.0" | 856 | tslib "^2.5.0" |
783 | 857 | ||
858 | "@aws-sdk/signature-v4-multi-region@3.344.0": | ||
859 | version "3.344.0" | ||
860 | resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.344.0.tgz#38c2da1c75c13d93964ac4a3682b427eeb75253a" | ||
861 | integrity sha512-B5hN9b0Qa3UvpzsLjGIeCZ9AXE1qpwSXNXEeGcAdUIyf6lG3l+JMREKr+ZVaqAwAcZCOWmUyuuHIhkiK5YzClg== | ||
862 | dependencies: | ||
863 | "@aws-sdk/protocol-http" "3.342.0" | ||
864 | "@aws-sdk/signature-v4" "3.342.0" | ||
865 | "@aws-sdk/types" "3.342.0" | ||
866 | tslib "^2.5.0" | ||
867 | |||
784 | "@aws-sdk/signature-v4@3.329.0": | 868 | "@aws-sdk/signature-v4@3.329.0": |
785 | version "3.329.0" | 869 | version "3.329.0" |
786 | resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.329.0.tgz#8d40683189678f49504169c923e8342247b1da70" | 870 | resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.329.0.tgz#8d40683189678f49504169c923e8342247b1da70" |
@@ -794,6 +878,20 @@ | |||
794 | "@aws-sdk/util-utf8" "3.310.0" | 878 | "@aws-sdk/util-utf8" "3.310.0" |
795 | tslib "^2.5.0" | 879 | tslib "^2.5.0" |
796 | 880 | ||
881 | "@aws-sdk/signature-v4@3.342.0": | ||
882 | version "3.342.0" | ||
883 | resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.342.0.tgz#c2249594c53c76891986e3a54a077062a0b55b63" | ||
884 | integrity sha512-OWrGO2UOa1ENpy0kYd2shK4sklQygWUqvWLx9FotDbjIeUIEfAnqoPq/QqcXVrNyT/UvPi4iIrjHJEO8JCNRmA== | ||
885 | dependencies: | ||
886 | "@aws-sdk/eventstream-codec" "3.342.0" | ||
887 | "@aws-sdk/is-array-buffer" "3.310.0" | ||
888 | "@aws-sdk/types" "3.342.0" | ||
889 | "@aws-sdk/util-hex-encoding" "3.310.0" | ||
890 | "@aws-sdk/util-middleware" "3.342.0" | ||
891 | "@aws-sdk/util-uri-escape" "3.310.0" | ||
892 | "@aws-sdk/util-utf8" "3.310.0" | ||
893 | tslib "^2.5.0" | ||
894 | |||
797 | "@aws-sdk/smithy-client@3.329.0": | 895 | "@aws-sdk/smithy-client@3.329.0": |
798 | version "3.329.0" | 896 | version "3.329.0" |
799 | resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.329.0.tgz#54705963939855c87ae6e6c88196d23e819d728e" | 897 | resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.329.0.tgz#54705963939855c87ae6e6c88196d23e819d728e" |
@@ -803,6 +901,15 @@ | |||
803 | "@aws-sdk/types" "3.329.0" | 901 | "@aws-sdk/types" "3.329.0" |
804 | tslib "^2.5.0" | 902 | tslib "^2.5.0" |
805 | 903 | ||
904 | "@aws-sdk/smithy-client@3.342.0": | ||
905 | version "3.342.0" | ||
906 | resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.342.0.tgz#976ec7ca4e029145707c33d6300d60efcee53214" | ||
907 | integrity sha512-HQ4JejjHU2X7OAZPwixFG+EyPSjmoZqll7EvWjPSKyclWrM320haWWz1trVzjG/AgPfeDLfRkH/JoMr13lECew== | ||
908 | dependencies: | ||
909 | "@aws-sdk/middleware-stack" "3.342.0" | ||
910 | "@aws-sdk/types" "3.342.0" | ||
911 | tslib "^2.5.0" | ||
912 | |||
806 | "@aws-sdk/token-providers@3.335.0": | 913 | "@aws-sdk/token-providers@3.335.0": |
807 | version "3.335.0" | 914 | version "3.335.0" |
808 | resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.335.0.tgz#fcd7bdf62a17343c3bd6f57f58511e6eda7b81f9" | 915 | resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.335.0.tgz#fcd7bdf62a17343c3bd6f57f58511e6eda7b81f9" |
@@ -821,6 +928,13 @@ | |||
821 | dependencies: | 928 | dependencies: |
822 | tslib "^2.5.0" | 929 | tslib "^2.5.0" |
823 | 930 | ||
931 | "@aws-sdk/types@3.342.0": | ||
932 | version "3.342.0" | ||
933 | resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.342.0.tgz#0bcba3b5966f28e0725122697a19ece8647afbec" | ||
934 | integrity sha512-5uyXVda/AgUpdZNJ9JPHxwyxr08miPiZ/CKSMcRdQVjcNnrdzY9m/iM9LvnQT44sQO+IEEkF2IoZIWvZcq199A== | ||
935 | dependencies: | ||
936 | tslib "^2.5.0" | ||
937 | |||
824 | "@aws-sdk/url-parser@3.329.0": | 938 | "@aws-sdk/url-parser@3.329.0": |
825 | version "3.329.0" | 939 | version "3.329.0" |
826 | resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.329.0.tgz#a2862834a832ec1d379791f5233e378b75fc63ad" | 940 | resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.329.0.tgz#a2862834a832ec1d379791f5233e378b75fc63ad" |
@@ -830,6 +944,15 @@ | |||
830 | "@aws-sdk/types" "3.329.0" | 944 | "@aws-sdk/types" "3.329.0" |
831 | tslib "^2.5.0" | 945 | tslib "^2.5.0" |
832 | 946 | ||
947 | "@aws-sdk/url-parser@3.342.0": | ||
948 | version "3.342.0" | ||
949 | resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.342.0.tgz#c0be80c1d88b0ff8a8224de0ff7de64ccd5ef186" | ||
950 | integrity sha512-r4s/FDK6iywl8l4TqEwIwtNvxWO0kZes03c/yCiRYqxlkjVmbXEOodn5IAAweAeS9yqC3sl/wKbsaoBiGFn45g== | ||
951 | dependencies: | ||
952 | "@aws-sdk/querystring-parser" "3.342.0" | ||
953 | "@aws-sdk/types" "3.342.0" | ||
954 | tslib "^2.5.0" | ||
955 | |||
833 | "@aws-sdk/util-arn-parser@3.310.0": | 956 | "@aws-sdk/util-arn-parser@3.310.0": |
834 | version "3.310.0" | 957 | version "3.310.0" |
835 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz#861ff8810851be52a320ec9e4786f15b5fc74fba" | 958 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz#861ff8810851be52a320ec9e4786f15b5fc74fba" |
@@ -904,6 +1027,15 @@ | |||
904 | "@aws-sdk/types" "3.329.0" | 1027 | "@aws-sdk/types" "3.329.0" |
905 | tslib "^2.5.0" | 1028 | tslib "^2.5.0" |
906 | 1029 | ||
1030 | "@aws-sdk/util-format-url@3.342.0": | ||
1031 | version "3.342.0" | ||
1032 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.342.0.tgz#c2f0e0fd831b2fadb1341bce7fdaad3da3e61cf4" | ||
1033 | integrity sha512-GXFxd7unAT3FkJmfTLABcbzDLMiLAtaWYcUlfV/6oHGxc+Pgv/IRq+0kWeBOlivqwRKxr8rAaCS0U8NcnSASDA== | ||
1034 | dependencies: | ||
1035 | "@aws-sdk/querystring-builder" "3.342.0" | ||
1036 | "@aws-sdk/types" "3.342.0" | ||
1037 | tslib "^2.5.0" | ||
1038 | |||
907 | "@aws-sdk/util-hex-encoding@3.310.0": | 1039 | "@aws-sdk/util-hex-encoding@3.310.0": |
908 | version "3.310.0" | 1040 | version "3.310.0" |
909 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.310.0.tgz#19294c78986c90ae33f04491487863dc1d33bd87" | 1041 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.310.0.tgz#19294c78986c90ae33f04491487863dc1d33bd87" |
@@ -925,6 +1057,13 @@ | |||
925 | dependencies: | 1057 | dependencies: |
926 | tslib "^2.5.0" | 1058 | tslib "^2.5.0" |
927 | 1059 | ||
1060 | "@aws-sdk/util-middleware@3.342.0": | ||
1061 | version "3.342.0" | ||
1062 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.342.0.tgz#db8f50136bcba3d480d5c8e5340aecaa1e1c3a6c" | ||
1063 | integrity sha512-P2LYyMP4JUFZBy9DcMvCDxWU34mlShCyrqBZ1ouuGW7UMgRb1PTEvpLAVndIWn9H+1KGDFjMqOWp1FZHr4YZOA== | ||
1064 | dependencies: | ||
1065 | tslib "^2.5.0" | ||
1066 | |||
928 | "@aws-sdk/util-retry@3.329.0": | 1067 | "@aws-sdk/util-retry@3.329.0": |
929 | version "3.329.0" | 1068 | version "3.329.0" |
930 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.329.0.tgz#20b71504dd907e70a457cd56dcd131d08d6de39c" | 1069 | resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.329.0.tgz#20b71504dd907e70a457cd56dcd131d08d6de39c" |