Compare commits

..

No commits in common. "develop" and "release/5.2.0" have entirely different histories.

2746 changed files with 150025 additions and 174164 deletions

View File

@ -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"
]
}
}

View File

@ -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.

View File

@ -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

View File

@ -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: |

View File

@ -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

View File

@ -1,6 +1,4 @@
name: "PeerTube CodeQL config"
paths-ignore:
- packages/tests
- packages/server-commands
- packages/types-generator
- server/tests

View File

@ -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]"

View File

@ -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

View File

@ -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'

View File

@ -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
View File

@ -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/

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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>).

View File

@ -1,4 +0,0 @@
src
meta.json
tsconfig.json
scripts

View File

@ -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)
```

View File

@ -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": {}
}

View File

@ -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)

View File

@ -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()

View File

@ -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)
}

View File

@ -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
}

View File

@ -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.')
}

View File

@ -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!')
}

View File

@ -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
}

View File

@ -1 +0,0 @@
export * from './cli.js'

View File

@ -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" }
]
}

View File

@ -1,4 +0,0 @@
src
meta.json
tsconfig.json
scripts

View File

@ -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)
```

View File

@ -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)

View File

@ -1 +0,0 @@
export * from './register.js'

View File

@ -1 +0,0 @@
export * from './server.js'

View File

@ -1,2 +0,0 @@
export * from './shared/index.js'
export * from './process.js'

View File

@ -1,3 +0,0 @@
export * from './common.js'
export * from './process-vod.js'
export * from './transcoding-logger.js'

View File

@ -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)
}
}

View File

@ -1 +0,0 @@
export * from './supported-job.js'

View File

@ -1,3 +0,0 @@
export * from './config-manager.js'
export * from './http.js'
export * from './logger.js'

View File

@ -1,2 +0,0 @@
export * from './ipc-client.js'
export * from './ipc-server.js'

View File

@ -1,2 +0,0 @@
export * from './ipc-request.model.js'
export * from './ipc-response.model.js'

View File

@ -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" }
]
}

View File

@ -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
View File

@ -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

View File

@ -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,

View File

@ -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()
})
}

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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')
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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');`)

View File

@ -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'))
})
})

View File

@ -35,7 +35,7 @@ function checkEndMessage (options: {
}
{
const checkEmail = 'Check your email'
const checkEmail = 'Check your emails'
if (requiresEmailVerification) {
expect(message).toContain(checkEmail)

View File

@ -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'))
})
})

View File

@ -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

View File

@ -93,14 +93,5 @@ function buildConfig (suiteFile: string = undefined) {
}
}
if (filename === 'video-password.e2e-spec.ts') {
return {
signup: {
enabled: true,
limit: -1
}
}
}
return {}
}

View File

@ -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))

View File

@ -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',

View File

@ -6,10 +6,6 @@
"esModuleInterop": true,
"module": "commonjs",
"target": "es5",
"typeRoots": [
"../node_modules/@types",
"../node_modules"
],
"types": [
"node",
"@wdio/globals/types",

View File

@ -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')
}
],

View File

@ -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,

View File

@ -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,

View File

@ -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/*' ]
}
}
},

View File

@ -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": {}

View File

@ -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',

View File

@ -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'

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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',

View File

@ -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)

View File

@ -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',

View File

@ -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">

View File

@ -28,8 +28,14 @@
}
}
my-copy-button {
.copy-button {
@include margin-left(3px);
border: 0;
my-global-icon {
width: 15px;
}
}
.account-info {

View File

@ -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 }

View File

@ -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',

View File

@ -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 = [
{

View File

@ -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>

View File

@ -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']
}
}

View File

@ -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>

View File

@ -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({

View File

@ -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`
)
}
}
}

View File

@ -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>

View File

@ -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: {

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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'

View File

@ -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>

View File

@ -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()
}

View File

@ -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'

View File

@ -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>

View File

@ -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)

View File

@ -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">

View File

@ -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!`
)
)

View File

@ -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>

View File

@ -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)

View File

@ -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'

View File

@ -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