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",
|
||||
"root": true,
|
||||
"rules": {
|
||||
"eol-last": [
|
||||
"error",
|
||||
|
@ -127,20 +126,18 @@
|
|||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"packages/tests/fixtures",
|
||||
"apps/**/dist",
|
||||
"packages/**/dist",
|
||||
"server/dist",
|
||||
"packages/types-generator/tests",
|
||||
"*.js",
|
||||
"/client",
|
||||
"/dist"
|
||||
"node_modules/",
|
||||
"server/tests/fixtures"
|
||||
],
|
||||
"parserOptions": {
|
||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true,
|
||||
"project": [
|
||||
"./tsconfig.eslint.json"
|
||||
],
|
||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true
|
||||
"./tsconfig.json",
|
||||
"./shared/tsconfig.json",
|
||||
"./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,
|
||||
demonstrations.
|
||||
|
||||
### User documentation
|
||||
|
||||
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
|
||||
```
|
||||
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.
|
||||
|
||||
Some hints:
|
||||
* Routes are defined in [/server/core/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/core/controllers) directory
|
||||
* Parameters validators are defined in [/server/core/middlewares/validators](https://github.com/Chocobozzz/PeerTube/tree/develop/server/core/middlewares/validators) directory
|
||||
* Models sent/received by the controllers are defined in [/packages/models](https://github.com/Chocobozzz/PeerTube/tree/develop/packages/models) directory
|
||||
* Routes are defined in [/server/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/controllers) 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 [/shared/models](https://github.com/Chocobozzz/PeerTube/tree/develop/shared/models) directory
|
||||
|
||||
|
||||
## Improve the website
|
||||
|
@ -254,6 +242,15 @@ To test emails with PeerTube:
|
|||
* 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`
|
||||
|
||||
### 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
|
||||
|
||||
PeerTube can be configured using environment variables.
|
||||
|
|
|
@ -32,12 +32,4 @@ runs:
|
|||
|
||||
- name: Install peertube runner dependencies
|
||||
shell: bash
|
||||
run: cd apps/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
|
||||
run: cd packages/peertube-runner && yarn install --frozen-lockfile
|
||||
|
|
5
.github/workflows/benchmark.yml
vendored
5
.github/workflows/benchmark.yml
vendored
|
@ -35,14 +35,13 @@ jobs:
|
|||
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '16.x'
|
||||
|
||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
startClient=`date +%s`
|
||||
npm run build:server
|
||||
npm run build:client
|
||||
endClient=`date +%s`
|
||||
clientBuildTime=$((endClient-startClient))
|
||||
|
@ -72,7 +71,7 @@ jobs:
|
|||
|
||||
- name: Run benchmark
|
||||
run: |
|
||||
npm run benchmark-server -- -o benchmark.json
|
||||
node dist/scripts/benchmark.js -o benchmark.json
|
||||
|
||||
- name: Display result
|
||||
run: |
|
||||
|
|
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -65,4 +65,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- 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"
|
||||
|
||||
paths-ignore:
|
||||
- packages/tests
|
||||
- packages/server-commands
|
||||
- packages/types-generator
|
||||
- server/tests
|
||||
|
|
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
|
||||
git fetch --force --tags
|
||||
|
||||
one="{ \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bookworm\" }"
|
||||
two="{ \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-bookworm,chocobozzz/peertube:$(git describe --abbrev=0)-bookworm\" }"
|
||||
one="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bullseye\" }"
|
||||
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\" }"
|
||||
|
||||
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'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Build
|
||||
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'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Angular bundlewatch
|
||||
uses: jackyef/bundlewatch-gh-action@master
|
||||
|
@ -36,12 +36,12 @@ jobs:
|
|||
run: |
|
||||
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"
|
||||
./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
|
||||
if: github.event_name != 'pull_request'
|
||||
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
|
||||
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
|
||||
NODE_PENDING_JOB_WAIT: 250
|
||||
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_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }}
|
||||
YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -56,7 +55,7 @@ jobs:
|
|||
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '16.x'
|
||||
|
||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||
|
||||
|
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,8 +1,8 @@
|
|||
# NPM instalation
|
||||
node_modules
|
||||
/node_modules/
|
||||
/server/tools/node_modules
|
||||
*npm-debug.log
|
||||
yarn-error.log
|
||||
.yarn
|
||||
|
||||
# Testing
|
||||
/test1/
|
||||
|
@ -11,8 +11,8 @@ yarn-error.log
|
|||
/test4/
|
||||
/test5/
|
||||
/test6/
|
||||
/packages/tests/fixtures/video_high_bitrate_1080p.mp4
|
||||
/packages/tests/fixtures/video_59fps.mp4
|
||||
/server/tests/fixtures/video_high_bitrate_1080p.mp4
|
||||
/server/tests/fixtures/video_59fps.mp4
|
||||
|
||||
# Production
|
||||
/storage
|
||||
|
@ -23,7 +23,6 @@ yarn-error.log
|
|||
/ffmpeg-4/
|
||||
/thumbnails/
|
||||
/torrents/
|
||||
/web-videos/
|
||||
/videos/
|
||||
/previews/
|
||||
/logs/
|
||||
|
@ -49,14 +48,12 @@ yarn-error.log
|
|||
/*.tar.xz
|
||||
/*.asc
|
||||
*.DS_Store
|
||||
/server/tools/import-mediacore.ts
|
||||
/docker-volume/
|
||||
/init.mp4
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# EsLint
|
||||
.eslintcache
|
||||
|
||||
# Compiled output
|
||||
dist
|
||||
# Packages
|
||||
/packages/types/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
|
||||
|
||||
## 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
|
||||
|
||||
### 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** 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
|
||||
|
||||
### Maintenance
|
||||
|
|
29
CREDITS.md
29
CREDITS.md
|
@ -11,30 +11,30 @@
|
|||
* Filip Bengtsson
|
||||
* Ihor Hordiichuk
|
||||
* Jeff Huang
|
||||
* Payman Moghadam
|
||||
* Simon Brosdetzko
|
||||
* kontrollanten
|
||||
* Jiri Podhorecky
|
||||
* Payman Moghadam
|
||||
* Phongpanot
|
||||
* hecko
|
||||
* Milo Ivir
|
||||
* Laurent Ettouati
|
||||
* Milo Ivir
|
||||
* kimsible
|
||||
* Zet
|
||||
* GunChleoc
|
||||
* Clemens Schielicke
|
||||
* Racida S
|
||||
* Sveinn í Felli
|
||||
* Ewout van Mansom
|
||||
* Marcin Mikołajczak
|
||||
* Ewout van Mansom
|
||||
* Eivind Ødegård
|
||||
* Sveinn í Felli
|
||||
* Tirifto
|
||||
* Kim
|
||||
* Wicklow
|
||||
* Armin
|
||||
* Hannes Ylä-Jääski
|
||||
* Vodoyo Kamal
|
||||
* Mohamad Reza
|
||||
* Vodoyo Kamal
|
||||
* Wicklow
|
||||
* John Livingston
|
||||
* Kimsible
|
||||
* Besnik Bleta
|
||||
|
@ -71,7 +71,6 @@
|
|||
* jan Seli
|
||||
* lutangar
|
||||
* 李奕寯
|
||||
* Blood Axe
|
||||
* Martin Hoefler
|
||||
* Porrumentzio
|
||||
* Poslovitch
|
||||
|
@ -95,10 +94,12 @@
|
|||
* Ms Kimsible
|
||||
* Thomas Citharel
|
||||
* Benjamin Bouvier
|
||||
* Blood Axe
|
||||
* Joe Bill
|
||||
* Kemal Oktay Aktoğan
|
||||
* Lucas Declercq
|
||||
* Sirxy
|
||||
* chris@famichiki.tube
|
||||
* matograine
|
||||
* Alexander Ivanov
|
||||
* Daniel Santos
|
||||
|
@ -150,8 +151,8 @@
|
|||
* Benjamin Seitz
|
||||
* Bob Oob
|
||||
* Booteille
|
||||
* Chris Sakura 佐倉くりす on Youtube
|
||||
* DontUseGithub
|
||||
* Farooq Karimi Zadeh
|
||||
* I_Automne
|
||||
* Iñigo
|
||||
* Joan Montané
|
||||
|
@ -195,6 +196,7 @@
|
|||
* Eder Etxebarria
|
||||
* Ehsan Gholami
|
||||
* Elga Ahmad Prayoga
|
||||
* Farooq Karimi Zadeh
|
||||
* Girish Ramakrishnan
|
||||
* Hakim Oubouali
|
||||
* Hans Meiser
|
||||
|
@ -204,7 +206,6 @@
|
|||
* Jocelyn Jaubert
|
||||
* Johan Fleury
|
||||
* Jurij Podgoršek
|
||||
* Kindred La Boneta
|
||||
* Kiro
|
||||
* Leopere
|
||||
* Linus
|
||||
|
@ -234,7 +235,6 @@
|
|||
* Ömer Faruk Çakmak
|
||||
* AQR_Rastiq
|
||||
* Al-Hassan Abdel-Raouf
|
||||
* Alecks Gates
|
||||
* Amos Tamam
|
||||
* Andrew Morgan
|
||||
* Andy Khit
|
||||
|
@ -246,12 +246,10 @@
|
|||
* Average Dude
|
||||
* BitTube
|
||||
* Boo Teille
|
||||
* Branislav Pavelka
|
||||
* Dashie
|
||||
* David Luís Pereira Pires
|
||||
* David Marzal
|
||||
* EndoGai
|
||||
* Ettore Atalan
|
||||
* Fatih Özsoy
|
||||
* FediverseTV
|
||||
* Florent Fayolle
|
||||
|
@ -267,7 +265,6 @@
|
|||
* HybridGlucose
|
||||
* J C Worm
|
||||
* Jan Marsalek
|
||||
* José M
|
||||
* Joël Galeran
|
||||
* Julien Lemaire
|
||||
* Lucas Teixeira
|
||||
|
@ -302,7 +299,6 @@
|
|||
* libertas
|
||||
* merty
|
||||
* plr20
|
||||
* q_h
|
||||
* qwerty
|
||||
* spf
|
||||
* taziden
|
||||
|
@ -320,6 +316,7 @@
|
|||
* Agron
|
||||
* Aitozl
|
||||
* Alberto Mardegan
|
||||
* Alecks Gates
|
||||
* Alejandro Criado-Pérez
|
||||
* Aleksandr Sokolov
|
||||
* Alexander F. Rødseth
|
||||
|
@ -345,6 +342,7 @@
|
|||
* Cadence Ember
|
||||
* Cale
|
||||
* Charles de Lacombe
|
||||
* Chris Sakura 佐倉くりす on Youtube - 日本語は第二言語やけ、間違っとったら思いっきり叩いてくださいw つたない日本語ばっかりやけど頑張りまーす♪
|
||||
* Christoph Geschwind
|
||||
* Chronos
|
||||
* Claude
|
||||
|
@ -384,7 +382,6 @@
|
|||
* Iván Cabaleiro
|
||||
* J Webb
|
||||
* Jacen
|
||||
* Jackson Chen
|
||||
* Jacob
|
||||
* Jacques Foucry
|
||||
* Jagannath Bhat
|
||||
|
@ -494,7 +491,6 @@
|
|||
* Vagelis F
|
||||
* Varik Valefor
|
||||
* Vegard Fjeldberg
|
||||
* Victor Hampel
|
||||
* Vik
|
||||
* Vincent Stakenburg
|
||||
* WhiredPlanck
|
||||
|
@ -544,6 +540,7 @@
|
|||
* philippe lhardy
|
||||
* pitchum
|
||||
* potedeo
|
||||
* q_h
|
||||
* rdxuan
|
||||
* retiolus
|
||||
* 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>
|
||||
<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 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>).
|
||||
|
|
|
@ -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": [
|
||||
"projects/**/*",
|
||||
"node_modules/",
|
||||
"src/standalone/embed-player-api/dist"
|
||||
"src/standalone/player/dist"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
|
@ -14,7 +14,6 @@
|
|||
"project": [
|
||||
"tsconfig.eslint.json"
|
||||
],
|
||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true,
|
||||
"createDefaultProgram": false
|
||||
},
|
||||
"extends": [
|
||||
|
|
4
client/.gitignore
vendored
4
client/.gitignore
vendored
|
@ -12,5 +12,5 @@
|
|||
/e2e/local.log
|
||||
/e2e/browserstack.err
|
||||
/e2e/screenshots
|
||||
/src/standalone/embed-player-api/build
|
||||
/src/standalone/embed-player-api/dist
|
||||
/src/standalone/player/build
|
||||
/src/standalone/player/dist
|
||||
|
|
|
@ -195,14 +195,11 @@
|
|||
"path-browserify",
|
||||
"deep-merge",
|
||||
"escape-string-regexp",
|
||||
"mousetrap",
|
||||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"deepmerge",
|
||||
"core-js/features/reflect",
|
||||
"@formatjs/intl-locale/polyfill",
|
||||
"@formatjs/intl-locale/should-polyfill",
|
||||
"@formatjs/intl-pluralrules/polyfill-force",
|
||||
"@formatjs/intl-pluralrules/should-polyfill"
|
||||
"core-js/features/reflect"
|
||||
],
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { browserSleep, go, isAndroid } from '../utils'
|
||||
import { go } from '../utils'
|
||||
|
||||
export class LoginPage {
|
||||
|
||||
|
@ -23,20 +23,12 @@ export class LoginPage {
|
|||
await $('input#username').setValue(username)
|
||||
await $('input#password').setValue(password)
|
||||
|
||||
await browserSleep(1000)
|
||||
await browser.pause(1000)
|
||||
|
||||
const submit = $('.login-form-and-externals > form input[type=submit]')
|
||||
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()
|
||||
}
|
||||
await $('form input[type=submit]').click()
|
||||
|
||||
if (this.isMobileDevice) {
|
||||
const menuToggle = $('.top-left-block button')
|
||||
const menuToggle = $('.top-left-block span[role=button]')
|
||||
|
||||
await $('h2=Our content selection').waitForDisplayed()
|
||||
|
||||
|
@ -87,7 +79,7 @@ export class LoginPage {
|
|||
await logout.click()
|
||||
|
||||
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 {
|
||||
|
||||
|
@ -117,26 +117,6 @@ export class MyAccountPage {
|
|||
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
|
||||
|
||||
private async getVideoElement (name: string) {
|
||||
|
|
|
@ -29,34 +29,29 @@ export class PlayerPage {
|
|||
}
|
||||
|
||||
async playAndPauseVideo (isAutoplay: boolean, waitUntilSec: number) {
|
||||
// Autoplay is disabled on mobile and Safari
|
||||
if (isIOS() || isSafari() || isMobileDevice() || isAutoplay === false) {
|
||||
await this.playVideo()
|
||||
}
|
||||
const videojsElem = () => $('div.video-js')
|
||||
|
||||
await $('div.video-js.vjs-has-started').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
|
||||
await videojsElem().waitForExist()
|
||||
|
||||
// Autoplay is disabled on iOS and Safari
|
||||
if (isIOS() || isSafari() || isMobileDevice()) {
|
||||
// We can't play the video if it is not muted
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -66,15 +61,4 @@ export class PlayerPage {
|
|||
await playButton().waitForClickable()
|
||||
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 $('#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) {
|
||||
await $('#search-video').setValue(search)
|
||||
await $('.search-button').click()
|
||||
await $('my-header .icon-search').click()
|
||||
|
||||
await browser.waitUntil(() => {
|
||||
return $('my-video-miniature').isDisplayed()
|
||||
|
|
|
@ -64,16 +64,6 @@ export class VideoUploadPage {
|
|||
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 () {
|
||||
return $('.submit-container my-button')
|
||||
}
|
||||
|
|
|
@ -9,12 +9,11 @@ export class VideoWatchPage {
|
|||
waitWatchVideoName (videoName: string) {
|
||||
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
|
||||
|
||||
return browser.waitUntil(async () => {
|
||||
return await $('.video-info .video-info-name').isExisting() &&
|
||||
(await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
|
||||
return (await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -44,25 +43,19 @@ export class VideoWatchPage {
|
|||
return $('my-privacy-concerns').isDisplayed()
|
||||
}
|
||||
|
||||
async goOnAssociatedEmbed (passwordProtected = false) {
|
||||
async goOnAssociatedEmbed () {
|
||||
let url = await browser.getUrl()
|
||||
url = url.replace('/w/', '/videos/embed/')
|
||||
url = url.replace(':3333', ':9001')
|
||||
|
||||
await go(url)
|
||||
|
||||
if (passwordProtected) await this.waitEmbedForVideoPasswordForm()
|
||||
else await this.waitEmbedForDisplayed()
|
||||
await this.waitEmbedForDisplayed()
|
||||
}
|
||||
|
||||
waitEmbedForDisplayed () {
|
||||
return $('.vjs-big-play-button').waitForDisplayed()
|
||||
}
|
||||
|
||||
waitEmbedForVideoPasswordForm () {
|
||||
return $('#video-password-input').waitForDisplayed()
|
||||
}
|
||||
|
||||
isEmbedWarningDisplayed () {
|
||||
return $('.peertube-dock-description').isDisplayed()
|
||||
}
|
||||
|
@ -145,78 +138,4 @@ export class VideoWatchPage {
|
|||
|
||||
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()
|
||||
})
|
||||
|
||||
it('Should play an internal web video', async () => {
|
||||
await go(FIXTURE_URLS.INTERNAL_WEB_VIDEO)
|
||||
it('Should play an internal webtorrent video', async () => {
|
||||
await go(FIXTURE_URLS.INTERNAL_WEBTORRENT_VIDEO)
|
||||
|
||||
await videoWatchPage.waitWatchVideoName(internalVideoName)
|
||||
await checkCorrectlyPlay(playerPage)
|
||||
|
@ -52,8 +52,8 @@ describe('Private videos all workflow', () => {
|
|||
await checkCorrectlyPlay(playerPage)
|
||||
})
|
||||
|
||||
it('Should play an internal Web Video in embed', async () => {
|
||||
await go(FIXTURE_URLS.INTERNAL_EMBED_WEB_VIDEO)
|
||||
it('Should play an internal WebTorrent video in embed', async () => {
|
||||
await go(FIXTURE_URLS.INTERNAL_EMBED_WEBTORRENT_VIDEO)
|
||||
|
||||
await videoWatchPage.waitEmbedForDisplayed()
|
||||
await checkCorrectlyPlay(playerPage)
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Videos all workflow', () => {
|
|||
let videoNameToExcept = videoName
|
||||
|
||||
if (isMobileDevice() || isSafari()) {
|
||||
await go(FIXTURE_URLS.WEB_VIDEO)
|
||||
await go(FIXTURE_URLS.WEBTORRENT_VIDEO)
|
||||
videoNameToExcept = 'E2E tests'
|
||||
} else {
|
||||
await videoListPage.clickOnVideo(videoName)
|
||||
|
@ -176,7 +176,7 @@ describe('Videos all workflow', () => {
|
|||
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
|
||||
|
||||
const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.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', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
|
@ -83,8 +83,4 @@ describe('Custom server defaults', () => {
|
|||
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) {
|
||||
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'
|
||||
}
|
||||
|
||||
function isAndroid () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android'
|
||||
}
|
||||
|
||||
function isSafari () {
|
||||
return browser.capabilities['browserName'] &&
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
|
@ -26,6 +20,7 @@ function isIOS () {
|
|||
async function go (url: string) {
|
||||
await browser.url(url)
|
||||
|
||||
// Hide notifications that could fail tests when hiding buttons
|
||||
await browser.execute(() => {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = 'p-toast { display: none }'
|
||||
|
@ -46,7 +41,6 @@ export {
|
|||
isMobileDevice,
|
||||
isSafari,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
waitServerUp,
|
||||
go,
|
||||
browserSleep
|
||||
|
|
|
@ -93,14 +93,5 @@ function buildConfig (suiteFile: string = undefined) {
|
|||
}
|
||||
}
|
||||
|
||||
if (filename === 'video-password.e2e-spec.ts') {
|
||||
return {
|
||||
signup: {
|
||||
enabled: true,
|
||||
limit: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ import { join, resolve } from 'path'
|
|||
|
||||
function runServer (appInstance: number, config: any = {}) {
|
||||
const env = Object.create(process.env)
|
||||
|
||||
env['NODE_OPTIONS'] = ''
|
||||
env['NODE_ENV'] = 'test'
|
||||
env['NODE_APP_INSTANCE'] = appInstance + ''
|
||||
|
||||
|
@ -45,10 +43,7 @@ function runServer (appInstance: number, config: any = {}) {
|
|||
|
||||
function runCommand (command: string) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
// Reset NODE_OPTIONS env set by webdriverio
|
||||
const env = { ...process.env, NODE_OPTIONS: '' }
|
||||
|
||||
const p = exec(command, { env, cwd: getRootCWD() })
|
||||
const p = exec(command, { cwd: getRootCWD() })
|
||||
|
||||
p.stderr.on('data', data => console.error(data.toString()))
|
||||
p.on('error', err => rej(err))
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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_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_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',
|
||||
|
||||
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_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a',
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"../node_modules/@types",
|
||||
"../node_modules"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
|
|
|
@ -17,32 +17,18 @@ function buildMainOptions (sessionName: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function buildBStackDesktopOptions (options: {
|
||||
sessionName: string
|
||||
resolution: string
|
||||
os?: string
|
||||
osVersion?: string
|
||||
}) {
|
||||
const { sessionName, resolution, os, osVersion } = options
|
||||
|
||||
function buildBStackDesktopOptions (sessionName: string, resolution: string, os?: string) {
|
||||
return {
|
||||
'bstack:options': {
|
||||
...buildMainOptions(sessionName),
|
||||
|
||||
os,
|
||||
osVersion,
|
||||
resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildBStackMobileOptions (options: {
|
||||
sessionName: string
|
||||
deviceName: string
|
||||
osVersion: string
|
||||
}) {
|
||||
const { sessionName, deviceName, osVersion } = options
|
||||
|
||||
function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string) {
|
||||
return {
|
||||
'bstack:options': {
|
||||
...buildMainOptions(sessionName),
|
||||
|
@ -67,45 +53,45 @@ module.exports = {
|
|||
{
|
||||
browserName: 'Chrome',
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Latest Chrome Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
||||
...buildBStackDesktopOptions('Latest Chrome Desktop', '1280x1024')
|
||||
},
|
||||
{
|
||||
browserName: 'Firefox',
|
||||
browserVersion: '78', // Very old ESR
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Firefox ESR Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
||||
...buildBStackDesktopOptions('Firefox ESR Desktop', '1280x1024', 'Windows')
|
||||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
browserVersion: '12.1',
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Safari Desktop', resolution: '1280x1024' })
|
||||
...buildBStackDesktopOptions('Safari Desktop', '1280x1024')
|
||||
},
|
||||
{
|
||||
browserName: 'Firefox',
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Firefox Latest', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
|
||||
...buildBStackDesktopOptions('Firefox Latest', '1280x1024')
|
||||
},
|
||||
{
|
||||
browserName: 'Edge',
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Edge Latest', resolution: '1280x1024' })
|
||||
...buildBStackDesktopOptions('Edge Latest', '1280x1024')
|
||||
},
|
||||
|
||||
{
|
||||
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',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 8 Plus', osVersion: '12.4' })
|
||||
...buildBStackMobileOptions('Safari iPhone', 'iPhone 8 Plus', '12.4')
|
||||
},
|
||||
{
|
||||
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',
|
||||
'acceptInsecureCerts': true,
|
||||
'goog:chromeOptions': {
|
||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||
args: [ '--disable-gpu', windowSizeArg ],
|
||||
prefs
|
||||
}
|
||||
},
|
||||
|
@ -43,7 +43,7 @@ module.exports = {
|
|||
}
|
||||
],
|
||||
|
||||
services: [ 'shared-store' ],
|
||||
services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
|
||||
|
||||
beforeSession: beforeLocalSession,
|
||||
beforeSuite: beforeLocalSuite,
|
||||
|
|
|
@ -22,7 +22,6 @@ module.exports = {
|
|||
{
|
||||
'browserName': 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
binary: '/usr/bin/google-chrome-stable',
|
||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||
prefs
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ module.exports = {
|
|||
}
|
||||
],
|
||||
|
||||
services: [ 'shared-store' ],
|
||||
services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
|
||||
|
||||
beforeSession: beforeLocalSession,
|
||||
beforeSuite: beforeLocalSuite,
|
||||
|
|
|
@ -59,7 +59,7 @@ export const config = {
|
|||
// 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
|
||||
// gets prepended directly.
|
||||
baseUrl: 'http://127.0.0.1:9001',
|
||||
baseUrl: 'http://localhost:9001',
|
||||
//
|
||||
// Default timeout for all waitFor* commands.
|
||||
waitforTimeout: 5000,
|
||||
|
@ -80,7 +80,7 @@ export const config = {
|
|||
framework: 'mocha',
|
||||
//
|
||||
// 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
|
||||
// specFileRetriesDelay: 0,
|
||||
|
@ -107,6 +107,14 @@ export const config = {
|
|||
|
||||
tsNodeOpts: {
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
},
|
||||
|
||||
tsConfigPathsOpts: {
|
||||
baseUrl: './',
|
||||
paths: {
|
||||
'@server/*': [ '../../server/*' ],
|
||||
'@shared/*': [ '../../shared/*' ]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "6.0.1",
|
||||
"version": "5.2.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"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'",
|
||||
"webpack": "webpack",
|
||||
"eslint": "eslint",
|
||||
|
@ -24,9 +24,6 @@
|
|||
"ngx-extractor": "ngx-extractor",
|
||||
"stylelint": "stylelint"
|
||||
},
|
||||
"workspaces": [
|
||||
"../packages/*"
|
||||
],
|
||||
"typings": "*.d.ts",
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.0.2",
|
||||
|
@ -50,18 +47,14 @@
|
|||
"@angular/service-worker": "^16.0.2",
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@formatjs/intl-locale": "^3.3.1",
|
||||
"@formatjs/intl-pluralrules": "^5.2.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||
"@ng-select/ng-select": "^11.2.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^14.0.1",
|
||||
"@ng-select/ng-select": "^10.0.3",
|
||||
"@ngx-loading-bar/core": "^6.0.0",
|
||||
"@ngx-loading-bar/http-client": "^6.0.0",
|
||||
"@ngx-loading-bar/router": "^6.0.0",
|
||||
"@peertube/maildev": "^1.2.0",
|
||||
"@peertube/p2p-media-loader-core": "^1.0.15",
|
||||
"@peertube/p2p-media-loader-hlsjs": "^1.0.15",
|
||||
"@peertube/peertube-core-utils": "*",
|
||||
"@peertube/peertube-models": "*",
|
||||
"@peertube/p2p-media-loader-core": "^1.0.14",
|
||||
"@peertube/p2p-media-loader-hlsjs": "^1.0.14",
|
||||
"@peertube/videojs-contextmenu": "^5.5.0",
|
||||
"@peertube/xliffmerge": "^2.0.3",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
|
@ -71,48 +64,58 @@
|
|||
"@types/jschannel": "^1.0.0",
|
||||
"@types/linkifyjs": "^2.1.2",
|
||||
"@types/lodash-es": "^4.17.0",
|
||||
"@types/markdown-it": "^13.0.2",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/sanitize-html": "2.9.2",
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@types/sha.js": "^2.4.0",
|
||||
"@types/video.js": "^7.3.40",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"@types/webtorrent": "^0.109.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@wdio/browserstack-service": "^8.10.5",
|
||||
"@wdio/cli": "^8.10.5",
|
||||
"@wdio/local-runner": "^8.10.5",
|
||||
"@wdio/mocha-framework": "^8.10.4",
|
||||
"@wdio/shared-store-service": "^8.10.5",
|
||||
"@wdio/spec-reporter": "^8.10.5",
|
||||
"angular2-hotkeys": "^13.1.0",
|
||||
"angularx-qrcode": "16.0.0",
|
||||
"babel-loader": "^9.1.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"cache-chunk-store": "^3.0.0",
|
||||
"chart.js": "^4.3.0",
|
||||
"chartjs-plugin-zoom": "~2.0.1",
|
||||
"chromedriver": "^113.0.0",
|
||||
"core-js": "^3.22.8",
|
||||
"css-loader": "^6.2.0",
|
||||
"debug": "^4.3.1",
|
||||
"dexie": "^3.2.2",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-jsdoc": "^46.8.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsdoc": "^44.2.4",
|
||||
"eslint-plugin-prefer-arrow": "latest",
|
||||
"expect-webdriverio": "^4.2.3",
|
||||
"focus-visible": "^5.0.2",
|
||||
"geckodriver": "^4.0.0",
|
||||
"hls.js": "~1.3",
|
||||
"html-loader": "^4.1.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"intl-messageformat": "^10.1.0",
|
||||
"jschannel": "^1.0.2",
|
||||
"linkify-html": "^4.0.2",
|
||||
"linkifyjs": "^4.0.2",
|
||||
"lodash-es": "^4.17.4",
|
||||
"markdown-it": "13.0.2",
|
||||
"markdown-it": "13.0.1",
|
||||
"mini-css-extract-plugin": "^2.2.0",
|
||||
"ngx-uploadx": "^6.1.0",
|
||||
"path-browserify": "^1.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"primeng": "^16.0.0-rc.2",
|
||||
"process": "^0.11.10",
|
||||
"purify-css": "^1.2.5",
|
||||
"querystring": "^0.2.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rxjs": "^7.3.0",
|
||||
"sanitize-html": "^2.1.2",
|
||||
|
@ -120,17 +123,23 @@
|
|||
"sass-loader": "^13.2.0",
|
||||
"sha.js": "^2.4.11",
|
||||
"socket.io-client": "^4.5.4",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.0.0",
|
||||
"stylelint": "^15.1.0",
|
||||
"stylelint-config-sass-guidelines": "^10.0.0",
|
||||
"tinykeys": "^2.1.0",
|
||||
"ts-loader": "^9.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "~5.1.0",
|
||||
"typescript": "~4.9.5",
|
||||
"url": "^0.11.0",
|
||||
"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-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webtorrent": "1.8.26",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api'
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||
import { Actor } from '@peertube/peertube-models'
|
||||
import { Actor } from '@shared/models/actors'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about-follows',
|
||||
|
|
|
@ -3,8 +3,8 @@ import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@ang
|
|||
import { ActivatedRoute } from '@angular/router'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { AboutHTML } from '@app/shared/shared-instance'
|
||||
import { HTMLServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||
import { copyToClipboard } from '@root-helpers/utils'
|
||||
import { HTMLServerConfig, ServerStats } from '@shared/models/server'
|
||||
import { ResolverData } from './about-instance.resolver'
|
||||
import { ContactAdminModalComponent } from './contact-admin-modal.component'
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'
|
|||
import { ServerService } from '@app/core'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||
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 = {
|
||||
serverStats: ServerStats
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
|
||||
|
||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</button>
|
||||
<my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
@ -15,9 +12,8 @@
|
|||
<input
|
||||
type="text" id="fromName" class="form-control"
|
||||
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 class="form-group">
|
||||
|
@ -25,9 +21,8 @@
|
|||
<input
|
||||
type="text" id="fromEmail" class="form-control"
|
||||
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 class="form-group">
|
||||
|
@ -36,14 +31,14 @@
|
|||
type="text" id="subject" class="form-control"
|
||||
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 class="form-group">
|
||||
<label i18n for="body">Your message</label>
|
||||
<textarea id="body" formControlName="body" class="form-control" [ngClass]="{ 'input-error': formErrors['body'] }">
|
||||
</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 *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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
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 = {
|
||||
subject?: string
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { ServerStats } from '@peertube/peertube-models'
|
||||
import { ServerStats } from '@shared/models/server'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-statistics',
|
||||
|
|
|
@ -2,10 +2,10 @@ import { from, Subject, Subscription } from 'rxjs'
|
|||
import { concatMap, map, switchMap, tap } from 'rxjs/operators'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/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 { 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({
|
||||
selector: 'my-account-video-channels',
|
||||
|
@ -98,7 +98,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
|||
videoChannel,
|
||||
videoPagination: this.videosPagination,
|
||||
sort: this.videosSort,
|
||||
nsfw: this.videoService.nsfwPolicyToParam(this.nsfwPolicy)
|
||||
nsfwPolicy: this.nsfwPolicy
|
||||
}
|
||||
|
||||
return this.videoService.getVideoChannelVideos(options)
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
|
|||
import { ComponentPaginationLight, DisableForReuseHook, ScreenService } from '@app/core'
|
||||
import { Account, AccountService, VideoService } from '@app/shared/shared-main'
|
||||
import { VideoFilters } from '@app/shared/shared-video-miniature'
|
||||
import { VideoSortField } from '@peertube/peertube-models'
|
||||
import { VideoSortField } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
|
|
@ -18,18 +18,18 @@
|
|||
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
||||
></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>
|
||||
</div>
|
||||
|
||||
<div class="actor-handle">
|
||||
<span>@{{ account.nameWithHost }}</span>
|
||||
|
||||
<my-copy-button
|
||||
[value]="account.nameWithHostForced" i18n-notification notification="Username copied"
|
||||
title="Copy account handle" i18n-title
|
||||
></my-copy-button>
|
||||
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
||||
class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
|
||||
>
|
||||
<my-global-icon iconName="copy"></my-global-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="actor-counters">
|
||||
|
|
|
@ -28,8 +28,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
my-copy-button {
|
||||
.copy-button {
|
||||
@include margin-left(3px);
|
||||
|
||||
border: 0;
|
||||
|
||||
my-global-icon {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-info {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
VideoService
|
||||
} from '@app/shared/shared-main'
|
||||
import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
|
||||
import { HttpStatusCode, User, UserRight } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, User, UserRight } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './accounts.component.html',
|
||||
|
@ -115,6 +115,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
this.redirectService.redirectToHomepage()
|
||||
}
|
||||
|
||||
activateCopiedMessage () {
|
||||
this.notifier.success($localize`Username copied`)
|
||||
}
|
||||
|
||||
searchChanged (search: string) {
|
||||
const queryParams = { search }
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
|||
import { AuthService, ScreenService, ServerService } from '@app/core'
|
||||
import { ListOverflowItem } from '@app/shared/shared-main'
|
||||
import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { UserRight } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './admin.component.html',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { UserRight } from '@shared/models'
|
||||
|
||||
export const ConfigRoutes: Routes = [
|
||||
{
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||
</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 class="form-group" formGroupName="captions">
|
||||
|
@ -36,7 +36,7 @@
|
|||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||
</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 class="form-group" formGroupName="torrents">
|
||||
|
@ -50,21 +50,7 @@
|
|||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.cache.torrents.size" class="form-error" role="alert">{{ 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 *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@ -88,18 +74,17 @@
|
|||
<my-help>
|
||||
<ng-template ptTemplate="customHtml">
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
||||
<pre>console.log('my instance is amazing');</pre>
|
||||
Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationJavascript" formControlName="javascript" class="form-control" dir="ltr"
|
||||
id="customizationJavascript" formControlName="javascript" class="form-control"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
|
||||
></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 class="form-group">
|
||||
|
@ -108,16 +93,16 @@
|
|||
<my-help>
|
||||
<ng-template ptTemplate="customHtml">
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
||||
Write CSS code directly. Example:<br /><br />
|
||||
<pre>
|
||||
#custom-css {{ '{' }}
|
||||
color: red;
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</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>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
</ng-container>
|
||||
|
@ -125,10 +110,10 @@
|
|||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationCSS" formControlName="css" class="form-control" dir="ltr"
|
||||
id="customizationCSS" formControlName="css" class="form-control"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
|
||||
></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>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
@ -10,7 +10,7 @@ export class EditAdvancedConfigurationComponent {
|
|||
@Input() form: FormGroup
|
||||
@Input() formErrors: any
|
||||
|
||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
||||
getCacheSize (type: 'captions' | 'previews' | 'torrents') {
|
||||
return this.form.value['cache'][type]['size']
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
[clearable]="false"
|
||||
></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 class="form-group" formGroupName="trending">
|
||||
|
@ -51,7 +51,7 @@
|
|||
</select>
|
||||
</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>
|
||||
</div>
|
||||
|
@ -126,7 +126,7 @@
|
|||
</select>
|
||||
</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 class="form-group">
|
||||
|
@ -137,7 +137,7 @@
|
|||
[formError]="formErrors['broadcastMessage.message']" markdownType="to-unsafe-html"
|
||||
></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>
|
||||
|
||||
</ng-container>
|
||||
|
@ -193,7 +193,7 @@
|
|||
<span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
||||
</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>
|
||||
</div>
|
||||
|
@ -209,7 +209,7 @@
|
|||
<span i18n>{form.value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
||||
</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>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
@ -230,7 +230,7 @@
|
|||
|
||||
<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 class="form-group">
|
||||
|
@ -244,7 +244,7 @@
|
|||
[clearable]="false"
|
||||
></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 class="form-group">
|
||||
<ng-container formGroupName="history">
|
||||
|
@ -282,7 +282,7 @@
|
|||
<span i18n>jobs in parallel</span>
|
||||
</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 class="form-group" formGroupName="http">
|
||||
|
@ -313,7 +313,7 @@
|
|||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
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">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
|
@ -345,18 +345,6 @@
|
|||
</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>
|
||||
|
||||
|
@ -377,7 +365,7 @@
|
|||
<span i18n>{form.value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
||||
</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>
|
||||
|
@ -439,7 +427,7 @@
|
|||
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 class="mt-3">
|
||||
|
@ -537,7 +525,7 @@
|
|||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
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>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
@ -565,7 +553,7 @@
|
|||
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 class="form-group" formGroupName="contactForm">
|
||||
|
@ -600,27 +588,22 @@
|
|||
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 class="form-group">
|
||||
<my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
|
||||
<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 ptTemplate="help">
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
If your instance is explicitly allowed by Twitter, 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 />
|
||||
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.
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</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 { FormGroup } from '@angular/forms'
|
||||
import { MenuService, ThemeService } from '@app/core'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig } from '@shared/models'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { prepareIcu } from '@app/helpers'
|
||||
|
||||
export type ResolutionOption = {
|
||||
id: string
|
||||
|
@ -99,7 +99,10 @@ export class EditConfigurationService {
|
|||
return {
|
||||
value,
|
||||
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-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:
|
||||
|
||||
<ul>
|
||||
|
@ -78,7 +78,7 @@
|
|||
</ul>
|
||||
</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.
|
||||
</span>
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import omit from 'lodash-es/omit'
|
||||
import { forkJoin } from 'rxjs'
|
||||
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 {
|
||||
ADMIN_EMAIL_VALIDATOR,
|
||||
CACHE_SIZE_VALIDATOR,
|
||||
CACHE_CAPTIONS_SIZE_VALIDATOR,
|
||||
CACHE_PREVIEWS_SIZE_VALIDATOR,
|
||||
CONCURRENCY_VALIDATOR,
|
||||
INDEX_URL_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 { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
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'
|
||||
|
||||
type ComponentCustomConfig = CustomConfig & {
|
||||
|
@ -118,16 +120,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
size: CACHE_PREVIEWS_SIZE_VALIDATOR
|
||||
},
|
||||
captions: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
size: CACHE_CAPTIONS_SIZE_VALIDATOR
|
||||
},
|
||||
torrents: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
storyboards: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
size: CACHE_CAPTIONS_SIZE_VALIDATOR
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
|
@ -189,7 +188,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
hls: {
|
||||
enabled: null
|
||||
},
|
||||
webVideos: {
|
||||
webtorrent: {
|
||||
enabled: null
|
||||
},
|
||||
remoteRunners: {
|
||||
|
@ -224,11 +223,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
enabled: null
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
|
|
|
@ -19,10 +19,9 @@
|
|||
name="instanceCustomHomepageContent" formControlName="content"
|
||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||
[formError]="formErrors['instanceCustomHomepage.content']"
|
||||
dir="ltr"
|
||||
></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>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
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 class="form-group">
|
||||
|
@ -27,7 +27,7 @@
|
|||
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
|
||||
></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 class="form-group">
|
||||
|
@ -91,7 +91,7 @@
|
|||
|
||||
<ng-template ptTemplate="help">
|
||||
<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.
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
@ -118,7 +118,7 @@
|
|||
</select>
|
||||
</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 class="form-group">
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
<span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
|
||||
</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 class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
|
@ -66,7 +66,7 @@
|
|||
<span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
|
||||
</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 class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
|
@ -77,7 +77,7 @@
|
|||
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
|
||||
></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>
|
||||
|
||||
</ng-container>
|
||||
|
@ -178,7 +178,7 @@
|
|||
formControlName="threads"
|
||||
[clearable]="false"
|
||||
></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 class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
||||
|
@ -193,7 +193,7 @@
|
|||
>
|
||||
</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>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig } from '@shared/models'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
||||
|
||||
|
|
|
@ -67,11 +67,11 @@
|
|||
<div class="callout callout-light pt-2 mt-2 pb-0">
|
||||
<h3 class="callout-title" i18n>Output formats</h3>
|
||||
|
||||
<ng-container formGroupName="webVideos">
|
||||
<ng-container formGroupName="webtorrent">
|
||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="transcodingWebVideosEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Web Videos enabled"
|
||||
inputName="transcodingWebTorrentEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="WebTorrent enabled"
|
||||
>
|
||||
<ng-template ptTemplate="help">
|
||||
<ng-container>
|
||||
|
@ -93,14 +93,14 @@
|
|||
<ng-container i18n>
|
||||
<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>
|
||||
<li>Resolution change is smoother</li>
|
||||
<li>Faster playback especially with long videos</li>
|
||||
<li>More stable playback (less bugs/infinite loading)</li>
|
||||
</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-template>
|
||||
</my-peertube-checkbox>
|
||||
|
@ -175,7 +175,7 @@
|
|||
[clearable]="false"
|
||||
></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 class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||
|
@ -187,7 +187,7 @@
|
|||
<span i18n>jobs in parallel</span>
|
||||
</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 class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||
|
@ -201,7 +201,7 @@
|
|||
[clearable]="false"
|
||||
></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>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig } from '@shared/models'
|
||||
import { ConfigService } from '../shared/config.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 videoStudioControl = this.form.get('videoStudio.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 => {
|
||||
if (newValue === false && !hlsControl.disabled) {
|
||||
hlsControl.disable()
|
||||
|
@ -104,12 +105,12 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
|||
|
||||
hlsControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false && !webVideosControl.disabled) {
|
||||
webVideosControl.disable()
|
||||
if (newValue === false && !webtorrentControl.disabled) {
|
||||
webtorrentControl.disable()
|
||||
}
|
||||
|
||||
if (newValue === true && !webVideosControl.enabled) {
|
||||
webVideosControl.enable()
|
||||
if (newValue === true && !webtorrentControl.enabled) {
|
||||
webtorrentControl.enable()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -121,7 +122,7 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
|||
})
|
||||
|
||||
transcodingControl.updateValueAndValidity()
|
||||
webVideosControl.updateValueAndValidity()
|
||||
webtorrentControl.updateValueAndValidity()
|
||||
videoStudioControl.updateValueAndValidity()
|
||||
hlsControl.updateValueAndValidity()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { catchError } from 'rxjs/operators'
|
|||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/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 { environment } from '../../../../environments/environment'
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
[value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||
[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"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
|
@ -28,14 +29,14 @@
|
|||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px">
|
||||
<th style="width: 40px">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Actions</th>
|
||||
<th scope="col" 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 scope="col" style="width: 100px;" i18n [ngbTooltip]="sortTooltip" container="body" 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>Actions</th>
|
||||
<th i18n>Follower</th>
|
||||
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
|
||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { Component, OnInit } from '@angular/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 { InstanceFollowService } from '@app/shared/shared-instance'
|
||||
import { DropdownAction } from '@app/shared/shared-main'
|
||||
import { ActorFollow } from '@peertube/peertube-models'
|
||||
import { ActorFollow } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-followers-list',
|
||||
|
@ -63,9 +63,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
|||
.subscribe({
|
||||
next: () => {
|
||||
// eslint-disable-next-line max-len
|
||||
const message = formatICU(
|
||||
$localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) }
|
||||
const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) },
|
||||
$localize`Follow requests accepted`
|
||||
)
|
||||
this.notifier.success(message)
|
||||
|
||||
|
@ -78,9 +78,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
|||
|
||||
async rejectFollower (follows: ActorFollow[]) {
|
||||
// eslint-disable-next-line max-len
|
||||
const message = formatICU(
|
||||
$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]) }
|
||||
const message = prepareIcu($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]) },
|
||||
$localize`Do you really want to reject these follow requests?`
|
||||
)
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Reject`)
|
||||
|
@ -90,9 +90,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
|||
.subscribe({
|
||||
next: () => {
|
||||
// eslint-disable-next-line max-len
|
||||
const message = formatICU(
|
||||
$localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) }
|
||||
const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||
{ count: follows.length, followerName: this.buildFollowerName(follows[0]) },
|
||||
$localize`Follow requests rejected`
|
||||
)
|
||||
this.notifier.success(message)
|
||||
|
||||
|
@ -110,9 +110,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
|||
message += '<br /><br />'
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
message += formatICU(
|
||||
$localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`,
|
||||
icuParams
|
||||
message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
|
||||
icuParams,
|
||||
$localize`Do you really want to delete these follow requests?`
|
||||
)
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Delete`)
|
||||
|
@ -122,9 +122,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
|
|||
.subscribe({
|
||||
next: () => {
|
||||
// eslint-disable-next-line max-len
|
||||
const message = formatICU(
|
||||
$localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
|
||||
icuParams
|
||||
const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
|
||||
icuParams,
|
||||
$localize`Follow requests removed`
|
||||
)
|
||||
|
||||
this.notifier.success(message)
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
<div class="modal-header">
|
||||
<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"></my-global-icon>
|
||||
</button>
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
@ -17,7 +15,7 @@
|
|||
class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
|
||||
></textarea>
|
||||
|
||||
<div *ngIf="formErrors.hostsOrHandles" class="form-error" role="alert">
|
||||
<div *ngIf="formErrors.hostsOrHandles" class="form-error">
|
||||
{{ formErrors.hostsOrHandles }}
|
||||
|
||||
<div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/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 { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance'
|
||||
|
@ -62,9 +62,9 @@ export class FollowModalComponent extends FormReactive implements OnInit {
|
|||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success(
|
||||
formatICU(
|
||||
$localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`,
|
||||
{ count: hostsOrHandles.length }
|
||||
prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)(
|
||||
{ count: hostsOrHandles.length },
|
||||
$localize`Follow request(s) sent!`
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||
[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"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
|
@ -33,14 +34,14 @@
|
|||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px">
|
||||
<th style="width: 40px">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
||||
<th scope="col" i18n>Following</th>
|
||||
<th scope="col" 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 scope="col" style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
|
||||
<th style="width: 150px;" i18n>Action</th>
|
||||
<th i18n>Following</th>
|
||||
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ import { Component, OnInit, ViewChild } from '@angular/core'
|
|||
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
||||
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 { DropdownAction } from '@app/shared/shared-main'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { prepareIcu } from '@app/helpers'
|
||||
|
||||
@Component({
|
||||
templateUrl: './following-list.component.html',
|
||||
|
@ -64,9 +64,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
|
|||
async removeFollowing (follows: ActorFollow[]) {
|
||||
const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) }
|
||||
|
||||
const message = formatICU(
|
||||
$localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`,
|
||||
icuParams
|
||||
const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)(
|
||||
icuParams,
|
||||
$localize`Do you really want to unfollow these entries?`
|
||||
)
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Unfollow`)
|
||||
|
@ -76,9 +76,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
|
|||
.subscribe({
|
||||
next: () => {
|
||||
// eslint-disable-next-line max-len
|
||||
const message = formatICU(
|
||||
$localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`,
|
||||
icuParams
|
||||
const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)(
|
||||
icuParams,
|
||||
$localize`You are not following them anymore.`
|
||||
)
|
||||
|
||||
this.notifier.success(message)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { UserRight } from '@shared/models'
|
||||
import { FollowersListComponent } from './followers-list'
|
||||
import { FollowingListComponent } from './following-list/following-list.component'
|
||||
|
||||
|
|
|
@ -24,20 +24,18 @@
|
|||
>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">
|
||||
<span i18n class="visually-hidden">More information</span>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
||||
<th scope="col" style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</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>
|
||||
<th style="width: 40px;"></th>
|
||||
<th style="width: 150px;" i18n>Action</th>
|
||||
<th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
|
||||
<th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
|
||||
<th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-redundancy>
|
||||
<tr>
|
||||
<td class="expand-cell">
|
||||
<my-table-expander-icon [pRowToggler]="redundancy" [expanded]="expanded" i18n-tooltip tooltip="List redundancies"></my-table-expander-icon>
|
||||
<td class="expand-cell" [pRowToggler]="redundancy">
|
||||
<my-table-expander-icon i18n-ngbTooltip ngbTooltip="List redundancies" [expanded]="expanded"></my-table-expander-icon>
|
||||
</td>
|
||||
|
||||
<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