Compare commits
No commits in common. "develop" and "release/5.2.0" have entirely different histories.
develop
...
release/5.
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": "standard-with-typescript",
|
"extends": "standard-with-typescript",
|
||||||
"root": true,
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"eol-last": [
|
"eol-last": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -127,20 +126,18 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"node_modules",
|
"node_modules/",
|
||||||
"packages/tests/fixtures",
|
"server/tests/fixtures"
|
||||||
"apps/**/dist",
|
|
||||||
"packages/**/dist",
|
|
||||||
"server/dist",
|
|
||||||
"packages/types-generator/tests",
|
|
||||||
"*.js",
|
|
||||||
"/client",
|
|
||||||
"/dist"
|
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true,
|
||||||
"project": [
|
"project": [
|
||||||
"./tsconfig.eslint.json"
|
"./tsconfig.json",
|
||||||
],
|
"./shared/tsconfig.json",
|
||||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true
|
"./scripts/tsconfig.json",
|
||||||
|
"./server/tsconfig.json",
|
||||||
|
"./server/tools/tsconfig.json",
|
||||||
|
"./packages/peertube-runner/tsconfig.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
.github/CONTRIBUTING.md
vendored
31
.github/CONTRIBUTING.md
vendored
|
@ -53,25 +53,13 @@ interested in, user interface, design, decentralized architecture...
|
||||||
You can help to write the documentation of the REST API, code, architecture,
|
You can help to write the documentation of the REST API, code, architecture,
|
||||||
demonstrations.
|
demonstrations.
|
||||||
|
|
||||||
### User documentation
|
For the REST API you can see the documentation in [/support/doc/api](https://github.com/Chocobozzz/PeerTube/tree/develop/support/doc/api) directory.
|
||||||
|
Then, you can just open the `openapi.yaml` file in a special editor like [http://editor.swagger.io/](http://editor.swagger.io/) to easily see and edit the documentation. You can also use [redoc-cli](https://github.com/Redocly/redoc/blob/master/cli/README.md) and run `redoc-cli serve --watch support/doc/api/openapi.yaml` to see the final result.
|
||||||
The official user documentation is available on https://docs.joinpeertube.org/
|
|
||||||
|
|
||||||
You can update it by writing markdown files in the following repository: https://framagit.org/framasoft/peertube/documentation/
|
|
||||||
|
|
||||||
### REST API documentation
|
|
||||||
|
|
||||||
The [REST API documentation](https://docs.joinpeertube.org/api-rest-reference.html) is generated from `support/doc/api/openapi.yaml` file.
|
|
||||||
To quickly get a preview of your changes, you can generate the documentation *on the fly* using the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
npx @redocly/cli preview-docs ./support/doc/api/openapi.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Some hints:
|
Some hints:
|
||||||
* Routes are defined in [/server/core/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/core/controllers) directory
|
* Routes are defined in [/server/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/controllers) directory
|
||||||
* Parameters validators are defined in [/server/core/middlewares/validators](https://github.com/Chocobozzz/PeerTube/tree/develop/server/core/middlewares/validators) directory
|
* Parameters validators are defined in [/server/middlewares/validators](https://github.com/Chocobozzz/PeerTube/tree/develop/server/middlewares/validators) directory
|
||||||
* Models sent/received by the controllers are defined in [/packages/models](https://github.com/Chocobozzz/PeerTube/tree/develop/packages/models) directory
|
* Models sent/received by the controllers are defined in [/shared/models](https://github.com/Chocobozzz/PeerTube/tree/develop/shared/models) directory
|
||||||
|
|
||||||
|
|
||||||
## Improve the website
|
## Improve the website
|
||||||
|
@ -254,6 +242,15 @@ To test emails with PeerTube:
|
||||||
* Run [mailslurper](http://mailslurper.com/)
|
* Run [mailslurper](http://mailslurper.com/)
|
||||||
* Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
* Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||||
|
|
||||||
|
### OpenAPI documentation
|
||||||
|
|
||||||
|
The [REST API documentation](https://docs.joinpeertube.org/api-rest-reference.html) is generated from `support/doc/api/openapi.yaml` file.
|
||||||
|
To quickly get a preview of your changes, you can generate the documentation *on the fly* using the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx @redocly/cli preview-docs ./support/doc/api/openapi.yaml
|
||||||
|
```
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
PeerTube can be configured using environment variables.
|
PeerTube can be configured using environment variables.
|
||||||
|
|
|
@ -32,12 +32,4 @@ runs:
|
||||||
|
|
||||||
- name: Install peertube runner dependencies
|
- name: Install peertube runner dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd apps/peertube-runner && yarn install --frozen-lockfile
|
run: cd packages/peertube-runner && yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Install peertube CLI dependencies
|
|
||||||
shell: bash
|
|
||||||
run: cd apps/peertube-cli && yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Display PeerTube dependencies
|
|
||||||
shell: bash
|
|
||||||
run: ls -l node_modules/@peertube
|
|
||||||
|
|
5
.github/workflows/benchmark.yml
vendored
5
.github/workflows/benchmark.yml
vendored
|
@ -35,14 +35,13 @@ jobs:
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
startClient=`date +%s`
|
startClient=`date +%s`
|
||||||
npm run build:server
|
|
||||||
npm run build:client
|
npm run build:client
|
||||||
endClient=`date +%s`
|
endClient=`date +%s`
|
||||||
clientBuildTime=$((endClient-startClient))
|
clientBuildTime=$((endClient-startClient))
|
||||||
|
@ -72,7 +71,7 @@ jobs:
|
||||||
|
|
||||||
- name: Run benchmark
|
- name: Run benchmark
|
||||||
run: |
|
run: |
|
||||||
npm run benchmark-server -- -o benchmark.json
|
node dist/scripts/benchmark.js -o benchmark.json
|
||||||
|
|
||||||
- name: Display result
|
- name: Display result
|
||||||
run: |
|
run: |
|
||||||
|
|
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript-typescript' ]
|
language: [ 'javascript' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
config-file: ./.github/workflows/codeql/codeql-config.yml
|
config-file: ./.github/workflows/codeql/codeql-config.yml
|
||||||
|
@ -51,7 +51,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -65,4 +65,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
4
.github/workflows/codeql/codeql-config.yml
vendored
4
.github/workflows/codeql/codeql-config.yml
vendored
|
@ -1,6 +1,4 @@
|
||||||
name: "PeerTube CodeQL config"
|
name: "PeerTube CodeQL config"
|
||||||
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- packages/tests
|
- server/tests
|
||||||
- packages/server-commands
|
|
||||||
- packages/types-generator
|
|
||||||
|
|
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
|
@ -24,8 +24,8 @@ jobs:
|
||||||
# FIXME: https://github.com/actions/checkout/issues/290
|
# FIXME: https://github.com/actions/checkout/issues/290
|
||||||
git fetch --force --tags
|
git fetch --force --tags
|
||||||
|
|
||||||
one="{ \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bookworm\" }"
|
one="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bullseye\" }"
|
||||||
two="{ \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-bookworm,chocobozzz/peertube:$(git describe --abbrev=0)-bookworm\" }"
|
two="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-bullseye,chocobozzz/peertube:$(git describe --abbrev=0)-bullseye\" }"
|
||||||
three="{ \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
|
three="{ \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
|
||||||
|
|
||||||
matrix="[$one,$two,$three]"
|
matrix="[$one,$two,$three]"
|
||||||
|
|
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run nightly
|
run: npm run nightly
|
||||||
|
|
6
.github/workflows/stats.yml
vendored
6
.github/workflows/stats.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: Angular bundlewatch
|
- name: Angular bundlewatch
|
||||||
uses: jackyef/bundlewatch-gh-action@master
|
uses: jackyef/bundlewatch-gh-action@master
|
||||||
|
@ -36,12 +36,12 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
wget "https://github.com/boyter/scc/releases/download/v3.0.0/scc-3.0.0-x86_64-unknown-linux.zip"
|
wget "https://github.com/boyter/scc/releases/download/v3.0.0/scc-3.0.0-x86_64-unknown-linux.zip"
|
||||||
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
||||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,packages/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,server/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||||
|
|
||||||
- name: PeerTube client stats
|
- name: PeerTube client stats
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
npm run client:build-stats > client-build-stats.json
|
node dist/scripts/client-build-stats.js > client-build-stats.json
|
||||||
|
|
||||||
- name: PeerTube client lighthouse report
|
- name: PeerTube client lighthouse report
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -46,7 +46,6 @@ jobs:
|
||||||
PGHOST: localhost
|
PGHOST: localhost
|
||||||
NODE_PENDING_JOB_WAIT: 250
|
NODE_PENDING_JOB_WAIT: 250
|
||||||
ENABLE_OBJECT_STORAGE_TESTS: true
|
ENABLE_OBJECT_STORAGE_TESTS: true
|
||||||
ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS: true
|
|
||||||
OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }}
|
OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }}
|
||||||
OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }}
|
OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }}
|
||||||
YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@ -56,7 +55,7 @@ jobs:
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||||
|
|
||||||
|
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,8 +1,8 @@
|
||||||
# NPM instalation
|
# NPM instalation
|
||||||
node_modules
|
/node_modules/
|
||||||
|
/server/tools/node_modules
|
||||||
*npm-debug.log
|
*npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.yarn
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
/test1/
|
/test1/
|
||||||
|
@ -11,8 +11,8 @@ yarn-error.log
|
||||||
/test4/
|
/test4/
|
||||||
/test5/
|
/test5/
|
||||||
/test6/
|
/test6/
|
||||||
/packages/tests/fixtures/video_high_bitrate_1080p.mp4
|
/server/tests/fixtures/video_high_bitrate_1080p.mp4
|
||||||
/packages/tests/fixtures/video_59fps.mp4
|
/server/tests/fixtures/video_59fps.mp4
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
/storage
|
/storage
|
||||||
|
@ -23,7 +23,6 @@ yarn-error.log
|
||||||
/ffmpeg-4/
|
/ffmpeg-4/
|
||||||
/thumbnails/
|
/thumbnails/
|
||||||
/torrents/
|
/torrents/
|
||||||
/web-videos/
|
|
||||||
/videos/
|
/videos/
|
||||||
/previews/
|
/previews/
|
||||||
/logs/
|
/logs/
|
||||||
|
@ -49,14 +48,12 @@ yarn-error.log
|
||||||
/*.tar.xz
|
/*.tar.xz
|
||||||
/*.asc
|
/*.asc
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
/server/tools/import-mediacore.ts
|
||||||
/docker-volume/
|
/docker-volume/
|
||||||
/init.mp4
|
/init.mp4
|
||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# EsLint
|
# Packages
|
||||||
.eslintcache
|
/packages/types/dist/
|
||||||
|
|
||||||
# Compiled output
|
|
||||||
dist
|
|
||||||
|
|
10
.mocharc.cjs
10
.mocharc.cjs
|
@ -1,10 +0,0 @@
|
||||||
process.env.ESBK_TSCONFIG_PATH = './packages/tests/tsconfig.json'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
"node-option": [
|
|
||||||
"loader=tsx",
|
|
||||||
"no-warnings",
|
|
||||||
"conditions=peertube:tsx"
|
|
||||||
],
|
|
||||||
"timeout": 30000
|
|
||||||
}
|
|
187
CHANGELOG.md
187
CHANGELOG.md
|
@ -1,190 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v6.0.1
|
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
|
||||||
|
|
||||||
* If you upgrade from PeerTube **< v6.0.0**, please follow v6.0.0 IMPORTANT NOTES
|
|
||||||
* We've made some modifications in v6.0.0 IMPORTANT NOTES, so if you upgrade from PeerTube v6.0.0:
|
|
||||||
* Ensure `location = /api/v1/videos/upload-resumable {` has been replaced by `location ~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$ {` in your nginx configuration
|
|
||||||
* Ensure you updated `storage.web_videos` configuration value to use `web-videos/` directory name
|
|
||||||
* Ensure your directory name on filesystem is the same as `storage.web_videos` configuration value: directory on filesystem must be renamed from `videos/` to `web-videos/` to represent the value of `storage.web_videos`
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
* Fix CPU going to 100% on odd cpu count
|
|
||||||
* Increase storyboard generation job TTL
|
|
||||||
* Add missing `generate-video-storyboard` job type in admin jobs list
|
|
||||||
* Regenerate storyboard after studio job
|
|
||||||
|
|
||||||
|
|
||||||
## v6.0.0
|
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
|
||||||
|
|
||||||
We have many important notes in this release. We know it's a pain for sysadmin, but consider each one as a major step forward for PeerTube quality!
|
|
||||||
|
|
||||||
#### Sysadmins important notes
|
|
||||||
|
|
||||||
* Remove NodeJS 16 support (see https://nodejs.org/fr/blog/announcements/nodejs16-eol):
|
|
||||||
* Please upgrade to NodeJS 18 before upgrading PeerTube
|
|
||||||
* If you use NodeSource repository, you may have to migrate to their new repository: https://github.com/nodesource/distributions/wiki/How-to-migrate-to-the-new-repository
|
|
||||||
* Check in `production.yaml` that you use `127.0.0.1` instead of `localhost` for `listen.hostname`, `database.hostname` and `redis.hostname` as Node 18 favours IPv6 for `localhost` resolution
|
|
||||||
|
|
||||||
* Remove WebTorrent support in player:
|
|
||||||
* "WebTorrent videos" are renamed to "Web Video". The video format is the same, we just stop to use P2P for these videos
|
|
||||||
* There is no "Auto" quality anymore for Web Videos. The viewer has to explicitly choose the video resolution
|
|
||||||
* We still use P2P with the HLS player, which is the recommended transcoding format since several versions
|
|
||||||
* See https://github.com/Chocobozzz/PeerTube/issues/5465 for more information
|
|
||||||
|
|
||||||
* Configuration key that you must update in your `production.yaml` if not automatically done by your upgrade script:
|
|
||||||
* `storage.videos` must be **renamed** to `storage.web_videos`: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L151
|
|
||||||
* Configuration value of `storage.web_videos` must have the directory name to be **changed** from `videos/` to `web-videos/`: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L151
|
|
||||||
* Directory on filesystem must be **renamed** from `videos/` to `web-videos/` to represent the value of `storage.web_videos`
|
|
||||||
* Classic installation: `sudo -u peertube mv '/var/www/peertube/storage/videos/' '/var/www/peertube/storage/web-videos/'`
|
|
||||||
* Docker installation: `mv '/path-to-docker-installation/docker-volume/data/videos/' '/path-to-docker-installation/docker-volume/data/web-videos/'`
|
|
||||||
* `transcoding.webtorrent` must be **renamed** to `transcoding.web_videos`: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L532
|
|
||||||
* `object_storage.videos` must be **renamed** to `object_storage.web_videos`. The value of `object_storage.web_videos.bucket_name` doesn't need to be changed: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L223
|
|
||||||
* `storage.storyboards` must be **added**: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L157
|
|
||||||
|
|
||||||
* PeerTube Docker image now uses `bookworm`. `chocobozzz/peertube:production-bullseye` needs to be replaced by `chocobozzz/peertube:production-bookworm`
|
|
||||||
|
|
||||||
* Env configuration that your must update if you use Docker:
|
|
||||||
* `PEERTUBE_TRANSCODING_WEBTORRENT_ENABLED` must be **renamed** to `PEERTUBE_TRANSCODING_WEB_VIDEOS_ENABLED`
|
|
||||||
* `PEERTUBE_OBJECT_STORAGE_VIDEOS_BUCKET_NAME` must be **renamed** to `PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BUCKET_NAME`
|
|
||||||
* `PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX` must be **renamed** to `PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_PREFIX`
|
|
||||||
* `PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL` must be **renamed** to `PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BASE_URL`
|
|
||||||
|
|
||||||
* You must update nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube
|
|
||||||
* `location ~ ^/static/(thumbnails|avatars)/ {` block must be removed
|
|
||||||
* `location = /api/v1/videos/upload-resumable {` must be updated to `location ~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$ {`
|
|
||||||
* `location ~ ^(/static/(webseed|streaming-playlists)/private/)|^/download {` must be updated to `location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {`
|
|
||||||
* `location ~ ^/static/(webseed|redundancy|streaming-playlists)/ {` must be updated to `location ~ ^/static/(webseed|web-videos|redundancy|streaming-playlists)/ {`
|
|
||||||
|
|
||||||
* Tracing requires `--experimental-loader=@opentelemetry/instrumentation/hook.mjs` node option: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L264
|
|
||||||
|
|
||||||
#### Developers important notes
|
|
||||||
|
|
||||||
* REST API breaking changes:
|
|
||||||
* Removed `webtorrentEnabled` from user response (deprecated since 4.1 in favour of `p2pEnabled`)
|
|
||||||
* Removed `avatar` and `banner` fields from account/channel responses (deprecated since 4.2 in favour of `avatars` and `banners`)
|
|
||||||
* Removed `filter` query when listing videos (deprecated since 4.0 in favour of `isLocal` and `include`)
|
|
||||||
* Deprecate `/api/v1/videos/:id/webtorrent` video file routes in favour of `/api/v1/videos/:id/web-videos` routes
|
|
||||||
* Deprecate `hasWebtorrentFiles` body video filter in favour of `hasWebVideoFiles` when listing videos
|
|
||||||
* Deprecate `webtorrent` `transcodingType` in favour of `web-video` in `/api/v1/videos/{id}/transcoding` route
|
|
||||||
* `currentTime` is now required to notify the user is watching the video using `/api/v1/videos/{id}/views` (introduced in 4.2)
|
|
||||||
|
|
||||||
* Static server paths breaking changes:
|
|
||||||
* `/static/webseed/...` is deprecated in favour of `/static/web-videos/...`
|
|
||||||
* `/object-storage-proxy/webseed/...` is deprecated in favour of `/object-storage-proxy/web-videos/...`
|
|
||||||
* `/static/thumbnails/...` is deprecated in favour of `/static/lazy-thumbnails/...`
|
|
||||||
|
|
||||||
* Plugin API breaking changes:
|
|
||||||
* Deprecated `webtorrent` key in `getFiles()` helper result. Use `webVideo` instead
|
|
||||||
|
|
||||||
|
|
||||||
### CLI tools
|
|
||||||
|
|
||||||
* Removed unmaintained `peertube-import-videos` (also aliased as `peertube import-videos` or `peertube import`) script
|
|
||||||
* PeerTube remote CLI is much more simpler to install using NPM: https://docs.joinpeertube.org/maintain/tools#remote-peertube-cli
|
|
||||||
* Support moving video files from object storage to filesystem: https://docs.joinpeertube.org/maintain/tools#move-video-files-from-object-storage-to-filesystem
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :tada: **Add "Password protected" video privacy** [#5836](https://github.com/Chocobozzz/PeerTube/pull/5836) :tada:
|
|
||||||
* A single password can be set using the web interface at video upload/import/update
|
|
||||||
* The [REST API](https://docs.joinpeertube.org/api-rest-reference.html#tag/Video-Passwords) can store as many passwords as you want, allowing developers to use this feature to easily give or revoke access to a video *on the fly*
|
|
||||||
* Developers that use PeerTube embeds can set the video password using [the embed API](https://docs.joinpeertube.org/api/embed-player#setvideopassword-promise-void)
|
|
||||||
* :tada: **Add video storyboard support** :tada:
|
|
||||||
* PeerTube automatically generates a storyboard on video upload/import
|
|
||||||
* Viewers can see the image around the targeted timecode when hovering the progress bar
|
|
||||||
* Storyboard of videos uploaded/imported before v6 can be generated by the admin using `npm run create-generate-storyboard-job` command: https://docs.joinpeertube.org/maintain/tools#generate-storyboard
|
|
||||||
* :tada: **Add ability for users to replace their video file** :tada:
|
|
||||||
* Has to be enabled by the PeerTube instance administrator
|
|
||||||
* The user can replace the video file in the *Update Video* page
|
|
||||||
* The *re-upload* date is displayed under the video player
|
|
||||||
* :tada: **Add video chapters support** :tada:
|
|
||||||
* Add chapters in the upload/import/update video page or let PeerTube automatically imports them from the video container/youtube-dl
|
|
||||||
* Markers are displayed in the player progress bar to symbolize a chapter
|
|
||||||
* Chapter title is displayed when hovering/touching the player progress bar
|
|
||||||
* Better video player:
|
|
||||||
* More efficient as we don't rebuild the player every time the played video changes
|
|
||||||
* The player keeps the current player settings (playback speed, fullscreen...) when the played video changes
|
|
||||||
* Automatically adjust the player size to match video ratio
|
|
||||||
* Improve SEO and video link sharing:
|
|
||||||
* Use short video/channel/account URLs in sitemap and for canonical tags
|
|
||||||
* Add JSON-LD tag in embed page
|
|
||||||
* Embed page does not forbid indexation anymore: we use a canonical tag instead that targets the watch page
|
|
||||||
* Forbid indexation of remote videos, accounts and channels (instead of providing an invalid canonical tag)
|
|
||||||
* Truncate OpenGraph/Twitter card link description
|
|
||||||
* Fix client accessibility and keyboard navigation:
|
|
||||||
* Fix links in bootstrap alerts color
|
|
||||||
* Better input placeholder contrast
|
|
||||||
* Fix video miniature link label
|
|
||||||
* Add ability to disable hotkeys
|
|
||||||
* Improve table overall accessibility
|
|
||||||
* Wrap icons that can lead to an action inside buttons
|
|
||||||
* Fix left menu admin/my-library menu accessibility
|
|
||||||
* And many more improvements!
|
|
||||||
* Improve remote runner management:
|
|
||||||
* Add ability to remove runner jobs
|
|
||||||
* Add runner job state quick filter
|
|
||||||
* Merge registration tokens and runners tables in same page
|
|
||||||
* Add copy button to copy registration token
|
|
||||||
* Add ability for admins to force transcoding on a specific video even if it's in broken state (stuck in *To Transcode* for example)
|
|
||||||
* Add an option to sign federated fetches (ActivityPub based software such as Mastodon may require it to access content)
|
|
||||||
* Download video file directly from S3 using pre signed URLs
|
|
||||||
* Lazy download remote video thumbnails to reduce storage
|
|
||||||
* Improve recommended videos when the watched video doesn't have tags set
|
|
||||||
* Add more rate limits in configuration (`plugins`, `well-known`, `feeds`, `activity_pub` and `client` endpoints)
|
|
||||||
* Add ability to reset video *Originally published at* attribute
|
|
||||||
* Add ability for admins to set the default user channel name [#6000](https://github.com/Chocobozzz/PeerTube/pull/6000)
|
|
||||||
* Server now uses [ESM modules](https://nodejs.org/api/esm.html)
|
|
||||||
* Add worker threads Prometheus metrics
|
|
||||||
* Performance:
|
|
||||||
* Process unicast HTTP job in worker threads
|
|
||||||
* Sign ActivityPub requests in worker threads
|
|
||||||
* Optimize recommended videos HTTP request
|
|
||||||
* Optimize videos SQL queries when filtering on lives or tags
|
|
||||||
* Optimize `/videos/{id}/views` endpoint with many viewers
|
|
||||||
* Add ability to disable PeerTube HTTP logs
|
|
||||||
* Optimize homepage videos HTTP queries
|
|
||||||
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
* Don't cache upload response if the video has been deleted
|
|
||||||
* Fix broken upgrade script when using custom database port
|
|
||||||
* Prevent duplicate runner names
|
|
||||||
* Avoid runner job update error
|
|
||||||
* Notify remote runners there are available jobs when a job is aborted/errored
|
|
||||||
* Fix updating P2P settings in left menu
|
|
||||||
* Fix 500 HTTP error on invalid short UUID conversion
|
|
||||||
* Don't display admin email in `security.txt` well-known endpoint
|
|
||||||
* Optimize `update-host` script to fix out of memory error
|
|
||||||
* Fix error log when using an unconventional distribution of FFmpeg with a non-standard version string [#5917](https://github.com/Chocobozzz/PeerTube/pull/5917)
|
|
||||||
* Fix live replay REST API breaking change: `replaySettings.privacy` is not required anymore
|
|
||||||
* Fix broken live replay when updating replay privacy
|
|
||||||
* More robust *About* page when getting category from server
|
|
||||||
* Fix `ERR_HTTP_HEADERS_SENT` crash
|
|
||||||
* Avoid illegal characters in torrent filename
|
|
||||||
* Avoid federation error log with remote `Like` on `Note`
|
|
||||||
* Fix atom feed with *Science & Technology* category
|
|
||||||
* Support empty value returned by `filter:api.video.get.result` hook
|
|
||||||
* Prevent remote subscribe on accounts (not yet supported by PeerTube)
|
|
||||||
* Fix feed audio file mimetype
|
|
||||||
* Fix video quality on high video resolution/fps
|
|
||||||
* Fix disabling Object Storage ACL using Docker env `PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL_PUBLIC` and `PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL_PRIVATE` in `.env`
|
|
||||||
* Correctly end live session on ffprobe error
|
|
||||||
* Fix video stats X axis with old videos
|
|
||||||
* Fix empty master playlist upload on s3
|
|
||||||
* Correctly generate `production.yaml.new` that should merge your current `production.yaml` with new keys defined by PeerTube
|
|
||||||
* Fix card font color theme
|
|
||||||
* Respect "transcode original resolution" setting when using remote runners
|
|
||||||
* Prevent player mobile buttons flickering
|
|
||||||
* Fix graph zooming end date
|
|
||||||
|
|
||||||
|
|
||||||
## v5.2.1
|
## v5.2.1
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
@ -201,7 +16,7 @@ We have many important notes in this release. We know it's a pain for sysadmin,
|
||||||
|
|
||||||
* **Important** Remove NodeJS 14 support
|
* **Important** Remove NodeJS 14 support
|
||||||
* **Important** You must update your nginx configuration to support remote runners: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L101
|
* **Important** You must update your nginx configuration to support remote runners: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L101
|
||||||
* Add `storage.tmp_persistent` directory in configuration file. **You must configure it in your production.yaml**: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L148
|
* Add `storage.tmp_persistent` directory in configuration file. **You must configure it in your production.yaml**: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L128
|
||||||
* PeerTube requires **Docker Compose >= v2** for Docker compose installation
|
* PeerTube requires **Docker Compose >= v2** for Docker compose installation
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
|
|
29
CREDITS.md
29
CREDITS.md
|
@ -11,30 +11,30 @@
|
||||||
* Filip Bengtsson
|
* Filip Bengtsson
|
||||||
* Ihor Hordiichuk
|
* Ihor Hordiichuk
|
||||||
* Jeff Huang
|
* Jeff Huang
|
||||||
* Payman Moghadam
|
|
||||||
* Simon Brosdetzko
|
* Simon Brosdetzko
|
||||||
* kontrollanten
|
* kontrollanten
|
||||||
* Jiri Podhorecky
|
* Jiri Podhorecky
|
||||||
|
* Payman Moghadam
|
||||||
* Phongpanot
|
* Phongpanot
|
||||||
* hecko
|
* hecko
|
||||||
* Milo Ivir
|
|
||||||
* Laurent Ettouati
|
* Laurent Ettouati
|
||||||
|
* Milo Ivir
|
||||||
* kimsible
|
* kimsible
|
||||||
* Zet
|
* Zet
|
||||||
* GunChleoc
|
* GunChleoc
|
||||||
* Clemens Schielicke
|
* Clemens Schielicke
|
||||||
* Racida S
|
* Racida S
|
||||||
* Sveinn í Felli
|
|
||||||
* Ewout van Mansom
|
|
||||||
* Marcin Mikołajczak
|
* Marcin Mikołajczak
|
||||||
|
* Ewout van Mansom
|
||||||
* Eivind Ødegård
|
* Eivind Ødegård
|
||||||
|
* Sveinn í Felli
|
||||||
* Tirifto
|
* Tirifto
|
||||||
* Kim
|
* Kim
|
||||||
* Wicklow
|
|
||||||
* Armin
|
* Armin
|
||||||
* Hannes Ylä-Jääski
|
* Hannes Ylä-Jääski
|
||||||
* Vodoyo Kamal
|
|
||||||
* Mohamad Reza
|
* Mohamad Reza
|
||||||
|
* Vodoyo Kamal
|
||||||
|
* Wicklow
|
||||||
* John Livingston
|
* John Livingston
|
||||||
* Kimsible
|
* Kimsible
|
||||||
* Besnik Bleta
|
* Besnik Bleta
|
||||||
|
@ -71,7 +71,6 @@
|
||||||
* jan Seli
|
* jan Seli
|
||||||
* lutangar
|
* lutangar
|
||||||
* 李奕寯
|
* 李奕寯
|
||||||
* Blood Axe
|
|
||||||
* Martin Hoefler
|
* Martin Hoefler
|
||||||
* Porrumentzio
|
* Porrumentzio
|
||||||
* Poslovitch
|
* Poslovitch
|
||||||
|
@ -95,10 +94,12 @@
|
||||||
* Ms Kimsible
|
* Ms Kimsible
|
||||||
* Thomas Citharel
|
* Thomas Citharel
|
||||||
* Benjamin Bouvier
|
* Benjamin Bouvier
|
||||||
|
* Blood Axe
|
||||||
* Joe Bill
|
* Joe Bill
|
||||||
* Kemal Oktay Aktoğan
|
* Kemal Oktay Aktoğan
|
||||||
* Lucas Declercq
|
* Lucas Declercq
|
||||||
* Sirxy
|
* Sirxy
|
||||||
|
* chris@famichiki.tube
|
||||||
* matograine
|
* matograine
|
||||||
* Alexander Ivanov
|
* Alexander Ivanov
|
||||||
* Daniel Santos
|
* Daniel Santos
|
||||||
|
@ -150,8 +151,8 @@
|
||||||
* Benjamin Seitz
|
* Benjamin Seitz
|
||||||
* Bob Oob
|
* Bob Oob
|
||||||
* Booteille
|
* Booteille
|
||||||
|
* Chris Sakura 佐倉くりす on Youtube
|
||||||
* DontUseGithub
|
* DontUseGithub
|
||||||
* Farooq Karimi Zadeh
|
|
||||||
* I_Automne
|
* I_Automne
|
||||||
* Iñigo
|
* Iñigo
|
||||||
* Joan Montané
|
* Joan Montané
|
||||||
|
@ -195,6 +196,7 @@
|
||||||
* Eder Etxebarria
|
* Eder Etxebarria
|
||||||
* Ehsan Gholami
|
* Ehsan Gholami
|
||||||
* Elga Ahmad Prayoga
|
* Elga Ahmad Prayoga
|
||||||
|
* Farooq Karimi Zadeh
|
||||||
* Girish Ramakrishnan
|
* Girish Ramakrishnan
|
||||||
* Hakim Oubouali
|
* Hakim Oubouali
|
||||||
* Hans Meiser
|
* Hans Meiser
|
||||||
|
@ -204,7 +206,6 @@
|
||||||
* Jocelyn Jaubert
|
* Jocelyn Jaubert
|
||||||
* Johan Fleury
|
* Johan Fleury
|
||||||
* Jurij Podgoršek
|
* Jurij Podgoršek
|
||||||
* Kindred La Boneta
|
|
||||||
* Kiro
|
* Kiro
|
||||||
* Leopere
|
* Leopere
|
||||||
* Linus
|
* Linus
|
||||||
|
@ -234,7 +235,6 @@
|
||||||
* Ömer Faruk Çakmak
|
* Ömer Faruk Çakmak
|
||||||
* AQR_Rastiq
|
* AQR_Rastiq
|
||||||
* Al-Hassan Abdel-Raouf
|
* Al-Hassan Abdel-Raouf
|
||||||
* Alecks Gates
|
|
||||||
* Amos Tamam
|
* Amos Tamam
|
||||||
* Andrew Morgan
|
* Andrew Morgan
|
||||||
* Andy Khit
|
* Andy Khit
|
||||||
|
@ -246,12 +246,10 @@
|
||||||
* Average Dude
|
* Average Dude
|
||||||
* BitTube
|
* BitTube
|
||||||
* Boo Teille
|
* Boo Teille
|
||||||
* Branislav Pavelka
|
|
||||||
* Dashie
|
* Dashie
|
||||||
* David Luís Pereira Pires
|
* David Luís Pereira Pires
|
||||||
* David Marzal
|
* David Marzal
|
||||||
* EndoGai
|
* EndoGai
|
||||||
* Ettore Atalan
|
|
||||||
* Fatih Özsoy
|
* Fatih Özsoy
|
||||||
* FediverseTV
|
* FediverseTV
|
||||||
* Florent Fayolle
|
* Florent Fayolle
|
||||||
|
@ -267,7 +265,6 @@
|
||||||
* HybridGlucose
|
* HybridGlucose
|
||||||
* J C Worm
|
* J C Worm
|
||||||
* Jan Marsalek
|
* Jan Marsalek
|
||||||
* José M
|
|
||||||
* Joël Galeran
|
* Joël Galeran
|
||||||
* Julien Lemaire
|
* Julien Lemaire
|
||||||
* Lucas Teixeira
|
* Lucas Teixeira
|
||||||
|
@ -302,7 +299,6 @@
|
||||||
* libertas
|
* libertas
|
||||||
* merty
|
* merty
|
||||||
* plr20
|
* plr20
|
||||||
* q_h
|
|
||||||
* qwerty
|
* qwerty
|
||||||
* spf
|
* spf
|
||||||
* taziden
|
* taziden
|
||||||
|
@ -320,6 +316,7 @@
|
||||||
* Agron
|
* Agron
|
||||||
* Aitozl
|
* Aitozl
|
||||||
* Alberto Mardegan
|
* Alberto Mardegan
|
||||||
|
* Alecks Gates
|
||||||
* Alejandro Criado-Pérez
|
* Alejandro Criado-Pérez
|
||||||
* Aleksandr Sokolov
|
* Aleksandr Sokolov
|
||||||
* Alexander F. Rødseth
|
* Alexander F. Rødseth
|
||||||
|
@ -345,6 +342,7 @@
|
||||||
* Cadence Ember
|
* Cadence Ember
|
||||||
* Cale
|
* Cale
|
||||||
* Charles de Lacombe
|
* Charles de Lacombe
|
||||||
|
* Chris Sakura 佐倉くりす on Youtube - 日本語は第二言語やけ、間違っとったら思いっきり叩いてくださいw つたない日本語ばっかりやけど頑張りまーす♪
|
||||||
* Christoph Geschwind
|
* Christoph Geschwind
|
||||||
* Chronos
|
* Chronos
|
||||||
* Claude
|
* Claude
|
||||||
|
@ -384,7 +382,6 @@
|
||||||
* Iván Cabaleiro
|
* Iván Cabaleiro
|
||||||
* J Webb
|
* J Webb
|
||||||
* Jacen
|
* Jacen
|
||||||
* Jackson Chen
|
|
||||||
* Jacob
|
* Jacob
|
||||||
* Jacques Foucry
|
* Jacques Foucry
|
||||||
* Jagannath Bhat
|
* Jagannath Bhat
|
||||||
|
@ -494,7 +491,6 @@
|
||||||
* Vagelis F
|
* Vagelis F
|
||||||
* Varik Valefor
|
* Varik Valefor
|
||||||
* Vegard Fjeldberg
|
* Vegard Fjeldberg
|
||||||
* Victor Hampel
|
|
||||||
* Vik
|
* Vik
|
||||||
* Vincent Stakenburg
|
* Vincent Stakenburg
|
||||||
* WhiredPlanck
|
* WhiredPlanck
|
||||||
|
@ -544,6 +540,7 @@
|
||||||
* philippe lhardy
|
* philippe lhardy
|
||||||
* pitchum
|
* pitchum
|
||||||
* potedeo
|
* potedeo
|
||||||
|
* q_h
|
||||||
* rdxuan
|
* rdxuan
|
||||||
* retiolus
|
* retiolus
|
||||||
* ruvilonix
|
* ruvilonix
|
||||||
|
|
|
@ -116,7 +116,7 @@ Be it as a user or an instance administrator, you can decide what your experienc
|
||||||
|
|
||||||
<h3 align="right">Communities that help each other</h3>
|
<h3 align="right">Communities that help each other</h3>
|
||||||
<p align="right">
|
<p align="right">
|
||||||
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>).
|
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>).
|
||||||
</p>
|
</p>
|
||||||
<p align="right">
|
<p align="right">
|
||||||
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>).
|
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>).
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
src
|
|
||||||
meta.json
|
|
||||||
tsconfig.json
|
|
||||||
scripts
|
|
|
@ -1,43 +0,0 @@
|
||||||
# PeerTube CLI
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
See https://docs.joinpeertube.org/maintain/tools#remote-tools
|
|
||||||
|
|
||||||
## Dev
|
|
||||||
|
|
||||||
## Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
yarn install --pure-lockfile
|
|
||||||
cd apps/peertube-cli && yarn install --pure-lockfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
npm run dev:peertube-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
npm run build:peertube-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
node apps/peertube-cli/dist/peertube-cli.js --help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Publish on NPM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
(cd apps/peertube-cli && npm version patch) && npm run build:peertube-cli && (cd apps/peertube-cli && npm publish --access=public)
|
|
||||||
```
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@peertube/peertube-cli",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/peertube.js",
|
|
||||||
"bin": "dist/peertube.js",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.x"
|
|
||||||
},
|
|
||||||
"scripts": {},
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"private": false,
|
|
||||||
"devDependencies": {
|
|
||||||
"application-config": "^2.0.0",
|
|
||||||
"cli-table3": "^0.6.0",
|
|
||||||
"netrc-parser": "^3.1.6"
|
|
||||||
},
|
|
||||||
"dependencies": {}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import * as esbuild from 'esbuild'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
|
|
||||||
const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
|
|
||||||
|
|
||||||
export const esbuildOptions = {
|
|
||||||
entryPoints: [ './src/peertube.ts' ],
|
|
||||||
bundle: true,
|
|
||||||
platform: 'node',
|
|
||||||
format: 'esm',
|
|
||||||
target: 'node16',
|
|
||||||
external: [
|
|
||||||
'./lib-cov/fluent-ffmpeg',
|
|
||||||
'pg-hstore'
|
|
||||||
],
|
|
||||||
outfile: './dist/peertube.js',
|
|
||||||
banner: {
|
|
||||||
js: `const require = (await import("node:module")).createRequire(import.meta.url);` +
|
|
||||||
`const __filename = (await import("node:url")).fileURLToPath(import.meta.url);` +
|
|
||||||
`const __dirname = (await import("node:path")).dirname(__filename);`
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
'process.env.PACKAGE_VERSION': `'${packageJSON.version}'`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await esbuild.build(esbuildOptions)
|
|
|
@ -1,7 +0,0 @@
|
||||||
import * as esbuild from 'esbuild'
|
|
||||||
import { esbuildOptions } from './build.js'
|
|
||||||
|
|
||||||
const context = await esbuild.context(esbuildOptions)
|
|
||||||
|
|
||||||
// Enable watch mode
|
|
||||||
await context.watch()
|
|
|
@ -1,171 +0,0 @@
|
||||||
import CliTable3 from 'cli-table3'
|
|
||||||
import prompt from 'prompt'
|
|
||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared/index.js'
|
|
||||||
|
|
||||||
export function defineAuthProgram () {
|
|
||||||
const program = new Command()
|
|
||||||
.name('auth')
|
|
||||||
.description('Register your accounts on remote instances to use them with other commands')
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('add')
|
|
||||||
.description('remember your accounts on remote instances for easier use')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('--default', 'add the entry as the new default')
|
|
||||||
.action(options => {
|
|
||||||
/* eslint-disable no-import-assign */
|
|
||||||
prompt.override = options
|
|
||||||
prompt.start()
|
|
||||||
prompt.get({
|
|
||||||
properties: {
|
|
||||||
url: {
|
|
||||||
description: 'instance url',
|
|
||||||
conform: value => isURLaPeerTubeInstance(value),
|
|
||||||
message: 'It should be an URL (https://peertube.example.com)',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
conform: value => typeof value === 'string' && value.length !== 0,
|
|
||||||
message: 'Name must be only letters, spaces, or dashes',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
hidden: true,
|
|
||||||
replace: '*',
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, async (_, result) => {
|
|
||||||
|
|
||||||
// Check credentials
|
|
||||||
try {
|
|
||||||
// Strip out everything after the domain:port.
|
|
||||||
// See https://github.com/Chocobozzz/PeerTube/issues/3520
|
|
||||||
result.url = stripExtraneousFromPeerTubeUrl(result.url)
|
|
||||||
|
|
||||||
const server = buildServer(result.url)
|
|
||||||
await assignToken(server, result.username, result.password)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
await setInstance(result.url, result.username, result.password, options.default)
|
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('del <url>')
|
|
||||||
.description('Unregisters a remote instance')
|
|
||||||
.action(async url => {
|
|
||||||
await delInstance(url)
|
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('list')
|
|
||||||
.description('List registered remote instances')
|
|
||||||
.action(async () => {
|
|
||||||
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
|
||||||
|
|
||||||
const table = new CliTable3({
|
|
||||||
head: [ 'instance', 'login' ],
|
|
||||||
colWidths: [ 30, 30 ]
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
settings.remotes.forEach(element => {
|
|
||||||
if (!netrc.machines[element]) return
|
|
||||||
|
|
||||||
table.push([
|
|
||||||
element,
|
|
||||||
netrc.machines[element].login
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(table.toString())
|
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('set-default <url>')
|
|
||||||
.description('Set an existing entry as default')
|
|
||||||
.action(async url => {
|
|
||||||
const settings = await getSettings()
|
|
||||||
const instanceExists = settings.remotes.includes(url)
|
|
||||||
|
|
||||||
if (instanceExists) {
|
|
||||||
settings.default = settings.remotes.indexOf(url)
|
|
||||||
await writeSettings(settings)
|
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
} else {
|
|
||||||
console.log('<url> is not a registered instance.')
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program.addHelpText('after', '\n\n Examples:\n\n' +
|
|
||||||
' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' +
|
|
||||||
' $ peertube auth add -u https://peertube.cpy.re -U root\n' +
|
|
||||||
' $ peertube auth list\n' +
|
|
||||||
' $ peertube auth del https://peertube.cpy.re\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
return program
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Private
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function delInstance (url: string) {
|
|
||||||
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
|
||||||
|
|
||||||
const index = settings.remotes.indexOf(url)
|
|
||||||
settings.remotes.splice(index)
|
|
||||||
|
|
||||||
if (settings.default === index) settings.default = -1
|
|
||||||
|
|
||||||
await writeSettings(settings)
|
|
||||||
|
|
||||||
delete netrc.machines[url]
|
|
||||||
|
|
||||||
await netrc.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setInstance (url: string, username: string, password: string, isDefault: boolean) {
|
|
||||||
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
|
|
||||||
|
|
||||||
if (settings.remotes.includes(url) === false) {
|
|
||||||
settings.remotes.push(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefault || settings.remotes.length === 1) {
|
|
||||||
settings.default = settings.remotes.length - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeSettings(settings)
|
|
||||||
|
|
||||||
netrc.machines[url] = { login: username, password }
|
|
||||||
await netrc.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isURLaPeerTubeInstance (url: string) {
|
|
||||||
return url.startsWith('http://') || url.startsWith('https://')
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripExtraneousFromPeerTubeUrl (url: string) {
|
|
||||||
// Get everything before the 3rd /.
|
|
||||||
const urlLength = url.includes('/', 8)
|
|
||||||
? url.indexOf('/', 8)
|
|
||||||
: url.length
|
|
||||||
|
|
||||||
return url.substring(0, urlLength)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { assignToken, buildServer } from './shared/index.js'
|
|
||||||
|
|
||||||
export function defineGetAccessProgram () {
|
|
||||||
const program = new Command()
|
|
||||||
.name('get-access-token')
|
|
||||||
.description('Get a peertube access token')
|
|
||||||
.alias('token')
|
|
||||||
|
|
||||||
program
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-n, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!options.url ||
|
|
||||||
!options.username ||
|
|
||||||
!options.password
|
|
||||||
) {
|
|
||||||
if (!options.url) console.error('--url field is required.')
|
|
||||||
if (!options.username) console.error('--username field is required.')
|
|
||||||
if (!options.password) console.error('--password field is required.')
|
|
||||||
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = buildServer(options.url)
|
|
||||||
await assignToken(server, options.username, options.password)
|
|
||||||
|
|
||||||
console.log(server.accessToken)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot get access token: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return program
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
import CliTable3 from 'cli-table3'
|
|
||||||
import { isAbsolute } from 'path'
|
|
||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { PluginType, PluginType_Type } from '@peertube/peertube-models'
|
|
||||||
import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js'
|
|
||||||
|
|
||||||
export function definePluginsProgram () {
|
|
||||||
const program = new Command()
|
|
||||||
|
|
||||||
program
|
|
||||||
.name('plugins')
|
|
||||||
.description('Manage instance plugins/themes')
|
|
||||||
.alias('p')
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('list')
|
|
||||||
.description('List installed plugins')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('-t, --only-themes', 'List themes only')
|
|
||||||
.option('-P, --only-plugins', 'List plugins only')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await pluginsListCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot list plugins: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('install')
|
|
||||||
.description('Install a plugin or a theme')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('-P --path <path>', 'Install from a path')
|
|
||||||
.option('-n, --npm-name <npmName>', 'Install from npm')
|
|
||||||
.option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await installPluginCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot install plugin: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('update')
|
|
||||||
.description('Update a plugin or a theme')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('-P --path <path>', 'Update from a path')
|
|
||||||
.option('-n, --npm-name <npmName>', 'Update from npm')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await updatePluginCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot update plugin: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('uninstall')
|
|
||||||
.description('Uninstall a plugin or a theme')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('-n, --npm-name <npmName>', 'NPM plugin/theme name')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await uninstallPluginCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot uninstall plugin: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return program
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function pluginsListCLI (options: CommonProgramOptions & { onlyThemes?: true, onlyPlugins?: true }) {
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
let pluginType: PluginType_Type
|
|
||||||
if (options.onlyThemes) pluginType = PluginType.THEME
|
|
||||||
if (options.onlyPlugins) pluginType = PluginType.PLUGIN
|
|
||||||
|
|
||||||
const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType })
|
|
||||||
|
|
||||||
const table = new CliTable3({
|
|
||||||
head: [ 'name', 'version', 'homepage' ],
|
|
||||||
colWidths: [ 50, 20, 50 ]
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
for (const plugin of data) {
|
|
||||||
const npmName = plugin.type === PluginType.PLUGIN
|
|
||||||
? 'peertube-plugin-' + plugin.name
|
|
||||||
: 'peertube-theme-' + plugin.name
|
|
||||||
|
|
||||||
table.push([
|
|
||||||
npmName,
|
|
||||||
plugin.version,
|
|
||||||
plugin.homepage
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(table.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installPluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string, pluginVersion?: string }) {
|
|
||||||
if (!options.path && !options.npmName) {
|
|
||||||
throw new Error('You need to specify the npm name or the path of the plugin you want to install.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.path && !isAbsolute(options.path)) {
|
|
||||||
throw new Error('Path should be absolute.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
|
|
||||||
|
|
||||||
console.log('Plugin installed.')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePluginCLI (options: CommonProgramOptions & { path?: string, npmName?: string }) {
|
|
||||||
if (!options.path && !options.npmName) {
|
|
||||||
throw new Error('You need to specify the npm name or the path of the plugin you want to update.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.path && !isAbsolute(options.path)) {
|
|
||||||
throw new Error('Path should be absolute.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
await server.plugins.update({ npmName: options.npmName, path: options.path })
|
|
||||||
|
|
||||||
console.log('Plugin updated.')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstallPluginCLI (options: CommonProgramOptions & { npmName?: string }) {
|
|
||||||
if (!options.npmName) {
|
|
||||||
throw new Error('You need to specify the npm name of the plugin/theme you want to uninstall.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
await server.plugins.uninstall({ npmName: options.npmName })
|
|
||||||
|
|
||||||
console.log('Plugin uninstalled.')
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
import bytes from 'bytes'
|
|
||||||
import CliTable3 from 'cli-table3'
|
|
||||||
import { URL } from 'url'
|
|
||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { forceNumber, uniqify } from '@peertube/peertube-core-utils'
|
|
||||||
import { HttpStatusCode, VideoRedundanciesTarget } from '@peertube/peertube-models'
|
|
||||||
import { assignToken, buildServer, CommonProgramOptions, getServerCredentials } from './shared/index.js'
|
|
||||||
|
|
||||||
export function defineRedundancyProgram () {
|
|
||||||
const program = new Command()
|
|
||||||
.name('redundancy')
|
|
||||||
.description('Manage instance redundancies')
|
|
||||||
.alias('r')
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('list-remote-redundancies')
|
|
||||||
.description('List remote redundancies on your videos')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await listRedundanciesCLI({ target: 'my-videos', ...options })
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot list remote redundancies: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('list-my-redundancies')
|
|
||||||
.description('List your redundancies of remote videos')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await listRedundanciesCLI({ target: 'remote-videos', ...options })
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot list redundancies: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('add')
|
|
||||||
.description('Duplicate a video in your redundancy system')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.requiredOption('-v, --video <videoId>', 'Video id to duplicate', parseInt)
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await addRedundancyCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot duplicate video: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('remove')
|
|
||||||
.description('Remove a video from your redundancies')
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.requiredOption('-v, --video <videoId>', 'Video id to remove from redundancies', parseInt)
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
await removeRedundancyCLI(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot remove redundancy: ' + err)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return program
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function listRedundanciesCLI (options: CommonProgramOptions & { target: VideoRedundanciesTarget }) {
|
|
||||||
const { target } = options
|
|
||||||
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
const { data } = await server.redundancy.listVideos({ start: 0, count: 100, sort: 'name', target })
|
|
||||||
|
|
||||||
const table = new CliTable3({
|
|
||||||
head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
for (const redundancy of data) {
|
|
||||||
const webVideoFiles = redundancy.redundancies.files
|
|
||||||
const streamingPlaylists = redundancy.redundancies.streamingPlaylists
|
|
||||||
|
|
||||||
let totalSize = ''
|
|
||||||
if (target === 'remote-videos') {
|
|
||||||
const tmp = webVideoFiles.concat(streamingPlaylists)
|
|
||||||
.reduce((a, b) => a + b.size, 0)
|
|
||||||
|
|
||||||
// FIXME: don't use external dependency to stringify bytes: we already have the functions in the client
|
|
||||||
totalSize = bytes(tmp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const instances = uniqify(
|
|
||||||
webVideoFiles.concat(streamingPlaylists)
|
|
||||||
.map(r => r.fileUrl)
|
|
||||||
.map(u => new URL(u).host)
|
|
||||||
)
|
|
||||||
|
|
||||||
table.push([
|
|
||||||
redundancy.id.toString(),
|
|
||||||
redundancy.name,
|
|
||||||
redundancy.url,
|
|
||||||
webVideoFiles.length,
|
|
||||||
streamingPlaylists.length,
|
|
||||||
instances.join('\n'),
|
|
||||||
totalSize
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(table.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addRedundancyCLI (options: { video: number } & CommonProgramOptions) {
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
if (!options.video || isNaN(options.video)) {
|
|
||||||
throw new Error('You need to specify the video id to duplicate and it should be a number.')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await server.redundancy.addVideo({ videoId: options.video })
|
|
||||||
|
|
||||||
console.log('Video will be duplicated by your instance!')
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message.includes(HttpStatusCode.CONFLICT_409)) {
|
|
||||||
throw new Error('This video is already duplicated by your instance.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.message.includes(HttpStatusCode.NOT_FOUND_404)) {
|
|
||||||
throw new Error('This video id does not exist.')
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeRedundancyCLI (options: CommonProgramOptions & { video: number }) {
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
if (!options.video || isNaN(options.video)) {
|
|
||||||
throw new Error('You need to specify the video id to remove from your redundancies')
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoId = forceNumber(options.video)
|
|
||||||
|
|
||||||
const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' })
|
|
||||||
let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id)
|
|
||||||
|
|
||||||
if (!videoRedundancy) {
|
|
||||||
const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' })
|
|
||||||
videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!videoRedundancy) {
|
|
||||||
throw new Error('Video redundancy not found.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = videoRedundancy.redundancies.files
|
|
||||||
.concat(videoRedundancy.redundancies.streamingPlaylists)
|
|
||||||
.map(r => r.id)
|
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
await server.redundancy.removeVideo({ redundancyId: id })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Video redundancy removed!')
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
import { access, constants } from 'fs/promises'
|
|
||||||
import { isAbsolute } from 'path'
|
|
||||||
import { inspect } from 'util'
|
|
||||||
import { Command } from '@commander-js/extra-typings'
|
|
||||||
import { VideoPrivacy } from '@peertube/peertube-models'
|
|
||||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
|
||||||
import { assignToken, buildServer, getServerCredentials, listOptions } from './shared/index.js'
|
|
||||||
|
|
||||||
type UploadOptions = {
|
|
||||||
url?: string
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
thumbnail?: string
|
|
||||||
preview?: string
|
|
||||||
file?: string
|
|
||||||
videoName?: string
|
|
||||||
category?: string
|
|
||||||
licence?: string
|
|
||||||
language?: string
|
|
||||||
tags?: string
|
|
||||||
nsfw?: true
|
|
||||||
videoDescription?: string
|
|
||||||
privacy?: number
|
|
||||||
channelName?: string
|
|
||||||
noCommentsEnabled?: true
|
|
||||||
support?: string
|
|
||||||
noWaitTranscoding?: true
|
|
||||||
noDownloadEnabled?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defineUploadProgram () {
|
|
||||||
const program = new Command('upload')
|
|
||||||
.description('Upload a video on a PeerTube instance')
|
|
||||||
.alias('up')
|
|
||||||
|
|
||||||
program
|
|
||||||
.option('-u, --url <url>', 'Server url')
|
|
||||||
.option('-U, --username <username>', 'Username')
|
|
||||||
.option('-p, --password <token>', 'Password')
|
|
||||||
.option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
|
|
||||||
.option('-v, --preview <previewPath>', 'Preview path')
|
|
||||||
.option('-f, --file <file>', 'Video absolute file path')
|
|
||||||
.option('-n, --video-name <name>', 'Video name')
|
|
||||||
.option('-c, --category <category_number>', 'Category number')
|
|
||||||
.option('-l, --licence <licence_number>', 'Licence number')
|
|
||||||
.option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
|
|
||||||
.option('-t, --tags <tags>', 'Video tags', listOptions)
|
|
||||||
.option('-N, --nsfw', 'Video is Not Safe For Work')
|
|
||||||
.option('-d, --video-description <description>', 'Video description')
|
|
||||||
.option('-P, --privacy <privacy_number>', 'Privacy', parseInt)
|
|
||||||
.option('-C, --channel-name <channel_name>', 'Channel name')
|
|
||||||
.option('--no-comments-enabled', 'Disable video comments')
|
|
||||||
.option('-s, --support <support>', 'Video support text')
|
|
||||||
.option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video')
|
|
||||||
.option('--no-download-enabled', 'Disable video download')
|
|
||||||
.option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
|
|
||||||
.action(async options => {
|
|
||||||
try {
|
|
||||||
const { url, username, password } = await getServerCredentials(options)
|
|
||||||
|
|
||||||
if (!options.videoName || !options.file) {
|
|
||||||
if (!options.videoName) console.error('--video-name is required.')
|
|
||||||
if (!options.file) console.error('--file is required.')
|
|
||||||
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAbsolute(options.file) === false) {
|
|
||||||
console.error('File path should be absolute.')
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
await run({ ...options, url, username, password })
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Cannot upload video: ' + err.message)
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return program
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Private
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function run (options: UploadOptions) {
|
|
||||||
const { url, username, password } = options
|
|
||||||
|
|
||||||
const server = buildServer(url)
|
|
||||||
await assignToken(server, username, password)
|
|
||||||
|
|
||||||
await access(options.file, constants.F_OK)
|
|
||||||
|
|
||||||
console.log('Uploading %s video...', options.videoName)
|
|
||||||
|
|
||||||
const baseAttributes = await buildVideoAttributesFromCommander(server, options)
|
|
||||||
|
|
||||||
const attributes = {
|
|
||||||
...baseAttributes,
|
|
||||||
|
|
||||||
fixture: options.file,
|
|
||||||
thumbnailfile: options.thumbnail,
|
|
||||||
previewfile: options.preview
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await server.videos.upload({ attributes })
|
|
||||||
console.log(`Video ${options.videoName} uploaded.`)
|
|
||||||
process.exit(0)
|
|
||||||
} catch (err) {
|
|
||||||
const message = err.message || ''
|
|
||||||
if (message.includes('413')) {
|
|
||||||
console.error('Aborted: user quota is exceeded or video file is too big for this PeerTube instance.')
|
|
||||||
} else {
|
|
||||||
console.error(inspect(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildVideoAttributesFromCommander (server: PeerTubeServer, options: UploadOptions, defaultAttributes: any = {}) {
|
|
||||||
const defaultBooleanAttributes = {
|
|
||||||
nsfw: false,
|
|
||||||
commentsEnabled: true,
|
|
||||||
downloadEnabled: true,
|
|
||||||
waitTranscoding: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
|
|
||||||
|
|
||||||
for (const key of Object.keys(defaultBooleanAttributes)) {
|
|
||||||
if (options[key] !== undefined) {
|
|
||||||
booleanAttributes[key] = options[key]
|
|
||||||
} else if (defaultAttributes[key] !== undefined) {
|
|
||||||
booleanAttributes[key] = defaultAttributes[key]
|
|
||||||
} else {
|
|
||||||
booleanAttributes[key] = defaultBooleanAttributes[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoAttributes = {
|
|
||||||
name: options.videoName || defaultAttributes.name,
|
|
||||||
category: options.category || defaultAttributes.category || undefined,
|
|
||||||
licence: options.licence || defaultAttributes.licence || undefined,
|
|
||||||
language: options.language || defaultAttributes.language || undefined,
|
|
||||||
privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC,
|
|
||||||
support: options.support || defaultAttributes.support || undefined,
|
|
||||||
description: options.videoDescription || defaultAttributes.description || undefined,
|
|
||||||
tags: options.tags || defaultAttributes.tags || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(videoAttributes, booleanAttributes)
|
|
||||||
|
|
||||||
if (options.channelName) {
|
|
||||||
const videoChannel = await server.channels.get({ channelName: options.channelName })
|
|
||||||
|
|
||||||
Object.assign(videoAttributes, { channelId: videoChannel.id })
|
|
||||||
|
|
||||||
if (!videoAttributes.support && videoChannel.support) {
|
|
||||||
Object.assign(videoAttributes, { support: videoChannel.support })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return videoAttributes
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './cli.js'
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "./",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "../../packages/core-utils" },
|
|
||||||
{ "path": "../../packages/models" },
|
|
||||||
{ "path": "../../packages/node-utils" },
|
|
||||||
{ "path": "../../packages/server-commands" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
src
|
|
||||||
meta.json
|
|
||||||
tsconfig.json
|
|
||||||
scripts
|
|
|
@ -1,43 +0,0 @@
|
||||||
# PeerTube runner
|
|
||||||
|
|
||||||
Runner program to execute jobs (transcoding...) of remote PeerTube instances.
|
|
||||||
|
|
||||||
Commands below has to be run at the root of PeerTube git repository.
|
|
||||||
|
|
||||||
## Dev
|
|
||||||
|
|
||||||
### Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
yarn install --pure-lockfile
|
|
||||||
cd apps/peertube-runner && yarn install --pure-lockfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### Develop
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
npm run dev:peertube-runner
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
npm run build:peertube-runner
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
node apps/peertube-runner/dist/peertube-runner.js --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### Publish on NPM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd peertube-root
|
|
||||||
(cd apps/peertube-runner && npm version patch) && npm run build:peertube-runner && (cd apps/peertube-runner && npm publish --access=public)
|
|
||||||
```
|
|
|
@ -1,27 +0,0 @@
|
||||||
import * as esbuild from 'esbuild'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
|
|
||||||
const packageJSON = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
|
|
||||||
|
|
||||||
export const esbuildOptions = {
|
|
||||||
entryPoints: [ './src/peertube-runner.ts' ],
|
|
||||||
bundle: true,
|
|
||||||
platform: 'node',
|
|
||||||
format: 'esm',
|
|
||||||
target: 'node16',
|
|
||||||
external: [
|
|
||||||
'./lib-cov/fluent-ffmpeg',
|
|
||||||
'pg-hstore'
|
|
||||||
],
|
|
||||||
outfile: './dist/peertube-runner.js',
|
|
||||||
banner: {
|
|
||||||
js: `const require = (await import("node:module")).createRequire(import.meta.url);` +
|
|
||||||
`const __filename = (await import("node:url")).fileURLToPath(import.meta.url);` +
|
|
||||||
`const __dirname = (await import("node:path")).dirname(__filename);`
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
'process.env.PACKAGE_VERSION': `'${packageJSON.version}'`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await esbuild.build(esbuildOptions)
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './register.js'
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './server.js'
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './shared/index.js'
|
|
||||||
export * from './process.js'
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './common.js'
|
|
||||||
export * from './process-vod.js'
|
|
||||||
export * from './transcoding-logger.js'
|
|
|
@ -1,201 +0,0 @@
|
||||||
import { remove } from 'fs-extra/esm'
|
|
||||||
import { join } from 'path'
|
|
||||||
import {
|
|
||||||
RunnerJobVODAudioMergeTranscodingPayload,
|
|
||||||
RunnerJobVODHLSTranscodingPayload,
|
|
||||||
RunnerJobVODWebVideoTranscodingPayload,
|
|
||||||
VODAudioMergeTranscodingSuccess,
|
|
||||||
VODHLSTranscodingSuccess,
|
|
||||||
VODWebVideoTranscodingSuccess
|
|
||||||
} from '@peertube/peertube-models'
|
|
||||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
|
||||||
import { ConfigManager } from '../../../shared/config-manager.js'
|
|
||||||
import { logger } from '../../../shared/index.js'
|
|
||||||
import { buildFFmpegVOD, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common.js'
|
|
||||||
|
|
||||||
export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) {
|
|
||||||
const { server, job, runnerToken } = options
|
|
||||||
|
|
||||||
const payload = job.payload
|
|
||||||
|
|
||||||
let ffmpegProgress: number
|
|
||||||
let inputPath: string
|
|
||||||
|
|
||||||
const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
|
|
||||||
|
|
||||||
const updateProgressInterval = scheduleTranscodingProgress({
|
|
||||||
job,
|
|
||||||
server,
|
|
||||||
runnerToken,
|
|
||||||
progressGetter: () => ffmpegProgress
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(`Downloading input file ${payload.input.videoFileUrl} for web video transcoding job ${job.jobToken}`)
|
|
||||||
|
|
||||||
inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
|
||||||
|
|
||||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running web video transcoding.`)
|
|
||||||
|
|
||||||
const ffmpegVod = buildFFmpegVOD({
|
|
||||||
onJobProgress: progress => { ffmpegProgress = progress }
|
|
||||||
})
|
|
||||||
|
|
||||||
await ffmpegVod.transcode({
|
|
||||||
type: 'video',
|
|
||||||
|
|
||||||
inputPath,
|
|
||||||
|
|
||||||
outputPath,
|
|
||||||
|
|
||||||
inputFileMutexReleaser: () => {},
|
|
||||||
|
|
||||||
resolution: payload.output.resolution,
|
|
||||||
fps: payload.output.fps
|
|
||||||
})
|
|
||||||
|
|
||||||
const successBody: VODWebVideoTranscodingSuccess = {
|
|
||||||
videoFile: outputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.runnerJobs.success({
|
|
||||||
jobToken: job.jobToken,
|
|
||||||
jobUUID: job.uuid,
|
|
||||||
runnerToken,
|
|
||||||
payload: successBody
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
if (inputPath) await remove(inputPath)
|
|
||||||
if (outputPath) await remove(outputPath)
|
|
||||||
if (updateProgressInterval) clearInterval(updateProgressInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVODHLSTranscodingPayload>) {
|
|
||||||
const { server, job, runnerToken } = options
|
|
||||||
const payload = job.payload
|
|
||||||
|
|
||||||
let ffmpegProgress: number
|
|
||||||
let inputPath: string
|
|
||||||
|
|
||||||
const uuid = buildUUID()
|
|
||||||
const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `${uuid}-${payload.output.resolution}.m3u8`)
|
|
||||||
const videoFilename = `${uuid}-${payload.output.resolution}-fragmented.mp4`
|
|
||||||
const videoPath = join(join(ConfigManager.Instance.getTranscodingDirectory(), videoFilename))
|
|
||||||
|
|
||||||
const updateProgressInterval = scheduleTranscodingProgress({
|
|
||||||
job,
|
|
||||||
server,
|
|
||||||
runnerToken,
|
|
||||||
progressGetter: () => ffmpegProgress
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(`Downloading input file ${payload.input.videoFileUrl} for HLS transcoding job ${job.jobToken}`)
|
|
||||||
|
|
||||||
inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
|
||||||
|
|
||||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running HLS transcoding.`)
|
|
||||||
|
|
||||||
const ffmpegVod = buildFFmpegVOD({
|
|
||||||
onJobProgress: progress => { ffmpegProgress = progress }
|
|
||||||
})
|
|
||||||
|
|
||||||
await ffmpegVod.transcode({
|
|
||||||
type: 'hls',
|
|
||||||
copyCodecs: false,
|
|
||||||
inputPath,
|
|
||||||
hlsPlaylist: { videoFilename },
|
|
||||||
outputPath,
|
|
||||||
|
|
||||||
inputFileMutexReleaser: () => {},
|
|
||||||
|
|
||||||
resolution: payload.output.resolution,
|
|
||||||
fps: payload.output.fps
|
|
||||||
})
|
|
||||||
|
|
||||||
const successBody: VODHLSTranscodingSuccess = {
|
|
||||||
resolutionPlaylistFile: outputPath,
|
|
||||||
videoFile: videoPath
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.runnerJobs.success({
|
|
||||||
jobToken: job.jobToken,
|
|
||||||
jobUUID: job.uuid,
|
|
||||||
runnerToken,
|
|
||||||
payload: successBody
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
if (inputPath) await remove(inputPath)
|
|
||||||
if (outputPath) await remove(outputPath)
|
|
||||||
if (videoPath) await remove(videoPath)
|
|
||||||
if (updateProgressInterval) clearInterval(updateProgressInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processAudioMergeTranscoding (options: ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>) {
|
|
||||||
const { server, job, runnerToken } = options
|
|
||||||
const payload = job.payload
|
|
||||||
|
|
||||||
let ffmpegProgress: number
|
|
||||||
let audioPath: string
|
|
||||||
let inputPath: string
|
|
||||||
|
|
||||||
const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
|
|
||||||
|
|
||||||
const updateProgressInterval = scheduleTranscodingProgress({
|
|
||||||
job,
|
|
||||||
server,
|
|
||||||
runnerToken,
|
|
||||||
progressGetter: () => ffmpegProgress
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(
|
|
||||||
`Downloading input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
|
|
||||||
`for audio merge transcoding job ${job.jobToken}`
|
|
||||||
)
|
|
||||||
|
|
||||||
audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
|
|
||||||
inputPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job })
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
|
|
||||||
`for job ${job.jobToken}. Running audio merge transcoding.`
|
|
||||||
)
|
|
||||||
|
|
||||||
const ffmpegVod = buildFFmpegVOD({
|
|
||||||
onJobProgress: progress => { ffmpegProgress = progress }
|
|
||||||
})
|
|
||||||
|
|
||||||
await ffmpegVod.transcode({
|
|
||||||
type: 'merge-audio',
|
|
||||||
|
|
||||||
audioPath,
|
|
||||||
inputPath,
|
|
||||||
|
|
||||||
outputPath,
|
|
||||||
|
|
||||||
inputFileMutexReleaser: () => {},
|
|
||||||
|
|
||||||
resolution: payload.output.resolution,
|
|
||||||
fps: payload.output.fps
|
|
||||||
})
|
|
||||||
|
|
||||||
const successBody: VODAudioMergeTranscodingSuccess = {
|
|
||||||
videoFile: outputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.runnerJobs.success({
|
|
||||||
jobToken: job.jobToken,
|
|
||||||
jobUUID: job.uuid,
|
|
||||||
runnerToken,
|
|
||||||
payload: successBody
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
if (audioPath) await remove(audioPath)
|
|
||||||
if (inputPath) await remove(inputPath)
|
|
||||||
if (outputPath) await remove(outputPath)
|
|
||||||
if (updateProgressInterval) clearInterval(updateProgressInterval)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './supported-job.js'
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './config-manager.js'
|
|
||||||
export * from './http.js'
|
|
||||||
export * from './logger.js'
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './ipc-client.js'
|
|
||||||
export * from './ipc-server.js'
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './ipc-request.model.js'
|
|
||||||
export * from './ipc-response.model.js'
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "./",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "../../packages/core-utils" },
|
|
||||||
{ "path": "../../packages/ffmpeg" },
|
|
||||||
{ "path": "../../packages/models" },
|
|
||||||
{ "path": "../../packages/node-utils" },
|
|
||||||
{ "path": "../../packages/server-commands" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"projects/**/*",
|
"projects/**/*",
|
||||||
"node_modules/",
|
"node_modules/",
|
||||||
"src/standalone/embed-player-api/dist"
|
"src/standalone/player/dist"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,6 @@
|
||||||
"project": [
|
"project": [
|
||||||
"tsconfig.eslint.json"
|
"tsconfig.eslint.json"
|
||||||
],
|
],
|
||||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true,
|
|
||||||
"createDefaultProgram": false
|
"createDefaultProgram": false
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|
4
client/.gitignore
vendored
4
client/.gitignore
vendored
|
@ -12,5 +12,5 @@
|
||||||
/e2e/local.log
|
/e2e/local.log
|
||||||
/e2e/browserstack.err
|
/e2e/browserstack.err
|
||||||
/e2e/screenshots
|
/e2e/screenshots
|
||||||
/src/standalone/embed-player-api/build
|
/src/standalone/player/build
|
||||||
/src/standalone/embed-player-api/dist
|
/src/standalone/player/dist
|
||||||
|
|
|
@ -195,14 +195,11 @@
|
||||||
"path-browserify",
|
"path-browserify",
|
||||||
"deep-merge",
|
"deep-merge",
|
||||||
"escape-string-regexp",
|
"escape-string-regexp",
|
||||||
|
"mousetrap",
|
||||||
"is-plain-object",
|
"is-plain-object",
|
||||||
"parse-srcset",
|
"parse-srcset",
|
||||||
"deepmerge",
|
"deepmerge",
|
||||||
"core-js/features/reflect",
|
"core-js/features/reflect"
|
||||||
"@formatjs/intl-locale/polyfill",
|
|
||||||
"@formatjs/intl-locale/should-polyfill",
|
|
||||||
"@formatjs/intl-pluralrules/polyfill-force",
|
|
||||||
"@formatjs/intl-pluralrules/should-polyfill"
|
|
||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { browserSleep, go, isAndroid } from '../utils'
|
import { go } from '../utils'
|
||||||
|
|
||||||
export class LoginPage {
|
export class LoginPage {
|
||||||
|
|
||||||
|
@ -23,20 +23,12 @@ export class LoginPage {
|
||||||
await $('input#username').setValue(username)
|
await $('input#username').setValue(username)
|
||||||
await $('input#password').setValue(password)
|
await $('input#password').setValue(password)
|
||||||
|
|
||||||
await browserSleep(1000)
|
await browser.pause(1000)
|
||||||
|
|
||||||
const submit = $('.login-form-and-externals > form input[type=submit]')
|
await $('form input[type=submit]').click()
|
||||||
await submit.click()
|
|
||||||
|
|
||||||
// Have to do this on Android, don't really know why
|
|
||||||
// I think we need to "escape" from the password input, so click twice on the submit button
|
|
||||||
if (isAndroid()) {
|
|
||||||
await browserSleep(2000)
|
|
||||||
await submit.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMobileDevice) {
|
if (this.isMobileDevice) {
|
||||||
const menuToggle = $('.top-left-block button')
|
const menuToggle = $('.top-left-block span[role=button]')
|
||||||
|
|
||||||
await $('h2=Our content selection').waitForDisplayed()
|
await $('h2=Our content selection').waitForDisplayed()
|
||||||
|
|
||||||
|
@ -87,7 +79,7 @@ export class LoginPage {
|
||||||
await logout.click()
|
await logout.click()
|
||||||
|
|
||||||
await browser.waitUntil(() => {
|
await browser.waitUntil(() => {
|
||||||
return $$('.login-buttons-block, my-error-page a[href="/login"]').some(e => e.isDisplayed())
|
return $('.login-buttons-block, my-error-page a[href="/login"]').isDisplayed()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getCheckbox, go, selectCustomSelect } from '../utils'
|
import { getCheckbox, go } from '../utils'
|
||||||
|
|
||||||
export class MyAccountPage {
|
export class MyAccountPage {
|
||||||
|
|
||||||
|
@ -117,26 +117,6 @@ export class MyAccountPage {
|
||||||
return go(url)
|
return go(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePlaylistPrivacy (playlistUUID: string, privacy: 'Public' | 'Private' | 'Unlisted') {
|
|
||||||
go('/my-library/video-playlists/update/' + playlistUUID)
|
|
||||||
|
|
||||||
await browser.waitUntil(async () => {
|
|
||||||
return (await $('form .video-playlist-title').getText() === 'PLAYLIST')
|
|
||||||
})
|
|
||||||
|
|
||||||
await selectCustomSelect('videoChannelId', 'Main root channel')
|
|
||||||
await selectCustomSelect('privacy', privacy)
|
|
||||||
|
|
||||||
const submit = await $('form input[type=submit]')
|
|
||||||
await submit.waitForClickable()
|
|
||||||
await submit.scrollIntoView()
|
|
||||||
await submit.click()
|
|
||||||
|
|
||||||
return browser.waitUntil(async () => {
|
|
||||||
return (await browser.getUrl()).includes('my-library/video-playlists')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// My account Videos
|
// My account Videos
|
||||||
|
|
||||||
private async getVideoElement (name: string) {
|
private async getVideoElement (name: string) {
|
||||||
|
|
|
@ -29,34 +29,29 @@ export class PlayerPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async playAndPauseVideo (isAutoplay: boolean, waitUntilSec: number) {
|
async playAndPauseVideo (isAutoplay: boolean, waitUntilSec: number) {
|
||||||
// Autoplay is disabled on mobile and Safari
|
const videojsElem = () => $('div.video-js')
|
||||||
if (isIOS() || isSafari() || isMobileDevice() || isAutoplay === false) {
|
|
||||||
await this.playVideo()
|
|
||||||
}
|
|
||||||
|
|
||||||
await $('div.video-js.vjs-has-started').waitForExist()
|
await videojsElem().waitForExist()
|
||||||
|
|
||||||
await browserSleep(2000)
|
|
||||||
|
|
||||||
await browser.waitUntil(async () => {
|
|
||||||
return (await this.getWatchVideoPlayerCurrentTime()) >= waitUntilSec
|
|
||||||
}, { timeout: Math.max(waitUntilSec * 2 * 1000, 30000) })
|
|
||||||
|
|
||||||
// Pause video
|
|
||||||
await $('div.video-js').click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async playVideo () {
|
|
||||||
await $('div.video-js.vjs-paused, div.video-js.vjs-playing').waitForExist()
|
|
||||||
|
|
||||||
if (await $('div.video-js.vjs-playing').isExisting()) return
|
|
||||||
|
|
||||||
// Autoplay is disabled on iOS and Safari
|
// Autoplay is disabled on iOS and Safari
|
||||||
if (isIOS() || isSafari() || isMobileDevice()) {
|
if (isIOS() || isSafari() || isMobileDevice()) {
|
||||||
// We can't play the video if it is not muted
|
// We can't play the video if it is not muted
|
||||||
await browser.execute(`document.querySelector('video').muted = true`)
|
await browser.execute(`document.querySelector('video').muted = true`)
|
||||||
|
await this.clickOnPlayButton()
|
||||||
|
} else if (isAutoplay === false) {
|
||||||
|
await this.clickOnPlayButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await browserSleep(2000)
|
||||||
|
|
||||||
|
await browser.waitUntil(async () => {
|
||||||
|
return (await this.getWatchVideoPlayerCurrentTime()) >= waitUntilSec
|
||||||
|
})
|
||||||
|
|
||||||
|
await videojsElem().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async playVideo () {
|
||||||
return this.clickOnPlayButton()
|
return this.clickOnPlayButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +61,4 @@ export class PlayerPage {
|
||||||
await playButton().waitForClickable()
|
await playButton().waitForClickable()
|
||||||
await playButton().click()
|
await playButton().click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillEmbedVideoPassword (videoPassword: string) {
|
|
||||||
const videoPasswordInput = $('input#video-password-input')
|
|
||||||
const confirmButton = await $('button#video-password-submit')
|
|
||||||
|
|
||||||
await videoPasswordInput.clearValue()
|
|
||||||
await videoPasswordInput.setValue(videoPassword)
|
|
||||||
await confirmButton.waitForClickable()
|
|
||||||
|
|
||||||
return confirmButton.click()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,26 +62,4 @@ export class SignupPage {
|
||||||
await $('#displayName').setValue(options.displayName || `${options.name} channel display name`)
|
await $('#displayName').setValue(options.displayName || `${options.name} channel display name`)
|
||||||
await $('#name').setValue(options.name)
|
await $('#name').setValue(options.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fullSignup ({ accountInfo, channelInfo }: {
|
|
||||||
accountInfo: {
|
|
||||||
username: string
|
|
||||||
password?: string
|
|
||||||
displayName?: string
|
|
||||||
email?: string
|
|
||||||
}
|
|
||||||
channelInfo: {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
await this.clickOnRegisterInMenu()
|
|
||||||
await this.validateStep()
|
|
||||||
await this.checkTerms()
|
|
||||||
await this.validateStep()
|
|
||||||
await this.fillAccountStep(accountInfo)
|
|
||||||
await this.validateStep()
|
|
||||||
await this.fillChannelStep(channelInfo)
|
|
||||||
await this.validateStep()
|
|
||||||
await this.getEndMessage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ export class VideoSearchPage {
|
||||||
|
|
||||||
async search (search: string) {
|
async search (search: string) {
|
||||||
await $('#search-video').setValue(search)
|
await $('#search-video').setValue(search)
|
||||||
await $('.search-button').click()
|
await $('my-header .icon-search').click()
|
||||||
|
|
||||||
await browser.waitUntil(() => {
|
await browser.waitUntil(() => {
|
||||||
return $('my-video-miniature').isDisplayed()
|
return $('my-video-miniature').isDisplayed()
|
||||||
|
|
|
@ -64,16 +64,6 @@ export class VideoUploadPage {
|
||||||
return selectCustomSelect('privacy', 'Private')
|
return selectCustomSelect('privacy', 'Private')
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAsPasswordProtected (videoPassword: string) {
|
|
||||||
selectCustomSelect('privacy', 'Password protected')
|
|
||||||
|
|
||||||
const videoPasswordInput = $('input#videoPassword')
|
|
||||||
await videoPasswordInput.waitForClickable()
|
|
||||||
await videoPasswordInput.clearValue()
|
|
||||||
|
|
||||||
return videoPasswordInput.setValue(videoPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSecondStepSubmitButton () {
|
private getSecondStepSubmitButton () {
|
||||||
return $('.submit-container my-button')
|
return $('.submit-container my-button')
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,11 @@ export class VideoWatchPage {
|
||||||
waitWatchVideoName (videoName: string) {
|
waitWatchVideoName (videoName: string) {
|
||||||
if (this.isSafari) return browserSleep(5000)
|
if (this.isSafari) return browserSleep(5000)
|
||||||
|
|
||||||
// On mobile we display the first node, on desktop the second one
|
// On mobile we display the first node, on desktop the second
|
||||||
const index = this.isMobileDevice ? 0 : 1
|
const index = this.isMobileDevice ? 0 : 1
|
||||||
|
|
||||||
return browser.waitUntil(async () => {
|
return browser.waitUntil(async () => {
|
||||||
return await $('.video-info .video-info-name').isExisting() &&
|
return (await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
|
||||||
(await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,25 +43,19 @@ export class VideoWatchPage {
|
||||||
return $('my-privacy-concerns').isDisplayed()
|
return $('my-privacy-concerns').isDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
async goOnAssociatedEmbed (passwordProtected = false) {
|
async goOnAssociatedEmbed () {
|
||||||
let url = await browser.getUrl()
|
let url = await browser.getUrl()
|
||||||
url = url.replace('/w/', '/videos/embed/')
|
url = url.replace('/w/', '/videos/embed/')
|
||||||
url = url.replace(':3333', ':9001')
|
url = url.replace(':3333', ':9001')
|
||||||
|
|
||||||
await go(url)
|
await go(url)
|
||||||
|
await this.waitEmbedForDisplayed()
|
||||||
if (passwordProtected) await this.waitEmbedForVideoPasswordForm()
|
|
||||||
else await this.waitEmbedForDisplayed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitEmbedForDisplayed () {
|
waitEmbedForDisplayed () {
|
||||||
return $('.vjs-big-play-button').waitForDisplayed()
|
return $('.vjs-big-play-button').waitForDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
waitEmbedForVideoPasswordForm () {
|
|
||||||
return $('#video-password-input').waitForDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmbedWarningDisplayed () {
|
isEmbedWarningDisplayed () {
|
||||||
return $('.peertube-dock-description').isDisplayed()
|
return $('.peertube-dock-description').isDisplayed()
|
||||||
}
|
}
|
||||||
|
@ -145,78 +138,4 @@ export class VideoWatchPage {
|
||||||
|
|
||||||
return elem()
|
return elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
isPasswordProtected () {
|
|
||||||
return $('#confirmInput').isExisting()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillVideoPassword (videoPassword: string) {
|
|
||||||
const videoPasswordInput = await $('input#confirmInput')
|
|
||||||
await videoPasswordInput.waitForClickable()
|
|
||||||
await videoPasswordInput.clearValue()
|
|
||||||
await videoPasswordInput.setValue(videoPassword)
|
|
||||||
|
|
||||||
const confirmButton = await $('input[value="Confirm"]')
|
|
||||||
await confirmButton.waitForClickable()
|
|
||||||
return confirmButton.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async like () {
|
|
||||||
const likeButton = await $('.action-button-like')
|
|
||||||
const isActivated = (await likeButton.getAttribute('class')).includes('activated')
|
|
||||||
|
|
||||||
let count: number
|
|
||||||
try {
|
|
||||||
count = parseInt(await $('.action-button-like > .count').getText())
|
|
||||||
} catch (error) {
|
|
||||||
count = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
await likeButton.waitForClickable()
|
|
||||||
await likeButton.click()
|
|
||||||
|
|
||||||
if (isActivated) {
|
|
||||||
if (count === 1) {
|
|
||||||
return expect(!await $('.action-button-like > .count').isExisting())
|
|
||||||
} else {
|
|
||||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count - 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createThread (comment: string) {
|
|
||||||
const textarea = await $('my-video-comment-add textarea')
|
|
||||||
await textarea.waitForClickable()
|
|
||||||
|
|
||||||
await textarea.setValue(comment)
|
|
||||||
|
|
||||||
const confirmButton = await $('.comment-buttons .orange-button')
|
|
||||||
await confirmButton.waitForClickable()
|
|
||||||
await confirmButton.click()
|
|
||||||
|
|
||||||
const createdComment = await (await $('.comment-html p')).getText()
|
|
||||||
|
|
||||||
return expect(createdComment).toBe(comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createReply (comment: string) {
|
|
||||||
const replyButton = await $('button.comment-action-reply')
|
|
||||||
await replyButton.waitForClickable()
|
|
||||||
await replyButton.scrollIntoView()
|
|
||||||
await replyButton.click()
|
|
||||||
|
|
||||||
const textarea = await $('my-video-comment my-video-comment-add textarea')
|
|
||||||
await textarea.waitForClickable()
|
|
||||||
await textarea.setValue(comment)
|
|
||||||
|
|
||||||
const confirmButton = await $('my-video-comment .comment-buttons .orange-button')
|
|
||||||
await confirmButton.waitForClickable()
|
|
||||||
await confirmButton.click()
|
|
||||||
|
|
||||||
const createdComment = await (await $('.is-child .comment-html p')).getText()
|
|
||||||
|
|
||||||
return expect(createdComment).toBe(comment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ describe('Private videos all workflow', () => {
|
||||||
return loginPage.loginOnPeerTube2()
|
return loginPage.loginOnPeerTube2()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should play an internal web video', async () => {
|
it('Should play an internal webtorrent video', async () => {
|
||||||
await go(FIXTURE_URLS.INTERNAL_WEB_VIDEO)
|
await go(FIXTURE_URLS.INTERNAL_WEBTORRENT_VIDEO)
|
||||||
|
|
||||||
await videoWatchPage.waitWatchVideoName(internalVideoName)
|
await videoWatchPage.waitWatchVideoName(internalVideoName)
|
||||||
await checkCorrectlyPlay(playerPage)
|
await checkCorrectlyPlay(playerPage)
|
||||||
|
@ -52,8 +52,8 @@ describe('Private videos all workflow', () => {
|
||||||
await checkCorrectlyPlay(playerPage)
|
await checkCorrectlyPlay(playerPage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should play an internal Web Video in embed', async () => {
|
it('Should play an internal WebTorrent video in embed', async () => {
|
||||||
await go(FIXTURE_URLS.INTERNAL_EMBED_WEB_VIDEO)
|
await go(FIXTURE_URLS.INTERNAL_EMBED_WEBTORRENT_VIDEO)
|
||||||
|
|
||||||
await videoWatchPage.waitEmbedForDisplayed()
|
await videoWatchPage.waitEmbedForDisplayed()
|
||||||
await checkCorrectlyPlay(playerPage)
|
await checkCorrectlyPlay(playerPage)
|
||||||
|
|
|
@ -89,7 +89,7 @@ describe('Videos all workflow', () => {
|
||||||
let videoNameToExcept = videoName
|
let videoNameToExcept = videoName
|
||||||
|
|
||||||
if (isMobileDevice() || isSafari()) {
|
if (isMobileDevice() || isSafari()) {
|
||||||
await go(FIXTURE_URLS.WEB_VIDEO)
|
await go(FIXTURE_URLS.WEBTORRENT_VIDEO)
|
||||||
videoNameToExcept = 'E2E tests'
|
videoNameToExcept = 'E2E tests'
|
||||||
} else {
|
} else {
|
||||||
await videoListPage.clickOnVideo(videoName)
|
await videoListPage.clickOnVideo(videoName)
|
||||||
|
@ -176,7 +176,7 @@ describe('Videos all workflow', () => {
|
||||||
await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000)
|
await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should watch the WEB VIDEO playlist in the embed', async () => {
|
it('Should watch the webtorrent playlist in the embed', async () => {
|
||||||
if (isUploadUnsupported()) return
|
if (isUploadUnsupported()) return
|
||||||
|
|
||||||
const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`)
|
const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { LoginPage } from '../po/login.po'
|
import { LoginPage } from '../po/login.po'
|
||||||
import { VideoUploadPage } from '../po/video-upload.po'
|
import { VideoUploadPage } from '../po/video-upload.po'
|
||||||
import { VideoWatchPage } from '../po/video-watch.po'
|
import { VideoWatchPage } from '../po/video-watch.po'
|
||||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
import { go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||||
|
|
||||||
describe('Custom server defaults', () => {
|
describe('Custom server defaults', () => {
|
||||||
let videoUploadPage: VideoUploadPage
|
let videoUploadPage: VideoUploadPage
|
||||||
|
@ -83,8 +83,4 @@ describe('Custom server defaults', () => {
|
||||||
await checkP2P(false)
|
await checkP2P(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,7 +35,7 @@ function checkEndMessage (options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const checkEmail = 'Check your email'
|
const checkEmail = 'Check your emails'
|
||||||
|
|
||||||
if (requiresEmailVerification) {
|
if (requiresEmailVerification) {
|
||||||
expect(message).toContain(checkEmail)
|
expect(message).toContain(checkEmail)
|
||||||
|
|
|
@ -1,229 +0,0 @@
|
||||||
import { LoginPage } from '../po/login.po'
|
|
||||||
import { SignupPage } from '../po/signup.po'
|
|
||||||
import { PlayerPage } from '../po/player.po'
|
|
||||||
import { VideoUploadPage } from '../po/video-upload.po'
|
|
||||||
import { VideoWatchPage } from '../po/video-watch.po'
|
|
||||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
|
||||||
import { MyAccountPage } from '../po/my-account.po'
|
|
||||||
|
|
||||||
describe('Password protected videos', () => {
|
|
||||||
let videoUploadPage: VideoUploadPage
|
|
||||||
let loginPage: LoginPage
|
|
||||||
let videoWatchPage: VideoWatchPage
|
|
||||||
let signupPage: SignupPage
|
|
||||||
let playerPage: PlayerPage
|
|
||||||
let myAccountPage: MyAccountPage
|
|
||||||
let passwordProtectedVideoUrl: string
|
|
||||||
let playlistUrl: string
|
|
||||||
|
|
||||||
const seed = Math.random()
|
|
||||||
const passwordProtectedVideoName = seed + ' - password protected'
|
|
||||||
const publicVideoName1 = seed + ' - public 1'
|
|
||||||
const publicVideoName2 = seed + ' - public 2'
|
|
||||||
const videoPassword = 'password'
|
|
||||||
const regularUsername = 'user_1'
|
|
||||||
const regularUserPassword = 'user password'
|
|
||||||
const playlistName = seed + ' - playlist'
|
|
||||||
|
|
||||||
function testRateAndComment () {
|
|
||||||
it('Should add and remove like on video', async function () {
|
|
||||||
await videoWatchPage.like()
|
|
||||||
await videoWatchPage.like()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should create thread on video', async function () {
|
|
||||||
await videoWatchPage.createThread('My first comment')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should reply to thread on video', async function () {
|
|
||||||
await videoWatchPage.createReply('My first reply')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
await waitServerUp()
|
|
||||||
|
|
||||||
loginPage = new LoginPage(isMobileDevice())
|
|
||||||
videoUploadPage = new VideoUploadPage()
|
|
||||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
|
||||||
signupPage = new SignupPage()
|
|
||||||
playerPage = new PlayerPage()
|
|
||||||
myAccountPage = new MyAccountPage()
|
|
||||||
|
|
||||||
await browser.maximizeWindow()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Owner', function () {
|
|
||||||
before(async () => {
|
|
||||||
await loginPage.loginAsRootUser()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should login, upload a public video and save it to a playlist', async () => {
|
|
||||||
await videoUploadPage.navigateTo()
|
|
||||||
await videoUploadPage.uploadVideo('video.mp4')
|
|
||||||
await videoUploadPage.validSecondUploadStep(publicVideoName1)
|
|
||||||
|
|
||||||
await videoWatchPage.clickOnSave()
|
|
||||||
|
|
||||||
await videoWatchPage.createPlaylist(playlistName)
|
|
||||||
|
|
||||||
await videoWatchPage.saveToPlaylist(playlistName)
|
|
||||||
await browser.pause(5000)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should upload a password protected video', async () => {
|
|
||||||
await videoUploadPage.navigateTo()
|
|
||||||
await videoUploadPage.uploadVideo('video2.mp4')
|
|
||||||
await videoUploadPage.setAsPasswordProtected(videoPassword)
|
|
||||||
await videoUploadPage.validSecondUploadStep(passwordProtectedVideoName)
|
|
||||||
|
|
||||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
|
||||||
|
|
||||||
passwordProtectedVideoUrl = await browser.getUrl()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should save to playlist the password protected video', async () => {
|
|
||||||
await videoWatchPage.clickOnSave()
|
|
||||||
await videoWatchPage.saveToPlaylist(playlistName)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should upload a second public video and save it to playlist', async () => {
|
|
||||||
await videoUploadPage.navigateTo()
|
|
||||||
|
|
||||||
await videoUploadPage.uploadVideo('video3.mp4')
|
|
||||||
await videoUploadPage.validSecondUploadStep(publicVideoName2)
|
|
||||||
|
|
||||||
await videoWatchPage.clickOnSave()
|
|
||||||
await videoWatchPage.saveToPlaylist(playlistName)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should play video without password', async function () {
|
|
||||||
await go(passwordProtectedVideoUrl)
|
|
||||||
|
|
||||||
expect(!await videoWatchPage.isPasswordProtected())
|
|
||||||
|
|
||||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
|
||||||
|
|
||||||
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
|
|
||||||
await playerPage.playAndPauseVideo(false, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
testRateAndComment()
|
|
||||||
|
|
||||||
it('Should play video on embed without password', async function () {
|
|
||||||
await videoWatchPage.goOnAssociatedEmbed()
|
|
||||||
await playerPage.playAndPauseVideo(false, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the playlist in my account', async function () {
|
|
||||||
await go('/')
|
|
||||||
await myAccountPage.navigateToMyPlaylists()
|
|
||||||
const videosNumberText = await myAccountPage.getPlaylistVideosText(playlistName)
|
|
||||||
|
|
||||||
expect(videosNumberText).toEqual('3 videos')
|
|
||||||
await myAccountPage.clickOnPlaylist(playlistName)
|
|
||||||
|
|
||||||
const count = await myAccountPage.countTotalPlaylistElements()
|
|
||||||
expect(count).toEqual(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should update the playlist to public', async () => {
|
|
||||||
const url = await browser.getUrl()
|
|
||||||
const regex = /\/([a-f0-9-]+)$/i
|
|
||||||
const match = url.match(regex)
|
|
||||||
const uuid = match ? match[1] : null
|
|
||||||
|
|
||||||
await myAccountPage.updatePlaylistPrivacy(uuid, 'Public')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should watch the playlist', async () => {
|
|
||||||
await myAccountPage.clickOnPlaylist(playlistName)
|
|
||||||
await myAccountPage.playPlaylist()
|
|
||||||
playlistUrl = await browser.getUrl()
|
|
||||||
|
|
||||||
await videoWatchPage.waitUntilVideoName(publicVideoName1, 40 * 1000)
|
|
||||||
await videoWatchPage.waitUntilVideoName(passwordProtectedVideoName, 40 * 1000)
|
|
||||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await loginPage.logout()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Regular users', function () {
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
await signupPage.fullSignup({
|
|
||||||
accountInfo: {
|
|
||||||
username: regularUsername,
|
|
||||||
password: regularUserPassword
|
|
||||||
},
|
|
||||||
channelInfo: {
|
|
||||||
name: 'user_1_channel'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should requires password to play video', async function () {
|
|
||||||
await go(passwordProtectedVideoUrl)
|
|
||||||
|
|
||||||
expect(await videoWatchPage.isPasswordProtected())
|
|
||||||
|
|
||||||
await videoWatchPage.fillVideoPassword(videoPassword)
|
|
||||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
|
||||||
|
|
||||||
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
|
|
||||||
await playerPage.playAndPauseVideo(true, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
testRateAndComment()
|
|
||||||
|
|
||||||
it('Should requires password to play video on embed', async function () {
|
|
||||||
await videoWatchPage.goOnAssociatedEmbed(true)
|
|
||||||
await playerPage.fillEmbedVideoPassword(videoPassword)
|
|
||||||
await playerPage.playAndPauseVideo(false, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should watch the playlist without password protected video', async () => {
|
|
||||||
await go(playlistUrl)
|
|
||||||
await playerPage.playVideo()
|
|
||||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await loginPage.logout()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Anonymous users', function () {
|
|
||||||
it('Should requires password to play video', async function () {
|
|
||||||
await go(passwordProtectedVideoUrl)
|
|
||||||
|
|
||||||
expect(await videoWatchPage.isPasswordProtected())
|
|
||||||
|
|
||||||
await videoWatchPage.fillVideoPassword(videoPassword)
|
|
||||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
|
||||||
|
|
||||||
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
|
|
||||||
await playerPage.playAndPauseVideo(true, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should requires password to play video on embed', async function () {
|
|
||||||
await videoWatchPage.goOnAssociatedEmbed(true)
|
|
||||||
await playerPage.fillEmbedVideoPassword(videoPassword)
|
|
||||||
await playerPage.playAndPauseVideo(false, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should watch the playlist without password protected video', async () => {
|
|
||||||
await go(playlistUrl)
|
|
||||||
await playerPage.playVideo()
|
|
||||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -8,12 +8,6 @@ function isMobileDevice () {
|
||||||
return platformName === 'android' || platformName === 'ios'
|
return platformName === 'android' || platformName === 'ios'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAndroid () {
|
|
||||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
|
||||||
|
|
||||||
return platformName === 'android'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSafari () {
|
function isSafari () {
|
||||||
return browser.capabilities['browserName'] &&
|
return browser.capabilities['browserName'] &&
|
||||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||||
|
@ -26,6 +20,7 @@ function isIOS () {
|
||||||
async function go (url: string) {
|
async function go (url: string) {
|
||||||
await browser.url(url)
|
await browser.url(url)
|
||||||
|
|
||||||
|
// Hide notifications that could fail tests when hiding buttons
|
||||||
await browser.execute(() => {
|
await browser.execute(() => {
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.innerHTML = 'p-toast { display: none }'
|
style.innerHTML = 'p-toast { display: none }'
|
||||||
|
@ -46,7 +41,6 @@ export {
|
||||||
isMobileDevice,
|
isMobileDevice,
|
||||||
isSafari,
|
isSafari,
|
||||||
isIOS,
|
isIOS,
|
||||||
isAndroid,
|
|
||||||
waitServerUp,
|
waitServerUp,
|
||||||
go,
|
go,
|
||||||
browserSleep
|
browserSleep
|
||||||
|
|
|
@ -93,14 +93,5 @@ function buildConfig (suiteFile: string = undefined) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filename === 'video-password.e2e-spec.ts') {
|
|
||||||
return {
|
|
||||||
signup: {
|
|
||||||
enabled: true,
|
|
||||||
limit: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { join, resolve } from 'path'
|
||||||
|
|
||||||
function runServer (appInstance: number, config: any = {}) {
|
function runServer (appInstance: number, config: any = {}) {
|
||||||
const env = Object.create(process.env)
|
const env = Object.create(process.env)
|
||||||
|
|
||||||
env['NODE_OPTIONS'] = ''
|
|
||||||
env['NODE_ENV'] = 'test'
|
env['NODE_ENV'] = 'test'
|
||||||
env['NODE_APP_INSTANCE'] = appInstance + ''
|
env['NODE_APP_INSTANCE'] = appInstance + ''
|
||||||
|
|
||||||
|
@ -45,10 +43,7 @@ function runServer (appInstance: number, config: any = {}) {
|
||||||
|
|
||||||
function runCommand (command: string) {
|
function runCommand (command: string) {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
// Reset NODE_OPTIONS env set by webdriverio
|
const p = exec(command, { cwd: getRootCWD() })
|
||||||
const env = { ...process.env, NODE_OPTIONS: '' }
|
|
||||||
|
|
||||||
const p = exec(command, { env, cwd: getRootCWD() })
|
|
||||||
|
|
||||||
p.stderr.on('data', data => console.error(data.toString()))
|
p.stderr.on('data', data => console.error(data.toString()))
|
||||||
p.on('error', err => rej(err))
|
p.on('error', err => rej(err))
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
const FIXTURE_URLS = {
|
const FIXTURE_URLS = {
|
||||||
INTERNAL_WEB_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0',
|
INTERNAL_WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?mode=webtorrent&start=0',
|
||||||
INTERNAL_HLS_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?start=0',
|
INTERNAL_HLS_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?start=0',
|
||||||
|
|
||||||
INTERNAL_EMBED_WEB_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0',
|
INTERNAL_EMBED_WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?mode=webtorrent&start=0',
|
||||||
INTERNAL_EMBED_HLS_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?start=0',
|
INTERNAL_EMBED_HLS_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?start=0',
|
||||||
|
|
||||||
INTERNAL_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/w/tKQmHcqdYZRdCszLUiWM3V?start=0',
|
INTERNAL_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/w/tKQmHcqdYZRdCszLUiWM3V?start=0',
|
||||||
INTERNAL_EMBED_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/videos/embed/tKQmHcqdYZRdCszLUiWM3V?start=0',
|
INTERNAL_EMBED_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/videos/embed/tKQmHcqdYZRdCszLUiWM3V?start=0',
|
||||||
|
|
||||||
WEB_VIDEO: 'https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e',
|
WEBTORRENT_VIDEO: 'https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e',
|
||||||
|
|
||||||
HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50',
|
HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50',
|
||||||
HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a',
|
HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a',
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"typeRoots": [
|
|
||||||
"../node_modules/@types",
|
|
||||||
"../node_modules"
|
|
||||||
],
|
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"@wdio/globals/types",
|
"@wdio/globals/types",
|
||||||
|
|
|
@ -17,32 +17,18 @@ function buildMainOptions (sessionName: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBStackDesktopOptions (options: {
|
function buildBStackDesktopOptions (sessionName: string, resolution: string, os?: string) {
|
||||||
sessionName: string
|
|
||||||
resolution: string
|
|
||||||
os?: string
|
|
||||||
osVersion?: string
|
|
||||||
}) {
|
|
||||||
const { sessionName, resolution, os, osVersion } = options
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'bstack:options': {
|
'bstack:options': {
|
||||||
...buildMainOptions(sessionName),
|
...buildMainOptions(sessionName),
|
||||||
|
|
||||||
os,
|
os,
|
||||||
osVersion,
|
|
||||||
resolution
|
resolution
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBStackMobileOptions (options: {
|
function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string) {
|
||||||
sessionName: string
|
|
||||||
deviceName: string
|
|
||||||
osVersion: string
|
|
||||||
}) {
|
|
||||||
const { sessionName, deviceName, osVersion } = options
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'bstack:options': {
|
'bstack:options': {
|
||||||
...buildMainOptions(sessionName),
|
...buildMainOptions(sessionName),
|
||||||
|
@ -67,45 +53,45 @@ module.exports = {
|
||||||
{
|
{
|
||||||
browserName: 'Chrome',
|
browserName: 'Chrome',
|
||||||
|
|
||||||
...buildBStackDesktopOptions({ sessionName: 'Latest Chrome Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
...buildBStackDesktopOptions('Latest Chrome Desktop', '1280x1024')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Firefox',
|
browserName: 'Firefox',
|
||||||
browserVersion: '78', // Very old ESR
|
browserVersion: '78', // Very old ESR
|
||||||
|
|
||||||
...buildBStackDesktopOptions({ sessionName: 'Firefox ESR Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
...buildBStackDesktopOptions('Firefox ESR Desktop', '1280x1024', 'Windows')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Safari',
|
browserName: 'Safari',
|
||||||
browserVersion: '12.1',
|
browserVersion: '12.1',
|
||||||
|
|
||||||
...buildBStackDesktopOptions({ sessionName: 'Safari Desktop', resolution: '1280x1024' })
|
...buildBStackDesktopOptions('Safari Desktop', '1280x1024')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Firefox',
|
browserName: 'Firefox',
|
||||||
|
|
||||||
...buildBStackDesktopOptions({ sessionName: 'Firefox Latest', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
...buildBStackDesktopOptions('Firefox Latest', '1280x1024')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Edge',
|
browserName: 'Edge',
|
||||||
|
|
||||||
...buildBStackDesktopOptions({ sessionName: 'Edge Latest', resolution: '1280x1024' })
|
...buildBStackDesktopOptions('Edge Latest', '1280x1024')
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
browserName: 'Chrome',
|
browserName: 'Chrome',
|
||||||
|
|
||||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S8', osVersion: '7.0' })
|
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S8', '7.0')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Safari',
|
browserName: 'Safari',
|
||||||
|
|
||||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 8 Plus', osVersion: '12.4' })
|
...buildBStackMobileOptions('Safari iPhone', 'iPhone 8 Plus', '12.4')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
browserName: 'Safari',
|
browserName: 'Safari',
|
||||||
|
|
||||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad 7th', osVersion: '13' })
|
...buildBStackMobileOptions('Safari iPad', 'iPad 7th', '13')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ module.exports = {
|
||||||
'browserName': 'chrome',
|
'browserName': 'chrome',
|
||||||
'acceptInsecureCerts': true,
|
'acceptInsecureCerts': true,
|
||||||
'goog:chromeOptions': {
|
'goog:chromeOptions': {
|
||||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
args: [ '--disable-gpu', windowSizeArg ],
|
||||||
prefs
|
prefs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -43,7 +43,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
services: [ 'shared-store' ],
|
services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
|
||||||
|
|
||||||
beforeSession: beforeLocalSession,
|
beforeSession: beforeLocalSession,
|
||||||
beforeSuite: beforeLocalSuite,
|
beforeSuite: beforeLocalSuite,
|
||||||
|
|
|
@ -22,7 +22,6 @@ module.exports = {
|
||||||
{
|
{
|
||||||
'browserName': 'chrome',
|
'browserName': 'chrome',
|
||||||
'goog:chromeOptions': {
|
'goog:chromeOptions': {
|
||||||
binary: '/usr/bin/google-chrome-stable',
|
|
||||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||||
prefs
|
prefs
|
||||||
}
|
}
|
||||||
|
@ -38,7 +37,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
services: [ 'shared-store' ],
|
services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
|
||||||
|
|
||||||
beforeSession: beforeLocalSession,
|
beforeSession: beforeLocalSession,
|
||||||
beforeSuite: beforeLocalSuite,
|
beforeSuite: beforeLocalSuite,
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const config = {
|
||||||
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
||||||
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
||||||
// gets prepended directly.
|
// gets prepended directly.
|
||||||
baseUrl: 'http://127.0.0.1:9001',
|
baseUrl: 'http://localhost:9001',
|
||||||
//
|
//
|
||||||
// Default timeout for all waitFor* commands.
|
// Default timeout for all waitFor* commands.
|
||||||
waitforTimeout: 5000,
|
waitforTimeout: 5000,
|
||||||
|
@ -80,7 +80,7 @@ export const config = {
|
||||||
framework: 'mocha',
|
framework: 'mocha',
|
||||||
//
|
//
|
||||||
// The number of times to retry the entire specfile when it fails as a whole
|
// The number of times to retry the entire specfile when it fails as a whole
|
||||||
specFileRetries: 2,
|
specFileRetries: 1,
|
||||||
//
|
//
|
||||||
// Delay in seconds between the spec file retry attempts
|
// Delay in seconds between the spec file retry attempts
|
||||||
// specFileRetriesDelay: 0,
|
// specFileRetriesDelay: 0,
|
||||||
|
@ -107,6 +107,14 @@ export const config = {
|
||||||
|
|
||||||
tsNodeOpts: {
|
tsNodeOpts: {
|
||||||
project: require('path').join(__dirname, './tsconfig.json')
|
project: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
},
|
||||||
|
|
||||||
|
tsConfigPathsOpts: {
|
||||||
|
baseUrl: './',
|
||||||
|
paths: {
|
||||||
|
'@server/*': [ '../../server/*' ],
|
||||||
|
'@shared/*': [ '../../shared/*' ]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "peertube-client",
|
"name": "peertube-client",
|
||||||
"version": "6.0.1",
|
"version": "5.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npm run lint-ts && npm run lint-scss",
|
"lint": "npm run lint-ts && npm run lint-scss",
|
||||||
"lint-ts": "eslint --cache --ext .ts src/standalone/**/*.ts && npm run ng lint",
|
"lint-ts": "eslint --ext .ts src/standalone/**/*.ts && npm run ng lint",
|
||||||
"lint-scss": "stylelint 'src/**/*.scss'",
|
"lint-scss": "stylelint 'src/**/*.scss'",
|
||||||
"webpack": "webpack",
|
"webpack": "webpack",
|
||||||
"eslint": "eslint",
|
"eslint": "eslint",
|
||||||
|
@ -24,9 +24,6 @@
|
||||||
"ngx-extractor": "ngx-extractor",
|
"ngx-extractor": "ngx-extractor",
|
||||||
"stylelint": "stylelint"
|
"stylelint": "stylelint"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
|
||||||
"../packages/*"
|
|
||||||
],
|
|
||||||
"typings": "*.d.ts",
|
"typings": "*.d.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^16.0.2",
|
"@angular-devkit/build-angular": "^16.0.2",
|
||||||
|
@ -50,18 +47,14 @@
|
||||||
"@angular/service-worker": "^16.0.2",
|
"@angular/service-worker": "^16.0.2",
|
||||||
"@babel/core": "^7.18.5",
|
"@babel/core": "^7.18.5",
|
||||||
"@babel/preset-env": "^7.18.2",
|
"@babel/preset-env": "^7.18.2",
|
||||||
"@formatjs/intl-locale": "^3.3.1",
|
"@ng-bootstrap/ng-bootstrap": "^14.0.1",
|
||||||
"@formatjs/intl-pluralrules": "^5.2.2",
|
"@ng-select/ng-select": "^10.0.3",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
|
||||||
"@ng-select/ng-select": "^11.2.0",
|
|
||||||
"@ngx-loading-bar/core": "^6.0.0",
|
"@ngx-loading-bar/core": "^6.0.0",
|
||||||
"@ngx-loading-bar/http-client": "^6.0.0",
|
"@ngx-loading-bar/http-client": "^6.0.0",
|
||||||
"@ngx-loading-bar/router": "^6.0.0",
|
"@ngx-loading-bar/router": "^6.0.0",
|
||||||
"@peertube/maildev": "^1.2.0",
|
"@peertube/maildev": "^1.2.0",
|
||||||
"@peertube/p2p-media-loader-core": "^1.0.15",
|
"@peertube/p2p-media-loader-core": "^1.0.14",
|
||||||
"@peertube/p2p-media-loader-hlsjs": "^1.0.15",
|
"@peertube/p2p-media-loader-hlsjs": "^1.0.14",
|
||||||
"@peertube/peertube-core-utils": "*",
|
|
||||||
"@peertube/peertube-models": "*",
|
|
||||||
"@peertube/videojs-contextmenu": "^5.5.0",
|
"@peertube/videojs-contextmenu": "^5.5.0",
|
||||||
"@peertube/xliffmerge": "^2.0.3",
|
"@peertube/xliffmerge": "^2.0.3",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
|
@ -71,48 +64,58 @@
|
||||||
"@types/jschannel": "^1.0.0",
|
"@types/jschannel": "^1.0.0",
|
||||||
"@types/linkifyjs": "^2.1.2",
|
"@types/linkifyjs": "^2.1.2",
|
||||||
"@types/lodash-es": "^4.17.0",
|
"@types/lodash-es": "^4.17.0",
|
||||||
"@types/markdown-it": "^13.0.2",
|
"@types/markdown-it": "^12.0.1",
|
||||||
"@types/node": "^18.13.0",
|
"@types/node": "^18.13.0",
|
||||||
"@types/sanitize-html": "2.9.2",
|
"@types/sanitize-html": "2.6.2",
|
||||||
"@types/sha.js": "^2.4.0",
|
"@types/sha.js": "^2.4.0",
|
||||||
"@types/video.js": "^7.3.40",
|
"@types/video.js": "^7.3.40",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
"@types/webtorrent": "^0.109.0",
|
||||||
"@typescript-eslint/parser": "^6.7.5",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
|
"@typescript-eslint/parser": "^5.43.0",
|
||||||
"@wdio/browserstack-service": "^8.10.5",
|
"@wdio/browserstack-service": "^8.10.5",
|
||||||
"@wdio/cli": "^8.10.5",
|
"@wdio/cli": "^8.10.5",
|
||||||
"@wdio/local-runner": "^8.10.5",
|
"@wdio/local-runner": "^8.10.5",
|
||||||
"@wdio/mocha-framework": "^8.10.4",
|
"@wdio/mocha-framework": "^8.10.4",
|
||||||
"@wdio/shared-store-service": "^8.10.5",
|
"@wdio/shared-store-service": "^8.10.5",
|
||||||
"@wdio/spec-reporter": "^8.10.5",
|
"@wdio/spec-reporter": "^8.10.5",
|
||||||
|
"angular2-hotkeys": "^13.1.0",
|
||||||
"angularx-qrcode": "16.0.0",
|
"angularx-qrcode": "16.0.0",
|
||||||
"babel-loader": "^9.1.0",
|
"babel-loader": "^9.1.0",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"cache-chunk-store": "^3.0.0",
|
||||||
"chart.js": "^4.3.0",
|
"chart.js": "^4.3.0",
|
||||||
"chartjs-plugin-zoom": "~2.0.1",
|
"chartjs-plugin-zoom": "~2.0.1",
|
||||||
|
"chromedriver": "^113.0.0",
|
||||||
"core-js": "^3.22.8",
|
"core-js": "^3.22.8",
|
||||||
"css-loader": "^6.2.0",
|
"css-loader": "^6.2.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
|
"dexie": "^3.2.2",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-plugin-import": "2.28.1",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-jsdoc": "^46.8.2",
|
"eslint-plugin-jsdoc": "^44.2.4",
|
||||||
"eslint-plugin-prefer-arrow": "latest",
|
"eslint-plugin-prefer-arrow": "latest",
|
||||||
"expect-webdriverio": "^4.2.3",
|
"expect-webdriverio": "^4.2.3",
|
||||||
"focus-visible": "^5.0.2",
|
"focus-visible": "^5.0.2",
|
||||||
|
"geckodriver": "^4.0.0",
|
||||||
"hls.js": "~1.3",
|
"hls.js": "~1.3",
|
||||||
"html-loader": "^4.1.0",
|
"html-loader": "^4.1.0",
|
||||||
"html-webpack-plugin": "^5.3.1",
|
"html-webpack-plugin": "^5.3.1",
|
||||||
|
"https-browserify": "^1.0.0",
|
||||||
"intl-messageformat": "^10.1.0",
|
"intl-messageformat": "^10.1.0",
|
||||||
"jschannel": "^1.0.2",
|
"jschannel": "^1.0.2",
|
||||||
"linkify-html": "^4.0.2",
|
"linkify-html": "^4.0.2",
|
||||||
"linkifyjs": "^4.0.2",
|
"linkifyjs": "^4.0.2",
|
||||||
"lodash-es": "^4.17.4",
|
"lodash-es": "^4.17.4",
|
||||||
"markdown-it": "13.0.2",
|
"markdown-it": "13.0.1",
|
||||||
"mini-css-extract-plugin": "^2.2.0",
|
"mini-css-extract-plugin": "^2.2.0",
|
||||||
"ngx-uploadx": "^6.1.0",
|
"ngx-uploadx": "^6.1.0",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"primeng": "^16.0.0-rc.2",
|
"primeng": "^16.0.0-rc.2",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"purify-css": "^1.2.5",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"sanitize-html": "^2.1.2",
|
"sanitize-html": "^2.1.2",
|
||||||
|
@ -120,17 +123,23 @@
|
||||||
"sass-loader": "^13.2.0",
|
"sass-loader": "^13.2.0",
|
||||||
"sha.js": "^2.4.11",
|
"sha.js": "^2.4.11",
|
||||||
"socket.io-client": "^4.5.4",
|
"socket.io-client": "^4.5.4",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"stream-http": "^3.0.0",
|
||||||
"stylelint": "^15.1.0",
|
"stylelint": "^15.1.0",
|
||||||
"stylelint-config-sass-guidelines": "^10.0.0",
|
"stylelint-config-sass-guidelines": "^10.0.0",
|
||||||
"tinykeys": "^2.1.0",
|
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "~5.1.0",
|
"typescript": "~4.9.5",
|
||||||
|
"url": "^0.11.0",
|
||||||
"video.js": "^7.19.2",
|
"video.js": "^7.19.2",
|
||||||
|
"videostream": "~3.2.1",
|
||||||
|
"wdio-chromedriver-service": "^8.1.1",
|
||||||
|
"wdio-geckodriver-service": "^5.0.1",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.73.0",
|
||||||
"webpack-bundle-analyzer": "^4.4.2",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^5.0.1",
|
"webpack-cli": "^5.0.1",
|
||||||
|
"webtorrent": "1.8.26",
|
||||||
|
"whatwg-fetch": "^3.0.0",
|
||||||
"zone.js": "~0.13.0"
|
"zone.js": "~0.13.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
|
import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
|
||||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||||
import { Actor } from '@peertube/peertube-models'
|
import { Actor } from '@shared/models/actors'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-about-follows',
|
selector: 'my-about-follows',
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@ang
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { Notifier, ServerService } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
import { AboutHTML } from '@app/shared/shared-instance'
|
import { AboutHTML } from '@app/shared/shared-instance'
|
||||||
import { HTMLServerConfig, ServerStats } from '@peertube/peertube-models'
|
|
||||||
import { copyToClipboard } from '@root-helpers/utils'
|
import { copyToClipboard } from '@root-helpers/utils'
|
||||||
|
import { HTMLServerConfig, ServerStats } from '@shared/models/server'
|
||||||
import { ResolverData } from './about-instance.resolver'
|
import { ResolverData } from './about-instance.resolver'
|
||||||
import { ContactAdminModalComponent } from './contact-admin-modal.component'
|
import { ContactAdminModalComponent } from './contact-admin-modal.component'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||||
import { AboutHTML, InstanceService } from '@app/shared/shared-instance'
|
import { AboutHTML, InstanceService } from '@app/shared/shared-instance'
|
||||||
import { About, ServerStats } from '@peertube/peertube-models'
|
import { About, ServerStats } from '@shared/models/server'
|
||||||
|
|
||||||
export type ResolverData = {
|
export type ResolverData = {
|
||||||
serverStats: ServerStats
|
serverStats: ServerStats
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
|
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon>
|
||||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
|
||||||
<my-global-icon iconName="cross"></my-global-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -15,9 +12,8 @@
|
||||||
<input
|
<input
|
||||||
type="text" id="fromName" class="form-control"
|
type="text" id="fromName" class="form-control"
|
||||||
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
|
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
|
||||||
autocomplete="name"
|
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.fromName" class="form-error" role="alert">{{ formErrors.fromName }}</div>
|
<div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -25,9 +21,8 @@
|
||||||
<input
|
<input
|
||||||
type="text" id="fromEmail" class="form-control"
|
type="text" id="fromEmail" class="form-control"
|
||||||
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
|
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
|
||||||
i18n-placeholder placeholder="Example: john@example.com" autocomplete="email"
|
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.fromEmail" class="form-error" role="alert">{{ formErrors.fromEmail }}</div>
|
<div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -36,14 +31,14 @@
|
||||||
type="text" id="subject" class="form-control"
|
type="text" id="subject" class="form-control"
|
||||||
formControlName="subject" [ngClass]="{ 'input-error': formErrors['subject'] }"
|
formControlName="subject" [ngClass]="{ 'input-error': formErrors['subject'] }"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.subject" class="form-error" role="alert">{{ formErrors.subject }}</div>
|
<div *ngIf="formErrors.subject" class="form-error">{{ formErrors.subject }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="body">Your message</label>
|
<label i18n for="body">Your message</label>
|
||||||
<textarea id="body" formControlName="body" class="form-control" [ngClass]="{ 'input-error': formErrors['body'] }">
|
<textarea id="body" formControlName="body" class="form-control" [ngClass]="{ 'input-error': formErrors['body'] }">
|
||||||
</textarea>
|
</textarea>
|
||||||
<div *ngIf="formErrors.body" class="form-error" role="alert">{{ formErrors.body }}</div>
|
<div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { InstanceService } from '@app/shared/shared-instance'
|
import { InstanceService } from '@app/shared/shared-instance'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { HTMLServerConfig, HttpStatusCode } from '@peertube/peertube-models'
|
import { HTMLServerConfig, HttpStatusCode } from '@shared/models'
|
||||||
|
|
||||||
type Prefill = {
|
type Prefill = {
|
||||||
subject?: string
|
subject?: string
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ServerStats } from '@peertube/peertube-models'
|
import { ServerStats } from '@shared/models/server'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-statistics',
|
selector: 'my-instance-statistics',
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { from, Subject, Subscription } from 'rxjs'
|
||||||
import { concatMap, map, switchMap, tap } from 'rxjs/operators'
|
import { concatMap, map, switchMap, tap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ComponentPagination, hasMoreItems, MarkdownService, User, UserService } from '@app/core'
|
import { ComponentPagination, hasMoreItems, MarkdownService, User, UserService } from '@app/core'
|
||||||
import { SimpleMemoize } from '@app/helpers'
|
|
||||||
import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
||||||
import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
|
import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
|
||||||
import { NSFWPolicyType, VideoSortField } from '@peertube/peertube-models'
|
import { NSFWPolicyType, VideoSortField } from '@shared/models'
|
||||||
|
import { SimpleMemoize } from '@app/helpers'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-video-channels',
|
selector: 'my-account-video-channels',
|
||||||
|
@ -98,7 +98,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
||||||
videoChannel,
|
videoChannel,
|
||||||
videoPagination: this.videosPagination,
|
videoPagination: this.videosPagination,
|
||||||
sort: this.videosSort,
|
sort: this.videosSort,
|
||||||
nsfw: this.videoService.nsfwPolicyToParam(this.nsfwPolicy)
|
nsfwPolicy: this.nsfwPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.videoService.getVideoChannelVideos(options)
|
return this.videoService.getVideoChannelVideos(options)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ComponentPaginationLight, DisableForReuseHook, ScreenService } from '@app/core'
|
import { ComponentPaginationLight, DisableForReuseHook, ScreenService } from '@app/core'
|
||||||
import { Account, AccountService, VideoService } from '@app/shared/shared-main'
|
import { Account, AccountService, VideoService } from '@app/shared/shared-main'
|
||||||
import { VideoFilters } from '@app/shared/shared-video-miniature'
|
import { VideoFilters } from '@app/shared/shared-video-miniature'
|
||||||
import { VideoSortField } from '@peertube/peertube-models'
|
import { VideoSortField } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-videos',
|
selector: 'my-account-videos',
|
||||||
|
|
|
@ -18,18 +18,18 @@
|
||||||
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
||||||
></my-user-moderation-dropdown>
|
></my-user-moderation-dropdown>
|
||||||
|
|
||||||
<span *ngIf="accountUser?.blocked" tabindex="0" [ngbTooltip]="accountUser.blockedReason" class="pt-badge badge-danger" i18n>Banned</span>
|
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="pt-badge badge-danger" i18n>Banned</span>
|
||||||
|
|
||||||
<my-account-block-badges [account]="account"></my-account-block-badges>
|
<my-account-block-badges [account]="account"></my-account-block-badges>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-handle">
|
<div class="actor-handle">
|
||||||
<span>@{{ account.nameWithHost }}</span>
|
<span>@{{ account.nameWithHost }}</span>
|
||||||
|
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
||||||
<my-copy-button
|
class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
|
||||||
[value]="account.nameWithHostForced" i18n-notification notification="Username copied"
|
>
|
||||||
title="Copy account handle" i18n-title
|
<my-global-icon iconName="copy"></my-global-icon>
|
||||||
></my-copy-button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-counters">
|
<div class="actor-counters">
|
||||||
|
|
|
@ -28,8 +28,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
my-copy-button {
|
.copy-button {
|
||||||
@include margin-left(3px);
|
@include margin-left(3px);
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-info {
|
.account-info {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
VideoService
|
VideoService
|
||||||
} from '@app/shared/shared-main'
|
} from '@app/shared/shared-main'
|
||||||
import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
|
import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
|
||||||
import { HttpStatusCode, User, UserRight } from '@peertube/peertube-models'
|
import { HttpStatusCode, User, UserRight } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './accounts.component.html',
|
templateUrl: './accounts.component.html',
|
||||||
|
@ -115,6 +115,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
this.redirectService.redirectToHomepage()
|
this.redirectService.redirectToHomepage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activateCopiedMessage () {
|
||||||
|
this.notifier.success($localize`Username copied`)
|
||||||
|
}
|
||||||
|
|
||||||
searchChanged (search: string) {
|
searchChanged (search: string) {
|
||||||
const queryParams = { search }
|
const queryParams = { search }
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
||||||
import { AuthService, ScreenService, ServerService } from '@app/core'
|
import { AuthService, ScreenService, ServerService } from '@app/core'
|
||||||
import { ListOverflowItem } from '@app/shared/shared-main'
|
import { ListOverflowItem } from '@app/shared/shared-main'
|
||||||
import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
|
import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
|
||||||
import { UserRight } from '@peertube/peertube-models'
|
import { UserRight } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './admin.component.html',
|
templateUrl: './admin.component.html',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Routes } from '@angular/router'
|
import { Routes } from '@angular/router'
|
||||||
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
||||||
import { UserRightGuard } from '@app/core'
|
import { UserRightGuard } from '@app/core'
|
||||||
import { UserRight } from '@peertube/peertube-models'
|
import { UserRight } from '@shared/models'
|
||||||
|
|
||||||
export const ConfigRoutes: Routes = [
|
export const ConfigRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.cache.previews.size" class="form-error" role="alert">{{ formErrors.cache.previews.size }}</div>
|
<div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="captions">
|
<div class="form-group" formGroupName="captions">
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.cache.captions.size" class="form-error" role="alert">{{ formErrors.cache.captions.size }}</div>
|
<div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrents">
|
<div class="form-group" formGroupName="torrents">
|
||||||
|
@ -50,21 +50,7 @@
|
||||||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.cache.torrents.size" class="form-error" role="alert">{{ formErrors.cache.torrents.size }}</div>
|
<div *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrents">
|
|
||||||
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
|
||||||
|
|
||||||
<div class="number-with-unit">
|
|
||||||
<input
|
|
||||||
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
|
||||||
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.storyboards.size'] }"
|
|
||||||
>
|
|
||||||
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors.cache.storyboards.size" class="form-error" role="alert">{{ formErrors.cache.storyboards.size }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -88,18 +74,17 @@
|
||||||
<my-help>
|
<my-help>
|
||||||
<ng-template ptTemplate="customHtml">
|
<ng-template ptTemplate="customHtml">
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
|
||||||
<pre>console.log('my instance is amazing');</pre>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</my-help>
|
</my-help>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="customizationJavascript" formControlName="javascript" class="form-control" dir="ltr"
|
id="customizationJavascript" formControlName="javascript" class="form-control"
|
||||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
|
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error" role="alert">{{ formErrors.instance.customizations.javascript }}</div>
|
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -108,16 +93,16 @@
|
||||||
<my-help>
|
<my-help>
|
||||||
<ng-template ptTemplate="customHtml">
|
<ng-template ptTemplate="customHtml">
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
Write CSS code directly. Example:<br /><br />
|
||||||
<pre>
|
<pre>
|
||||||
#custom-css {{ '{' }}
|
#custom-css {{ '{' }}
|
||||||
color: red;
|
color: red;
|
||||||
{{ '}' }}
|
{{ '}' }}
|
||||||
</pre>
|
</pre>
|
||||||
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
|
||||||
<pre>
|
<pre>
|
||||||
#custom-css .logged-in-email {{ '{' }}
|
#custom-css .logged-in-email {{ '{' }}
|
||||||
color: red;
|
color: red;
|
||||||
{{ '}' }}
|
{{ '}' }}
|
||||||
</pre>
|
</pre>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -125,10 +110,10 @@
|
||||||
</my-help>
|
</my-help>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="customizationCSS" formControlName="css" class="form-control" dir="ltr"
|
id="customizationCSS" formControlName="css" class="form-control"
|
||||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
|
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div *ngIf="formErrors.instance.customizations.css" class="form-error" role="alert">{{ formErrors.instance.customizations.css }}</div>
|
<div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class EditAdvancedConfigurationComponent {
|
||||||
@Input() form: FormGroup
|
@Input() form: FormGroup
|
||||||
@Input() formErrors: any
|
@Input() formErrors: any
|
||||||
|
|
||||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
getCacheSize (type: 'captions' | 'previews' | 'torrents') {
|
||||||
return this.form.value['cache'][type]['size']
|
return this.form.value['cache'][type]['size']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors.instance.defaultClientRoute }}</div>
|
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="trending">
|
<div class="form-group" formGroupName="trending">
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors.trending.videos.algorithms.default }}</div>
|
<div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,7 +126,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.broadcastMessage.level" class="form-error" role="alert">{{ formErrors.broadcastMessage.level }}</div>
|
<div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
[formError]="formErrors['broadcastMessage.message']" markdownType="to-unsafe-html"
|
[formError]="formErrors['broadcastMessage.message']" markdownType="to-unsafe-html"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.broadcastMessage.message" class="form-error" role="alert">{{ formErrors.broadcastMessage.message }}</div>
|
<div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
<span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
<span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.signup.limit" class="form-error" role="alert">{{ formErrors.signup.limit }}</div>
|
<div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
|
||||||
|
|
||||||
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -209,7 +209,7 @@
|
||||||
<span i18n>{form.value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
<span i18n>{form.value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.signup.minimumAge" class="form-error" role="alert">{{ formErrors.signup.minimumAge }}</div>
|
<div *ngIf="formErrors.signup.minimumAge" class="form-error">{{ formErrors.signup.minimumAge }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
|
|
||||||
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
<my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||||
|
|
||||||
<div *ngIf="formErrors.user.videoQuota" class="form-error" role="alert">{{ formErrors.user.videoQuota }}</div>
|
<div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -244,7 +244,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors.user.videoQuotaDaily }}</div>
|
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<ng-container formGroupName="history">
|
<ng-container formGroupName="history">
|
||||||
|
@ -282,7 +282,7 @@
|
||||||
<span i18n>jobs in parallel</span>
|
<span i18n>jobs in parallel</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.import.concurrency" class="form-error" role="alert">{{ formErrors.import.concurrency }}</div>
|
<div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="http">
|
<div class="form-group" formGroupName="http">
|
||||||
|
@ -313,7 +313,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="importSynchronizationEnabled" formControlName="enabled"
|
inputName="importSynchronizationEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube (requires allowing import with HTTP URL)"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||||
|
@ -345,18 +345,6 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container formGroupName="videoFile">
|
|
||||||
<ng-container formGroupName="update">
|
|
||||||
<div class="form-group">
|
|
||||||
<my-peertube-checkbox
|
|
||||||
inputName="videoFileUpdateEnabled" formControlName="enabled"
|
|
||||||
i18n-labelText labelText="Allow users to upload a new version of their video"
|
|
||||||
>
|
|
||||||
</my-peertube-checkbox>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -377,7 +365,7 @@
|
||||||
<span i18n>{form.value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
<span i18n>{form.value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors.videoChannels.maxPerUser }}</div>
|
<div *ngIf="formErrors.videoChannels.maxPerUser" class="form-error">{{ formErrors.videoChannels.maxPerUser }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -439,7 +427,7 @@
|
||||||
formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
|
formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors.search.searchIndex.url" class="form-error" role="alert">{{ formErrors.search.searchIndex.url }}</div>
|
<div *ngIf="formErrors.search.searchIndex.url" class="form-error">{{ formErrors.search.searchIndex.url }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
@ -537,7 +525,7 @@
|
||||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
|
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
@ -565,7 +553,7 @@
|
||||||
formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
|
formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors.admin.email" class="form-error" role="alert">{{ formErrors.admin.email }}</div>
|
<div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="contactForm">
|
<div class="form-group" formGroupName="contactForm">
|
||||||
|
@ -600,27 +588,22 @@
|
||||||
formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
|
formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors.services.twitter.username" class="form-error" role="alert">{{ formErrors.services.twitter.username }}</div>
|
<div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
|
<my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
|
||||||
<ng-template ptTemplate="label">
|
<ng-template ptTemplate="label">
|
||||||
<ng-container i18n>Instance allowed by Twitter/X</ng-container>
|
<ng-container i18n>Instance allowed by Twitter</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template ptTemplate="help">
|
<ng-template ptTemplate="help">
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
<p class="mb-2">
|
If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
|
||||||
If your instance is explicitly allowed by Twitter/X, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
|
If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
|
||||||
If the instance is not, we use an image link card that will redirect to your PeerTube instance.
|
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/w/blabla) on
|
||||||
</p>
|
<a class="link-orange" target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
|
||||||
|
to see if you instance is allowed.
|
||||||
<p>
|
|
||||||
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/w/blabla) on
|
|
||||||
<a class="link-orange" target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
|
|
||||||
to see if you instance is allowed.
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { MenuService, ThemeService } from '@app/core'
|
import { MenuService, ThemeService } from '@app/core'
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
import { HTMLServerConfig } from '@shared/models'
|
||||||
import { ConfigService } from '../shared/config.service'
|
import { ConfigService } from '../shared/config.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { formatICU } from '@app/helpers'
|
import { prepareIcu } from '@app/helpers'
|
||||||
|
|
||||||
export type ResolutionOption = {
|
export type ResolutionOption = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -99,7 +99,10 @@ export class EditConfigurationService {
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
|
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
|
||||||
unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value })
|
unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)(
|
||||||
|
{ value },
|
||||||
|
$localize`threads`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
<div class="col-md-7 col-xl-5"></div>
|
<div class="col-md-7 col-xl-5"></div>
|
||||||
<div class="col-md-5 col-xl-5">
|
<div class="col-md-5 col-xl-5">
|
||||||
|
|
||||||
<div role="alert" class="form-error submit-error" i18n *ngIf="!form.valid && isUpdateAllowed()">
|
<div class="form-error submit-error" i18n *ngIf="!form.valid && isUpdateAllowed()">
|
||||||
There are errors in the form:
|
There are errors in the form:
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span role="alert" class="form-error submit-error" i18n *ngIf="!hasLiveAllowReplayConsistentOptions()">
|
<span class="form-error submit-error" i18n *ngIf="!hasLiveAllowReplayConsistentOptions()">
|
||||||
You cannot allow live replay if you don't enable transcoding.
|
You cannot allow live replay if you don't enable transcoding.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
import omit from 'lodash-es/omit'
|
import omit from 'lodash-es/omit'
|
||||||
import { forkJoin } from 'rxjs'
|
import { forkJoin } from 'rxjs'
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
@ -8,7 +9,8 @@ import { Notifier } from '@app/core'
|
||||||
import { ServerService } from '@app/core/server/server.service'
|
import { ServerService } from '@app/core/server/server.service'
|
||||||
import {
|
import {
|
||||||
ADMIN_EMAIL_VALIDATOR,
|
ADMIN_EMAIL_VALIDATOR,
|
||||||
CACHE_SIZE_VALIDATOR,
|
CACHE_CAPTIONS_SIZE_VALIDATOR,
|
||||||
|
CACHE_PREVIEWS_SIZE_VALIDATOR,
|
||||||
CONCURRENCY_VALIDATOR,
|
CONCURRENCY_VALIDATOR,
|
||||||
INDEX_URL_VALIDATOR,
|
INDEX_URL_VALIDATOR,
|
||||||
INSTANCE_NAME_VALIDATOR,
|
INSTANCE_NAME_VALIDATOR,
|
||||||
|
@ -26,7 +28,7 @@ import {
|
||||||
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
|
import { CustomPageService } from '@app/shared/shared-main/custom-page'
|
||||||
import { CustomConfig, CustomPage, HTMLServerConfig } from '@peertube/peertube-models'
|
import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models'
|
||||||
import { EditConfigurationService } from './edit-configuration.service'
|
import { EditConfigurationService } from './edit-configuration.service'
|
||||||
|
|
||||||
type ComponentCustomConfig = CustomConfig & {
|
type ComponentCustomConfig = CustomConfig & {
|
||||||
|
@ -118,16 +120,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
previews: {
|
previews: {
|
||||||
size: CACHE_SIZE_VALIDATOR
|
size: CACHE_PREVIEWS_SIZE_VALIDATOR
|
||||||
},
|
},
|
||||||
captions: {
|
captions: {
|
||||||
size: CACHE_SIZE_VALIDATOR
|
size: CACHE_CAPTIONS_SIZE_VALIDATOR
|
||||||
},
|
},
|
||||||
torrents: {
|
torrents: {
|
||||||
size: CACHE_SIZE_VALIDATOR
|
size: CACHE_CAPTIONS_SIZE_VALIDATOR
|
||||||
},
|
|
||||||
storyboards: {
|
|
||||||
size: CACHE_SIZE_VALIDATOR
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
|
@ -189,7 +188,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
hls: {
|
hls: {
|
||||||
enabled: null
|
enabled: null
|
||||||
},
|
},
|
||||||
webVideos: {
|
webtorrent: {
|
||||||
enabled: null
|
enabled: null
|
||||||
},
|
},
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
|
@ -224,11 +223,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
enabled: null
|
enabled: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoFile: {
|
|
||||||
update: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autoBlacklist: {
|
autoBlacklist: {
|
||||||
videos: {
|
videos: {
|
||||||
ofUsers: {
|
ofUsers: {
|
||||||
|
|
|
@ -19,10 +19,9 @@
|
||||||
name="instanceCustomHomepageContent" formControlName="content"
|
name="instanceCustomHomepageContent" formControlName="content"
|
||||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||||
[formError]="formErrors['instanceCustomHomepage.content']"
|
[formError]="formErrors['instanceCustomHomepage.content']"
|
||||||
dir="ltr"
|
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error" role="alert">{{ formErrors.instanceCustomHomepage.content }}</div>
|
<div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
|
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.name" class="form-error" role="alert">{{ formErrors.instance.name }}</div>
|
<div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
|
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.shortDescription" class="form-error" role="alert">{{ formErrors.instance.shortDescription }}</div>
|
<div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
|
|
||||||
<ng-template ptTemplate="help">
|
<ng-template ptTemplate="help">
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br />
|
Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
|
||||||
Moreover, the NSFW checkbox on video upload will be automatically checked by default.
|
Moreover, the NSFW checkbox on video upload will be automatically checked by default.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error" role="alert">{{ formErrors.instance.defaultNSFWPolicy }}</div>
|
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
|
<span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.live.maxInstanceLives" class="form-error" role="alert">{{ formErrors.live.maxInstanceLives }}</div>
|
<div *ngIf="formErrors.live.maxInstanceLives" class="form-error">{{ formErrors.live.maxInstanceLives }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
<span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
|
<span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.live.maxUserLives" class="form-error" role="alert">{{ formErrors.live.maxUserLives }}</div>
|
<div *ngIf="formErrors.live.maxUserLives" class="form-error">{{ formErrors.live.maxUserLives }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
|
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
|
||||||
></my-select-options>
|
></my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors.live.maxDuration" class="form-error" role="alert">{{ formErrors.live.maxDuration }}</div>
|
<div *ngIf="formErrors.live.maxDuration" class="form-error">{{ formErrors.live.maxDuration }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
formControlName="threads"
|
formControlName="threads"
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
<div *ngIf="formErrors.live.transcoding.threads" class="form-error" role="alert">{{ formErrors.live.transcoding.threads }}</div>
|
<div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
<div class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
>
|
>
|
||||||
</my-select-options>
|
</my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors.live.transcoding.profile" class="form-error" role="alert">{{ formErrors.live.transcoding.profile }}</div>
|
<div *ngIf="formErrors.live.transcoding.profile" class="form-error">{{ formErrors.live.transcoding.profile }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
import { HTMLServerConfig } from '@shared/models'
|
||||||
import { ConfigService } from '../shared/config.service'
|
import { ConfigService } from '../shared/config.service'
|
||||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
||||||
|
|
||||||
|
|
|
@ -67,11 +67,11 @@
|
||||||
<div class="callout callout-light pt-2 mt-2 pb-0">
|
<div class="callout callout-light pt-2 mt-2 pb-0">
|
||||||
<h3 class="callout-title" i18n>Output formats</h3>
|
<h3 class="callout-title" i18n>Output formats</h3>
|
||||||
|
|
||||||
<ng-container formGroupName="webVideos">
|
<ng-container formGroupName="webtorrent">
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="transcodingWebVideosEnabled" formControlName="enabled"
|
inputName="transcodingWebTorrentEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Web Videos enabled"
|
i18n-labelText labelText="WebTorrent enabled"
|
||||||
>
|
>
|
||||||
<ng-template ptTemplate="help">
|
<ng-template ptTemplate="help">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
|
@ -93,14 +93,14 @@
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
<strong>Requires ffmpeg >= 4.1</strong>
|
<strong>Requires ffmpeg >= 4.1</strong>
|
||||||
|
|
||||||
<p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with Web Videos:</p>
|
<p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Resolution change is smoother</li>
|
<li>Resolution change is smoother</li>
|
||||||
<li>Faster playback especially with long videos</li>
|
<li>Faster playback especially with long videos</li>
|
||||||
<li>More stable playback (less bugs/infinite loading)</li>
|
<li>More stable playback (less bugs/infinite loading)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>If you also enabled Web Videos support, it will multiply videos storage by 2</p>
|
<p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors.transcoding.threads" class="form-error" role="alert">{{ formErrors.transcoding.threads }}</div>
|
<div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||||
|
@ -187,7 +187,7 @@
|
||||||
<span i18n>jobs in parallel</span>
|
<span i18n>jobs in parallel</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors.transcoding.concurrency" class="form-error" role="alert">{{ formErrors.transcoding.concurrency }}</div>
|
<div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||||
|
@ -201,7 +201,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-options>
|
></my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors.transcoding.profile" class="form-error" role="alert">{{ formErrors.transcoding.profile }}</div>
|
<div *ngIf="formErrors.transcoding.profile" class="form-error">{{ formErrors.transcoding.profile }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
import { HTMLServerConfig } from '@shared/models'
|
||||||
import { ConfigService } from '../shared/config.service'
|
import { ConfigService } from '../shared/config.service'
|
||||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
||||||
|
|
||||||
|
@ -89,9 +90,9 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
||||||
const transcodingControl = this.form.get('transcoding.enabled')
|
const transcodingControl = this.form.get('transcoding.enabled')
|
||||||
const videoStudioControl = this.form.get('videoStudio.enabled')
|
const videoStudioControl = this.form.get('videoStudio.enabled')
|
||||||
const hlsControl = this.form.get('transcoding.hls.enabled')
|
const hlsControl = this.form.get('transcoding.hls.enabled')
|
||||||
const webVideosControl = this.form.get('transcoding.webVideos.enabled')
|
const webtorrentControl = this.form.get('transcoding.webtorrent.enabled')
|
||||||
|
|
||||||
webVideosControl.valueChanges
|
webtorrentControl.valueChanges
|
||||||
.subscribe(newValue => {
|
.subscribe(newValue => {
|
||||||
if (newValue === false && !hlsControl.disabled) {
|
if (newValue === false && !hlsControl.disabled) {
|
||||||
hlsControl.disable()
|
hlsControl.disable()
|
||||||
|
@ -104,12 +105,12 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
hlsControl.valueChanges
|
hlsControl.valueChanges
|
||||||
.subscribe(newValue => {
|
.subscribe(newValue => {
|
||||||
if (newValue === false && !webVideosControl.disabled) {
|
if (newValue === false && !webtorrentControl.disabled) {
|
||||||
webVideosControl.disable()
|
webtorrentControl.disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === true && !webVideosControl.enabled) {
|
if (newValue === true && !webtorrentControl.enabled) {
|
||||||
webVideosControl.enable()
|
webtorrentControl.enable()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
||||||
})
|
})
|
||||||
|
|
||||||
transcodingControl.updateValueAndValidity()
|
transcodingControl.updateValueAndValidity()
|
||||||
webVideosControl.updateValueAndValidity()
|
webtorrentControl.updateValueAndValidity()
|
||||||
videoStudioControl.updateValueAndValidity()
|
videoStudioControl.updateValueAndValidity()
|
||||||
hlsControl.updateValueAndValidity()
|
hlsControl.updateValueAndValidity()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { catchError } from 'rxjs/operators'
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor } from '@app/core'
|
import { RestExtractor } from '@app/core'
|
||||||
import { CustomConfig } from '@peertube/peertube-models'
|
import { CustomConfig } from '@shared/models'
|
||||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
[value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
[value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers"
|
||||||
[(selection)]="selectedRows"
|
[(selection)]="selectedRows"
|
||||||
>
|
>
|
||||||
<ng-template pTemplate="caption">
|
<ng-template pTemplate="caption">
|
||||||
|
@ -28,14 +29,14 @@
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" style="width: 40px">
|
<th style="width: 40px">
|
||||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 150px;" i18n>Actions</th>
|
<th style="width: 150px;" i18n>Actions</th>
|
||||||
<th scope="col" i18n>Follower</th>
|
<th i18n>Follower</th>
|
||||||
<th scope="col" style="width: 100px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
<th scope="col" style="width: 100px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
|
<th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
|
||||||
<th scope="col" style="width: 150px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { formatICU } from '@app/helpers'
|
import { prepareIcu } from '@app/helpers'
|
||||||
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
||||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||||
import { DropdownAction } from '@app/shared/shared-main'
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
import { ActorFollow } from '@peertube/peertube-models'
|
import { ActorFollow } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-followers-list',
|
selector: 'my-followers-list',
|
||||||
|
@ -63,9 +63,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||||
$localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) },
|
||||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) }
|
$localize`Follow requests accepted`
|
||||||
)
|
)
|
||||||
this.notifier.success(message)
|
this.notifier.success(message)
|
||||||
|
|
||||||
|
@ -78,9 +78,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
||||||
|
|
||||||
async rejectFollower (follows: ActorFollow[]) {
|
async rejectFollower (follows: ActorFollow[]) {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
|
||||||
$localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`,
|
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) },
|
||||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) }
|
$localize`Do you really want to reject these follow requests?`
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await this.confirmService.confirm(message, $localize`Reject`)
|
const res = await this.confirmService.confirm(message, $localize`Reject`)
|
||||||
|
@ -90,9 +90,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||||
$localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) },
|
||||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) }
|
$localize`Follow requests rejected`
|
||||||
)
|
)
|
||||||
this.notifier.success(message)
|
this.notifier.success(message)
|
||||||
|
|
||||||
|
@ -110,9 +110,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
||||||
message += '<br /><br />'
|
message += '<br /><br />'
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
message += formatICU(
|
message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
|
||||||
$localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`,
|
icuParams,
|
||||||
icuParams
|
$localize`Do you really want to delete these follow requests?`
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await this.confirmService.confirm(message, $localize`Delete`)
|
const res = await this.confirmService.confirm(message, $localize`Delete`)
|
||||||
|
@ -122,9 +122,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||||
$localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
icuParams,
|
||||||
icuParams
|
$localize`Follow requests removed`
|
||||||
)
|
)
|
||||||
|
|
||||||
this.notifier.success(message)
|
this.notifier.success(message)
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Follow</h4>
|
<h4 i18n class="modal-title">Follow</h4>
|
||||||
|
|
||||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
<my-global-icon iconName="cross"></my-global-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -17,7 +15,7 @@
|
||||||
class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
|
class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.hostsOrHandles" class="form-error" role="alert">
|
<div *ngIf="formErrors.hostsOrHandles" class="form-error">
|
||||||
{{ formErrors.hostsOrHandles }}
|
{{ formErrors.hostsOrHandles }}
|
||||||
|
|
||||||
<div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
|
<div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier } from '@app/core'
|
||||||
import { formatICU } from '@app/helpers'
|
import { prepareIcu } from '@app/helpers'
|
||||||
import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
|
import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
|
||||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||||
|
@ -62,9 +62,9 @@ export class FollowModalComponent extends FormReactive implements OnInit {
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success(
|
this.notifier.success(
|
||||||
formatICU(
|
prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)(
|
||||||
$localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`,
|
{ count: hostsOrHandles.length },
|
||||||
{ count: hostsOrHandles.length }
|
$localize`Follow request(s) sent!`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
|
||||||
[(selection)]="selectedRows"
|
[(selection)]="selectedRows"
|
||||||
>
|
>
|
||||||
<ng-template pTemplate="caption">
|
<ng-template pTemplate="caption">
|
||||||
|
@ -33,14 +34,14 @@
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" style="width: 40px">
|
<th style="width: 40px">
|
||||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
<th style="width: 150px;" i18n>Action</th>
|
||||||
<th scope="col" i18n>Following</th>
|
<th i18n>Following</th>
|
||||||
<th scope="col" style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
<th scope="col" style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
<th scope="col" style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
|
<th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
||||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||||
import { ActorFollow } from '@peertube/peertube-models'
|
import { ActorFollow } from '@shared/models'
|
||||||
import { FollowModalComponent } from './follow-modal.component'
|
import { FollowModalComponent } from './follow-modal.component'
|
||||||
import { DropdownAction } from '@app/shared/shared-main'
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
import { formatICU } from '@app/helpers'
|
import { prepareIcu } from '@app/helpers'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './following-list.component.html',
|
templateUrl: './following-list.component.html',
|
||||||
|
@ -64,9 +64,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
|
||||||
async removeFollowing (follows: ActorFollow[]) {
|
async removeFollowing (follows: ActorFollow[]) {
|
||||||
const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) }
|
const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) }
|
||||||
|
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)(
|
||||||
$localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`,
|
icuParams,
|
||||||
icuParams
|
$localize`Do you really want to unfollow these entries?`
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await this.confirmService.confirm(message, $localize`Unfollow`)
|
const res = await this.confirmService.confirm(message, $localize`Unfollow`)
|
||||||
|
@ -76,9 +76,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const message = formatICU(
|
const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)(
|
||||||
$localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`,
|
icuParams,
|
||||||
icuParams
|
$localize`You are not following them anymore.`
|
||||||
)
|
)
|
||||||
|
|
||||||
this.notifier.success(message)
|
this.notifier.success(message)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Routes } from '@angular/router'
|
import { Routes } from '@angular/router'
|
||||||
import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
|
import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
|
||||||
import { UserRightGuard } from '@app/core'
|
import { UserRightGuard } from '@app/core'
|
||||||
import { UserRight } from '@peertube/peertube-models'
|
import { UserRight } from '@shared/models'
|
||||||
import { FollowersListComponent } from './followers-list'
|
import { FollowersListComponent } from './followers-list'
|
||||||
import { FollowingListComponent } from './following-list/following-list.component'
|
import { FollowingListComponent } from './following-list/following-list.component'
|
||||||
|
|
||||||
|
|
|
@ -24,20 +24,18 @@
|
||||||
>
|
>
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" style="width: 40px;">
|
<th style="width: 40px;"></th>
|
||||||
<span i18n class="visually-hidden">More information</span>
|
<th style="width: 150px;" i18n>Action</th>
|
||||||
</th>
|
<th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
|
||||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
<th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
|
||||||
<th scope="col" style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
|
<th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
|
||||||
<th scope="col" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
|
|
||||||
<th scope="col" style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-redundancy>
|
<ng-template pTemplate="body" let-expanded="expanded" let-redundancy>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="expand-cell">
|
<td class="expand-cell" [pRowToggler]="redundancy">
|
||||||
<my-table-expander-icon [pRowToggler]="redundancy" [expanded]="expanded" i18n-tooltip tooltip="List redundancies"></my-table-expander-icon>
|
<my-table-expander-icon i18n-ngbTooltip ngbTooltip="List redundancies" [expanded]="expanded"></my-table-expander-icon>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user